diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3ea673e..decbefefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features +* [#996](https://github.com/toptal/chewy/pull/996): Add `context:` option to `import`/`import!` for passing custom data to crutch blocks and field value procs without redundant DB queries. + ### Changes * [#977](https://github.com/toptal/chewy/pull/977): Fewer files on gem installation [@ericproulx](https://github.com/ericproulx). diff --git a/lib/chewy/fields/root.rb b/lib/chewy/fields/root.rb index e821cb9ca..3d3650424 100644 --- a/lib/chewy/fields/root.rb +++ b/lib/chewy/fields/root.rb @@ -61,8 +61,8 @@ def compose_id(object) # @param fields [Array] a list of fields to compose, every field will be composed if empty # @return [Hash] JSON-ready hash with stringified keys # - def compose(object, crutches = nil, fields: []) - result = evaluate([object, crutches]) + def compose(object, crutches = nil, fields: [], context: {}) + result = evaluate([object, crutches, context]) if children.present? child_fields = if fields.present? @@ -72,7 +72,7 @@ def compose(object, crutches = nil, fields: []) end child_fields.each_with_object({}) do |field, memo| - memo.merge!(field.compose(result, crutches) || {}) + memo.merge!(field.compose(result, crutches, context) || {}) end.as_json elsif fields.present? result.as_json(only: fields, root: false) diff --git a/lib/chewy/index/crutch.rb b/lib/chewy/index/crutch.rb index 4377187af..e11fa1c5d 100644 --- a/lib/chewy/index/crutch.rb +++ b/lib/chewy/index/crutch.rb @@ -9,9 +9,12 @@ module Crutch end class Crutches - def initialize(index, collection) + attr_reader :context + + def initialize(index, collection, context = {}) @index = index @collection = collection + @context = context @crutches_instances = {} end @@ -26,7 +29,14 @@ def respond_to_missing?(name, include_private = false) end def [](name) - @crutches_instances[name] ||= @index._crutches[:"#{name}"].call(@collection) + @crutches_instances[name] ||= begin + block = @index._crutches[:"#{name}"] + if block.arity > 1 || block.arity < -1 + block.call(@collection, @context) + else + block.call(@collection) + end + end end end diff --git a/lib/chewy/index/import.rb b/lib/chewy/index/import.rb index 8de6a877a..db502accd 100644 --- a/lib/chewy/index/import.rb +++ b/lib/chewy/index/import.rb @@ -115,13 +115,13 @@ def bulk(**options) # @param crutches [Object] optional crutches object; if omitted - a crutch for the single passed object is created as a fallback # @param fields [Array] and array of fields to restrict the generated document # @return [Hash] a JSON-ready hash - def compose(object, crutches = nil, fields: []) - crutches ||= Chewy::Index::Crutch::Crutches.new self, [object] + def compose(object, crutches = nil, fields: [], context: {}) + crutches ||= Chewy::Index::Crutch::Crutches.new self, [object], context if witchcraft? && root.children.present? - cauldron(fields: fields).brew(object, crutches) + cauldron(fields: fields).brew(object, crutches, context) else - root.compose(object, crutches, fields: fields) + root.compose(object, crutches, fields: fields, context: context) end end diff --git a/lib/chewy/index/import/bulk_builder.rb b/lib/chewy/index/import/bulk_builder.rb index 52e3fdcfd..a328866f0 100644 --- a/lib/chewy/index/import/bulk_builder.rb +++ b/lib/chewy/index/import/bulk_builder.rb @@ -13,11 +13,12 @@ class BulkBuilder # @param to_index [Array] objects to index # @param delete [Array] objects or ids to delete # @param fields [Array] and array of fields for documents update - def initialize(index, to_index: [], delete: [], fields: []) + def initialize(index, to_index: [], delete: [], fields: [], context: {}) @index = index @to_index = to_index @delete = delete @fields = fields.map!(&:to_sym) + @context = context end # Returns ES API-ready bulk requiest body. @@ -42,7 +43,7 @@ def index_objects_by_id private def crutches_for_index - @crutches_for_index ||= Chewy::Index::Crutch::Crutches.new @index, @to_index + @crutches_for_index ||= Chewy::Index::Crutch::Crutches.new @index, @to_index, @context end def index_entry(object) @@ -257,7 +258,7 @@ def join_field? end def data_for(object, fields: [], crutches: crutches_for_index) - @index.compose(object, crutches, fields: fields) + @index.compose(object, crutches, fields: fields, context: @context) end def parent_changed?(data, old_parent) diff --git a/lib/chewy/index/import/routine.rb b/lib/chewy/index/import/routine.rb index 61004955a..774dbc468 100644 --- a/lib/chewy/index/import/routine.rb +++ b/lib/chewy/index/import/routine.rb @@ -56,6 +56,7 @@ def initialize(index, **options) {} end end + @context = @options.delete(:context) || {} @errors = [] @stats = {} @leftovers = [] @@ -78,7 +79,7 @@ def create_indexes! # @param delete [Array] any acceptable objects for deleting # @return [true, false] the result of the request, true if no errors def process(index: [], delete: []) - bulk_builder = BulkBuilder.new(@index, to_index: index, delete: delete, fields: @options[:update_fields]) + bulk_builder = BulkBuilder.new(@index, to_index: index, delete: delete, fields: @options[:update_fields], context: @context) bulk_body = bulk_builder.bulk_body if @options[:journal] diff --git a/lib/chewy/index/witchcraft.rb b/lib/chewy/index/witchcraft.rb index a6b8dc0a7..528f8c225 100644 --- a/lib/chewy/index/witchcraft.rb +++ b/lib/chewy/index/witchcraft.rb @@ -51,15 +51,15 @@ def initialize(index, fields: []) @fields = fields end - def brew(object, crutches = nil) - alicorn.call(locals, object, crutches).as_json + def brew(object, crutches = nil, context = {}) + alicorn.call(locals, object, crutches, context).as_json end private def alicorn @alicorn ||= singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - -> (locals, object0, crutches) do + -> (locals, object0, crutches, context) do #{composed_values(@index.root, 0)} end RUBY @@ -182,6 +182,7 @@ def source_for(proc, nesting) source = replace_lvar(source, proc_params[n], :"object#{n}") if proc_params[n] end source = replace_lvar(source, proc_params[nesting + 1], :crutches) if proc_params[nesting + 1] + source = replace_lvar(source, proc_params[nesting + 2], :context) if proc_params[nesting + 2] binding_variable_list(source).each do |variable| locals.push(proc.binding.eval(variable.to_s)) diff --git a/spec/chewy/index/import/bulk_builder_spec.rb b/spec/chewy/index/import/bulk_builder_spec.rb index 5fe598bed..993a9971f 100644 --- a/spec/chewy/index/import/bulk_builder_spec.rb +++ b/spec/chewy/index/import/bulk_builder_spec.rb @@ -157,6 +157,90 @@ def derived end end + context 'context' do + before do + stub_index(:cities) do + crutch :names do |collection, context| + context[:names] || collection.to_h { |item| [item.id, "Name#{item.id}"] } + end + + field :name, value: ->(o, c) { c.names[o.id] } + end + end + + let(:to_index) { [double(id: 42)] } + + context 'without context' do + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => 'Name42'}}} + ]) + end + end + + context 'with context' do + subject { described_class.new(index, to_index: to_index, delete: delete, fields: fields, context: {names: {42 => 'ContextName42'}}) } + + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => 'ContextName42'}}} + ]) + end + end + + context 'with context passed to field value' do + before do + stub_index(:cities) do + crutch :names do |collection| + collection.to_h { |item| [item.id, "Name#{item.id}"] } + end + + field :name, value: ->(o, crutches, context) { context[:prefix].to_s + crutches.names[o.id] } + end + end + + context 'without context' do + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => 'Name42'}}} + ]) + end + end + + context 'with context' do + subject { described_class.new(index, to_index: to_index, delete: delete, fields: fields, context: {prefix: '[ctx] '}) } + + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => '[ctx] Name42'}}} + ]) + end + end + end + + context 'witchcraft' do + before { CitiesIndex.witchcraft! } + + context 'without context' do + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => 'Name42'}}} + ]) + end + end + + context 'with context' do + subject { described_class.new(index, to_index: to_index, delete: delete, fields: fields, context: {names: {42 => 'ContextName42'}}) } + + specify do + expect(subject.bulk_body).to eq([ + {index: {_id: 42, data: {'name' => 'ContextName42'}}} + ]) + end + end + end + end + context 'empty ids' do before do stub_index(:cities) do diff --git a/spec/chewy/index/witchcraft_spec.rb b/spec/chewy/index/witchcraft_spec.rb index 0d4a28c4d..d25f4ce78 100644 --- a/spec/chewy/index/witchcraft_spec.rb +++ b/spec/chewy/index/witchcraft_spec.rb @@ -84,6 +84,19 @@ def self.mapping(&block) end end + context 'context' do + mapping do + field :name, value: ->(_o, _c, ctx) { ctx[:name] } + end + + context do + let(:object) { double(name: 'Name') } + let(:crutches) { double } + let(:context) { {name: 'FromContext'} } + specify { expect(index.cauldron.brew(object, crutches, context)).to eq({name: 'FromContext'}.as_json) } + end + end + context 'nesting' do context do mapping do