From 38001615e0eb47f67db54f8a013557ff81f6fb66 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 24 Oct 2024 16:09:07 +0200 Subject: [PATCH] Introduce Core Source Schema Directives (#30) --- cspell.yml | 1 + spec/Appendix A -- Field Selection.md | 637 +++++++++++++++----------- spec/Section 2 -- Source Schema.md | 440 +++++++++++++----- spec/temp.md | 30 ++ 4 files changed, 739 insertions(+), 369 deletions(-) diff --git a/cspell.yml b/cspell.yml index 06d3a5b..9bf17bf 100644 --- a/cspell.yml +++ b/cspell.yml @@ -27,6 +27,7 @@ words: - tatooine - zuck - zuckerberg + - subselections ignoreWords: - Aremergeable diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 72bc131..e001a7b 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -2,65 +2,79 @@ ## Introduction -This appendix focuses on the specification of the {FieldSelectionMap} scalar type. -{FieldSelectionMap} is designed to express semantic equivalence between arguments of a field and fields within the result type. -Specifically, it allows defining complex relationships between input arguments and fields in the output object by encapsulating these relationships within a parsable string format. -It is used in the `@is` and `@require` directives. +This appendix focuses on the specification of the {FieldSelectionMap} scalar +type. {FieldSelectionMap} is designed to express semantic equivalence between +arguments of a field and fields within the result type. Specifically, it allows +defining complex relationships between input arguments and fields in the output +object by encapsulating these relationships within a parsable string format. It +is used in the `@is` and `@require` directives. To illustrate, consider a simple example from a GraphQL schema: ```graphql type Query { - userById(userId: ID! @is(field: "id")): User! @lookup + userById(userId: ID! @is(field: "id")): User! @lookup } ``` -In this schema, the `userById` query uses the `@is` directive with {FieldSelectionMap} to declare that the `userId` argument is semantically equivalent to the `User.id` field. +In this schema, the `userById` query uses the `@is` directive with +{FieldSelectionMap} to declare that the `userId` argument is semantically +equivalent to the `User.id` field. An example query might look like this: ```graphql query { - userById(userId: "123") { - id - } + userById(userId: "123") { + id + } } ``` -Here, it is exptected that the `userId` "123" corresponds directly to `User.id`, resulting in the following response if correctly implemented: +Here, it is expected that the `userId` "123" corresponds directly to `User.id`, +resulting in the following response if correctly implemented: ```json { - "data": { - "userById": { - "id": "123" - } + "data": { + "userById": { + "id": "123" } + } } ``` -The {FieldSelectionMap} scalar is represented as a string that, when parsed, produces a {SelectedValue}. +The {FieldSelectionMap} scalar is represented as a string that, when parsed, +produces a {SelectedValue}. -A {SelectedValue} must exactly match the shape of the argument value to be considered valid. For non-scalar arguments, you must specify each field of the input type in {SelectedObjectValue}. +A {SelectedValue} must exactly match the shape of the argument value to be +considered valid. For non-scalar arguments, you must specify each field of the +input type in {SelectedObjectValue}. ```graphql example extend type Query { - findUserByName(user: UserInput! @is(field: "{ firstName: firstName }")): User @lookup + findUserByName(user: UserInput! @is(field: "{ firstName: firstName }")): User + @lookup } ``` ```graphql counter-example extend type Query { - findUserByName(user: UserInput! @is(field: "firstName")): User @lookup + findUserByName(user: UserInput! @is(field: "firstName")): User @lookup } ``` ### Scope -The {FieldSelectionMap} scalar type is used to establish semantic equivalence between an argument and fields within a specific output type. -This output type is always a composite type, but the way it's determined can vary depending on the directive and context in which the {FieldSelectionMap} is used. -For example, when used with the `@is` directive, the {FieldSelectionMap} maps between the argument and fields in the return type of the field. -However, when used with the `@require` directive, it maps between the argument and fields in the object type on which the field is defined. +The {FieldSelectionMap} scalar type is used to establish semantic equivalence +between an argument and fields within a specific output type. This output type +is always a composite type, but the way it's determined can vary depending on +the directive and context in which the {FieldSelectionMap} is used. + +For example, when used with the `@is` directive, the {FieldSelectionMap} maps +between the argument and fields in the return type of the field. However, when +used with the `@require` directive, it maps between the argument and fields in +the object type on which the field is defined. Consider this example: @@ -75,9 +89,12 @@ type Product { } ``` -In this case, `"dimension.size"` and `"dimension.weight"` refer to fields of the `Product` type, not the `DeliveryEstimates` return type. +In this case, `"dimension.size"` and `"dimension.weight"` refer to fields of the +`Product` type, not the `DeliveryEstimates` return type. -Consequently, a {FieldSelectionMap} must be interpreted in the context of a specific argument, its associated directive, and the relevant output type as determined by that directive's behavior. +Consequently, a {FieldSelectionMap} must be interpreted in the context of a +specific argument, its associated directive, and the relevant output type as +determined by that directive's behavior. **Examples** @@ -87,7 +104,7 @@ This example maps the `Product.weight` field to the `weight` argument: ```graphql example type Product { - shippingCost(weight: Float @require(field: "weight")): Currency + shippingCost(weight: Float @require(field: "weight")): Currency } ``` @@ -95,26 +112,29 @@ This example maps the `Product.shippingWeight` field to the `weight` argument: ```graphql example type Product { - shippingCost(weight: Float @require(field: "shippingWeight")): Currency + shippingCost(weight: Float @require(field: "shippingWeight")): Currency } ``` -Nested fields can be mapped to arguments by specifying the path. -This example maps the nested field `Product.packaging.weight` to the `weight` argument: +Nested fields can be mapped to arguments by specifying the path. This example +maps the nested field `Product.packaging.weight` to the `weight` argument: ```graphql example type Product { - shippingCost(weight: Float @require(field: "packaging.weight")): Currency + shippingCost(weight: Float @require(field: "packaging.weight")): Currency } ``` Complex objects can be mapped to arguments by specifying each field. -This example maps the `Product.width` and `Product.height` fields to the `dimension` argument: +This example maps the `Product.width` and `Product.height` fields to the +`dimension` argument: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "{ width: width height: height }")): Currency + shippingCost( + dimension: DimensionInput @require(field: "{ width: width height: height }") + ): Currency } ``` @@ -122,53 +142,70 @@ The shorthand equivalent is: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "{ width height }")): Currency + shippingCost( + dimension: DimensionInput @require(field: "{ width height }") + ): Currency } ``` -In case the input field names do not match the output field names, explicit mapping is required. +In case the input field names do not match the output field names, explicit +mapping is required. ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "{ w: width h: height }")): Currency + shippingCost( + dimension: DimensionInput @require(field: "{ w: width h: height }") + ): Currency } ``` -Even if `Product.dimension` has all the fields needed for the input object, an explicit mapping is always required. +Even if `Product.dimension` has all the fields needed for the input object, an +explicit mapping is always required. This example is NOT allowed because it lacks explicit mapping: ```graphql counter-example type Product { - shippingCost(dimension: DimensionInput @require(field: "dimension")): Currency + shippingCost(dimension: DimensionInput @require(field: "dimension")): Currency } ``` -Instead, you can traverse into output fields by specifying the path. +Instead, you can traverse into output fields by specifying the path. This example shows how to map nested fields explicitly: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "{ width: dimension.width height: dimension.height }")): Currency + shippingCost( + dimension: DimensionInput + @require(field: "{ width: dimension.width height: dimension.height }") + ): Currency } ``` -The path does NOT affect the structure of the input object. It is only used to traverse the output object: +The path does NOT affect the structure of the input object. It is only used to +traverse the output object: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "{ width: size.width height: size.height }")): Currency + shippingCost( + dimension: DimensionInput + @require(field: "{ width: size.width height: size.height }") + ): Currency } ``` -To avoid repeating yourself, you can prefix the selection with a path that ends in a dot to traverse INTO the output type. +To avoid repeating yourself, you can prefix the selection with a path that ends +in a dot to traverse INTO the output type. -This affects how fields get interpreted but does NOT affect the structure of the input object: +This affects how fields get interpreted but does NOT affect the structure of the +input object: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "dimension.{ width height }")): Currency + shippingCost( + dimension: DimensionInput @require(field: "dimension.{ width height }") + ): Currency } ``` @@ -176,29 +213,38 @@ This example is equivalent to the previous one: ```graphql example type Product { - shippingCost(dimension: DimensionInput @require(field: "size.{ width height }")): Currency + shippingCost( + dimension: DimensionInput @require(field: "size.{ width height }") + ): Currency } ``` -The path syntax is required for lists because list-valued path expressions would be ambiguous otherwise. +The path syntax is required for lists because list-valued path expressions would +be ambiguous otherwise. This example is NOT allowed because it lacks the dot syntax for lists: ```graphql counter-example type Product { - shippingCost(dimensions: [DimensionInput] @require(field: "{ width: dimensions.width height: dimensions.height }")): Currency + shippingCost( + dimensions: [DimensionInput] + @require(field: "{ width: dimensions.width height: dimensions.height }") + ): Currency } ``` -Instead, use the path syntax and brackets to specify the list elements: +Instead, use the path syntax and brackets to specify the list elements: ```graphql example type Product { - shippingCost(dimensions: [DimensionInput] @require(field: "dimensions[{ width height }]")): Currency + shippingCost( + dimensions: [DimensionInput] @require(field: "dimensions[{ width height }]") + ): Currency } ``` -With the path syntax it is possible to also select fields from a list of nested objects: +With the path syntax it is possible to also select fields from a list of nested +objects: ```graphql example type Product { @@ -206,32 +252,43 @@ type Product { } ``` -For more complex input objects, all these constructs can be nested. -This allows for detailed and precise mappings. +For more complex input objects, all these constructs can be nested. This allows +for detailed and precise mappings. -This example nests the `weight` field and the `dimension` object with its `width` and `height` fields: +This example nests the `weight` field and the `dimension` object with its +`width` and `height` fields: ```graphql example type Product { - shippingCost(package: PackageInput @require(field: "{ weight, dimension: dimension.{ width height } }")): Currency + shippingCost( + package: PackageInput + @require(field: "{ weight, dimension: dimension.{ width height } }") + ): Currency } ``` -This example nests the `weight` field and the `size` object with its `width` and `height` fields: +This example nests the `weight` field and the `size` object with its `width` and +`height` fields: ```graphql example type Product { - shippingCost(package: PackageInput @require(field: "{ weight, size: dimension.{ width height } }")): Currency + shippingCost( + package: PackageInput + @require(field: "{ weight, size: dimension.{ width height } }") + ): Currency } ``` -The label can be used to nest values that aren't nested in the output. +The label can be used to nest values that aren't nested in the output. This example nests `Product.width` and `Product.height` under `dimension`: ```graphql example type Product { - shippingCost(package: PackageInput @require(field: "{ weight, dimension: { width height } }")): Currency + shippingCost( + package: PackageInput + @require(field: "{ weight, dimension: { width height } }") + ): Currency } ``` @@ -239,87 +296,103 @@ In the following example, dimensions are nested under `dimension` in the output: ```graphql example type Product { - shippingCost(package: PackageInput @require(field: "{ weight, dimension: dimension.{ width height } }")): Currency + shippingCost( + package: PackageInput + @require(field: "{ weight, dimension: dimension.{ width height } }") + ): Currency } ``` ## Language -According to the GraphQL specification, an argument is a key-value pair in which the key is the name of the argument and the value is a `Value`. +According to the GraphQL specification, an argument is a key-value pair in which +the key is the name of the argument and the value is a `Value`. -The `Value` of an argument can take various forms: it might be a scalar value (such as `Int`, `Float`, `String`, `Boolean`, `Null`, or `Enum`), a list (`ListValue`), an input object (`ObjectValue`), or a `Variable`. +The `Value` of an argument can take various forms: it might be a scalar value +(such as `Int`, `Float`, `String`, `Boolean`, `Null`, or `Enum`), a list +(`ListValue`), an input object (`ObjectValue`), or a `Variable`. -Within the scope of the {FieldSelectionMap}, the relationship between input and output is established by defining the `Value` of the argument as a selection of fields from the output object. +Within the scope of the {FieldSelectionMap}, the relationship between input and +output is established by defining the `Value` of the argument as a selection of +fields from the output object. -Yet only certain types of `Value` have a semantic meaning. -`ObjectValue` and `ListValue` are used to define the structure of the value. Scalar values, on the other hand, do not carry semantic importance in this context. +Yet only certain types of `Value` have a semantic meaning. `ObjectValue` and +`ListValue` are used to define the structure of the value. Scalar values, on the +other hand, do not carry semantic importance in this context. -While variables may have legitimate use cases, they are considered out of scope for the current discussion. +While variables may have legitimate use cases, they are considered out of scope +for the current discussion. -However, it's worth noting that there could be potential applications for allowing them in the future. +However, it's worth noting that there could be potential applications for +allowing them in the future. -Given that these potential values do not align with the standard literals defined in the GraphQL specification, a new literal called {SelectedValue} is introduced, along with {SelectedObjectValue}. +Given that these potential values do not align with the standard literals +defined in the GraphQL specification, a new literal called {SelectedValue} is +introduced, along with {SelectedObjectValue}. Beyond these literals, an additional literal called {Path} is necessary. ### Name -Is equivalent to the {Name} defined in the [GraphQL specification](https://spec.graphql.org/October2021/#Name) +Is equivalent to the {Name} defined in the +[GraphQL specification](https://spec.graphql.org/October2021/#Name) ### Path -Path :: - - < TypeName > . PathSemgent - - PathSemgent -PathSegment :: - - FieldName - - FieldName . PathSemgent - - FieldName < TypeName > . PathSemgent +Path :: - < TypeName > . PathSegment - PathSegment + +PathSegment :: - FieldName - FieldName . PathSegment - FieldName < TypeName > . +PathSegment -FieldName :: - - Name +FieldName :: - Name -TypeName :: - - Name +TypeName :: - Name -The {Path} literal is a string used to select a single output value from the _return type_ by specifying a path to that value. -This path is defined as a sequence of field names, each separated by a period (`.`) to create segments. +The {Path} literal is a string used to select a single output value from the +_return type_ by specifying a path to that value. This path is defined as a +sequence of field names, each separated by a period (`.`) to create segments. -``` example +```example book.title ``` -Each segment specifies a field in the context of the parent, with the root segment referencing a field in the _return type_ of the query. -Arguments are not allowed in a {Path}. +Each segment specifies a field in the context of the parent, with the root +segment referencing a field in the _return type_ of the query. Arguments are not +allowed in a {Path}. -To select a field when dealing with abstract types, the segment selecting the parent field must specify the concrete type of the field using angle brackets after the field name if the field is not defined on an interface. +To select a field when dealing with abstract types, the segment selecting the +parent field must specify the concrete type of the field using angle brackets +after the field name if the field is not defined on an interface. -In the following example, the path `mediaById.isbn` specifies that `mediaById` returns a `Book`, and the `isbn` field is selected from that `Book`. +In the following example, the path `mediaById.isbn` specifies that +`mediaById` returns a `Book`, and the `isbn` field is selected from that `Book`. -``` example +```example mediaById.isbn ``` ### SelectedValue -SelectedValue :: - - Path - - SelectedObjectValue - - Path . SelectedObjectValue - - SelectedValue | SelectedValue + +SelectedValue :: - Path - SelectedObjectValue - Path . SelectedObjectValue - +SelectedValue | SelectedValue A {SelectedValue} is defined as either a {Path} or a {SelectedObjectValue} -A {Path} is designed to point to only a single value, although it may reference multiple fields depending on the return type. -To allow selection from different paths based on type, a {Path} can include multiple paths separated by a pipe (`|`). +A {Path} is designed to point to only a single value, although it may reference +multiple fields depending on the return type. To allow selection from different +paths based on type, a {Path} can include multiple paths separated by a pipe +(`|`). -In the following example, the value could be `title` when referring to a `Book` and `movieTitle` when referring to a `Movie`. +In the following example, the value could be `title` when referring to a `Book` +and `movieTitle` when referring to a `Movie`. -``` example +```example mediaById.title | mediaById.movieTitle ``` -The `|` operator can be used to match multiple possible {SelectedValue}. -This operator is applied when mapping an abstract output type to a `@oneOf` input type. +The `|` operator can be used to match multiple possible {SelectedValue}. This +operator is applied when mapping an abstract output type to a `@oneOf` input +type. ```example { movieId: .id } | { productId: .id } @@ -330,19 +403,25 @@ This operator is applied when mapping an abstract output type to a `@oneOf` inpu ``` ### SelectedObjectValue -SelectedObjectValue :: - - { SelectedObjectField+ } -SelectedObjectField :: - - Name: SelectedValue +SelectedObjectValue :: - { SelectedObjectField+ } + +SelectedObjectField :: - Name: SelectedValue -{SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. -It has to be used when the expected input type is an object type. +{SelectedObjectValue} are unordered lists of keyed input values wrapped in +curly-braces `{}`. It has to be used when the expected input type is an object +type. -This structure is similar to the `ObjectValue` defined in the GraphQL specification, but it differs by allowing the inclusion of {Path} values within a {SelectedValue}, thus extending the traditional `ObjectValue` capabilities to support direct path selections. +This structure is similar to the `ObjectValue` defined in the GraphQL +specification, but it differs by allowing the inclusion of {Path} values within +a {SelectedValue}, thus extending the traditional `ObjectValue` capabilities to +support direct path selections. -A {SelectedObjectValue} following a {Path} is scoped to the type of the field selected by the {Path}. -This means that the root of all {SelectedValue} inside the selection is no longer scoped to the root (defined by `@is` or `@require`) but to the field selected by the {Path}. The {Path} does not affect the structure of the input type. +A {SelectedObjectValue} following a {Path} is scoped to the type of the field +selected by the {Path}. This means that the root of all {SelectedValue} inside +the selection is no longer scoped to the root (defined by `@is` or `@require`) +but to the field selected by the {Path}. The {Path} does not affect the +structure of the input type. This allows for reducing repetition in the selection. @@ -350,8 +429,10 @@ The following example is valid: ```graphql example type Product { - dimension: Dimension! - shippingCost(dimension: DimensionInput! @require(field: "dimension.{ size weight }")): Int! + dimension: Dimension! + shippingCost( + dimension: DimensionInput! @require(field: "dimension.{ size weight }") + ): Int! } ``` @@ -359,76 +440,91 @@ The following example is equivalent to the previous one: ```graphql example type Product { - dimensions: Dimension! - shippingCost(dimensions: DimensionInput! @require(field: "{ size: dimensions.size weight: dimensions.weight }")): Int! @lookup + dimensions: Dimension! + shippingCost( + dimensions: DimensionInput! + @require(field: "{ size: dimensions.size weight: dimensions.weight }") + ): Int! @lookup } ``` - ### SelectedListValue -SelectedListValue :: - - [ SelectedValue ] -A {SelectedListValue} is an ordered list of {SelectedValue} wrapped in square brackets `[]`. -It is used to express semantic equivalence between an argument expecting a list of values and the values of a list field within the output object. +SelectedListValue :: - [ SelectedValue ] -The {SelectedListValue} differs from the `ListValue` defined in the GraphQL specification by only allowing one {SelectedValue} as an element. +A {SelectedListValue} is an ordered list of {SelectedValue} wrapped in square +brackets `[]`. It is used to express semantic equivalence between an argument +expecting a list of values and the values of a list field within the output +object. + +The {SelectedListValue} differs from the `ListValue` defined in the GraphQL +specification by only allowing one {SelectedValue} as an element. The following example is valid: ```graphql example type Product { - parts: [Part!]! - partIds(partIds: [ID!]! @require(field: "parts[id]")): [ID!]! + parts: [Part!]! + partIds(partIds: [ID!]! @require(field: "parts[id]")): [ID!]! } -``` +``` -In this example, the `partIds` argument is semantically equivalent to the `id` fields of the `parts` list. +In this example, the `partIds` argument is semantically equivalent to the `id` +fields of the `parts` list. -The following example is invalid because it uses multiple {SelectedValue} as elements: +The following example is invalid because it uses multiple {SelectedValue} as +elements: ```graphql counter-example type Product { - parts: [Part!]! - partIds(parts: [PartInput!]! @require(field: "parts[id name]")): [ID!]! + parts: [Part!]! + partIds(parts: [PartInput!]! @require(field: "parts[id name]")): [ID!]! } input PartInput { - id: ID! - name: String! + id: ID! + name: String! } -``` +``` -A {SelectedObjectValue} can be used as an element of a {SelectedListValue} to select multiple object fields as long as the input type is a list of structurally equivalent objects. +A {SelectedObjectValue} can be used as an element of a {SelectedListValue} to +select multiple object fields as long as the input type is a list of +structurally equivalent objects. -Similar to {SelectedObjectValue}, a {SelectedListValue} following a {Path} is scoped to the type of the field selected by the {Path}. -This means that the root of all {SelectedValue} inside the selection is no longer scoped to the root (defined by `@is` or `@require`) but to the field selected by the {Path}. The {Path} does not affect the structure of the input type. +Similar to {SelectedObjectValue}, a {SelectedListValue} following a {Path} is +scoped to the type of the field selected by the {Path}. This means that the root +of all {SelectedValue} inside the selection is no longer scoped to the root +(defined by `@is` or `@require`) but to the field selected by the {Path}. The +{Path} does not affect the structure of the input type. The following example is valid: ```graphql example type Product { - parts: [Part!]! - partIds(parts: [PartInput!]! @require(field: "parts[{ id name }]")): [ID!]! + parts: [Part!]! + partIds(parts: [PartInput!]! @require(field: "parts[{ id name }]")): [ID!]! } input PartInput { - id: ID! - name: String! + id: ID! + name: String! } ``` -In case the input type is a nested list, the shape of the input object must match the shape of the output object. +In case the input type is a nested list, the shape of the input object must +match the shape of the output object. ```graphql example type Product { - parts: [[Part!]]! - partIds(parts: [[PartInput!]]! @require(field: "parts[[{ id name }]]")): [ID!]! + parts: [[Part!]]! + partIds( + parts: [[PartInput!]]! @require(field: "parts[[{ id name }]]") + ): [ID!]! } input PartInput { - id: ID! - name: String! + id: ID! + name: String! } ``` @@ -436,99 +532,111 @@ The following example is valid: ```graphql example type Query { - findLocation(location: LocationInput! @is(field: "{ coordinates: coordinates[{lat: x lon: y}]}")): Location @lookup + findLocation( + location: LocationInput! + @is(field: "{ coordinates: coordinates[{lat: x lon: y}]}") + ): Location @lookup } type Coordinate { - x: Int! - y: Int! + x: Int! + y: Int! } type Location { - coordinates: [Coordinate!]! + coordinates: [Coordinate!]! } input PositionInput { - lat: Int! - lon: Int! + lat: Int! + lon: Int! } input LocationInput { - coordinates: [PositionInput!]! + coordinates: [PositionInput!]! } ``` ## Validation -Validation ensures that {FieldSelectionMap} scalars are semantically correct within the given context. -Validation of {FieldSelectionMap} scalars occurs during the composition phase, ensuring that all {FieldSelectionMap} entries are syntactically correct and semantically meaningful relative to the context. +Validation ensures that {FieldSelectionMap} scalars are semantically correct +within the given context. -Composition is only possible if the {FieldSelectionMap} is validated successfully. An invalid {FieldSelectionMap} results in undefined behavior, making composition impossible. +Validation of {FieldSelectionMap} scalars occurs during the composition phase, +ensuring that all {FieldSelectionMap} entries are syntactically correct and +semantically meaningful relative to the context. -In this section, we will assume the following type system in order to demonstrate examples: +Composition is only possible if the {FieldSelectionMap} is validated +successfully. An invalid {FieldSelectionMap} results in undefined behavior, +making composition impossible. + +In this section, we will assume the following type system in order to +demonstrate examples: ```graphql type Query { - mediaById(mediaId: ID!): Media - findMedia(input: FindMediaInput): Media - searchStore(search: SearchStoreInput): [Store]! - storeById(id: ID!): Store + mediaById(mediaId: ID!): Media + findMedia(input: FindMediaInput): Media + searchStore(search: SearchStoreInput): [Store]! + storeById(id: ID!): Store } type Store { - id: ID! - city: String! - media: [Media!]! + id: ID! + city: String! + media: [Media!]! } interface Media { - id: ID! + id: ID! } type Book implements Media { - id: ID! - title: String! - isbn: String! - author: Author! + id: ID! + title: String! + isbn: String! + author: Author! } type Movie implements Media { - id: ID! - movieTitle: String! - releaseDate: String! + id: ID! + movieTitle: String! + releaseDate: String! } type Author { - id: ID! - books: [Book!]! + id: ID! + books: [Book!]! } input FindMediaInput @oneOf { - bookId: ID - movieId: ID + bookId: ID + movieId: ID } type SearchStoreInput { - city: String - hasInStock: FindMediaInput + city: String + hasInStock: FindMediaInput } ``` ### Path Field Selections -Each segment of a {Path} must correspond to a valid field defined on the current type context. +Each segment of a {Path} must correspond to a valid field defined on the current +type context. **Formal Specification** -- For each {segment} in the {Path}: - - If the {segment} is a field - - Let {fieldName} be the field name in the current {segment}. - - {fieldName} must be defined on the current type in scope. +- For each {segment} in the {Path}: +- If the {segment} is a field + - Let {fieldName} be the field name in the current {segment}. + - {fieldName} must be defined on the current type in scope. **Explanatory Text** -The {Path} literal is used to reference a specific output field from a input field. -Each segment in the {Path} must correspond to a field that is valid within the current type scope. +The {Path} literal is used to reference a specific output field from a input +field. Each segment in the {Path} must correspond to a field that is valid +within the current type scope. For example, the following {Path} is valid in the context of `Book`: @@ -540,8 +648,10 @@ title .title ``` -Incorrect paths where the field does not exist on the specified type is not valid result in validation errors. -For instance, if `.movieId` is referenced but `movieId` is not a field of `Book`, will result in an invalid {Path}. +Incorrect paths where the field does not exist on the specified type is not +valid result in validation errors. For instance, if `.movieId` is +referenced but `movieId` is not a field of `Book`, will result in an invalid +{Path}. ```graphql counter-example movieId @@ -553,23 +663,27 @@ movieId ### Path Terminal Field Selections -Each terminal segment of a {Path} must follow the rules regarding whether the selected field is a leaf node. +Each terminal segment of a {Path} must follow the rules regarding whether the +selected field is a leaf node. **Formal Specification** -- For each {segment} in the {Path}: - - Let {selectedType} be the unwrapped type of the current {segment}. - - If {selectedType} is a scalar or enum: - - There must not be any further segments in {Path}. - - If {selectedType} is an object, interface, or union: - - There must be another segment in {Path}. +- For each {segment} in the {Path}: +- Let {selectedType} be the unwrapped type of the current {segment}. +- If {selectedType} is a scalar or enum: + - There must not be any further segments in {Path}. +- If {selectedType} is an object, interface, or union: + - There must be another segment in {Path}. **Explanatory Text** -A {Path} that refers to scalar or enum fields must end at those fields. No further field selections are allowed after a scalar or enum. -On the other hand, fields returning objects, interfaces, or unions must continue to specify further selections until you reach a scalar or enum field. +A {Path} that refers to scalar or enum fields must end at those fields. No +further field selections are allowed after a scalar or enum. On the other hand, +fields returning objects, interfaces, or unions must continue to specify further +selections until you reach a scalar or enum field. -For example, the following {Path} is valid if `title` is a scalar field on the `Book` type: +For example, the following {Path} is valid if `title` is a scalar field on the +`Book` type: ```graphql example book.title @@ -581,7 +695,8 @@ The following {Path} is invalid because `title` should not have subselections: book.title.something ``` -For non-leaf fields, the {Path} must continue to specify subselections until a leaf field is reached: +For non-leaf fields, the {Path} must continue to specify subselections until a +leaf field is reached: ```graphql example book.author.id @@ -595,43 +710,46 @@ book.author ### Type Reference Is Possible -Each segment of a {Path} that references a type, must be a type that is valid in the current context. +Each segment of a {Path} that references a type, must be a type that is valid in +the current context. **Formal Specification** -- For each {segment} in a {Path}: - - If {segment} is a type reference: - - Let {type} be the type referenced in the {segment}. - - Let {parentType} be the type of the parent of the {segment}. - - Let {applicableTypes} be the intersection of - {GetPossibleTypes(type)} and {GetPossibleTypes(parentType)}. - - {applicableTypes} must not be empty. +- For each {segment} in a {Path}: +- If {segment} is a type reference: + - Let {type} be the type referenced in the {segment}. + - Let {parentType} be the type of the parent of the {segment}. + - Let {applicableTypes} be the intersection of {GetPossibleTypes(type)} and + {GetPossibleTypes(parentType)}. + - {applicableTypes} must not be empty. GetPossibleTypes(type): -- If {type} is an object type, return a set containing {type}. -- If {type} is an interface type, return the set of types implementing {type}. -- If {type} is a union type, return the set of possible types of {type}. +- If {type} is an object type, return a set containing {type}. +- If {type} is an interface type, return the set of types implementing {type}. +- If {type} is a union type, return the set of possible types of {type}. **Explanatory Text** -Type references inside a {Path} must be valid within the context of the surrounding type. -A type reference is only valid if the referenced type could logically apply within the parent type. - +Type references inside a {Path} must be valid within the context of the +surrounding type. A type reference is only valid if the referenced type could +logically apply within the parent type. ### Values of Correct Type **Formal Specification** -- For each SelectedValue {value}: - - Let {type} be the type expected in the position {value} is found. - - {value} must be coercible to {type}. +- For each SelectedValue {value}: +- Let {type} be the type expected in the position {value} is found. +- {value} must be coercible to {type}. **Explanatory Text** -Literal values must be compatible with the type expected in the position they are found. +Literal values must be compatible with the type expected in the position they +are found. -The following examples are valid use of value literals in the context of {FieldSelectionMap} scalar: +The following examples are valid use of value literals in the context of +{FieldSelectionMap} scalar: ```graphql example type Query { @@ -639,8 +757,8 @@ type Query { } type Store { - id: ID - city: String! + id: ID + city: String! } ``` @@ -652,8 +770,8 @@ type Query { } type Store { - id: Int - city: String! + id: Int + city: String! } ``` @@ -661,14 +779,16 @@ type Store { **Formal Specification** -- For each Selected Object Field {field} in the document: - - Let {fieldName} be the Name of {field}. - - Let {fieldDefinition} be the field definition provided by the parent selected object type named {fieldName}. - - {fieldDefinition} must exist. +- For each Selected Object Field {field} in the document: +- Let {fieldName} be the Name of {field}. +- Let {fieldDefinition} be the field definition provided by the parent selected + object type named {fieldName}. +- {fieldDefinition} must exist. **Explanatory Text** -Every field provided in an selected object value must be defined in the set of possible fields of that input object's expected type. +Every field provided in an selected object value must be defined in the set of +possible fields of that input object's expected type. For example, the following is valid: @@ -678,12 +798,13 @@ type Query { } type Store { - id: ID - city: String! + id: ID + city: String! } ``` -In contrast, the following is invalid because it uses a field "address" which is not defined on the expected type: +In contrast, the following is invalid because it uses a field "address" which is +not defined on the expected type: ```graphql counter-example extend type Query { @@ -691,8 +812,8 @@ extend type Query { } type Store { - id: ID - city: String! + id: ID + city: String! } ``` @@ -700,15 +821,16 @@ type Store { **Formal Specification** -- For each selected object value {selectedObject}: - - For every {field} in {selectedObject}: - - Let {name} be the Name of {field}. - - Let {fields} be all Selected Object Fields named {name} in {selectedObject}. - - {fields} must be the set containing only {field}. +- For each selected object value {selectedObject}: +- For every {field} in {selectedObject}: + - Let {name} be the Name of {field}. + - Let {fields} be all Selected Object Fields named {name} in {selectedObject}. + - {fields} must be the set containing only {field}. **Explanatory Text** -Selected objects must not contain more than one field with the same name, as it would create ambiguity and potential conflicts. +Selected objects must not contain more than one field with the same name, as it +would create ambiguity and potential conflicts. For example, the following is invalid: @@ -718,8 +840,8 @@ extend type Query { } type Store { - id: ID - city: String! + id: ID + city: String! } ``` @@ -727,29 +849,30 @@ type Store { **Formal Specification** -- For each Selected Object: - - Let {fields} be the fields provided by that Selected Object. - - Let {fieldDefinitions} be the set of input object field definitions of that Selected Object. - - For each {fieldDefinition} in {fieldDefinitions}: - - Let {type} be the expected type of {fieldDefinition}. - - Let {defaultValue} be the default value of {fieldDefinition}. - - If {type} is Non-Null and {defaultValue} does not exist: - - Let {fieldName} be the name of {fieldDefinition}. - - Let {field} be the input object field in {fields} named {fieldName}. - - {field} must exist. +- For each Selected Object: +- Let {fields} be the fields provided by that Selected Object. +- Let {fieldDefinitions} be the set of input object field definitions of that + Selected Object. +- For each {fieldDefinition} in {fieldDefinitions}: + - Let {type} be the expected type of {fieldDefinition}. + - Let {defaultValue} be the default value of {fieldDefinition}. + - If {type} is Non-Null and {defaultValue} does not exist: + - Let {fieldName} be the name of {fieldDefinition}. + - Let {field} be the input object field in {fields} named {fieldName}. + - {field} must exist. **Explanatory Text** -Input object fields may be required. -This means that a selected object field is required if the corresponding input field is required. -Otherwise, the selected object field is optional. +Input object fields may be required. This means that a selected object field is +required if the corresponding input field is required. Otherwise, the selected +object field is optional. For instance, if the `UserInput` type requires the `id` field: ```graphql example input UserInput { - id: ID! - name: String! + id: ID! + name: String! } ``` @@ -761,7 +884,8 @@ extend type Query { } ``` -If the `UserInput` type requires the `name` field, but the `User` type has an optional `name` field, the following selection would be valid. +If the `UserInput` type requires the `name` field, but the `User` type has an +optional `name` field, the following selection would be valid. ```graphql example extend type Query { @@ -769,17 +893,18 @@ extend type Query { } type User { - id: ID - name: String + id: ID + name: String } input UserInput { - id: ID - name: String! + id: ID + name: String! } ``` -But if the `UserInput` type requires the `name` field but it's not defined in the `User` type, the selection would be invalid. +But if the `UserInput` type requires the `name` field but it's not defined in +the `User` type, the selection would be invalid. ```graphql counter-example extend type Query { @@ -787,11 +912,11 @@ extend type Query { } type User { - id: ID + id: ID } input UserInput { - id: ID - name: String! + id: ID + name: String! } -``` \ No newline at end of file +``` diff --git a/spec/Section 2 -- Source Schema.md b/spec/Section 2 -- Source Schema.md index b8e20ce..5bbc683 100644 --- a/spec/Section 2 -- Source Schema.md +++ b/spec/Section 2 -- Source Schema.md @@ -1,244 +1,458 @@ # Source Schema +A _source schema_ is a GraphQL schema that is part of a larger _composite +schema_. Source schemas use directives to express intent and requirements for +the composition process. In the following chapters, we will describe the +directives that are used to annotate a source schema. + ## Directives -### @entityResolver +### @lookup ```graphql -directive @entityResolver on FIELD_DEFINITION +directive @lookup on FIELD_DEFINITION ``` -Entity resolvers are fields on the query root type of a subgraph that can -resolve an entity by a stable key. The stable key is defined by the arguments of -the field. +The `@lookup` directive is used within a _source schema_ to specify output +fields that can be used by the _distributed GraphQL executor_ to resolve an +entity by a stable key. + +The stable key is defined by the arguments of the field. Each argument must +match a field on the return type of the lookup field. + +Source schemas can provide multiple lookup fields for the same entity that +resolve the entity by different keys. + +In this example, the source schema specifies that the `Product` entity can be +resolved with the `productById` field or the `productByName` field. Both lookup +fields are able to resolve the `Product` entity but do so with different keys. ```graphql example -extend type Query { - version: Int # NOT an entity resolver. - personById(id: ID!): Person @entityResolver +type Query { + version: Int # NOT a lookup field. + productById(id: ID!): Product @lookup + productByName(name: String!): Product @lookup } -extend type Person { - id: ID! # matches the argument of personById +type Product @key(fields: "id") @key(fields: "name") { + id: ID! + name: String! } ``` -The arguments of an entity resolver field must match fields of the returning -type. +The arguments of a lookup field must correspond to fields specified as an entity +key with the `@key` directive on the entity type. ```graphql example -extend type Query { - node(id: ID!): Node @entityResolver +type Query { + node(id: ID!): Node @lookup } -interface Node { +interface Node @key(fields: "id") { id: ID! } ``` -When an entity resolver returns an interface all implementing types are inferred -as entities. +Lookup fields may return object, interface, or union types. In case a lookup +field returns an abstract type (interface type or union type), all possible +object types are considered entities and must have keys that correspond with the +field's argument signature. ```graphql example -extend type Query { - entityById(id: ID!, categoryId: Int): Entity @entityResolver +type Query { + product(id: ID!, categoryId: Int): Product @lookup +} + +union Product = Electronics | Clothing + +type Electronics @key(fields: "id categoryId") { + id: ID! + categoryId: Int + name: String + brand: String + price: Float +} + +type Clothing @key(fields: "id categoryId") { + id: ID! + categoryId: Int + name: String + size: String + price: Float +} +``` + +The following example shows an invalid lookup field as the `Clothing` type does +not declare a key that corresponds with the lookup field's argument signature. + +```graphql counter-example +type Query { + product(id: ID!, categoryId: Int): Product @lookup } -union Entity = Cat | Dog +union Product = Electronics | Clothing -extend type Dog { +type Electronics @key(fields: "id categoryId") { id: ID! categoryId: Int + name: String + brand: String + price: Float } -extend type Cat { +# Clothing does not have a key that corresponds +# with the lookup field's argument signature. +type Clothing @key(fields: "id") { id: ID! categoryId: Int + name: String + size: String + price: Float +} +``` + +If the lookup returns an interface, the interface must also be annotated with a +`@key` directive and declare its keys. + +```graphql example +interface Node @key(fields: "id") { + id: ID! +} +``` + +Lookup fields must be accessible from the Query type. If not directly on the +Query type, they must be accessible via fields that do not require arguments, +starting from the Query root type. + +```graphql example +type Query { + lookups: Lookups! +} + +type Lookups { + productById(id: ID!): Product @lookup +} + +type Product @key(fields: "id") { + id: ID! } ``` +Lookups can also be nested within other lookups and allow resolving nested +entities that are part of an aggregate. In the following example the `Product` +can be resolved by its ID but also the `ProductPrice` can be resolved by passing +in a composite key containing the product ID and region name of the product +price. + +```graphql example +type Query { + productById(id: ID!): Product @lookup +} + +type Product @key(fields: "id") { + id: ID! + price(regionName: String!): ProductPrice @lookup +} + +type ProductPrice @key(fields: "regionName product { id }") { + regionName: String! + product: Product + value: Float! +} +``` + +Nested lookups must immediately follow the parent lookup and cannot be nested +with fields in between. + +```graphql counter-example +type Query { + productById(id: ID!): Product @lookup +} + +type Product @key(fields: "id") { + id: ID! + details: ProductDetails +} + +type ProductDetails { + price(regionName: String!): ProductPrice @lookup +} + +type ProductPrice @key(fields: "regionName product { id }") { + regionName: String! + product: Product + value: Float! +} +``` + +### @internal + +```graphql +directive @internal on FIELD_DEFINITION +``` + +The `@internal` directive is used to mark lookup fields as internal. Internal +lookup fields are not used as entry points in the composite schema and can only +be used by the _distributed GraphQL executor_ to resolve additional data for an +entity. + +```graphql example +type Query { + # lookup field and possible entry point + reviewById(id: ID!): Review @lookup + + # internal lookup field + productById(id: ID!): Product @lookup @internal +} +``` + +The `@internal` directive provides control over which source schemas are used to +resolve entities and which source schemas merely contribute data to entities. +Further, using `@internal` allows hiding "technical" lookup fields that are not +meant for the client-facing composite schema. + ### @is ```graphql -directive @is( - field: FieldSelection - coordinate: Schemacoordinate -) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +directive @is(field: FieldSelectionMap!) on ARGUMENT_DEFINITION ``` -The `@is` directive is utilized to establish semantic equivalence between -disparate type system members across distinct subgraphs, which the schema -composition uses to connect types. +The `@is` directive is utilized on lookup fields to describe how the arguments +can be mapped from the entity type that the lookup field resolves. The mapping +establishes semantic equivalence between disparate type system members across +source schemas and is used in cases where the argument does not 1:1 align with a +field on the entity type. In the following example, the directive specifies that the `id` argument on the field `Query.personById` and the field `Person.id` on the return type of the -field are semantically the same. This information is used to infer an entity -resolver for `Person` from the field `Query.personById`. +field are semantically the same. + +Note: In this case the `@is` directive could also be omitted as the argument and +field names match. ```graphql example extend type Query { - personById(id: ID! @is(field: "id")): Person @entityResolver + personById(id: ID! @is(field: "id")): Person @lookup } ``` -The `@is` directive also allows to refer to nested fields relative to `Person`. +The `@is` directive also allows referring to nested fields relative to `Person`. ```graphql example extend type Query { - personByAddressId(id: ID! @is(field: "address { id }")): Person + personByAddressId(id: ID! @is(field: "address.id")): Person } ``` -The `@is` directive not limited to a single argument. +The `@is` directive is not limited to a single argument. ```graphql example extend type Query { personByAddressId( - id: ID! @is(field: "address { id }") + id: ID! @is(field: "address.id") kind: PersonKind @is(field: "kind") ): Person } ``` -The directive can also establish semantic equivalence between two output fields. -In this example, the field `productSKU` is semantically equivalent to the field -`Product.sku`, allowing the schema composition to infer the connection of the -`Product` with the `Review` type. +The `@is` directive can also be used in combination with `@oneOf` to specify +lookup fields that can resolve entities by different keys. ```graphql example -extend type Review { - productSKU: ID! @is(coordinate: "Product.sku") @internal - product: Product @resolve +extend type Query { + person( + by: PersonByInput @is(field: "{ id } | { addressId: address.id } { name }") + ): Person } -``` - -The `@is` directive can use either the `field` or `coordinate` argument. If both -are specified, the schema composition must fail. -```graphql counter-example -extend type Review { - productSKU: ID! - @is(coordinate: "Product.sku", field: "product { sku }") - @internal - product: Product @resolve +input PersonByInput @oneOf { + id: ID + addressId: ID + name: String } ``` **Arguments:** -- `field`: Represents a GraphQL field selection syntax that refers to field - relative to the current type; or, when used on arguments it refers to a field - relative to the return type. -- `coordinate`: Represents a schema coordinate that refers to a type system - member. +- `field`: Represents a selection path map syntax. -### @shareable +### @require ```graphql -directive @shareable repeatable on OBJECT | FIELD_DEFINITION +directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION ``` -By default, only one subgraph is allowed to contribute a particular field to an -object type. This prevents subgraphs from inadvertently defining similarly named -fields that are semantically not the same. - -Fields have to be explicitly marked as `@shareable` to allow multiple subgraphs -to define it. And it ensures the step of allowing a field to be served from -multiple subgraphs is an explicit, coordinated decision. - -If multiple subgraphs define the same field, these are assumed to be -semantically equivalent, and the executor is free to choose between them as it -sees fit. +The `@require` directive is used to express data requirements with other source +schemas. Arguments annotated with the `@require` directive are removed from the +_composite schema_ and the value for these will be resolved by the _distributed +executor_. -Note: Key fields are always considered sharable. +```graphql example +type Product { + id: ID! + delivery( + zip: String! + size: Int! @require(field: "dimension.size") + weight: Int! @require(field: "dimension.weight") + ): DeliveryEstimates +} +``` -### @require +The upper example would translate to the following in the _composite schema_. -```graphql -directive @require( - field: FieldSelection! -) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +```graphql example +type Product { + id: ID! + delivery(zip: String!): DeliveryEstimates +} ``` -The `@require` directive is used to express data requirements with other -subgraphs. Arguments annotated with the `@require` directive are removed from -the public exposed schema and the value for these will be resolved by the -executor. +This can also be done by using input types. The selection path map specifies +which data is required and needs to be resolved from other source schemas. If +the input type is only used to express a requirements it is removed from the +composite schema. ```graphql example type Product { id: ID! delivery( zip: String! - size: Int! @require(field: "dimension { size }") - weight: Int! @require(field: "dimension { weight }") + dimension: ProductDimensionInput! @require(field: "{ size: dimension.size, weight: dimension.weight }")) ): DeliveryEstimates } ``` -This can also be done by using input types. All fields of the input type that -match the required output type are required. If the input type is only used to -express a requirement it is removed from the public schema. +If the input types do not match the output type structure the selection map +syntax can be used to specify how requirements translate to the input object. ```graphql example type Product { id: ID! delivery( zip: String! - dimension: ProductDimensionInput! @require(field: "dimension")) + dimension: ProductDimensionInput! + @require(field: "{ productSize: dimension.size, productWeight: dimension.weight }")) ): DeliveryEstimates } + +type ProductDimension { + size: Int! + weight: Int! +} + +input ProductDimensionInput { + productSize: Int! + productWeight: Int! +} ``` -### @provides +**Arguments:** + +- `field`: Represents a selection path map syntax. + +### @key ```graphql -directive @provides(fields: SelectionSet!) on FIELD_DEFINITION +directive @key(fields: SelectionSet!) repeatable on OBJECT | INTERFACE ``` -The `@provides` directive is an optimization hint specifying child fields that -can be resolved locally at the given subgraph through a particular query path. -This allows for a variation of overlapping field to improve data fetching. +The @key directive is used to designate an entity's unique key, which identifies +how to uniquely reference an instance of an entity across different source +schemas. It allows a source schema to indicate which fields form a unique +identifier, or **key**, for an entity. -### @external +```graphql example +type Product @key(fields: "id") { + id: ID! + sku: String! + name: String! + price: Float! +} +``` -```graphql -directive @external on OBJECT_DEFINITION | INTERFACE_DEFINITION | FIELD_DEFINITION +Each occurrence of the @key directive on an object or interface type specifies +one distinct unique key for that entity, which enables a gateway to perform +lookups and resolve instances of the entity based on that key. + +```graphql example +type Product @key(fields: "id") @key(fields: "key") { + id: ID! + sku: String! + name: String! + price: Float! +} ``` -The `@external` directive is used in combination with the `@provides` directive -and specifies data that is not owned ba a particular subgraph. +While multiple keys define separate ways to reference the same entity based on +different sets of fields, a composite key allows to uniquely identify an entity +by using a combination of multiple fields. -### @override +```graphql example +type Product @key(fields: "id sku") { + id: ID! + sku: String! + name: String! + price: Float! +} +``` + +The directive is applicable to both OBJECT and INTERFACE types. This allows +entities that implement an interface to inherit the key(s) defined at the +interface level, ensuring consistent identification across different +implementations of that interface. + +**Arguments:** + +- `fields`: Represents a selection set syntax. + +### @shareable ```graphql -directive @override(from: String!) on FIELD_DEFINITION +directive @shareable repeatable on OBJECT | FIELD_DEFINITION ``` -The `@override` directive allows to migrate fields from one subgraph to another. +By default, only a single source schema is allowed to contribute a particular +field to an object type. This prevents source schemas from inadvertently +defining similarly named fields that are semantically not the same. -### @internal +Fields have to be explicitly marked as `@shareable` to allow multiple source +schemas to define it, and ensures that the step of allowing a field to be served +from multiple source schemas is an explicit, coordinated decision. + +If multiple source schemas define the same field, these are assumed to be +semantically equivalent, and the executor is free to choose between them as it +sees fit. + +Note: Key fields are always considered sharable. + +### @provides ```graphql -directive @internal on OBJECT | INTERFACE | FIELD_DEFINITION | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCALAR +directive @provides(fields: SelectionSet!) on FIELD_DEFINITION ``` -The `@internal` directive signals to the composition process that annotated type -system members shall not be included into the public schema but still can be -used by the executor to build resolvers. +The `@provides` directive is an optimization hint specifying child fields that +can be resolved locally at the given source schema through a particular query +path. This allows for a variation of overlapping field to improve data fetching. + +**Arguments:** -### Schemacoordinate +- `fields`: Represents a selection set syntax. + +### @external ```graphql -scalar Schemacoordinate +directive @external on OBJECT_DEFINITION | INTERFACE_DEFINITION | FIELD_DEFINITION ``` -The `Schemacoordinate` scalar represents a schema coordinate syntax. +The `@external` directive is used in combination with the `@provides` directive +and specifies data that is not owned ba a particular source schema. -```graphql example -Product.id -``` +### @override -```graphql example -Product.estimateDelivery(zip:) +```graphql +directive @override(from: String!) on FIELD_DEFINITION ``` + +The `@override` directive allows to migrate fields from one source schema to +another. diff --git a/spec/temp.md b/spec/temp.md index 561ef52..9220f23 100644 --- a/spec/temp.md +++ b/spec/temp.md @@ -62,6 +62,36 @@ composite schemas spec therefore allows multiple keys to be defined for each entity type, and each subgraph defines the particular keys that it is able to support. +Lookups can also be nested if they can be reached through other lookups. + +```graphql example +type Query { + organization(id: ID!): Organization @lookup +} + +type Organization { + repository(name: String!): Repository @lookup +} + +type Repository @key(fields: "id organization { id }") { + name: String! + organization: Organization +} +``` + +The arguments of a lookup field must correspond to fields specified by a `@key` +directive annotated on the return type of the lookup field. + +```graphql example +type Query { + node(id: ID!): Node @lookup +} + +interface Node @key(fields: "id") { + id: ID! +} +``` + # Composition The composition of subgraphs describes the process of merging multiple subgraph