graphql-stitching 1.2.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +67 -17
- data/docs/README.md +2 -1
- data/docs/mechanics.md +2 -1
- data/docs/resolver.md +101 -0
- data/lib/graphql/stitching/client.rb +5 -1
- data/lib/graphql/stitching/composer/{boundary_config.rb → resolver_config.rb} +18 -13
- data/lib/graphql/stitching/composer/validate_interfaces.rb +4 -4
- data/lib/graphql/stitching/composer/validate_resolvers.rb +97 -0
- data/lib/graphql/stitching/composer.rb +107 -112
- data/lib/graphql/stitching/executor/{boundary_source.rb → resolver_source.rb} +40 -32
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +3 -4
- data/lib/graphql/stitching/planner.rb +30 -41
- data/lib/graphql/stitching/planner_step.rb +6 -6
- data/lib/graphql/stitching/resolver/arguments.rb +284 -0
- data/lib/graphql/stitching/resolver/keys.rb +206 -0
- data/lib/graphql/stitching/resolver.rb +70 -0
- data/lib/graphql/stitching/shaper.rb +3 -3
- data/lib/graphql/stitching/skip_include.rb +1 -1
- data/lib/graphql/stitching/supergraph/key_directive.rb +13 -0
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +4 -4
- data/lib/graphql/stitching/supergraph/to_definition.rb +165 -0
- data/lib/graphql/stitching/supergraph.rb +31 -144
- data/lib/graphql/stitching/util.rb +28 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +3 -2
- metadata +11 -7
- data/lib/graphql/stitching/boundary.rb +0 -29
- data/lib/graphql/stitching/composer/validate_boundaries.rb +0 -96
- data/lib/graphql/stitching/export_selection.rb +0 -42
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 448ca61527e02c6f232fa811e7e99ba1aa243bc58685f313ca5613a6724ea306
         | 
| 4 | 
            +
              data.tar.gz: a0af76ea7eb429d60f5a7e4df4c39103539a01a69136c769f027cef52a108cab
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 8cea8a8674d67a421059e32778ffc3d1bbd699fa307efe2aead4571925ed77d8da7852431ef99c5809e0106ff16af212c26a6372c211b2d287d3a2c4cef5eb80
         | 
| 7 | 
            +
              data.tar.gz: 972f74c60afef0c615d8dc6a3069bb7ddfbe88a84e22a8940d496037ab67f23491c639e8cc3e145b98697c921eb6c20577babec4be2a8e6a3221264e925cdc01
         | 
    
        data/README.md
    CHANGED
    
    | @@ -6,7 +6,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso | |
| 6 6 |  | 
| 7 7 | 
             
            **Supports:**
         | 
| 8 8 | 
             
            - Merged object and abstract types.
         | 
| 9 | 
            -
            - Multiple keys per merged type.
         | 
| 9 | 
            +
            - Multiple and composite keys per merged type.
         | 
| 10 10 | 
             
            - Shared objects, fields, enums, and inputs across locations.
         | 
| 11 11 | 
             
            - Combining local and remote schemas.
         | 
| 12 12 | 
             
            - File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
         | 
| @@ -94,7 +94,7 @@ To facilitate this merging of types, stitching must know how to cross-reference | |
| 94 94 | 
             
            Types merge through resolver queries identified by a `@stitch` directive:
         | 
| 95 95 |  | 
| 96 96 | 
             
            ```graphql
         | 
| 97 | 
            -
            directive @stitch(key: String | 
| 97 | 
            +
            directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
         | 
| 98 98 | 
             
            ```
         | 
