From b2029681821a5519b02f72d82c428848f8261f56 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Mon, 22 Jun 2026 23:13:39 -0400 Subject: [PATCH] rename to graphql-breadth --- README.md | 86 ++-- Rakefile | 2 +- benchmark/run.rb | 38 +- ...th_exec.gemspec => graphql-breadth.gemspec | 16 +- lib/graphql/{breadth_exec.rb => breadth.rb} | 30 +- .../authorization.rb | 2 +- .../directive_resolvers.rb | 2 +- .../{breadth_exec => breadth}/errors.rb | 28 +- .../{breadth_exec => breadth}/executor.rb | 4 +- .../executor/abstract_execution_scope.rb | 66 +++ .../breadth/executor/error_formatter.rb | 173 ++++++++ .../breadth/executor/execution_directive.rb | 93 ++++ .../breadth/executor/execution_field.rb | 260 ++++++++++++ .../breadth/executor/execution_planner.rb | 398 ++++++++++++++++++ .../executor/execution_promise.rb | 2 +- .../breadth/executor/execution_scope.rb | 181 ++++++++ .../breadth/executor/has_attributes.rb | 34 ++ .../executor/input_formatter.rb | 12 +- lib/graphql/breadth/executor/lazy_element.rb | 127 ++++++ .../breadth/executor/path_formatter.rb | 120 ++++++ .../field_resolvers.rb | 2 +- .../has_breadth_resolver.rb | 2 +- .../{breadth_exec => breadth}/incremental.rb | 2 +- .../incremental/context.rb | 2 +- .../incremental/defer_usage.rb | 2 +- .../incremental/deferred_delivery.rb | 2 +- .../incremental/deferred_execution_scope.rb | 2 +- .../incremental/partitioner.rb | 2 +- .../incremental/publisher.rb | 2 +- .../incremental/result.rb | 2 +- .../incremental/selection.rb | 2 +- .../introspection.rb | 2 +- .../{breadth_exec => breadth}/lazy_loader.rb | 2 +- .../subscription_response_stream.rb | 2 +- .../{breadth_exec => breadth}/tracer.rb | 2 +- lib/graphql/{breadth_exec => breadth}/util.rb | 4 +- .../{breadth_exec => breadth}/version.rb | 4 +- .../executor/abstract_execution_scope.rb | 64 --- .../breadth_exec/executor/error_formatter.rb | 171 -------- .../executor/execution_directive.rb | 91 ---- .../breadth_exec/executor/execution_field.rb | 258 ------------ .../executor/execution_planner.rb | 396 ----------------- .../breadth_exec/executor/execution_scope.rb | 179 -------- .../breadth_exec/executor/has_attributes.rb | 32 -- .../breadth_exec/executor/lazy_element.rb | 125 ------ .../breadth_exec/executor/path_formatter.rb | 118 ------ test/fixtures.rb | 46 +- .../execution_errors_test.rb | 26 +- .../executor/abstracts_test.rb | 16 +- .../executor/aggregate_selections_test.rb | 8 +- .../executor/arguments_test.rb | 10 +- .../executor/authorization_test.rb | 16 +- .../executor/conditionals_test.rb | 18 +- .../executor/directives_test.rb | 24 +- .../executor/error_formatter_test.rb | 24 +- .../executor/errors_test.rb | 30 +- .../executor/execution_field_test.rb | 12 +- .../executor/execution_promise_test.rb | 16 +- .../executor/fragments_test.rb | 10 +- .../executor/incremental_test.rb | 20 +- .../executor/input_formatter_test.rb | 26 +- .../executor/introspection_test.rb | 8 +- .../executor/lists_test.rb | 26 +- .../executor/loaders_test.rb | 34 +- .../executor/star_wars_test.rb | 2 +- .../executor/subscriptions_test.rb | 32 +- .../executor/tracers_test.rb | 28 +- .../executor_test.rb | 10 +- .../field_resolvers_test.rb | 22 +- .../has_breadth_resolver_test.rb | 26 +- .../incremental/context_test.rb | 12 +- .../incremental/publisher_test.rb | 6 +- test/star_wars_fixtures.rb | 24 +- test/test_helper.rb | 12 +- 74 files changed, 1854 insertions(+), 1836 deletions(-) rename graphql-breadth_exec.gemspec => graphql-breadth.gemspec (73%) rename lib/graphql/{breadth_exec.rb => breadth.rb} (59%) rename lib/graphql/{breadth_exec => breadth}/authorization.rb (97%) rename lib/graphql/{breadth_exec => breadth}/directive_resolvers.rb (98%) rename lib/graphql/{breadth_exec => breadth}/errors.rb (88%) rename lib/graphql/{breadth_exec => breadth}/executor.rb (99%) create mode 100644 lib/graphql/breadth/executor/abstract_execution_scope.rb create mode 100644 lib/graphql/breadth/executor/error_formatter.rb create mode 100644 lib/graphql/breadth/executor/execution_directive.rb create mode 100644 lib/graphql/breadth/executor/execution_field.rb create mode 100644 lib/graphql/breadth/executor/execution_planner.rb rename lib/graphql/{breadth_exec => breadth}/executor/execution_promise.rb (99%) create mode 100644 lib/graphql/breadth/executor/execution_scope.rb create mode 100644 lib/graphql/breadth/executor/has_attributes.rb rename lib/graphql/{breadth_exec => breadth}/executor/input_formatter.rb (98%) create mode 100644 lib/graphql/breadth/executor/lazy_element.rb create mode 100644 lib/graphql/breadth/executor/path_formatter.rb rename lib/graphql/{breadth_exec => breadth}/field_resolvers.rb (99%) rename lib/graphql/{breadth_exec => breadth}/has_breadth_resolver.rb (99%) rename lib/graphql/{breadth_exec => breadth}/incremental.rb (95%) rename lib/graphql/{breadth_exec => breadth}/incremental/context.rb (99%) rename lib/graphql/{breadth_exec => breadth}/incremental/defer_usage.rb (94%) rename lib/graphql/{breadth_exec => breadth}/incremental/deferred_delivery.rb (97%) rename lib/graphql/{breadth_exec => breadth}/incremental/deferred_execution_scope.rb (98%) rename lib/graphql/{breadth_exec => breadth}/incremental/partitioner.rb (99%) rename lib/graphql/{breadth_exec => breadth}/incremental/publisher.rb (98%) rename lib/graphql/{breadth_exec => breadth}/incremental/result.rb (97%) rename lib/graphql/{breadth_exec => breadth}/incremental/selection.rb (95%) rename lib/graphql/{breadth_exec => breadth}/introspection.rb (99%) rename lib/graphql/{breadth_exec => breadth}/lazy_loader.rb (99%) rename lib/graphql/{breadth_exec => breadth}/subscription_response_stream.rb (97%) rename lib/graphql/{breadth_exec => breadth}/tracer.rb (99%) rename lib/graphql/{breadth_exec => breadth}/util.rb (93%) rename lib/graphql/{breadth_exec => breadth}/version.rb (56%) delete mode 100644 lib/graphql/breadth_exec/executor/abstract_execution_scope.rb delete mode 100644 lib/graphql/breadth_exec/executor/error_formatter.rb delete mode 100644 lib/graphql/breadth_exec/executor/execution_directive.rb delete mode 100644 lib/graphql/breadth_exec/executor/execution_field.rb delete mode 100644 lib/graphql/breadth_exec/executor/execution_planner.rb delete mode 100644 lib/graphql/breadth_exec/executor/execution_scope.rb delete mode 100644 lib/graphql/breadth_exec/executor/has_attributes.rb delete mode 100644 lib/graphql/breadth_exec/executor/lazy_element.rb delete mode 100644 lib/graphql/breadth_exec/executor/path_formatter.rb rename test/graphql/{breadth_exec => breadth}/execution_errors_test.rb (56%) rename test/graphql/{breadth_exec => breadth}/executor/abstracts_test.rb (80%) rename test/graphql/{breadth_exec => breadth}/executor/aggregate_selections_test.rb (83%) rename test/graphql/{breadth_exec => breadth}/executor/arguments_test.rb (76%) rename test/graphql/{breadth_exec => breadth}/executor/authorization_test.rb (87%) rename test/graphql/{breadth_exec => breadth}/executor/conditionals_test.rb (72%) rename test/graphql/{breadth_exec => breadth}/executor/directives_test.rb (78%) rename test/graphql/{breadth_exec => breadth}/executor/error_formatter_test.rb (86%) rename test/graphql/{breadth_exec => breadth}/executor/errors_test.rb (82%) rename test/graphql/{breadth_exec => breadth}/executor/execution_field_test.rb (90%) rename test/graphql/{breadth_exec => breadth}/executor/execution_promise_test.rb (58%) rename test/graphql/{breadth_exec => breadth}/executor/fragments_test.rb (81%) rename test/graphql/{breadth_exec => breadth}/executor/incremental_test.rb (97%) rename test/graphql/{breadth_exec => breadth}/executor/input_formatter_test.rb (98%) rename test/graphql/{breadth_exec => breadth}/executor/introspection_test.rb (97%) rename test/graphql/{breadth_exec => breadth}/executor/lists_test.rb (80%) rename test/graphql/{breadth_exec => breadth}/executor/loaders_test.rb (81%) rename test/graphql/{breadth_exec => breadth}/executor/star_wars_test.rb (99%) rename test/graphql/{breadth_exec => breadth}/executor/subscriptions_test.rb (89%) rename test/graphql/{breadth_exec => breadth}/executor/tracers_test.rb (89%) rename test/graphql/{breadth_exec => breadth}/executor_test.rb (79%) rename test/graphql/{breadth_exec => breadth}/field_resolvers_test.rb (75%) rename test/graphql/{breadth_exec => breadth}/has_breadth_resolver_test.rb (82%) rename test/graphql/{breadth_exec => breadth}/incremental/context_test.rb (73%) rename test/graphql/{breadth_exec => breadth}/incremental/publisher_test.rb (89%) diff --git a/README.md b/README.md index cf0a200..735b691 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ _**The core algorithm backing Shopify's _GraphQL Cardinal_ engine.** Learn more ruby 3.2.1 (2023-02-08 revision 31819e82c8) +YJIT [arm64-darwin23] Non-lazy comparison: -graphql-breadth_exec: 1000 x 3 scalars: 1257.1 i/s +graphql-breadth: 1000 x 3 scalars: 1257.1 i/s graphql-ruby resolve_batch: 1000 x 3 scalars: 773.2 i/s - 1.63x slower graphql-ruby classic: 1000 x 3 scalars: 102.0 i/s - 12.32x slower Lazy comparison: -graphql-breadth_exec LazyLoader: 1000 x 1 lazy scalar: 2469.5 i/s +graphql-breadth LazyLoader: 1000 x 1 lazy scalar: 2469.5 i/s graphql-ruby execute_next + dataloader: 1000 x 1 lazy scalar: 533.1 i/s - 4.63x slower graphql-ruby execute_next + graphql-batch: 1000 x 1 lazy scalar: 291.7 i/s - 8.47x slower graphql-ruby graphql-batch: 1000 x 1 lazy scalar: 178.7 i/s - 13.82x slower @@ -37,7 +37,7 @@ The execution algorithm is proven at scale in production. This implementation st ## Execute a query ```ruby -executor = GraphQL::BreadthExec::Executor.new( +executor = GraphQL::Breadth::Executor.new( MyGraphQLSchema, GraphQL.parse(document), root_object: { ... }, @@ -108,10 +108,10 @@ This taxonomy provides the following API, which is useful while writing resolver ## Field resolvers -For each field implementation, set up a `GraphQL::BreadthExec::FieldResolver` or use a [keyword helper](#resolver-keywords): +For each field implementation, set up a `GraphQL::Breadth::FieldResolver` or use a [keyword helper](#resolver-keywords): ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) exec_field.map_objects { |object| object.some_method } end @@ -125,11 +125,11 @@ A field resolver receives: - `exec_field.arguments`: a hash of resolved arguments provided to the field. * `context`: the request context. -A resolver **must return a mapped set of results** for the field's objects, or invoke a [lazy resolver hook](#lazy-resolvers). To attach a field resolver to a field, use the `GraphQL::BreadthExec::HasBreadthResolver` field mixin: +A resolver **must return a mapped set of results** for the field's objects, or invoke a [lazy resolver hook](#lazy-resolvers). To attach a field resolver to a field, use the `GraphQL::Breadth::HasBreadthResolver` field mixin: ```ruby class BaseField < GraphQL::Schema::Field - include GraphQL::BreadthExec::HasBreadthResolver::Field + include GraphQL::Breadth::HasBreadthResolver::Field end class BaseObject < GraphQL::Schema::Object @@ -147,17 +147,17 @@ end The core library includes several basic resolvers for common needs: -* `GraphQL::BreadthExec::MethodResolver.new(:method_to_call, ...)` (chained methods) -* `GraphQL::BreadthExec::HashKeyResolver.new("some_key")` (symbol or string key) -* `GraphQL::BreadthExec::ValueResolver.new(true)` (static value) -* `GraphQL::BreadthExec::SelfResolver.new` (resolves original objects) +* `GraphQL::Breadth::MethodResolver.new(:method_to_call, ...)` (chained methods) +* `GraphQL::Breadth::HashKeyResolver.new("some_key")` (symbol or string key) +* `GraphQL::Breadth::ValueResolver.new(true)` (static value) +* `GraphQL::Breadth::SelfResolver.new` (resolves original objects) ### Early return Field resolvers may return early with a value for all objects using `resolve_all`. This is commonly used to resolve `nil` or an eager value across all field objects. ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) return exec_field.resolve_all(nil) if exec_field.arguments[:key].nil? @@ -171,7 +171,7 @@ end Field resolvers may build resources that could be shared with other fields across their scope. It's easy to share this sort of data using `attributes`. Both execution fields [and their scopes](#execution-taxonomy) support setting attributes: ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) # cache wrapped objects on the parent scope... wrapped_objects = exec_field.scope.attributes[:wrapped_objects] ||= begin @@ -190,7 +190,7 @@ When reading attributes, prefer using `element.attribute(key)` to avoid allocati To error out specific object positions within a field, error instances must be mapped into the field's result set. Use the `handle_or_reraise` helper within a StandardError rescue block to optimally handle raised mapping errors: ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) exec_field.objects.map do |obj| obj.valid? ? obj.my_field : GraphQL::ExecutionError.new("Not valid") @@ -204,7 +204,7 @@ end This pattern is so common that it's provided as the `map_objects` helper. Just remember when calling `map_objects` that the results may include inlined error positions: ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) exec_field.map_objects(&:do_stuff!) # << maps to results OR inlined errors end @@ -214,7 +214,7 @@ end Any error raised during field execution _outside_ of a rescued mapping loop will result in all field objects receiving the same error: ```ruby -class MyFieldResolver < GraphQL::BreadthExec::FieldResolver +class MyFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) raise GraphQL::ExecutionError.new("no key") if exec_field.arguments[:key].nil? @@ -249,7 +249,7 @@ In the above, we'll want `Image.sources` to batch across all instances of the fi For the simplest loading scenarios, field resolvers can delegate to their own lazy loading hook with keys that will batch across instances of the field: ```ruby -class BasicLazyResolver < GraphQL::BreadthExec::FieldResolver +class BasicLazyResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) exec_field.lazy(keys: exec_field.objects.map(&:id)) end @@ -264,7 +264,7 @@ end Calling `exec_field.lazy` defers a set of keys for lazy fulfillment, which then get loaded by the resolver's `perform_lazy` hook that **must return a mapped set of results**. You can also scope lazy loading with arguments, and wrap it with pre- and post-processing steps: ```ruby -class FancyLazyResolver < GraphQL::BreadthExec::FieldResolver +class FancyLazyResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) mapped_keys = exec_field.objects.map { |obj| obj.valid? ? obj.id : nil } @@ -287,7 +287,7 @@ end It's extremely common for a mapped set of lazy keys to have `nil` positions that must be retained to match the resolver's breadth set. These nil keys should almost never be loaded, so they are omitted from batching and resolve as nil by default. If you specifically want to treat nil as a loadable value, specify `load_nil_keys: true`. ```ruby -class MaybeNilKeysResolver < GraphQL::BreadthExec::FieldResolver +class MaybeNilKeysResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) mapped_keys = exec_field.objects.map { |obj| obj.ready? ? obj.id : nil } @@ -305,7 +305,7 @@ end In many cases, a resolver can eagerly evaluate the result of some keys. Use `eager_values` to inject pre-resolved values into a lazy loader: ```ruby -class MaskingResolver < GraphQL::BreadthExec::FieldResolver +class MaskingResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _context) eager_values = {} mapped_keys = exec_field.objects.map do |obj| @@ -330,7 +330,7 @@ Eager values are specific to their field instance and will _not_ be shared by fi Queries shared across field resolvers need a common LazyLoader class. ```ruby -class SharedLoader < GraphQL::BreadthExec::LazyLoader +class SharedLoader < GraphQL::Breadth::LazyLoader def initialize(group:) super() @group = group @@ -343,7 +343,7 @@ class SharedLoader < GraphQL::BreadthExec::LazyLoader end end -class SharedLazyResolver < GraphQL::BreadthExec::FieldResolver +class SharedLazyResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) mapped_keys = exec_field.objects.map { |obj| obj.valid? ? obj.id : nil } @@ -359,7 +359,7 @@ end In this case `exec_field.lazy` is called with a LazyLoader class, which delegates loading to an instance of the provided loader class rather than the resolver's own `perform_lazy` method. Within a loader class, call `fulfill_key` to deliver each loaded record. Lazy loaders do NOT require fulfillment of each provided key; unfulfilled keys simply return as `nil`. You can also set up a lazy loader class to fulfill by mapped set, although this frequently adds a mapping layer that calling `fulfill_key` directly would avoid: ```ruby -class MapLoader < GraphQL::BreadthExec::LazyLoader +class MapLoader < GraphQL::Breadth::LazyLoader def map? = true def perform_map(keys, context) @@ -374,7 +374,7 @@ end Lazy loaders support passing any complex object as loader keys. These complex objects can be reduced to a primitive identity within the loader's internal mapping table using the `identity_for` hook: ```ruby -class IdentityLoader < GraphQL::BreadthExec::LazyLoader +class IdentityLoader < GraphQL::Breadth::LazyLoader def identity_for(key) "#{key.path}/#{key.handle}" end @@ -394,7 +394,7 @@ Later on, it may be simpler to derive the same identity via the loaded result an Multiple loads can be built and awaited: ```ruby -class AwaitingResolver < GraphQL::BreadthExec::FieldResolver +class AwaitingResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _context) keys = exec_field.objects.map(&:key) @@ -415,7 +415,7 @@ end Lazy sequencing can be chained: ```ruby -class ChainingResolver < GraphQL::BreadthExec::FieldResolver +class ChainingResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _context) exec_field .lazy(PrefixLoader, args: { prefix: "a" }, keys: exec_field.objects.map(&:key)) @@ -440,7 +440,7 @@ These three phases repeat each time an abstract position is resolved to build, p A field resolver may define a `plan` method that runs during the field's planning phase. This hook may register preloads and/or make tree annotations. Its return value is never captured or used. ```ruby -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) exec_field.preload(AssociationLazyLoader, args: { association: :sprockets }) end @@ -456,7 +456,7 @@ end Both execution scopes and fields may bind lazy loaders during the planning phase that will perform preloads before the element executes. Use the `preload` method: ```ruby -class AssociationPreload < GraphQL::BreadthExec::LazyLoader +class AssociationPreload < GraphQL::Breadth::LazyLoader def initialize(association:) super() @association = association @@ -468,7 +468,7 @@ class AssociationPreload < GraphQL::BreadthExec::LazyLoader end end -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) exec_field.preload(AssociationPreload, args: { association: :sprockets }) # or ... @@ -490,7 +490,7 @@ So – scope preloads are useful for loading authorization dependencies and/or s The `preload` method does not require loader keys and will use the scope or field's resolved objects as keys by default. You can also manually pass keys, which is useful when chaining: ```ruby -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) exec_field # uses the field's resolved objects as keys... @@ -509,7 +509,7 @@ end Some lazy preloads may need to be configured at the time of execution when objects are actually avaiable for a scope or field. The `on_preload` hook may be used during planning to configure preloads in a just-in-time manner. ```ruby -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) exec_field.on_preload do widgets = exec_field.objects.grep(Widget) @@ -524,7 +524,7 @@ end It's useful to use the root scope as a preload target where all fields in the document can pool common work (ex: loading auth dependencies into a context cache). However – abstract selection branches are planned _lazily after resolution_, at which time the document above their subtree has been sealed and no longer accepts preloads. Use `planning_root` to always locate the highest unplanned scope and operate there: ```ruby -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) exec_field.scope.planning_root.preload(AuthContextLoader, keys: [context[:agent].id]) end @@ -538,7 +538,7 @@ While navigating up the execution tree, you may call `allows_preload?` on scopes It can be useful to make notes about the execution tree while planning. Both execution scopes and fields provide an `attributes` hash for freeform annotations: ```ruby -class WidgetResolver < GraphQL::BreadthExec::FieldResolver +class WidgetResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, context) ancestor_scope = exec_field.scope&.parent if ancestor_scope && ancestor_scope.parent_type == Sprocket @@ -552,14 +552,14 @@ Once tree annotations are set during the planning phase, field resolvers can res ## Authorization -The breadth executor includes a authorization model that can be customized as needed. Create a `GraphQL::BreadthExec::Authorization` subclass and pass it to your executor: +The breadth executor includes a authorization model that can be customized as needed. Create a `GraphQL::Breadth::Authorization` subclass and pass it to your executor: ```ruby -class MyAuthorization < GraphQL::BreadthExec::Authorization +class MyAuthorization < GraphQL::Breadth::Authorization # ... detail auth behaviors end -GraphQL::BreadthExec::Executor.new( +GraphQL::Breadth::Executor.new( MyGraphQLSchema, GraphQL.parse(document), authorization: MyAuthorization, @@ -598,7 +598,7 @@ query { } ``` -To implement a runtime directive, set up a `BreadthExec::DirectiveResolver` and assign it to the directive class: +To implement a runtime directive, set up a `Breadth::DirectiveResolver` and assign it to the directive class: ```ruby class LanguageDirectiveResolver < DirectiveResolver @@ -610,7 +610,7 @@ class LanguageDirectiveResolver < DirectiveResolver end class Language < GraphQL::Schema::Directive - extend GraphQL::BreadthExec::HasBreadthResolver::Directive + extend GraphQL::Breadth::HasBreadthResolver::Directive graphql_name("language") argument :lang, String, required: true @@ -679,7 +679,7 @@ This architecture makes cascading resolvers run repeatedly on every field in a s ## Incremental results (`@defer`) -Query and mutation operations that may contain `@defer` should use `incremental_result`. This always returns a `GraphQL::BreadthExec::Incremental::Result`, even when the operation has no active deferred work: +Query and mutation operations that may contain `@defer` should use `incremental_result`. This always returns a `GraphQL::Breadth::Incremental::Result`, even when the operation has no active deferred work: ```ruby result = executor.incremental_result @@ -699,10 +699,10 @@ The basic and incremental entry points are intentionally strict. Call either `re ## Subscriptions -Query and mutation execution use `result`, which always returns a normal GraphQL result hash. Subscription operations use `subscribe`, which returns a `GraphQL::BreadthExec::SubscriptionResponseStream` on successful source setup, or a normal GraphQL result hash for public setup errors. Each entry point is strict about its operation type, matching the `execute` / `subscribe` split in graphql-js: calling `result` (or `incremental_result`) for a subscription operation raises an implementation error, and calling `subscribe` for a query or mutation operation raises an implementation error. A controller that accepts both inspects the operation type and dispatches accordingly: +Query and mutation execution use `result`, which always returns a normal GraphQL result hash. Subscription operations use `subscribe`, which returns a `GraphQL::Breadth::SubscriptionResponseStream` on successful source setup, or a normal GraphQL result hash for public setup errors. Each entry point is strict about its operation type, matching the `execute` / `subscribe` split in graphql-js: calling `result` (or `incremental_result`) for a subscription operation raises an implementation error, and calling `subscribe` for a query or mutation operation raises an implementation error. A controller that accepts both inspects the operation type and dispatches accordingly: ```ruby -executor = GraphQL::BreadthExec::Executor.new( +executor = GraphQL::Breadth::Executor.new( MyGraphQLSchema, GraphQL.parse(document), variables: { ... }, @@ -725,7 +725,7 @@ Subscription root fields use two field resolver hooks: * `resolve(exec_field, context)` runs once per yielded source event. The source event is used as the root object for that event's GraphQL execution. ```ruby -class OnWriteValueResolver < GraphQL::BreadthExec::FieldResolver +class OnWriteValueResolver < GraphQL::Breadth::FieldResolver def subscribe(exec_field, context) context[:write_value_events] end @@ -754,7 +754,7 @@ write_value_events = Enumerator.new do |events| events << { value: "second" } end -executor = GraphQL::BreadthExec::Executor.new( +executor = GraphQL::Breadth::Executor.new( MyGraphQLSchema, GraphQL.parse(%| subscription WatchWrites { diff --git a/Rakefile b/Rakefile index 672fbc4..eadb353 100644 --- a/Rakefile +++ b/Rakefile @@ -43,7 +43,7 @@ namespace :benchmark do GraphQLBenchmark.benchmark_introspection end - desc "Benchmark GraphQL Ruby resolve_batch against breadth_exec" + desc "Benchmark GraphQL Ruby resolve_batch against breadth" task :resolve_batch do prepare_benchmark GraphQLBenchmark.benchmark_resolve_batch diff --git a/benchmark/run.rb b/benchmark/run.rb index 85318dd..e6918a4 100644 --- a/benchmark/run.rb +++ b/benchmark/run.rb @@ -3,7 +3,7 @@ require "debug" require "graphql" require "graphql/batch" -require "graphql/breadth_exec" +require "graphql/breadth" require "benchmark/ips" require "memory_profiler" @@ -28,7 +28,7 @@ def analyze_query(_query, _analyzers, multiplex_analyzers: []) DOCUMENT = GraphQL.parse(BASIC_DOCUMENT) CARDINAL_SCHEMA = SCHEMA - CARDINAL_TRACER = GraphQL::BreadthExec::Tracer.new + CARDINAL_TRACER = GraphQL::Breadth::Tracer.new RESOLVE_BATCH_OBJECT_COUNT = 1_000 RESOLVE_BATCH_DOCUMENT = GraphQL.parse(%|{ widgets { @@ -67,18 +67,18 @@ def analyze_query(_query, _analyzers, multiplex_analyzers: []) RESOLVE_BATCH_RESOLVERS = { "Widget" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), - "score" => GraphQL::BreadthExec::HashKeyResolver.new("score"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), + "score" => GraphQL::Breadth::HashKeyResolver.new("score"), }, "Query" => { - "widgets" => GraphQL::BreadthExec::HashKeyResolver.new("widgets"), + "widgets" => GraphQL::Breadth::HashKeyResolver.new("widgets"), }, }.freeze RESOLVE_BATCH_BREADTH_SCHEMA = GraphQL::Schema.from_definition(RESOLVE_BATCH_SDL) - class NoopBreadthLazyLoader < GraphQL::BreadthExec::LazyLoader + class NoopBreadthLazyLoader < GraphQL::Breadth::LazyLoader def map? true end @@ -88,7 +88,7 @@ def perform_map(keys, _context) end end - class LazyHashKeyResolver < GraphQL::BreadthExec::FieldResolver + class LazyHashKeyResolver < GraphQL::Breadth::FieldResolver def initialize(key) @key = key end @@ -108,7 +108,7 @@ def resolve(exec_field, _context) "score" => LazyHashKeyResolver.new("score"), }, "Query" => { - "widgets" => GraphQL::BreadthExec::HashKeyResolver.new("widgets"), + "widgets" => GraphQL::Breadth::HashKeyResolver.new("widgets"), }, }.freeze @@ -357,7 +357,7 @@ def benchmark_execution end x.report("graphql-cardinal #{num_objects} resolvers") do - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, DOCUMENT, resolvers: BREADTH_RESOLVERS, @@ -390,7 +390,7 @@ def benchmark_lazy_execution end x.report("graphql-cardinal: #{num_objects} lazy resolvers") do - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, DOCUMENT, resolvers: BREADTH_DEFERRED_RESOLVERS, @@ -413,7 +413,7 @@ def benchmark_introspection end x.report("graphql-cardinal introspection") do - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, document, resolvers: BREADTH_RESOLVERS, @@ -444,8 +444,8 @@ def benchmark_resolve_batch warn "Skipping graphql-ruby resolve_batch: GraphQL::Execution::Next#execute_next is not available in graphql #{GraphQL::VERSION}." end - x.report("graphql-breadth_exec: #{RESOLVE_BATCH_OBJECT_COUNT} x 3 scalars") do - GraphQL::BreadthExec::Executor.new( + x.report("graphql-breadth: #{RESOLVE_BATCH_OBJECT_COUNT} x 3 scalars") do + GraphQL::Breadth::Executor.new( RESOLVE_BATCH_BREADTH_SCHEMA, RESOLVE_BATCH_DOCUMENT, resolvers: RESOLVE_BATCH_RESOLVERS, @@ -464,8 +464,8 @@ def benchmark_lazy_scalars Benchmark.ips do |x| x.config(time: iterations, warmup: 1) - x.report("graphql-breadth_exec lazy: #{RESOLVE_BATCH_OBJECT_COUNT} x 3 scalars") do - GraphQL::BreadthExec::Executor.new( + x.report("graphql-breadth lazy: #{RESOLVE_BATCH_OBJECT_COUNT} x 3 scalars") do + GraphQL::Breadth::Executor.new( RESOLVE_BATCH_BREADTH_SCHEMA, RESOLVE_BATCH_DOCUMENT, resolvers: LAZY_SCALAR_BREADTH_RESOLVERS, @@ -516,8 +516,8 @@ def benchmark_lazy_field_batch execute_graphql_ruby(LazyScalarDataloaderSchema, document: LAZY_FIELD_BATCH_DOCUMENT, root_value: RESOLVE_BATCH_DATA) end - x.report("graphql-breadth_exec LazyLoader: #{RESOLVE_BATCH_OBJECT_COUNT} x 1 lazy scalar") do - GraphQL::BreadthExec::Executor.new( + x.report("graphql-breadth LazyLoader: #{RESOLVE_BATCH_OBJECT_COUNT} x 1 lazy scalar") do + GraphQL::Breadth::Executor.new( RESOLVE_BATCH_BREADTH_SCHEMA, LAZY_FIELD_BATCH_DOCUMENT, resolvers: LAZY_SCALAR_BREADTH_RESOLVERS, @@ -546,7 +546,7 @@ def memory_profile with_data_sizes(sizes) do |data_source, num_objects| report = MemoryProfiler.report do - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, DOCUMENT, resolvers: BREADTH_RESOLVERS, diff --git a/graphql-breadth_exec.gemspec b/graphql-breadth.gemspec similarity index 73% rename from graphql-breadth_exec.gemspec rename to graphql-breadth.gemspec index 1b136db..4ab55e9 100644 --- a/graphql-breadth_exec.gemspec +++ b/graphql-breadth.gemspec @@ -1,23 +1,23 @@ # frozen_string_literal: true -require_relative 'lib/graphql/breadth_exec/version' +require_relative 'lib/graphql/breadth/version' Gem::Specification.new do |spec| - spec.name = 'graphql-breadth_exec' - spec.version = GraphQL::BreadthExec::VERSION + spec.name = 'graphql-breadth' + spec.version = GraphQL::Breadth::VERSION spec.authors = ['Greg MacWilliam'] spec.summary = 'A breadth-first executor for GraphQL Ruby' spec.description = 'A breadth-first executor for GraphQL Ruby' - spec.homepage = 'https://github.com/gmac/graphql-breadth-exec' + spec.homepage = 'https://github.com/gmac/graphql-breadth' spec.license = 'MIT' spec.required_ruby_version = '>= 2.7.0' spec.metadata = { - 'homepage_uri' => 'https://github.com/gmac/graphql-breadth-exec', - 'changelog_uri' => 'https://github.com/gmac/graphql-breadth-exec/releases', - 'source_code_uri' => 'https://github.com/gmac/graphql-breadth-exec', - 'bug_tracker_uri' => 'https://github.com/gmac/graphql-breadth-exec/issues', + 'homepage_uri' => 'https://github.com/gmac/graphql-breadth', + 'changelog_uri' => 'https://github.com/gmac/graphql-breadth/releases', + 'source_code_uri' => 'https://github.com/gmac/graphql-breadth', + 'bug_tracker_uri' => 'https://github.com/gmac/graphql-breadth/issues', } spec.files = `git ls-files -z`.split("\x0").reject do |f| diff --git a/lib/graphql/breadth_exec.rb b/lib/graphql/breadth.rb similarity index 59% rename from lib/graphql/breadth_exec.rb rename to lib/graphql/breadth.rb index 39e7527..8fae326 100644 --- a/lib/graphql/breadth_exec.rb +++ b/lib/graphql/breadth.rb @@ -5,7 +5,7 @@ require "set" module GraphQL - module BreadthExec + module Breadth class << self #: Proc? attr_accessor :on_report_error @@ -36,17 +36,17 @@ class ExecutionField; end end end -require_relative "breadth_exec/util" -require_relative "breadth_exec/errors" -require_relative "breadth_exec/authorization" -require_relative "breadth_exec/executor/execution_promise" -require_relative "breadth_exec/lazy_loader" -require_relative "breadth_exec/tracer" -require_relative "breadth_exec/field_resolvers" -require_relative "breadth_exec/directive_resolvers" -require_relative "breadth_exec/subscription_response_stream" -require_relative "breadth_exec/has_breadth_resolver" -require_relative "breadth_exec/introspection" -require_relative "breadth_exec/executor" -require_relative "breadth_exec/incremental" -require_relative "breadth_exec/version" +require_relative "breadth/util" +require_relative "breadth/errors" +require_relative "breadth/authorization" +require_relative "breadth/executor/execution_promise" +require_relative "breadth/lazy_loader" +require_relative "breadth/tracer" +require_relative "breadth/field_resolvers" +require_relative "breadth/directive_resolvers" +require_relative "breadth/subscription_response_stream" +require_relative "breadth/has_breadth_resolver" +require_relative "breadth/introspection" +require_relative "breadth/executor" +require_relative "breadth/incremental" +require_relative "breadth/version" diff --git a/lib/graphql/breadth_exec/authorization.rb b/lib/graphql/breadth/authorization.rb similarity index 97% rename from lib/graphql/breadth_exec/authorization.rb rename to lib/graphql/breadth/authorization.rb index 94555e6..1c7f282 100644 --- a/lib/graphql/breadth_exec/authorization.rb +++ b/lib/graphql/breadth/authorization.rb @@ -4,7 +4,7 @@ require "graphql" module GraphQL - module BreadthExec + module Breadth class Authorization #: (Executor::ExecutionField[untyped], GraphQL::Query::Context) -> bool def authorized_field?(exec_field, context) diff --git a/lib/graphql/breadth_exec/directive_resolvers.rb b/lib/graphql/breadth/directive_resolvers.rb similarity index 98% rename from lib/graphql/breadth_exec/directive_resolvers.rb rename to lib/graphql/breadth/directive_resolvers.rb index 36e3f27..9264e02 100644 --- a/lib/graphql/breadth_exec/directive_resolvers.rb +++ b/lib/graphql/breadth/directive_resolvers.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class DirectiveResolver #: (?wraps: bool, ?cascades: bool) -> void def initialize(wraps: false, cascades: false) diff --git a/lib/graphql/breadth_exec/errors.rb b/lib/graphql/breadth/errors.rb similarity index 88% rename from lib/graphql/breadth_exec/errors.rb rename to lib/graphql/breadth/errors.rb index 1ea305b..262acdf 100644 --- a/lib/graphql/breadth_exec/errors.rb +++ b/lib/graphql/breadth/errors.rb @@ -4,7 +4,7 @@ require "graphql" module GraphQL - module BreadthExec + module Breadth class Error < StandardError; end class ImplementationError < Error; end class MethodNotImplementedError < Error; end @@ -19,14 +19,14 @@ class << self #: ( #| untyped, - #| ?exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped]?, + #| ?exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped]?, #| ?cause: Exception? #| ) -> ExecutionError def from(err, exec_field: nil, cause: nil) return err if err.equal?(UNREPORTED_ERROR) case err - when BreadthExec::ExecutionError + when Breadth::ExecutionError if err.exec_field != exec_field || err.cause != cause err = err.dup err.exec_field = exec_field if exec_field @@ -47,7 +47,7 @@ def from(err, exec_field: nil, cause: nil) DEFAULT_MESSAGE = "An unknown error occurred" - #: GraphQL::BreadthExec::Executor::ExecutionField[untyped]? + #: GraphQL::Breadth::Executor::ExecutionField[untyped]? attr_accessor :exec_field #: Exception? @@ -61,7 +61,7 @@ def from(err, exec_field: nil, cause: nil) #: ( #| ?String?, - #| ?exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped]?, + #| ?exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped]?, #| ?nodes: Array[GraphQL::Language::Nodes::AbstractNode], #| ?extensions: extensions_hash?, #| ?cause: Exception?, @@ -119,7 +119,7 @@ class ExecutionErrorSet < ExecutionError #: ( #| ?String?, - #| ?exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped]?, + #| ?exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped]?, #| ?errors: Array[ExecutionError], #| ) -> void def initialize(message = nil, exec_field: nil, errors: []) @@ -153,7 +153,7 @@ class FieldAuthorizationError < ExecutionError #: ( #| ?String?, - #| exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped], + #| exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped], #| ?extensions: extensions_hash?, #| ?cause: Exception?, #| ) -> void @@ -165,7 +165,7 @@ def initialize(message = nil, exec_field:, extensions: nil, cause: nil) class InvalidNullError < ExecutionError self.error_code = "INVALID_NULL".freeze - #: (?String?, exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped], ?list_item: bool) -> void + #: (?String?, exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped], ?list_item: bool) -> void def initialize(message = nil, exec_field:, list_item: false) message ||= if list_item list_type = Util.unwrap_non_null(exec_field.type) @@ -207,14 +207,14 @@ class InputValidatorError < InputError class InputValidationErrorSet < ExecutionErrorSet; end class InvalidListResultError < ImplementationError - #: GraphQL::BreadthExec::Executor::ExecutionField[untyped] + #: GraphQL::Breadth::Executor::ExecutionField[untyped] attr_reader :exec_field #: Class attr_reader :result_type #: ( - #| exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped], + #| exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped], #| result_type: Class, #| ) -> void def initialize(exec_field:, result_type:) @@ -225,7 +225,7 @@ def initialize(exec_field:, result_type:) end class ResultCountMismatchError < ImplementationError - #: GraphQL::BreadthExec::Executor::ExecutionField[untyped] + #: GraphQL::Breadth::Executor::ExecutionField[untyped] attr_reader :exec_field #: Integer @@ -236,7 +236,7 @@ class ResultCountMismatchError < ImplementationError #: ( #| ?String?, - #| exec_field: GraphQL::BreadthExec::Executor::ExecutionField[untyped], + #| exec_field: GraphQL::Breadth::Executor::ExecutionField[untyped], #| expected_count: Integer, #| actual_count: Integer, #| ) -> void @@ -258,11 +258,11 @@ def initialize(from, to) end class LazySequencingError < ImplementationError - #: GraphQL::BreadthExec::Executor::LazyElement + #: GraphQL::Breadth::Executor::LazyElement attr_reader :lazy_element #: ( - #| lazy_element: GraphQL::BreadthExec::Executor::LazyElement, + #| lazy_element: GraphQL::Breadth::Executor::LazyElement, #| method_name: String, #| ) -> void def initialize(lazy_element:, method_name:) diff --git a/lib/graphql/breadth_exec/executor.rb b/lib/graphql/breadth/executor.rb similarity index 99% rename from lib/graphql/breadth_exec/executor.rb rename to lib/graphql/breadth/executor.rb index b808fd9..801529f 100644 --- a/lib/graphql/breadth_exec/executor.rb +++ b/lib/graphql/breadth/executor.rb @@ -13,7 +13,7 @@ require_relative "./executor/path_formatter" module GraphQL - module BreadthExec + module Breadth class Executor #: type type_resolver = ^(untyped, GraphQL::Query::Context) -> singleton(GraphQL::Schema::Member)? #: type resolver = FieldResolver | DirectiveResolver | type_resolver @@ -275,7 +275,7 @@ def execute begin @input.coerce_variable_values(operation.variables, @query.provided_variables || EMPTY_OBJECT) rescue InputValidationErrorSet => e - GraphQL::BreadthExec.report_error(e) + GraphQL::Breadth.report_error(e) end execute_with_directives(@planner.root_directives_for_operation(operation)) do diff --git a/lib/graphql/breadth/executor/abstract_execution_scope.rb b/lib/graphql/breadth/executor/abstract_execution_scope.rb new file mode 100644 index 0000000..3d7fb2d --- /dev/null +++ b/lib/graphql/breadth/executor/abstract_execution_scope.rb @@ -0,0 +1,66 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class AbstractExecutionScope + #: singleton(GraphQL::Schema::Member) + attr_reader :parent_type + + #: ExecutionField[untyped] + attr_reader :parent_field + + #: Array[ExecutionScope] + attr_reader :scopes + + #: ( + #| parent_type: singleton(GraphQL::Schema::Member), + #| parent_field: ExecutionField[untyped], + #| scopes: Array[ExecutionScope], + #| ) -> void + def initialize(parent_type:, parent_field:, scopes:) + @parent_type = parent_type + @parent_field = parent_field + @scopes = scopes + @executed = false + end + + #: -> bool + def executed? + @executed + end + + #: -> Executor + def executor + @parent_field.executor + end + + #: -> ExecutionScope + def parent + @parent_field.scope + end + + #: -> Array[String] + def path + @parent_field.path + end + + #: -> Integer + def depth + @parent_field.depth + end + + #: -> Array[untyped] + def objects + @objects ||= @scopes.flat_map(&:objects) + end + + #: -> Array[untyped] + def results + @results ||= @scopes.flat_map(&:results) + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/error_formatter.rb b/lib/graphql/breadth/executor/error_formatter.rb new file mode 100644 index 0000000..2f984dd --- /dev/null +++ b/lib/graphql/breadth/executor/error_formatter.rb @@ -0,0 +1,173 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class ErrorFormatter + class State + #: Array[Hash[String, untyped]] + attr_reader :errors + + #: error_path + attr_reader :actual_path + + #: error_path + attr_reader :base_path + + #: (?error_path) -> void + def initialize(base_path = EMPTY_ARRAY) + @base_path = base_path + @actual_path = [] + @errors = [] + end + + #: -> error_path + def current_path + @base_path + @actual_path + end + end + + #: ( + #| executor: Executor, + #| invalidated_results: Hash[untyped, ExecutionError], + #| abstract_result_types: Hash[untyped, singleton(GraphQL::Schema::Object)], + #| ) -> void + def initialize(executor:, invalidated_results:, abstract_result_types:) + @executor = executor + @context = executor.context + @invalidated_results = invalidated_results + @abstract_result_types = abstract_result_types + end + + #: ( + #| singleton(GraphQL::Schema::Object), + #| Array[selection_node], + #| Hash[String, untyped], + #| ?error_path, + #| ) -> [Hash[String, untyped]?, Array[error_hash]] + def format_object(parent_type, selections, data, base_path = EMPTY_ARRAY) + return [data, EMPTY_ARRAY] if @invalidated_results.empty? + + state = State.new(base_path) + + if (err = @invalidated_results[data]) + add_formatted_error(err, state) + return [nil, state.errors] + end + + data = propagate_object_scope_errors(data, parent_type, selections, state) + [data, state.errors] + end + + private + + #: (untyped, singleton(GraphQL::Schema::Object), Array[selection_node], State) -> untyped + def propagate_object_scope_errors(raw_object, parent_type, selections, state) + return nil if raw_object.nil? + + selections.each do |node| + case node + when GraphQL::Language::Nodes::Field + field_key = node.alias || node.name + + state.actual_path << field_key + + begin + node_type = @context.types.field(parent_type, node.name).type + named_type = node_type.unwrap + raw_value = raw_object.fetch(field_key, Executor::UNDEFINED) + next if raw_value.equal?(Executor::UNDEFINED) + + raw_object[field_key] = if (err = @invalidated_results[raw_value]) + add_formatted_error(err, state) + nil + elsif node_type.list? + propagate_list_scope_errors(raw_value, node_type, node.selections, state) + elsif named_type.kind.leaf? + raw_value + else + propagate_object_scope_errors(raw_value, named_type, node.selections, state) + end + + return nil if node_type.non_null? && raw_object[field_key].nil? + ensure + state.actual_path.pop + end + + when GraphQL::Language::Nodes::InlineFragment + fragment_type = node.type ? @context.types.type(node.type.name) : parent_type + next unless result_of_type?(raw_object, parent_type, fragment_type) + + result = propagate_object_scope_errors(raw_object, fragment_type, node.selections, state) + return nil if result.nil? + + when GraphQL::Language::Nodes::FragmentSpread + fragment = @executor.fragments.fetch(node.name) + fragment_type = @context.types.type(fragment.type.name) + next unless result_of_type?(raw_object, parent_type, fragment_type) + + result = propagate_object_scope_errors(raw_object, fragment_type, fragment.selections, state) + return nil if result.nil? + end + end + + raw_object + end + + #: (Array[untyped]?, singleton(GraphQL::Schema::Member), Array[selection_node], State) -> Array[untyped]? + def propagate_list_scope_errors(raw_list, current_node_type, selections, state) + return nil if raw_list.nil? + + item_node_type = Util.unwrap_non_null(current_node_type).of_type + named_type = item_node_type.unwrap + + raw_list.map!.with_index do |raw_list_element, index| + state.actual_path << index + + begin + result = if (err = @invalidated_results[raw_list_element]) + add_formatted_error(err, state) + nil + elsif item_node_type.list? + propagate_list_scope_errors(raw_list_element, item_node_type, selections, state) + elsif named_type.kind.leaf? + raw_list_element + else + propagate_object_scope_errors(raw_list_element, named_type, selections, state) + end + + return nil if result.nil? && item_node_type.non_null? + + result + ensure + state.actual_path.pop + end + end + end + + #: (untyped, singleton(GraphQL::Schema::Member), singleton(GraphQL::Schema::Member)) -> bool + def result_of_type?(result, current_type, inquiry_type) + result_type = current_type.kind.abstract? ? @abstract_result_types[result] : current_type + raise ImplementationError, "No type annotation recorded for abstract result" if result_type.nil? + + if inquiry_type.kind.abstract? + @context.types.possible_types(inquiry_type).include?(result_type) + else + result_type == inquiry_type + end + end + + #: (ExecutionError, State) -> void + def add_formatted_error(error, state) + error.each do |err| + next if err.equal?(UNREPORTED_ERROR) + + state.errors << err.to_h.tap { _1["path"] = state.current_path } + @context.errors << err.cause if err.cause + end + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/execution_directive.rb b/lib/graphql/breadth/executor/execution_directive.rb new file mode 100644 index 0000000..d303aa2 --- /dev/null +++ b/lib/graphql/breadth/executor/execution_directive.rb @@ -0,0 +1,93 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class ExecutionDirective + include HasAttributes + + NOOP_DIRECTIVE_RESOLVER = NoopDirectiveResolver.new + + class << self + #: ( + #| executor: Executor, + #| node: GraphQL::Language::Nodes::Directive, + #| definition: singleton(GraphQL::Schema::Directive), + #| ?resolver: DirectiveResolver?, + #| ?depth: Integer, + #| ) -> ExecutionDirective + def build(executor:, node:, definition:, resolver: nil, depth: 0) + new( + executor: executor, + name: node.name, + node: node, + definition: definition, + resolver: resolver, + depth: depth, + ) + end + end + + #: String + attr_reader :name + + #: graphql_arguments + attr_reader :arguments + + #: singleton(GraphQL::Schema::Directive) + attr_reader :definition + + #: DirectiveResolver + attr_reader :resolver + + #: GraphQL::Language::Nodes::Directive? + attr_reader :node + + #: Integer + attr_reader :depth + + #: ( + #| executor: Executor, + #| name: String, + #| definition: singleton(GraphQL::Schema::Directive), + #| ?arguments: graphql_arguments?, + #| ?node: GraphQL::Language::Nodes::Directive?, + #| ?resolver: DirectiveResolver?, + #| ?depth: Integer, + #| ) -> void + def initialize(executor:, name:, definition:, arguments: nil, node: nil, resolver: nil, depth: 0) + @name = name + @node = node + @definition = definition + @resolver = resolver || NOOP_DIRECTIVE_RESOLVER + @depth = depth + @arguments, @argument_errors = if arguments.nil? + executor.input.coerce_argument_values(@definition, @node) + else + [arguments, EMPTY_ARRAY] + end + @mutable_arguments = nil + end + + #: () -> String + def inspect + "#" + end + + #: () -> graphql_arguments + def mutable_arguments + @mutable_arguments ||= Util.deep_copy(@arguments) + end + + #: () -> void + def validate! + unless @argument_errors.empty? + @argument_errors.each { _1.add_parent_node(@node) } if @node + raise InputValidationErrorSet.new(errors: @argument_errors) + end + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/execution_field.rb b/lib/graphql/breadth/executor/execution_field.rb new file mode 100644 index 0000000..5455e8b --- /dev/null +++ b/lib/graphql/breadth/executor/execution_field.rb @@ -0,0 +1,260 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class ExecutionField + include LazyElement + include HasAttributes + + LAZY_STATE_EXECUTING = :executing + + # Field's name as it appears in the response. + #: String + attr_reader :key + + # Field's name according to schema. + #: String + attr_reader :name + + #: GraphQL::Schema::Field? + attr_reader :definition + + #: Array[ExecutionDirective] + attr_reader :directives + + #: FieldResolver + attr_reader :resolver + + #: Array[GraphQL::Language::Nodes::Field] + attr_reader :nodes + + # Only populated during incremental (`@defer`) execution; nil on the hot path. + #: Array[Incremental::Selection]? + attr_reader :incremental_selections + + #: graphql_arguments + attr_reader :arguments + + #: ExecutionScope + attr_accessor :scope + + #: singleton(GraphQL::Schema::Member) + attr_accessor :type + + #: untyped + attr_accessor :result + + #: ( + #| String, + #| nodes: Array[GraphQL::Language::Nodes::Field], + #| scope: ExecutionScope, + #| definition: GraphQL::Schema::Field, + #| resolver: FieldResolver, + #| ?directives: Array[ExecutionDirective], + #| ?incremental_selections: Array[Incremental::Selection]?, + #| ) -> void + def initialize(key, nodes:, scope:, definition:, resolver:, directives: EMPTY_ARRAY, incremental_selections: nil) + super() + @key = key.freeze + @scope = scope + @definition = definition + @directives = directives.freeze + @resolver = resolver + @name = definition.graphql_name.freeze + @nodes = nodes.freeze + @type = definition.type + @result = nil + @arguments, @argument_errors = executor.input.coerce_argument_values(@definition, @nodes.first) + @mutable_arguments = nil + @incremental_selections = incremental_selections + @path = nil + @schema_path = nil + end + + #: () -> Array[ObjectType] + def objects + @scope.objects + end + + #: () -> GraphQL::Query::Context + def context + @scope.context + end + + #: () -> singleton(GraphQL::Schema::Member) + def parent_type + @scope.parent_type + end + + #: () -> Executor + def executor + @scope.executor + end + + #: () -> ExecutionScope + def root + @scope.root + end + + #: () -> ExecutionScope + def planning_root + @scope.planning_root + end + + #: -> Integer + def depth + path.length + end + + #: () -> Array[String] + def schema_path + @schema_path ||= [*@scope.schema_path, name].freeze + end + + #: ( + #| loader_class: singleton(LazyLoader), + #| keys: Array[untyped], + #| ?args: loader_args?, + #| ?eager_values: Hash[untyped, untyped]?, + #| ?load_nil_keys: bool, + #| ) -> ExecutionPromise + def lazy(loader_class:, keys:, args: nil, eager_values: nil, load_nil_keys: false) + unless allows_lazy? + raise LazySequencingError.new(lazy_element: self, method_name: "lazy") + end + + executor.lazy_loader_for(loader_class, args).load( + element: self, + keys: keys, + eager_values: eager_values, + load_nil_keys: load_nil_keys, + ) + end + + #: (Array[ExecutionPromise]) -> ExecutionPromise + def await_all(promises) + super + end + + #: () -> bool + def allows_lazy? + @lazy_state == LAZY_STATE_EXECUTING + end + + #: () -> Array[String] + def path + @path ||= [*@scope.path, @key].freeze + end + + #: (Integer) -> error_path + def object_path(index) + path = @scope.object_path(index) + path << @key + path + end + + #: [T] () { (untyped) -> (T | ExecutionError) } -> Array[T | ExecutionError] + def map_objects(&block) + objects.map do |obj| + yield(obj) + rescue StandardError => e + handle_or_reraise(e) + end + end + + #: [T] () { (untyped, Integer) -> (T | ExecutionError) } -> Array[T | ExecutionError] + def map_objects_with_index(&block) + objects.map.with_index do |obj, index| + yield(obj, index) + rescue StandardError => e + handle_or_reraise(e) + end + end + + #: [T] (T) -> Array[T] + def resolve_all(value) + value = case value + when StandardError + handle_or_reraise(value) + else + value + end + Array.new(objects.length, value) + end + + #: (Exception) -> ExecutionError + def handle_or_reraise(error) + executor.handle_or_reraise(error, exec_field: self) + end + + #: () -> Array[selection_node] + def selections + if @nodes.length > 1 + @nodes.flat_map(&:selections) + else + @nodes.first.selections + end + end + + #: () -> graphql_arguments + def mutable_arguments + @mutable_arguments ||= Util.deep_copy(arguments) + end + + #: () -> void + def validate! + unless @argument_errors.empty? + node = @nodes.first + @argument_errors.each { _1.add_parent_node(node) } + raise ExecutionErrorSet.new(exec_field: self, errors: @argument_errors) + end + end + + #: () -> bool + def lazy_result? + @result.is_a?(ExecutionPromise) + end + + #: () -> bool + def has_result? + !@result.nil? + end + + #: () -> bool + def propagates_null? + current_type = @type + return false unless current_type.kind.wraps? + + while current_type.list? + return false unless current_type.non_null? + + current_type = Util.unwrap_non_null(current_type).of_type + end + + current_type.non_null? + end + + #: () -> String + def inspect + alias_prefix = key == name ? "" : "#{key} => " + "#" + end + + def lazy_state_executing! + raise LazyStateTransitionError.new(@lazy_state, LAZY_STATE_EXECUTING) unless @lazy_state == LAZY_STATE_PRELOADING + + @lazy_state = LAZY_STATE_EXECUTING + end + + private + + #: () -> bool + def lazy_state_lockable? + @lazy_state == LAZY_STATE_PRELOADING || @lazy_state == LAZY_STATE_EXECUTING + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/execution_planner.rb b/lib/graphql/breadth/executor/execution_planner.rb new file mode 100644 index 0000000..665019d --- /dev/null +++ b/lib/graphql/breadth/executor/execution_planner.rb @@ -0,0 +1,398 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class ExecutionPlanner + QUERY_OPERATION = "query".freeze + MUTATION_OPERATION = "mutation".freeze + SUBSCRIPTION_OPERATION = "subscription".freeze + TYPENAME_FIELD = "__typename".freeze + ENGINE_RUNTIME_DIRECTIVES = ["skip", "include", "defer", "stream"].freeze + + class << self + #: (String, GraphQL::Query::Context) -> singleton(GraphQL::Schema::Object) + def root_type_for_operation(operation_type, context) + case operation_type + when QUERY_OPERATION + context.types.query_root + when MUTATION_OPERATION + context.types.mutation_root + when SUBSCRIPTION_OPERATION + context.types.subscription_root + else + raise ArgumentError, "unexpected root type name: #{operation_type}" + end + end + end + + #: ( + #| executor: Executor, + #| resolvers: Hash[String, Hash[String, resolver]], + #| ) -> void + def initialize(executor:, resolvers:) + @executor = executor + @resolvers = resolvers + @schema = @executor.schema + @context = @executor.context + @planned_scopes_by_field = {}.compare_by_identity + @has_runtime_directives = false + end + + #: (ExecutionField[untyped]) -> ExecutionScope? + def planned_scope_for(exec_field) + @planned_scopes_by_field[exec_field] + end + + #: (String) -> singleton(GraphQL::Schema::Object) + def root_type_for_operation(operation_type) + self.class.root_type_for_operation(operation_type, @context) + end + + #: (GraphQL::Language::Nodes::OperationDefinition) -> Array[ExecutionDirective] + def root_directives_for_operation(operation) + operation.directives.map { |node| build_execution_directive(node, depth: 0) } + end + + #: ( + #| GraphQL::Language::Nodes::OperationDefinition, + #| root_object: untyped, + #| result: graphql_result, + #| ) -> Array[ExecutionScope] + def root_scopes_for_operation(operation, root_object:, result:) + case operation.operation_type + when QUERY_OPERATION + [ + ExecutionScope.new( + executor: @executor, + parent_type: root_type_for_operation(QUERY_OPERATION), + selections: operation.selections, + objects: [root_object], + results: [result], + ), + ] + when MUTATION_OPERATION + # each mutation field must run serially as its own scope + mutation_type = root_type_for_operation(MUTATION_OPERATION) + selections_grouped_by_key(mutation_type, operation.selections).each_value.map do |selections| + ExecutionScope.new( + executor: @executor, + parent_type: mutation_type, + selections: selections.freeze, + objects: [root_object], + results: [result], + ) + end + when SUBSCRIPTION_OPERATION + [ + ExecutionScope.new( + executor: @executor, + parent_type: root_type_for_operation(SUBSCRIPTION_OPERATION), + selections: operation.selections, + objects: [root_object], + results: [result], + ), + ] + else + raise ImplementationError, "Unsupported operation type: #{operation.operation_type}" + end + end + + #: (Array[ExecutionScope]) -> Array[ExecutionScope] + def plan_scopes(scopes) + scopes.delete_if { _1.objects.empty? } + scopes.freeze + return scopes if scopes.empty? + + scopes.each do |exec_scope| + # invoke planning hooks from bottom-up to bubble configuration... + build_execution_tree(exec_scope).reverse_each do |exec_field| + exec_field.resolver.plan(exec_field, @context) + end + end + + scopes + end + + private + + # Fast path: just build AST nodes grouped by key without defer overhead. + #: ( + #| singleton(GraphQL::Schema::Member) parent_type, + #| Array[selection_node] selections, + #| ?map: Hash[String, Array[GraphQL::Language::Nodes::Field]], + #| ) -> Hash[String, Array[GraphQL::Language::Nodes::Field]] + def selections_grouped_by_key(parent_type, selections, map: Hash.new { |h, k| h[k] = [] }) + types = @context.types + selections.each do |node| + next if node_skipped?(node) + + case node + when GraphQL::Language::Nodes::Field + map[(node.alias || node.name).freeze] << node + when GraphQL::Language::Nodes::InlineFragment + fragment_type = node.type ? types.type(node.type.name) : parent_type + if parent_type_possible?(fragment_type, parent_type) + selections_grouped_by_key(parent_type, node.selections, map: map) + end + when GraphQL::Language::Nodes::FragmentSpread + fragment = @executor.fragments.fetch(node.name) + fragment_type = types.type(fragment.type.name) + if parent_type_possible?(fragment_type, parent_type) + selections_grouped_by_key(parent_type, fragment.selections, map: map) + end + end + end + + map + end + + # Incremental path: wraps each field node in an incremental selection carrying the enclosing `@defer` usage. + #: ( + #| singleton(GraphQL::Schema::Member) parent_type, + #| Array[selection_node] selections, + #| ?map: Hash[String, Array[Incremental::Selection]], + #| ?defer_usage: Incremental::DeferUsage?, + #| ) -> Hash[String, Array[Incremental::Selection]] + def incremental_selections_grouped_by_key(parent_type, selections, map: Hash.new { |h, k| h[k] = [] }, defer_usage: nil) + types = @context.types + selections.each do |node| + next if node_skipped?(node) + + case node + when GraphQL::Language::Nodes::Field + map[(node.alias || node.name).freeze] << Incremental::Selection.new(node, defer_usage:) + when GraphQL::Language::Nodes::InlineFragment + fragment_type = node.type ? types.type(node.type.name) : parent_type + if parent_type_possible?(fragment_type, parent_type) + incremental_selections_grouped_by_key(parent_type, node.selections, map: map, defer_usage: defer_usage_for(node, defer_usage) || defer_usage) + end + when GraphQL::Language::Nodes::FragmentSpread + fragment = @executor.fragments.fetch(node.name) + fragment_type = types.type(fragment.type.name) + if parent_type_possible?(fragment_type, parent_type) + incremental_selections_grouped_by_key(parent_type, fragment.selections, map: map, defer_usage: defer_usage_for(node, defer_usage) || defer_usage) + end + end + end + + map + end + + #: (selection_node node) -> bool + def node_skipped?(node) + return false if node.directives.empty? + + node.directives.any? do |directive| + if directive.name == "skip" + argument_value(directive.arguments.first) + elsif directive.name == "include" + !argument_value(directive.arguments.first) + elsif !ENGINE_RUNTIME_DIRECTIVES.include?(directive.name) + @has_runtime_directives = true + false + end + end + end + + #: (untyped) -> untyped + def argument_value(arg) + value = arg.value + if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) + @executor.variables[value.name] + else + value + end + end + + #: ( + #| singleton(GraphQL::Schema::Member) fragment_type, + #| singleton(GraphQL::Schema::Member) parent_type, + #| ) -> bool + def parent_type_possible?(fragment_type, parent_type) + fragment_type == parent_type || @context.types.possible_types(fragment_type).include?(parent_type) + end + + #: (GraphQL::Language::Nodes::InlineFragment | GraphQL::Language::Nodes::FragmentSpread, Incremental::DeferUsage?) -> Incremental::DeferUsage? + def defer_usage_for(node, parent) + return nil if node.directives.empty? + + directive = node.directives.find { _1.name == "defer" } + return nil unless directive + + condition = directive.arguments.find { _1.name == "if" } + return nil if condition && argument_value(condition) == false + + label_arg = directive.arguments.find { _1.name == "label" } + label = label_arg ? argument_value(label_arg) : nil + label = nil unless label.is_a?(String) + + Incremental::DeferUsage.new(label, parent:) + end + + #: ( + #| ExecutionScope, + #| ?Array[ExecutionField[untyped]], + #| ) -> Array[ExecutionField[untyped]] + def build_execution_tree(exec_scope, ordered_fields = []) + if @executor.incremental? + build_incremental_execution_fields(exec_scope, ordered_fields) + else + build_execution_fields(exec_scope, ordered_fields) + end + + exec_scope.fields.freeze + ordered_fields + end + + # Fast path for building basic scope fields without incremental info (majority use case) + #: (ExecutionScope, Array[ExecutionField[untyped]]) -> void + def build_execution_fields(exec_scope, ordered_fields) + @has_runtime_directives = false + selections_by_key = selections_grouped_by_key(exec_scope.parent_type, exec_scope.selections) + has_runtime_directives = @has_runtime_directives + + selections_by_key.each do |key, nodes| + exec_field = build_execution_field(key, nodes, exec_scope, has_runtime_directives) + add_execution_field_branch(exec_field, ordered_fields) + end + end + + # Incremental path for partitioning fields into deferred delivery groups, and registering deferred scopes. + #: (ExecutionScope, Array[ExecutionField[untyped]]) -> void + def build_incremental_execution_fields(exec_scope, ordered_fields) + @has_runtime_directives = false + + parent_usages = EMPTY_SET + selections_by_key = if exec_scope.is_a?(Incremental::DeferredExecutionScope) + parent_usages = exec_scope.defer_usages + exec_scope.field_selections + elsif (parent_field = exec_scope.parent_field) + map = Hash.new { |h, k| h[k] = [] } + parent_incremental_selections = parent_field.incremental_selections #: as !nil + parent_incremental_selections.each do |incremental_selection| + incremental_selections_grouped_by_key( + exec_scope.parent_type, + incremental_selection.node.selections, + map: map, + defer_usage: incremental_selection.defer_usage, + ) + end + + parent_usages = Incremental::Partitioner.defer_usages_for(parent_incremental_selections) + map + else + incremental_selections_grouped_by_key(exec_scope.parent_type, exec_scope.selections) + end + + has_runtime_directives = @has_runtime_directives + base_selections, selections_by_defer_usage = Incremental::Partitioner.partition(selections_by_key, parent_usages:) + + selections_by_defer_usage.each do |defer_usage, incremental_selections| + incremental = @executor.incremental #: as !nil + incremental.register_deferred_scope(exec_scope, incremental_selections, defer_usage) + end + + base_selections.each do |key, incremental_selections| + exec_field = build_execution_field(key, incremental_selections.map(&:node), exec_scope, has_runtime_directives, incremental_selections:) + add_execution_field_branch(exec_field, ordered_fields) + end + end + + #: (ExecutionField[untyped], Array[ExecutionField[untyped]]) -> void + def add_execution_field_branch(exec_field, ordered_fields) + exec_field.scope.fields[exec_field.key] = exec_field + ordered_fields << exec_field + + return_type = exec_field.type.unwrap + return if return_type.kind.leaf? || return_type.kind.abstract? + + next_scope = ExecutionScope.new( + executor: @executor, + parent_type: return_type, + parent_field: exec_field, + selections: exec_field.selections, + deferred: exec_field.scope.deferred?, + objects: [], + results: [], + ) + @planned_scopes_by_field[exec_field] = next_scope + build_execution_tree(next_scope, ordered_fields) + end + + #: ( + #| String key, + #| Array[GraphQL::Language::Nodes::Field] nodes, + #| ExecutionScope exec_scope, + #| ?bool has_runtime_directives, + #| ?incremental_selections: Array[Incremental::Selection]?, + #| ) -> ExecutionField[untyped] + def build_execution_field(key, nodes, exec_scope, has_runtime_directives = false, incremental_selections: nil) + first_node = nodes.first #: as !nil + node_name = first_node.name + + definition = @context.types.field(exec_scope.parent_type, node_name) + resolver = if definition.is_a?(HasBreadthResolver::Field) && definition.breadth_resolver + definition.breadth_resolver + elsif definition.introspection? + if definition.graphql_name == TYPENAME_FIELD + Introspection::TYPENAME_RESOLVER + elsif exec_scope.parent_type.equal?(@context.types.query_root) + Introspection::ENTRYPOINT_RESOLVERS.dig(definition.graphql_name) + else + Introspection::TYPE_RESOLVERS.dig(exec_scope.parent_type.graphql_name, definition.graphql_name) + end + else + @resolvers.dig(exec_scope.parent_type.graphql_name, definition.graphql_name) + end + + unless resolver + raise NotImplementedError, "No field resolver for '#{exec_scope.parent_type.graphql_name}.#{definition.graphql_name}'" + end + + directives = exec_scope.parent_field&.directives || EMPTY_ARRAY + if has_runtime_directives + nodes.each do |node| + node.directives.each do |directive| + next if ENGINE_RUNTIME_DIRECTIVES.include?(directive.name) + + directives = directives.dup if directives.frozen? + directives << build_execution_directive(directive, depth: exec_scope.depth + 1) + end + end + directives.freeze + end + + ExecutionField.new( + key, + nodes: nodes, + scope: exec_scope, + definition: definition, + resolver: resolver, + directives: directives, + incremental_selections: incremental_selections, + ) + end + + #: (GraphQL::Language::Nodes::Directive, ?depth: Integer) -> ExecutionDirective + def build_execution_directive(node, depth: 0) + definition = @schema.directives[node.name] + + resolver = if definition.respond_to?(:breadth_resolver) && definition.breadth_resolver + definition.breadth_resolver + else + @resolvers["@#{definition.graphql_name}"] + end + ExecutionDirective.build( + executor: @executor, + node: node, + definition: definition, + resolver: resolver, + depth: depth, + ) + end + end + end + end +end diff --git a/lib/graphql/breadth_exec/executor/execution_promise.rb b/lib/graphql/breadth/executor/execution_promise.rb similarity index 99% rename from lib/graphql/breadth_exec/executor/execution_promise.rb rename to lib/graphql/breadth/executor/execution_promise.rb index 0d491ff..8eca1bb 100644 --- a/lib/graphql/breadth_exec/executor/execution_promise.rb +++ b/lib/graphql/breadth/executor/execution_promise.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class Executor class ExecutionPromise class Deferred diff --git a/lib/graphql/breadth/executor/execution_scope.rb b/lib/graphql/breadth/executor/execution_scope.rb new file mode 100644 index 0000000..aaa88f3 --- /dev/null +++ b/lib/graphql/breadth/executor/execution_scope.rb @@ -0,0 +1,181 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + class ExecutionScope + include LazyElement + include HasAttributes + + #: Executor + attr_reader :executor + + #: singleton(GraphQL::Schema::Object) + attr_reader :parent_type + + #: ExecutionField[untyped]? + attr_reader :parent_field + + #: Array[selection_node] + attr_reader :selections + + #: Array[untyped] + attr_reader :objects + + #: Array[untyped] + attr_reader :results + + #: AbstractExecutionScope? + attr_reader :abstraction + + #: Array[String] + attr_reader :path + + #: ExecutionScope? + attr_reader :parent + + #: Hash[String, ExecutionField[untyped]] + attr_reader :fields + + #: bool + attr_writer :executed + + #: ( + #| executor: Executor, + #| parent_type: singleton(GraphQL::Schema::Object), + #| selections: Array[selection_node], + #| objects: Array[untyped], + #| results: Array[untyped], + #| ?abstraction: AbstractExecutionScope?, + #| ?parent_field: ExecutionField[untyped]?, + #| ?path: Array[String], + #| ?parent: ExecutionScope?, + #| ?deferred: bool, + #| ) -> void + def initialize( + executor:, + parent_type:, + selections:, + objects:, + results:, + abstraction: nil, + parent_field: nil, + path: [], + parent: nil, + deferred: false + ) + super() + @executor = executor + @parent_type = parent_type + @parent_field = parent_field + @selections = selections + @objects = objects + @results = results + @abstraction = abstraction + @path = (parent_field ? parent_field.path : path).freeze + @parent = parent_field ? parent_field.scope : parent + @fields = {} + @deferred = deferred + @executed = false + @root = nil + @planning_root = nil + @aborted = false + end + + #: -> GraphQL::Query::Context + def context + @executor.context + end + + #: -> ExecutionScope + def root + @root ||= @parent ? @parent.root : self + end + + #: -> ExecutionScope + def planning_root + @planning_root ||= (abstraction || @parent.nil?) ? self : @parent.planning_root + end + + #: -> Integer + def depth + @path.length + end + + #: -> Array[String] + def schema_path + if (field = parent_field) + field.schema_path + else + EMPTY_ARRAY + end + end + + #: (Integer) -> error_path + def object_path(index) + @executor.paths.object_path(self, index) + end + + #: -> bool + def executed? + @executed + end + + #: -> bool + def abort! + @aborted = true + end + + #: -> bool + def aborted? + @aborted + end + + #: -> bool + def deferred? + @deferred + end + + #: -> ExecutionScope? + def deferred_root + return self if deferred? + + exec_scope = @parent + while exec_scope + return exec_scope if exec_scope.deferred? + + exec_scope = exec_scope.parent + end + + nil + end + + #: -> bool + def aborted_subtree? + return true if @aborted + + exec_field = parent_field #: ExecutionField[untyped]? + while exec_field + if exec_field.scope.aborted? + abort! + return true + end + exec_field = exec_field.scope.parent_field + end + false + end + + #: -> bool + def has_authorized_objects? + @objects.frozen? && !@objects.empty? + end + + #: -> String + def inspect + "#" + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/has_attributes.rb b/lib/graphql/breadth/executor/has_attributes.rb new file mode 100644 index 0000000..eea4648 --- /dev/null +++ b/lib/graphql/breadth/executor/has_attributes.rb @@ -0,0 +1,34 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + module HasAttributes + def initialize(...) + super(...) + @attributes = nil + end + + #: () -> Hash[untyped, untyped] + def attributes + @attributes ||= {} + end + + #: (untyped, ?default: untyped) -> untyped + def attribute(key, default: nil) + return default unless @attributes + + @attributes.fetch(key, default) + end + + #: (untyped) -> bool + def attribute?(key) + return false unless @attributes + + @attributes.key?(key) + end + end + end + end +end diff --git a/lib/graphql/breadth_exec/executor/input_formatter.rb b/lib/graphql/breadth/executor/input_formatter.rb similarity index 98% rename from lib/graphql/breadth_exec/executor/input_formatter.rb rename to lib/graphql/breadth/executor/input_formatter.rb index 1cebb72..015f91e 100644 --- a/lib/graphql/breadth_exec/executor/input_formatter.rb +++ b/lib/graphql/breadth/executor/input_formatter.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class Executor class InputFormatter #: type input_hash = Hash[String | Symbol, untyped] @@ -290,9 +290,9 @@ def coerce_argument_values(member, node, variables: @variables, state: @default_ end if !argument_errors.empty? && argument_errors.any?(InputCoercionError) - # If we got here, then GraphQL Ruby passed something that BreadthExec didn't (implies a BreadthExec bug) + # If we got here, then GraphQL Ruby passed something that Breadth didn't (implies a Breadth bug) # Report the error rather than raising so that shimmed fields can still use GraphQL Ruby values - GraphQL::BreadthExec.report_error(InputValidationErrorSet.new(errors: argument_errors.grep(InputCoercionError))) + GraphQL::Breadth.report_error(InputValidationErrorSet.new(errors: argument_errors.grep(InputCoercionError))) arguments = EMPTY_OBJECT else # otherwise, validate the resolved field argument structure @@ -387,7 +387,7 @@ def coerce_input_value(input_value, type, state: State.new) elsif type.kind.leaf? result = begin type.coerce_input(input_value, @context) - rescue GraphQL::ExecutionError, GraphQL::BreadthExec::ExecutionError => e + rescue GraphQL::ExecutionError, GraphQL::Breadth::ExecutionError => e return state.add_coercion_error(e.message, extensions: e.extensions) rescue StandardError => e return state.add_coercion_error("Expected type \"#{type.graphql_name}\".") @@ -533,7 +533,7 @@ def value_from_ast(value_node, type, variables: nil, state: State.new) result = begin type.coerce_input(value_node, @context) - rescue GraphQL::ExecutionError, GraphQL::BreadthExec::ExecutionError => e + rescue GraphQL::ExecutionError, GraphQL::Breadth::ExecutionError => e return state.add_coercion_error(e.message, extensions: e.extensions) rescue StandardError => e return state.add_coercion_error("Expected type \"#{type.graphql_name}\".") @@ -606,7 +606,7 @@ def validate_value(member, value, state: State.new, as: nil) unless member.validators.empty? member.validators.each do |validator| # Always validate statically with no object. - # BreadthExec does not support object-contextual arguments. + # Breadth does not support object-contextual arguments. result = validator.validate(nil, @context, value) next if result.nil? diff --git a/lib/graphql/breadth/executor/lazy_element.rb b/lib/graphql/breadth/executor/lazy_element.rb new file mode 100644 index 0000000..3606773 --- /dev/null +++ b/lib/graphql/breadth/executor/lazy_element.rb @@ -0,0 +1,127 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + # @requires_ancestor: Kernel + module LazyElement + LAZY_STATE_PRELOADING = :preloading + LAZY_STATE_LOCKED = :locked + + def initialize(...) + super(...) + @sync_preloads = nil + @lazy_preloads = nil + @lazy_state = LAZY_STATE_PRELOADING + @preload_promises = nil + end + + #: () -> Array[ExecutionPromise] + def preload_promises + @preload_promises ||= [] + end + + #: () -> bool + def lazy_preloads? + @preload_promises && !@preload_promises.empty? + end + + #: () -> bool + def allows_preload? + @lazy_state == LAZY_STATE_PRELOADING + end + + #: () { (LazyElement) -> void } -> void + def on_preload(&block) + unless allows_preload? + Kernel.raise LazySequencingError.new(lazy_element: self, method_name: "on_preload") + end + + @sync_preloads ||= [] + @sync_preloads << block + end + + #: ( + #| singleton(GraphQL::Breadth::LazyLoader), + #| ?args: loader_args?, + #| ?keys: Array[untyped]?, + #| ) -> ExecutionPromise + def preload(loader_class, args: nil, keys: nil) + unless allows_preload? + Kernel.raise LazySequencingError.new(lazy_element: self, method_name: "preload") + end + + if keys + loader = executor.lazy_loader_for(loader_class, args) + loader.load(element: self, keys: keys).with_registry(preload_promises) + else + @lazy_preloads ||= {} + deferred = (@lazy_preloads[[loader_class, args]] ||= ExecutionPromise::Deferred.new(registry: preload_promises)) + deferred.promise + end + end + + #: () -> void + def preload! + while @sync_preloads && !@sync_preloads.empty? + sync_preloads = @sync_preloads + @sync_preloads = nil + sync_preloads.each(&:call) + end + + return unless @lazy_preloads + + preloads = @lazy_preloads + @lazy_preloads = nil + + preloads.each do |(loader_class, args), pre_deferred| + loader = executor.lazy_loader_for(loader_class, args) + loader.load(element: self, keys: objects, pre_deferred: pre_deferred) + end + end + + #: (Array[ExecutionPromise]) -> ExecutionPromise + def await_all(promises) + promise = ExecutionPromise.all(promises) + registry = promises.first&.registry + + if registry && promises.all? { _1.registry.equal?(registry) } + promise.with_registry(registry) + else + promise + end + end + + #: () -> Array[untyped] + def objects + Kernel.raise NotImplementedError + end + + #: () -> Executor + def executor + Kernel.raise NotImplementedError + end + + #: () -> Array[String] + def path + Kernel.raise NotImplementedError + end + + #: () -> void + def lazy_state_locked! + Kernel.raise LazyStateTransitionError.new(@lazy_state, LAZY_STATE_LOCKED) unless lazy_state_lockable? + + @lazy_state = LAZY_STATE_LOCKED + end + + private + + #: () -> bool + def lazy_state_lockable? + @lazy_state == LAZY_STATE_PRELOADING + end + end + end + end +end diff --git a/lib/graphql/breadth/executor/path_formatter.rb b/lib/graphql/breadth/executor/path_formatter.rb new file mode 100644 index 0000000..ba7fe1b --- /dev/null +++ b/lib/graphql/breadth/executor/path_formatter.rb @@ -0,0 +1,120 @@ +# typed: true +# frozen_string_literal: true + +module GraphQL + module Breadth + class Executor + # Builds paths to specific breadth object positions as exact object paths, ex: + # - Scope path (namespace): ["products", "variants"] + # - Object path (exact path): ["products", 0, "variants", 1] + # Object pathing assembles breadth indices for all scopes ascending from the targeted scope, ex: + # { + # => { + # 0 => [0, 1, 1, 2], # << zero index always maps objects in this scope to objects in the parent scope + # 1 => [0, 0, 1, 0], # << maps a first-order list in this scope + # 2 => [ ... ], # << maps a second-order list in this scope, etc... + # }, + # => { ... }, + # } + # All numeric indexing arrays have size matching the number of objects in their scope. + # To read an object path, we follow the Nth position of each scope's index map ascending up the scope tree. + # Building these indices adds execution overhead that isn't needed regularly, so this is only done as part of deferred work. + class PathFormatter + #: Hash[Executor::ExecutionScope, Hash[Integer, Array[Integer]]] + attr_reader :indices_by_scope + + def initialize + @indices_by_scope = Hash.new { |h1, exec_scope| h1[exec_scope] = Hash.new { |h2, index| h2[index] = [] } }.compare_by_identity + end + + #: (Executor::ExecutionScope, Integer) -> error_path + def object_path(exec_scope, index) + current_path = [] + + current_scope = exec_scope #: Executor::ExecutionScope? + breadth_index = index + while current_scope + # index the scope unless it has already been done + scope_indices = @indices_by_scope[current_scope] + index_scope(current_scope, scope_indices) if scope_indices.empty? + + # loop backward through all the scope's indices... + # - all scopes have at least one index that defines the parent scope position + # - list scopes have additional indices for each layer of list wrapping + i = scope_indices.length - 1 + while i >= 0 + if i.zero? + # at the lowest index, recalibrate for the next highest scope + breadth_index = scope_indices[i][breadth_index] #: as Integer + else + # higher indices add list positions to the current path + current_path.prepend(scope_indices[i][breadth_index]) + end + i -= 1 + end + + # before going up a scope, add the parent field key into current path + key = current_scope.parent_field&.key + current_path.prepend(key) if key + current_scope = current_scope.parent + end + + current_path + end + + private + + #: (Executor::ExecutionScope, Hash[Integer, Array[Integer]]) -> void + def index_scope(exec_scope, scope_indices) + raise ArgumentError, "Scope must not be indexed" unless scope_indices.empty? + raise ArgumentError, "Scope must be executed" unless exec_scope.executed? + + parent_objects = exec_scope.objects + current_type = exec_scope.parent_type + if (parent_field = exec_scope.parent_field) + parent_objects = parent_field.result #: as !nil + current_type = parent_field.type + end + + object_path = [] + i = 0 + while i < parent_objects.length + object_path[0] = i + build_indices(current_type, parent_objects[i], object_path, scope_indices) + i += 1 + end + + scope_indices.each(&:freeze) + scope_indices.freeze + end + + #: (singleton(GraphQL::Schema::Member), untyped, Array[Integer], Hash[Integer, Array[Integer]]) -> void + def build_indices(current_type, object, object_path, next_indices) + return if object.nil? || object.is_a?(ExecutionError) + + if current_type.list? + raise ImplementationError, "Expected Array, got #{object.class}" unless object.is_a?(Array) + + current_type = Util.unwrap_non_null(current_type) + + i = 0 + while i < object.length + object_path << i + build_indices(current_type.of_type, object[i], object_path, next_indices) + object_path.pop + i += 1 + end + else + i = 0 + while i < object_path.length + next_index = object_path[i] #: as !nil + next_map = next_indices[i] #: as !nil + next_map << next_index + i += 1 + end + end + end + end + end + end +end diff --git a/lib/graphql/breadth_exec/field_resolvers.rb b/lib/graphql/breadth/field_resolvers.rb similarity index 99% rename from lib/graphql/breadth_exec/field_resolvers.rb rename to lib/graphql/breadth/field_resolvers.rb index b39e359..d32f88e 100644 --- a/lib/graphql/breadth_exec/field_resolvers.rb +++ b/lib/graphql/breadth/field_resolvers.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class FieldResolver #: (Executor::ExecutionField[untyped], GraphQL::Query::Context) -> void def plan(_exec_field, _ctx) diff --git a/lib/graphql/breadth_exec/has_breadth_resolver.rb b/lib/graphql/breadth/has_breadth_resolver.rb similarity index 99% rename from lib/graphql/breadth_exec/has_breadth_resolver.rb rename to lib/graphql/breadth/has_breadth_resolver.rb index a6bbf84..b4dd0b8 100644 --- a/lib/graphql/breadth_exec/has_breadth_resolver.rb +++ b/lib/graphql/breadth/has_breadth_resolver.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module HasBreadthResolver module Field CORRECT_USAGE = "HasBreadthResolver::Field must be included in GraphQL::Schema::Field" diff --git a/lib/graphql/breadth_exec/incremental.rb b/lib/graphql/breadth/incremental.rb similarity index 95% rename from lib/graphql/breadth_exec/incremental.rb rename to lib/graphql/breadth/incremental.rb index 7905d45..aefd681 100644 --- a/lib/graphql/breadth_exec/incremental.rb +++ b/lib/graphql/breadth/incremental.rb @@ -11,7 +11,7 @@ require_relative "incremental/context" module GraphQL - module BreadthExec + module Breadth module Incremental end end diff --git a/lib/graphql/breadth_exec/incremental/context.rb b/lib/graphql/breadth/incremental/context.rb similarity index 99% rename from lib/graphql/breadth_exec/incremental/context.rb rename to lib/graphql/breadth/incremental/context.rb index a075664..c334b38 100644 --- a/lib/graphql/breadth_exec/incremental/context.rb +++ b/lib/graphql/breadth/incremental/context.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class Context #: Executor diff --git a/lib/graphql/breadth_exec/incremental/defer_usage.rb b/lib/graphql/breadth/incremental/defer_usage.rb similarity index 94% rename from lib/graphql/breadth_exec/incremental/defer_usage.rb rename to lib/graphql/breadth/incremental/defer_usage.rb index 13fc4c6..d0565cf 100644 --- a/lib/graphql/breadth_exec/incremental/defer_usage.rb +++ b/lib/graphql/breadth/incremental/defer_usage.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class DeferUsage #: String? diff --git a/lib/graphql/breadth_exec/incremental/deferred_delivery.rb b/lib/graphql/breadth/incremental/deferred_delivery.rb similarity index 97% rename from lib/graphql/breadth_exec/incremental/deferred_delivery.rb rename to lib/graphql/breadth/incremental/deferred_delivery.rb index 508f5ee..c0cb49f 100644 --- a/lib/graphql/breadth_exec/incremental/deferred_delivery.rb +++ b/lib/graphql/breadth/incremental/deferred_delivery.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class DeferredDelivery #: error_path diff --git a/lib/graphql/breadth_exec/incremental/deferred_execution_scope.rb b/lib/graphql/breadth/incremental/deferred_execution_scope.rb similarity index 98% rename from lib/graphql/breadth_exec/incremental/deferred_execution_scope.rb rename to lib/graphql/breadth/incremental/deferred_execution_scope.rb index eae7a27..b290ccf 100644 --- a/lib/graphql/breadth_exec/incremental/deferred_execution_scope.rb +++ b/lib/graphql/breadth/incremental/deferred_execution_scope.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class DeferredExecutionScope < Executor::ExecutionScope #: Executor::ExecutionScope diff --git a/lib/graphql/breadth_exec/incremental/partitioner.rb b/lib/graphql/breadth/incremental/partitioner.rb similarity index 99% rename from lib/graphql/breadth_exec/incremental/partitioner.rb rename to lib/graphql/breadth/incremental/partitioner.rb index 2518d2c..9c305ed 100644 --- a/lib/graphql/breadth_exec/incremental/partitioner.rb +++ b/lib/graphql/breadth/incremental/partitioner.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class Partitioner class << self diff --git a/lib/graphql/breadth_exec/incremental/publisher.rb b/lib/graphql/breadth/incremental/publisher.rb similarity index 98% rename from lib/graphql/breadth_exec/incremental/publisher.rb rename to lib/graphql/breadth/incremental/publisher.rb index ca3c2ba..a66254f 100644 --- a/lib/graphql/breadth_exec/incremental/publisher.rb +++ b/lib/graphql/breadth/incremental/publisher.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class Publisher def initialize diff --git a/lib/graphql/breadth_exec/incremental/result.rb b/lib/graphql/breadth/incremental/result.rb similarity index 97% rename from lib/graphql/breadth_exec/incremental/result.rb rename to lib/graphql/breadth/incremental/result.rb index 151e02a..ca3167e 100644 --- a/lib/graphql/breadth_exec/incremental/result.rb +++ b/lib/graphql/breadth/incremental/result.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class Result #: graphql_result diff --git a/lib/graphql/breadth_exec/incremental/selection.rb b/lib/graphql/breadth/incremental/selection.rb similarity index 95% rename from lib/graphql/breadth_exec/incremental/selection.rb rename to lib/graphql/breadth/incremental/selection.rb index 7745f81..41e4538 100644 --- a/lib/graphql/breadth_exec/incremental/selection.rb +++ b/lib/graphql/breadth/incremental/selection.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Incremental class Selection #: GraphQL::Language::Nodes::Field diff --git a/lib/graphql/breadth_exec/introspection.rb b/lib/graphql/breadth/introspection.rb similarity index 99% rename from lib/graphql/breadth_exec/introspection.rb rename to lib/graphql/breadth/introspection.rb index e77bbf0..a6588cd 100644 --- a/lib/graphql/breadth_exec/introspection.rb +++ b/lib/graphql/breadth/introspection.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth module Introspection module Schema class EndpointResolver < FieldResolver diff --git a/lib/graphql/breadth_exec/lazy_loader.rb b/lib/graphql/breadth/lazy_loader.rb similarity index 99% rename from lib/graphql/breadth_exec/lazy_loader.rb rename to lib/graphql/breadth/lazy_loader.rb index 5f6204b..6ca4378 100644 --- a/lib/graphql/breadth_exec/lazy_loader.rb +++ b/lib/graphql/breadth/lazy_loader.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth #: [ContextType < GraphQL::Query::Context] class LazyLoader class LazyFulfillment diff --git a/lib/graphql/breadth_exec/subscription_response_stream.rb b/lib/graphql/breadth/subscription_response_stream.rb similarity index 97% rename from lib/graphql/breadth_exec/subscription_response_stream.rb rename to lib/graphql/breadth/subscription_response_stream.rb index a35e1e7..3ca9396 100644 --- a/lib/graphql/breadth_exec/subscription_response_stream.rb +++ b/lib/graphql/breadth/subscription_response_stream.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class SubscriptionResponseStream include Enumerable diff --git a/lib/graphql/breadth_exec/tracer.rb b/lib/graphql/breadth/tracer.rb similarity index 99% rename from lib/graphql/breadth_exec/tracer.rb rename to lib/graphql/breadth/tracer.rb index 35a8ce2..5ff7b46 100644 --- a/lib/graphql/breadth_exec/tracer.rb +++ b/lib/graphql/breadth/tracer.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class Tracer #: (Executor, GraphQL::Query::Context) -> void def start(executor, context) diff --git a/lib/graphql/breadth_exec/util.rb b/lib/graphql/breadth/util.rb similarity index 93% rename from lib/graphql/breadth_exec/util.rb rename to lib/graphql/breadth/util.rb index a631604..6ffdee6 100644 --- a/lib/graphql/breadth_exec/util.rb +++ b/lib/graphql/breadth/util.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec + module Breadth class Util class << self #: (untyped value, ?stringify_keys: bool) -> untyped @@ -31,7 +31,7 @@ def initialize(identity) @identity = identity end - def class = ::GraphQL::BreadthExec::Util::NilLike + def class = ::GraphQL::Breadth::Util::NilLike def is_a?(type) = type === self def nil? = true def ! = true diff --git a/lib/graphql/breadth_exec/version.rb b/lib/graphql/breadth/version.rb similarity index 56% rename from lib/graphql/breadth_exec/version.rb rename to lib/graphql/breadth/version.rb index 80e01f4..27102f1 100644 --- a/lib/graphql/breadth_exec/version.rb +++ b/lib/graphql/breadth/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module GraphQL - module BreadthExec - VERSION = "0.0.1" + module Breadth + VERSION = "0.9.0" end end diff --git a/lib/graphql/breadth_exec/executor/abstract_execution_scope.rb b/lib/graphql/breadth_exec/executor/abstract_execution_scope.rb deleted file mode 100644 index 8757efd..0000000 --- a/lib/graphql/breadth_exec/executor/abstract_execution_scope.rb +++ /dev/null @@ -1,64 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class AbstractExecutionScope - #: singleton(GraphQL::Schema::Member) - attr_reader :parent_type - - #: ExecutionField[untyped] - attr_reader :parent_field - - #: Array[ExecutionScope] - attr_reader :scopes - - #: ( - #| parent_type: singleton(GraphQL::Schema::Member), - #| parent_field: ExecutionField[untyped], - #| scopes: Array[ExecutionScope], - #| ) -> void - def initialize(parent_type:, parent_field:, scopes:) - @parent_type = parent_type - @parent_field = parent_field - @scopes = scopes - @executed = false - end - - #: -> bool - def executed? - @executed - end - - #: -> Executor - def executor - @parent_field.executor - end - - #: -> ExecutionScope - def parent - @parent_field.scope - end - - #: -> Array[String] - def path - @parent_field.path - end - - #: -> Integer - def depth - @parent_field.depth - end - - #: -> Array[untyped] - def objects - @objects ||= @scopes.flat_map(&:objects) - end - - #: -> Array[untyped] - def results - @results ||= @scopes.flat_map(&:results) - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/error_formatter.rb b/lib/graphql/breadth_exec/executor/error_formatter.rb deleted file mode 100644 index 37bbef7..0000000 --- a/lib/graphql/breadth_exec/executor/error_formatter.rb +++ /dev/null @@ -1,171 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class ErrorFormatter - class State - #: Array[Hash[String, untyped]] - attr_reader :errors - - #: error_path - attr_reader :actual_path - - #: error_path - attr_reader :base_path - - #: (?error_path) -> void - def initialize(base_path = EMPTY_ARRAY) - @base_path = base_path - @actual_path = [] - @errors = [] - end - - #: -> error_path - def current_path - @base_path + @actual_path - end - end - - #: ( - #| executor: Executor, - #| invalidated_results: Hash[untyped, ExecutionError], - #| abstract_result_types: Hash[untyped, singleton(GraphQL::Schema::Object)], - #| ) -> void - def initialize(executor:, invalidated_results:, abstract_result_types:) - @executor = executor - @context = executor.context - @invalidated_results = invalidated_results - @abstract_result_types = abstract_result_types - end - - #: ( - #| singleton(GraphQL::Schema::Object), - #| Array[selection_node], - #| Hash[String, untyped], - #| ?error_path, - #| ) -> [Hash[String, untyped]?, Array[error_hash]] - def format_object(parent_type, selections, data, base_path = EMPTY_ARRAY) - return [data, EMPTY_ARRAY] if @invalidated_results.empty? - - state = State.new(base_path) - - if (err = @invalidated_results[data]) - add_formatted_error(err, state) - return [nil, state.errors] - end - - data = propagate_object_scope_errors(data, parent_type, selections, state) - [data, state.errors] - end - - private - - #: (untyped, singleton(GraphQL::Schema::Object), Array[selection_node], State) -> untyped - def propagate_object_scope_errors(raw_object, parent_type, selections, state) - return nil if raw_object.nil? - - selections.each do |node| - case node - when GraphQL::Language::Nodes::Field - field_key = node.alias || node.name - - state.actual_path << field_key - - begin - node_type = @context.types.field(parent_type, node.name).type - named_type = node_type.unwrap - raw_value = raw_object.fetch(field_key, Executor::UNDEFINED) - next if raw_value.equal?(Executor::UNDEFINED) - - raw_object[field_key] = if (err = @invalidated_results[raw_value]) - add_formatted_error(err, state) - nil - elsif node_type.list? - propagate_list_scope_errors(raw_value, node_type, node.selections, state) - elsif named_type.kind.leaf? - raw_value - else - propagate_object_scope_errors(raw_value, named_type, node.selections, state) - end - - return nil if node_type.non_null? && raw_object[field_key].nil? - ensure - state.actual_path.pop - end - - when GraphQL::Language::Nodes::InlineFragment - fragment_type = node.type ? @context.types.type(node.type.name) : parent_type - next unless result_of_type?(raw_object, parent_type, fragment_type) - - result = propagate_object_scope_errors(raw_object, fragment_type, node.selections, state) - return nil if result.nil? - - when GraphQL::Language::Nodes::FragmentSpread - fragment = @executor.fragments.fetch(node.name) - fragment_type = @context.types.type(fragment.type.name) - next unless result_of_type?(raw_object, parent_type, fragment_type) - - result = propagate_object_scope_errors(raw_object, fragment_type, fragment.selections, state) - return nil if result.nil? - end - end - - raw_object - end - - #: (Array[untyped]?, singleton(GraphQL::Schema::Member), Array[selection_node], State) -> Array[untyped]? - def propagate_list_scope_errors(raw_list, current_node_type, selections, state) - return nil if raw_list.nil? - - item_node_type = Util.unwrap_non_null(current_node_type).of_type - named_type = item_node_type.unwrap - - raw_list.map!.with_index do |raw_list_element, index| - state.actual_path << index - - begin - result = if (err = @invalidated_results[raw_list_element]) - add_formatted_error(err, state) - nil - elsif item_node_type.list? - propagate_list_scope_errors(raw_list_element, item_node_type, selections, state) - elsif named_type.kind.leaf? - raw_list_element - else - propagate_object_scope_errors(raw_list_element, named_type, selections, state) - end - - return nil if result.nil? && item_node_type.non_null? - - result - ensure - state.actual_path.pop - end - end - end - - #: (untyped, singleton(GraphQL::Schema::Member), singleton(GraphQL::Schema::Member)) -> bool - def result_of_type?(result, current_type, inquiry_type) - result_type = current_type.kind.abstract? ? @abstract_result_types[result] : current_type - raise ImplementationError, "No type annotation recorded for abstract result" if result_type.nil? - - if inquiry_type.kind.abstract? - @context.types.possible_types(inquiry_type).include?(result_type) - else - result_type == inquiry_type - end - end - - #: (ExecutionError, State) -> void - def add_formatted_error(error, state) - error.each do |err| - next if err.equal?(UNREPORTED_ERROR) - - state.errors << err.to_h.tap { _1["path"] = state.current_path } - @context.errors << err.cause if err.cause - end - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/execution_directive.rb b/lib/graphql/breadth_exec/executor/execution_directive.rb deleted file mode 100644 index d5c3a9b..0000000 --- a/lib/graphql/breadth_exec/executor/execution_directive.rb +++ /dev/null @@ -1,91 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class ExecutionDirective - include HasAttributes - - NOOP_DIRECTIVE_RESOLVER = NoopDirectiveResolver.new - - class << self - #: ( - #| executor: Executor, - #| node: GraphQL::Language::Nodes::Directive, - #| definition: singleton(GraphQL::Schema::Directive), - #| ?resolver: DirectiveResolver?, - #| ?depth: Integer, - #| ) -> ExecutionDirective - def build(executor:, node:, definition:, resolver: nil, depth: 0) - new( - executor: executor, - name: node.name, - node: node, - definition: definition, - resolver: resolver, - depth: depth, - ) - end - end - - #: String - attr_reader :name - - #: graphql_arguments - attr_reader :arguments - - #: singleton(GraphQL::Schema::Directive) - attr_reader :definition - - #: DirectiveResolver - attr_reader :resolver - - #: GraphQL::Language::Nodes::Directive? - attr_reader :node - - #: Integer - attr_reader :depth - - #: ( - #| executor: Executor, - #| name: String, - #| definition: singleton(GraphQL::Schema::Directive), - #| ?arguments: graphql_arguments?, - #| ?node: GraphQL::Language::Nodes::Directive?, - #| ?resolver: DirectiveResolver?, - #| ?depth: Integer, - #| ) -> void - def initialize(executor:, name:, definition:, arguments: nil, node: nil, resolver: nil, depth: 0) - @name = name - @node = node - @definition = definition - @resolver = resolver || NOOP_DIRECTIVE_RESOLVER - @depth = depth - @arguments, @argument_errors = if arguments.nil? - executor.input.coerce_argument_values(@definition, @node) - else - [arguments, EMPTY_ARRAY] - end - @mutable_arguments = nil - end - - #: () -> String - def inspect - "#" - end - - #: () -> graphql_arguments - def mutable_arguments - @mutable_arguments ||= Util.deep_copy(@arguments) - end - - #: () -> void - def validate! - unless @argument_errors.empty? - @argument_errors.each { _1.add_parent_node(@node) } if @node - raise InputValidationErrorSet.new(errors: @argument_errors) - end - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/execution_field.rb b/lib/graphql/breadth_exec/executor/execution_field.rb deleted file mode 100644 index edffb65..0000000 --- a/lib/graphql/breadth_exec/executor/execution_field.rb +++ /dev/null @@ -1,258 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class ExecutionField - include LazyElement - include HasAttributes - - LAZY_STATE_EXECUTING = :executing - - # Field's name as it appears in the response. - #: String - attr_reader :key - - # Field's name according to schema. - #: String - attr_reader :name - - #: GraphQL::Schema::Field? - attr_reader :definition - - #: Array[ExecutionDirective] - attr_reader :directives - - #: FieldResolver - attr_reader :resolver - - #: Array[GraphQL::Language::Nodes::Field] - attr_reader :nodes - - # Only populated during incremental (`@defer`) execution; nil on the hot path. - #: Array[Incremental::Selection]? - attr_reader :incremental_selections - - #: graphql_arguments - attr_reader :arguments - - #: ExecutionScope - attr_accessor :scope - - #: singleton(GraphQL::Schema::Member) - attr_accessor :type - - #: untyped - attr_accessor :result - - #: ( - #| String, - #| nodes: Array[GraphQL::Language::Nodes::Field], - #| scope: ExecutionScope, - #| definition: GraphQL::Schema::Field, - #| resolver: FieldResolver, - #| ?directives: Array[ExecutionDirective], - #| ?incremental_selections: Array[Incremental::Selection]?, - #| ) -> void - def initialize(key, nodes:, scope:, definition:, resolver:, directives: EMPTY_ARRAY, incremental_selections: nil) - super() - @key = key.freeze - @scope = scope - @definition = definition - @directives = directives.freeze - @resolver = resolver - @name = definition.graphql_name.freeze - @nodes = nodes.freeze - @type = definition.type - @result = nil - @arguments, @argument_errors = executor.input.coerce_argument_values(@definition, @nodes.first) - @mutable_arguments = nil - @incremental_selections = incremental_selections - @path = nil - @schema_path = nil - end - - #: () -> Array[ObjectType] - def objects - @scope.objects - end - - #: () -> GraphQL::Query::Context - def context - @scope.context - end - - #: () -> singleton(GraphQL::Schema::Member) - def parent_type - @scope.parent_type - end - - #: () -> Executor - def executor - @scope.executor - end - - #: () -> ExecutionScope - def root - @scope.root - end - - #: () -> ExecutionScope - def planning_root - @scope.planning_root - end - - #: -> Integer - def depth - path.length - end - - #: () -> Array[String] - def schema_path - @schema_path ||= [*@scope.schema_path, name].freeze - end - - #: ( - #| loader_class: singleton(LazyLoader), - #| keys: Array[untyped], - #| ?args: loader_args?, - #| ?eager_values: Hash[untyped, untyped]?, - #| ?load_nil_keys: bool, - #| ) -> ExecutionPromise - def lazy(loader_class:, keys:, args: nil, eager_values: nil, load_nil_keys: false) - unless allows_lazy? - raise LazySequencingError.new(lazy_element: self, method_name: "lazy") - end - - executor.lazy_loader_for(loader_class, args).load( - element: self, - keys: keys, - eager_values: eager_values, - load_nil_keys: load_nil_keys, - ) - end - - #: (Array[ExecutionPromise]) -> ExecutionPromise - def await_all(promises) - super - end - - #: () -> bool - def allows_lazy? - @lazy_state == LAZY_STATE_EXECUTING - end - - #: () -> Array[String] - def path - @path ||= [*@scope.path, @key].freeze - end - - #: (Integer) -> error_path - def object_path(index) - path = @scope.object_path(index) - path << @key - path - end - - #: [T] () { (untyped) -> (T | ExecutionError) } -> Array[T | ExecutionError] - def map_objects(&block) - objects.map do |obj| - yield(obj) - rescue StandardError => e - handle_or_reraise(e) - end - end - - #: [T] () { (untyped, Integer) -> (T | ExecutionError) } -> Array[T | ExecutionError] - def map_objects_with_index(&block) - objects.map.with_index do |obj, index| - yield(obj, index) - rescue StandardError => e - handle_or_reraise(e) - end - end - - #: [T] (T) -> Array[T] - def resolve_all(value) - value = case value - when StandardError - handle_or_reraise(value) - else - value - end - Array.new(objects.length, value) - end - - #: (Exception) -> ExecutionError - def handle_or_reraise(error) - executor.handle_or_reraise(error, exec_field: self) - end - - #: () -> Array[selection_node] - def selections - if @nodes.length > 1 - @nodes.flat_map(&:selections) - else - @nodes.first.selections - end - end - - #: () -> graphql_arguments - def mutable_arguments - @mutable_arguments ||= Util.deep_copy(arguments) - end - - #: () -> void - def validate! - unless @argument_errors.empty? - node = @nodes.first - @argument_errors.each { _1.add_parent_node(node) } - raise ExecutionErrorSet.new(exec_field: self, errors: @argument_errors) - end - end - - #: () -> bool - def lazy_result? - @result.is_a?(ExecutionPromise) - end - - #: () -> bool - def has_result? - !@result.nil? - end - - #: () -> bool - def propagates_null? - current_type = @type - return false unless current_type.kind.wraps? - - while current_type.list? - return false unless current_type.non_null? - - current_type = Util.unwrap_non_null(current_type).of_type - end - - current_type.non_null? - end - - #: () -> String - def inspect - alias_prefix = key == name ? "" : "#{key} => " - "#" - end - - def lazy_state_executing! - raise LazyStateTransitionError.new(@lazy_state, LAZY_STATE_EXECUTING) unless @lazy_state == LAZY_STATE_PRELOADING - - @lazy_state = LAZY_STATE_EXECUTING - end - - private - - #: () -> bool - def lazy_state_lockable? - @lazy_state == LAZY_STATE_PRELOADING || @lazy_state == LAZY_STATE_EXECUTING - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/execution_planner.rb b/lib/graphql/breadth_exec/executor/execution_planner.rb deleted file mode 100644 index cb1e740..0000000 --- a/lib/graphql/breadth_exec/executor/execution_planner.rb +++ /dev/null @@ -1,396 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class ExecutionPlanner - QUERY_OPERATION = "query".freeze - MUTATION_OPERATION = "mutation".freeze - SUBSCRIPTION_OPERATION = "subscription".freeze - TYPENAME_FIELD = "__typename".freeze - ENGINE_RUNTIME_DIRECTIVES = ["skip", "include", "defer", "stream"].freeze - - class << self - #: (String, GraphQL::Query::Context) -> singleton(GraphQL::Schema::Object) - def root_type_for_operation(operation_type, context) - case operation_type - when QUERY_OPERATION - context.types.query_root - when MUTATION_OPERATION - context.types.mutation_root - when SUBSCRIPTION_OPERATION - context.types.subscription_root - else - raise ArgumentError, "unexpected root type name: #{operation_type}" - end - end - end - - #: ( - #| executor: Executor, - #| resolvers: Hash[String, Hash[String, resolver]], - #| ) -> void - def initialize(executor:, resolvers:) - @executor = executor - @resolvers = resolvers - @schema = @executor.schema - @context = @executor.context - @planned_scopes_by_field = {}.compare_by_identity - @has_runtime_directives = false - end - - #: (ExecutionField[untyped]) -> ExecutionScope? - def planned_scope_for(exec_field) - @planned_scopes_by_field[exec_field] - end - - #: (String) -> singleton(GraphQL::Schema::Object) - def root_type_for_operation(operation_type) - self.class.root_type_for_operation(operation_type, @context) - end - - #: (GraphQL::Language::Nodes::OperationDefinition) -> Array[ExecutionDirective] - def root_directives_for_operation(operation) - operation.directives.map { |node| build_execution_directive(node, depth: 0) } - end - - #: ( - #| GraphQL::Language::Nodes::OperationDefinition, - #| root_object: untyped, - #| result: graphql_result, - #| ) -> Array[ExecutionScope] - def root_scopes_for_operation(operation, root_object:, result:) - case operation.operation_type - when QUERY_OPERATION - [ - ExecutionScope.new( - executor: @executor, - parent_type: root_type_for_operation(QUERY_OPERATION), - selections: operation.selections, - objects: [root_object], - results: [result], - ), - ] - when MUTATION_OPERATION - # each mutation field must run serially as its own scope - mutation_type = root_type_for_operation(MUTATION_OPERATION) - selections_grouped_by_key(mutation_type, operation.selections).each_value.map do |selections| - ExecutionScope.new( - executor: @executor, - parent_type: mutation_type, - selections: selections.freeze, - objects: [root_object], - results: [result], - ) - end - when SUBSCRIPTION_OPERATION - [ - ExecutionScope.new( - executor: @executor, - parent_type: root_type_for_operation(SUBSCRIPTION_OPERATION), - selections: operation.selections, - objects: [root_object], - results: [result], - ), - ] - else - raise ImplementationError, "Unsupported operation type: #{operation.operation_type}" - end - end - - #: (Array[ExecutionScope]) -> Array[ExecutionScope] - def plan_scopes(scopes) - scopes.delete_if { _1.objects.empty? } - scopes.freeze - return scopes if scopes.empty? - - scopes.each do |exec_scope| - # invoke planning hooks from bottom-up to bubble configuration... - build_execution_tree(exec_scope).reverse_each do |exec_field| - exec_field.resolver.plan(exec_field, @context) - end - end - - scopes - end - - private - - # Fast path: just build AST nodes grouped by key without defer overhead. - #: ( - #| singleton(GraphQL::Schema::Member) parent_type, - #| Array[selection_node] selections, - #| ?map: Hash[String, Array[GraphQL::Language::Nodes::Field]], - #| ) -> Hash[String, Array[GraphQL::Language::Nodes::Field]] - def selections_grouped_by_key(parent_type, selections, map: Hash.new { |h, k| h[k] = [] }) - types = @context.types - selections.each do |node| - next if node_skipped?(node) - - case node - when GraphQL::Language::Nodes::Field - map[(node.alias || node.name).freeze] << node - when GraphQL::Language::Nodes::InlineFragment - fragment_type = node.type ? types.type(node.type.name) : parent_type - if parent_type_possible?(fragment_type, parent_type) - selections_grouped_by_key(parent_type, node.selections, map: map) - end - when GraphQL::Language::Nodes::FragmentSpread - fragment = @executor.fragments.fetch(node.name) - fragment_type = types.type(fragment.type.name) - if parent_type_possible?(fragment_type, parent_type) - selections_grouped_by_key(parent_type, fragment.selections, map: map) - end - end - end - - map - end - - # Incremental path: wraps each field node in an incremental selection carrying the enclosing `@defer` usage. - #: ( - #| singleton(GraphQL::Schema::Member) parent_type, - #| Array[selection_node] selections, - #| ?map: Hash[String, Array[Incremental::Selection]], - #| ?defer_usage: Incremental::DeferUsage?, - #| ) -> Hash[String, Array[Incremental::Selection]] - def incremental_selections_grouped_by_key(parent_type, selections, map: Hash.new { |h, k| h[k] = [] }, defer_usage: nil) - types = @context.types - selections.each do |node| - next if node_skipped?(node) - - case node - when GraphQL::Language::Nodes::Field - map[(node.alias || node.name).freeze] << Incremental::Selection.new(node, defer_usage:) - when GraphQL::Language::Nodes::InlineFragment - fragment_type = node.type ? types.type(node.type.name) : parent_type - if parent_type_possible?(fragment_type, parent_type) - incremental_selections_grouped_by_key(parent_type, node.selections, map: map, defer_usage: defer_usage_for(node, defer_usage) || defer_usage) - end - when GraphQL::Language::Nodes::FragmentSpread - fragment = @executor.fragments.fetch(node.name) - fragment_type = types.type(fragment.type.name) - if parent_type_possible?(fragment_type, parent_type) - incremental_selections_grouped_by_key(parent_type, fragment.selections, map: map, defer_usage: defer_usage_for(node, defer_usage) || defer_usage) - end - end - end - - map - end - - #: (selection_node node) -> bool - def node_skipped?(node) - return false if node.directives.empty? - - node.directives.any? do |directive| - if directive.name == "skip" - argument_value(directive.arguments.first) - elsif directive.name == "include" - !argument_value(directive.arguments.first) - elsif !ENGINE_RUNTIME_DIRECTIVES.include?(directive.name) - @has_runtime_directives = true - false - end - end - end - - #: (untyped) -> untyped - def argument_value(arg) - value = arg.value - if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier) - @executor.variables[value.name] - else - value - end - end - - #: ( - #| singleton(GraphQL::Schema::Member) fragment_type, - #| singleton(GraphQL::Schema::Member) parent_type, - #| ) -> bool - def parent_type_possible?(fragment_type, parent_type) - fragment_type == parent_type || @context.types.possible_types(fragment_type).include?(parent_type) - end - - #: (GraphQL::Language::Nodes::InlineFragment | GraphQL::Language::Nodes::FragmentSpread, Incremental::DeferUsage?) -> Incremental::DeferUsage? - def defer_usage_for(node, parent) - return nil if node.directives.empty? - - directive = node.directives.find { _1.name == "defer" } - return nil unless directive - - condition = directive.arguments.find { _1.name == "if" } - return nil if condition && argument_value(condition) == false - - label_arg = directive.arguments.find { _1.name == "label" } - label = label_arg ? argument_value(label_arg) : nil - label = nil unless label.is_a?(String) - - Incremental::DeferUsage.new(label, parent:) - end - - #: ( - #| ExecutionScope, - #| ?Array[ExecutionField[untyped]], - #| ) -> Array[ExecutionField[untyped]] - def build_execution_tree(exec_scope, ordered_fields = []) - if @executor.incremental? - build_incremental_execution_fields(exec_scope, ordered_fields) - else - build_execution_fields(exec_scope, ordered_fields) - end - - exec_scope.fields.freeze - ordered_fields - end - - # Fast path for building basic scope fields without incremental info (majority use case) - #: (ExecutionScope, Array[ExecutionField[untyped]]) -> void - def build_execution_fields(exec_scope, ordered_fields) - @has_runtime_directives = false - selections_by_key = selections_grouped_by_key(exec_scope.parent_type, exec_scope.selections) - has_runtime_directives = @has_runtime_directives - - selections_by_key.each do |key, nodes| - exec_field = build_execution_field(key, nodes, exec_scope, has_runtime_directives) - add_execution_field_branch(exec_field, ordered_fields) - end - end - - # Incremental path for partitioning fields into deferred delivery groups, and registering deferred scopes. - #: (ExecutionScope, Array[ExecutionField[untyped]]) -> void - def build_incremental_execution_fields(exec_scope, ordered_fields) - @has_runtime_directives = false - - parent_usages = EMPTY_SET - selections_by_key = if exec_scope.is_a?(Incremental::DeferredExecutionScope) - parent_usages = exec_scope.defer_usages - exec_scope.field_selections - elsif (parent_field = exec_scope.parent_field) - map = Hash.new { |h, k| h[k] = [] } - parent_incremental_selections = parent_field.incremental_selections #: as !nil - parent_incremental_selections.each do |incremental_selection| - incremental_selections_grouped_by_key( - exec_scope.parent_type, - incremental_selection.node.selections, - map: map, - defer_usage: incremental_selection.defer_usage, - ) - end - - parent_usages = Incremental::Partitioner.defer_usages_for(parent_incremental_selections) - map - else - incremental_selections_grouped_by_key(exec_scope.parent_type, exec_scope.selections) - end - - has_runtime_directives = @has_runtime_directives - base_selections, selections_by_defer_usage = Incremental::Partitioner.partition(selections_by_key, parent_usages:) - - selections_by_defer_usage.each do |defer_usage, incremental_selections| - incremental = @executor.incremental #: as !nil - incremental.register_deferred_scope(exec_scope, incremental_selections, defer_usage) - end - - base_selections.each do |key, incremental_selections| - exec_field = build_execution_field(key, incremental_selections.map(&:node), exec_scope, has_runtime_directives, incremental_selections:) - add_execution_field_branch(exec_field, ordered_fields) - end - end - - #: (ExecutionField[untyped], Array[ExecutionField[untyped]]) -> void - def add_execution_field_branch(exec_field, ordered_fields) - exec_field.scope.fields[exec_field.key] = exec_field - ordered_fields << exec_field - - return_type = exec_field.type.unwrap - return if return_type.kind.leaf? || return_type.kind.abstract? - - next_scope = ExecutionScope.new( - executor: @executor, - parent_type: return_type, - parent_field: exec_field, - selections: exec_field.selections, - deferred: exec_field.scope.deferred?, - objects: [], - results: [], - ) - @planned_scopes_by_field[exec_field] = next_scope - build_execution_tree(next_scope, ordered_fields) - end - - #: ( - #| String key, - #| Array[GraphQL::Language::Nodes::Field] nodes, - #| ExecutionScope exec_scope, - #| ?bool has_runtime_directives, - #| ?incremental_selections: Array[Incremental::Selection]?, - #| ) -> ExecutionField[untyped] - def build_execution_field(key, nodes, exec_scope, has_runtime_directives = false, incremental_selections: nil) - first_node = nodes.first #: as !nil - node_name = first_node.name - - definition = @context.types.field(exec_scope.parent_type, node_name) - resolver = if definition.is_a?(HasBreadthResolver::Field) && definition.breadth_resolver - definition.breadth_resolver - elsif definition.introspection? - if definition.graphql_name == TYPENAME_FIELD - Introspection::TYPENAME_RESOLVER - elsif exec_scope.parent_type.equal?(@context.types.query_root) - Introspection::ENTRYPOINT_RESOLVERS.dig(definition.graphql_name) - else - Introspection::TYPE_RESOLVERS.dig(exec_scope.parent_type.graphql_name, definition.graphql_name) - end - else - @resolvers.dig(exec_scope.parent_type.graphql_name, definition.graphql_name) - end - - unless resolver - raise NotImplementedError, "No field resolver for '#{exec_scope.parent_type.graphql_name}.#{definition.graphql_name}'" - end - - directives = exec_scope.parent_field&.directives || EMPTY_ARRAY - if has_runtime_directives - nodes.each do |node| - node.directives.each do |directive| - next if ENGINE_RUNTIME_DIRECTIVES.include?(directive.name) - - directives = directives.dup if directives.frozen? - directives << build_execution_directive(directive, depth: exec_scope.depth + 1) - end - end - directives.freeze - end - - ExecutionField.new( - key, - nodes: nodes, - scope: exec_scope, - definition: definition, - resolver: resolver, - directives: directives, - incremental_selections: incremental_selections, - ) - end - - #: (GraphQL::Language::Nodes::Directive, ?depth: Integer) -> ExecutionDirective - def build_execution_directive(node, depth: 0) - definition = @schema.directives[node.name] - - resolver = if definition.respond_to?(:breadth_resolver) && definition.breadth_resolver - definition.breadth_resolver - else - @resolvers["@#{definition.graphql_name}"] - end - ExecutionDirective.build( - executor: @executor, - node: node, - definition: definition, - resolver: resolver, - depth: depth, - ) - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/execution_scope.rb b/lib/graphql/breadth_exec/executor/execution_scope.rb deleted file mode 100644 index f81364d..0000000 --- a/lib/graphql/breadth_exec/executor/execution_scope.rb +++ /dev/null @@ -1,179 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - class ExecutionScope - include LazyElement - include HasAttributes - - #: Executor - attr_reader :executor - - #: singleton(GraphQL::Schema::Object) - attr_reader :parent_type - - #: ExecutionField[untyped]? - attr_reader :parent_field - - #: Array[selection_node] - attr_reader :selections - - #: Array[untyped] - attr_reader :objects - - #: Array[untyped] - attr_reader :results - - #: AbstractExecutionScope? - attr_reader :abstraction - - #: Array[String] - attr_reader :path - - #: ExecutionScope? - attr_reader :parent - - #: Hash[String, ExecutionField[untyped]] - attr_reader :fields - - #: bool - attr_writer :executed - - #: ( - #| executor: Executor, - #| parent_type: singleton(GraphQL::Schema::Object), - #| selections: Array[selection_node], - #| objects: Array[untyped], - #| results: Array[untyped], - #| ?abstraction: AbstractExecutionScope?, - #| ?parent_field: ExecutionField[untyped]?, - #| ?path: Array[String], - #| ?parent: ExecutionScope?, - #| ?deferred: bool, - #| ) -> void - def initialize( - executor:, - parent_type:, - selections:, - objects:, - results:, - abstraction: nil, - parent_field: nil, - path: [], - parent: nil, - deferred: false - ) - super() - @executor = executor - @parent_type = parent_type - @parent_field = parent_field - @selections = selections - @objects = objects - @results = results - @abstraction = abstraction - @path = (parent_field ? parent_field.path : path).freeze - @parent = parent_field ? parent_field.scope : parent - @fields = {} - @deferred = deferred - @executed = false - @root = nil - @planning_root = nil - @aborted = false - end - - #: -> GraphQL::Query::Context - def context - @executor.context - end - - #: -> ExecutionScope - def root - @root ||= @parent ? @parent.root : self - end - - #: -> ExecutionScope - def planning_root - @planning_root ||= (abstraction || @parent.nil?) ? self : @parent.planning_root - end - - #: -> Integer - def depth - @path.length - end - - #: -> Array[String] - def schema_path - if (field = parent_field) - field.schema_path - else - EMPTY_ARRAY - end - end - - #: (Integer) -> error_path - def object_path(index) - @executor.paths.object_path(self, index) - end - - #: -> bool - def executed? - @executed - end - - #: -> bool - def abort! - @aborted = true - end - - #: -> bool - def aborted? - @aborted - end - - #: -> bool - def deferred? - @deferred - end - - #: -> ExecutionScope? - def deferred_root - return self if deferred? - - exec_scope = @parent - while exec_scope - return exec_scope if exec_scope.deferred? - - exec_scope = exec_scope.parent - end - - nil - end - - #: -> bool - def aborted_subtree? - return true if @aborted - - exec_field = parent_field #: ExecutionField[untyped]? - while exec_field - if exec_field.scope.aborted? - abort! - return true - end - exec_field = exec_field.scope.parent_field - end - false - end - - #: -> bool - def has_authorized_objects? - @objects.frozen? && !@objects.empty? - end - - #: -> String - def inspect - "#" - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/has_attributes.rb b/lib/graphql/breadth_exec/executor/has_attributes.rb deleted file mode 100644 index c1fe39b..0000000 --- a/lib/graphql/breadth_exec/executor/has_attributes.rb +++ /dev/null @@ -1,32 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - module HasAttributes - def initialize(...) - super(...) - @attributes = nil - end - - #: () -> Hash[untyped, untyped] - def attributes - @attributes ||= {} - end - - #: (untyped, ?default: untyped) -> untyped - def attribute(key, default: nil) - return default unless @attributes - - @attributes.fetch(key, default) - end - - #: (untyped) -> bool - def attribute?(key) - return false unless @attributes - - @attributes.key?(key) - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/lazy_element.rb b/lib/graphql/breadth_exec/executor/lazy_element.rb deleted file mode 100644 index 84f52fd..0000000 --- a/lib/graphql/breadth_exec/executor/lazy_element.rb +++ /dev/null @@ -1,125 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - # @requires_ancestor: Kernel - module LazyElement - LAZY_STATE_PRELOADING = :preloading - LAZY_STATE_LOCKED = :locked - - def initialize(...) - super(...) - @sync_preloads = nil - @lazy_preloads = nil - @lazy_state = LAZY_STATE_PRELOADING - @preload_promises = nil - end - - #: () -> Array[ExecutionPromise] - def preload_promises - @preload_promises ||= [] - end - - #: () -> bool - def lazy_preloads? - @preload_promises && !@preload_promises.empty? - end - - #: () -> bool - def allows_preload? - @lazy_state == LAZY_STATE_PRELOADING - end - - #: () { (LazyElement) -> void } -> void - def on_preload(&block) - unless allows_preload? - Kernel.raise LazySequencingError.new(lazy_element: self, method_name: "on_preload") - end - - @sync_preloads ||= [] - @sync_preloads << block - end - - #: ( - #| singleton(GraphQL::BreadthExec::LazyLoader), - #| ?args: loader_args?, - #| ?keys: Array[untyped]?, - #| ) -> ExecutionPromise - def preload(loader_class, args: nil, keys: nil) - unless allows_preload? - Kernel.raise LazySequencingError.new(lazy_element: self, method_name: "preload") - end - - if keys - loader = executor.lazy_loader_for(loader_class, args) - loader.load(element: self, keys: keys).with_registry(preload_promises) - else - @lazy_preloads ||= {} - deferred = (@lazy_preloads[[loader_class, args]] ||= ExecutionPromise::Deferred.new(registry: preload_promises)) - deferred.promise - end - end - - #: () -> void - def preload! - while @sync_preloads && !@sync_preloads.empty? - sync_preloads = @sync_preloads - @sync_preloads = nil - sync_preloads.each(&:call) - end - - return unless @lazy_preloads - - preloads = @lazy_preloads - @lazy_preloads = nil - - preloads.each do |(loader_class, args), pre_deferred| - loader = executor.lazy_loader_for(loader_class, args) - loader.load(element: self, keys: objects, pre_deferred: pre_deferred) - end - end - - #: (Array[ExecutionPromise]) -> ExecutionPromise - def await_all(promises) - promise = ExecutionPromise.all(promises) - registry = promises.first&.registry - - if registry && promises.all? { _1.registry.equal?(registry) } - promise.with_registry(registry) - else - promise - end - end - - #: () -> Array[untyped] - def objects - Kernel.raise NotImplementedError - end - - #: () -> Executor - def executor - Kernel.raise NotImplementedError - end - - #: () -> Array[String] - def path - Kernel.raise NotImplementedError - end - - #: () -> void - def lazy_state_locked! - Kernel.raise LazyStateTransitionError.new(@lazy_state, LAZY_STATE_LOCKED) unless lazy_state_lockable? - - @lazy_state = LAZY_STATE_LOCKED - end - - private - - #: () -> bool - def lazy_state_lockable? - @lazy_state == LAZY_STATE_PRELOADING - end - end - end -end diff --git a/lib/graphql/breadth_exec/executor/path_formatter.rb b/lib/graphql/breadth_exec/executor/path_formatter.rb deleted file mode 100644 index 5ccc597..0000000 --- a/lib/graphql/breadth_exec/executor/path_formatter.rb +++ /dev/null @@ -1,118 +0,0 @@ -# typed: true -# frozen_string_literal: true - -module GraphQL::BreadthExec - class Executor - # Builds paths to specific breadth object positions as exact object paths, ex: - # - Scope path (namespace): ["products", "variants"] - # - Object path (exact path): ["products", 0, "variants", 1] - # Object pathing assembles breadth indices for all scopes ascending from the targeted scope, ex: - # { - # => { - # 0 => [0, 1, 1, 2], # << zero index always maps objects in this scope to objects in the parent scope - # 1 => [0, 0, 1, 0], # << maps a first-order list in this scope - # 2 => [ ... ], # << maps a second-order list in this scope, etc... - # }, - # => { ... }, - # } - # All numeric indexing arrays have size matching the number of objects in their scope. - # To read an object path, we follow the Nth position of each scope's index map ascending up the scope tree. - # Building these indices adds execution overhead that isn't needed regularly, so this is only done as part of deferred work. - class PathFormatter - #: Hash[Executor::ExecutionScope, Hash[Integer, Array[Integer]]] - attr_reader :indices_by_scope - - def initialize - @indices_by_scope = Hash.new { |h1, exec_scope| h1[exec_scope] = Hash.new { |h2, index| h2[index] = [] } }.compare_by_identity - end - - #: (Executor::ExecutionScope, Integer) -> error_path - def object_path(exec_scope, index) - current_path = [] - - current_scope = exec_scope #: Executor::ExecutionScope? - breadth_index = index - while current_scope - # index the scope unless it has already been done - scope_indices = @indices_by_scope[current_scope] - index_scope(current_scope, scope_indices) if scope_indices.empty? - - # loop backward through all the scope's indices... - # - all scopes have at least one index that defines the parent scope position - # - list scopes have additional indices for each layer of list wrapping - i = scope_indices.length - 1 - while i >= 0 - if i.zero? - # at the lowest index, recalibrate for the next highest scope - breadth_index = scope_indices[i][breadth_index] #: as Integer - else - # higher indices add list positions to the current path - current_path.prepend(scope_indices[i][breadth_index]) - end - i -= 1 - end - - # before going up a scope, add the parent field key into current path - key = current_scope.parent_field&.key - current_path.prepend(key) if key - current_scope = current_scope.parent - end - - current_path - end - - private - - #: (Executor::ExecutionScope, Hash[Integer, Array[Integer]]) -> void - def index_scope(exec_scope, scope_indices) - raise ArgumentError, "Scope must not be indexed" unless scope_indices.empty? - raise ArgumentError, "Scope must be executed" unless exec_scope.executed? - - parent_objects = exec_scope.objects - current_type = exec_scope.parent_type - if (parent_field = exec_scope.parent_field) - parent_objects = parent_field.result #: as !nil - current_type = parent_field.type - end - - object_path = [] - i = 0 - while i < parent_objects.length - object_path[0] = i - build_indices(current_type, parent_objects[i], object_path, scope_indices) - i += 1 - end - - scope_indices.each(&:freeze) - scope_indices.freeze - end - - #: (singleton(GraphQL::Schema::Member), untyped, Array[Integer], Hash[Integer, Array[Integer]]) -> void - def build_indices(current_type, object, object_path, next_indices) - return if object.nil? || object.is_a?(ExecutionError) - - if current_type.list? - raise ImplementationError, "Expected Array, got #{object.class}" unless object.is_a?(Array) - - current_type = Util.unwrap_non_null(current_type) - - i = 0 - while i < object.length - object_path << i - build_indices(current_type.of_type, object[i], object_path, next_indices) - object_path.pop - i += 1 - end - else - i = 0 - while i < object_path.length - next_index = object_path[i] #: as !nil - next_map = next_indices[i] #: as !nil - next_map << next_index - i += 1 - end - end - end - end - end -end diff --git a/test/fixtures.rb b/test/fixtures.rb index 8e717b6..b1f2363 100644 --- a/test/fixtures.rb +++ b/test/fixtures.rb @@ -52,14 +52,14 @@ SCHEMA = GraphQL::Schema.from_definition(SDL) -class WriteValueResolver < GraphQL::BreadthExec::FieldResolver +class WriteValueResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.objects.each { _1["writeValue"]["value"] = exec_field.arguments[:value] } exec_field.objects.map { _1["writeValue"] } end end -class SimpleLoader < GraphQL::BreadthExec::LazyLoader +class SimpleLoader < GraphQL::Breadth::LazyLoader def initialize(group: nil) super() @group = group @@ -74,7 +74,7 @@ def perform_map(keys, _ctx) end end -class DeferredHashResolver < GraphQL::BreadthExec::FieldResolver +class DeferredHashResolver < GraphQL::Breadth::FieldResolver def initialize(key) @key = key end @@ -105,45 +105,45 @@ def perform(keys) end BREADTH_RESOLVERS = { - **GraphQL::BreadthExec::Introspection::TYPE_RESOLVERS, + **GraphQL::Breadth::Introspection::TYPE_RESOLVERS, "Node" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), "__type__" => ->(obj, ctx) { ctx.types.type(obj["__typename__"]) }, }, "HasMetafields" => { - "metafield" => GraphQL::BreadthExec::HashKeyResolver.new("metafield"), + "metafield" => GraphQL::Breadth::HashKeyResolver.new("metafield"), "__type__" => ->(obj, ctx) { ctx.types.type(obj["__typename__"]) }, }, "Metafield" => { - "key" => GraphQL::BreadthExec::HashKeyResolver.new("key"), - "value" => GraphQL::BreadthExec::HashKeyResolver.new("value"), + "key" => GraphQL::Breadth::HashKeyResolver.new("key"), + "value" => GraphQL::Breadth::HashKeyResolver.new("value"), }, "Product" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), - "maybe" => GraphQL::BreadthExec::HashKeyResolver.new("maybe"), - "must" => GraphQL::BreadthExec::HashKeyResolver.new("must"), - "variants" => GraphQL::BreadthExec::HashKeyResolver.new("variants"), - "metafield" => GraphQL::BreadthExec::HashKeyResolver.new("metafield"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), + "maybe" => GraphQL::Breadth::HashKeyResolver.new("maybe"), + "must" => GraphQL::Breadth::HashKeyResolver.new("must"), + "variants" => GraphQL::Breadth::HashKeyResolver.new("variants"), + "metafield" => GraphQL::Breadth::HashKeyResolver.new("metafield"), }, "ProductConnection" => { - "nodes" => GraphQL::BreadthExec::HashKeyResolver.new("nodes"), + "nodes" => GraphQL::Breadth::HashKeyResolver.new("nodes"), }, "Variant" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), }, "VariantConnection" => { - "nodes" => GraphQL::BreadthExec::HashKeyResolver.new("nodes"), + "nodes" => GraphQL::Breadth::HashKeyResolver.new("nodes"), }, "WriteValuePayload" => { - "value" => GraphQL::BreadthExec::HashKeyResolver.new("value"), + "value" => GraphQL::Breadth::HashKeyResolver.new("value"), }, "Query" => { - **GraphQL::BreadthExec::Introspection::ENTRYPOINT_RESOLVERS, - "products" => GraphQL::BreadthExec::HashKeyResolver.new("products"), - "nodes" => GraphQL::BreadthExec::HashKeyResolver.new("nodes"), - "node" => GraphQL::BreadthExec::HashKeyResolver.new("node"), + **GraphQL::Breadth::Introspection::ENTRYPOINT_RESOLVERS, + "products" => GraphQL::Breadth::HashKeyResolver.new("products"), + "nodes" => GraphQL::Breadth::HashKeyResolver.new("nodes"), + "node" => GraphQL::Breadth::HashKeyResolver.new("node"), }, "Mutation" => { "writeValue" => WriteValueResolver.new, diff --git a/test/graphql/breadth_exec/execution_errors_test.rb b/test/graphql/breadth/execution_errors_test.rb similarity index 56% rename from test/graphql/breadth_exec/execution_errors_test.rb rename to test/graphql/breadth/execution_errors_test.rb index b02d2b4..ccff801 100644 --- a/test/graphql/breadth_exec/execution_errors_test.rb +++ b/test/graphql/breadth/execution_errors_test.rb @@ -2,16 +2,16 @@ require "test_helper" -class GraphQL::BreadthExec::ExecutionErrorsTest < Minitest::Test +class GraphQL::Breadth::ExecutionErrorsTest < Minitest::Test def setup @exec_field = Object.new @other_exec_field = Object.new end def test_execution_error_from_returns_same_unreported_error_instance - err = GraphQL::BreadthExec::ExecutionError.from(GraphQL::BreadthExec::UNREPORTED_ERROR, exec_field: @exec_field) + err = GraphQL::Breadth::ExecutionError.from(GraphQL::Breadth::UNREPORTED_ERROR, exec_field: @exec_field) - assert_same GraphQL::BreadthExec::UNREPORTED_ERROR, err + assert_same GraphQL::Breadth::UNREPORTED_ERROR, err assert_nil err.exec_field assert_nil err.cause end @@ -19,7 +19,7 @@ def test_execution_error_from_returns_same_unreported_error_instance def test_execution_error_from_wraps_graphql_execution_error ext = { "code" => "WHOOPS" } gem_err = GraphQL::ExecutionError.new("Whoops", extensions: ext) - err = GraphQL::BreadthExec::ExecutionError.from(gem_err, exec_field: @exec_field) + err = GraphQL::Breadth::ExecutionError.from(gem_err, exec_field: @exec_field) assert_equal "Whoops", err.message assert_equal ext, err.extensions @@ -28,8 +28,8 @@ def test_execution_error_from_wraps_graphql_execution_error end def test_execution_error_from_dups_execution_error_when_exec_field_differs - original = GraphQL::BreadthExec::ExecutionError.new("Whoops", exec_field: @other_exec_field) - err = GraphQL::BreadthExec::ExecutionError.from(original, exec_field: @exec_field) + original = GraphQL::Breadth::ExecutionError.new("Whoops", exec_field: @other_exec_field) + err = GraphQL::Breadth::ExecutionError.from(original, exec_field: @exec_field) refute_same original, err assert_equal "Whoops", err.message @@ -37,28 +37,28 @@ def test_execution_error_from_dups_execution_error_when_exec_field_differs end def test_execution_error_from_preserves_execution_error_set_type - original = GraphQL::BreadthExec::ExecutionErrorSet.new("Combined errors", exec_field: @other_exec_field) + original = GraphQL::Breadth::ExecutionErrorSet.new("Combined errors", exec_field: @other_exec_field) original.add_error("Error 1") original.add_error("Error 2") - err = GraphQL::BreadthExec::ExecutionError.from(original, exec_field: @exec_field) + err = GraphQL::Breadth::ExecutionError.from(original, exec_field: @exec_field) - assert_instance_of GraphQL::BreadthExec::ExecutionErrorSet, err + assert_instance_of GraphQL::Breadth::ExecutionErrorSet, err assert_equal 2, err.errors.size assert_same @exec_field, err.exec_field end def test_input_error_keeps_input_path_when_wrapped - original = GraphQL::BreadthExec::InputError.new("Invalid input", path: ["field", "input"]) - err = GraphQL::BreadthExec::ExecutionError.from(original, exec_field: @exec_field) + original = GraphQL::Breadth::InputError.new("Invalid input", path: ["field", "input"]) + err = GraphQL::Breadth::ExecutionError.from(original, exec_field: @exec_field) - assert_instance_of GraphQL::BreadthExec::InputError, err + assert_instance_of GraphQL::Breadth::InputError, err assert_equal ["field", "input"], err.path assert_same @exec_field, err.exec_field end def test_result_count_mismatch_error_message exec_field = Struct.new(:path).new(["widget"]) - err = GraphQL::BreadthExec::ResultCountMismatchError.new(exec_field:, expected_count: 1, actual_count: 3) + err = GraphQL::Breadth::ResultCountMismatchError.new(exec_field:, expected_count: 1, actual_count: 3) assert_equal 1, err.expected_count assert_equal 3, err.actual_count diff --git a/test/graphql/breadth_exec/executor/abstracts_test.rb b/test/graphql/breadth/executor/abstracts_test.rb similarity index 80% rename from test/graphql/breadth_exec/executor/abstracts_test.rb rename to test/graphql/breadth/executor/abstracts_test.rb index e7d0e08..d340391 100644 --- a/test/graphql/breadth_exec/executor/abstracts_test.rb +++ b/test/graphql/breadth/executor/abstracts_test.rb @@ -2,9 +2,9 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::AbstractsTest < Minitest::Test +class GraphQL::Breadth::Executor::AbstractsTest < Minitest::Test class TypeErrorField < GraphQL::Schema::Field - include GraphQL::BreadthExec::HasBreadthResolver::Field + include GraphQL::Breadth::HasBreadthResolver::Field end module TypeErrorNode @@ -18,7 +18,7 @@ class TypeErrorProduct < GraphQL::Schema::Object field_class TypeErrorField field :id, String, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("id") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("id") end end @@ -26,7 +26,7 @@ class TypeErrorQuery < GraphQL::Schema::Object field_class TypeErrorField field :node, TypeErrorNode, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("node") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("node") end end @@ -64,7 +64,7 @@ def test_abstract_type_object_access "node" => { "id" => "Product/1", "__typename" => "Product" }, } - assert_equal expected, breadth_exec(document, source).dig("data") + assert_equal expected, breadth(document, source).dig("data") end def test_abstract_type_list_access @@ -94,18 +94,18 @@ def test_abstract_type_list_access ], } - assert_equal expected, breadth_exec(document, source).dig("data") + assert_equal expected, breadth(document, source).dig("data") end def test_unresolved_abstract_type_reports_basic_schema_type_error TypeErrorSchema.type_errors = [] - executor = GraphQL::BreadthExec::Executor.new( + executor = GraphQL::Breadth::Executor.new( TypeErrorSchema, GraphQL.parse(%|{ node { id } }|), root_object: { "node" => { "id" => "Product/1" } }, ) - assert_raises(GraphQL::BreadthExec::ImplementationError) { executor.result } + assert_raises(GraphQL::Breadth::ImplementationError) { executor.result } error, context = TypeErrorSchema.type_errors.first assert_kind_of GraphQL::UnresolvedTypeError, error diff --git a/test/graphql/breadth_exec/executor/aggregate_selections_test.rb b/test/graphql/breadth/executor/aggregate_selections_test.rb similarity index 83% rename from test/graphql/breadth_exec/executor/aggregate_selections_test.rb rename to test/graphql/breadth/executor/aggregate_selections_test.rb index a30687c..faac63b 100644 --- a/test/graphql/breadth_exec/executor/aggregate_selections_test.rb +++ b/test/graphql/breadth/executor/aggregate_selections_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::AggregateSelectionsTest < Minitest::Test +class GraphQL::Breadth::Executor::AggregateSelectionsTest < Minitest::Test NODE_SOURCE = { "node" => { "title" => "Banana", @@ -42,7 +42,7 @@ def test_aggregate_field_selections }, } - assert_equal source, breadth_exec(document, source).dig("data") + assert_equal source, breadth(document, source).dig("data") end def test_aggregate_field_access_across_inline_fragments @@ -62,7 +62,7 @@ def test_aggregate_field_access_across_inline_fragments } }| - assert_equal NODE_EXPECTED, breadth_exec(document, NODE_SOURCE).dig("data") + assert_equal NODE_EXPECTED, breadth(document, NODE_SOURCE).dig("data") end def test_aggregate_field_access_across_fragment_spreads @@ -84,6 +84,6 @@ def test_aggregate_field_access_across_fragment_spreads } }| - assert_equal NODE_EXPECTED, breadth_exec(document, NODE_SOURCE).dig("data") + assert_equal NODE_EXPECTED, breadth(document, NODE_SOURCE).dig("data") end end diff --git a/test/graphql/breadth_exec/executor/arguments_test.rb b/test/graphql/breadth/executor/arguments_test.rb similarity index 76% rename from test/graphql/breadth_exec/executor/arguments_test.rb rename to test/graphql/breadth/executor/arguments_test.rb index bc908a0..f882d97 100644 --- a/test/graphql/breadth_exec/executor/arguments_test.rb +++ b/test/graphql/breadth/executor/arguments_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ArgumentsTest < Minitest::Test +class GraphQL::Breadth::Executor::ArgumentsTest < Minitest::Test def test_arguments_receive_string_variables document = %|mutation($value: String!) { writeValue(value: $value) { @@ -12,7 +12,7 @@ def test_arguments_receive_string_variables source = { "writeValue" => { "value" => nil } } expected = { "writeValue" => { "value" => "success!" } } - assert_equal expected, breadth_exec(document, source, variables: { "value" => "success!" }).dig("data") + assert_equal expected, breadth(document, source, variables: { "value" => "success!" }).dig("data") end def test_arguments_receive_symbol_variables @@ -24,14 +24,14 @@ def test_arguments_receive_symbol_variables source = { "writeValue" => { "value" => nil } } expected = { "writeValue" => { "value" => "success!" } } - assert_equal expected, breadth_exec(document, source, variables: { value: "success!" }).dig("data") + assert_equal expected, breadth(document, source, variables: { value: "success!" }).dig("data") end def test_invalid_field_arguments_are_reported_then_raised_during_execution result = nil - reported = assert_error_reported(GraphQL::BreadthExec::InputValidationErrorSet) do - result = breadth_exec( + reported = assert_error_reported(GraphQL::Breadth::InputValidationErrorSet) do + result = breadth( %|mutation { writeValue { value } }|, { "writeValue" => { "value" => "unchanged" } }, ) diff --git a/test/graphql/breadth_exec/executor/authorization_test.rb b/test/graphql/breadth/executor/authorization_test.rb similarity index 87% rename from test/graphql/breadth_exec/executor/authorization_test.rb rename to test/graphql/breadth/executor/authorization_test.rb index eb86097..024ce10 100644 --- a/test/graphql/breadth_exec/executor/authorization_test.rb +++ b/test/graphql/breadth/executor/authorization_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::AuthorizationTest < Minitest::Test +class GraphQL::Breadth::Executor::AuthorizationTest < Minitest::Test AUTH_SCHEMA = GraphQL::Schema.from_definition(%| type Widget { title: String @@ -16,11 +16,11 @@ class GraphQL::BreadthExec::Executor::AuthorizationTest < Minitest::Test AUTH_RESOLVERS = { "Widget" => { - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), }, "Query" => { - "widget" => GraphQL::BreadthExec::HashKeyResolver.new("widget"), - "widgets" => GraphQL::BreadthExec::HashKeyResolver.new("widgets"), + "widget" => GraphQL::Breadth::HashKeyResolver.new("widget"), + "widgets" => GraphQL::Breadth::HashKeyResolver.new("widgets"), }, }.freeze @@ -33,7 +33,7 @@ class GraphQL::BreadthExec::Executor::AuthorizationTest < Minitest::Test ], }.freeze - class RecordingAuthorization < GraphQL::BreadthExec::Authorization + class RecordingAuthorization < GraphQL::Breadth::Authorization class << self attr_accessor :calls, :deny_field, :deny_type, :object_invalidations end @@ -64,7 +64,7 @@ def authorize_objects_in_scope?(exec_scope, context) def unauthorized_object_indices(exec_scope, context) self.class.calls << [:objects, exec_scope.parent_type.graphql_name, exec_scope.path] - self.class.object_invalidations.fetch(exec_scope.path, GraphQL::BreadthExec::EMPTY_OBJECT) + self.class.object_invalidations.fetch(exec_scope.path, GraphQL::Breadth::EMPTY_OBJECT) end end @@ -73,7 +73,7 @@ def setup end def execute(document) - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( AUTH_SCHEMA, GraphQL.parse(document), resolvers: AUTH_RESOLVERS, @@ -115,7 +115,7 @@ def test_checks_type_authorization_for_scopes def test_invalidates_unauthorized_objects_before_scope_execution RecordingAuthorization.object_invalidations = { ["widgets"] => { - 1 => GraphQL::BreadthExec::ExecutionError.new("Object hidden"), + 1 => GraphQL::Breadth::ExecutionError.new("Object hidden"), }, } diff --git a/test/graphql/breadth_exec/executor/conditionals_test.rb b/test/graphql/breadth/executor/conditionals_test.rb similarity index 72% rename from test/graphql/breadth_exec/executor/conditionals_test.rb rename to test/graphql/breadth/executor/conditionals_test.rb index 8547044..5ca055b 100644 --- a/test/graphql/breadth_exec/executor/conditionals_test.rb +++ b/test/graphql/breadth/executor/conditionals_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ConditionalsTest < Minitest::Test +class GraphQL::Breadth::Executor::ConditionalsTest < Minitest::Test SOURCE = { "products" => { "nodes" => [ @@ -31,7 +31,7 @@ def test_follows_skip_directive_omissions } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE).dig("data") end def test_follows_skip_directive_inclusions @@ -44,7 +44,7 @@ def test_follows_skip_directive_inclusions } }| - assert_equal SOURCE, breadth_exec(document, SOURCE).dig("data") + assert_equal SOURCE, breadth(document, SOURCE).dig("data") end def test_follows_skip_directives_with_string_variable @@ -57,7 +57,7 @@ def test_follows_skip_directives_with_string_variable } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE, variables: { "skip" => true }).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE, variables: { "skip" => true }).dig("data") end def test_follows_skip_directives_with_symbol_variable @@ -70,7 +70,7 @@ def test_follows_skip_directives_with_symbol_variable } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE, variables: { skip: true }).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE, variables: { skip: true }).dig("data") end def test_follows_include_directive_omissions @@ -83,7 +83,7 @@ def test_follows_include_directive_omissions } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE).dig("data") end def test_follows_include_directive_inclusions @@ -96,7 +96,7 @@ def test_follows_include_directive_inclusions } }| - assert_equal SOURCE, breadth_exec(document, SOURCE).dig("data") + assert_equal SOURCE, breadth(document, SOURCE).dig("data") end def test_follows_include_directives_with_string_variable @@ -109,7 +109,7 @@ def test_follows_include_directives_with_string_variable } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE, variables: { "include" => false }).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE, variables: { "include" => false }).dig("data") end def test_follows_include_directives_with_symbol_variable @@ -122,6 +122,6 @@ def test_follows_include_directives_with_symbol_variable } }| - assert_equal SKIPPED_SOURCE, breadth_exec(document, SOURCE, variables: { include: false }).dig("data") + assert_equal SKIPPED_SOURCE, breadth(document, SOURCE, variables: { include: false }).dig("data") end end diff --git a/test/graphql/breadth_exec/executor/directives_test.rb b/test/graphql/breadth/executor/directives_test.rb similarity index 78% rename from test/graphql/breadth_exec/executor/directives_test.rb rename to test/graphql/breadth/executor/directives_test.rb index c5761be..f2f0ea5 100644 --- a/test/graphql/breadth_exec/executor/directives_test.rb +++ b/test/graphql/breadth/executor/directives_test.rb @@ -2,8 +2,8 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::DirectivesTest < Minitest::Test - class RecordingDirectiveResolver < GraphQL::BreadthExec::DirectiveResolver +class GraphQL::Breadth::Executor::DirectivesTest < Minitest::Test + class RecordingDirectiveResolver < GraphQL::Breadth::DirectiveResolver attr_reader :calls def initialize(cascades: false) @@ -20,7 +20,7 @@ def resolve(exec_directive, _ctx, current_field: nil) end end - class ReplaceDirectiveResolver < GraphQL::BreadthExec::DirectiveResolver + class ReplaceDirectiveResolver < GraphQL::Breadth::DirectiveResolver def initialize super(wraps: true) end @@ -47,11 +47,11 @@ def resolve(exec_directive, _ctx, current_field: nil) BASE_RESOLVERS = { "Widget" => { - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), - "nested" => GraphQL::BreadthExec::HashKeyResolver.new("nested"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), + "nested" => GraphQL::Breadth::HashKeyResolver.new("nested"), }, "Query" => { - "widget" => GraphQL::BreadthExec::HashKeyResolver.new("widget"), + "widget" => GraphQL::Breadth::HashKeyResolver.new("widget"), }, }.freeze @@ -74,7 +74,7 @@ def test_resolves_operation_and_field_directives } }|) - result = GraphQL::BreadthExec::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result + result = GraphQL::Breadth::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result assert_equal "Original", result.dig("data", "widget", "title") assert_equal [ @@ -85,7 +85,7 @@ def test_resolves_operation_and_field_directives def test_builds_directives_from_schema_definitions document = GraphQL.parse(%|query @record(value: "root") { widget { title } }|) - executor = GraphQL::BreadthExec::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: BASE_RESOLVERS, root_object: SOURCE) + executor = GraphQL::Breadth::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: BASE_RESOLVERS, root_object: SOURCE) operation = executor.query.selected_operation exec_directive = executor.planner.root_directives_for_operation(operation).first @@ -103,7 +103,7 @@ def test_wrapping_field_directive_can_short_circuit_resolution } }|) - result = GraphQL::BreadthExec::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result + result = GraphQL::Breadth::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result assert_equal "Overridden", result.dig("data", "widget", "title") end @@ -113,8 +113,8 @@ def test_invalid_directive_arguments_are_reported_then_raised_during_execution resolvers = BASE_RESOLVERS.merge("@replace" => replace) result = nil - reported = assert_error_reported(GraphQL::BreadthExec::InputValidationErrorSet) do - result = GraphQL::BreadthExec::Executor.new( + reported = assert_error_reported(GraphQL::Breadth::InputValidationErrorSet) do + result = GraphQL::Breadth::Executor.new( DIRECTIVE_SCHEMA, GraphQL.parse(%|{ widget { title @replace } }|), resolvers: resolvers, @@ -151,7 +151,7 @@ def test_cascading_field_directive_applies_to_descendants } }|) - result = GraphQL::BreadthExec::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result + result = GraphQL::Breadth::Executor.new(DIRECTIVE_SCHEMA, document, resolvers: resolvers, root_object: SOURCE).result assert_equal "Original", result.dig("data", "widget", "title") assert_equal "Nested", result.dig("data", "widget", "nested", "title") diff --git a/test/graphql/breadth_exec/executor/error_formatter_test.rb b/test/graphql/breadth/executor/error_formatter_test.rb similarity index 86% rename from test/graphql/breadth_exec/executor/error_formatter_test.rb rename to test/graphql/breadth/executor/error_formatter_test.rb index 74e236b..124a700 100644 --- a/test/graphql/breadth_exec/executor/error_formatter_test.rb +++ b/test/graphql/breadth/executor/error_formatter_test.rb @@ -2,21 +2,21 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ErrorFormatterTest < Minitest::Test +class GraphQL::Breadth::Executor::ErrorFormatterTest < Minitest::Test TEST_RESOLVERS = { "Node" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), "__type__" => ->(obj, ctx) { ctx.types.type(obj["__typename__"]) }, }, "Test" => { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), - "req" => GraphQL::BreadthExec::HashKeyResolver.new("req"), - "opt" => GraphQL::BreadthExec::HashKeyResolver.new("opt"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), + "req" => GraphQL::Breadth::HashKeyResolver.new("req"), + "opt" => GraphQL::Breadth::HashKeyResolver.new("opt"), }, "Query" => { - "node" => GraphQL::BreadthExec::HashKeyResolver.new("node"), - "test" => GraphQL::BreadthExec::HashKeyResolver.new("test"), - "reqField" => GraphQL::BreadthExec::HashKeyResolver.new("reqField"), + "node" => GraphQL::Breadth::HashKeyResolver.new("node"), + "test" => GraphQL::Breadth::HashKeyResolver.new("test"), + "reqField" => GraphQL::Breadth::HashKeyResolver.new("reqField"), }, }.freeze @@ -86,7 +86,7 @@ def test_inline_errors_in_null_positions_report source = { "test" => [ { "req" => "yes", "opt" => nil }, - { "req" => "yes", "opt" => GraphQL::BreadthExec::ExecutionError.new("Not okay!") }, + { "req" => "yes", "opt" => GraphQL::Breadth::ExecutionError.new("Not okay!") }, ], } @@ -137,7 +137,7 @@ def test_multiple_locations_for_duplicate_field_selections def test_formats_errors_with_extensions schema = "type Query { test: String! }" source = { - "test" => GraphQL::BreadthExec::ExecutionError.new("Not okay!", extensions: { + "test" => GraphQL::Breadth::ExecutionError.new("Not okay!", extensions: { "code" => "TEST", reason: "sorry", }), @@ -160,7 +160,7 @@ def test_pushes_original_errors_into_context schema = "type Query { test: String! }" cause = StandardError.new("Boom.") source = { - "test" => GraphQL::BreadthExec::ExecutionError.new("Not okay!", cause: cause), + "test" => GraphQL::Breadth::ExecutionError.new("Not okay!", cause: cause), } result = exec_test(schema, "{ test }", source) do |executor| @@ -192,7 +192,7 @@ def test_formats_error_message_for_non_null_list_items private def exec_test(schema, query, source) - executor = GraphQL::BreadthExec::Executor.new( + executor = GraphQL::Breadth::Executor.new( GraphQL::Schema.from_definition(schema), GraphQL.parse(query), resolvers: TEST_RESOLVERS, diff --git a/test/graphql/breadth_exec/executor/errors_test.rb b/test/graphql/breadth/executor/errors_test.rb similarity index 82% rename from test/graphql/breadth_exec/executor/errors_test.rb rename to test/graphql/breadth/executor/errors_test.rb index b8be10b..f54314b 100644 --- a/test/graphql/breadth_exec/executor/errors_test.rb +++ b/test/graphql/breadth/executor/errors_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ErrorsTest < Minitest::Test +class GraphQL::Breadth::Executor::ErrorsTest < Minitest::Test BUILD_ERROR_SCHEMA = GraphQL::Schema.from_definition(%| type Query { badList: [String] @@ -10,14 +10,14 @@ class GraphQL::BreadthExec::Executor::ErrorsTest < Minitest::Test } |) - BUILD_ERROR_SCHEMA.rescue_from(GraphQL::BreadthExec::InvalidListResultError) do + BUILD_ERROR_SCHEMA.rescue_from(GraphQL::Breadth::InvalidListResultError) do GraphQL::ExecutionError.new("Handled build error") end BUILD_ERROR_RESOLVERS = { "Query" => { - "badList" => GraphQL::BreadthExec::HashKeyResolver.new("badList"), - "ok" => GraphQL::BreadthExec::HashKeyResolver.new("ok"), + "badList" => GraphQL::Breadth::HashKeyResolver.new("badList"), + "ok" => GraphQL::Breadth::HashKeyResolver.new("ok"), }, }.freeze @@ -38,22 +38,22 @@ def self.coerce_result(_value, _context) end class ResultCoercionField < GraphQL::Schema::Field - include GraphQL::BreadthExec::HasBreadthResolver::Field + include GraphQL::Breadth::HasBreadthResolver::Field end class ResultCoercionQuery < GraphQL::Schema::Object field_class ResultCoercionField field :nullable_nil, NilResultScalar, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("nullableNil") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("nullableNil") end field :required_nil, NilResultScalar, null: false do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("requiredNil") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("requiredNil") end field :nullable_error, ErrorResultScalar, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("nullableError") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("nullableError") end end @@ -85,7 +85,7 @@ def test_nullable_positional_error_adds_path "nodes" => [ { "maybe" => "okay!" }, { "maybe" => nil }, - { "maybe" => GraphQL::BreadthExec::ExecutionError.new("Not okay!") }, + { "maybe" => GraphQL::Breadth::ExecutionError.new("Not okay!") }, ], }, } @@ -107,7 +107,7 @@ def test_nullable_positional_error_adds_path }], } - assert_equal expected, breadth_exec(document, source) + assert_equal expected, breadth(document, source) end def test_handled_build_result_error_stays_field_local @@ -133,7 +133,7 @@ def test_handled_build_result_error_stays_field_local }], } - executor = GraphQL::BreadthExec::Executor.new(BUILD_ERROR_SCHEMA, document, resolvers: BUILD_ERROR_RESOLVERS, root_object: source) + executor = GraphQL::Breadth::Executor.new(BUILD_ERROR_SCHEMA, document, resolvers: BUILD_ERROR_RESOLVERS, root_object: source) assert_equal expected, executor.result end @@ -193,7 +193,7 @@ def test_non_null_positional_error_adds_path_and_propagates "products" => { "nodes" => [ { "must" => "okay!" }, - { "must" => GraphQL::BreadthExec::ExecutionError.new("Not okay!") }, + { "must" => GraphQL::Breadth::ExecutionError.new("Not okay!") }, ], }, } @@ -209,7 +209,7 @@ def test_non_null_positional_error_adds_path_and_propagates }], } - assert_equal expected, breadth_exec(document, source) + assert_equal expected, breadth(document, source) end def test_null_in_non_null_position_propagates @@ -242,7 +242,7 @@ def test_null_in_non_null_position_propagates }], } - assert_equal expected, breadth_exec(document, source) + assert_equal expected, breadth(document, source) end private @@ -250,7 +250,7 @@ def test_null_in_non_null_position_propagates def execute_result_coercion_query(document, source) ResultCoercionSchema.type_errors = [] - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( ResultCoercionSchema, GraphQL.parse(document), root_object: source, diff --git a/test/graphql/breadth_exec/executor/execution_field_test.rb b/test/graphql/breadth/executor/execution_field_test.rb similarity index 90% rename from test/graphql/breadth_exec/executor/execution_field_test.rb rename to test/graphql/breadth/executor/execution_field_test.rb index a5c3a76..c4f2298 100644 --- a/test/graphql/breadth_exec/executor/execution_field_test.rb +++ b/test/graphql/breadth/executor/execution_field_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ExecutionFieldTest < Minitest::Test +class GraphQL::Breadth::Executor::ExecutionFieldTest < Minitest::Test TEST_SCHEMA = GraphQL::Schema.from_definition(%| input Input { name: String } @@ -65,7 +65,7 @@ def test_resolve_all_maps_graphql_execution_error_to_object_set assert_equal 2, result.size assert result.all? { _1.equal?(result.first) } - assert_instance_of GraphQL::BreadthExec::ExecutionError, result.first + assert_instance_of GraphQL::Breadth::ExecutionError, result.first assert_equal exec_field, result.first.exec_field assert_nil result.first.path end @@ -103,12 +103,12 @@ def test_field_and_scope_attributes_api def build_field(document_string, variables: {}, objects: [{}]) document = GraphQL.parse(document_string) - executor = GraphQL::BreadthExec::Executor.new(TEST_SCHEMA, document, variables: variables) + executor = GraphQL::Breadth::Executor.new(TEST_SCHEMA, document, variables: variables) operation = executor.query.selected_operation executor.input.coerce_variable_values(operation.variables, executor.query.provided_variables || {}) node = operation.selections.first parent_type = executor.query.root_type_for_operation(operation.operation_type) - exec_scope = GraphQL::BreadthExec::Executor::ExecutionScope.new( + exec_scope = GraphQL::Breadth::Executor::ExecutionScope.new( executor: executor, parent_type: parent_type, selections: operation.selections, @@ -117,12 +117,12 @@ def build_field(document_string, variables: {}, objects: [{}]) ) definition = executor.query.get_field(parent_type, node.name) - GraphQL::BreadthExec::Executor::ExecutionField.new( + GraphQL::Breadth::Executor::ExecutionField.new( node.alias || node.name, nodes: [node], scope: exec_scope, definition: definition, - resolver: GraphQL::BreadthExec::HashKeyResolver.new(node.name), + resolver: GraphQL::Breadth::HashKeyResolver.new(node.name), ) end end diff --git a/test/graphql/breadth_exec/executor/execution_promise_test.rb b/test/graphql/breadth/executor/execution_promise_test.rb similarity index 58% rename from test/graphql/breadth_exec/executor/execution_promise_test.rb rename to test/graphql/breadth/executor/execution_promise_test.rb index 2bc4a5b..49774c9 100644 --- a/test/graphql/breadth_exec/executor/execution_promise_test.rb +++ b/test/graphql/breadth/executor/execution_promise_test.rb @@ -2,9 +2,9 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::ExecutionPromiseTest < Minitest::Test +class GraphQL::Breadth::Executor::ExecutionPromiseTest < Minitest::Test def test_then_dispatches_synchronously - promise = GraphQL::BreadthExec::Executor::ExecutionPromise.new + promise = GraphQL::Breadth::Executor::ExecutionPromise.new seen = nil chained = promise.then { |value| seen = value.upcase } @@ -16,8 +16,8 @@ def test_then_dispatches_synchronously end def test_then_adopts_nested_execution_promise - inner = GraphQL::BreadthExec::Executor::ExecutionPromise.new - outer = GraphQL::BreadthExec::Executor::ExecutionPromise.new { |resolve, _reject| resolve.call(inner) } + inner = GraphQL::Breadth::Executor::ExecutionPromise.new + outer = GraphQL::Breadth::Executor::ExecutionPromise.new { |resolve, _reject| resolve.call(inner) } refute_predicate outer, :resolved? inner.send(:resolve, "done") @@ -28,16 +28,16 @@ def test_then_adopts_nested_execution_promise def test_registry_tracks_chained_promises registry = [] - promise = GraphQL::BreadthExec::Executor::ExecutionPromise.new(registry: registry) + promise = GraphQL::Breadth::Executor::ExecutionPromise.new(registry: registry) chained = promise.then { |value| value } assert_equal [promise, chained], registry end def test_all_resolves_in_order - first = GraphQL::BreadthExec::Executor::ExecutionPromise.new - second = GraphQL::BreadthExec::Executor::ExecutionPromise.new - all = GraphQL::BreadthExec::Executor::ExecutionPromise.all([first, second]) + first = GraphQL::Breadth::Executor::ExecutionPromise.new + second = GraphQL::Breadth::Executor::ExecutionPromise.new + all = GraphQL::Breadth::Executor::ExecutionPromise.all([first, second]) second.send(:resolve, "b") first.send(:resolve, "a") diff --git a/test/graphql/breadth_exec/executor/fragments_test.rb b/test/graphql/breadth/executor/fragments_test.rb similarity index 81% rename from test/graphql/breadth_exec/executor/fragments_test.rb rename to test/graphql/breadth/executor/fragments_test.rb index 1a73a0e..0f89434 100644 --- a/test/graphql/breadth_exec/executor/fragments_test.rb +++ b/test/graphql/breadth/executor/fragments_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::FragmentsTest < Minitest::Test +class GraphQL::Breadth::Executor::FragmentsTest < Minitest::Test SOURCE = { "products" => { "nodes" => [{ @@ -27,7 +27,7 @@ def test_selects_via_inline_fragments }, } - assert_equal expected, breadth_exec(document, SOURCE).dig("data") + assert_equal expected, breadth(document, SOURCE).dig("data") end def test_selects_via_fragment_spreads @@ -48,7 +48,7 @@ def test_selects_via_fragment_spreads }, } - assert_equal expected, breadth_exec(document, SOURCE).dig("data") + assert_equal expected, breadth(document, SOURCE).dig("data") end def test_selects_via_nested_fragments @@ -71,7 +71,7 @@ def test_selects_via_nested_fragments }, } - assert_equal expected, breadth_exec(document, SOURCE).dig("data") + assert_equal expected, breadth(document, SOURCE).dig("data") end def test_selects_via_abstract_fragments @@ -93,6 +93,6 @@ def test_selects_via_abstract_fragments }, } - assert_equal expected, breadth_exec(document, SOURCE).dig("data") + assert_equal expected, breadth(document, SOURCE).dig("data") end end diff --git a/test/graphql/breadth_exec/executor/incremental_test.rb b/test/graphql/breadth/executor/incremental_test.rb similarity index 97% rename from test/graphql/breadth_exec/executor/incremental_test.rb rename to test/graphql/breadth/executor/incremental_test.rb index 22a13ab..db0497e 100644 --- a/test/graphql/breadth_exec/executor/incremental_test.rb +++ b/test/graphql/breadth/executor/incremental_test.rb @@ -2,8 +2,8 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::IncrementalTest < Minitest::Test - class BatchTrackingLoader < GraphQL::BreadthExec::LazyLoader +class GraphQL::Breadth::Executor::IncrementalTest < Minitest::Test + class BatchTrackingLoader < GraphQL::Breadth::LazyLoader class << self attr_accessor :perform_keys end @@ -20,7 +20,7 @@ def perform_map(keys, _ctx) end end - class LazyHashResolver < GraphQL::BreadthExec::FieldResolver + class LazyHashResolver < GraphQL::Breadth::FieldResolver def initialize(key) @key = key end @@ -77,7 +77,7 @@ def test_incremental_result_returns_normal_result_without_defer }, } - assert_instance_of GraphQL::BreadthExec::Incremental::Result, result + assert_instance_of GraphQL::Breadth::Incremental::Result, result refute result.incremental? assert_equal expected, result.initial_result assert_equal [], result.subsequent_results.to_a @@ -92,7 +92,7 @@ def test_incremental_result_raises_after_result }|) executor.result - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do executor.incremental_result end @@ -107,7 +107,7 @@ def test_result_raises_after_incremental_result }|) executor.incremental_result - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do executor.result end @@ -146,7 +146,7 @@ def test_incremental_result_does_not_defer_when_if_is_false }, } - assert_instance_of GraphQL::BreadthExec::Incremental::Result, result + assert_instance_of GraphQL::Breadth::Incremental::Result, result refute result.incremental? assert_equal expected, result.initial_result assert_equal [], result.subsequent_results.to_a @@ -198,7 +198,7 @@ def test_incremental_result_defers_fragment_for_each_list_object } }|).incremental_result - assert_instance_of GraphQL::BreadthExec::Incremental::Result, result + assert_instance_of GraphQL::Breadth::Incremental::Result, result assert_equal( { "data" => { @@ -855,7 +855,7 @@ def test_incremental_result_filters_deferred_payload_for_null_bubbled_list_eleme } }|, source: source).incremental_result - assert_instance_of GraphQL::BreadthExec::Incremental::Result, result + assert_instance_of GraphQL::Breadth::Incremental::Result, result initial = result.initial_result assert_equal( @@ -916,7 +916,7 @@ def test_ready_deferred_executions_batch_lazy_work private def build_executor(document, source: SOURCE, resolvers: BREADTH_RESOLVERS) - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, GraphQL.parse(document), resolvers: resolvers, diff --git a/test/graphql/breadth_exec/executor/input_formatter_test.rb b/test/graphql/breadth/executor/input_formatter_test.rb similarity index 98% rename from test/graphql/breadth_exec/executor/input_formatter_test.rb rename to test/graphql/breadth/executor/input_formatter_test.rb index 6a217b9..9f9f728 100644 --- a/test/graphql/breadth_exec/executor/input_formatter_test.rb +++ b/test/graphql/breadth/executor/input_formatter_test.rb @@ -4,10 +4,10 @@ require "test_helper" module GraphQL - module BreadthExec + module Breadth class Executor class InputFormatterTest < Minitest::Test - UNDEFINED = GraphQL::BreadthExec::Executor::UNDEFINED + UNDEFINED = GraphQL::Breadth::Executor::UNDEFINED TEST_SCHEMA = GraphQL::Schema.from_definition(%| enum TestStatus { @@ -87,8 +87,8 @@ def coerce_input(_input, _context) def setup @context = GraphQL::Query.new(TEST_SCHEMA, "{ __typename }").context - @input = GraphQL::BreadthExec::Executor::InputFormatter.new(@context) - @state = GraphQL::BreadthExec::Executor::InputFormatter::State.new + @input = GraphQL::Breadth::Executor::InputFormatter.new(@context) + @state = GraphQL::Breadth::Executor::InputFormatter::State.new end # === type_from_ast === @@ -176,7 +176,7 @@ def test_coerce_variable_values_empty_inputs_returns_empty def test_coerce_variable_values_non_input_type_raises_error var_defs = get_variables_ast("$value: Query") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, { "value" => {} }) end @@ -187,7 +187,7 @@ def test_coerce_variable_values_non_input_type_raises_error def test_coerce_variable_values_unknown_type_raises_error var_defs = get_variables_ast("$value: UnknownType!") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, { "value" => "anything" }) end @@ -225,7 +225,7 @@ def test_coerce_variable_values_applies_default_when_not_provided def test_coerce_variable_values_raises_error_when_non_null_isnt_provided var_defs = get_variables_ast("$name: String!") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, {}) end @@ -236,7 +236,7 @@ def test_coerce_variable_values_raises_error_when_non_null_isnt_provided def test_coerce_variable_values_raises_error_when_non_null_provided_as_null var_defs = get_variables_ast("$name: String!") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, { "name" => nil }) end @@ -261,7 +261,7 @@ def test_coerce_variable_values_nullable_variable_with_null_value_is_null def test_coerce_variable_values_invalid_value_raises_error var_defs = get_variables_ast("$count: Int") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, { "count" => "not-an-int" }) end @@ -279,7 +279,7 @@ def test_coerce_variable_values_formats_multiple_variables def test_coerce_variable_values_raises_error_for_list_of_non_null_containing_null var_defs = get_variables_ast("$input: [String!]") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, { "input" => ["A", nil, "B"] }) end @@ -290,7 +290,7 @@ def test_coerce_variable_values_raises_error_for_list_of_non_null_containing_nul def test_coerce_variable_values_raises_with_multiple_errors var_defs = get_variables_ast("$a: String!, $b: Int!") - error = assert_raises(GraphQL::BreadthExec::InputValidationErrorSet) do + error = assert_raises(GraphQL::Breadth::InputValidationErrorSet) do @input.coerce_variable_values(var_defs, {}) end @@ -442,7 +442,7 @@ def test_coerce_argument_values_preserves_extensions_from_coerce_input_execution assert_equal(1, errors.size) assert_equal("Custom coercion failure", errors.first.message) assert_equal({ "code" => "CUSTOM_ERROR" }, errors.first.extensions) - end #: as GraphQL::BreadthExec::InputValidationErrorSet + end #: as GraphQL::Breadth::InputValidationErrorSet assert_equal(1, reported.errors.size) end @@ -457,7 +457,7 @@ def test_coerce_argument_values_reports_error_for_coercion_errors assert_equal({}, result) assert_equal(1, errors.size) assert_instance_of(InputCoercionError, errors.first) - end #: as GraphQL::BreadthExec::InputValidationErrorSet + end #: as GraphQL::Breadth::InputValidationErrorSet assert_equal(1, reported.errors.size) assert_equal('Argument "input" of non-null type "RequiredFieldsInput!" must not be null.', reported.message) diff --git a/test/graphql/breadth_exec/executor/introspection_test.rb b/test/graphql/breadth/executor/introspection_test.rb similarity index 97% rename from test/graphql/breadth_exec/executor/introspection_test.rb rename to test/graphql/breadth/executor/introspection_test.rb index 93ab497..b225144 100644 --- a/test/graphql/breadth_exec/executor/introspection_test.rb +++ b/test/graphql/breadth/executor/introspection_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::IntrospectionTest < Minitest::Test +class GraphQL::Breadth::Executor::IntrospectionTest < Minitest::Test TEST_SCHEMA = GraphQL::Schema.from_definition(%| """@stitch description""" directive @stitch(key: ID!) repeatable on FIELD_DEFINITION @@ -60,9 +60,9 @@ class GraphQL::BreadthExec::Executor::IntrospectionTest < Minitest::Test TEST_SCHEMA.get_type("String").specified_by_url("https://string.test") TEST_RESOLVERS = { - **GraphQL::BreadthExec::Introspection::TYPE_RESOLVERS, + **GraphQL::Breadth::Introspection::TYPE_RESOLVERS, "Query" => { - **GraphQL::BreadthExec::Introspection::ENTRYPOINT_RESOLVERS, + **GraphQL::Breadth::Introspection::ENTRYPOINT_RESOLVERS, } }.freeze @@ -537,6 +537,6 @@ def test_introspect_argument_default_values private def execute_query(document, schema: TEST_SCHEMA) - GraphQL::BreadthExec::Executor.new(schema, GraphQL.parse(document), resolvers: TEST_RESOLVERS, root_object: {}).result + GraphQL::Breadth::Executor.new(schema, GraphQL.parse(document), resolvers: TEST_RESOLVERS, root_object: {}).result end end diff --git a/test/graphql/breadth_exec/executor/lists_test.rb b/test/graphql/breadth/executor/lists_test.rb similarity index 80% rename from test/graphql/breadth_exec/executor/lists_test.rb rename to test/graphql/breadth/executor/lists_test.rb index 01e1b0c..01ee004 100644 --- a/test/graphql/breadth_exec/executor/lists_test.rb +++ b/test/graphql/breadth/executor/lists_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class ObjectPathResolver < GraphQL::BreadthExec::FieldResolver +class ObjectPathResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects_with_index do |_object, index| "#{exec_field.scope.object_path(index).join(".")}|#{exec_field.object_path(index).join(".")}" @@ -10,7 +10,7 @@ def resolve(exec_field, _ctx) end end -class GraphQL::BreadthExec::Executor::ListsTest < Minitest::Test +class GraphQL::Breadth::Executor::ListsTest < Minitest::Test TEST_SCHEMA = GraphQL::Schema.from_definition(%| enum WidgetStatus { YES @@ -34,18 +34,18 @@ class GraphQL::BreadthExec::Executor::ListsTest < Minitest::Test TEST_RESOLVERS = { "Widget" => { - "parent" => GraphQL::BreadthExec::HashKeyResolver.new("parent"), - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), - "status" => GraphQL::BreadthExec::HashKeyResolver.new("status"), - "titles" => GraphQL::BreadthExec::HashKeyResolver.new("titles"), - "statuses" => GraphQL::BreadthExec::HashKeyResolver.new("statuses"), - "children" => GraphQL::BreadthExec::HashKeyResolver.new("children"), - "titleGroups" => GraphQL::BreadthExec::HashKeyResolver.new("titleGroups"), - "statusGroups" => GraphQL::BreadthExec::HashKeyResolver.new("statusGroups"), - "childGroups" => GraphQL::BreadthExec::HashKeyResolver.new("childGroups"), + "parent" => GraphQL::Breadth::HashKeyResolver.new("parent"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), + "status" => GraphQL::Breadth::HashKeyResolver.new("status"), + "titles" => GraphQL::Breadth::HashKeyResolver.new("titles"), + "statuses" => GraphQL::Breadth::HashKeyResolver.new("statuses"), + "children" => GraphQL::Breadth::HashKeyResolver.new("children"), + "titleGroups" => GraphQL::Breadth::HashKeyResolver.new("titleGroups"), + "statusGroups" => GraphQL::Breadth::HashKeyResolver.new("statusGroups"), + "childGroups" => GraphQL::Breadth::HashKeyResolver.new("childGroups"), }, "Query" => { - "widget" => GraphQL::BreadthExec::HashKeyResolver.new("widget"), + "widget" => GraphQL::Breadth::HashKeyResolver.new("widget"), }, }.freeze @@ -202,6 +202,6 @@ def test_resolves_nested_leaf_lists private def execute_query(document, source, schema: TEST_SCHEMA, resolvers: TEST_RESOLVERS) - GraphQL::BreadthExec::Executor.new(schema, GraphQL.parse(document), resolvers: resolvers, root_object: source).result + GraphQL::Breadth::Executor.new(schema, GraphQL.parse(document), resolvers: resolvers, root_object: source).result end end diff --git a/test/graphql/breadth_exec/executor/loaders_test.rb b/test/graphql/breadth/executor/loaders_test.rb similarity index 81% rename from test/graphql/breadth_exec/executor/loaders_test.rb rename to test/graphql/breadth/executor/loaders_test.rb index 1345f6d..71c3219 100644 --- a/test/graphql/breadth_exec/executor/loaders_test.rb +++ b/test/graphql/breadth/executor/loaders_test.rb @@ -2,9 +2,9 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::LoadersTest < Minitest::Test +class GraphQL::Breadth::Executor::LoadersTest < Minitest::Test - class FancyLoader < GraphQL::BreadthExec::LazyLoader + class FancyLoader < GraphQL::Breadth::LazyLoader class << self attr_accessor :perform_keys end @@ -26,19 +26,19 @@ def perform_map(keys, _ctx) end end - class FirstResolver < GraphQL::BreadthExec::FieldResolver + class FirstResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.lazy(loader_class: FancyLoader, args: { group: "a" }, keys: exec_field.objects.map { _1["first"] }) end end - class SecondResolver < GraphQL::BreadthExec::FieldResolver + class SecondResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.lazy(loader_class: FancyLoader, args: { group: "a" }, keys: exec_field.objects.map { _1["second"] }) end end - class ThirdResolver < GraphQL::BreadthExec::FieldResolver + class ThirdResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.lazy(loader_class: FancyLoader, args: { group: "b" }, keys: exec_field.objects.map { _1["third"] }).then do |values| values @@ -46,7 +46,7 @@ def resolve(exec_field, _ctx) end end - class EagerValuesResolver < GraphQL::BreadthExec::FieldResolver + class EagerValuesResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.lazy( loader_class: FancyLoader, @@ -57,7 +57,7 @@ def resolve(exec_field, _ctx) end end - class FieldPreloadResolver < GraphQL::BreadthExec::FieldResolver + class FieldPreloadResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, _ctx) exec_field.on_preload do exec_field.preload( @@ -75,7 +75,7 @@ def resolve(exec_field, _ctx) end end - class ScopePreloadResolver < GraphQL::BreadthExec::FieldResolver + class ScopePreloadResolver < GraphQL::Breadth::FieldResolver def plan(exec_field, _ctx) exec_scope = exec_field.scope exec_scope.on_preload do @@ -113,11 +113,11 @@ def resolve(exec_field, _ctx) "first" => FirstResolver.new, "second" => SecondResolver.new, "third" => ThirdResolver.new, - "syncObject" => GraphQL::BreadthExec::HashKeyResolver.new("syncObject"), - "syncScalar" => GraphQL::BreadthExec::HashKeyResolver.new("syncScalar"), + "syncObject" => GraphQL::Breadth::HashKeyResolver.new("syncObject"), + "syncScalar" => GraphQL::Breadth::HashKeyResolver.new("syncScalar"), }, "Query" => { - "widget" => GraphQL::BreadthExec::HashKeyResolver.new("widget"), + "widget" => GraphQL::Breadth::HashKeyResolver.new("widget"), }, }.freeze @@ -152,7 +152,7 @@ def test_splits_loaders_by_group_across_fields } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) assert_equal expected, executor.result assert_equal [["Apple", "Banana"], ["Coconut"]], FancyLoader.perform_keys end @@ -186,7 +186,7 @@ def test_maintains_ordered_selections_around_object_fields } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) result = executor.result assert_equal expected, result assert_equal result.dig("data", "widget").keys, expected.dig("data", "widget").keys @@ -221,7 +221,7 @@ def test_maintains_ordered_selections_around_leaf_fields } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: LOADER_RESOLVERS, root_object: source) result = executor.result assert_equal expected, result assert_equal result.dig("data", "widget").keys, expected.dig("data", "widget").keys @@ -255,7 +255,7 @@ def test_lazy_field_uses_eager_values_without_loading_them } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) assert_equal expected, executor.result assert_equal [["Banana"]], FancyLoader.perform_keys end @@ -285,7 +285,7 @@ def test_resumes_field_execution_after_lazy_preloads } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) assert_equal expected, executor.result assert_equal [["Apple"]], FancyLoader.perform_keys end @@ -318,7 +318,7 @@ def test_resumes_scope_execution_after_lazy_preloads } } - executor = GraphQL::BreadthExec::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) + executor = GraphQL::Breadth::Executor.new(LOADER_SCHEMA, document, resolvers: resolvers, root_object: source) assert_equal expected, executor.result assert_equal [["Apple"], ["Banana"]], FancyLoader.perform_keys end diff --git a/test/graphql/breadth_exec/executor/star_wars_test.rb b/test/graphql/breadth/executor/star_wars_test.rb similarity index 99% rename from test/graphql/breadth_exec/executor/star_wars_test.rb rename to test/graphql/breadth/executor/star_wars_test.rb index e642083..54fda1b 100644 --- a/test/graphql/breadth_exec/executor/star_wars_test.rb +++ b/test/graphql/breadth/executor/star_wars_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::StarWarsTest < Minitest::Test +class GraphQL::Breadth::Executor::StarWarsTest < Minitest::Test def test_correctly_identifies_r2d2_as_the_hero result = execute_star_wars <<~GRAPHQL query HeroNameQuery { diff --git a/test/graphql/breadth_exec/executor/subscriptions_test.rb b/test/graphql/breadth/executor/subscriptions_test.rb similarity index 89% rename from test/graphql/breadth_exec/executor/subscriptions_test.rb rename to test/graphql/breadth/executor/subscriptions_test.rb index 71281d3..de54f91 100644 --- a/test/graphql/breadth_exec/executor/subscriptions_test.rb +++ b/test/graphql/breadth/executor/subscriptions_test.rb @@ -4,7 +4,7 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::SubscriptionsTest < Minitest::Test +class GraphQL::Breadth::Executor::SubscriptionsTest < Minitest::Test SUBSCRIPTION_SCHEMA = GraphQL::Schema.from_definition(%| schema { query: Query @@ -26,7 +26,7 @@ class GraphQL::BreadthExec::Executor::SubscriptionsTest < Minitest::Test } |) - class SourceResolver < GraphQL::BreadthExec::FieldResolver + class SourceResolver < GraphQL::Breadth::FieldResolver attr_reader :resolve_calls attr_reader :subscribe_calls @@ -52,14 +52,14 @@ def resolve(exec_field, ctx) end end - class EventErrorResolver < GraphQL::BreadthExec::FieldResolver + class EventErrorResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.resolve_all(GraphQL::ExecutionError.new("Event failed")) end end def test_result_raises_for_subscription_operation - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do subscription_executor(%|subscription { onWriteValue { value } }|).result end @@ -67,7 +67,7 @@ def test_result_raises_for_subscription_operation end def test_incremental_result_raises_for_subscription_operation - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do subscription_executor(%|subscription { onWriteValue { value } }|).incremental_result end @@ -75,7 +75,7 @@ def test_incremental_result_raises_for_subscription_operation end def test_subscribe_raises_for_query_operation - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do subscription_executor(%|{ noop }|).subscribe end @@ -88,7 +88,7 @@ def test_subscribe_returns_subscription_response_stream_for_subscription_operati root_object: { "events" => [{ "value" => "direct" }] }, ).subscribe - assert_instance_of GraphQL::BreadthExec::SubscriptionResponseStream, result + assert_instance_of GraphQL::Breadth::SubscriptionResponseStream, result assert_equal [{ "data" => { "onWriteValue" => { "value" => "direct" } } }], result.to_a end @@ -105,7 +105,7 @@ def test_subscribe_raises_for_plain_field_resolver_without_subscription_hook error = assert_raises(NotImplementedError) do subscription_executor( %|subscription { onWriteValue { value } }|, - source_resolver: GraphQL::BreadthExec::SelfResolver.new, + source_resolver: GraphQL::Breadth::SelfResolver.new, ).subscribe end @@ -115,7 +115,7 @@ def test_subscribe_raises_for_plain_field_resolver_without_subscription_hook def test_subscribe_raises_for_non_enumerable_source resolver = SourceResolver.new(subscribe: ->(_exec_field, _ctx) { Object.new }) - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do subscription_executor(%|subscription { onWriteValue { value } }|, source_resolver: resolver).subscribe end @@ -123,9 +123,9 @@ def test_subscribe_raises_for_non_enumerable_source end def test_subscribe_raises_for_lazy_source_setup - resolver = SourceResolver.new(subscribe: ->(_exec_field, _ctx) { GraphQL::BreadthExec::Executor::ExecutionPromise.new }) + resolver = SourceResolver.new(subscribe: ->(_exec_field, _ctx) { GraphQL::Breadth::Executor::ExecutionPromise.new }) - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do subscription_executor(%|subscription { onWriteValue { value } }|, source_resolver: resolver).subscribe end @@ -138,7 +138,7 @@ def test_stream_yields_one_result_for_one_source_event root_object: { "events" => [{ "value" => "first" }] }, ).subscribe - assert_instance_of GraphQL::BreadthExec::SubscriptionResponseStream, stream + assert_instance_of GraphQL::Breadth::SubscriptionResponseStream, stream assert_equal( [{ "data" => { "onWriteValue" => { "value" => "first" } } }], stream.to_a, @@ -294,7 +294,7 @@ def test_result_raises_after_subscription_response_stream_was_created executor = subscription_executor(%|subscription { onWriteValue { value } }|) executor.subscribe - error = assert_raises(GraphQL::BreadthExec::ImplementationError) { executor.result } + error = assert_raises(GraphQL::Breadth::ImplementationError) { executor.result } assert_equal "Use subscribe for subscription operations", error.message end @@ -302,7 +302,7 @@ def test_result_raises_after_subscription_response_stream_was_created private def subscription_executor(document, source_resolver: SourceResolver.new, root_object: { "events" => [] }, context: {}) - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SUBSCRIPTION_SCHEMA, GraphQL.parse(document), resolvers: subscription_resolvers(source_resolver), @@ -314,13 +314,13 @@ def subscription_executor(document, source_resolver: SourceResolver.new, root_ob def subscription_resolvers(source_resolver) { "Query" => { - "noop" => GraphQL::BreadthExec::ValueResolver.new("noop"), + "noop" => GraphQL::Breadth::ValueResolver.new("noop"), }, "Subscription" => { "onWriteValue" => source_resolver, }, "WriteValuePayload" => { - "value" => GraphQL::BreadthExec::HashKeyResolver.new("value"), + "value" => GraphQL::Breadth::HashKeyResolver.new("value"), "lazyValue" => DeferredHashResolver.new("lazyValue"), "error" => EventErrorResolver.new, }, diff --git a/test/graphql/breadth_exec/executor/tracers_test.rb b/test/graphql/breadth/executor/tracers_test.rb similarity index 89% rename from test/graphql/breadth_exec/executor/tracers_test.rb rename to test/graphql/breadth/executor/tracers_test.rb index f0c213e..ea36ce4 100644 --- a/test/graphql/breadth_exec/executor/tracers_test.rb +++ b/test/graphql/breadth/executor/tracers_test.rb @@ -2,14 +2,14 @@ require "test_helper" -class GraphQL::BreadthExec::Executor::TracersTest < Minitest::Test - class BoomResolver < GraphQL::BreadthExec::FieldResolver +class GraphQL::Breadth::Executor::TracersTest < Minitest::Test + class BoomResolver < GraphQL::Breadth::FieldResolver def resolve(_exec_field, _context) raise StandardError, "boom" end end - class ExtensionTracer < GraphQL::BreadthExec::Tracer + class ExtensionTracer < GraphQL::Breadth::Tracer def before_execute(executor, _context) executor.response_extensions[:trace] = { started: true } end @@ -19,7 +19,7 @@ def after_execute(executor, _context, duration:) end end - class TestTracer < GraphQL::BreadthExec::Tracer + class TestTracer < GraphQL::Breadth::Tracer attr_reader :start_calls, :finish_calls, :finish_durations attr_reader :before_execute_calls, :after_execute_calls, :execute_durations attr_reader :before_format_errors_calls @@ -130,21 +130,21 @@ def setup end def test_calls_start_finish_and_execute_hooks - result = breadth_exec(@document, @source, tracers: [@tracer]) + result = breadth(@document, @source, tracers: [@tracer]) assert_equal @source, result["data"] assert_equal 1, @tracer.start_calls.size assert_equal 1, @tracer.finish_calls.size assert_equal 1, @tracer.before_execute_calls.size assert_equal 1, @tracer.after_execute_calls.size - assert @tracer.start_calls.all? { _1.is_a?(GraphQL::BreadthExec::Executor) } - assert @tracer.finish_calls.all? { _1.is_a?(GraphQL::BreadthExec::Executor) } + assert @tracer.start_calls.all? { _1.is_a?(GraphQL::Breadth::Executor) } + assert @tracer.finish_calls.all? { _1.is_a?(GraphQL::Breadth::Executor) } assert @tracer.finish_durations.all? { _1.is_a?(Float) && _1 >= 0 } assert @tracer.execute_durations.all? { _1.is_a?(Float) && _1 >= 0 } end def test_calls_field_resolve_hooks_with_parent_type_field_name_and_object_count - result = breadth_exec(@document, @source, tracers: [@tracer]) + result = breadth(@document, @source, tracers: [@tracer]) assert_equal @source, result["data"] assert_equal( @@ -160,7 +160,7 @@ def test_calls_field_resolve_hooks_with_parent_type_field_name_and_object_count end def test_calls_scope_hooks - result = breadth_exec(@document, @source, tracers: [@tracer]) + result = breadth(@document, @source, tracers: [@tracer]) assert_equal @source, result["data"] assert_equal( @@ -170,7 +170,7 @@ def test_calls_scope_hooks end def test_calls_field_result_build_hooks - result = breadth_exec(@document, @source, tracers: [@tracer]) + result = breadth(@document, @source, tracers: [@tracer]) assert_equal @source, result["data"] assert_equal( @@ -186,14 +186,14 @@ def test_calls_field_result_build_hooks end def test_calls_before_format_errors - breadth_exec(@document, @source, tracers: [@tracer]) + breadth(@document, @source, tracers: [@tracer]) assert_equal 1, @tracer.before_format_errors_calls.size - assert @tracer.before_format_errors_calls.all? { _1.is_a?(GraphQL::BreadthExec::Executor) } + assert @tracer.before_format_errors_calls.all? { _1.is_a?(GraphQL::Breadth::Executor) } end def test_tracers_can_install_response_extensions - result = breadth_exec(@document, @source, tracers: [ExtensionTracer.new]) + result = breadth(@document, @source, tracers: [ExtensionTracer.new]) assert_equal( { @@ -214,7 +214,7 @@ def test_calls_on_exception_for_unhandled_errors ) assert_raises(StandardError) do - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( SCHEMA, GraphQL.parse("{ products(first: 1) { nodes { id } } }"), resolvers: resolvers, diff --git a/test/graphql/breadth_exec/executor_test.rb b/test/graphql/breadth/executor_test.rb similarity index 79% rename from test/graphql/breadth_exec/executor_test.rb rename to test/graphql/breadth/executor_test.rb index 34a5331..8be1582 100644 --- a/test/graphql/breadth_exec/executor_test.rb +++ b/test/graphql/breadth/executor_test.rb @@ -2,9 +2,9 @@ require "test_helper" -class GraphQL::BreadthExec::ExecutorTest < Minitest::Test +class GraphQL::Breadth::ExecutorTest < Minitest::Test def test_resolves_basic_query - assert_equal BASIC_SOURCE, breadth_exec(BASIC_DOCUMENT, BASIC_SOURCE).dig("data") + assert_equal BASIC_SOURCE, breadth(BASIC_DOCUMENT, BASIC_SOURCE).dig("data") end def test_resolves_typename_field @@ -31,7 +31,7 @@ def test_resolves_typename_field "node" => { "__typename" => "Product" }, } - assert_equal expected, breadth_exec(document, source).dig("data") + assert_equal expected, breadth(document, source).dig("data") end def test_mutations_run_serially @@ -57,14 +57,14 @@ def test_mutations_run_serially "c" => { "value" => "test3" }, } - assert_equal expected, breadth_exec(document, source).dig("data") + assert_equal expected, breadth(document, source).dig("data") end def test_raises_not_implemented_for_missing_resolvers document = %|{ noResolver }| error = assert_raises(NotImplementedError) do - breadth_exec(document, {}) + breadth(document, {}) end assert_equal "No field resolver for 'Query.noResolver'", error.message diff --git a/test/graphql/breadth_exec/field_resolvers_test.rb b/test/graphql/breadth/field_resolvers_test.rb similarity index 75% rename from test/graphql/breadth_exec/field_resolvers_test.rb rename to test/graphql/breadth/field_resolvers_test.rb index cf20d98..064b98d 100644 --- a/test/graphql/breadth_exec/field_resolvers_test.rb +++ b/test/graphql/breadth/field_resolvers_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class GraphQL::BreadthExec::FieldResolversTest < Minitest::Test +class GraphQL::Breadth::FieldResolversTest < Minitest::Test TEST_SCHEMA = GraphQL::Schema.from_definition(%| type Query { title: String @@ -23,7 +23,7 @@ def ping end end - class HandleResolvedTestResolver < GraphQL::BreadthExec::FieldResolver + class HandleResolvedTestResolver < GraphQL::Breadth::FieldResolver def resolve(_exec_field, _context) [] end @@ -34,7 +34,7 @@ def test_hash_key_resolver_with_string_key "{ title }", { "title" => "Do Androids Dream of Electric Sheep?" }, "Query" => { - "title" => GraphQL::BreadthExec::HashKeyResolver.new("title"), + "title" => GraphQL::Breadth::HashKeyResolver.new("title"), }, ) @@ -49,7 +49,7 @@ def test_method_resolver_chains_methods "{ chained }", " hello ", "Query" => { - "chained" => GraphQL::BreadthExec::MethodResolver.new(:strip, :upcase, :reverse), + "chained" => GraphQL::Breadth::MethodResolver.new(:strip, :upcase, :reverse), }, ) @@ -61,7 +61,7 @@ def test_method_resolver_uses_fallback_when_chain_returns_nil "{ fallback }", Machine.new, "Query" => { - "fallback" => GraphQL::BreadthExec::MethodResolver.new(:missing_value, :upcase, fallback: "default"), + "fallback" => GraphQL::Breadth::MethodResolver.new(:missing_value, :upcase, fallback: "default"), }, ) @@ -73,7 +73,7 @@ def test_method_resolver_uses_present_value_over_fallback "{ fallback }", Machine.new, "Query" => { - "fallback" => GraphQL::BreadthExec::MethodResolver.new(:ping, fallback: "default"), + "fallback" => GraphQL::Breadth::MethodResolver.new(:ping, fallback: "default"), }, ) @@ -85,7 +85,7 @@ def test_self_resolver_returns_source_object "{ self }", "source value", "Query" => { - "self" => GraphQL::BreadthExec::SelfResolver.new, + "self" => GraphQL::Breadth::SelfResolver.new, }, ) @@ -97,7 +97,7 @@ def test_value_resolver_returns_configured_value "{ value }", Object.new, "Query" => { - "value" => GraphQL::BreadthExec::ValueResolver.new("constant"), + "value" => GraphQL::Breadth::ValueResolver.new("constant"), }, ) @@ -115,17 +115,17 @@ def test_handle_resolved_yields_array_directly def test_handle_resolved_wraps_promise_with_then resolver = HandleResolvedTestResolver.new - promise = GraphQL::BreadthExec::Executor::ExecutionPromise.new + promise = GraphQL::Breadth::Executor::ExecutionPromise.new result = resolver.handle_resolved(promise) { |values| values.map(&:upcase) } - assert_instance_of GraphQL::BreadthExec::Executor::ExecutionPromise, result + assert_instance_of GraphQL::Breadth::Executor::ExecutionPromise, result end private def execute(document_string, root_object, resolvers) - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( TEST_SCHEMA, GraphQL.parse(document_string), resolvers: resolvers, diff --git a/test/graphql/breadth_exec/has_breadth_resolver_test.rb b/test/graphql/breadth/has_breadth_resolver_test.rb similarity index 82% rename from test/graphql/breadth_exec/has_breadth_resolver_test.rb rename to test/graphql/breadth/has_breadth_resolver_test.rb index 9ed8ae8..d218e4c 100644 --- a/test/graphql/breadth_exec/has_breadth_resolver_test.rb +++ b/test/graphql/breadth/has_breadth_resolver_test.rb @@ -2,25 +2,25 @@ require "test_helper" -class GraphQL::BreadthExec::HasBreadthResolverTest < Minitest::Test - class LanguageDirectiveResolver < GraphQL::BreadthExec::DirectiveResolver +class GraphQL::Breadth::HasBreadthResolverTest < Minitest::Test + class LanguageDirectiveResolver < GraphQL::Breadth::DirectiveResolver def resolve(exec_directive, context, current_field: nil) context[:language] = exec_directive.arguments[:language] end end - class LanguageFieldResolver < GraphQL::BreadthExec::FieldResolver + class LanguageFieldResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, context) exec_field.resolve_all(context[:language]) end end class BaseField < GraphQL::Schema::Field - include GraphQL::BreadthExec::HasBreadthResolver::Field + include GraphQL::Breadth::HasBreadthResolver::Field end class InContext < GraphQL::Schema::Directive - extend GraphQL::BreadthExec::HasBreadthResolver::Directive + extend GraphQL::Breadth::HasBreadthResolver::Directive self.breadth_resolver = LanguageDirectiveResolver.new @@ -33,7 +33,7 @@ class Widget < GraphQL::Schema::Object field_class BaseField field :id, String, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("id") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("id") end end @@ -41,7 +41,7 @@ class Query < GraphQL::Schema::Object field_class BaseField field :widget, Widget, null: true do |field| - field.breadth_resolver = GraphQL::BreadthExec::HashKeyResolver.new("widget") + field.breadth_resolver = GraphQL::Breadth::HashKeyResolver.new("widget") end field :language, String, null: true do |field| @@ -85,7 +85,7 @@ class ShortcutSchema < GraphQL::Schema def test_field_breadth_resolver_is_preferred_over_resolver_map resolvers = { "Query" => { - "widget" => GraphQL::BreadthExec::ValueResolver.new({ "id" => "map" }), + "widget" => GraphQL::Breadth::ValueResolver.new({ "id" => "map" }), }, } @@ -96,7 +96,7 @@ def test_field_breadth_resolver_is_preferred_over_resolver_map def test_directive_breadth_resolver_is_preferred_over_resolver_map resolvers = { - "@inContext" => GraphQL::BreadthExec::ValueResolver.new("unused"), + "@inContext" => GraphQL::Breadth::ValueResolver.new("unused"), } result = execute(TestSchema, %|query @inContext(language: "DE") { language }|, {}, resolvers:) @@ -107,7 +107,7 @@ def test_directive_breadth_resolver_is_preferred_over_resolver_map def test_falls_back_to_resolver_map_without_field_breadth_resolver resolvers = { "Query" => { - "fallback" => GraphQL::BreadthExec::ValueResolver.new("from-map"), + "fallback" => GraphQL::Breadth::ValueResolver.new("from-map"), }, } @@ -129,7 +129,7 @@ def test_symbol_shortcuts end def test_invalid_symbol_breadth_resolver_raises_error - error = assert_raises(GraphQL::BreadthExec::ImplementationError) do + error = assert_raises(GraphQL::Breadth::ImplementationError) do query_type = Class.new(GraphQL::Schema::Object) do graphql_name "BadQuery" field_class BaseField @@ -149,8 +149,8 @@ def test_invalid_symbol_breadth_resolver_raises_error private - def execute(schema, document, source, resolvers: GraphQL::BreadthExec::EMPTY_OBJECT) - GraphQL::BreadthExec::Executor.new( + def execute(schema, document, source, resolvers: GraphQL::Breadth::EMPTY_OBJECT) + GraphQL::Breadth::Executor.new( schema, GraphQL.parse(document), resolvers: resolvers, diff --git a/test/graphql/breadth_exec/incremental/context_test.rb b/test/graphql/breadth/incremental/context_test.rb similarity index 73% rename from test/graphql/breadth_exec/incremental/context_test.rb rename to test/graphql/breadth/incremental/context_test.rb index 10b86fe..4c30bec 100644 --- a/test/graphql/breadth_exec/incremental/context_test.rb +++ b/test/graphql/breadth/incremental/context_test.rb @@ -2,11 +2,11 @@ require "test_helper" -class GraphQL::BreadthExec::Incremental::ContextTest < Minitest::Test - DeferredDelivery = GraphQL::BreadthExec::Incremental::DeferredDelivery - DeferredExecutionScope = GraphQL::BreadthExec::Incremental::DeferredExecutionScope - ExecutionScope = GraphQL::BreadthExec::Executor::ExecutionScope - IncrementalContext = GraphQL::BreadthExec::Incremental::Context +class GraphQL::Breadth::Incremental::ContextTest < Minitest::Test + DeferredDelivery = GraphQL::Breadth::Incremental::DeferredDelivery + DeferredExecutionScope = GraphQL::Breadth::Incremental::DeferredExecutionScope + ExecutionScope = GraphQL::Breadth::Executor::ExecutionScope + IncrementalContext = GraphQL::Breadth::Incremental::Context def test_completed_payloads_deduplicate_deliveries context = IncrementalContext.new(nil, data: {}) @@ -23,7 +23,7 @@ def test_completed_payloads_deduplicate_deliveries end def test_deferred_execution_scope_is_not_ready_when_base_scope_aborted - executor = GraphQL::BreadthExec::Executor.new( + executor = GraphQL::Breadth::Executor.new( SCHEMA, GraphQL.parse("{ noResolver }"), resolvers: BREADTH_RESOLVERS, diff --git a/test/graphql/breadth_exec/incremental/publisher_test.rb b/test/graphql/breadth/incremental/publisher_test.rb similarity index 89% rename from test/graphql/breadth_exec/incremental/publisher_test.rb rename to test/graphql/breadth/incremental/publisher_test.rb index 1c4cf26..c1e9dec 100644 --- a/test/graphql/breadth_exec/incremental/publisher_test.rb +++ b/test/graphql/breadth/incremental/publisher_test.rb @@ -2,9 +2,9 @@ require "test_helper" -class GraphQL::BreadthExec::Incremental::PublisherTest < Minitest::Test - DeferredDelivery = GraphQL::BreadthExec::Incremental::DeferredDelivery - Publisher = GraphQL::BreadthExec::Incremental::Publisher +class GraphQL::Breadth::Incremental::PublisherTest < Minitest::Test + DeferredDelivery = GraphQL::Breadth::Incremental::DeferredDelivery + Publisher = GraphQL::Breadth::Incremental::Publisher def test_pending_assigns_stable_ids_and_includes_label publisher = Publisher.new diff --git a/test/star_wars_fixtures.rb b/test/star_wars_fixtures.rb index 74519af..765e882 100644 --- a/test/star_wars_fixtures.rb +++ b/test/star_wars_fixtures.rb @@ -101,7 +101,7 @@ }, }.freeze -class StarWarsFriendsResolver < GraphQL::BreadthExec::FieldResolver +class StarWarsFriendsResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects do |character| character["friends"].map { |id| STAR_WARS_DATA[id] } @@ -109,17 +109,17 @@ def resolve(exec_field, _ctx) end end -class StarWarsSecretBackstoryResolver < GraphQL::BreadthExec::FieldResolver +class StarWarsSecretBackstoryResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects_with_index do |_character, index| path = exec_field.path.dup path.insert(-2, index) if exec_field.objects.length > 1 - GraphQL::BreadthExec::ExecutionError.new("secretBackstory is secret.", path: path, exec_field: exec_field) + GraphQL::Breadth::ExecutionError.new("secretBackstory is secret.", path: path, exec_field: exec_field) end end end -class StarWarsHeroResolver < GraphQL::BreadthExec::FieldResolver +class StarWarsHeroResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects do exec_field.arguments[:episode] == "EMPIRE" ? STAR_WARS_DATA["1000"] : STAR_WARS_DATA["2001"] @@ -127,7 +127,7 @@ def resolve(exec_field, _ctx) end end -class StarWarsHumanResolver < GraphQL::BreadthExec::FieldResolver +class StarWarsHumanResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects do character = STAR_WARS_DATA[exec_field.arguments[:id]] @@ -136,7 +136,7 @@ def resolve(exec_field, _ctx) end end -class StarWarsDroidResolver < GraphQL::BreadthExec::FieldResolver +class StarWarsDroidResolver < GraphQL::Breadth::FieldResolver def resolve(exec_field, _ctx) exec_field.map_objects do character = STAR_WARS_DATA[exec_field.arguments[:id]] @@ -146,10 +146,10 @@ def resolve(exec_field, _ctx) end STAR_WARS_CHARACTER_RESOLVERS = { - "id" => GraphQL::BreadthExec::HashKeyResolver.new("id"), - "name" => GraphQL::BreadthExec::HashKeyResolver.new("name"), + "id" => GraphQL::Breadth::HashKeyResolver.new("id"), + "name" => GraphQL::Breadth::HashKeyResolver.new("name"), "friends" => StarWarsFriendsResolver.new, - "appearsIn" => GraphQL::BreadthExec::HashKeyResolver.new("appearsIn"), + "appearsIn" => GraphQL::Breadth::HashKeyResolver.new("appearsIn"), "secretBackstory" => StarWarsSecretBackstoryResolver.new, }.freeze @@ -162,11 +162,11 @@ def resolve(exec_field, _ctx) }, "Human" => { **STAR_WARS_CHARACTER_RESOLVERS, - "homePlanet" => GraphQL::BreadthExec::HashKeyResolver.new("homePlanet"), + "homePlanet" => GraphQL::Breadth::HashKeyResolver.new("homePlanet"), }, "Droid" => { **STAR_WARS_CHARACTER_RESOLVERS, - "primaryFunction" => GraphQL::BreadthExec::HashKeyResolver.new("primaryFunction"), + "primaryFunction" => GraphQL::Breadth::HashKeyResolver.new("primaryFunction"), }, "Query" => { "hero" => StarWarsHeroResolver.new, @@ -176,7 +176,7 @@ def resolve(exec_field, _ctx) }.freeze def execute_star_wars(query, variables: {}) - GraphQL::BreadthExec::Executor.new( + GraphQL::Breadth::Executor.new( STAR_WARS_SCHEMA, GraphQL.parse(query), resolvers: STAR_WARS_RESOLVERS, diff --git a/test/test_helper.rb b/test/test_helper.rb index e4bcd56..bbafeb4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,13 +14,13 @@ require 'minitest/autorun' require 'minitest/stub_const' -require 'graphql/breadth_exec' +require 'graphql/breadth' require 'graphql/batch' require_relative './fixtures' require_relative './star_wars_fixtures' -def breadth_exec(query, source, variables: {}, context: {}, tracers: [GraphQL::BreadthExec::Tracer.new]) - GraphQL::BreadthExec::Executor.new( +def breadth(query, source, variables: {}, context: {}, tracers: [GraphQL::Breadth::Tracer.new]) + GraphQL::Breadth::Executor.new( SCHEMA, GraphQL.parse(query), resolvers: BREADTH_RESOLVERS, @@ -32,10 +32,10 @@ def breadth_exec(query, source, variables: {}, context: {}, tracers: [GraphQL::B end def assert_error_reported(expected_class, &block) - original_handler = GraphQL::BreadthExec.on_report_error + original_handler = GraphQL::Breadth.on_report_error reported_error = nil - GraphQL::BreadthExec.on_report_error = ->(error) do + GraphQL::Breadth.on_report_error = ->(error) do reported_error = error end @@ -47,5 +47,5 @@ def assert_error_reported(expected_class, &block) assert_equal(expected_class, reported_error.class) reported_error ensure - GraphQL::BreadthExec.on_report_error = original_handler + GraphQL::Breadth.on_report_error = original_handler end