From fa5f41b52f1d49608667a965b6735b89b0d0a9eb Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Wed, 19 Aug 2026 16:07:58 -0300 Subject: [PATCH 1/4] OXEAN-133 Remove relation filter syntax --- lib/graphoid/definitions/filters.rb | 19 ---- lib/graphoid/definitions/types.rb | 12 +-- .../relation_filter_restrictions_spec.rb | 50 ++++++++++ .../queries/relations/belongs_to_spec.rb | 39 -------- .../queries/relations/embeds_many_spec.rb | 48 --------- .../queries/relations/embeds_one_spec.rb | 81 --------------- .../has_and_belongs_to_many_selves_spec.rb | 99 ------------------- .../relations/has_and_belongs_to_many_spec.rb | 56 ----------- .../queries/relations/has_many_spec.rb | 92 ----------------- .../relations/has_many_through_spec.rb | 58 ----------- .../queries/relations/has_one_spec.rb | 38 ------- .../queries/relations/has_one_through_spec.rb | 48 --------- 12 files changed, 53 insertions(+), 587 deletions(-) create mode 100644 spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/belongs_to_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/embeds_many_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/embeds_one_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_selves_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_many_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_many_through_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_one_spec.rb delete mode 100644 spec/tester_mongo/spec/graphoid/queries/relations/has_one_through_spec.rb diff --git a/lib/graphoid/definitions/filters.rb b/lib/graphoid/definitions/filters.rb index 490b38ae..92fe83b2 100644 --- a/lib/graphoid/definitions/filters.rb +++ b/lib/graphoid/definitions/filters.rb @@ -40,25 +40,6 @@ def generate(model) end end - Relation.relations_of(model).each do |name, relation| - relation_class = relation.class_name.safe_constantize - relation_name = Utils.graphqlize(relation_class.name) - - next unless relation_class - - relation_filter = LIST[relation_class] - relation_filter = "Graphoid::Types::#{relation_name}Filter" unless relation_filter - - relation_name = Utils.camelize(name) - - if Relation.new(relation).many? - %w[some none every].each do |suffix| - argument "#{relation_name}_#{suffix}", relation_filter, required: false, camelize: false - end - else - argument relation_name.to_s, relation_filter, required: false, camelize: false - end - end end end LIST[model] diff --git a/lib/graphoid/definitions/types.rb b/lib/graphoid/definitions/types.rb index 23fcdf8c..75742c70 100644 --- a/lib/graphoid/definitions/types.rb +++ b/lib/graphoid/definitions/types.rb @@ -4,30 +4,24 @@ module Graphoid module Resolvers def self.resolver_class(relation_class, relation_type, association) association_name = association.name - relation_name = Utils.graphqlize(relation_class.name) Class.new(GraphQL::Schema::Resolver) do type [relation_type], null: true self.const_set(:ASSOCIATION_NAME, association_name) - filter = Graphoid::Filters::LIST[relation_class] - filter = "Graphoid::Types::#{relation_name}Filter" unless filter - order = Graphoid::Sorter::LIST[relation_class] - order = "Graphoid::Types::#{relation_name}Sorter" unless order - - argument :where, filter, required: false + order = Graphoid::Sorter::LIST[relation_class] + order = "Graphoid::Types::#{Utils.graphqlize(relation_class.name)}Sorter" unless order argument :order, order, required: false argument :limit, GraphQL::Types::Int, required: false argument :skip, GraphQL::Types::Int, required: false - def resolve(where: nil, order: nil, limit: nil, skip: nil) + def resolve(order: nil, limit: nil, skip: nil) obj = self.object processor = Graphoid::Queries::Processor association_name = self.class.const_get(:ASSOCIATION_NAME) result = obj.send(association_name) - result = processor.execute(result, where) if where.present? if order.present? order = processor.parse_order(obj.send(association_name), order) diff --git a/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb new file mode 100644 index 00000000..b4f08cfc --- /dev/null +++ b/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'when querying generated relations' do + def validation_errors(query) + TesterMongoSchema.validate(query) + end + + def expect_argument_not_accepted(query, argument) + error = validation_errors(query).first + + expect(error.message).to include("doesn't accept argument '#{argument}'") + end + + it 'should reject a one-valued relation filter' do + expect_argument_not_accepted( + '{ people(where: { account: { id_not: null } }) { id } }', + 'account' + ) + end + + %w[some none every].each do |operator| + it "should reject a to-many #{operator} relation filter" do + expect_argument_not_accepted( + "{ accounts(where: { labels_#{operator}: { id_not: null } }) { id } }", + "labels_#{operator}" + ) + end + end + + it 'should reject where on a selected to-many relation' do + expect_argument_not_accepted( + '{ accounts { labels(where: { name: "label" }) { id } } }', + 'where' + ) + end + + it 'should allow scalar filters and unfiltered relation selection' do + errors = validation_errors(<<~GRAPHQL) + { + accounts(where: { stringField: "account" }) { + labels(order: { id: ASC }, limit: 1, skip: 0) { id } + } + } + GRAPHQL + + expect(errors).to be_empty + end +end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/belongs_to_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/belongs_to_spec.rb deleted file mode 100644 index 312a0bf3..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/belongs_to_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryBelongsTo', type: :request do - let!(:delete) { Account.delete_all; Person.delete_all; } - subject { Helper.resolve(self, 'people', @query) } - - let!(:a0) { Account.create!(string_field: 'bobi') } - let!(:a1) { Account.create!(string_field: 'boca') } - let!(:a2) { Account.create!(string_field: 'boce') } - - let!(:p0) { Person.create!(camelCase: 'a', account: a0) } - let!(:p1) { Person.create!(camelCase: 'b', account: a1) } - let!(:p2) { Person.create!(camelCase: 'c', account: a2) } - - describe 'belongs_to' do - it 'filters belongs_to relation' do - @query = %{ - query { - people(where: { - account: { stringField_contains: "boc" } - }) { - id - account { - id - } - } - } - } - - expect(subject.size).to eq(2) - expect(subject[0]['id']).to eq p1.id.to_s - expect(subject[1]['id']).to eq p2.id.to_s - expect(subject[0]['account']['id']).to eq a1.id.to_s - expect(subject[1]['account']['id']).to eq a2.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/embeds_many_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/embeds_many_spec.rb deleted file mode 100644 index 57dccb27..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/embeds_many_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryEmbedsMany', type: :request do - let!(:delete) { Account.delete_all; } - subject { Helper.resolve(self, 'accounts', @query) } - - let!(:a0) do - Account.create!(string_field: 'bob', snakes: []) - end - let!(:a1) do - Account.create!(string_field: 'bob', snakes: []) - end - let!(:a2) do - Account.create!(string_field: 'boc', snakes: []) - end - - let!(:s0) { Snake.create!(name: 'a', camelCase: 1, snake_case: 1.0, account: a0) } - let!(:s1) { Snake.create!(name: 'a', camelCase: 2, snake_case: 1.0, account: a0) } - let!(:s2) { Snake.create!(name: 'b', camelCase: 1, snake_case: 1.0, account: a1) } - let!(:s3) { Snake.create!(name: 'b', camelCase: 2, snake_case: 2.0, account: a1) } - let!(:s4) { Snake.create!(name: 'c', camelCase: 1, snake_case: 1.0, account: a2) } - let!(:s5) { Snake.create!(name: 'a', camelCase: 2, snake_case: 2.0, account: a2) } - - describe 'filtering with conditions in embeds_many relations' do - it 'filters _some' do - @query = %{ - query { - accounts(where: { - stringField: "bob", - snakes_some: { name: "a", camelCase: 1 } - }) { - id - snakes { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a0.id.to_s - expect(subject[0]['snakes'][0]['id']).to eq s0.id.to_s - expect(subject[0]['snakes'][1]['id']).to eq s1.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/embeds_one_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/embeds_one_spec.rb deleted file mode 100644 index cff99b11..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/embeds_one_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryEmbedsOne', type: :request do - next if ENV['DRIVER'] != 'mongo' - - let!(:delete) { Account.delete_all; Person.delete_all } - subject { Helper.resolve(self, 'accounts', @query) } - - let!(:a0) { Account.create!(string_field: 'bob') } - let!(:a1) { Account.create!(string_field: 'bob') } - let!(:a2) { Account.create!(string_field: 'boc') } - - let!(:v0) { Value.create!(text: 'ac', name: 'ac', account: a0) } - let!(:v1) { Value.create!(text: 'ba', name: 'aa', account: a1) } - let!(:v2) { Value.create!(text: 'bb', name: 'ab', account: a2) } - - describe 'filtering with conditions in embeds_one relations' do - it 'filters properly' do - @query = %{ - query { - accounts(where: { - value: { text: "bb" } - }) { - id - value { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a2.id.to_s - expect(subject[0]['value']['id']).to eq v2.id.to_s - end - - it 'with _filter' do - @query = %{ - query { - accounts(where: { - stringField: "bob", - value: { text_contains: "a", name_not: "ac" } - }) { - id - value { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a1.id.to_s - expect(subject[0]['value']['id']).to eq v1.id.to_s - end - - it 'with OR' do - @query = %{ - query { - accounts(where: { - value: { OR: [ { text: "b" }, { text: "c" } ] } - }) { - id - value { - id - } - } - } - } - - pending('this implementation needs more brain cells') - raise - - expect(subject.size).to eq(3) - expect(subject[0]['id']).to eq a1.id.to_s - expect(subject[0]['value']['id']).to eq v1.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_selves_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_selves_spec.rb deleted file mode 100644 index 579a5f22..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_selves_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasAndBelongsToManySelves', type: :request do - let!(:delete) { User.delete_all; } - subject { Helper.resolve(self, 'users', @query) } - - if ENV['DRIVER'] == 'mongo' - let!(:u0) { User.create!(name: 'ba') } - let!(:u1) { User.create!(name: 'be') } - let!(:u2) { User.create!(name: 'bi') } - let!(:u3) { User.create!(name: 'bu', dependents: [u0]) } # this creates a dependency on u0 - let!(:u4) { User.create!(name: 'bo', dependencies: [u1]) } - let!(:u5) { User.create!(name: 'bh', dependencies: [u3, u4], dependents: [u2]) } - else # active record - let!(:u0) { User.create!(name: 'ba') } - let!(:u1) { User.create!(name: 'be') } - let!(:u2) { User.create!(name: 'bi') } - let!(:u3) { User.create!(name: 'bu') } - let!(:u4) { User.create!(name: 'bo') } - let!(:u5) { User.create!(name: 'bh') } - let!(:f0) { Follow.create!(follower: u3, followee: u0) } - let!(:f0) { Follow.create!(follower: u4, followee: u1) } - let!(:f0) { Follow.create!(follower: u5, followee: u3) } - let!(:f0) { Follow.create!(follower: u5, followee: u4) } - let!(:f0) { Follow.create!(follower: u2, followee: u5) } - end - - describe 'filtering has_and_belongs_to_many relations' do - it 'filters _some in mongoid' do - @query = %{ - query { - users(where: { - dependencies_some: { name: "bu" } - }) { - id - dependencies { - id - dependents { - id - } - } - } - } - } - - if ENV['DRIVER'] == 'mongo' - expect(subject.size).to eq(2) - expect(subject[0]['id']).to eq u0.id.to_s - expect(subject[0]['dependencies'][0]['id']).to eq u3.id.to_s - expect(subject[0]['dependencies'][0]['dependents'][0]['id']).to eq u0.id.to_s - - expect(subject[1]['id']).to eq u5.id.to_s - expect(subject[1]['dependencies'][0]['id']).to eq u3.id.to_s - expect(subject[1]['dependencies'][0]['dependents'][0]['id']).to eq u0.id.to_s - - expect(subject[1]['dependencies'][1]['id']).to eq u4.id.to_s - expect(subject[1]['dependencies'][1]['dependents'][0]['id']).to eq u5.id.to_s - end - end - - it 'filters _some in active record' do - @query = %{ - query { - users(where: { - followers_some: { name: "bu" } - }) { - id - followers { - id - followees { - id - } - } - } - } - } - - if ENV['DRIVER'] != 'mongo' - # TODO - pending('not working yet - needs more brain cells') - raise - - expect(subject.size).to eq(2) - expect(subject[0]['id']).to eq u0.id.to_s - expect(subject[0]['followers'][0]['id']).to eq u3.id.to_s - expect(subject[0]['followers'][0]['followees'][0]['id']).to eq u0.id.to_s - - expect(subject[1]['id']).to eq u5.id.to_s - expect(subject[1]['followers'][0]['id']).to eq u3.id.to_s - expect(subject[1]['followers'][0]['followees'][0]['id']).to eq u0.id.to_s - - expect(subject[1]['followers'][1]['id']).to eq u4.id.to_s - expect(subject[1]['followers'][1]['followees'][0]['id']).to eq u5.id.to_s - end - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_spec.rb deleted file mode 100644 index a2d725e7..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_and_belongs_to_many_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasAndBelongsToMany', type: :request do - let!(:delete) { User.delete_all; Account.delete_all; } - subject { Helper.resolve(self, 'users', @query) } - - let!(:u0) { User.create!(name: 'ba') } - let!(:u1) { User.create!(name: 'be') } - let!(:u2) { User.create!(name: 'bi') } - - let!(:a0) { Account.create!(snake_case: 'ba') } - let!(:a1) { Account.create!(snake_case: 'be') } - let!(:a2) { Account.create!(snake_case: 'bu') } - - before do - u0.update!(accounts: [a0, a1]) - u1.update!(accounts: [a1, a2]) - u2.update!(accounts: [a2, a0]) - end - - describe 'filtering has_and_belongs_to_many relations' do - it 'filters _some' do - @query = %{ - query { - users(where: { accounts_some: { snakeCase: "bu" } }){ - id - accounts(order: { id: ASC }) { - id - users(where: { name_in: ["be", "bi"] }) { - id - } - } - } - } - } - - expect(subject.size).to eq(2) - - expect(subject[0]['id']).to eq u1.id.to_s - expect(subject[0]['accounts'][0]['id']).to eq a1.id.to_s - expect(subject[0]['accounts'][0]['users'][0]['id']).to eq u1.id.to_s - expect(subject[0]['accounts'][1]['id']).to eq a2.id.to_s - expect(subject[0]['accounts'][1]['users'][0]['id']).to eq u1.id.to_s - expect(subject[0]['accounts'][1]['users'][1]['id']).to eq u2.id.to_s - - expect(subject[1]['id']).to eq u2.id.to_s - expect(subject[1]['accounts'][0]['id']).to eq a0.id.to_s - expect(subject[1]['accounts'][0]['users'][0]['id']).to eq u2.id.to_s - expect(subject[1]['accounts'][1]['id']).to eq a2.id.to_s - expect(subject[1]['accounts'][1]['users'][0]['id']).to eq u1.id.to_s - expect(subject[1]['accounts'][1]['users'][1]['id']).to eq u2.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_many_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_many_spec.rb deleted file mode 100644 index 81fa4a1d..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_many_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasMany', type: :request do - let!(:delete) { Account.delete_all; Label.delete_all } - subject { Helper.resolve(self, 'accounts', @query) } - - let!(:a0) { Account.create!(string_field: 'bob') } - let!(:a1) { Account.create!(string_field: 'boc') } - let!(:a2) { Account.create!(string_field: 'bob') } - - let!(:l0) { Label.create!(name: 'a', amount: 1, account: a0) } - let!(:l1) { Label.create!(name: 'a', amount: 2, account: a0) } - let!(:l2) { Label.create!(name: 'a', amount: 1, account: a1) } - let!(:l3) { Label.create!(name: 'a', amount: 2, account: a1) } - let!(:l4) { Label.create!(name: 'a', amount: 1, account: a2) } - let!(:l5) { Label.create!(name: 'b', amount: 3, account: a2) } - - describe 'filtering with conditions in has_many relations' do - it 'filters _some on has_many' do - @query = %{ - query { - accounts(where: { - stringField: "bob", - labels_some: { amount: 2, name: "a" } - }) { - id - labels { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a0.id.to_s - expect(subject[0]['labels'][0]['id']).to eq l0.id.to_s - expect(subject[0]['labels'][1]['id']).to eq l1.id.to_s - end - - it 'filters _none on has_many' do - @query = %{ - query { - accounts(where: { - labels_none: { amount: 2 } - }) { - id - labels(order: { id: ASC }) { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a2.id.to_s - expect(subject[0]['labels'][0]['id']).to eq l4.id.to_s - expect(subject[0]['labels'][1]['id']).to eq l5.id.to_s - end - - it 'filters _every on has_many' do - @query = %{ - query { - accounts(where: { - labels_every: { name: "a" } - }) { - id - labels { - id - } - } - } - } - - if ENV['DRIVER'] == 'mongo' - # TODO: build the _every division for mongoid - pending - raise - end - - expect(subject.size).to eq(2) - expect(subject[0]['id']).to eq a0.id.to_s - expect(subject[0]['labels'][0]['id']).to eq l0.id.to_s - expect(subject[0]['labels'][1]['id']).to eq l1.id.to_s - - expect(subject[1]['id']).to eq a1.id.to_s - expect(subject[1]['labels'][0]['id']).to eq l2.id.to_s - expect(subject[1]['labels'][1]['id']).to eq l3.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_many_through_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_many_through_spec.rb deleted file mode 100644 index 4e7d80f7..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_many_through_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasManyThrough', type: :request do - next if ENV['DRIVER'] == 'mongo' - - let!(:delete) { Contract.delete_all; Player.delete_all; Team.delete_all; } - subject { Helper.resolve(self, 'players', @query) } - - let!(:p0) { Player.create!(name: 'ba') } - let!(:p1) { Player.create!(name: 'be') } - let!(:p2) { Player.create!(name: 'bi') } - - let!(:t0) { Team.create!(name: 'ba') } - let!(:t1) { Team.create!(name: 'be') } - let!(:t2) { Team.create!(name: 'bu') } - - before do - p0.update!(teams: [t0, t1]) - p1.update!(teams: [t1, t2]) - p2.update!(teams: [t2, t0]) - end - - describe 'filtering has_many through relations' do - it 'filters _some' do - @query = %{ - query { - players(where: { teams_some: { name: "bu" } }){ - id - teams(order: { id: ASC }) { - id - players(where: { name_in: ["be", "bi"] }) { - id - } - } - } - } - } - - expect(subject.size).to eq(2) - - expect(subject[0]['id']).to eq p1.id.to_s - expect(subject[0]['teams'][0]['id']).to eq t1.id.to_s - expect(subject[0]['teams'][0]['players'][0]['id']).to eq p1.id.to_s - expect(subject[0]['teams'][1]['id']).to eq t2.id.to_s - expect(subject[0]['teams'][1]['players'][0]['id']).to eq p1.id.to_s - expect(subject[0]['teams'][1]['players'][1]['id']).to eq p2.id.to_s - - expect(subject[1]['id']).to eq p2.id.to_s - expect(subject[1]['teams'][0]['id']).to eq t0.id.to_s - expect(subject[1]['teams'][0]['players'][0]['id']).to eq p2.id.to_s - expect(subject[1]['teams'][1]['id']).to eq t2.id.to_s - expect(subject[1]['teams'][1]['players'][0]['id']).to eq p1.id.to_s - expect(subject[1]['teams'][1]['players'][1]['id']).to eq p2.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_one_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_one_spec.rb deleted file mode 100644 index bf4a8828..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_one_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasOne', type: :request do - let!(:delete) { Account.delete_all; Person.delete_all } - subject { Helper.resolve(self, 'accounts', @query) } - - let!(:a0) { Account.create!(string_field: 'bob') } - let!(:a1) { Account.create!(string_field: 'bob') } - let!(:a2) { Account.create!(string_field: 'boc') } - - let!(:p0) { Person.create!(snake_case: 'a', account: a0) } - let!(:p1) { Person.create!(snake_case: 'b', account: a1) } - let!(:p2) { Person.create!(snake_case: 'c', account: a2) } - - describe 'filtering with conditions in has_one relations' do - it 'filters by has_one relation' do - @query = %{ - query { - accounts(where: { - stringField: "bob", - person: { snakeCase: "b" } - }) { - id - person { - id - } - } - } - } - - expect(subject.size).to eq(1) - expect(subject[0]['id']).to eq a1.id.to_s - expect(subject[0]['person']['id']).to eq p1.id.to_s - end - end -end diff --git a/spec/tester_mongo/spec/graphoid/queries/relations/has_one_through_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relations/has_one_through_spec.rb deleted file mode 100644 index cae1dcc1..00000000 --- a/spec/tester_mongo/spec/graphoid/queries/relations/has_one_through_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'QueryHasOneThrough', type: :request do - next # if ENV['DRIVER'] == "mongo" - # TODO: make "through" relations not always be referenced to many. - - let!(:delete) { Brain.delete_all; Person.delete_all; Account.delete_all } - subject { Helper.resolve(self, 'accounts', @query) } - - let!(:a0) { Account.create! } - let!(:a1) { Account.create! } - let!(:a2) { Account.create! } - - let!(:p0) { Person.create!(account: a0) } - let!(:p1) { Person.create!(account: a1) } - let!(:p2) { Person.create!(account: a2) } - - let!(:b0) { Brain.create!(name: 'b0', person: p0) } - let!(:b1) { Brain.create!(name: 'b1', person: p1) } - let!(:b2) { Brain.create!(name: 'c0', person: p2) } - - describe 'filtering with conditions in has_one_through relations' do - it 'filters by has_one relation' do - @query = %{ - query { - accounts(where: { - brain: { name_contains: "b" } - }) { - id - brain { - id - } - } - } - } - - expect(subject.size).to eq(2) - - expect(subject[0]['id']).to eq a0.id.to_s - expect(subject[0]['brain']['id']).to eq b0.id.to_s - - expect(subject[1]['id']).to eq a1.id.to_s - expect(subject[1]['brain']['id']).to eq b1.id.to_s - end - end -end From 5c885807ba29da91e067c4c7e33df6160e7986fb Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Thu, 20 Aug 2026 12:59:59 -0300 Subject: [PATCH 2/4] OXEAN-133 Add nested filter opt-in --- lib/graphoid/definitions/filters.rb | 18 ++++++++++++++++++ lib/graphoid/graphoid.rb | 1 + lib/graphoid/mongoid_nested_filter.rb | 19 +++++++++++++++++++ spec/tester_mongo/app/models/person.rb | 2 +- .../relation_filter_restrictions_spec.rb | 9 ++++----- 5 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 lib/graphoid/mongoid_nested_filter.rb diff --git a/lib/graphoid/definitions/filters.rb b/lib/graphoid/definitions/filters.rb index 92fe83b2..e10803ab 100644 --- a/lib/graphoid/definitions/filters.rb +++ b/lib/graphoid/definitions/filters.rb @@ -40,6 +40,24 @@ def generate(model) end end + Relation.relations_of(model).each do |name, relation| + next unless relation.options[:graphoid_nested_filter] + + relation_class = relation.class_name.safe_constantize + next unless relation_class + + relation_name = Utils.graphqlize(relation_class.name) + relation_filter = LIST[relation_class] || "Graphoid::Types::#{relation_name}Filter" + relation_name = Utils.camelize(name) + + if Relation.new(relation).many? + %w[some none every].each do |suffix| + argument "#{relation_name}_#{suffix}", relation_filter, required: false, camelize: false + end + else + argument relation_name, relation_filter, required: false, camelize: false + end + end end end LIST[model] diff --git a/lib/graphoid/graphoid.rb b/lib/graphoid/graphoid.rb index ce848026..671d9e64 100644 --- a/lib/graphoid/graphoid.rb +++ b/lib/graphoid/graphoid.rb @@ -7,6 +7,7 @@ require 'graphoid/scalars' require 'graphoid/argument' require 'graphoid/graphield' +require 'graphoid/mongoid_nested_filter' require 'graphoid/operators/attribute' require 'graphoid/operators/relation' diff --git a/lib/graphoid/mongoid_nested_filter.rb b/lib/graphoid/mongoid_nested_filter.rb new file mode 100644 index 00000000..49141b7b --- /dev/null +++ b/lib/graphoid/mongoid_nested_filter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Graphoid + module MongoidNestedFilter + private + + def define_association!(macro_name, name, options = {}, &block) + options = options.dup + nested_filter = options.delete(:graphoid_nested_filter) + association = super + + # Mongoid validates association options before Graphoid can inspect the reflection. + association.options[:graphoid_nested_filter] = nested_filter if nested_filter + association + end + end +end + +Mongoid::Association::Macros::ClassMethods.prepend(Graphoid::MongoidNestedFilter) diff --git a/spec/tester_mongo/app/models/person.rb b/spec/tester_mongo/app/models/person.rb index 5af1af87..d7c824f2 100644 --- a/spec/tester_mongo/app/models/person.rb +++ b/spec/tester_mongo/app/models/person.rb @@ -8,5 +8,5 @@ class Person field :camelCase, type: String field :name, type: String - belongs_to :account + belongs_to :account, graphoid_nested_filter: true end diff --git a/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb b/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb index b4f08cfc..f1e7e248 100644 --- a/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/relation_filter_restrictions_spec.rb @@ -13,11 +13,10 @@ def expect_argument_not_accepted(query, argument) expect(error.message).to include("doesn't accept argument '#{argument}'") end - it 'should reject a one-valued relation filter' do - expect_argument_not_accepted( - '{ people(where: { account: { id_not: null } }) { id } }', - 'account' - ) + it 'should allow an explicitly opted-in relation filter' do + errors = validation_errors('{ people(where: { account: { id_not: null } }) { id } }') + + expect(errors).to be_empty end %w[some none every].each do |operator| From 243152b50d3838fc344ccc0d6711df6444467ff8 Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Thu, 20 Aug 2026 13:22:48 -0300 Subject: [PATCH 3/4] OXEAN-133 Update relation selection spec --- .../queries/attributes/subfilter_spec.rb | 78 ++----------------- 1 file changed, 7 insertions(+), 71 deletions(-) diff --git a/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb b/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb index 2e410248..690bb6e9 100644 --- a/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb @@ -25,87 +25,23 @@ [@h0, @a0, @p0, @l0, @l1].map(&:destroy) end - describe 'applies filters in related models' do - it 'when has many' do + describe 'when selecting related models' do + it 'should return all has-many relations after a scalar root filter' do @action = 'accounts' @query = %{ query { - accounts { + accounts(where: { integerField: 2 }) { id - labels(where: { amount: 2, name: "l0" }) { + labels { id } } } } - expect(subject[0]['labels'].size).to eq(1) - expect(subject[0]['labels'][0]['id']).to eq @l0.id.to_s + expect(subject.size).to eq(1) + label_ids = subject[0]['labels'].map { |label| label['id'] } + expect(label_ids).to contain_exactly(@l0.id.to_s, @l1.id.to_s) end - - # DEPRECATED: this is not supported anymore - # it 'when has one and does not match' do - # @action = 'account' - # @query = %{ - # query { - # account { - # id - # person(where: { name: "something" }) { - # id - # } - # } - # } - # } - - # expect(subject['person']).not_to be - # end - - # it 'when has one and matches' do - # @action = 'account' - # @query = %{ - # query { - # account { - # id - # person(where: { name: "p0" }) { - # id - # } - # } - # } - # } - - # expect(subject['person']['id']).to eq @p0.id.to_s - # end - - # it 'when belongs to and does not match' do - # @action = 'account' - # @query = %{ - # query { - # account { - # id - # house(where: { name: "something" }) { - # id - # } - # } - # } - # } - - # expect(subject['house']).not_to be - # end - - # it 'when belongs to and does and matches' do - # @action = 'account' - # @query = %{ - # query { - # account { - # id - # house(where: { name: "h0" }) { - # id - # } - # } - # } - # } - - # expect(subject['house']['id']).to eq @h0.id.to_s - # end end end From 4b3e4e785cf5b99549b222c8444637edadcb67b6 Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Thu, 20 Aug 2026 15:33:12 -0300 Subject: [PATCH 4/4] OXEAN-133 Assert selected relation filter rejection --- .../queries/attributes/subfilter_spec.rb | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb b/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb index 690bb6e9..62a0782e 100644 --- a/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/attributes/subfilter_spec.rb @@ -1,47 +1,22 @@ # frozen_string_literal: true -# TODO: should apply submodel filtering on embeeded relations -# TODO: elaborate more complex cases - require 'rails_helper' -describe 'QuerySubFilter', type: :request do - subject { Helper.resolve(self, @action, @query) } - - before(:all) do - Person.delete_all - Label.delete_all - Account.delete_all - House.delete_all - - @h0 = House.create!(name: 'h0') - @a0 = Account.create!(integer_field: 2, house: @h0) - @p0 = Person.create!(account: @a0, name: 'p0', snake_case: 'snake', camelCase: 'camel') - @l0 = Label.create!(account: @a0, name: 'l0', amount: 2) - @l1 = Label.create!(account: @a0, name: 'l1', amount: 2) - end - - after(:all) do - [@h0, @a0, @p0, @l0, @l1].map(&:destroy) - end - - describe 'when selecting related models' do - it 'should return all has-many relations after a scalar root filter' do - @action = 'accounts' - @query = %{ +describe 'QuerySubFilter' do + describe 'when filtering a selected relation' do + it 'should reject where on a has-many relation' do + errors = TesterMongoSchema.validate(<<~GRAPHQL) query { - accounts(where: { integerField: 2 }) { + accounts { id - labels { + labels(where: { amount: 2, name: "l0" }) { id } } } - } + GRAPHQL - expect(subject.size).to eq(1) - label_ids = subject[0]['labels'].map { |label| label['id'] } - expect(label_ids).to contain_exactly(@l0.id.to_s, @l1.id.to_s) + expect(errors.first.message).to include("Field 'labels' doesn't accept argument 'where'") end end end