From 0f4bdfacdaebb2cbb7896eb57b7f7ad0c1c38369 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Wed, 17 Apr 2024 21:09:53 +0200 Subject: [PATCH 01/17] Adds field selection RFC --- rfcs/field-selection.md | 1450 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1450 insertions(+) create mode 100644 rfcs/field-selection.md diff --git a/rfcs/field-selection.md b/rfcs/field-selection.md new file mode 100644 index 0000000..a6dfaf6 --- /dev/null +++ b/rfcs/field-selection.md @@ -0,0 +1,1450 @@ +This RFC proposes a syntax for `FieldSelection` that allows for flexible field selections and +reshaping of input objects. + +# Motivation + +The directive `@is` specifies semantic equivalence between an argument and fields within the +resulting type. The argument named `field` accepts a string which adheres to a specific format +defined by the scalar type `FieldSelection`. + +For instance, consider the following field definition: +```graphql +type Query { + userById(userId: ID! @is(field: "id")): User! @lookup +} +``` + +In this example, the semantic equivalence is established between the argument `userId` and the field +`User.id`. This equivalence instructs the system on composition, validation, and execution to treat +`userId` as semantically identical to `User.id`. + +Consider the execution of a query as follows: +```graphql +query { + userById(userId: "123") { + id + } +} +``` +In this scenario, the only correct response that does not result in an error would be: +```json +{ + "data": { + "userById": { + "id": "123" + } + } +} +``` + +The scalar `FieldSelection` is similarly used in the `@requires` directive, despite its use in +different contexts. This document aims to explore various cases where `FieldSelection` is used +and to discuss potential solutions to challenges presented by its use. + +# Cases +This section outlines various scenarios that need to be addressed. It is intended to describe the +problem cases and is not focused on providing solutions. + +## Single field +This subsection addresses cases involving a single field reference. It details the simplest scenario +where a single field must be referenced, establishing semantic equivalence between an argument and a +corresponding field within a type. + +In the following example we specifcy the semantic equivalence between the argument `id` and the field `User.id`. +```graphql +extend type Query { + userById(id: ID!): User @lookup +} +``` + +### Single Field - Fieldname Matches +In the simplest scenario, the field name in the argument matches the field name in the returned +type. For example, the argument `id` is semantically equivalent to the field `User.id`. + +```graphql +extend type Query { + userById(id: ID!): User @lookup +} + +type User { + id: ID! +} +``` + +### Single Field - Fieldname is Different +In some instances, the field name in the returned type differs from the name of the argument. In +this scenario, the argument `userId` is semantically equivalent to the field `User.id`. + +```graphql +extend type Query { + userById(userId: ID!): User @lookup +} +``` + +### Single Field - Field is Deeper in the Tree +This case addresses situations where the field is nested deeper within a type's structure. Here, the +argument `addressId` is semantically equivalent to the field `User.address.id`. + + +```graphql +extend type Query { + userByAddressId(id: ID!): User @lookup +} +type User { + id: ID! + address: Address +} +type Address { + id: ID! +} +``` + +### Single Field - Field is Deeper in the Tree and the Fieldname is Different +This case is similar to the previous, but involves a different field name, where the argument +`addressId` is semantically equivalent to `User.address.id`. + +```graphql +extend type Query { + userByAddressId(addressId: ID!): User @lookup +} + +type User { + id: ID! + address: Address +} + +type Address { + addressId: ID! +} +``` + +### Single Field - Abstract Type - Field Matches Name +In cases involving abstract types, the argument may correspond to fields in different concrete types +but with matching field names. For instance, the argument `id` could be semantically equivalent to +either `Movie.id` or `Book.id`. + +```graphql +extend type Query { + mediaById(id: ID! ): MovieOrBook @lookup +} + +type Movie { + id: ID! +} + +type Book { + id: ID! +} + +union MovieOrBook = Movie | Book +``` + +### Single Field - Abstract Type - Field is Different +In this variation involving abstract types, the field names differ across implementations. Here, the +argument `id` corresponds semantically to `Movie.movieId` or `Book.bookId`. + +```graphql +extend type Query { + mediaById(id: ID! ): MovieOrBook @lookup +} + +type Movie { + movieId: ID! +} + +type Book { + bookId: ID! +} + +union MovieOrBook = Movie | Book +``` + +## Multiple Field Reference +Another scenario is when a field references multiple fields within the returned type. This is +particularly useful when the returning type has a composite key and the argument is an input +type. + +### Multiple Field Reference - Fieldname Matches +In this scenario, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically +equivalent to the fields `User.firstName` and `User.lastName`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + firstName: String! + lastName: String! +} +``` + +### Multiple Field Reference - Fieldname is Different +Here, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically equivalent to +the fields `User.givenName` and `User.familyName`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + givenName: String! + familyName: String! +} +``` + +### Multiple Field Reference - Output field is Deeper in the Tree +This scenario involves the input fields `UserInput.firstName` and `UserInput.lastName` being +semantically equivalent to the fields `Profile.firstName` and `User.lastName`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +type Profile { + firstName: String! +} +``` + +### Multiple Field Reference - Output field is Deeper in the Tree with Abstract Type +In this case, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either +`EntraProfile.firstName` or `AdfsProfile.firstName`, and `User.lastName`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +union Profile = EntraProfile | AdfsProfile + +type EntraProfile { + firstName: String! +} + +type AdfsProfile { + name: String! +} +``` + +### Multiple Field Reference - Output field is Deeper in the Tree with Abstract Type and Fieldname is Different +Here, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either +`EntraProfile.name` or `AdfsProfile.userName`, and `User.lastName`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +union Profile = EntraProfile | AdfsProfile + +type EntraProfile { + name: String! +} + +type AdfsProfile { + userName: String! +} +``` + +### Multiple Field Reference - Input field is Deeper in the Tree +This introduces additional complexity as the input object requires reshaping. The input fields +`UserInput.info.firstName` and `UserInput.name` correspond to the fields `User.firstName` and +`User.name`. + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + firstName: String! + name: String! +} +``` + +### Multiple Field Reference - Input field is Deeper in the Tree with Abstract Type +In this instance, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to +either `EntraUser.firstName` or `AdfsUser.lastName`, and `User.name`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +interface User { + name: String! +} + +type EntraUser implements User { + firstName: String! +} + +type AdfsUser implements User { + lastName: String! +} +``` + +### Multiple Field Reference - Input field is Deeper in the Tree and Output field is Deeper in the Tree +Here, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields +`User.profile.firstName` and `User.name`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + profile: Profile + name: String! +} + +type Profile { + firstName: String! +} +``` + +### Multiple Field Reference - Input field is Deeper in the Tree and Output field is Deeper in the Tree with Abstract Type +Finally, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields +`EntraProfile.firstName` and `AdfsProfile.name`, and `User.name`. + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + profile: Profile + name: String! +} + +union Profile = EntraProfile | AdfsProfile + +type EntraProfile { + firstName: String! +} + +type AdfsProfile { + name: String! +} +``` + +## Edge Cases - Input Field Shares Name with Argument and Output Field + +This subsection explores a the case where the input field `UserInput.id` is semantically equivalent +to the field `User.id`. Such a scenario introduces ambiguity because the same field name `id` is +used across different contexts, which current solutions struggle to express clearly. + +```graphql +type Query { + userById(id: InputObject @is(field: "id")): User @lookup +} + +input InputObject { + id: ID! +} + +type User { + id: ID! +} +``` + +# Different Possible Solutions + +There are numerous potential solutions to address the challenges presented by `FieldSelection` +however, each solution introduces its own set of complexities. + +## FieldSelection as a selection set + +The `FieldSelection` scalar extends the concept of a selection set, allowing for flexible field selections. + +In the simplest form, a single field reference might appear as follows: + +**Single Field Reference** +```graphql +extend type Query { + userById(id: ID! @is(field: "id")): User @lookup +} +``` + +A more complex scenario involves multiple fields: + +**Multiple Field Reference** +```graphql +extend type Query { + findUserByName(user: UserInput! @is(field: "firstName lastName")): User @lookup +} +``` + +Abstract fields across different types can also be specified easily: +**Abstract Field Reference** +```graphql +extend type Query { + mediaById(id: ID! @is(field: "... Movie { id } ... Book { id }")): MovieOrBook @lookup +} +``` + +For fields nested deeper in a type's hierarchy: +**Single Field Nested Reference** +```graphql +extend type Query { + userById(userId: ID! @is(field: "address { id }")): User @lookup +} +``` + +This notation implies that the nested field `id` under `address` is semantically equivalent to the argument `userId`. + +However, complexities come when multiple fields need to be referenced, particularly when they are nested: + +**Multiple Field Reference - Output field is deeper in the tree** +```graphql +type Query { + findUserByName(user: UserInput! @is(field: "profile { firstName } lastName")): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} +``` + +The challenge here is to clearly define that `User.profile.firstName` is equivalent to `UserInput.firstName`. + +To address this, a notation like input object fields could be utilized: +```graphql +type Query { + findUserByName(user: UserInput! @is(field: "firstName: { profile { firstName } } lastName")): User @lookup +} +``` +However, this solution mixes input fields and output fields, potentially leading to confusion in +interpreting the mappings. + +## `FieldSelection` as a path combined with an input object +Another solution is to treat the `FieldSelection` as a path and borrow a few concepts from the input object syntax to shape the input object. + +A single field reference would look exactly the same as in the previous example. + +**Single Field Reference** +```graphql +extend type Query { + userById(id: ID! @is(field: "id")): User @lookup +} +``` + +Multiple field references follow the same format: +**Multiple Field Reference** +```graphql +extend type Query { + findUserByName(user: UserInput! @is(field: "firstName lastName")): User @lookup +} +``` + +To reference a deeper nested field, a path syntax is used: +**Single Field Nested Reference** +```graphql +extend type Query { + userById(userId: ID! @is(field: "address.id")): User @lookup +} +``` + +This approach also allows for pointing to fields across different hierarchies: + +**Multiple Field Reference - Output field is deeper in the tree** +```graphql +type Query { + findUserByName(user: UserInput! @is(field: "profile.firstName lastName")): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} +``` + +To handle renaming, simply specify the new name followed by the path to the field: +```graphql +type Query { + findUserByName(user: UserInput! @is(field: "firstName: profile.name lastName")): User @lookup +} +``` + +The main challenge with this approach is abstract types. A generic path syntax might solve this for some cases: + +**Output field is deeper in the tree with abstract type and fieldname is different** +```graphql +extend type Query { + findUserByName(user: UserInput! @is(field: "firstName: profile.name lastName")): User @lookup +} +``` + +However, it fails when multiple abstract types are possible for the same field unless we introduce a +new syntax to handle this: + +```graphql +extend type Query { + mediaById(id: ID! @is(field: ".id | .id")): MovieOrBook @lookup +} +``` + + +## `FieldSelection` Inline Selection + +When evaluating the challenges of previous approaches: +- The first approach effectively handles abstract types using selection sets equal to query +operations, but struggles with reshaping the input object due to the limitations of the selection +set syntax. +- The second approach excels in reshaping the input object by specifying exact fields, thus avoiding +the ambiguities of selection sets, but it falls short in handling abstract types due to the +constraints of the path syntax. + +To combine the best of both worlds we use the input object like syntax of the path syntax and +combine it with the selection set syntax of the first approach. + +### Concept Overview +This outline is not a formal specification but aims to clarify the concept: +1. A `FieldSelection` consists of `Selection`s and `InputField`s: + - A `Selection` targets a single field within the selection set of the return type: + - The simplest form of a `Selection` is an `ObjectField` that references a field within the selection set of the returning type. + - A `Selection` inherently acts as an `InputField` with the same name as the field it selects: + - If multiple field names are possible, the `Selection` must be nested within an `InputField`. + - Selecting multiple fields within a single `Selection` is prohibited to prevent ambiguity. + - An `InputField` corresponds directly to a field within the input object: + - It is a key-value pair where the key is the field name and the value is either an `ObjectField` or a `Selection` -> `fieldName: value`. + - If the value of an `ObjectField` begins with a `{`, it is classified as an `ObjectValue`. + - Otherwise, it is treated as a `Selection`. + - An `ObjectValue` is an aggregate of `InputField`s. + +So simple field selection would look like this: +**Single Field Reference** +```graphql +extend type Query { + userById(id: ID! @is(field: "id")): User @lookup +} +``` + +and multiple field selection would look like this: +**Multiple Field Reference** +```graphql +extend type Query { + findUserByName(user: UserInput! @is(field: "firstName lastName")): User @lookup +} +``` + +if you need to rename a field you just use a `InputField` and set the value to an `ObjectField` + +**Multiple Field Reference - Fieldname is different** +```graphql +extend type Query { + # Selection: +--------+ +---------+ + # InputField: +---------+ +---------+ + findUserByName(user: UserInput! @is(field: "fristName: givenName lastName: familyName")): User @lookup +} +``` + +You can easily express abstract types as well + +**Abstract Field Reference** +```graphql +extend type Query { + mediaById(id: ID! @is(field: "... Movie { id } ... Book { id }")): MovieOrBook @lookup +} +``` + +but you can also shape the input object + +**Multiple Field Reference - Output field is deeper in the tree** +```graphql +type Query { + # + # Selection: +--------------------+ +--------+ + # InputField: +---------+ + findUserByName(user: UserInput! @is(field: "firstName: profile { firstName } lastName")): User @lookup +} +``` + +you can use abstract types as well + +**Multiple Field Reference - Output field is deeper in the tree with abstract type and fieldname is different** +```graphql + # + # Selection: +------------------------------------------------------------------------+ +--------+ + # InputField: +---------+ + findUserByName(user: UserInput! @is(field: "firstName: profile { ... on EntraProfile { name } ... on AdfsProfile { firstName } } lastName")): User @lookup +``` + +or even nest the input object + +**Multiple Field Reference - Input field is deeper in the tree and output field is deeper in the tree with abstract type** +```graphql + # + # Selection: +-----------------------------------------------------------------------+ +--------+ + # InputField: +---------+ + # ObjectValue: +---------+ +-+ + findUserByName(user: UserInput! @is(field: "profile: {firstName: profile { ... on EntraProfile { name } ... on AdfsProfile { firstName } } } lastName")): User @lookup +``` + +# Overview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name Query Solution1 Solution2 Solution3
+Single Field - Fieldname Matches + + +```graphql +extend type Query { + userById(id: ID!): User @lookup +} + +type User { + id: ID! +} +``` + + + +```graphql +@is(field: "id") +``` + + + +```graphql +@is(field: "id") +``` + + + +```graphql +@is(field: "id") +``` + +
+Single Field - Fieldname is different + + +```graphql +extend type Query { + userById(userId: ID!): User @lookup +} + +type User { + id: ID! +} +``` + + + +```graphql +@is(field: "id") +``` + + + +```graphql +@is(field: "id") +``` + + + +```graphql +@is(field: "id") +``` + +
+Single Field - Field is deeper in the tree + + +```graphql +extend type Query { + userByAddressId(id: ID!): User @lookup +} +type User { + id: ID! + address: Address +} +type Address { + id: ID! +} +``` + + + +```graphql +@is(field: "address { id }") +``` + + + +```graphql +@is(field: "address.id") +``` + + + +```graphql +@is(field: "address { id }") +``` + +
+Single Field - Field is deeper in the tree and the fieldname is different + + +```graphql +extend type Query { + userByAddressId(addressId: ID!): User @lookup +} + +type User { + id: ID! + address: Address +} + +type Address { + addressId: ID! +} +``` + + + +```graphql +@is(field: "address { id }") +``` + + + +```graphql +@is(field: "address.id") +``` + + + +```graphql +@is(field: "address { id }") +``` + +
+Single Field - Abstract Type - Field Matches Name + + +```graphql +extend type Query { + mediaById(id: ID! ): MovieOrBook @lookup +} + +type Movie { + id: ID! +} + +type Book { + id: ID! +} + +union MovieOrBook = Movie | Book +``` + + + +```graphql +@is(field: "... Movie { id } ... Book { id }") +``` + + + +```graphql +@is(field: ".id | .id") +``` + + + +```graphql +@is(field: "... Movie { id } ... Book { id }")j +``` +
+Single Field - Abstract Type - Field is different + + +```graphql +extend type Query { + mediaById(id: ID! ): MovieOrBook @lookup +} + +type Movie { + movieId: ID! +} + +type Book { + bookId: ID! +} + +union MovieOrBook = Movie | Book +``` + + + +```graphql +@is(field: "... Movie { movieId } ... Book { bookId }") +``` + + + +```graphql +@is(field: ".movieid | .bookId") +``` + + + +```graphql +@is(field: "... Movie { movieId } ... Book { bookId }") +``` +
+Multiple Field Reference - Fieldname Matches + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + firstName: String! + lastName: String! +} +``` + + + +```graphql +@is(field: "firstName lastName") +``` + + + +```graphql +@is(field: "firstName lastName") +``` + + + +```graphql +@is(field: "firstName lastName") +``` +
+Multiple Field Reference - Fieldname is different + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + givenName: String! + familyName: String! +} +``` + + + +```graphql +@is(field: "firstName: givenName lastName: familyName") +``` + + + +```graphql +@is(field: "firstName: givenName lastName: familyName") +``` + + + +```graphql +@is(field: "firstName: givenName lastName: familyName") +``` +
+Multiple Field Reference - Output field is deeper in the tree + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +type Profile { + firstName: String! +} +``` + + + +🚨 This is ambigious + +```graphql +@is(field: "profile { firstName } lastName") +``` + + + +```graphql +@is(field: "profile.firstName lastName") +``` + + + +```graphql +@is(field: "profile { firstName } lastName") +``` +
+Multiple Field Reference - Output field is deeper in the tree with abstract type + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +union Profile = EntraProfile | AdfsProfile + +type EntraProfile { + firstName: String! +} + +type AdfsProfile { + name: String! +} +``` + + + +🚨 This is ambigious + +```graphql +@is(field: "profile { ... on EntraProfile { firstName } ... on AdfsProfile { firstName } } lastName") +``` + + + +```graphql +@is(field: "profile.firstName | profile.firstName lastName") +``` + + + +```graphql +@is(field: "profile { ... on EntraProfile { firstName } ... on AdfsProfile { firstName} } lastName") +``` +
+Multiple Field Reference - Output field is deeper in the tree with abstract type and fieldname is different + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + firstName: String! + lastName: String! +} + +type User { + profile: Profile + lastName: String! +} + +union Profile = EntraProfile | AdfsProfile +``` + + + +🚨 This is ambigious + +```graphql +@is(field: "firstName: { profile { ... on EntraProfile { name } ... on AdfsProfile { userName } } lastName") +``` + + + +```graphql +@is(field: "firstName: profile.name | profile.userName lastName") +``` + + + +```graphql +@is(field: "firstName: profile { ... on EntraProfile { name } ... on AdfsProfile { userName } lastName") +``` +
+Multiple Field Reference - Input field is deeper in the tree + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + firstName: String! + name: String! +} +``` + + + +🚨 There is no reasonable way to express this + + + +```graphql +@is(field: "profile: { firstName: firstName} lastName") +``` + + + +```graphql +@is(field: "profile: { firstName: firstName} lastName") +``` +
+Multiple Field Reference - Input field is deeper in the tree with abstract type + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +interface User { + name: String! +} + +type EntraUser implements User { + firstName: String! +} + +type AdfsUser implements User { + lastName: String! +} +``` + + + +🚨 There is no reasonable way to express this + + + +```graphql +@is(field: "profile: { firstName: .firstname | .lastName} name") +``` + + + +```graphql +@is(field: "profile: { firstName: ... EntraUser { firstName } ... AdfsUser {lastName } } name") +``` +
+Multiple Field Reference - Input field is deeper in the tree and output field is deeper in the tree + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + profile: Profile + name: String! +} + +type Profile { + firstName: String! +} +``` + + + +🚨 There is no reasonable way to express this + + + +```graphql +@is(field: "info: { firstName: profie.firstName } name") +``` + + + +```graphql +@is(field: "info: { firstName: profile { firstName } } name") +``` +
+Multiple Field Reference - Input field is deeper in the tree and output field is deeper in the tree with abstract type + + + +```graphql +type Query { + findUserByName(user: UserInput!): User @lookup +} + +input UserInput { + info: Info + name: String! +} + +type Info { + firstName: String! +} + +type User { + profile: Profile + name: String! +} + +union Profile = EntraProfile | AdfsProfile + +type EntraProfile { + firstName: String! +} + +type AdfsProfile { + name: String! +} +``` + + + +🚨 There is no reasonable way to express this + + + +```graphql +@is(field: "info: { firstName: profile.firstName | profile.name } name") +``` + + + +```graphql +@is(field: "info: { firstName: profile { ... EntraProfile { firstName } ... AdfsProfile { name } } } name") +``` +
\ No newline at end of file From ff25ca6d3d83a409dd7f1c5fd82aa752bfacf445 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Wed, 17 Apr 2024 21:13:21 +0200 Subject: [PATCH 02/17] Add note about alias --- rfcs/field-selection.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rfcs/field-selection.md b/rfcs/field-selection.md index a6dfaf6..4e2d056 100644 --- a/rfcs/field-selection.md +++ b/rfcs/field-selection.md @@ -585,6 +585,9 @@ This outline is not a formal specification but aims to clarify the concept: - A `Selection` inherently acts as an `InputField` with the same name as the field it selects: - If multiple field names are possible, the `Selection` must be nested within an `InputField`. - Selecting multiple fields within a single `Selection` is prohibited to prevent ambiguity. + - Aliases are not supported within a `Selection`. + - Arguments are not supported within a `Selection`. + - Directives are not supported within a `Selection`. - An `InputField` corresponds directly to a field within the input object: - It is a key-value pair where the key is the field name and the value is either an `ObjectField` or a `Selection` -> `fieldName: value`. - If the value of an `ObjectField` begins with a `{`, it is classified as an `ObjectValue`. From 83d9b6a19fbd62e5bf007e84388fb9ac6be71530 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 2 May 2024 18:07:47 +0200 Subject: [PATCH 03/17] Add initial appendix for field seletion --- spec/Appendix A -- Field Selection.md | 156 ++++++++++++++++++++++++++ spec/Spec.md | 4 +- 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 spec/Appendix A -- Field Selection.md diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md new file mode 100644 index 0000000..403a504 --- /dev/null +++ b/spec/Appendix A -- Field Selection.md @@ -0,0 +1,156 @@ +# Appendix A: Specification of FieldSelection Scalar + +## Introduction + +This appendix focuses on the specification of the {FieldSelection} scalar type. {FieldSelection} 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 `@requires` directives. + + +To illustrate, consider a simple example from a GraphQL schema: + +```graphql +type Query { + userById(userId: ID! @is(field: "id")): User! @lookup +} +``` + +In this schema, the `userById` query uses the `@is` directive with {FieldSelection} 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 + } +} +``` + +Here, it is exptected that the `userId` "123" corresponds directly to `User.id`, resulting in the +following response if correctly implemented: + +```json +{ + "data": { + "userById": { + "id": "123" + } + } +} +``` + +The {FieldSelection} scalar type is used to establish semantic equivalence between an argument and +the fields within the associated return type. To accomplish this, the scalar must define the +relationship between input fields or arguments and the fields in the resulting object. +Consequently, a {FieldSelection} only makes sense in the context of a specific argument and its +return type. + +The {FieldSelection} 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}. + +```graphql example +extend type Query { + findUserByName(user: UserInput! @is(field: "{ firstName: firstName }")): User @lookup +} +``` + +```graphql counter-example +extend type Query { + findUserByName(user: UserInput! @is(field: "firstName")): User @lookup +} +``` + + +## 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`. + +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 {FieldSelection}, 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, and variables +are excluded as they do not exist. +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. + +### Path +Path :: + - Name + - Path . Path + - Path | Path + - Name < Name > . Path + +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 +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}. + +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`. + +``` example +mediaById.isbn +``` + +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 (`|`), such as in + +In the following example, the value could be `title` when referring to a `Book` and `movieTitle` +when referring to a `Movie`. + +``` example +mediaById.title | mediaById.movieTitle +``` + +### SelectedValue +SelectedValue :: + - Path + - SelectedObjectValue + +A {SelectedValue} is defined as either a {Path} or a {SelectedObjectValue} + +### SelectedObjectValue +SelectedObjectValue :: + - { SelectedObjectField+ } + +SelectedObjectField :: + - Name: SelectedValue + +{SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. +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. + +### Name +Is equivalent to the `Name` defined in the GraphQL specification. + + diff --git a/spec/Spec.md b/spec/Spec.md index 112d195..d138d78 100644 --- a/spec/Spec.md +++ b/spec/Spec.md @@ -85,10 +85,10 @@ Note: This is an example of a non-normative note. # [Subgraph](Section%202%20--%20Source%20Schema.md) -# [Supergraph](Section%203%20--%20Supergraph.md) - # [Composition](Section%204%20--%20Composition.md) # [Execution](Section%205%20--%20Execution.md) # [Shared Types](Section%206%20--%20Shared%20Types.md) + +# [Appendix A -- Field Selection](Appendix%20A%20--%20Field%20Selection.md) From 3519d0ea1b6b28f1441e9c389a98733efae12a7b Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 6 Jun 2024 09:52:04 +0200 Subject: [PATCH 04/17] Add validation rules --- spec/Appendix A -- Field Selection.md | 420 +++++++++++++++++++++++++- 1 file changed, 412 insertions(+), 8 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 403a504..483905d 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -8,7 +8,6 @@ type. Specifically, it allows defining complex relationships between input argum the output object by encapsulating these relationships within a parsable string format. It is used in the `@is` and `@requires` directives. - To illustrate, consider a simple example from a GraphQL schema: ```graphql @@ -93,7 +92,6 @@ Beyond these literals, an additional literal called {Path} is necessary. Path :: - Name - Path . Path - - Path | Path - Name < Name > . Path The {Path} literal is a string used to select a single output value from the _return type_ by @@ -120,9 +118,18 @@ In the following example, the path `mediaById..isbn` specifies that `media mediaById.isbn ``` +### 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 (`|`), such as in +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`. @@ -131,12 +138,45 @@ when referring to a `Movie`. mediaById.title | mediaById.movieTitle ``` -### SelectedValue -SelectedValue :: - - Path - - SelectedObjectValue +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. -A {SelectedValue} is defined as either a {Path} or a {SelectedObjectValue} +```example +{ movieId: .id } | { productId: .id } +``` + +```example +{ nested: { movieId: .id } | { productId: .id }} +``` + +To select nested structured data, a {SelectedObjectValue} can be used as a segment in the path. The value +must select data in the same shape as the input. This is primarily used when mapping an object +inside a list to an input. The fields within a {SelectedObjectValue} are scoped to the parent field +of the path. + +```graphql example +type Query { + findLocation(location: LocationInput! @is(field: "{ coordinates: coordinates.{ lat: x, lon: y}}")): Location @lookup +} + +type Coordinate { + x: Int! + y: Int! +} + +type Location { + coordinates: [Coordinate!]! +} + +input PositionInput { + lat: Int! + lon: Int! +} + +input LocationInput { + coordinates: [PositionInput!]! +} +``` ### SelectedObjectValue SelectedObjectValue :: @@ -153,4 +193,368 @@ traditional `ObjectValue` capabilities to support direct path selections. ### Name Is equivalent to the `Name` defined in the GraphQL specification. +## Validation +Validation ensures that {FieldSelection} scalars are semantically correct within the given context. + +Validation of {FieldSelection} scalars occurs during the composition phase, ensuring that all +{FieldSelection} entries are syntactically correct and semantically meaningful relative to the +context. + +Composition is only possible if the {FieldSelection} is validated successfully. An invalid +{FieldSelection} results in undefined behavior, making composition impossible. + +### Examples +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]! +} + +type Store { + id: ID! + city: String! + media: [Media!]! +} + +interface Media { + id: ID! +} + +type Book implements Media { + id: ID! + title: String! + isbn: String! + author: Author! +} + +type Movie implements Media { + id: ID! + movieTitle: String! + releaseDate: String! +} + +type Author { + id: ID! + books: [Book!]! +} + +input FindMediaInput @oneOf{ + bookId: ID + movieId: ID +} + +type SearchStoreInput { + city: String + hasInStock: FindMediaInput +} +``` + +### Path Field Selections + +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. + +**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. + +For example, the following {Path} is valid in the context of `Book`: + +```graphql example +title +``` + +```graphql example +.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}. + +```graphql counter-example +movieId +``` + +```graphql counter-example +.movieId +``` + +### Path Terminal Field Selections + +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}. + +**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. + +For example, the following {Path} is valid if `title` is a scalar field on the `Book` type: + +```graphql example +book.title +``` + +The following {Path} is invalid because `title` should not have subselections: + +```graphql counter-example +book.title.something +``` + +For non-leaf fields, the {Path} must continue to specify subselections until a leaf field is reached: + +```graphql example +book.author.id +``` + +Invalid {Path} where non-leaf fields do not have further selections: + +```graphql counter-example +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. + +**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. + +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}. +**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. + + +```graphql +type Query { + """ + can only handle "id" and "city street" + """ + userByIdOrAddress( + userId: ID @is(field: "id") , + city: String @is(field: "city"), + address: String @is(field: "street") + ): User + + userById(userId: ID! @is(field: "id")): User + userByName(name: String! @is(field: "name")): User + userByCity(city: String! @is(field: "city")): User +} + +type User + @key(fields: "id") + @key(fields: "city") + @key(fields: "street") + @key(fields: "city street") { + id: ID! + name: String! + city: String! + street: String! +} +``` + +### 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}. + +**Explanatory Text** + +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 {FieldSelection} scalar: + +```graphql example +type Query { + userById(userId: ID! @is(field: "id")): User! @lookup +} + +type User { + id: ID +} +``` + +Non-coercible values are invalid. The following examples are invalid: + +```graphql counter-example +extend type Query { + userById(userId: ID! @is(field: "id")): User! @lookup +} + +type User { + id: Int +} +``` + +### Selected Object Field Names + +**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. + +**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. + +For example, the following is valid: + +```graphql example +type Query { + userById(userId: ID! @is(field: "id")): User! @lookup +} + +type User { + id: ID +} +``` + +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 { + userById(userId: ID! @is(field: "address")): User! @lookup +} + +type User { + id: Int +} +``` + +### Selected Object Field Uniqueness + +**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}. + +**Explanatory Text** + +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: + +```graphql counter-example +extend type Query { + userById(userId: ID! @is(field: "id id")): User! @lookup +} + +type User { + id: Int +} +``` + +### Required Selected Object Fields + +**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. + +**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. + +For instance, if the `UserInput` type requires the `id` field: + +```graphql example +input UserInput { + id: ID! + name: String! +} +``` + +Then, an invalid selection would be missing the required `id` field: + +```graphql counter-example +extend type Query { + userById(user: UserInput! @is(field: "{ name: name }")): User! @lookup +} +``` + +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 { + findUser(input: UserInput! @is(field: "{ name: name }")): User! @lookup +} + +type User { + id: ID + name: String +} + +input UserInput { + 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. + +```graphql counter-example +extend type Query { + findUser(input: UserInput! @is(field: "{ id: id }")): User! @lookup +} + +type User { + id: ID +} + +input UserInput { + id: ID + name: String! +} +``` \ No newline at end of file From 7c6a6ede2deca502f273fdabccd12ab5de2ccb62 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 6 Jun 2024 21:15:14 +0200 Subject: [PATCH 05/17] Cleanup --- spec/Appendix A -- Field Selection.md | 64 +++++++++------------------ 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 483905d..5caa6e0 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -211,6 +211,7 @@ type Query { mediaById(mediaId: ID!): Media findMedia(input: FindMediaInput): Media searchStore(search: SearchStoreInput): [Store]! + storeById(id: ID!): Store } type Store { @@ -241,7 +242,7 @@ type Author { books: [Book!]! } -input FindMediaInput @oneOf{ +input FindMediaInput @oneOf { bookId: ID movieId: ID } @@ -340,7 +341,7 @@ Each segment of a {Path} that references a type, must be a type that is valid in context. **Formal Specification** -**** + - For each {segment} in a {Path}: - If {segment} is a type reference: - Let {type} be the type referenced in the {segment}. @@ -361,34 +362,6 @@ Type references inside a {Path} must be valid within the context of the surround reference is only valid if the referenced type could logically apply within the parent type. -```graphql -type Query { - """ - can only handle "id" and "city street" - """ - userByIdOrAddress( - userId: ID @is(field: "id") , - city: String @is(field: "city"), - address: String @is(field: "street") - ): User - - userById(userId: ID! @is(field: "id")): User - userByName(name: String! @is(field: "name")): User - userByCity(city: String! @is(field: "city")): User -} - -type User - @key(fields: "id") - @key(fields: "city") - @key(fields: "street") - @key(fields: "city street") { - id: ID! - name: String! - city: String! - street: String! -} -``` - ### Values of Correct Type **Formal Specification** @@ -405,23 +378,25 @@ The following examples are valid use of value literals in the context of {FieldS ```graphql example type Query { - userById(userId: ID! @is(field: "id")): User! @lookup + storeById(id: ID! @is(field: "id")): Store! @lookup } -type User { +type Store { id: ID + city: String! } ``` Non-coercible values are invalid. The following examples are invalid: ```graphql counter-example -extend type Query { - userById(userId: ID! @is(field: "id")): User! @lookup +type Query { + storeById(id: ID! @is(field: "id")): Store! @lookup } -type User { +type Store { id: Int + city: String! } ``` @@ -442,11 +417,12 @@ For example, the following is valid: ```graphql example type Query { - userById(userId: ID! @is(field: "id")): User! @lookup + storeById(id: ID! @is(field: "id")): Store! @lookup } -type User { +type Store { id: ID + city: String! } ``` @@ -454,11 +430,12 @@ In contrast, the following is invalid because it uses a field "address" which is ```graphql counter-example extend type Query { - userById(userId: ID! @is(field: "address")): User! @lookup + storeById(id: ID! @is(field: "address")): Store! @lookup } -type User { - id: Int +type Store { + id: ID + city: String! } ``` @@ -480,11 +457,12 @@ For example, the following is invalid: ```graphql counter-example extend type Query { - userById(userId: ID! @is(field: "id id")): User! @lookup + storeById(id: ID! @is(field: "id id")): Store! @lookup } -type User { - id: Int +type Store { + id: ID + city: String! } ``` From d0bf799486a887b31fedcfe9dd2aadb46f3168cb Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 20 Jun 2024 19:20:12 +0200 Subject: [PATCH 06/17] Fixed Selection --- spec/Appendix A -- Field Selection.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 5caa6e0..2458ebb 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -88,11 +88,22 @@ specification, a new literal called {SelectedValue} is introduced, along with {S 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) + ### Path Path :: + - FieldName + - Path . FieldName + - FieldName < TypeName > . Path + +FieldName :: + - Name + +TypeName :: - Name - - Path . Path - - Name < Name > . Path The {Path} literal is a string used to select a single output value from the _return type_ by specifying a path to that value. From 493e8d714da531df7b1b912f6789ab44643dff00 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 20 Jun 2024 19:20:28 +0200 Subject: [PATCH 07/17] Fixed Selection --- spec/Appendix A -- Field Selection.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 2458ebb..ce3bdaf 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -1,8 +1,8 @@ -# Appendix A: Specification of FieldSelection Scalar +# Appendix A: Specification of FieldSelectionMap Scalar ## Introduction -This appendix focuses on the specification of the {FieldSelection} scalar type. {FieldSelection} is +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 @@ -16,7 +16,7 @@ type Query { } ``` -In this schema, the `userById` query uses the `@is` directive with {FieldSelection} to declare that +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: @@ -42,13 +42,13 @@ following response if correctly implemented: } ``` -The {FieldSelection} scalar type is used to establish semantic equivalence between an argument and +The {FieldSelectionMap} scalar type is used to establish semantic equivalence between an argument and the fields within the associated return type. To accomplish this, the scalar must define the relationship between input fields or arguments and the fields in the resulting object. -Consequently, a {FieldSelection} only makes sense in the context of a specific argument and its +Consequently, a {FieldSelectionMap} only makes sense in the context of a specific argument and its return type. -The {FieldSelection} 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 @@ -76,7 +76,7 @@ The `Value` of an argument can take various forms: it might be a scalar value (s `Float`, `String`, `Boolean`, `Null`, or `Enum`), a list (`ListValue`), an input object (`ObjectValue`), or a `Variable`. -Within the scope of the {FieldSelection}, the relationship between input and output is +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. @@ -205,14 +205,14 @@ traditional `ObjectValue` capabilities to support direct path selections. Is equivalent to the `Name` defined in the GraphQL specification. ## Validation -Validation ensures that {FieldSelection} scalars are semantically correct within the given context. +Validation ensures that {FieldSelectionMap} scalars are semantically correct within the given context. -Validation of {FieldSelection} scalars occurs during the composition phase, ensuring that all -{FieldSelection} entries are syntactically correct and semantically meaningful relative to the +Validation of {FieldSelectionMap} scalars occurs during the composition phase, ensuring that all +{FieldSelectionMap} entries are syntactically correct and semantically meaningful relative to the context. -Composition is only possible if the {FieldSelection} is validated successfully. An invalid -{FieldSelection} results in undefined behavior, making composition impossible. +Composition is only possible if the {FieldSelectionMap} is validated successfully. An invalid +{FieldSelectionMap} results in undefined behavior, making composition impossible. ### Examples In this section, we will assume the following type system in order to demonstrate examples: @@ -385,7 +385,7 @@ reference is only valid if the referenced type could logically apply within the 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 {FieldSelection} scalar: +The following examples are valid use of value literals in the context of {FieldSelectionMap} scalar: ```graphql example type Query { From 6a543c7dd966a9b688d0825530b87944494ada61 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Thu, 11 Jul 2024 17:57:47 +0200 Subject: [PATCH 08/17] Adds suggestions --- rfcs/field-selection.md | 111 ++++----- spec/Appendix A -- Field Selection.md | 335 +++++++++++++++++++------- 2 files changed, 302 insertions(+), 144 deletions(-) diff --git a/rfcs/field-selection.md b/rfcs/field-selection.md index 4e2d056..e352d5e 100644 --- a/rfcs/field-selection.md +++ b/rfcs/field-selection.md @@ -1,11 +1,9 @@ -This RFC proposes a syntax for `FieldSelection` that allows for flexible field selections and -reshaping of input objects. +This RFC proposes a syntax for `FieldSelection` that allows for flexible field selections and reshaping of input objects. # Motivation -The directive `@is` specifies semantic equivalence between an argument and fields within the -resulting type. The argument named `field` accepts a string which adheres to a specific format -defined by the scalar type `FieldSelection`. +The directive `@is` specifies semantic equivalence between an argument and fields within the resulting type. +The argument named `field` accepts a string which adheres to a specific format defined by the scalar type `FieldSelection`. For instance, consider the following field definition: ```graphql @@ -14,9 +12,8 @@ type Query { } ``` -In this example, the semantic equivalence is established between the argument `userId` and the field -`User.id`. This equivalence instructs the system on composition, validation, and execution to treat -`userId` as semantically identical to `User.id`. +In this example, the semantic equivalence is established between the argument `userId` and the field `User.id`. +This equivalence instructs the system on composition, validation, and execution to treat `userId` as semantically identical to `User.id`. Consider the execution of a query as follows: ```graphql @@ -37,18 +34,16 @@ In this scenario, the only correct response that does not result in an error wou } ``` -The scalar `FieldSelection` is similarly used in the `@requires` directive, despite its use in -different contexts. This document aims to explore various cases where `FieldSelection` is used -and to discuss potential solutions to challenges presented by its use. +The scalar `FieldSelection` is similarly used in the `@requires` directive, despite its use in different contexts. +This document aims to explore various cases where `FieldSelection` is used and to discuss potential solutions to challenges presented by its use. # Cases -This section outlines various scenarios that need to be addressed. It is intended to describe the -problem cases and is not focused on providing solutions. +This section outlines various scenarios that need to be addressed. +It is intended to describe the problem cases and is not focused on providing solutions. ## Single field -This subsection addresses cases involving a single field reference. It details the simplest scenario -where a single field must be referenced, establishing semantic equivalence between an argument and a -corresponding field within a type. +This subsection addresses cases involving a single field reference. +It details the simplest scenario where a single field must be referenced, establishing semantic equivalence between an argument and a corresponding field within a type. In the following example we specifcy the semantic equivalence between the argument `id` and the field `User.id`. ```graphql @@ -58,8 +53,8 @@ extend type Query { ``` ### Single Field - Fieldname Matches -In the simplest scenario, the field name in the argument matches the field name in the returned -type. For example, the argument `id` is semantically equivalent to the field `User.id`. +In the simplest scenario, the field name in the argument matches the field name in the returned type. +For example, the argument `id` is semantically equivalent to the field `User.id`. ```graphql extend type Query { @@ -72,19 +67,22 @@ type User { ``` ### Single Field - Fieldname is Different -In some instances, the field name in the returned type differs from the name of the argument. In -this scenario, the argument `userId` is semantically equivalent to the field `User.id`. +In some instances, the field name in the returned type differs from the name of the argument. +In this scenario, the argument `userId` is semantically equivalent to the field `User.id`. ```graphql extend type Query { userById(userId: ID!): User @lookup } + +type User { + id: ID! +} ``` ### Single Field - Field is Deeper in the Tree -This case addresses situations where the field is nested deeper within a type's structure. Here, the -argument `addressId` is semantically equivalent to the field `User.address.id`. - +This case addresses situations where the field is nested deeper within a type's structure. +Here, the argument `addressId` is semantically equivalent to the field `User.address.id`. ```graphql extend type Query { @@ -100,8 +98,7 @@ type Address { ``` ### Single Field - Field is Deeper in the Tree and the Fieldname is Different -This case is similar to the previous, but involves a different field name, where the argument -`addressId` is semantically equivalent to `User.address.id`. +This case is similar to the previous, but involves a different field name, where the argument `addressId` is semantically equivalent to `User.address.id`. ```graphql extend type Query { @@ -114,18 +111,17 @@ type User { } type Address { - addressId: ID! + id: ID! } ``` ### Single Field - Abstract Type - Field Matches Name -In cases involving abstract types, the argument may correspond to fields in different concrete types -but with matching field names. For instance, the argument `id` could be semantically equivalent to -either `Movie.id` or `Book.id`. +In cases involving abstract types, the argument may correspond to fields in different concrete types but with matching field names. +For instance, the argument `id` could be semantically equivalent to either `Movie.id` or `Book.id`. ```graphql extend type Query { - mediaById(id: ID! ): MovieOrBook @lookup + mediaById(id: ID!): MovieOrBook @lookup } type Movie { @@ -140,12 +136,12 @@ union MovieOrBook = Movie | Book ``` ### Single Field - Abstract Type - Field is Different -In this variation involving abstract types, the field names differ across implementations. Here, the -argument `id` corresponds semantically to `Movie.movieId` or `Book.bookId`. +In this variation involving abstract types, the field names differ across implementations. +Here, the argument `id` corresponds semantically to `Movie.movieId` or `Book.bookId`. ```graphql extend type Query { - mediaById(id: ID! ): MovieOrBook @lookup + mediaById(id: ID!): MovieOrBook @lookup } type Movie { @@ -160,13 +156,11 @@ union MovieOrBook = Movie | Book ``` ## Multiple Field Reference -Another scenario is when a field references multiple fields within the returned type. This is -particularly useful when the returning type has a composite key and the argument is an input -type. +Another scenario is when a field references multiple fields within the returned type. +This is particularly useful when the returning type has a composite key and the argument is an input type. ### Multiple Field Reference - Fieldname Matches -In this scenario, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically -equivalent to the fields `User.firstName` and `User.lastName`. +In this scenario, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically equivalent to the fields `User.firstName` and `User.lastName`. ```graphql type Query { @@ -185,8 +179,7 @@ type User { ``` ### Multiple Field Reference - Fieldname is Different -Here, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically equivalent to -the fields `User.givenName` and `User.familyName`. +Here, the input fields `UserInput.firstName` and `UserInput.lastName` are semantically equivalent to the fields `User.givenName` and `User.familyName`. ```graphql type Query { @@ -205,8 +198,7 @@ type User { ``` ### Multiple Field Reference - Output field is Deeper in the Tree -This scenario involves the input fields `UserInput.firstName` and `UserInput.lastName` being -semantically equivalent to the fields `Profile.firstName` and `User.lastName`. +This scenario involves the input fields `UserInput.firstName` and `UserInput.lastName` being semantically equivalent to the fields `Profile.firstName` and `User.lastName`. ```graphql type Query { @@ -229,8 +221,7 @@ type Profile { ``` ### Multiple Field Reference - Output field is Deeper in the Tree with Abstract Type -In this case, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either -`EntraProfile.firstName` or `AdfsProfile.firstName`, and `User.lastName`. +In this case, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either `EntraProfile.firstName` or `AdfsProfile.firstName`, and `User.lastName`. ```graphql type Query { @@ -258,8 +249,7 @@ type AdfsProfile { ``` ### Multiple Field Reference - Output field is Deeper in the Tree with Abstract Type and Fieldname is Different -Here, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either -`EntraProfile.name` or `AdfsProfile.userName`, and `User.lastName`. +Here, the input fields `UserInput.firstName` and `UserInput.lastName` correspond to either `EntraProfile.name` or `AdfsProfile.userName`, and `User.lastName`. ```graphql type Query { @@ -288,10 +278,8 @@ type AdfsProfile { ``` ### Multiple Field Reference - Input field is Deeper in the Tree -This introduces additional complexity as the input object requires reshaping. The input fields -`UserInput.info.firstName` and `UserInput.name` correspond to the fields `User.firstName` and -`User.name`. - +This introduces additional complexity as the input object requires reshaping. +The input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields `User.firstName` and `User.name`. ```graphql type Query { @@ -305,7 +293,7 @@ input UserInput { type Info { firstName: String! -} +e type User { firstName: String! @@ -314,8 +302,7 @@ type User { ``` ### Multiple Field Reference - Input field is Deeper in the Tree with Abstract Type -In this instance, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to -either `EntraUser.firstName` or `AdfsUser.lastName`, and `User.name`. +In this instance, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to either `EntraUser.firstName` or `AdfsUser.lastName`, and `User.name`. ```graphql type Query { @@ -345,8 +332,7 @@ type AdfsUser implements User { ``` ### Multiple Field Reference - Input field is Deeper in the Tree and Output field is Deeper in the Tree -Here, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields -`User.profile.firstName` and `User.name`. +Here, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields `User.profile.firstName` and `User.name`. ```graphql type Query { @@ -373,8 +359,7 @@ type Profile { ``` ### Multiple Field Reference - Input field is Deeper in the Tree and Output field is Deeper in the Tree with Abstract Type -Finally, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields -`EntraProfile.firstName` and `AdfsProfile.name`, and `User.name`. +Finally, the input fields `UserInput.info.firstName` and `UserInput.name` correspond to the fields `EntraProfile.firstName` and `AdfsProfile.name`, and `User.name`. ```graphql type Query { @@ -408,9 +393,8 @@ type AdfsProfile { ## Edge Cases - Input Field Shares Name with Argument and Output Field -This subsection explores a the case where the input field `UserInput.id` is semantically equivalent -to the field `User.id`. Such a scenario introduces ambiguity because the same field name `id` is -used across different contexts, which current solutions struggle to express clearly. +This subsection explores a the case where the input field `UserInput.id` is semantically equivalent to the field `User.id`. +Such a scenario introduces ambiguity because the same field name `id` is used across different contexts, which current solutions struggle to express clearly. ```graphql type Query { @@ -428,8 +412,7 @@ type User { # Different Possible Solutions -There are numerous potential solutions to address the challenges presented by `FieldSelection` -however, each solution introduces its own set of complexities. +There are numerous potential solutions to address the challenges presented by `FieldSelection` however, each solution introduces its own set of complexities. ## FieldSelection as a selection set @@ -454,6 +437,7 @@ extend type Query { ``` Abstract fields across different types can also be specified easily: + **Abstract Field Reference** ```graphql extend type Query { @@ -462,6 +446,7 @@ extend type Query { ``` For fields nested deeper in a type's hierarchy: + **Single Field Nested Reference** ```graphql extend type Query { @@ -617,7 +602,7 @@ if you need to rename a field you just use a `InputField` and set the value to a extend type Query { # Selection: +--------+ +---------+ # InputField: +---------+ +---------+ - findUserByName(user: UserInput! @is(field: "fristName: givenName lastName: familyName")): User @lookup + findUserByName(user: UserInput! @is(field: "firstName: givenName lastName: familyName")): User @lookup } ``` diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index ce3bdaf..8b5bfc3 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -2,11 +2,10 @@ ## 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 `@requires` 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: @@ -16,8 +15,7 @@ type Query { } ``` -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: @@ -29,8 +27,7 @@ query { } ``` -Here, it is exptected that the `userId` "123" corresponds directly to `User.id`, resulting in the -following response if correctly implemented: +Here, it is exptected that the `userId` "123" corresponds directly to `User.id`, resulting in the following response if correctly implemented: ```json { @@ -42,17 +39,9 @@ following response if correctly implemented: } ``` -The {FieldSelectionMap} scalar type is used to establish semantic equivalence between an argument and -the fields within the associated return type. To accomplish this, the scalar must define the -relationship between input fields or arguments and the fields in the resulting object. -Consequently, a {FieldSelectionMap} only makes sense in the context of a specific argument and its -return type. - 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 { @@ -66,38 +55,218 @@ extend type Query { } ``` +### 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. + +Consider this example: + +```graphql +type Product { + id: ID! + delivery( + zip: String! + size: Int! @require(field: "dimension.size") + weight: Int! @require(field: "dimension.weight") + ): DeliveryEstimates +} +``` + +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. + +### Examples + +Scalar fields can be mapped directly to arguments. + +This example maps the `Product.weight` field to the `weight` argument: + +```graphql example +type Product { + shippingCost(weight: Float @require(field: "weight")): Currency +} +``` + +This example maps the `Product.shippingWeight` field to the `weight` argument: + +```graphql example +type Product { + 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: + +```graphql example +type Product { + 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: + +```graphql example +type Product { + shippingCost(dimension: DimensionInput @require(field: "{ width: width height: height }")): Currency +} +``` + +The shorthand equivalent is: + +```graphql example +type Product { + 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. + +```graphql example +type Product { + 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. + +This example is NOT allowed because it lacks explicit mapping: + +```graphql counter-example +type Product { + shippingCost(dimension: DimensionInput @require(field: "dimension")): Currency +} +``` + +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 +} +``` + +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 +} +``` + +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: + +```graphql example +type Product { + shippingCost(dimension: DimensionInput @require(field: "dimension.{ width height }")): Currency +} +``` + +This example is equivalent to the previous one: + +```graphql example +type Product { + 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. + +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 +} +``` + +Instead, use the path syntax to traverse into the list elements: + +```graphql example +type Product { + shippingCost(dimensions: [DimensionInput] @require(field: "dimensions.{ width height }")): Currency +} +``` + +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: + +```graphql example +type Product { + 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: + +```graphql example +type Product { + 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. + +This example nests `Product.width` and `Product.height` under `dimension`: + +```graphql example +type Product { + shippingCost(package: PackageInput @require(field: "{ weight, dimension: { width height } }")): Currency +} +``` + +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 +} +``` ## 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, and variables -are excluded as they do not exist. -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}, +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. + +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 - - Path . FieldName - - FieldName < TypeName > . Path + - FieldName . PathSemgent + - FieldName < TypeName > . PathSemgent FieldName :: - Name @@ -105,25 +274,19 @@ FieldName :: 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 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. +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 mediaById.isbn @@ -138,19 +301,17 @@ 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 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 } @@ -160,14 +321,14 @@ when mapping an abstract output type to a `@oneOf` input type. { nested: { movieId: .id } | { productId: .id }} ``` -To select nested structured data, a {SelectedObjectValue} can be used as a segment in the path. The value -must select data in the same shape as the input. This is primarily used when mapping an object -inside a list to an input. The fields within a {SelectedObjectValue} are scoped to the parent field -of the path. +A {SelectedObjectValue} following a {Path} is scoped to the type of the field selected by the {Path}. +This means that the root of all {Path} inside the selection is no longer scoped to the root (defined by @is or @require) but to the field selected by the {Path}. + +This allows reshaping lists of objects into input objects: ```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 { @@ -189,6 +350,26 @@ input LocationInput { } ``` +The same principle applies to object values, which can be used to avoid repeating the same path multiple times. + +The following example is valid: + +```graphql example +type Product { + dimensions: Dimension! + shippingCost(dimensions: DimensionInput! @require(field: "{ size: dimensions.size weight: dimensions.weight }")): Int! @lookup +} +``` + +The following example is equivalent to the previous one: + +```graphql example +type Product { + dimensions: Dimension! + shippingCost(dimensions: DimensionInput! @require(field: "dimensions.{ size weight }")): Int! @lookup +} +``` + ### SelectedObjectValue SelectedObjectValue :: - { SelectedObjectField+ } @@ -196,10 +377,8 @@ SelectedObjectValue :: SelectedObjectField :: - Name: SelectedValue -{SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. -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. +{SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. +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. ### Name Is equivalent to the `Name` defined in the GraphQL specification. @@ -207,14 +386,10 @@ Is equivalent to the `Name` defined in the GraphQL specification. ## 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 of {FieldSelectionMap} scalars occurs during the composition phase, ensuring that all {FieldSelectionMap} entries are syntactically correct and semantically meaningful relative to the context. -Composition is only possible if the {FieldSelectionMap} is validated successfully. An invalid -{FieldSelectionMap} results in undefined behavior, making composition impossible. +Composition is only possible if the {FieldSelectionMap} is validated successfully. An invalid {FieldSelectionMap} results in undefined behavior, making composition impossible. -### Examples In this section, we will assume the following type system in order to demonstrate examples: ```graphql @@ -290,9 +465,8 @@ 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 @@ -304,8 +478,7 @@ 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** @@ -318,9 +491,8 @@ leaf node. **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: @@ -348,8 +520,7 @@ 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** @@ -369,8 +540,8 @@ GetPossibleTypes(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 @@ -494,7 +665,9 @@ type Store { **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: From 534bbcd12a4d9c05f6f8d137e711155d07312b54 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Wed, 24 Jul 2024 10:22:28 +0200 Subject: [PATCH 09/17] Add Selected List Value Node --- spec/Appendix A -- Field Selection.md | 143 ++++++++++++++++++++------ 1 file changed, 109 insertions(+), 34 deletions(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 8b5bfc3..8588ea0 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -79,7 +79,7 @@ In this case, `"dimension.size"` and `"dimension.weight"` refer to fields of the 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 +**Examples** Scalar fields can be mapped directly to arguments. @@ -190,11 +190,19 @@ type Product { } ``` -Instead, use the path syntax to traverse into the list elements: +Instead, use the path syntax and angle 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 + +```graphql example +type Product { + shippingCost(partIds: @require(field: "parts[id]")): Currency } ``` @@ -321,67 +329,134 @@ This operator is applied when mapping an abstract output type to a `@oneOf` inpu { nested: { movieId: .id } | { productId: .id }} ``` +### SelectedObjectValue +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. + +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 {Path} inside the selection is no longer scoped to the root (defined by @is or @require) but to 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 effect the structure of the input type. + +This allows to reduce repetition in the selection. -This allows reshaping lists of objects into input objects: +The following example is valid: ```graphql example -type Query { - findLocation(location: LocationInput! @is(field: "{ coordinates: coordinates.{ lat: x lon: y}}")): Location @lookup +type Product { + dimension: Dimension! + shippingCost(dimension: DimensionInput! @require(field: "dimension.{ size weight }")): Int! @lookup } +``` -type Coordinate { - x: Int! - y: Int! +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 } +``` -type Location { - coordinates: [Coordinate!]! + +### SelectedListValue +SelectedListValue :: + - [ SelectedValue ] + +A {SelectedListValue} is an ordered list of {SelectedValue} wrapped in square brackets `[]`. +It is used to express semantic equivalence between a an argument expecting a list of values and the values of a list fields within the output object. + +The {SelectedListValue} differs from the `ListValue` defined in the GraphQL specification by only allowing one {SelectedValue} as and element. + +The following example is valid: + +```graphql example +type Product { + parts: [Part!]! + partIds(partIds: [ID!]! @require(field: "parts[id]")): [ID!]! } +``` -input PositionInput { - lat: Int! - lon: Int! +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: + +```graphql counter-example +type Product { + parts: [Part!]! + partIds(parts: [PartInput!]! @require(field: "parts[id name]")): [ID!]! } -input LocationInput { - coordinates: [PositionInput!]! +input PartInput { + 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. + +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 effect the structure of the input type. -The same principle applies to object values, which can be used to avoid repeating the same path multiple times. - The following example is valid: ```graphql example type Product { - dimensions: Dimension! - shippingCost(dimensions: DimensionInput! @require(field: "{ size: dimensions.size weight: dimensions.weight }")): Int! @lookup + parts: [Part!]! + partIds(parts: [PartInput!]! @require(field: "parts[{ id name }]")): [ID!]! +} + +input PartInput { + id: ID! + name: String! } ``` -The following example is equivalent to the previous one: +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 { - dimensions: Dimension! - shippingCost(dimensions: DimensionInput! @require(field: "dimensions.{ size weight }")): Int! @lookup + parts: [[Part!]]! + partIds(parts: [[PartInput!]]! @require(field: "parts[[{ id name }]]")): [ID!]! +} + +input PartInput { + id: ID! + name: String! } ``` -### SelectedObjectValue -SelectedObjectValue :: - - { SelectedObjectField+ } +The following example is valid: -SelectedObjectField :: - - Name: SelectedValue +```graphql example +type Query { + findLocation(location: LocationInput! @is(field: "{ coordinates: coordinates[{lat: x lon: y}]}")): Location @lookup +} -{SelectedObjectValue} are unordered lists of keyed input values wrapped in curly-braces `{}`. -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. +type Coordinate { + x: Int! + y: Int! +} -### Name -Is equivalent to the `Name` defined in the GraphQL specification. +type Location { + coordinates: [Coordinate!]! +} + +input PositionInput { + lat: Int! + lon: Int! +} + +input LocationInput { + coordinates: [PositionInput!]! +} +``` ## Validation Validation ensures that {FieldSelectionMap} scalars are semantically correct within the given context. From 694f0c35c9f1523ce76ca51ff1f776e37266d77f Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:52:46 -0700 Subject: [PATCH 10/17] Fix Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 8588ea0..4e3d04e 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -190,7 +190,7 @@ type Product { } ``` -Instead, use the path syntax and angle brackets to specify the list elements: +Instead, use the path syntax and brackets to specify the list elements: ```graphql example type Product { From e5f6d1868018728b64bf1bbf40d9a9ff7355f1f3 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:53:35 -0700 Subject: [PATCH 11/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 4e3d04e..009cc17 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -198,7 +198,7 @@ type Product { } ``` -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 { From 2da210d52c2b133812c4b21255e040712de3a576 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:53:45 -0700 Subject: [PATCH 12/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 009cc17..3b9a92a 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -342,7 +342,7 @@ 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. 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 effect the structure of the input type. +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 to reduce repetition in the selection. From 1fe3592b682246319d2550c64b3ced8137c3304c Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:53:54 -0700 Subject: [PATCH 13/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 3b9a92a..da3fb24 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -344,7 +344,7 @@ This structure is similar to the `ObjectValue` defined in the GraphQL specificat 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 to reduce repetition in the selection. +This allows for reducing repetition in the selection. The following example is valid: From 582d2a29eff49c42c607f46948f617acc30f71c8 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:54:03 -0700 Subject: [PATCH 14/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index da3fb24..63e2cfa 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -370,7 +370,7 @@ SelectedListValue :: - [ SelectedValue ] A {SelectedListValue} is an ordered list of {SelectedValue} wrapped in square brackets `[]`. -It is used to express semantic equivalence between a an argument expecting a list of values and the values of a list fields within the output object. +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 and element. From 008fdf61b04f612909bf8059b4a3d798de0b690f Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:54:10 -0700 Subject: [PATCH 15/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index 63e2cfa..bdd770e 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -372,7 +372,7 @@ SelectedListValue :: 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 and element. +The {SelectedListValue} differs from the `ListValue` defined in the GraphQL specification by only allowing one {SelectedValue} as an element. The following example is valid: From add722d03ddfd82850d16ff1cc4141ef96dbe29e Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 09:54:17 -0700 Subject: [PATCH 16/17] chore: fix language Co-authored-by: Glen --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index bdd770e..e44e0ae 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -402,7 +402,7 @@ input PartInput { 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 effect the structure of the input type. +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: From 4674b63113ef57eba8adf56b5f0dab3a1b8d3916 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sat, 7 Sep 2024 10:28:11 -0700 Subject: [PATCH 17/17] Fixed lookup --- spec/Appendix A -- Field Selection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Appendix A -- Field Selection.md b/spec/Appendix A -- Field Selection.md index e44e0ae..72bc131 100644 --- a/spec/Appendix A -- Field Selection.md +++ b/spec/Appendix A -- Field Selection.md @@ -351,7 +351,7 @@ The following example is valid: ```graphql example type Product { dimension: Dimension! - shippingCost(dimension: DimensionInput! @require(field: "dimension.{ size weight }")): Int! @lookup + shippingCost(dimension: DimensionInput! @require(field: "dimension.{ size weight }")): Int! } ```