Skip to content

Commit

Permalink
Experimental support for @semanticNonNull (#4601)
Browse files Browse the repository at this point in the history
Summary:
This PR adds experimental support for `semanticNonNull` as described in apollographql/specs#42. Which is part of a broader effort to explore semantic nullability in GraphQL as explored in [RFC: SemanticNonNull type (null only on error) ](graphql/graphql-spec#1065). This directive-based approach should allow us to experiment with the concepts and identify issues as we work to understand the viability of semantic nullability in GraphQL.

## Experimental

As this is still an experimental implementation, it's designed to be minimally invasive rather than ideal in terms of architecture or performance. As the feature/RFCs stabilize I would imagine we would bake this into the schema crate and data structures as a first class concept.

This flag will not be broadly safe to enable in Relay since by default fields that are null due to error are still surfaced to the user. This is only safe to enable if:

1. Your network layer discards all payloads that have any field errors
2. You enable our [explicit error handling feature](#4416), which is still itself experimental.

## Missing Pieces

- [ ] Documentation about how to use this feature (purposeful, since this is experimental)
- [ ] Support for `semanticNonNullField` which allows a patching an existing type to define it's field's semantic nullability
- [ ] Validation
  - [ ] Invalid use of `levels` will simply panic.
  - [ ] Uses of `semanticNonNullField` will simply be ignored
  - [ ] There is no schema validation ensuring interface types have type-compatible semantic nullability

Pull Request resolved: #4601

Test Plan:
```
cargo test
```

I also spun up a version of [`grats-relay-example`](https://github.com/captbaritone/grats-relay-example) using Grat's [experimental support for `semanticNonNull`](https://grats.capt.dev/docs/guides/strict-semantic-nullability/) and was able to see it working end to end.

https://github.com/facebook/relay/assets/162735/dc979a58-95f3-4e55-9d9b-577afdd798ca

Reviewed By: alunyov

Differential Revision: D53191255

Pulled By: captbaritone

fbshipit-source-id: c09333f2b9475315d81792d33947fd908001c021
  • Loading branch information
captbaritone authored and facebook-github-bot committed Feb 5, 2024
1 parent e88306a commit cebc7f0
Show file tree
Hide file tree
Showing 84 changed files with 1,339 additions and 78 deletions.
Expand Up @@ -87,7 +87,7 @@ fragment TestFragment on User
alias: None,
definition: WithLocation {
location: argument_definitions.graphql:161:171,
item: FieldID(518),
item: FieldID(523),
},
arguments: [],
directives: [],
Expand Down
Expand Up @@ -180,7 +180,7 @@ fragment F2 on Query @argumentDefinitions(
alias: None,
definition: WithLocation {
location: fragment_with_arguments_defaulting.graphql:342:352,
item: FieldID(518),
item: FieldID(523),
},
arguments: [],
directives: [],
Expand Down
Expand Up @@ -4,7 +4,7 @@ fragment Foo on Users {
id
}
==================================== ERROR ====================================
✖︎ Unknown type 'Users'. Did you mean `User` or `Query`?
✖︎ Unknown type 'Users'. Did you mean `User`, `Opera`, or `Query`?

unknown-fragment-type-suggestions.invalid.graphql:2:17
1 │ # expected-to-throw
Expand Down
Expand Up @@ -151,7 +151,7 @@ type Foo {
alias: None,
definition: WithLocation {
location: client-fields.graphql:226:238,
item: FieldID(518),
item: FieldID(523),
},
arguments: [],
directives: [],
Expand Down Expand Up @@ -228,7 +228,7 @@ type Foo {
alias: None,
definition: WithLocation {
location: client-fields.graphql:367:370,
item: FieldID(519),
item: FieldID(524),
},
arguments: [],
directives: [],
Expand All @@ -245,15 +245,15 @@ type Foo {
},
InlineFragment {
type_condition: Some(
Object(79),
Object(81),
),
directives: [],
selections: [
ScalarField {
alias: None,
definition: WithLocation {
location: client-fields.graphql:470:472,
item: FieldID(520),
item: FieldID(525),
},
arguments: [],
directives: [],
Expand All @@ -279,14 +279,14 @@ type Foo {
},
variable_definitions: [],
used_global_variables: [],
type_condition: Object(79),
type_condition: Object(81),
directives: [],
selections: [
ScalarField {
alias: None,
definition: WithLocation {
location: client-fields.graphql:526:528,
item: FieldID(520),
item: FieldID(525),
},
arguments: [],
directives: [],
Expand Down
Expand Up @@ -57,7 +57,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_directive_arg_variable.graphql:100:115,
item: FieldID(519),
item: FieldID(524),
},
arguments: [],
directives: [
Expand Down Expand Up @@ -104,7 +104,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_directive_arg_variable.graphql:160:170,
item: FieldID(521),
item: FieldID(526),
},
arguments: [],
directives: [],
Expand All @@ -115,7 +115,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_directive_arg_variable.graphql:181:203,
item: FieldID(520),
item: FieldID(525),
},
arguments: [],
directives: [
Expand Down
Expand Up @@ -55,7 +55,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_variable_arg.graphql:100:115,
item: FieldID(519),
item: FieldID(524),
},
arguments: [
Argument {
Expand Down Expand Up @@ -91,7 +91,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_variable_arg.graphql:151:161,
item: FieldID(521),
item: FieldID(526),
},
arguments: [],
directives: [],
Expand All @@ -102,7 +102,7 @@ extend type Query {
alias: None,
definition: WithLocation {
location: custom_scalar_variable_arg.graphql:172:194,
item: FieldID(520),
item: FieldID(525),
},
arguments: [
Argument {
Expand Down
13 changes: 13 additions & 0 deletions compiler/crates/relay-config/src/typegen_config.rs
Expand Up @@ -111,6 +111,18 @@ pub struct TypegenConfig {
/// of an union with the raw type, null and undefined.
#[serde(default)]
pub typescript_exclude_undefined_from_nullable_union: bool,

/// EXPERIMENTAL: If your environment is configured to handles errors out of band, either via
/// a network layer which discards responses with errors, or via enabling strict
/// error handling in the runtime, you can enable this flag to have Relay generate
/// non-null types for fields which are marked as semantically non-null in
/// the schema.
///
/// Currently semantically non-null fields must be specified in your schema
/// using the `@semanticNonNull` directive as specified in:
/// https://github.com/apollographql/specs/pull/42
#[serde(default)]
pub experimental_emit_semantic_nullability_types: bool,
}

impl Default for TypegenConfig {
Expand All @@ -125,6 +137,7 @@ impl Default for TypegenConfig {
no_future_proof_enums: Default::default(),
eager_es_modules: Default::default(),
typescript_exclude_undefined_from_nullable_union: Default::default(),
experimental_emit_semantic_nullability_types: Default::default(),
}
}
}
14 changes: 14 additions & 0 deletions compiler/crates/relay-test-schema/src/testschema.graphql
Expand Up @@ -1191,3 +1191,17 @@ type Settings {
type WithWrongViewer {
actor_key: Viewer
}

extend type Query {
opera: Opera
}

type Opera {
composer: User @semanticNonNull
cast: [Portrayal] @semanticNonNull(levels: [0, 1])
}

type Portrayal {
singer: User @semanticNonNull
character: String @semanticNonNull
}
Expand Up @@ -38,7 +38,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand All @@ -65,7 +65,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -43,7 +43,7 @@ fragment Foo_node on Node {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down Expand Up @@ -72,7 +72,7 @@ fragment Foo_node on Node {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -42,7 +42,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(520),
# field_id: FieldID(525),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -39,7 +39,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -30,7 +30,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -34,7 +34,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -33,7 +33,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -30,7 +30,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -34,7 +34,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand All @@ -57,7 +57,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down Expand Up @@ -115,7 +115,7 @@ fragment RefetchableClientEdgeQuery_Foo_user_best_friend on User @__ClientEdgeGe
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -32,7 +32,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand All @@ -54,7 +54,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down Expand Up @@ -110,7 +110,7 @@ fragment RefetchableClientEdgeQuery_Foo_user_best_friend on User @__ClientEdgeGe
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -45,7 +45,7 @@ fragment Foo_user on ClientUser {
{
...BestFriendFragment @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: Some(
Expand Down Expand Up @@ -74,7 +74,7 @@ fragment Foo_user on ClientUser {
{
...BestFriendFragment @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: Some(
Expand Down
Expand Up @@ -45,7 +45,7 @@ fragment Foo_user on ClientUser {
{
...BestFriendFragment @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand All @@ -72,7 +72,7 @@ fragment Foo_user on ClientUser {
{
...BestFriendFragment @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand Down
Expand Up @@ -39,7 +39,7 @@ fragment Foo_user on User {
{
...BestFriendResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(519),
# field_id: FieldID(524),
# import_path: "BestFriendResolver",
# import_name: None,
# field_alias: None,
Expand All @@ -48,7 +48,7 @@ fragment Foo_user on User {
# live: false,
# output_type_info: Composite(
# ResolverNormalizationInfo {
# inner_type: Object(79),
# inner_type: Object(81),
# plural: false,
# normalization_operation: WithLocation {
# location: <generated>:59:70,
Expand Down
Expand Up @@ -22,7 +22,7 @@ extend type User {
fragment Foo_user on User {
...PopStarNameResolverFragment_name @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "PopStarNameResolver",
# import_name: None,
# field_alias: Some(
Expand Down
Expand Up @@ -12,7 +12,7 @@ extend type User {
fragment Foo_user on User {
__id @__RelayResolverMetadata
# RelayResolverMetadata {
# field_id: FieldID(518),
# field_id: FieldID(523),
# import_path: "PopStarNameResolver",
# import_name: None,
# field_alias: None,
Expand Down

0 comments on commit cebc7f0

Please sign in to comment.