From 53c3d6e28100cb8211eadf2772a99868d589e733 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 6 Apr 2021 17:34:16 -0700 Subject: [PATCH 01/41] Create QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 555 ++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 rfcs/QueryLevelNullability.md diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md new file mode 100644 index 000000000..4852c481a --- /dev/null +++ b/rfcs/QueryLevelNullability.md @@ -0,0 +1,555 @@ +# RFC: Query Level Nullability + +**Proposed by:** +- [Liz Jakubowski]() - Yelp +- [Alex Reilly]() - Yelp + +This RFC proposes creating a syntactical construct for client developers to +express "nullability" in their queries. + +## Defintions + +Nullability: a concept that exists accross many programming lanugage (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) +that is used to express when users can be certain that a value can or can never be `null` +(or the language equivilent). Nullability language constructs (eg `?` in Swift/Kotlin) +have become popular due to their ability to solve ergonomic problems in languages +such as those surrounding `NullPointerException` in Java. + +## πŸ“œ Problem Statement + +GraphQL is a "nullable by default" language meaning that all properties are allowed to be `null`. +This is in contrast to the two modern languages used on mobile clients, Swift and Kotlin, +which are both non-null by default languages. In Swift and Kotlin, unless developers otherwise +specify it, properties cannot be `null`. + +This mismatch creates some dissonance for developers who are currently forced into dealing the +problems that commonly surround nullablility in codebases that otherwise do not need to deal with +those problems. + +In Yelp's GraphQL schema, almost all object fields are nullable except for those with ID type. +This adheres to what seems to be the [official best practice](https://graphql.org/learn/best-practices/#nullability). + +This poses a problem for the mobile clients that use Apollo's codegen feature. The codegen provides +Swift/Kotlin types to represent queries used in the app. Nullable fields in the schema are represented +as optional properties on the resulting type: +```graphql +query GetBusinessName($encid: String!) { + business(encid: $encid) { + name + } +} +``` +```swift +struct GetBusinessNameQuery.Data.Business { + let name: String? +} +``` +In many cases, the client should error if the business `name` is nil. If codegen were out of the picture, +we would be able to throw an error at JSON response-parsing time if it's missing, or otherwise instantiate +a hand-written business object with a non-optional `name` property. From that point on, all feature code +can happily assume that it has a non-nil business name to work with. + +## πŸ§‘β€πŸ’» Proposed syntax +It would make more sense if the client could express that `name` must be non-nil _in the query itself_: +```graphql +query GetBusinessName($encid: String!) { + business(encid: $encid) { + name! <-- this! + } +} +``` +On web where codegen is not used, the client no longer needs to handle the case where expected fields are missing. +On mobile platforms where codegen is used, clients have full control over the optionality of the properties on the +generated types. Since optionality is expressed in the query rather than the schema, it's flexible enough to accommodate +various use-cases (e.g., where the business `name` _is_Β allowed to be optional). + +## Implementation +https://github.yelpcorp.com/wxue/graphql-js + +### // EVERYTHING BELOW THIS POINT IS NOT RELATED TO THE WIP RFC. IT IS BEING USED +### // AS A TEMPLATE + +### Use cases + +1. A GraphQL server wants to **log how often each field in the schema is + requested**. This may be implemented by incrementing a counter by the name of + the schema coordinate for each field executed in a request. + + _Existing implementations: Yelp (internal), Facebook (internal), + [Shopify (API health report)][shopify-api-health]_ + + [shopify-api-health]: https://shopify.dev/concepts/about-apis/versioning/api-health + +1. GraphiQL and other playgrounds / documentation sites want to show a list of + **search results** when a user searches for a type or field name. We can + display a list of schema coordinates that match the search term. A schema + coordinate can also be used in the hyperlink to form a permalink for + documentation for a particular field. + + _Existing implementations: GraphiQL, Apollo Studio (see "Prior Art")_ + +1. A developer may want to perform **analytics** on all known + [persisted queries][apq] - e.g. what are the most commonly used fields across + all documents. Schema coordinates may be used as the index/lookup keys when + storing this information in the database. + + _Existing implementations: Yelp (internal)_ + + [apq]: https://www.apollographql.com/docs/apollo-server/performance/apq/ + +1. A **GitHub bot** may want to warn developers in a Pull Request comment + whenever the schema diff contains a breaking change. Schema coordinates can be + used to provide a list of which fields were broken. + + _Existing implementations: GraphQL Inspector (see "Prior Art")_ + +1. **GraphQL IDEs** (e.g. GraphiQL, GraphQL Playground, Apollo Studio) may wish + to display the schema definition type of a node in a query when hovering over + it. + +
+ Example + ![](https://i.fluffy.cc/g78sJCjCJ0MsbNPhvgPXP46Kh9knBCKF.png) +
+ + Schema coordinates can be used to form the left hand side of this popover. + +_Existing implementations: Apollo Studio (see "Prior Art")_ + +## βœ… RFC Goals + +- There be one, unambiguous way to write a "schema coordinate" that refers to a + particular element in a GraphQL schema. (This is to avoid users having to + "simplify" more complex coordinates to produce a canonical representation.) +- Schema coordinate syntax should build off of existing de-facto standards + already adopted for this purpose (i.e. `Foo.bar`) +- Schema coordinate syntax is open for extension in the future. We should make + design choices that give us flexibility and anticipate future syntax needs + (based off of discussions around this RFC). + +## 🚫 RFC Non-goals + +- This does not cover "selectors" or "wildcard" syntax - e.g. `User.*`. _(See + alternatives considered.)_ +- There are **no proposed GraphQL language/syntax changes** +- There are **no proposed GraphQL runtime changes** +- [Schema coordinate non-goals](#-syntax-non-goals) + +## πŸ§‘β€πŸ’» Proposed syntax + +### `Type` + +Refers to a named type (e.g. something represented by `__typename` in a GraphQL +introspection call). + +### `Type.attribute` + +Refers to a named attribute on the named type. + +Not all types support this. For object types and interface types this is a field, +for input objects this would be an input field, for enums this would be an enum +value, for future GraphQL types this will relate to a related concept if they +have one (e.g. for the [proposed "tagged" type][tagged-type] it would refer to +the "member field"). + +[tagged-type]: https://github.com/graphql/graphql-spec/pull/733 + +### `Type.field(argName:)` + +Refers to a named argument on the named field of the named type. + +### `@directive` + +References the given named directive + +### `@directive(argName:)` + +References the named argument of the named directive. + +### ✨ Examples + +For example, consider the following schema: + +```graphql +directive @private(scope: String!) on FIELD + +type Person { + name: String + email: String @private(scope: "loggedIn") +} + +type Business { + name: String + owner: Person +} + +type Query { + searchBusinesses(name: String): [Business] +} +``` + +We can write the following schema coordinates: + +- `Person` uniquely identifies the the "Person" type +- `Business` uniquely identifies the the "Business" type +- `Person.name` uniquely identifies the "name" field on the "Person" type +- `Business.name` uniquely identifies the "name" field on the "Business" + type +- `Business.owner` uniquely identifies the "owner" field on the "Business" type +- `Query.searchBusinesses` uniquely identifies the "searchBusinesses" field on + the "Query" type +- `Query.searchBusinesses(name:)` uniquely identifies the "name" argument on the + "searchBusinesses" field on the "Query" type +- `@private` uniquely identifies the "private" directive +- `@private(scope:)` uniquely identifies the "scope" argument on the "private" + directive + +## 🎨 Prior art + +- The name "schema coordinates" is inspired from [GraphQL Java](https://github.com/graphql-java/graphql-java) + (4.3k stars), where "field coordinates" are already used in a similar way as + described in this RFC. + + - [GitHub comment](https://github.com/graphql/graphql-spec/issues/735#issuecomment-646979049) + - [Implementation](https://github.com/graphql-java/graphql-java/blob/2acb557474ca73/src/main/java/graphql/schema/FieldCoordinates.java) + +- GraphiQL displays schema coordinates in its documentation search tab: + + ![](https://i.fluffy.cc/5Cz9cpwLVsH1FsSF9VPVLwXvwrGpNh7q.png) + +- [GraphQL Inspector](https://github.com/kamilkisiela/graphql-inspector) (840 + stars) shows schema coordinates in its output: + + ![](https://i.imgur.com/HAf18rz.png) + +- [Apollo Studio](https://www.apollographql.com/docs/studio/) shows schema + coordinates when hovering over fields in a query: + + ![](https://i.fluffy.cc/g78sJCjCJ0MsbNPhvgPXP46Kh9knBCKF.png) + +## πŸ₯£ Document -> Schema Coordinate serialization + +Use cases 3 and 5 above imply that a mapping from GraphQL query nodes to schema +coordinates is performed. + +For example, consider the following schema: + +```graphql +type Person { + name: String +} + +type Business { + name: String + owner: Person +} + +type Query { + searchBusiness(name: String): [Business] +} +``` + +And the following query: + +```graphql +query { + searchBusinesses(name: "El Greco Deli") { + name + owner { + name + } + } +} +``` + +From the query above, we may calculate the following list of schema coordinates: + +- `Query.searchBusinesses` +- `Business.name` +- `Business.owner` +- `Person.name` + +`Query.searchBusinesses(name:)` is also a valid member of the output set. The +serialization algorithm may optionally choose to output all permutations of field +arguments used, should this be specified. + +A library has been written to demonstrate this mapping: +. + +## πŸ—³οΈ Alternatives considered + +### Naming + +- **"Schema Selectors"** + + "Selectors" is a term used in [HTML](https://www.w3.org/TR/selectors-api/) and + [CSS](https://drafts.csswg.org/selectors-4/) to _select_ parts of an HTML + document. + + This would be a compelling, familiar choice - however, we've decided to not + support wildcard expansion in this spec. See the section + [Syntax Non-goals](#-syntax-non-goals). + +- **"type/field pairs"** + + This was the original working name. However, there already exists more + established terminology for this concept, and we also wish to describe more + than just types on fields. + +- **"Field Coordinates"** + + "Field Coordinates" is already understood and used by the popular + [GraphQL Java](https://github.com/graphql-java/graphql-java) project. + + [Feedback in the August GraphQL Working Group meeting](https://youtu.be/FYF15RA9H3k?t=3786) + hinted that since we're targeting also describing arguments, _field_ + coordinates might not be the right name. Hence "Schema Coordinates" is chosen + instead, as a more generalized form of this. + +- **"GraphQL Coordinates"** + + Similar to Field Coordinates/Schema Coordinates - however, "GraphQL + Coordinates" is potentially ambiguous as to if it describes _schema_ members, + _query/document_ members or response object members. + +- **"Field path" / "GraphQL path"** + + [`path` exists as an attribute on `GraphQLResolveInfo`](https://github.com/graphql/graphql-js/blob/8f3d09b54260565/src/type/definition.js#L951). + + Given the following query: + + ```graphql + query { + searchBusinesses(name: "El Greco Deli") { + name + owner { + name + } + } + } + ``` + + `Person.name` in the response may be written as the following "field path": + + ```json + ["query", "searchBusinesses", 1, "owner", "name"] + ``` + + Note that here, the "path" is a serialized _response_ tree traversal, instead + of describing the location of the field in the _schema_. + + Since "path" is already used in GraphQL nomenclature to describe the location + of a field in a response, we'll avoid overloading this term. + +### Separator + +This RFC proposes using "`.`" as the separator character between a type and +field. The following have also been proposed: + +- `Foo::bar` +- `Foo#bar` +- `Foo->bar` +- `Foo~bar` +- `Foo:bar` + +"`.`" is already used in the existing implementations of field coordinates, hence +the suggested usage in this RFC. However, we may wish to consider one of the +alternatives above, should this conflict with existing or planned language +features. + +### Field Arguments + +We have discussed multiple options for selecting arguments on fields. ([PR][pr], +and [December WG Meeting][wg-meeting]). For example, consider the following +schema: + +[pr]: https://github.com/graphql/graphql-spec/pull/746#discussion_r526243627 +[wg-meeting]: https://youtu.be/Duh4MRXQRQA?t=2506 + +```graphql +type Query { + rollDice(numDice: Int, numSides: Int): Int +} +``` + +We may want to refer to the `numDice` argument in a schema selector. Two options +for this syntax are: + +1. `Query.rollDice.numDice` +1. `Query.rollDice(numDice:)` + +#### Pros for `Query.rollDice.numDice` + +- Less bytes/characters to type +- May allow for extension to include nested "field paths" (e.g. Foo.bar.Baz.qux...) +- [Prior usage][graphiql-usage] of this syntax to represent state internally + +[graphiql-usage]: https://github.com/graphql/graphql-spec/pull/746#issuecomment-752941039 + +#### Pros for `Query.rollDice(numDice:)` + +- Indicating arguments with colons disambiguates against other types of schema + nodes. For those unfamiliar with schema selectors, it may be unclear if the + third dot separated item refers to a directive or a child object etc. +- Using trailing colons for arguments is borrowed from other languages (e.g. + [Swift][swift]). This may indicate to users who are unfamiliar with schema + coordinates, but recognize this from other languages, that `numDice:` refers + to an argument. The function parentheses and colons more strongly communicate + "this is an argument!" than a second dot separator. + +#### Decision + +We are choosing `Query.rollDice(numDice:)` to optimize for **readability** and +**extensibility**. + +Given our expected use cases, we assume Schema Coordinates will be _read_ more +often than they are _written_ (e.g. error messages in a stack trace from a +schema linting tool). Readers may be unfamiliar with its syntax. We want to +"hint" as much as possible the meaning of the coordinates in its syntax. We +think `(numDice:)` more clearly communicates that "numDice" is an argument, over +`.numDice`. + +In addition, we want to be mindful of extensions to this syntax in the future. +Using dots only as a separator may overload the meaning of elements in schema +coordinates in the future. (If we capture new schema node types, or nested +paths.) + +> We should make sure that the spec enables future innovation including using it +> for things other than schema coordinates. To my mind the (foo:) syntax is more +> flexible in this regard. For example, I can imagine referring to: +> +> 1. `Foo.bar(baz.qux:)`: the qux field of the input object referred to from the +> baz argument of the bar field on the Foo type. +> 2. `Foo.bar(baz:).qux`: the qux field on the return type of the bar field +> (with baz: argument) of the Foo type. +> 3. `Foo.bar.baz.qux`: the qux field of the return type of the baz field on the +> return type of the bar field on type Foo. +> +> If we were to only use periods then all of these would come out the same as +> `Foo.bar.baz.qux`, and this ambiguity precludes this kind of reusal of the +> schema-coordinates syntax for this use case (which is outside the scope of the +> schema coordinates spec, for sure, but is still a potential use-case for the +> syntax). +> +> ~ [benjie](https://github.com/graphql/graphql-spec/pull/746#discussion_r527639917) + +## πŸ™… Syntax Non-goals + +This syntax consciously does not cover the following use cases: + +- **Wildcard selectors** + + Those familiar with `document.querySelector` may be expecting the ability to + pass "wildcards" or "star syntax" to be able to select multiple schema + elements. This implies multiple ways of _selecting_ a schema node. + + For example, `User.address` and `User.a*` might both resolve to `User.address`. + But `User.a*` could also ambiguously refer to `User.age`. + + It's unclear how wildcard expansion would work with respect to field + arguments\*, potentially violating the requirement of this schema to _uniquely_ + identify schema components. + + \* _(e.g. does `Query.getUser` also select all arguments on the `getUser` + field? Who knows! A discussion for another time.)_ + + A more general purpose schema selector language could be built on top of this + spec - however, we'll consider this **out of scope** for now. + +- **Nested field paths** + + This spec does _not_ support selecting schema members with a path from a root + type (e.g. `Query`). + + For example, given this schema + + ```graphql + type User { + name: String + bestFriend: User + } + + type Query { + userById(id: String): User + } + ``` + + The following are invalid schema coordinates: + + - `Query.userById.name` + - `User.bestFriend.bestFriend.bestFriend.name` + + This violates a non-goal that there be one, unambiguous way to write a + schema coordinate to refer to a schema member. Both examples can be + "simplified" to `User.name`, which _is_ a valid schema coordinate. + + Should a use case for this arise in the future, a follow up RFC may investigate + how schema coordinates could work with "field paths" (e.g. `["query", "searchBusinesses", 1, "owner", "name"]`) to cover this. + +- **Directive applications** + + This spec does _not_ support selecting applications of directive. + + For example: + + ```graphql + directive @private(scope: String!) on FIELD + + type User { + name: String + reviewCount: Int + friends: [User] + email: String @private(scope: "loggedIn") + } + ``` + + You _can_ select the definition of the `private` directive and its arguments + (with `@private` and `@private(scope:)` respectively), but you cannot select the + application of the `@private` on `User.email`. + + For the stated use cases of this RFC, it is more likely that consumers want to + select and track usage and changes to the definition of the custom directive + instead. + + If we _did_ want to support this, a syntax such as `User.email@private[0]` + could work. (The indexing is necessary since [multiple applications of the same + directive is allowed][multiple-directives], and each is considered unique.) + + [multiple-directives]: http://spec.graphql.org/draft/#sec-Directives-Are-Unique-Per-Location + +- **Union members** + + This spec does not support selecting members inside a union definition. + + For example: + + ```graphql + type Breakfast { + eggCount: Int + } + + type Lunch { + sandwichFilling: String + } + + union Meal = Breakfast | Lunch + ``` + + You may select the `Meal` definition (as "`Meal`"), but you may **not** select + members on `Meal` (e.g. `Meal.Breakfast` or `Meal.Lunch`). + + It is unclear what the use case for this would be, so we won't (yet?) support + this. In such cases, consumers may select type members directly (e.g. `Lunch`). + +## Answered questions + +- **Would we want to add a method to graphql-js?** A `fieldCoordinateToFieldNode` + method (for example) may take in a field coordinate string and return a field + AST node to serve as a helper / reference implementation of the algorithm to + look up the field node. + + _Update:_ [This was discussed in the August Working Group Meeting][meeting] - + it was suggested to keep any utilities as third party libraries to avoid edge + ambiguity problems, and to be able to iterate faster. + + [meeting]: https://youtu.be/FYF15RA9H3k?t=2865 From ff04f2df41e27514d3b45e40a722dd4d864b7b2c Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 6 Apr 2021 18:04:43 -0700 Subject: [PATCH 02/41] Optional -> Nullable --- rfcs/QueryLevelNullability.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 4852c481a..1ab37d149 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -9,12 +9,16 @@ express "nullability" in their queries. ## Defintions -Nullability: a concept that exists accross many programming lanugage (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) +Nullability: A concept that exists accross many programming lanugage (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) that is used to express when users can be certain that a value can or can never be `null` (or the language equivilent). Nullability language constructs (eg `?` in Swift/Kotlin) have become popular due to their ability to solve ergonomic problems in languages such as those surrounding `NullPointerException` in Java. +Codegen: Short for Code Generation, tools that exist for working with GraphQL which take queries as +input and output code in a language of choice that represents data that will result from those +queries. Codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings.](https://github.com/apollographql/apollo-tooling#code-generation) + ## πŸ“œ Problem Statement GraphQL is a "nullable by default" language meaning that all properties are allowed to be `null`. @@ -31,7 +35,7 @@ This adheres to what seems to be the [official best practice](https://graphql.or This poses a problem for the mobile clients that use Apollo's codegen feature. The codegen provides Swift/Kotlin types to represent queries used in the app. Nullable fields in the schema are represented -as optional properties on the resulting type: +as nullable properties on the resulting type, represented by `?` following the type name: ```graphql query GetBusinessName($encid: String!) { business(encid: $encid) { @@ -46,7 +50,7 @@ struct GetBusinessNameQuery.Data.Business { ``` In many cases, the client should error if the business `name` is nil. If codegen were out of the picture, we would be able to throw an error at JSON response-parsing time if it's missing, or otherwise instantiate -a hand-written business object with a non-optional `name` property. From that point on, all feature code +a hand-written business object with a non-nullable `name` property. From that point on, all feature code can happily assume that it has a non-nil business name to work with. ## πŸ§‘β€πŸ’» Proposed syntax @@ -59,9 +63,9 @@ query GetBusinessName($encid: String!) { } ``` On web where codegen is not used, the client no longer needs to handle the case where expected fields are missing. -On mobile platforms where codegen is used, clients have full control over the optionality of the properties on the -generated types. Since optionality is expressed in the query rather than the schema, it's flexible enough to accommodate -various use-cases (e.g., where the business `name` _is_Β allowed to be optional). +On mobile platforms where codegen is used, clients have full control over the nullability of the properties on the +generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate +various use-cases (e.g., where the business `name` _is_Β allowed to be nullable). ## Implementation https://github.yelpcorp.com/wxue/graphql-js From b823d7c700316127ef9ca74cbd5b44817cc810a2 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 6 Apr 2021 19:42:47 -0700 Subject: [PATCH 03/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 381 ++++------------------------------ 1 file changed, 36 insertions(+), 345 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 1ab37d149..eca2379f9 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -48,13 +48,14 @@ struct GetBusinessNameQuery.Data.Business { let name: String? } ``` -In many cases, the client should error if the business `name` is nil. If codegen were out of the picture, +In many cases, the client should error if the business `name` is null. If codegen were out of the picture, we would be able to throw an error at JSON response-parsing time if it's missing, or otherwise instantiate a hand-written business object with a non-nullable `name` property. From that point on, all feature code -can happily assume that it has a non-nil business name to work with. +can happily assume that it has a non-null business name to work with. ## πŸ§‘β€πŸ’» Proposed syntax -It would make more sense if the client could express that `name` must be non-nil _in the query itself_: + +It would make more sense if the client could express that `name` must be non-null _in the query itself_: ```graphql query GetBusinessName($encid: String!) { business(encid: $encid) { @@ -67,146 +68,53 @@ On mobile platforms where codegen is used, clients have full control over the nu generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate various use-cases (e.g., where the business `name` _is_Β allowed to be nullable). -## Implementation -https://github.yelpcorp.com/wxue/graphql-js - -### // EVERYTHING BELOW THIS POINT IS NOT RELATED TO THE WIP RFC. IT IS BEING USED -### // AS A TEMPLATE - -### Use cases - -1. A GraphQL server wants to **log how often each field in the schema is - requested**. This may be implemented by incrementing a counter by the name of - the schema coordinate for each field executed in a request. - - _Existing implementations: Yelp (internal), Facebook (internal), - [Shopify (API health report)][shopify-api-health]_ - - [shopify-api-health]: https://shopify.dev/concepts/about-apis/versioning/api-health - -1. GraphiQL and other playgrounds / documentation sites want to show a list of - **search results** when a user searches for a type or field name. We can - display a list of schema coordinates that match the search term. A schema - coordinate can also be used in the hyperlink to form a permalink for - documentation for a particular field. - - _Existing implementations: GraphiQL, Apollo Studio (see "Prior Art")_ - -1. A developer may want to perform **analytics** on all known - [persisted queries][apq] - e.g. what are the most commonly used fields across - all documents. Schema coordinates may be used as the index/lookup keys when - storing this information in the database. - - _Existing implementations: Yelp (internal)_ +In the case that a field decorated with `!` is `null`, the server is expected to return an error to the client. - [apq]: https://www.apollographql.com/docs/apollo-server/performance/apq/ +### `!` -1. A **GitHub bot** may want to warn developers in a Pull Request comment - whenever the schema diff contains a breaking change. Schema coordinates can be - used to provide a list of which fields were broken. +We have chosen `!` because `!` is already being used in the GraphQL spec to indicate that a field is non-nullable. +Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which both use similar syntax to indicate +"throw an exception if this value is `null`". - _Existing implementations: GraphQL Inspector (see "Prior Art")_ - -1. **GraphQL IDEs** (e.g. GraphiQL, GraphQL Playground, Apollo Studio) may wish - to display the schema definition type of a node in a query when hovering over - it. +### Use cases -
- Example - ![](https://i.fluffy.cc/g78sJCjCJ0MsbNPhvgPXP46Kh9knBCKF.png) -
+### ✨ Examples - Schema coordinates can be used to form the left hand side of this popover. +```graphql +query GetBusinessName($encid: String!) { + business(encid: $encid) { + name! <-- this! + } +} +``` +would codegen to the following type on iOS. -_Existing implementations: Apollo Studio (see "Prior Art")_ +```swift +struct GetBusinessNameQuery.Data.Business { + let name: String // lack of `?` indicates that `name` will never be `null` +} +``` ## βœ… RFC Goals - -- There be one, unambiguous way to write a "schema coordinate" that refers to a - particular element in a GraphQL schema. (This is to avoid users having to - "simplify" more complex coordinates to produce a canonical representation.) -- Schema coordinate syntax should build off of existing de-facto standards - already adopted for this purpose (i.e. `Foo.bar`) -- Schema coordinate syntax is open for extension in the future. We should make - design choices that give us flexibility and anticipate future syntax needs - (based off of discussions around this RFC). +- Non-nullable syntax that is based off of syntax that developers will already be familiar with ## 🚫 RFC Non-goals -- This does not cover "selectors" or "wildcard" syntax - e.g. `User.*`. _(See - alternatives considered.)_ -- There are **no proposed GraphQL language/syntax changes** -- There are **no proposed GraphQL runtime changes** -- [Schema coordinate non-goals](#-syntax-non-goals) - -## πŸ§‘β€πŸ’» Proposed syntax - -### `Type` - -Refers to a named type (e.g. something represented by `__typename` in a GraphQL -introspection call). - -### `Type.attribute` - -Refers to a named attribute on the named type. - -Not all types support this. For object types and interface types this is a field, -for input objects this would be an input field, for enums this would be an enum -value, for future GraphQL types this will relate to a related concept if they -have one (e.g. for the [proposed "tagged" type][tagged-type] it would refer to -the "member field"). - -[tagged-type]: https://github.com/graphql/graphql-spec/pull/733 - -### `Type.field(argName:)` - -Refers to a named argument on the named field of the named type. - -### `@directive` - -References the given named directive - -### `@directive(argName:)` - -References the named argument of the named directive. - -### ✨ Examples - -For example, consider the following schema: - -```graphql -directive @private(scope: String!) on FIELD +## πŸ—³οΈ Alternatives considered -type Person { - name: String - email: String @private(scope: "loggedIn") -} +### Make Schema Fields Non-Nullable Instead +Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) +// We definitely want to flesh this out -type Business { - name: String - owner: Person -} +### Alternatives to `!` +#### `!!` +This would follow the precedent set by Kotlin. -type Query { - searchBusinesses(name: String): [Business] -} -``` +## Implementation +https://github.yelpcorp.com/wxue/graphql-js -We can write the following schema coordinates: - -- `Person` uniquely identifies the the "Person" type -- `Business` uniquely identifies the the "Business" type -- `Person.name` uniquely identifies the "name" field on the "Person" type -- `Business.name` uniquely identifies the "name" field on the "Business" - type -- `Business.owner` uniquely identifies the "owner" field on the "Business" type -- `Query.searchBusinesses` uniquely identifies the "searchBusinesses" field on - the "Query" type -- `Query.searchBusinesses(name:)` uniquely identifies the "name" argument on the - "searchBusinesses" field on the "Query" type -- `@private` uniquely identifies the "private" directive -- `@private(scope:)` uniquely identifies the "scope" argument on the "private" - directive +### // EVERYTHING BELOW THIS POINT IS NOT RELATED TO THE WIP RFC. IT IS BEING USED +### // AS A TEMPLATE ## 🎨 Prior art @@ -231,211 +139,7 @@ We can write the following schema coordinates: ![](https://i.fluffy.cc/g78sJCjCJ0MsbNPhvgPXP46Kh9knBCKF.png) -## πŸ₯£ Document -> Schema Coordinate serialization - -Use cases 3 and 5 above imply that a mapping from GraphQL query nodes to schema -coordinates is performed. -For example, consider the following schema: - -```graphql -type Person { - name: String -} - -type Business { - name: String - owner: Person -} - -type Query { - searchBusiness(name: String): [Business] -} -``` - -And the following query: - -```graphql -query { - searchBusinesses(name: "El Greco Deli") { - name - owner { - name - } - } -} -``` - -From the query above, we may calculate the following list of schema coordinates: - -- `Query.searchBusinesses` -- `Business.name` -- `Business.owner` -- `Person.name` - -`Query.searchBusinesses(name:)` is also a valid member of the output set. The -serialization algorithm may optionally choose to output all permutations of field -arguments used, should this be specified. - -A library has been written to demonstrate this mapping: -. - -## πŸ—³οΈ Alternatives considered - -### Naming - -- **"Schema Selectors"** - - "Selectors" is a term used in [HTML](https://www.w3.org/TR/selectors-api/) and - [CSS](https://drafts.csswg.org/selectors-4/) to _select_ parts of an HTML - document. - - This would be a compelling, familiar choice - however, we've decided to not - support wildcard expansion in this spec. See the section - [Syntax Non-goals](#-syntax-non-goals). - -- **"type/field pairs"** - - This was the original working name. However, there already exists more - established terminology for this concept, and we also wish to describe more - than just types on fields. - -- **"Field Coordinates"** - - "Field Coordinates" is already understood and used by the popular - [GraphQL Java](https://github.com/graphql-java/graphql-java) project. - - [Feedback in the August GraphQL Working Group meeting](https://youtu.be/FYF15RA9H3k?t=3786) - hinted that since we're targeting also describing arguments, _field_ - coordinates might not be the right name. Hence "Schema Coordinates" is chosen - instead, as a more generalized form of this. - -- **"GraphQL Coordinates"** - - Similar to Field Coordinates/Schema Coordinates - however, "GraphQL - Coordinates" is potentially ambiguous as to if it describes _schema_ members, - _query/document_ members or response object members. - -- **"Field path" / "GraphQL path"** - - [`path` exists as an attribute on `GraphQLResolveInfo`](https://github.com/graphql/graphql-js/blob/8f3d09b54260565/src/type/definition.js#L951). - - Given the following query: - - ```graphql - query { - searchBusinesses(name: "El Greco Deli") { - name - owner { - name - } - } - } - ``` - - `Person.name` in the response may be written as the following "field path": - - ```json - ["query", "searchBusinesses", 1, "owner", "name"] - ``` - - Note that here, the "path" is a serialized _response_ tree traversal, instead - of describing the location of the field in the _schema_. - - Since "path" is already used in GraphQL nomenclature to describe the location - of a field in a response, we'll avoid overloading this term. - -### Separator - -This RFC proposes using "`.`" as the separator character between a type and -field. The following have also been proposed: - -- `Foo::bar` -- `Foo#bar` -- `Foo->bar` -- `Foo~bar` -- `Foo:bar` - -"`.`" is already used in the existing implementations of field coordinates, hence -the suggested usage in this RFC. However, we may wish to consider one of the -alternatives above, should this conflict with existing or planned language -features. - -### Field Arguments - -We have discussed multiple options for selecting arguments on fields. ([PR][pr], -and [December WG Meeting][wg-meeting]). For example, consider the following -schema: - -[pr]: https://github.com/graphql/graphql-spec/pull/746#discussion_r526243627 -[wg-meeting]: https://youtu.be/Duh4MRXQRQA?t=2506 - -```graphql -type Query { - rollDice(numDice: Int, numSides: Int): Int -} -``` - -We may want to refer to the `numDice` argument in a schema selector. Two options -for this syntax are: - -1. `Query.rollDice.numDice` -1. `Query.rollDice(numDice:)` - -#### Pros for `Query.rollDice.numDice` - -- Less bytes/characters to type -- May allow for extension to include nested "field paths" (e.g. Foo.bar.Baz.qux...) -- [Prior usage][graphiql-usage] of this syntax to represent state internally - -[graphiql-usage]: https://github.com/graphql/graphql-spec/pull/746#issuecomment-752941039 - -#### Pros for `Query.rollDice(numDice:)` - -- Indicating arguments with colons disambiguates against other types of schema - nodes. For those unfamiliar with schema selectors, it may be unclear if the - third dot separated item refers to a directive or a child object etc. -- Using trailing colons for arguments is borrowed from other languages (e.g. - [Swift][swift]). This may indicate to users who are unfamiliar with schema - coordinates, but recognize this from other languages, that `numDice:` refers - to an argument. The function parentheses and colons more strongly communicate - "this is an argument!" than a second dot separator. - -#### Decision - -We are choosing `Query.rollDice(numDice:)` to optimize for **readability** and -**extensibility**. - -Given our expected use cases, we assume Schema Coordinates will be _read_ more -often than they are _written_ (e.g. error messages in a stack trace from a -schema linting tool). Readers may be unfamiliar with its syntax. We want to -"hint" as much as possible the meaning of the coordinates in its syntax. We -think `(numDice:)` more clearly communicates that "numDice" is an argument, over -`.numDice`. - -In addition, we want to be mindful of extensions to this syntax in the future. -Using dots only as a separator may overload the meaning of elements in schema -coordinates in the future. (If we capture new schema node types, or nested -paths.) - -> We should make sure that the spec enables future innovation including using it -> for things other than schema coordinates. To my mind the (foo:) syntax is more -> flexible in this regard. For example, I can imagine referring to: -> -> 1. `Foo.bar(baz.qux:)`: the qux field of the input object referred to from the -> baz argument of the bar field on the Foo type. -> 2. `Foo.bar(baz:).qux`: the qux field on the return type of the bar field -> (with baz: argument) of the Foo type. -> 3. `Foo.bar.baz.qux`: the qux field of the return type of the baz field on the -> return type of the bar field on type Foo. -> -> If we were to only use periods then all of these would come out the same as -> `Foo.bar.baz.qux`, and this ambiguity precludes this kind of reusal of the -> schema-coordinates syntax for this use case (which is outside the scope of the -> schema coordinates spec, for sure, but is still a potential use-case for the -> syntax). -> -> ~ [benjie](https://github.com/graphql/graphql-spec/pull/746#discussion_r527639917) ## πŸ™… Syntax Non-goals @@ -544,16 +248,3 @@ This syntax consciously does not cover the following use cases: It is unclear what the use case for this would be, so we won't (yet?) support this. In such cases, consumers may select type members directly (e.g. `Lunch`). - -## Answered questions - -- **Would we want to add a method to graphql-js?** A `fieldCoordinateToFieldNode` - method (for example) may take in a field coordinate string and return a field - AST node to serve as a helper / reference implementation of the algorithm to - look up the field node. - - _Update:_ [This was discussed in the August Working Group Meeting][meeting] - - it was suggested to keep any utilities as third party libraries to avoid edge - ambiguity problems, and to be able to iterate faster. - - [meeting]: https://youtu.be/FYF15RA9H3k?t=2865 From 20aa3507c41c50324bdd1d563a189eccbcdaf1da Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 6 Apr 2021 20:32:00 -0700 Subject: [PATCH 04/41] Update QueryLevelNullability.md More of an explanation of the semantics of the swift force unwrap operator --- rfcs/QueryLevelNullability.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index eca2379f9..5f6c74ee0 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -63,6 +63,15 @@ query GetBusinessName($encid: String!) { } } ``` +Semantically the GraphQL `!` operator is nearly identical to it's counter-part in Swift (also represented by `!`) which is +referred to as the "force unwrap operator". In Swift, for example, you can cast a string to an integer with `Int("5")` +but the string being cast may not be a valid number, so that statement can return `null`. If you want to ensure +that the statement does not return `null` you can instead write `Int("5")!`. If you do that, an exception will be +thrown if the statement would return `null`. + +In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an +error rather than any data. + On web where codegen is not used, the client no longer needs to handle the case where expected fields are missing. On mobile platforms where codegen is used, clients have full control over the nullability of the properties on the generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate @@ -78,6 +87,8 @@ Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which bo ### Use cases +#### When a field is necessary to the function of the app + ### ✨ Examples ```graphql From 7f1274219441c7e16e7827e067cbbe0b6b362810 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 6 Apr 2021 22:29:25 -0700 Subject: [PATCH 05/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 40 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 5f6c74ee0..5d4fad19a 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -23,7 +23,7 @@ queries. Codegen tools exist for many platforms. As an example, [here's some inf GraphQL is a "nullable by default" language meaning that all properties are allowed to be `null`. This is in contrast to the two modern languages used on mobile clients, Swift and Kotlin, -which are both non-null by default languages. In Swift and Kotlin, unless developers otherwise +which are both non-null by default. In Swift and Kotlin, unless developers otherwise specify it, properties cannot be `null`. This mismatch creates some dissonance for developers who are currently forced into dealing the @@ -65,20 +65,18 @@ query GetBusinessName($encid: String!) { ``` Semantically the GraphQL `!` operator is nearly identical to it's counter-part in Swift (also represented by `!`) which is referred to as the "force unwrap operator". In Swift, for example, you can cast a string to an integer with `Int("5")` -but the string being cast may not be a valid number, so that statement can return `null`. If you want to ensure -that the statement does not return `null` you can instead write `Int("5")!`. If you do that, an exception will be -thrown if the statement would return `null`. +but the string being cast may not be a valid number, so that statement will return `null` rather than an integer if the +string cannot be turned into an integer. If you want to ensure that the statement does not return `null` you can instead +write `Int("5")!`. If you do that, an exception will be thrown if the statement would return `null`. In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an -error rather than any data. +error to the client rather than any data. On web where codegen is not used, the client no longer needs to handle the case where expected fields are missing. On mobile platforms where codegen is used, clients have full control over the nullability of the properties on the generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate various use-cases (e.g., where the business `name` _is_Β allowed to be nullable). -In the case that a field decorated with `!` is `null`, the server is expected to return an error to the client. - ### `!` We have chosen `!` because `!` is already being used in the GraphQL spec to indicate that a field is non-nullable. @@ -87,10 +85,11 @@ Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which bo ### Use cases -#### When a field is necessary to the function of the app +#### When a field is necessary to the function of the client ### ✨ Examples +#### Non-nullable field ```graphql query GetBusinessName($encid: String!) { business(encid: $encid) { @@ -106,6 +105,18 @@ struct GetBusinessNameQuery.Data.Business { } ``` +#### Non-nullable object with nullable fields +```graphql +query { + business(id: 4) { + reviews! { + rating + text + } + } +} +``` + ## βœ… RFC Goals - Non-nullable syntax that is based off of syntax that developers will already be familiar with @@ -121,6 +132,19 @@ Discussion on [this topic can be found here](https://medium.com/@calebmer/when-t #### `!!` This would follow the precedent set by Kotlin. +### Make non-nullability apply recursively +For example, everything in this tree would be non-nullable +```graphql +query! { + business(id: 4) { + name + } +} +``` +// TODO: This was rejected when it was proposed. Looking for more info as to why + + + ## Implementation https://github.yelpcorp.com/wxue/graphql-js From acf4d00d00c43aa391aaf49f809ceec836a226e9 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Mon, 12 Apr 2021 16:21:11 -0700 Subject: [PATCH 06/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 173 +++++++--------------------------- 1 file changed, 33 insertions(+), 140 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 5d4fad19a..c153a910d 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -1,8 +1,9 @@ # RFC: Query Level Nullability **Proposed by:** -- [Liz Jakubowski]() - Yelp -- [Alex Reilly]() - Yelp +- [Liz Jakubowski]() - Yelp iOS +- [Alex Reilly]() - Yelp iOS +- [Sanae Rosen]() - Yelp Android This RFC proposes creating a syntactical construct for client developers to express "nullability" in their queries. @@ -15,7 +16,7 @@ that is used to express when users can be certain that a value can or can never have become popular due to their ability to solve ergonomic problems in languages such as those surrounding `NullPointerException` in Java. -Codegen: Short for Code Generation, tools that exist for working with GraphQL which take queries as +Codegen: Short for Code Generation, tools that exist for working with GraphQL which take queries and schemas as input and output code in a language of choice that represents data that will result from those queries. Codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings.](https://github.com/apollographql/apollo-tooling#code-generation) @@ -28,7 +29,13 @@ specify it, properties cannot be `null`. This mismatch creates some dissonance for developers who are currently forced into dealing the problems that commonly surround nullablility in codebases that otherwise do not need to deal with -those problems. +those problems. This makes large numbers of nulls tedious and time-consuming to handle. + +It is often unclear how to handle partial results. +- What elements are essential to having an adequate user experience? How many elements can fail before you + should just show an error message instead? +- When any combination of elements can fail, it is very hard to anticipate all edge cases and how the app + might look and work, including transitions to other screens. In Yelp's GraphQL schema, almost all object fields are nullable except for those with ID type. This adheres to what seems to be the [official best practice](https://graphql.org/learn/best-practices/#nullability). @@ -86,6 +93,10 @@ Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which bo ### Use cases #### When a field is necessary to the function of the client +There are times where a field can be `null`, but a feature that queries for the field will not function +if it is `null`. For example if you are trying to render an information page for a movie, you won't +be able to do that if the name field of the movie is missing. In that case it would be preferable +to fail as early as possible. ### ✨ Examples @@ -128,6 +139,16 @@ query { Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) // We definitely want to flesh this out +### Use a directive such as `@non-null` instead +This is the alternative being used at some of the companies represented in this proposal for the time being. +However this feels like a common enough need to call for a language feature. A single language feature +rather than using directives as a workaround would enable more unified public tooling around GraphQL. + +### Write wrapper types that null-check fields +This is the alternative being used at some of the companies represented in this proposal for the time being. +It's quite labor intensive and the work is quite rote. It more or less undermines the purpose of +having code generation. + ### Alternatives to `!` #### `!!` This would follow the precedent set by Kotlin. @@ -143,143 +164,15 @@ query! { ``` // TODO: This was rejected when it was proposed. Looking for more info as to why - - -## Implementation -https://github.yelpcorp.com/wxue/graphql-js - -### // EVERYTHING BELOW THIS POINT IS NOT RELATED TO THE WIP RFC. IT IS BEING USED -### // AS A TEMPLATE - -## 🎨 Prior art - -- The name "schema coordinates" is inspired from [GraphQL Java](https://github.com/graphql-java/graphql-java) - (4.3k stars), where "field coordinates" are already used in a similar way as - described in this RFC. - - - [GitHub comment](https://github.com/graphql/graphql-spec/issues/735#issuecomment-646979049) - - [Implementation](https://github.com/graphql-java/graphql-java/blob/2acb557474ca73/src/main/java/graphql/schema/FieldCoordinates.java) - -- GraphiQL displays schema coordinates in its documentation search tab: - - ![](https://i.fluffy.cc/5Cz9cpwLVsH1FsSF9VPVLwXvwrGpNh7q.png) - -- [GraphQL Inspector](https://github.com/kamilkisiela/graphql-inspector) (840 - stars) shows schema coordinates in its output: - - ![](https://i.imgur.com/HAf18rz.png) - -- [Apollo Studio](https://www.apollographql.com/docs/studio/) shows schema - coordinates when hovering over fields in a query: - - ![](https://i.fluffy.cc/g78sJCjCJ0MsbNPhvgPXP46Kh9knBCKF.png) - - - ## πŸ™… Syntax Non-goals This syntax consciously does not cover the following use cases: -- **Wildcard selectors** - - Those familiar with `document.querySelector` may be expecting the ability to - pass "wildcards" or "star syntax" to be able to select multiple schema - elements. This implies multiple ways of _selecting_ a schema node. - - For example, `User.address` and `User.a*` might both resolve to `User.address`. - But `User.a*` could also ambiguously refer to `User.age`. - - It's unclear how wildcard expansion would work with respect to field - arguments\*, potentially violating the requirement of this schema to _uniquely_ - identify schema components. - - \* _(e.g. does `Query.getUser` also select all arguments on the `getUser` - field? Who knows! A discussion for another time.)_ - - A more general purpose schema selector language could be built on top of this - spec - however, we'll consider this **out of scope** for now. - -- **Nested field paths** - - This spec does _not_ support selecting schema members with a path from a root - type (e.g. `Query`). - - For example, given this schema - - ```graphql - type User { - name: String - bestFriend: User - } - - type Query { - userById(id: String): User - } - ``` - - The following are invalid schema coordinates: - - - `Query.userById.name` - - `User.bestFriend.bestFriend.bestFriend.name` - - This violates a non-goal that there be one, unambiguous way to write a - schema coordinate to refer to a schema member. Both examples can be - "simplified" to `User.name`, which _is_ a valid schema coordinate. - - Should a use case for this arise in the future, a follow up RFC may investigate - how schema coordinates could work with "field paths" (e.g. `["query", "searchBusinesses", 1, "owner", "name"]`) to cover this. - -- **Directive applications** - - This spec does _not_ support selecting applications of directive. - - For example: - - ```graphql - directive @private(scope: String!) on FIELD - - type User { - name: String - reviewCount: Int - friends: [User] - email: String @private(scope: "loggedIn") - } - ``` - - You _can_ select the definition of the `private` directive and its arguments - (with `@private` and `@private(scope:)` respectively), but you cannot select the - application of the `@private` on `User.email`. - - For the stated use cases of this RFC, it is more likely that consumers want to - select and track usage and changes to the definition of the custom directive - instead. - - If we _did_ want to support this, a syntax such as `User.email@private[0]` - could work. (The indexing is necessary since [multiple applications of the same - directive is allowed][multiple-directives], and each is considered unique.) - - [multiple-directives]: http://spec.graphql.org/draft/#sec-Directives-Are-Unique-Per-Location - -- **Union members** - - This spec does not support selecting members inside a union definition. - - For example: - - ```graphql - type Breakfast { - eggCount: Int - } - - type Lunch { - sandwichFilling: String - } - - union Meal = Breakfast | Lunch - ``` - - You may select the `Meal` definition (as "`Meal`"), but you may **not** select - members on `Meal` (e.g. `Meal.Breakfast` or `Meal.Lunch`). - - It is unclear what the use case for this would be, so we won't (yet?) support - this. In such cases, consumers may select type members directly (e.g. `Lunch`). +- **Default Values** + The syntax being used in this proposal causes queries to error out in the case that + a `null` is found. As an alternative, some languages provide syntax (eg `??` for Swift) + that says "if a value would be `null` make it some other value instead". We are + not interested in covering that in this proposal. + +## Implementation +GraphQL.js: https://github.yelpcorp.com/wxue/graphql-js From 486b23f283d8b7da4cda3fdbd94ee09c97587776 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Mon, 12 Apr 2021 16:27:40 -0700 Subject: [PATCH 07/41] spell check --- rfcs/QueryLevelNullability.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index c153a910d..da1736ab5 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -8,11 +8,11 @@ This RFC proposes creating a syntactical construct for client developers to express "nullability" in their queries. -## Defintions +## Definitions -Nullability: A concept that exists accross many programming lanugage (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) +Nullability: A concept that exists across many programming languages (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) that is used to express when users can be certain that a value can or can never be `null` -(or the language equivilent). Nullability language constructs (eg `?` in Swift/Kotlin) +(or the language equivalent). Nullability language constructs (eg `?` in Swift/Kotlin) have become popular due to their ability to solve ergonomic problems in languages such as those surrounding `NullPointerException` in Java. @@ -70,7 +70,7 @@ query GetBusinessName($encid: String!) { } } ``` -Semantically the GraphQL `!` operator is nearly identical to it's counter-part in Swift (also represented by `!`) which is +Semantically the GraphQL `!` operator is nearly identical to its counterpart in Swift (also represented by `!`) which is referred to as the "force unwrap operator". In Swift, for example, you can cast a string to an integer with `Int("5")` but the string being cast may not be a valid number, so that statement will return `null` rather than an integer if the string cannot be turned into an integer. If you want to ensure that the statement does not return `null` you can instead From 37d78b34cee0cee691090c5311ff4d93fecda4d5 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Mon, 12 Apr 2021 16:33:14 -0700 Subject: [PATCH 08/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index da1736ab5..a0ea27548 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -2,8 +2,8 @@ **Proposed by:** - [Liz Jakubowski]() - Yelp iOS -- [Alex Reilly]() - Yelp iOS - [Sanae Rosen]() - Yelp Android +- [Alex Reilly]() - Yelp iOS This RFC proposes creating a syntactical construct for client developers to express "nullability" in their queries. From 438bcac4b94a051ce9139d278e6e7dbc63f30b0c Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Tue, 13 Apr 2021 09:06:39 -0700 Subject: [PATCH 09/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index a0ea27548..d76e9c763 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -3,6 +3,7 @@ **Proposed by:** - [Liz Jakubowski]() - Yelp iOS - [Sanae Rosen]() - Yelp Android +- [Mark Larah](https://github.com/magicmark) - Yelp Web - [Alex Reilly]() - Yelp iOS This RFC proposes creating a syntactical construct for client developers to From b62519750a2465086b2d320fa6af607143a3305e Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:20:27 -0700 Subject: [PATCH 10/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index d76e9c763..9b31b3f7c 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -1,7 +1,7 @@ # RFC: Query Level Nullability **Proposed by:** -- [Liz Jakubowski]() - Yelp iOS +- [Liz Jakubowski](https://github.com/lizjakubowski) - Yelp iOS - [Sanae Rosen]() - Yelp Android - [Mark Larah](https://github.com/magicmark) - Yelp Web - [Alex Reilly]() - Yelp iOS From 4829dfdf30bb01f6af80ac0fb823d83e4074f5b6 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:20:52 -0700 Subject: [PATCH 11/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 9b31b3f7c..ff3e99dd8 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -28,9 +28,9 @@ This is in contrast to the two modern languages used on mobile clients, Swift an which are both non-null by default. In Swift and Kotlin, unless developers otherwise specify it, properties cannot be `null`. -This mismatch creates some dissonance for developers who are currently forced into dealing the -problems that commonly surround nullablility in codebases that otherwise do not need to deal with -those problems. This makes large numbers of nulls tedious and time-consuming to handle. +This mismatch creates some dissonance for developers who are currently forced to deal with +problems that commonly surround nullablility. This makes large numbers of null fields tedious and +time-consuming to work with. It is often unclear how to handle partial results. - What elements are essential to having an adequate user experience? How many elements can fail before you From 70762e3bb15f8dd338911ec0f4d7ce9b4f2eaab3 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:32:53 -0700 Subject: [PATCH 12/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index ff3e99dd8..5029b3419 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -105,7 +105,7 @@ to fail as early as possible. ```graphql query GetBusinessName($encid: String!) { business(encid: $encid) { - name! <-- this! + name! } } ``` From 1bc2272a0e9eb542c3b9e941cc2b9d71147190ef Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:33:11 -0700 Subject: [PATCH 13/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 5029b3419..00faedee4 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -17,9 +17,9 @@ that is used to express when users can be certain that a value can or can never have become popular due to their ability to solve ergonomic problems in languages such as those surrounding `NullPointerException` in Java. -Codegen: Short for Code Generation, tools that exist for working with GraphQL which take queries and schemas as -input and output code in a language of choice that represents data that will result from those -queries. Codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings.](https://github.com/apollographql/apollo-tooling#code-generation) +Codegen: Short for Code Generation, tools that exist for working with GraphQL which accept a schema and a set of documents as +input, and output code in a language of choice that represents the data returned by those +operations. GraphQL codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings](https://github.com/apollographql/apollo-tooling#code-generation). ## πŸ“œ Problem Statement From f8590340322a0696303888ecd1aa7f858f8be242 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:34:11 -0700 Subject: [PATCH 14/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 00faedee4..e7f4af425 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -5,6 +5,7 @@ - [Sanae Rosen]() - Yelp Android - [Mark Larah](https://github.com/magicmark) - Yelp Web - [Alex Reilly]() - Yelp iOS +- [Wei Xue]() - Yelp iOS This RFC proposes creating a syntactical construct for client developers to express "nullability" in their queries. From ad403316f523ab3a2397426e4153750f0a665ba8 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 12:35:50 -0700 Subject: [PATCH 15/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index e7f4af425..f4241edd7 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -7,8 +7,7 @@ - [Alex Reilly]() - Yelp iOS - [Wei Xue]() - Yelp iOS -This RFC proposes creating a syntactical construct for client developers to -express "nullability" in their queries. +This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested in a query. ## Definitions From c9a925da2be9a74d4180b83f48a1cf86779e854a Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 14:27:06 -0700 Subject: [PATCH 16/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index f4241edd7..f88ced601 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -41,7 +41,7 @@ It is often unclear how to handle partial results. In Yelp's GraphQL schema, almost all object fields are nullable except for those with ID type. This adheres to what seems to be the [official best practice](https://graphql.org/learn/best-practices/#nullability). -This poses a problem for the mobile clients that use Apollo's codegen feature. The codegen provides +This poses a problem for GraphQL clients that use codegen. The codegen provides Swift/Kotlin types to represent queries used in the app. Nullable fields in the schema are represented as nullable properties on the resulting type, represented by `?` following the type name: ```graphql From 787190645bd6c7b93df01d3ec6b8a9ea6b545368 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 14:42:57 -0700 Subject: [PATCH 17/41] Responded to feedback Reworked some of the language to make it more generally applicable --- rfcs/QueryLevelNullability.md | 44 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index f88ced601..8663b02b2 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -12,18 +12,19 @@ This RFC proposes a syntactical construct for GraphQL clients to express the **n ## Definitions Nullability: A concept that exists across many programming languages (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) -that is used to express when users can be certain that a value can or can never be `null` +that is used to express when developers can be certain that a value can or can never be `null` (or the language equivalent). Nullability language constructs (eg `?` in Swift/Kotlin) have become popular due to their ability to solve ergonomic problems in languages such as those surrounding `NullPointerException` in Java. Codegen: Short for Code Generation, tools that exist for working with GraphQL which accept a schema and a set of documents as input, and output code in a language of choice that represents the data returned by those -operations. GraphQL codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings](https://github.com/apollographql/apollo-tooling#code-generation). +operations. GraphQL codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings](https://github.com/apollographql/apollo-tooling#code-generation) and [GraphQL Codegen by The Guild](https://www.graphql-code-generator.com/). ## πŸ“œ Problem Statement -GraphQL is a "nullable by default" language meaning that all properties are allowed to be `null`. +According to [best practice](https://graphql.org/learn/best-practices/#nullability) GraphQL is +a "nullable by default" language meaning that all properties are allowed to be `null`. This is in contrast to the two modern languages used on mobile clients, Swift and Kotlin, which are both non-null by default. In Swift and Kotlin, unless developers otherwise specify it, properties cannot be `null`. @@ -32,16 +33,7 @@ This mismatch creates some dissonance for developers who are currently forced to problems that commonly surround nullablility. This makes large numbers of null fields tedious and time-consuming to work with. -It is often unclear how to handle partial results. -- What elements are essential to having an adequate user experience? How many elements can fail before you - should just show an error message instead? -- When any combination of elements can fail, it is very hard to anticipate all edge cases and how the app - might look and work, including transitions to other screens. - -In Yelp's GraphQL schema, almost all object fields are nullable except for those with ID type. -This adheres to what seems to be the [official best practice](https://graphql.org/learn/best-practices/#nullability). - -This poses a problem for GraphQL clients that use codegen. The codegen provides +This poses a problem for the clients that use codegened models. For example Apollo codegen provides Swift/Kotlin types to represent queries used in the app. Nullable fields in the schema are represented as nullable properties on the resulting type, represented by `?` following the type name: ```graphql @@ -56,10 +48,9 @@ struct GetBusinessNameQuery.Data.Business { let name: String? } ``` -In many cases, the client should error if the business `name` is null. If codegen were out of the picture, -we would be able to throw an error at JSON response-parsing time if it's missing, or otherwise instantiate -a hand-written business object with a non-nullable `name` property. From that point on, all feature code -can happily assume that it has a non-null business name to work with. +In many cases, the client should error if the business `name` is null. While the schema can have the nullable +fields for valid reasons (such as federation), the client wants to decide if it accepts a null value for] +the result to simplify the client-side logic. ## πŸ§‘β€πŸ’» Proposed syntax @@ -80,8 +71,7 @@ write `Int("5")!`. If you do that, an exception will be thrown if the statement In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an error to the client rather than any data. -On web where codegen is not used, the client no longer needs to handle the case where expected fields are missing. -On mobile platforms where codegen is used, clients have full control over the nullability of the properties on the +On platforms where codegen is used, clients have full control over the nullability of the properties on the generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate various use-cases (e.g., where the business `name` _is_Β allowed to be nullable). @@ -99,6 +89,12 @@ if it is `null`. For example if you are trying to render an information page for be able to do that if the name field of the movie is missing. In that case it would be preferable to fail as early as possible. +It is often unclear how to handle partial results. +- What elements are essential to having an adequate developer experience? How many elements can fail before you + should just show an error message instead? +- When any combination of elements can fail, it is very hard to anticipate all edge cases and how the UI + might look and work, including transitions to other screens. + ### ✨ Examples #### Non-nullable field @@ -136,15 +132,14 @@ query { ## πŸ—³οΈ Alternatives considered -### Make Schema Fields Non-Nullable Instead -Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) -// We definitely want to flesh this out - -### Use a directive such as `@non-null` instead +### Use a directive such as `@nonNull` instead This is the alternative being used at some of the companies represented in this proposal for the time being. However this feels like a common enough need to call for a language feature. A single language feature rather than using directives as a workaround would enable more unified public tooling around GraphQL. +### Make Schema Fields Non-Nullable Instead +Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) + ### Write wrapper types that null-check fields This is the alternative being used at some of the companies represented in this proposal for the time being. It's quite labor intensive and the work is quite rote. It more or less undermines the purpose of @@ -163,7 +158,6 @@ query! { } } ``` -// TODO: This was rejected when it was proposed. Looking for more info as to why ## πŸ™… Syntax Non-goals From c6723cea448c7ef6649455d98abc790e61d71d5d Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 14 Apr 2021 14:47:26 -0700 Subject: [PATCH 18/41] Use non-internal PR link --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 8663b02b2..0c3e0b0eb 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -170,4 +170,4 @@ This syntax consciously does not cover the following use cases: not interested in covering that in this proposal. ## Implementation -GraphQL.js: https://github.yelpcorp.com/wxue/graphql-js +GraphQL.js: https://github.com/graphql/graphql-js/pull/2824 From 2e31037c6e4386901b05909ab37495b73ebb5888 Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Fri, 16 Apr 2021 16:01:33 -0700 Subject: [PATCH 19/41] Rework some definitions, problem statement, alternatives considered --- rfcs/QueryLevelNullability.md | 195 +++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 62 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 0c3e0b0eb..c3c0a4dd2 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -1,100 +1,158 @@ # RFC: Query Level Nullability -**Proposed by:** +**Proposed by:** + +- [Alex Reilly]() - Yelp iOS - [Liz Jakubowski](https://github.com/lizjakubowski) - Yelp iOS -- [Sanae Rosen]() - Yelp Android - [Mark Larah](https://github.com/magicmark) - Yelp Web -- [Alex Reilly]() - Yelp iOS +- [Sanae Rosen]() - Yelp Android - [Wei Xue]() - Yelp iOS -This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested in a query. +This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested +in a query. ## Definitions -Nullability: A concept that exists across many programming languages (eg [Swift](https://developer.apple.com/documentation/swift/optional), [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) -that is used to express when developers can be certain that a value can or can never be `null` -(or the language equivalent). Nullability language constructs (eg `?` in Swift/Kotlin) -have become popular due to their ability to solve ergonomic problems in languages -such as those surrounding `NullPointerException` in Java. +- **Nullability**. A feature of many programming languages (eg [Swift](https://developer.apple.com/documentation/swift/optional), + [Kotlin](https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types), [SQL](https://www.w3schools.com/sql/sql_notnull.asp)) + that is used to indicate whether or not a value can be `null`. -Codegen: Short for Code Generation, tools that exist for working with GraphQL which accept a schema and a set of documents as -input, and output code in a language of choice that represents the data returned by those -operations. GraphQL codegen tools exist for many platforms. As an example, [here's some info about Apollo's offerings](https://github.com/apollographql/apollo-tooling#code-generation) and [GraphQL Codegen by The Guild](https://www.graphql-code-generator.com/). + Nullability language constructs (e.g. `?` in Swift/Kotlin) have become popular due to their ability to solve ergonomic + problems in programming languages, such as those surrounding `NullPointerException` in Java. -## πŸ“œ Problem Statement +- **Codegen**. Short for "code generation", in this proposal refers to tools that generate code to facilitate using +GraphQL on the client. GraphQL codegen tooling exists for many platforms: + - [Apollo](https://github.com/apollographql/apollo-tooling#code-generation) has a code generator for Android (Kotlin) + and iOS (Swift) clients + - [The Guild](https://www.graphql-code-generator.com/) has a TypeScript code generator for web clients -According to [best practice](https://graphql.org/learn/best-practices/#nullability) GraphQL is -a "nullable by default" language meaning that all properties are allowed to be `null`. -This is in contrast to the two modern languages used on mobile clients, Swift and Kotlin, -which are both non-null by default. In Swift and Kotlin, unless developers otherwise -specify it, properties cannot be `null`. + GraphQL codegen tools typically accept a schema and a set of documents as input, and output code in a language of + choice that represents the data returned by those operations. -This mismatch creates some dissonance for developers who are currently forced to deal with -problems that commonly surround nullablility. This makes large numbers of null fields tedious and -time-consuming to work with. + For example, the Apollo iOS codegen tool generates Swift types to represent each query document, as well as model types + representing the data returned from those queries. Notably, a nullable field in the schema becomes an `Optional` + property on the generated Swift model type, represented by `?` following the type name. -This poses a problem for the clients that use codegened models. For example Apollo codegen provides -Swift/Kotlin types to represent queries used in the app. Nullable fields in the schema are represented -as nullable properties on the resulting type, represented by `?` following the type name: -```graphql -query GetBusinessName($encid: String!) { - business(encid: $encid) { - name + In the example below, the `Business` schema type has a nullable field called `name`. + ```graphql + # Schema + type Business { + # The unique identifier for the business (non-nullable) + id: String! + + # The name of the business (nullable) + name: String } -} -``` -```swift -struct GetBusinessNameQuery.Data.Business { - let name: String? -} -``` -In many cases, the client should error if the business `name` is null. While the schema can have the nullable -fields for valid reasons (such as federation), the client wants to decide if it accepts a null value for] -the result to simplify the client-side logic. + + # Document + query GetBusinessName($id: String!) { + business(id: $id) { + name + } + } + ``` + At build time, Apollo generates the following Swift code (note: the code has been shortened for clarity). + ```swift + struct GetBusinessNameQuery { + let id: String + + struct Data { + let business: Business? + + struct Business { + /// Since the `Business.name` schema field is nullable, the corresponding codegen Swift property is `Optional` + let name: String? + } + } + } + ``` + The query can then be fetched, and the resulting data handled, as follows: + ```swift + GraphQL.fetch(query: GetBusinessNameQuery(id: "foo"), completion: { result in + guard case let .success(gqlResult) = result, let business = gqlResult.data.business else { return } + + // Often, the client needs to provide a default value in case `name` is `null`. + print(business?.name ?? "null") + } + ``` + +## πŸ“œ Problem Statement + +The two modern languages used on mobile clients, Swift and Kotlin, are both non-null by default. By contrast, in a +GraphQL type system, every field is nullable by default. From the [official GraphQL best practice](https://graphql.org/learn/best-practices/#nullability): + +> This is because there are many things that can go awry in a networked service backed by databases and other +> services. A database could go down, an asynchronous action could fail, an exception could be thrown. Beyond +> simply system failures, authorization can often be granular, where individual fields within a request can +> have different authorization rules. + +This discrepancy means that developers using strongly-typed languages must perform `null` checks anywhere that GraphQL +codegen types are used, significantly decreasing the benefits of codegen. In some cases, the codegen types may be so +complex that a new model type which encapsulates the `null` handling is written manually. + +While the schema can have nullable fields for valid reasons (such as federation), in some cases the client wants to decide +if it accepts a `null` value for the result to simplify the client-side logic. In addition, a syntax for this concept +would allow codegen tooling to generate model types that are more ergonomic to work with, since the fields would have the +desired nullability. ## πŸ§‘β€πŸ’» Proposed syntax -It would make more sense if the client could express that `name` must be non-null _in the query itself_: +The client can express that a schema field is required using the `!` syntax: ```graphql query GetBusinessName($encid: String!) { business(encid: $encid) { - name! <-- this! + name! } } ``` Semantically the GraphQL `!` operator is nearly identical to its counterpart in Swift (also represented by `!`) which is -referred to as the "force unwrap operator". In Swift, for example, you can cast a string to an integer with `Int("5")` -but the string being cast may not be a valid number, so that statement will return `null` rather than an integer if the -string cannot be turned into an integer. If you want to ensure that the statement does not return `null` you can instead -write `Int("5")!`. If you do that, an exception will be thrown if the statement would return `null`. +referred to as the "force unwrap operator". -In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an -error to the client rather than any data. +In Swift, for example, you can cast a string to an integer with `Int("5")` but the string being cast may not be a valid +number, so that statement will evaluate to `null` rather than an integer if the string cannot be cast to an integer. If +you want to ensure that the statement does not return `null` you can instead write `Int("5")!`. If you do that, an +exception will be thrown if the statement would evaluate to `null`. -On platforms where codegen is used, clients have full control over the nullability of the properties on the -generated types. Since nullability is expressed in the query rather than the schema, it's flexible enough to accommodate -various use-cases (e.g., where the business `name` _is_Β allowed to be nullable). +In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an error +to the client rather than any data. ### `!` We have chosen `!` because `!` is already being used in the GraphQL spec to indicate that a field is non-nullable. Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which both use similar syntax to indicate -"throw an exception if this value is `null`". +"throw an exception if this value is `null`". ### Use cases #### When a field is necessary to the function of the client -There are times where a field can be `null`, but a feature that queries for the field will not function -if it is `null`. For example if you are trying to render an information page for a movie, you won't -be able to do that if the name field of the movie is missing. In that case it would be preferable -to fail as early as possible. + +Expressing nullability in the query, as opposed to the schema, offers the client more flexibility and control over +whether or not an error is thrown. + +There are cases where a field is `nullable`, but a feature that fetches the field will not function if it is `null`. +For example if you are trying to render an information page for a movie, you won't be able to do that if the name field +of the movie is missing. In that case it would be preferable to fail as early as possible. + +According to the official GraphQL best practice, this field should be non-nullable: + +> When designing a GraphQL schema, it's important to keep in mind all the problems that could go wrong and if "null" is +> an appropriate value for a failed field. Typically it is, but occasionally, it's not. In those cases, use non-null +> types to make that guarantee. + +However, the same field may not be "vital" for every feature in the application that fetches it. Marking the field as +non-null in the schema would result in those other features erroring unnecessarily whenever the field is "null". + +#### When handling partial results It is often unclear how to handle partial results. + - What elements are essential to having an adequate developer experience? How many elements can fail before you should just show an error message instead? - When any combination of elements can fail, it is very hard to anticipate all edge cases and how the UI might look and work, including transitions to other screens. +Implementing these decisions significantly complicates the client-side logic for handling query results. + ### ✨ Examples #### Non-nullable field @@ -106,16 +164,20 @@ query GetBusinessName($encid: String!) { } ``` would codegen to the following type on iOS. - ```swift -struct GetBusinessNameQuery.Data.Business { - let name: String // lack of `?` indicates that `name` will never be `null` +struct GetBusinessNameQuery { + struct Data { + struct Business { + /// Lack of `?` indicates that `name` will never be `null` + let name: String + } + } } ``` #### Non-nullable object with nullable fields ```graphql -query { +query GetBusinessReviews { business(id: 4) { reviews! { rating @@ -126,16 +188,25 @@ query { ``` ## βœ… RFC Goals + - Non-nullable syntax that is based off of syntax that developers will already be familiar with +- Enable GraphQL codegen tools to generate more ergonomic types ## 🚫 RFC Non-goals ## πŸ—³οΈ Alternatives considered -### Use a directive such as `@nonNull` instead -This is the alternative being used at some of the companies represented in this proposal for the time being. -However this feels like a common enough need to call for a language feature. A single language feature -rather than using directives as a workaround would enable more unified public tooling around GraphQL. +### An official `@nonNull` directive + +This solution offers the same benefits as the proposed solution. Since many GraphQL codegen tools already support the `@skip` and `@include` directives, this solution likely has a faster turnaround. + +### Use a custom `@nonNull` directive + +This is an alternative being used at some of the companies represented in this proposal for the time being. + +While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients that rely on codegen, since codegen types typically cannot be customized based on a custom directive. + +This feels like a common enough need to call for a language feature. A single language feature would enable more unified public tooling around GraphQL. ### Make Schema Fields Non-Nullable Instead Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) From c7764d5710081c161cc402a046870e8bf3b9ced4 Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Fri, 16 Apr 2021 16:53:18 -0700 Subject: [PATCH 20/41] fixups --- rfcs/QueryLevelNullability.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index c3c0a4dd2..106eb18d6 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -69,7 +69,7 @@ GraphQL on the client. GraphQL codegen tooling exists for many platforms: The query can then be fetched, and the resulting data handled, as follows: ```swift GraphQL.fetch(query: GetBusinessNameQuery(id: "foo"), completion: { result in - guard case let .success(gqlResult) = result, let business = gqlResult.data.business else { return } + guard case let .success(gqlResult) = result, let business = gqlResult.data?.business else { return } // Often, the client needs to provide a default value in case `name` is `null`. print(business?.name ?? "null") @@ -92,15 +92,15 @@ complex that a new model type which encapsulates the `null` handling is written While the schema can have nullable fields for valid reasons (such as federation), in some cases the client wants to decide if it accepts a `null` value for the result to simplify the client-side logic. In addition, a syntax for this concept -would allow codegen tooling to generate model types that are more ergonomic to work with, since the fields would have the -desired nullability. +would allow codegen tooling to generate model types that are more ergonomic to work with, since the since the model +type's properties would have the desired nullability ## πŸ§‘β€πŸ’» Proposed syntax -The client can express that a schema field is required using the `!` syntax: +The client can express that a schema field is required using the `!` syntax in the query definition: ```graphql -query GetBusinessName($encid: String!) { - business(encid: $encid) { +query GetBusinessName($id: String!) { + business(id: $id) { name! } } @@ -157,8 +157,8 @@ Implementing these decisions significantly complicates the client-side logic for #### Non-nullable field ```graphql -query GetBusinessName($encid: String!) { - business(encid: $encid) { +query GetBusinessName($id: String!) { + business(id: $id) { name! } } @@ -166,10 +166,14 @@ query GetBusinessName($encid: String!) { would codegen to the following type on iOS. ```swift struct GetBusinessNameQuery { + let id: String + struct Data { + let business: Business? + struct Business { /// Lack of `?` indicates that `name` will never be `null` - let name: String + let name: String? } } } From 1c20e26010664110d370c6ace3e9406bdf1ac145 Mon Sep 17 00:00:00 2001 From: Young Kim Date: Sun, 18 Apr 2021 22:20:22 -0700 Subject: [PATCH 21/41] Add Young Min Kim to the proposal (#3) --- rfcs/QueryLevelNullability.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 0c3e0b0eb..c7db0139a 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -6,6 +6,7 @@ - [Mark Larah](https://github.com/magicmark) - Yelp Web - [Alex Reilly]() - Yelp iOS - [Wei Xue]() - Yelp iOS +- [Young Min Kim](https://github.com/aprilrd) - Netflix UI This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested in a query. From e9cb0768e70a3e0182dfe5bbc7f33b53d135dcd0 Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Mon, 19 Apr 2021 11:25:45 -0700 Subject: [PATCH 22/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 106eb18d6..003d0be66 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -91,7 +91,7 @@ codegen types are used, significantly decreasing the benefits of codegen. In som complex that a new model type which encapsulates the `null` handling is written manually. While the schema can have nullable fields for valid reasons (such as federation), in some cases the client wants to decide -if it accepts a `null` value for the result to simplify the client-side logic. In addition, a syntax for this concept +if it accepts a `null` value for the result to simplify the client-side logic. In addition, syntax for this concept would allow codegen tooling to generate model types that are more ergonomic to work with, since the since the model type's properties would have the desired nullability From 7255a8d07b243569230ee43434983137dbffb3e9 Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Mon, 19 Apr 2021 11:27:24 -0700 Subject: [PATCH 23/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 003d0be66..84ebb566e 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -93,7 +93,7 @@ complex that a new model type which encapsulates the `null` handling is written While the schema can have nullable fields for valid reasons (such as federation), in some cases the client wants to decide if it accepts a `null` value for the result to simplify the client-side logic. In addition, syntax for this concept would allow codegen tooling to generate model types that are more ergonomic to work with, since the since the model -type's properties would have the desired nullability +type's properties would have the desired nullability. ## πŸ§‘β€πŸ’» Proposed syntax From 254fd85819818fc2ec8b0aaef905cc0d0f712ac1 Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Mon, 19 Apr 2021 11:28:23 -0700 Subject: [PATCH 24/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 84ebb566e..fdf4e3a6b 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -97,7 +97,7 @@ type's properties would have the desired nullability. ## πŸ§‘β€πŸ’» Proposed syntax -The client can express that a schema field is required using the `!` syntax in the query definition: +The client can express that a schema field is required by using the `!` syntax in the query definition: ```graphql query GetBusinessName($id: String!) { business(id: $id) { From 3bd4f32449dbdcb8e45305577cb62b70ffa7098a Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Mon, 19 Apr 2021 11:41:40 -0700 Subject: [PATCH 25/41] Add work items section --- rfcs/QueryLevelNullability.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index c7db0139a..491a67f12 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -170,5 +170,19 @@ This syntax consciously does not cover the following use cases: that says "if a value would be `null` make it some other value instead". We are not interested in covering that in this proposal. -## Implementation -GraphQL.js: https://github.com/graphql/graphql-js/pull/2824 +## Work Items +Patches that will need to be made if this proposal is accepted. The +[RFC proposal process](https://github.com/graphql/graphql-spec/blob/main/CONTRIBUTING.md) +requires that a proof of concept is implemented in a GraphQL library. As work items are completed, +PRs will be linked here. +- Spec Changes +- Official Libraries + - GraphQL.js: https://github.com/graphql/graphql-js/pull/2824 +- 3rd Party Libararies + - [Apollo Android](https://github.com/apollographql/apollo-android) + - Code Gen + - Cache + - [Apollo iOS](https://github.com/apollographql/apollo-ios) + - Code Gen + - Cache + - [GraphQL Code Generator by The Guild](https://github.com/dotansimha/graphql-code-generator) From 96d0bee1a5fd422f11230c0204841456e3113e8b Mon Sep 17 00:00:00 2001 From: Liz Jakubowski Date: Mon, 19 Apr 2021 11:49:09 -0700 Subject: [PATCH 26/41] PR feedback --- rfcs/QueryLevelNullability.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index fdf4e3a6b..b18d6bf18 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -173,7 +173,7 @@ struct GetBusinessNameQuery { struct Business { /// Lack of `?` indicates that `name` will never be `null` - let name: String? + let name: String } } } @@ -200,15 +200,18 @@ query GetBusinessReviews { ## πŸ—³οΈ Alternatives considered -### An official `@nonNull` directive +### A `@nonNull` official directive This solution offers the same benefits as the proposed solution. Since many GraphQL codegen tools already support the `@skip` and `@include` directives, this solution likely has a faster turnaround. -### Use a custom `@nonNull` directive +### A `@nonNull` custom directive This is an alternative being used at some of the companies represented in this proposal for the time being. -While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients that rely on codegen, since codegen types typically cannot be customized based on a custom directive. +While this solution simplifies some client-side logic, it does not meaningfully improve the developer experience for clients. + +* The cache implementations of GraphQL client libraries also need to understand the custom directive to behave correctly. Currently, when a client library caches a null field based on an operation without a directive, it will return the null field for another operation with this directive. +* For clients that rely on codegen, codegen types typically cannot be customized based on a custom directive. See https://github.com/dotansimha/graphql-code-generator/discussions/5676 for an example. As a result, the optional codegen properties still need to be unwrapped in the code. This feels like a common enough need to call for a language feature. A single language feature would enable more unified public tooling around GraphQL. From e0a572bc467f34be152ed3d72ad55f32ff3da4ca Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Mon, 19 Apr 2021 14:40:38 -0700 Subject: [PATCH 27/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Liz Jakubowski --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 491a67f12..035a38c78 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -178,7 +178,7 @@ PRs will be linked here. - Spec Changes - Official Libraries - GraphQL.js: https://github.com/graphql/graphql-js/pull/2824 -- 3rd Party Libararies +- 3rd Party Libraries - [Apollo Android](https://github.com/apollographql/apollo-android) - Code Gen - Cache From 4aab23b17b8621914ef2bcabc99b24f60aa4c4ad Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Wed, 21 Apr 2021 14:18:54 -0700 Subject: [PATCH 28/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 035a38c78..38dd5566b 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -185,4 +185,7 @@ PRs will be linked here. - [Apollo iOS](https://github.com/apollographql/apollo-ios) - Code Gen - Cache + - [Apollo JS](https://github.com/apollographql/apollo-client) + - Code Gen + - Cache - [GraphQL Code Generator by The Guild](https://github.com/dotansimha/graphql-code-generator) From d891d96b6ccd9ca1feada0d554906c96fe2129ad Mon Sep 17 00:00:00 2001 From: Young Kim Date: Wed, 5 May 2021 21:35:29 -0700 Subject: [PATCH 29/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Stephen Spalding --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 97ae8bddf..76c026a00 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -93,7 +93,7 @@ complex that a new model type which encapsulates the `null` handling is written While the schema can have nullable fields for valid reasons (such as federation), in some cases the client wants to decide if it accepts a `null` value for the result to simplify the client-side logic. In addition, syntax for this concept -would allow codegen tooling to generate model types that are more ergonomic to work with, since the since the model +would allow codegen tooling to generate model types that are more ergonomic to work with, since the model type's properties would have the desired nullability. ## πŸ§‘β€πŸ’» Proposed syntax From 54a06eb2de98444a8ffe265714a41e9416844e10 Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 21:47:38 -0700 Subject: [PATCH 30/41] Formally specify Non-Null behavior This edit adds a distinct Behavior section that specifies how a client-side Non-Null operator functions This is separated from the Syntax proposal to avoid confusion. --- rfcs/QueryLevelNullability.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 97ae8bddf..e030a1c07 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -8,6 +8,7 @@ - [Sanae Rosen]() - Yelp Android - [Wei Xue]() - Yelp iOS - [Young Min Kim](https://github.com/aprilrd) - Netflix UI +- [Stephen Spalding](https://github.com/fotoetienne) - Netflix GraphQL Server Infrastructure This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested in a query. @@ -96,7 +97,29 @@ if it accepts a `null` value for the result to simplify the client-side logic. I would allow codegen tooling to generate model types that are more ergonomic to work with, since the since the model type's properties would have the desired nullability. -## πŸ§‘β€πŸ’» Proposed syntax + +## πŸ§‘β€πŸ’» Proposed Solution + +A client specified Non-Null designator. + +## 🎬 Behavior + +The proposed query-side Non-Null designator would have identical semantics as the current +SDL-defined [Non-Null](https://spec.graphql.org/June2018/#sec-Errors-and-Non-Nullability). Specifically: + + - If the result of resolving a field is null (either because the function to resolve the field returned null +or because an error occurred), and that field is of a Non-Null type, +**or the operation specifies this field as Non-Null**, +then a field error is thrown. The error must be added to the "errors" list in the response. + + - Since Non-Null type fields cannot be null, field errors are propagated to be handled by the parent field. +If the parent field may be null then it resolves to null, otherwise the field error +is further propagated to its parent field. + + - If all fields from the root of the request to the source of the field error return Non-Null types or are + specified as Non-Null in the operation, then the "data" entry in the response should be null. + +## ✏️ Proposed syntax The client can express that a schema field is required by using the `!` syntax in the query definition: ```graphql @@ -114,8 +137,7 @@ number, so that statement will evaluate to `null` rather than an integer if the you want to ensure that the statement does not return `null` you can instead write `Int("5")!`. If you do that, an exception will be thrown if the statement would evaluate to `null`. -In GraphQL, the `!` operator will act similarly. In the case that `name` does not exist, the query will return an error -to the client rather than any data. +In GraphQL, the `!` operator will act similarly. ### `!` @@ -123,7 +145,7 @@ We have chosen `!` because `!` is already being used in the GraphQL spec to indi Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which both use similar syntax to indicate "throw an exception if this value is `null`". -### Use cases +## Use cases #### When a field is necessary to the function of the client From e6fc0ed8d01d78c8ab9549f75faab1693e559ff1 Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 22:40:30 -0700 Subject: [PATCH 31/41] Added discussion on partial results --- rfcs/QueryLevelNullability.md | 59 ++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index e030a1c07..f36383d63 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -239,7 +239,64 @@ While this solution simplifies some client-side logic, it does not meaningfully This feels like a common enough need to call for a language feature. A single language feature would enable more unified public tooling around GraphQL. ### Make Schema Fields Non-Nullable Instead -Discussion on [this topic can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) + +It is intuitive that one should simply mark fields that are not intended to be null as non-null in the schema. +For example, in the following GraphQL schema: + +```graphql + type Business { + name: String + isStarred: Boolean + } +``` + +If we intend to always have a title and rating for a Movie, it may be tempting to mark these fields as Non-Null: + +```graphql + type Business { + name: String! + isStarred: Boolean! + } +``` + +Marking Schema fields as non-null can have particular problems in a distributed environment where there is a possibility +of partial failure regardless of whether the field is intended to have null as a valid state. + +When a non-nullable field results in null, the GraphQL server will recursively step through the field’s ancestors to find the next nullable field. In the following GraphQL response: + +```json +{ + "data": { + "business": { + "name": "The French Laundry" + "isStarred": false + } + } +} +``` + +If isStarred is non-nullable but returns null and business is nullable, the result will be: + +```json +{ + "data": { + "business": null + } +} +``` + +Even if name returns valid results, the response would no longer provide this data. If business is non-nullable, the response will be: +```json +{ + "data": null +} +``` + +In the case that the service storing user stars is unavailable, the UI may want to go ahead and render the component +without a star (effectively defaulting isStarred to false). Non-Null in the schema makes it impossible for the client +to receive partial results from the server, and thus potentially forces the entire component to fail rendering. + +More discussion on [when to use non-null can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) ### Write wrapper types that null-check fields This is the alternative being used at some of the companies represented in this proposal for the time being. From 550ffebf92e7cda5ee7bb8346f19e11ba1db1109 Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 23:07:33 -0700 Subject: [PATCH 32/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index f36383d63..88e366a6f 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -6,9 +6,9 @@ - [Liz Jakubowski](https://github.com/lizjakubowski) - Yelp iOS - [Mark Larah](https://github.com/magicmark) - Yelp Web - [Sanae Rosen]() - Yelp Android +- [Stephen Spalding](https://github.com/fotoetienne) - Netflix GraphQL Server Infrastructure - [Wei Xue]() - Yelp iOS - [Young Min Kim](https://github.com/aprilrd) - Netflix UI -- [Stephen Spalding](https://github.com/fotoetienne) - Netflix GraphQL Server Infrastructure This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested in a query. From 8515128b02f6db7863447c2b04f1841e9234a604 Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 23:17:25 -0700 Subject: [PATCH 33/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 88e366a6f..6bf4fb953 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -250,7 +250,7 @@ For example, in the following GraphQL schema: } ``` -If we intend to always have a title and rating for a Movie, it may be tempting to mark these fields as Non-Null: +If we intend to always have a title and rating for a Business, it may be tempting to mark these fields as Non-Null: ```graphql type Business { From c60e590cf6eb4f8edb36870805b70dcd9e9cd96a Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 23:17:48 -0700 Subject: [PATCH 34/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Alex Reilly --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 6bf4fb953..182056734 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -259,7 +259,7 @@ If we intend to always have a title and rating for a Business, it may be temptin } ``` -Marking Schema fields as non-null can have particular problems in a distributed environment where there is a possibility +Marking Schema fields as non-null can introduce particular problems in a distributed environment where there is a possibility of partial failure regardless of whether the field is intended to have null as a valid state. When a non-nullable field results in null, the GraphQL server will recursively step through the field’s ancestors to find the next nullable field. In the following GraphQL response: From e1e363a9c5fce9ab341de81ecc5623d79c3fda49 Mon Sep 17 00:00:00 2001 From: Stephen Spalding Date: Wed, 5 May 2021 23:25:38 -0700 Subject: [PATCH 35/41] typos --- rfcs/QueryLevelNullability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 182056734..5ba52a28c 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -250,7 +250,7 @@ For example, in the following GraphQL schema: } ``` -If we intend to always have a title and rating for a Business, it may be tempting to mark these fields as Non-Null: +If we intend to always have a name and isStarred for a Business, it may be tempting to mark these fields as Non-Null: ```graphql type Business { @@ -268,7 +268,7 @@ When a non-nullable field results in null, the GraphQL server will recursively s { "data": { "business": { - "name": "The French Laundry" + "name": "The French Laundry", "isStarred": false } } From 6e5409357ff61380c011b6d7770bfef5f211fe17 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Fri, 7 May 2021 10:51:24 -0700 Subject: [PATCH 36/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index d7f9de900..57196d8ea 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -298,6 +298,10 @@ to receive partial results from the server, and thus potentially forces the enti More discussion on [when to use non-null can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) +Additionally, marking a field non-null is not possible in every use case. For example, when a developer is using a 3rd-party API such as +[Github's GraphQL API](https://docs.github.com/en/graphql), they won't be able to alter Github's schema, but they may still +want to have certain fields be non-nullable in their application. + ### Write wrapper types that null-check fields This is the alternative being used at some of the companies represented in this proposal for the time being. It's quite labor intensive and the work is quite rote. It more or less undermines the purpose of From 6d5d16ca1426d92a5f68fffaab9e9ad159ab5796 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Fri, 7 May 2021 10:52:59 -0700 Subject: [PATCH 37/41] Update QueryLevelNullability.md --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 57196d8ea..d811bd2d9 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -299,7 +299,7 @@ to receive partial results from the server, and thus potentially forces the enti More discussion on [when to use non-null can be found here](https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8) Additionally, marking a field non-null is not possible in every use case. For example, when a developer is using a 3rd-party API such as -[Github's GraphQL API](https://docs.github.com/en/graphql), they won't be able to alter Github's schema, but they may still +[Github's GraphQL API](https://docs.github.com/en/graphql) they won't be able to alter Github's schema, but they may still want to have certain fields be non-nullable in their application. ### Write wrapper types that null-check fields From e32b1f254452eaa4058c98d5b887bc8c81ae13cf Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Fri, 7 May 2021 21:37:42 -0700 Subject: [PATCH 38/41] Cleanup after behavior was clarified --- rfcs/QueryLevelNullability.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index d811bd2d9..d8da02971 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -1,4 +1,4 @@ -# RFC: Query Level Nullability +# RFC: Operation Level Nullability **Proposed by:** @@ -10,8 +10,7 @@ - [Wei Xue]() - Yelp iOS - [Young Min Kim](https://github.com/aprilrd) - Netflix UI -This RFC proposes a syntactical construct for GraphQL clients to express the **nullability** of schema fields requested -in a query. +This RFC proposes a syntactical construct for GraphQL clients to express that fields in an operation are **non-null**. ## Definitions @@ -100,7 +99,7 @@ type's properties would have the desired nullability. ## πŸ§‘β€πŸ’» Proposed Solution -A client specified Non-Null designator. +A client specified non-null designator. ## 🎬 Behavior @@ -129,15 +128,6 @@ query GetBusinessName($id: String!) { } } ``` -Semantically the GraphQL `!` operator is nearly identical to its counterpart in Swift (also represented by `!`) which is -referred to as the "force unwrap operator". - -In Swift, for example, you can cast a string to an integer with `Int("5")` but the string being cast may not be a valid -number, so that statement will evaluate to `null` rather than an integer if the string cannot be cast to an integer. If -you want to ensure that the statement does not return `null` you can instead write `Int("5")!`. If you do that, an -exception will be thrown if the statement would evaluate to `null`. - -In GraphQL, the `!` operator will act similarly. ### `!` From b2652386ef9bb8a9135f6875cc5be30b1493a5e5 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Fri, 7 May 2021 21:42:44 -0700 Subject: [PATCH 39/41] Remove a few instances of query when they should be operation --- rfcs/QueryLevelNullability.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index d8da02971..10be0affc 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -30,7 +30,7 @@ GraphQL on the client. GraphQL codegen tooling exists for many platforms: GraphQL codegen tools typically accept a schema and a set of documents as input, and output code in a language of choice that represents the data returned by those operations. - For example, the Apollo iOS codegen tool generates Swift types to represent each query document, as well as model types + For example, the Apollo iOS codegen tool generates Swift types to represent each operation document, as well as model types representing the data returned from those queries. Notably, a nullable field in the schema becomes an `Optional` property on the generated Swift model type, represented by `?` following the type name. @@ -120,7 +120,7 @@ is further propagated to its parent field. ## ✏️ Proposed syntax -The client can express that a schema field is required by using the `!` syntax in the query definition: +The client can express that a schema field is required by using the `!` syntax in the operation definition: ```graphql query GetBusinessName($id: String!) { business(id: $id) { @@ -139,7 +139,7 @@ Incidentally the same precedent exists in Swift (`!`) and Kotlin (`!!`) which bo #### When a field is necessary to the function of the client -Expressing nullability in the query, as opposed to the schema, offers the client more flexibility and control over +Expressing nullability in the operation, as opposed to the schema, offers the client more flexibility and control over whether or not an error is thrown. There are cases where a field is `nullable`, but a feature that fetches the field will not function if it is `null`. From da52795e92d7030bc6010e55612cedde2e607575 Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Fri, 7 May 2021 21:54:55 -0700 Subject: [PATCH 40/41] revert non-null --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 10be0affc..1c0012d97 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -99,7 +99,7 @@ type's properties would have the desired nullability. ## πŸ§‘β€πŸ’» Proposed Solution -A client specified non-null designator. +A client specified Non-Null designator. ## 🎬 Behavior From 844248ce0ee1a0454dea3a6a9e38fce21f99cadf Mon Sep 17 00:00:00 2001 From: Alex Reilly Date: Thu, 13 May 2021 14:37:52 -0700 Subject: [PATCH 41/41] Update rfcs/QueryLevelNullability.md Co-authored-by: Wei Xue --- rfcs/QueryLevelNullability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/QueryLevelNullability.md b/rfcs/QueryLevelNullability.md index 1c0012d97..23786d11f 100644 --- a/rfcs/QueryLevelNullability.md +++ b/rfcs/QueryLevelNullability.md @@ -7,7 +7,7 @@ - [Mark Larah](https://github.com/magicmark) - Yelp Web - [Sanae Rosen]() - Yelp Android - [Stephen Spalding](https://github.com/fotoetienne) - Netflix GraphQL Server Infrastructure -- [Wei Xue]() - Yelp iOS +- [Wei Xue](https://github.com/xuewei8910) - Yelp iOS - [Young Min Kim](https://github.com/aprilrd) - Netflix UI This RFC proposes a syntactical construct for GraphQL clients to express that fields in an operation are **non-null**.