| 99 99 |  | 
| 100 100 | 
             
            This directive (or [static configuration](#sdl-based-schemas)) is applied to root queries where a merged type may be accessed in each location, and a `key` argument specifies a field needed from other locations to be used as a query argument.
         | 
| @@ -151,7 +151,7 @@ type Query { | |
| 151 151 | 
             
            ```
         | 
| 152 152 |  | 
| 153 153 | 
             
            * The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
         | 
| 154 | 
            -
            * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](# | 
| 154 | 
            +
            * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#argument-shapes) later).
         | 
| 155 155 |  | 
| 156 156 | 
             
            Each location that provides a unique variant of a type must provide at least one resolver query for the type. The exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) and/or [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain no exclusive data:
         | 
| 157 157 |  | 
| @@ -198,7 +198,7 @@ type Query { | |
| 198 198 | 
             
            To customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.
         | 
| 199 199 |  | 
| 200 200 | 
             
            ```graphql
         | 
| 201 | 
            -
            directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
         | 
| 201 | 
            +
            directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION
         | 
| 202 202 |  | 
| 203 203 | 
             
            type Product { sku: ID! }
         | 
| 204 204 | 
             
            type Order { id: ID! }
         | 
| @@ -212,19 +212,69 @@ type Query { | |
| 212 212 | 
             
            }
         | 
| 213 213 | 
             
            ```
         | 
| 214 214 |  | 
| 215 | 
            -
            ####  | 
| 215 | 
            +
            #### Argument shapes
         | 
| 216 216 |  | 
| 217 | 
            -
            Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For  | 
| 217 | 
            +
            Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, the `arguments` option may specify a template of GraphQL arguments that insert key selections:
         | 
| 218 218 |  | 
| 219 219 | 
             
            ```graphql
         | 
| 220 220 | 
             
            type Product {
         | 
| 221 221 | 
             
              id: ID!
         | 
| 222 222 | 
             
            }
         | 
| 223 223 | 
             
            type Query {
         | 
| 224 | 
            -
              product(byId: ID, bySku: ID): Product | 
| 224 | 
            +
              product(byId: ID, bySku: ID): Product
         | 
| 225 | 
            +
                @stitch(key: "id", arguments: "byId: $.id")
         | 
| 225 226 | 
             
            }
         | 
| 226 227 | 
             
            ```
         | 
| 227 228 |  | 
| 229 | 
            +
            Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`. This syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and other static values:
         | 
| 230 | 
            +
             | 
| 231 | 
            +
            ```graphql
         | 
| 232 | 
            +
            type Product {
         | 
| 233 | 
            +
              id: ID!
         | 
| 234 | 
            +
            }
         | 
| 235 | 
            +
            union Entity = Product
         | 
| 236 | 
            +
            input EntityKey {
         | 
| 237 | 
            +
              id: ID!
         | 
| 238 | 
            +
              type: String!
         | 
| 239 | 
            +
            }
         | 
| 240 | 
            +
             | 
| 241 | 
            +
            type Query {
         | 
| 242 | 
            +
              entities(keys: [EntityKey!]!, source: String="database"): [Entity]!
         | 
| 243 | 
            +
                @stitch(key: "id", arguments: "keys: { id: $.id, type: $.__typename }, source: 'cache'")
         | 
| 244 | 
            +
            }
         | 
| 245 | 
            +
            ```
         | 
| 246 | 
            +
             | 
| 247 | 
            +
            See [resolver arguments](./docs/resolver.md#arguments) for full documentation on shaping input.
         | 
| 248 | 
            +
             | 
| 249 | 
            +
            #### Composite type keys
         | 
| 250 | 
            +
             | 
| 251 | 
            +
            Resolver keys may make composite selections for multiple key fields and/or nested scopes, for example:
         | 
| 252 | 
            +
             | 
| 253 | 
            +
            ```graphql
         | 
| 254 | 
            +
            interface FieldOwner {
         | 
| 255 | 
            +
              id: ID!
         | 
| 256 | 
            +
              type: String!
         | 
| 257 | 
            +
            }
         | 
| 258 | 
            +
            type CustomField {
         | 
| 259 | 
            +
              owner: FieldOwner!
         | 
| 260 | 
            +
              key: String!
         | 
| 261 | 
            +
              value: String
         | 
| 262 | 
            +
            }
         | 
| 263 | 
            +
            input CustomFieldLookup {
         | 
| 264 | 
            +
              ownerId: ID!
         | 
| 265 | 
            +
              ownerType: String!
         | 
| 266 | 
            +
              key: String!
         | 
| 267 | 
            +
            }
         | 
| 268 | 
            +
            type Query {
         | 
| 269 | 
            +
              customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
         | 
| 270 | 
            +
                key: "owner { id type } key",
         | 
| 271 | 
            +
                arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.type, key: $.key }"
         | 
| 272 | 
            +
              )
         | 
| 273 | 
            +
            }
         | 
| 274 | 
            +
            ```
         | 
| 275 | 
            +
             | 
| 276 | 
            +
            Note that composite key selections may _not_ be distributed across locations. The complete selection criteria must be available in each location that provides the key.
         | 
| 277 | 
            +
             | 
| 228 278 | 
             
            #### Multiple type keys
         | 
| 229 279 |  | 
| 230 280 | 
             
            A type may exist in multiple locations across the graph using different keys, for example:
         | 
| @@ -265,7 +315,7 @@ type Query { | |
| 265 315 | 
             
            The `@stitch` directive can be added to class-based schemas with a directive class:
         | 
| 266 316 |  | 
| 267 317 | 
             
            ```ruby
         | 
| 268 | 
            -
            class  | 
| 318 | 
            +
            class StitchingResolver < GraphQL::Schema::Directive
         | 
| 269 319 | 
             
              graphql_name "stitch"
         | 
| 270 320 | 
             
              locations FIELD_DEFINITION
         | 
| 271 321 | 
             
              repeatable true
         | 
| @@ -274,7 +324,7 @@ end | |
| 274 324 |  | 
| 275 325 | 
             
            class Query < GraphQL::Schema::Object
         | 
| 276 326 | 
             
              field :product, Product, null: false do
         | 
| 277 | 
            -
                directive  | 
| 327 | 
            +
                directive StitchingResolver, key: "id"
         | 
| 278 328 | 
             
                argument :id, ID, required: true
         | 
| 279 329 | 
             
              end
         | 
| 280 330 | 
             
            end
         | 
| @@ -284,7 +334,7 @@ The `@stitch` directive can be exported from a class-based schema to an SDL stri | |
| 284 334 |  | 
| 285 335 | 
             
            #### SDL-based schemas
         | 
| 286 336 |  | 
| 287 | 
            -
            A clean  | 
| 337 | 
            +
            A clean schema may also have stitching directives applied via static configuration by passing a `stitch` array in [location settings](./docs/composer.md#performing-composition):
         | 
| 288 338 |  | 
| 289 339 | 
             
            ```ruby
         | 
| 290 340 | 
             
            sdl_string = <<~GRAPHQL
         | 
| @@ -316,7 +366,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({ | |
| 316 366 | 
             
            The library is configured to use a `@stitch` directive by default. You may customize this by setting a new name during initialization:
         | 
| 317 367 |  | 
| 318 368 | 
             
            ```ruby
         | 
| 319 | 
            -
            GraphQL::Stitching.stitch_directive = " | 
| 369 | 
            +
            GraphQL::Stitching.stitch_directive = "resolver"
         | 
| 320 370 | 
             
            ```
         | 
| 321 371 |  | 
| 322 372 | 
             
            ## Executables
         | 
| @@ -365,17 +415,17 @@ The `GraphQL::Stitching::HttpExecutable` class is provided as a simple executabl | |
| 365 415 | 
             
            The stitching executor automatically batches subgraph requests so that only one request is made per location per generation of data. This is done using batched queries that combine all data access for a given a location. For example:
         | 
| 366 416 |  | 
| 367 417 | 
             
            ```graphql
         | 
| 368 | 
            -
            query MyOperation_2 {
         | 
| 369 | 
            -
              _0_result: widgets(ids: | 
| 370 | 
            -
              _1_0_result: sprocket(id: | 
| 371 | 
            -
              _1_1_result: sprocket(id: | 
| 372 | 
            -
              _1_2_result: sprocket(id: | 
| 418 | 
            +
            query MyOperation_2($_0_key:[ID!]!, $_1_0_key:ID!, $_1_1_key:ID!, $_1_2_key:ID!) {
         | 
| 419 | 
            +
              _0_result: widgets(ids: $_0_key) { ... } # << 3 Widget
         | 
| 420 | 
            +
              _1_0_result: sprocket(id: $_1_0_key) { ... } # << 1 Sprocket
         | 
| 421 | 
            +
              _1_1_result: sprocket(id: $_1_1_key) { ... } # << 1 Sprocket
         | 
| 422 | 
            +
              _1_2_result: sprocket(id: $_1_2_key) { ... } # << 1 Sprocket
         | 
| 373 423 | 
             
            }
         | 
| 374 424 | 
             
            ```
         | 
| 375 425 |  | 
| 376 426 | 
             
            Tips:
         | 
| 377 427 |  | 
| 378 | 
            -
            * List queries (like the `widgets` selection above) are  | 
| 428 | 
            +
            * List queries (like the `widgets` selection above) are generally preferable as resolver queries because they keep the batched document consistent regardless of set size, and make for smaller documents that parse and validate faster.
         | 
| 379 429 | 
             
            * Assure that root field resolvers across your subgraph implement batching to anticipate cases like the three `sprocket` selections above.
         | 
| 380 430 |  | 
| 381 431 | 
             
            Otherwise, there's no developer intervention necessary (or generally possible) to improve upon data access. Note that multiple generations of data may still force the executor to return to a previous location for more data.
         | 
    
        data/docs/README.md
    CHANGED
    
    | @@ -14,4 +14,5 @@ Major components include: | |
| 14 14 |  | 
| 15 15 | 
             
            Additional topics:
         | 
| 16 16 |  | 
| 17 | 
            -
            - [Stitching mechanics](./mechanics.md) -  | 
| 17 | 
            +
            - [Stitching mechanics](./mechanics.md) - more about building for stitching and how it operates.
         | 
| 18 | 
            +
            - [Federation entities](./federation_entities.md) - more about Apollo Federation compatibility.
         | 
    
        data/docs/mechanics.md
    CHANGED
    
    | @@ -314,6 +314,7 @@ Merged types do not always require a resolver query. For example: | |
| 314 314 | 
             
            type Widget {
         | 
| 315 315 | 
             
              id: ID!
         | 
| 316 316 | 
             
              name: String
         | 
| 317 | 
            +
              price: Float
         | 
| 317 318 | 
             
            }
         | 
| 318 319 |  | 
| 319 320 | 
             
            type Query {
         | 
| @@ -344,4 +345,4 @@ type Query { | |
| 344 345 | 
             
            }
         | 
| 345 346 | 
             
            ```
         | 
| 346 347 |  | 
| 347 | 
            -
            In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a key field (such as `id` above) that allow them to join with data in other resolver locations.
         | 
| 348 | 
            +
            In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a shared key field (such as `id` above) that allow them to join with data in other resolver locations (such as `price` above).
         | 
    
        data/docs/resolver.md
    ADDED
    
    | @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            ## GraphQL::Stitching::Resolver
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            A `Resolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ### Arguments
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            #### Key insertions
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Key values fetched from previous locations may be inserted into arguments. Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`.
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ```graphql
         | 
| 14 | 
            +
            type Query {
         | 
| 15 | 
            +
              entity(id: ID!, type: String!): [Entity]!
         | 
| 16 | 
            +
                @stitch(key: "owner { id }", arguments: "id: $.owner.id, type: $.__typename")
         | 
| 17 | 
            +
            }
         | 
| 18 | 
            +
            ```
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            Key insertions are _not_ quoted to differentiate them from other literal values.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            #### Lists
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            List arguments may specify input just like non-list arguments, and [GraphQL list input coercion](https://spec.graphql.org/October2021/#sec-List.Input-Coercion) will assume the shape represents a list item:
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            ```graphql
         | 
| 27 | 
            +
            type Query {
         | 
| 28 | 
            +
              product(ids: [ID!]!, source: DataSource!): [Product]!
         | 
| 29 | 
            +
                @stitch(key: "id", arguments: "ids: $.id, source: CACHE")
         | 
| 30 | 
            +
            }
         | 
| 31 | 
            +
            ```
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            List resolvers (that return list types) may _only_ insert keys into repeatable list arguments, while non-list arguments may only contain static values. Nested list inputs are neither common nor practical, so are not supported.
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            #### Built-in scalars
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            Built-in scalars are written as normal literal values. For convenience, string literals may be enclosed in single quotes rather than escaped double-quotes:
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ```graphql
         | 
| 40 | 
            +
            type Query {
         | 
| 41 | 
            +
              product(id: ID!, source: String!): Product
         | 
| 42 | 
            +
                @stitch(key: "id", arguments: "id: $.id, source: 'cache'")
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              variant(id: ID!, limit: Int!): Variant
         | 
| 45 | 
            +
                @stitch(key: "id", arguments: "id: $.id, limit: 100")
         | 
| 46 | 
            +
            }
         | 
| 47 | 
            +
            ```
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            All scalar usage must be legal to the resolver field's arguments schema.
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            #### Enums
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            Enum literals may be provided anywhere in the input structure. They are _not_ quoted:
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            ```graphql
         | 
| 56 | 
            +
            enum DataSource {
         | 
| 57 | 
            +
              CACHE
         | 
| 58 | 
            +
            }
         | 
| 59 | 
            +
            type Query {
         | 
| 60 | 
            +
              product(id: ID!, source: DataSource!): [Product]!
         | 
| 61 | 
            +
                @stitch(key: "id", arguments: "id: $.id, source: CACHE")
         | 
| 62 | 
            +
            }
         | 
| 63 | 
            +
            ```
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            All enum usage must be legal to the resolver field's arguments schema.
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            #### Input Objects
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            Input objects may be provided anywhere in the input, even as nested structures. The stitching resolver will build the specified object shape:
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            ```graphql
         | 
| 72 | 
            +
            input ComplexKey {
         | 
| 73 | 
            +
              id: ID
         | 
| 74 | 
            +
              nested: ComplexKey
         | 
| 75 | 
            +
            }
         | 
| 76 | 
            +
            type Query {
         | 
| 77 | 
            +
              product(key: ComplexKey!): [Product]!
         | 
| 78 | 
            +
                @stitch(key: "id", arguments: "key: { nested: { id: $.id } }")
         | 
| 79 | 
            +
            }
         | 
| 80 | 
            +
            ```
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            Input object shapes must conform to their respective schema definitions based on their placement within resolver arguments.
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            #### Custom scalars
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            Custom scalar keys allow any input shape to be submitted, from primitive scalars to complex object structures. These values will be sent and recieved as untyped JSON input:
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            ```graphql
         | 
| 89 | 
            +
            type Product {
         | 
| 90 | 
            +
              id: ID!
         | 
| 91 | 
            +
            }
         | 
| 92 | 
            +
            union Entity = Product
         | 
| 93 | 
            +
            scalar Key
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            type Query {
         | 
| 96 | 
            +
              entities(representations: [Key!]!): [Entity]!
         | 
| 97 | 
            +
                @stitch(key: "id", arguments: "representations: { id: $.id, __typename: $.__typename }")
         | 
| 98 | 
            +
            }
         | 
| 99 | 
            +
            ```
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            Custom scalar arguments have no structured schema definition to validate against. This makes them flexible but quite lax, for better or worse.
         | 
| @@ -70,7 +70,11 @@ module GraphQL | |
| 70 70 | 
             
                  def load_plan(request)
         | 
| 71 71 | 
             
                    if @on_cache_read && plan_json = @on_cache_read.call(request)
         | 
| 72 72 | 
             
                      plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
         | 
| 73 | 
            -
             | 
| 73 | 
            +
             | 
| 74 | 
            +
                      # only use plans referencing current resolver versions
         | 
| 75 | 
            +
                      if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
         | 
| 76 | 
            +
                        return request.plan(plan)
         | 
| 77 | 
            +
                      end
         | 
| 74 78 | 
             
                    end
         | 
| 75 79 |  | 
| 76 80 | 
             
                    plan = request.plan
         | 
| @@ -2,7 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module GraphQL::Stitching
         | 
| 4 4 | 
             
              class Composer
         | 
| 5 | 
            -
                class  | 
| 5 | 
            +
                class ResolverConfig
         | 
| 6 6 | 
             
                  ENTITY_TYPENAME = "_Entity"
         | 
| 7 7 | 
             
                  ENTITIES_QUERY = "_entities"
         | 
| 8 8 |  | 
| @@ -12,10 +12,10 @@ module GraphQL::Stitching | |
| 12 12 |  | 
| 13 13 | 
             
                      assignments.each_with_object({}) do |kwargs, memo|
         | 
| 14 14 | 
             
                        type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
         | 
| 15 | 
            -
                        raise  | 
| 15 | 
            +
                        raise CompositionError, "Invalid stitch directive type `#{kwargs[:parent_type_name]}`" unless type
         | 
| 16 16 |  | 
| 17 17 | 
             
                        field = type.get_field(kwargs[:field_name])
         | 
| 18 | 
            -
                        raise  | 
| 18 | 
            +
                        raise CompositionError, "Invalid stitch directive field `#{kwargs[:field_name]}`" unless field
         | 
| 19 19 |  | 
| 20 20 | 
             
                        field_path = "#{location}.#{field.name}"
         | 
| 21 21 | 
             
                        memo[field_path] ||= []
         | 
| @@ -30,15 +30,15 @@ module GraphQL::Stitching | |
| 30 30 | 
             
                        entity_type.directives.each do |directive|
         | 
| 31 31 | 
             
                          next unless directive.graphql_name == "key"
         | 
| 32 32 |  | 
| 33 | 
            -
                          key = directive.arguments.keyword_arguments.fetch(:fields) | 
| 34 | 
            -
                           | 
| 35 | 
            -
             | 
| 33 | 
            +
                          key = Resolver.parse_key(directive.arguments.keyword_arguments.fetch(:fields))
         | 
| 34 | 
            +
                          key_fields = key.map { "#{_1.name}: $.#{_1.name}" }
         | 
| 36 35 | 
             
                          field_path = "#{location}._entities"
         | 
| 36 | 
            +
             | 
| 37 37 | 
             
                          memo[field_path] ||= []
         | 
| 38 38 | 
             
                          memo[field_path] << new(
         | 
| 39 | 
            -
                            key: key,
         | 
| 39 | 
            +
                            key: key.to_definition,
         | 
| 40 40 | 
             
                            type_name: entity_type.graphql_name,
         | 
| 41 | 
            -
                             | 
| 41 | 
            +
                            arguments: "representations: { #{key_fields.join(", ")}, __typename: $.__typename }",
         | 
| 42 42 | 
             
                          )
         | 
| 43 43 | 
             
                        end
         | 
| 44 44 | 
             
                      end
         | 
| @@ -48,7 +48,7 @@ module GraphQL::Stitching | |
| 48 48 | 
             
                      new(
         | 
| 49 49 | 
             
                        key: kwargs[:key],
         | 
| 50 50 | 
             
                        type_name: kwargs[:type_name] || kwargs[:typeName],
         | 
| 51 | 
            -
                         | 
| 51 | 
            +
                        arguments: kwargs[:arguments],
         | 
| 52 52 | 
             
                      )
         | 
| 53 53 | 
             
                    end
         | 
| 54 54 |  | 
| @@ -57,16 +57,21 @@ module GraphQL::Stitching | |
| 57 57 | 
             
                    def federation_entities_schema?(schema)
         | 
| 58 58 | 
             
                      entity_type = schema.get_type(ENTITY_TYPENAME)
         | 
| 59 59 | 
             
                      entities_query = schema.query.get_field(ENTITIES_QUERY)
         | 
| 60 | 
            -
                      entity_type && | 
| 60 | 
            +
                      entity_type &&
         | 
| 61 | 
            +
                        entity_type.kind.union? &&
         | 
| 62 | 
            +
                        entities_query &&
         | 
| 63 | 
            +
                        entities_query.arguments["representations"] &&
         | 
| 64 | 
            +
                        entities_query.type.list? &&
         | 
| 65 | 
            +
                        entities_query.type.unwrap == entity_type
         | 
| 61 66 | 
             
                    end
         | 
| 62 67 | 
             
                  end
         | 
| 63 68 |  | 
| 64 | 
            -
                  attr_reader :key, :type_name, : | 
| 69 | 
            +
                  attr_reader :key, :type_name, :arguments
         | 
| 65 70 |  | 
| 66 | 
            -
                  def initialize(key:, type_name:,  | 
| 71 | 
            +
                  def initialize(key:, type_name:, arguments: nil)
         | 
| 67 72 | 
             
                    @key = key
         | 
| 68 73 | 
             
                    @type_name = type_name
         | 
| 69 | 
            -
                    @ | 
| 74 | 
            +
                    @arguments = arguments
         | 
| 70 75 | 
             
                  end
         | 
| 71 76 | 
             
                end
         | 
| 72 77 | 
             
              end
         | 
| @@ -15,7 +15,7 @@ module GraphQL::Stitching | |
| 15 15 | 
             
                          # graphql-ruby will dynamically apply interface fields on a type implementation,
         | 
| 16 16 | 
             
                          # so check the delegation map to assure that all materialized fields have resolver locations.
         | 
| 17 17 | 
             
                          unless supergraph.locations_by_type_and_field[possible_type.graphql_name][field_name]&.any?
         | 
| 18 | 
            -
                            raise  | 
| 18 | 
            +
                            raise ValidationError, "Type #{possible_type.graphql_name} does not implement a `#{field_name}` field in any location, "\
         | 
| 19 19 | 
             
                              "which is required by interface #{interface_type.graphql_name}."
         | 
| 20 20 | 
             
                          end
         | 
| 21 21 |  | 
| @@ -24,7 +24,7 @@ module GraphQL::Stitching | |
| 24 24 | 
             
                          possible_type_structure = Util.flatten_type_structure(intersecting_field.type)
         | 
| 25 25 |  | 
| 26 26 | 
             
                          if possible_type_structure.length != interface_type_structure.length
         | 
| 27 | 
            -
                            raise  | 
| 27 | 
            +
                            raise ValidationError, "Incompatible list structures between field #{possible_type.graphql_name}.#{field_name} of type "\
         | 
| 28 28 | 
             
                              "#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
         | 
| 29 29 | 
             
                          end
         | 
| 30 30 |  | 
| @@ -32,12 +32,12 @@ module GraphQL::Stitching | |
| 32 32 | 
             
                            possible_struct = possible_type_structure[index]
         | 
| 33 33 |  | 
| 34 34 | 
             
                            if possible_struct.name != interface_struct.name
         | 
| 35 | 
            -
                              raise  | 
| 35 | 
            +
                              raise ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
         | 
| 36 36 | 
             
                                "#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
         | 
| 37 37 | 
             
                            end
         | 
| 38 38 |  | 
| 39 39 | 
             
                            if possible_struct.null? && interface_struct.non_null?
         | 
| 40 | 
            -
                              raise  | 
| 40 | 
            +
                              raise ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
         | 
| 41 41 | 
             
                                "#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
         | 
| 42 42 | 
             
                            end
         | 
| 43 43 | 
             
                          end
         | 
| @@ -0,0 +1,97 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module GraphQL::Stitching
         | 
| 4 | 
            +
              class Composer
         | 
| 5 | 
            +
                class ValidateResolvers < BaseValidator
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def perform(supergraph, composer)
         | 
| 8 | 
            +
                    supergraph.schema.types.each do |type_name, type|
         | 
| 9 | 
            +
                      # objects and interfaces that are not the root operation types
         | 
| 10 | 
            +
                      next unless type.kind.object? || type.kind.interface?
         | 
| 11 | 
            +
                      next if supergraph.schema.query == type || supergraph.schema.mutation == type
         | 
| 12 | 
            +
                      next if type.graphql_name.start_with?("__")
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      # multiple subschemas implement the type
         | 
| 15 | 
            +
                      subgraph_types_by_location = composer.subgraph_types_by_name_and_location[type_name]
         | 
| 16 | 
            +
                      next unless subgraph_types_by_location.length > 1
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      resolvers = supergraph.resolvers[type_name]
         | 
| 19 | 
            +
                      if resolvers&.any?
         | 
| 20 | 
            +
                        validate_as_resolver(supergraph, type, subgraph_types_by_location, resolvers)
         | 
| 21 | 
            +
                      elsif type.kind.object?
         | 
| 22 | 
            +
                        validate_as_shared(supergraph, type, subgraph_types_by_location)
         | 
| 23 | 
            +
                      end
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def validate_as_resolver(supergraph, type, subgraph_types_by_location, resolvers)
         | 
| 30 | 
            +
                    # abstract resolvers are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
         | 
| 31 | 
            +
                    return if type.kind.abstract?
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    # only one resolver allowed per type/location/key
         | 
| 34 | 
            +
                    resolvers_by_location_and_key = resolvers.each_with_object({}) do |resolver, memo|
         | 
| 35 | 
            +
                      if memo.dig(resolver.location, resolver.key.to_definition)
         | 
| 36 | 
            +
                        raise ValidationError, "Multiple resolver queries for `#{type.graphql_name}.#{resolver.key}` "\
         | 
| 37 | 
            +
                          "found in #{resolver.location}. Limit one resolver query per type and key in each location. "\
         | 
| 38 | 
            +
                          "Abstract resolvers provide all possible types."
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                      memo[resolver.location] ||= {}
         | 
| 41 | 
            +
                      memo[resolver.location][resolver.key.to_definition] = resolver
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    resolver_keys = resolvers.map(&:key)
         | 
| 45 | 
            +
                    resolver_key_strs = resolver_keys.map(&:to_definition).to_set
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # All non-key fields must be resolvable in at least one resolver location
         | 
| 48 | 
            +
                    supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
         | 
| 49 | 
            +
                      next if resolver_key_strs.include?(field_name)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      if locations.none? { resolvers_by_location_and_key[_1] }
         | 
| 52 | 
            +
                        where = locations.length > 1 ? "one of #{locations.join(", ")} locations" : locations.first
         | 
| 53 | 
            +
                        raise ValidationError, "A resolver query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    # All locations of a merged type must include at least one resolver key
         | 
| 58 | 
            +
                    supergraph.fields_by_type_and_location[type.graphql_name].each_key do |location|
         | 
| 59 | 
            +
                      if resolver_keys.none? { _1.locations.include?(location) }
         | 
| 60 | 
            +
                        raise ValidationError, "A resolver key is required for `#{type.graphql_name}` in #{location} to join with other locations."
         | 
| 61 | 
            +
                      end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    # verify that all outbound locations can access all inbound locations
         | 
| 65 | 
            +
                    resolver_locations = resolvers_by_location_and_key.keys
         | 
| 66 | 
            +
                    subgraph_types_by_location.each_key do |location|
         | 
| 67 | 
            +
                      remote_locations = resolver_locations.reject { _1 == location }
         | 
| 68 | 
            +
                      paths = supergraph.route_type_to_locations(type.graphql_name, location, remote_locations)
         | 
| 69 | 
            +
                      if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
         | 
| 70 | 
            +
                        raise ValidationError, "Cannot route `#{type.graphql_name}` resolvers in #{location} to all other locations. "\
         | 
| 71 | 
            +
                          "All locations must provide a resolver query with a joining key."
         | 
| 72 | 
            +
                      end
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def validate_as_shared(supergraph, type, subgraph_types_by_location)
         | 
| 77 | 
            +
                    expected_fields = begin
         | 
| 78 | 
            +
                      type.fields.keys.sort
         | 
| 79 | 
            +
                    rescue StandardError => e
         | 
| 80 | 
            +
                      # bug with inherited interfaces in older versions of GraphQL
         | 
| 81 | 
            +
                      if type.interfaces.any? { _1.is_a?(GraphQL::Schema::LateBoundType) }
         | 
| 82 | 
            +
                        raise CompositionError, "Merged interface inheritance requires GraphQL >= v2.0.3"
         | 
| 83 | 
            +
                      else
         | 
| 84 | 
            +
                        raise e
         | 
| 85 | 
            +
                      end
         | 
| 86 | 
            +
                    end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    subgraph_types_by_location.each do |location, subgraph_type|
         | 
| 89 | 
            +
                      if subgraph_type.fields.keys.sort != expected_fields
         | 
| 90 | 
            +
                        raise ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
         | 
| 91 | 
            +
                          "or else define resolver queries so that its unique fields may be accessed remotely."
         | 
| 92 | 
            +
                      end
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
            end
         |