diff --git a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb index 524ff4b9..47b6108b 100644 --- a/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb +++ b/packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator.rb @@ -26,6 +26,8 @@ def self.lookup_projection(current_path, schema_stack, projection, options) fields.merge!(add_fields(name, relation_projection, options)) end + pipeline.push({ '$addFields' => fields }) unless fields.empty? + pipeline end @@ -36,7 +38,7 @@ def self.add_fields(name, projection, options) projection.filter { |field| field.include?('@@@') } .map { |field_name| "#{name}.#{field_name.tr(":", ".")}" } .each_with_object({}) do |curr, acc| - acc[curr] = "$#{curr.tr("@@@", ".")}" + acc[curr] = "$#{curr.gsub("@@@", ".")}" end end @@ -67,8 +69,10 @@ def self.lookup_relation(current_path, schema_stack, name, projection, options) return [ # Push lookup to pipeline - { '$lookup' => - { 'from' => from, 'localField' => local_field, 'foreignField' => foreign_field, 'as' => as } }, + { + '$lookup' => { 'from' => from, 'localField' => local_field, 'foreignField' => foreign_field, + 'as' => as } + }, { '$unwind' => { 'path' => "$#{as}", 'preserveNullAndEmptyArrays' => true } }, # Recurse to get relations of relations diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/address.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/address.rb index f8976409..4ce21a83 100644 --- a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/address.rb +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/address.rb @@ -4,5 +4,6 @@ class Address field :city, type: String field :zip_code, type: String + embeds_one :meta, class_name: 'Meta' embedded_in :user end diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/author.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/author.rb index ce60b3be..53b468fd 100644 --- a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/author.rb +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/author.rb @@ -5,4 +5,5 @@ class Author field :last_name, type: String belongs_to :post + belongs_to :user end diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/co_author.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/co_author.rb new file mode 100644 index 00000000..5f1b1118 --- /dev/null +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/co_author.rb @@ -0,0 +1,8 @@ +class CoAuthor + include Mongoid::Document + include Mongoid::Timestamps + field :first_name, type: String + field :last_name, type: String + + belongs_to :user +end diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/meta.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/meta.rb new file mode 100644 index 00000000..d27863e8 --- /dev/null +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/meta.rb @@ -0,0 +1,6 @@ +class Meta + include Mongoid::Document + field :length, type: String + + embedded_in :address +end diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/post.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/post.rb index 5e2ec981..161c16a9 100644 --- a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/post.rb +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/post.rb @@ -8,4 +8,7 @@ class Post has_many :comments, dependent: :destroy has_one :author has_and_belongs_to_many :tags + belongs_to :user + + embeds_one :co_author end diff --git a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/user.rb b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/user.rb index 0e206352..7ed17d57 100644 --- a/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/user.rb +++ b/packages/forest_admin_datasource_mongoid/spec/dummy/app/models/user.rb @@ -5,4 +5,5 @@ class User field :name, type: String belongs_to :item, polymorphic: true embeds_many :addresses + embeds_one :address end diff --git a/packages/forest_admin_datasource_mongoid/spec/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator_spec.rb b/packages/forest_admin_datasource_mongoid/spec/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator_spec.rb index ca98d553..da324b27 100644 --- a/packages/forest_admin_datasource_mongoid/spec/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator_spec.rb +++ b/packages/forest_admin_datasource_mongoid/spec/lib/forest_admin_datasource_mongoid/utils/pipeline/lookup_generator_spec.rb @@ -5,9 +5,9 @@ module Utils module Pipeline include ForestAdminDatasourceToolkit::Components::Query describe LookupGenerator do - let(:stack) { [{ prefix: nil, as_fields: [], as_models: [] }] } - describe 'with the root collection' do + let(:stack) { [{ prefix: nil, as_fields: [], as_models: [] }] } + it 'crashes when non-existent relations are asked for' do projection = Projection.new(['myAuthor:firstname']) expect do @@ -29,28 +29,313 @@ module Pipeline expect(pipeline).to eq([]) end - # TODO: when relations are totally implemented - # it 'should load the post (relation)' do - # projection = Projection.new(['post__many_to_one:title']) - # pipeline = LookupGenerator.lookup(Comment, stack, projection, {}) - # - # expect(pipeline).to eq([ - # { - # '$lookup' => { - # 'from' => 'posts', - # 'localField' => 'post_id', - # 'foreignField' => '_id', - # 'as' => 'post__manyToOne' - # } - # }, - # { - # '$unwind' => { - # 'path' => '$post__manyToOne', - # 'preserveNullAndEmptyArrays' => true - # } - # } - # ]) - # end + it 'loads the post (relation)' do + projection = Projection.new(['post_id__many_to_one:title']) + pipeline = described_class.lookup(Comment, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'from' => 'Post', + 'localField' => 'post_id', + 'foreignField' => '_id', + 'as' => 'post_id__many_to_one' + } + }, + { + '$unwind' => { + 'path' => '$post_id__many_to_one', + 'preserveNullAndEmptyArrays' => true + } + } + ] + ) + end + + it 'loads the user (relation) with nested fields' do + projection = Projection.new(['user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Post, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'user_id' + } + }, + { '$unwind' => { 'path' => '$user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@city' => '$user_id__many_to_one.address.city' + } + } + ] + ) + end + + describe 'include' do + it 'returns the nested field if the parent is included' do + projection = Projection.new(['user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Post, stack, projection, { include: ['user_id__many_to_one'] }) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'user_id' + } + }, + { '$unwind' => { 'path' => '$user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@city' => '$user_id__many_to_one.address.city' + } + } + ] + ) + end + + it 'does not add the nested field if the parent is not included' do + projection = Projection.new(['user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Post, stack, projection, { include: [] }) + + expect(pipeline).to eq([]) + end + end + + describe 'exclude' do + it 'does not add the nested field if the parent is excluded' do + projection = Projection.new(['user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Post, stack, projection, { exclude: ['user_id__many_to_one'] }) + + expect(pipeline).to eq([]) + end + + it 'does not add the nested field if the parent is not included' do + projection = Projection.new(['user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Post, stack, projection, { exclude: [] }) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'user_id' + } + }, + { '$unwind' => { 'path' => '$user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@city' => '$user_id__many_to_one.address.city' + } + } + ] + ) + end + end + + it 'loads the user (relation) with double nested fields' do + projection = Projection.new(['user_id__many_to_one:address@@@meta@@@length']) + pipeline = described_class.lookup(Post, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'user_id' + } + }, + { '$unwind' => { 'path' => '$user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@meta@@@length' => '$user_id__many_to_one.address.meta.length' + } + } + ] + ) + end + + it 'loads the post user city (double relation with nested field)' do + projection = Projection.new(['post_id__many_to_one:user_id__many_to_one:address@@@city']) + pipeline = described_class.lookup(Author, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'post_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'Post', + 'localField' => 'post_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$lookup' => { + 'as' => 'post_id__many_to_one.user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'post_id__many_to_one.user_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one.user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@city' => '$user_id__many_to_one.address.city' + } + }, + { + '$addFields' => { + 'post_id__many_to_one.user_id__many_to_one.address@@@city' => '$post_id__many_to_one.user_id__many_to_one.address.city' + } + } + ] + ) + end + + it 'loads the post user address meta length (double relation with double nested field)' do + projection = Projection.new(['post_id__many_to_one:user_id__many_to_one:address@@@meta@@@length']) + pipeline = described_class.lookup(Author, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'post_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'Post', + 'localField' => 'post_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$lookup' => { + 'as' => 'post_id__many_to_one.user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'post_id__many_to_one.user_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one.user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$addFields' => { + 'user_id__many_to_one.address@@@meta@@@length' => '$user_id__many_to_one.address.meta.length' + } + }, + { + '$addFields' => { + 'post_id__many_to_one.user_id__many_to_one.address@@@meta@@@length' => '$post_id__many_to_one.user_id__many_to_one.address.meta.length' + } + } + ] + ) + end + + it 'loads the post co_author user (relation within fake relation)' do + projection = Projection.new(['co_author:user_id__many_to_one:name']) + pipeline = described_class.lookup(Post, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'co_author.user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'co_author.user_id' + } + }, + { '$unwind' => { 'path' => '$co_author.user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } } + ] + ) + end + + it 'loads the comment post user (nested relation)' do + projection = Projection.new(['post_id__many_to_one:user_id__many_to_one:name']) + pipeline = described_class.lookup(Comment, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'post_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'Post', + 'localField' => 'post_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one', 'preserveNullAndEmptyArrays' => true } }, + { + '$lookup' => { + 'as' => 'post_id__many_to_one.user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'post_id__many_to_one.user_id' + } + }, + { '$unwind' => { 'path' => '$post_id__many_to_one.user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } } + ] + ) + end + end + + describe 'with a reparented collection' do + let(:stack) do + [ + { prefix: nil, as_fields: [], as_models: ['co_author'] }, + { prefix: 'co_author', as_fields: [], as_models: [] } + ] + end + + it 'loads the post co_author user (relation)' do + projection = Projection.new(['user_id__many_to_one:name']) + pipeline = described_class.lookup(Post, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'user_id' + } + }, + { '$unwind' => { 'path' => '$user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } } + ] + ) + end + + it 'loads the user (relation within fake relation)' do + projection = Projection.new(['parent:user_id__many_to_one:name']) + pipeline = described_class.lookup(Post, stack, projection, {}) + + expect(pipeline).to eq( + [ + { + '$lookup' => { + 'as' => 'parent.user_id__many_to_one', + 'foreignField' => '_id', + 'from' => 'User', + 'localField' => 'parent.user_id' + } + }, + { '$unwind' => { 'path' => '$parent.user_id__many_to_one', 'preserveNullAndEmptyArrays' => true } } + ] + ) + end end end end