From 61902482f9b31d03f14a4aa1a0c6a5739f06c68e Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Mon, 27 Jul 2026 17:49:38 -0300 Subject: [PATCH 1/3] Add strict paginated model hook EXPLANATION: Added a direct `graphoid_load_paginated_data(scope, lookahead:)` model hook for generated pagination data and removed indirect `lookahead`/zero-argument `eager_load` dispatch through query objects. Added integration coverage for the strict hook, returned collections, unchanged no-hook pagination, and ignored legacy methods. Evaluated further N+1 architectures and retained the bounded first/second-hop API loader as the simplest query-efficient design. Verified both Mongo tester suites with 88 examples, 0 failures, and 7 existing pending examples each. --- PROMPT: great, since we are access to oxeanbits/graphoid implementation (and digitalize-api is the only consumer of this repo), search if an even more performance improvement could be done (address this n+1 solution in another query level, or another architeture to optimize memory and mongo finds). if we already achived a good point, lets target the simplest solution. portalize current PR solution into another PR (keep current one as it is), open a new branch in graphoid fork (and a PR), the new api PR could updated the submodule sha to the new-branch one (including several bumps already merged in master) --- lib/graphoid/queries/pagination.rb | 15 ++--- .../spec/graphoid/queries/pagination_spec.rb | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/lib/graphoid/queries/pagination.rb b/lib/graphoid/queries/pagination.rb index afe24d0a..73d9fa2e 100644 --- a/lib/graphoid/queries/pagination.rb +++ b/lib/graphoid/queries/pagination.rb @@ -30,15 +30,12 @@ def self.build(model) field :skip, GraphQL::Types::Int, null: true field :data, [grapho.type], null: true, extras: [:lookahead] - def data(lookahead:) - object ||= @object - # Mongoid::Criteria uses method_missing to send the method to the underlying Model - # Just implement def self.lookahead(object, lookahead) in your model to manage - # eager loading - obj = object.lookahead(object, lookahead) if object.respond_to? :lookahead - object = obj if obj - return object.eager_load if object.respond_to? :eager_load - object + define_method(:data) do |lookahead:| + if model.respond_to?(:graphoid_load_paginated_data) + model.graphoid_load_paginated_data(object, lookahead: lookahead) + else + object + end end def page_size diff --git a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb index 9e98c467..17ab2b89 100644 --- a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb @@ -84,5 +84,62 @@ ) end end + + context 'with a paginated data model hook', limit: 2, skip_param: 0 do + let(:hook_arguments) { {} } + + before do + arguments = hook_arguments + hook_result = [level2] + Level.define_singleton_method(:graphoid_load_paginated_data) do |scope, lookahead:| + arguments[:scope] = scope + arguments[:lookahead] = lookahead + hook_result + end + + post :execute, params: { query: query } + end + + after do + Level.singleton_class.remove_method(:graphoid_load_paginated_data) + end + + it 'passes the resolved scope and lookahead directly to the model' do + expect(hook_arguments[:scope]).to be_a(Mongoid::Criteria) + expect(hook_arguments[:scope].options).to include(limit: 2, skip: 0) + selected_fields = hook_arguments[:lookahead].selections.map { |selection| selection.field.name } + + expect(selected_fields).to contain_exactly('id', 'name', 'createdAt') + end + + it 'uses the collection returned by the model hook' do + expect(JSON.parse(response.body)['data']['levels']['data']).to contain_exactly( + { + 'id' => level2.id.to_s, + 'name' => level2.name, + 'createdAt' => level2.created_at.iso8601(3) + } + ) + end + end + + context 'with legacy model loading methods', limit: 2, skip_param: 0 do + before do + Level.define_singleton_method(:lookahead) { |*, **| raise 'legacy lookahead called' } + Level.define_singleton_method(:eager_load) { |*, **| raise 'legacy eager_load called' } + + post :execute, params: { query: query } + end + + after do + Level.singleton_class.remove_method(:lookahead) + Level.singleton_class.remove_method(:eager_load) + end + + it 'does not dispatch pagination loading through criteria methods' do + expect(response).to have_http_status(200) + expect(JSON.parse(response.body)['errors']).to be_nil + end + end end end From 6137981c2d5fc9c9b710d88d8f37ee5070ff7c8c Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Tue, 28 Jul 2026 10:30:23 -0300 Subject: [PATCH 2/3] Split paginated scope preparation from loading EXPLANATION: Split paginated data resolution into direct `graphoid_prepare_paginated_scope` and `graphoid_load_paginated_data` model stages, preserving the transformed scope between them. Added independent legacy `lookahead` and `eager_load` fallbacks for unmigrated models, plus integration coverage for direct hook sequencing and legacy lookahead behavior. Verified both Graphoid tester suites with 88 examples, 0 failures, and 7 existing pending examples each. --- PROMPT: ok agree with the architecture, lets update both api and graphoid prs --- lib/graphoid/queries/pagination.rb | 23 +++++++-- .../spec/graphoid/queries/pagination_spec.rb | 47 ++++++++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/lib/graphoid/queries/pagination.rb b/lib/graphoid/queries/pagination.rb index 73d9fa2e..3d8dbfea 100644 --- a/lib/graphoid/queries/pagination.rb +++ b/lib/graphoid/queries/pagination.rb @@ -8,6 +8,22 @@ def self.generate(*models) models.each { |model| Graphoid::Queries::Pagination.build(model) } end + def self.prepare_paginated_scope(model, scope, lookahead) + return model.graphoid_prepare_paginated_scope(scope, lookahead: lookahead) if + model.respond_to?(:graphoid_prepare_paginated_scope) + return scope unless scope.respond_to?(:lookahead) + + scope.lookahead(scope, lookahead) || scope + end + + def self.load_paginated_data(model, scope, lookahead) + return model.graphoid_load_paginated_data(scope, lookahead: lookahead) if + model.respond_to?(:graphoid_load_paginated_data) + return scope.eager_load if scope.respond_to?(:eager_load) + + scope + end + def self.build(model) Graphoid.initialize grapho = Graphoid.build(model) @@ -31,11 +47,8 @@ def self.build(model) field :data, [grapho.type], null: true, extras: [:lookahead] define_method(:data) do |lookahead:| - if model.respond_to?(:graphoid_load_paginated_data) - model.graphoid_load_paginated_data(object, lookahead: lookahead) - else - object - end + scope = Graphoid::Queries::Pagination.prepare_paginated_scope(model, object, lookahead) + Graphoid::Queries::Pagination.load_paginated_data(model, scope, lookahead) end def page_size diff --git a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb index 17ab2b89..dc3f0fea 100644 --- a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb @@ -85,29 +85,37 @@ end end - context 'with a paginated data model hook', limit: 2, skip_param: 0 do + context 'with paginated data model hooks', limit: 2, skip_param: 0 do let(:hook_arguments) { {} } before do arguments = hook_arguments - hook_result = [level2] + selected_name = level2.name + Level.define_singleton_method(:graphoid_prepare_paginated_scope) do |scope, lookahead:| + arguments[:prepare_scope] = scope + arguments[:prepare_lookahead] = lookahead + arguments[:prepared_scope] = scope.where(name: selected_name) + end Level.define_singleton_method(:graphoid_load_paginated_data) do |scope, lookahead:| - arguments[:scope] = scope - arguments[:lookahead] = lookahead - hook_result + arguments[:load_scope] = scope + arguments[:load_lookahead] = lookahead + scope.to_a end post :execute, params: { query: query } end after do + Level.singleton_class.remove_method(:graphoid_prepare_paginated_scope) Level.singleton_class.remove_method(:graphoid_load_paginated_data) end - it 'passes the resolved scope and lookahead directly to the model' do - expect(hook_arguments[:scope]).to be_a(Mongoid::Criteria) - expect(hook_arguments[:scope].options).to include(limit: 2, skip: 0) - selected_fields = hook_arguments[:lookahead].selections.map { |selection| selection.field.name } + it 'passes the resolved scope and lookahead through both model hooks' do + expect(hook_arguments[:prepare_scope]).to be_a(Mongoid::Criteria) + expect(hook_arguments[:prepare_scope].options).to include(limit: 2, skip: 0) + expect(hook_arguments[:load_scope]).to equal(hook_arguments[:prepared_scope]) + expect(hook_arguments[:load_lookahead]).to equal(hook_arguments[:prepare_lookahead]) + selected_fields = hook_arguments[:prepare_lookahead].selections.map { |selection| selection.field.name } expect(selected_fields).to contain_exactly('id', 'name', 'createdAt') end @@ -124,21 +132,34 @@ end context 'with legacy model loading methods', limit: 2, skip_param: 0 do + let(:legacy_calls) { [] } + before do - Level.define_singleton_method(:lookahead) { |*, **| raise 'legacy lookahead called' } - Level.define_singleton_method(:eager_load) { |*, **| raise 'legacy eager_load called' } + calls = legacy_calls + hook_result = [level2] + Level.define_singleton_method(:lookahead) do |_scope, _lookahead| + calls << :lookahead + hook_result + end post :execute, params: { query: query } end after do Level.singleton_class.remove_method(:lookahead) - Level.singleton_class.remove_method(:eager_load) end - it 'does not dispatch pagination loading through criteria methods' do + it 'falls back to the legacy lookahead convention' do expect(response).to have_http_status(200) expect(JSON.parse(response.body)['errors']).to be_nil + expect(JSON.parse(response.body)['data']['levels']['data']).to contain_exactly( + { + 'id' => level2.id.to_s, + 'name' => level2.name, + 'createdAt' => level2.created_at.iso8601(3) + } + ) + expect(legacy_calls).to eq([:lookahead]) end end end From 2aa5aca5a92e1d09e67300d8bd46f47c62681eb4 Mon Sep 17 00:00:00 2001 From: GiuseppeXD Date: Thu, 13 Aug 2026 12:41:51 -0300 Subject: [PATCH 3/3] Simplify paginated model hooks EXPLANATION: Inlined pagination hook dispatch into the generated data resolver, renamed the explicit stages to graphoid_lookahead and graphoid_eager_load, and retained legacy object.lookahead and scope.eager_load fallbacks. Updated hook integration coverage. Verified the current Mongo tester pagination suite with 7 examples and no failures, plus syntax and diff integrity; the Rails-6 fixture cannot boot under Ruby 4 because its legacy debugger requires readline. --- PROMPT: that looks solid, commit push and ensure updated vendor commit hash in API (since we are updating the graphoiq) --- lib/graphoid/queries/pagination.rb | 33 +++++++++---------- .../spec/graphoid/queries/pagination_spec.rb | 8 ++--- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/graphoid/queries/pagination.rb b/lib/graphoid/queries/pagination.rb index 3d8dbfea..6281fded 100644 --- a/lib/graphoid/queries/pagination.rb +++ b/lib/graphoid/queries/pagination.rb @@ -8,22 +8,6 @@ def self.generate(*models) models.each { |model| Graphoid::Queries::Pagination.build(model) } end - def self.prepare_paginated_scope(model, scope, lookahead) - return model.graphoid_prepare_paginated_scope(scope, lookahead: lookahead) if - model.respond_to?(:graphoid_prepare_paginated_scope) - return scope unless scope.respond_to?(:lookahead) - - scope.lookahead(scope, lookahead) || scope - end - - def self.load_paginated_data(model, scope, lookahead) - return model.graphoid_load_paginated_data(scope, lookahead: lookahead) if - model.respond_to?(:graphoid_load_paginated_data) - return scope.eager_load if scope.respond_to?(:eager_load) - - scope - end - def self.build(model) Graphoid.initialize grapho = Graphoid.build(model) @@ -47,8 +31,21 @@ def self.build(model) field :data, [grapho.type], null: true, extras: [:lookahead] define_method(:data) do |lookahead:| - scope = Graphoid::Queries::Pagination.prepare_paginated_scope(model, object, lookahead) - Graphoid::Queries::Pagination.load_paginated_data(model, scope, lookahead) + scope = if model.respond_to?(:graphoid_lookahead) + model.graphoid_lookahead(object, lookahead: lookahead) + elsif object.respond_to?(:lookahead) + object.lookahead(object, lookahead) || object + else + object + end + + if model.respond_to?(:graphoid_eager_load) + model.graphoid_eager_load(scope, lookahead: lookahead) + elsif scope.respond_to?(:eager_load) + scope.eager_load + else + scope + end end def page_size diff --git a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb index dc3f0fea..4837d3f1 100644 --- a/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb +++ b/spec/tester_mongo/spec/graphoid/queries/pagination_spec.rb @@ -91,12 +91,12 @@ before do arguments = hook_arguments selected_name = level2.name - Level.define_singleton_method(:graphoid_prepare_paginated_scope) do |scope, lookahead:| + Level.define_singleton_method(:graphoid_lookahead) do |scope, lookahead:| arguments[:prepare_scope] = scope arguments[:prepare_lookahead] = lookahead arguments[:prepared_scope] = scope.where(name: selected_name) end - Level.define_singleton_method(:graphoid_load_paginated_data) do |scope, lookahead:| + Level.define_singleton_method(:graphoid_eager_load) do |scope, lookahead:| arguments[:load_scope] = scope arguments[:load_lookahead] = lookahead scope.to_a @@ -106,8 +106,8 @@ end after do - Level.singleton_class.remove_method(:graphoid_prepare_paginated_scope) - Level.singleton_class.remove_method(:graphoid_load_paginated_data) + Level.singleton_class.remove_method(:graphoid_lookahead) + Level.singleton_class.remove_method(:graphoid_eager_load) end it 'passes the resolved scope and lookahead through both model hooks' do