Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@semanticNonNull/@catch: default levels to [0] instead of null #42

Merged
merged 6 commits into from
Jan 29, 2024

Conversation

martinbonnin
Copy link
Contributor

@martinbonnin martinbonnin commented Jan 12, 2024

Because this is bumping the version to 0.2, it's a new file but you can see the diff in this commit

Not anymore because there have been additional changes, you'll have to read the full new text, sorry!

```

`field` is used for client applications that do not control the base schema and must use type extensions.
When used on a field definition, `field` must be null.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to the main change in this PR, but...

I'm guessing you considered this already, but it would be possible to let regular GraphQL schema validation enforce this invariant for us by making a separate directive for the case where you specify a field, and then have that directive require a field be specified, and removing the argument from the field version.

As it stands we need ad-hoc validation that you do specify a field when used on a type, and that you don't specify a field when used on a field.

directive @semanticNonNull(level: Int = 0) repeatable on FIELD_DEFINITION
directive @semanticNonNullField(field: String!, level: Int = 0) repeatable on OBJECT

That said, I can see that having two directives is also cumbersome.

Copy link
Contributor Author

@martinbonnin martinbonnin Jan 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. The less custom validation I have to do, the happier I am. @semanticNonNullField(field: String!) feels like a mouthful of "fields" though. Other suggestions:

extend type User @semanticNonNullField(name: "friends", level: 0)
extend type User @addSemanticNonNull(forField: "friends", level: 0)

Also thinking about this, we could also remove the repeatable from the regular @semanticNonNull:

# pass all your levels all at once
directive @semanticNonNull(levels: [Int] = [0]) on FIELD_DEFINITION
# this ones needs to be repeatable for different fields. Which means we need validation that different directives for the same field do not conflict but 🤷 
directive @semanticNonNullField(name: String, levels: [Int] = [0]) repeatable on OBJECT | INTERFACE

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if we make levels plural, then we can remove the repeatable for the FIELD_DEFINITION variant. @addSemanticNonNull seem fine!

So the remaining validation we need to do:

  1. The field/level(s) being referenced exist and are currently nullable
  2. For @addSemanticNonNull, validate that the field exists
  3. Validate type compatibility with interfaces

In theory you could end up with a type getting marked as semantically non null in multiple ways. Probably that's okay? As long as it does not confuse tools:

type User @addSemanticNonNull(forField: "name") {
  name: String @semanticNonNull
}

Or

type User {
  friends: [User] @semanticNonNull(levels: [1,1,1,])
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory you could end up with a type getting marked as semantically non null in multiple ways.

More validation to be done but I think it's ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name nit: I find the word add to be a bit off for a directive because in a way all directives add something. semanticNonNullField works better for me but not a strong opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree directives are declarative and shouldn't start with a verb. I settled with @semanticNonNullField(name: String, levels: [Int]) in ba09b50, let me know what you think.

Also nitpick but it should really be @semanticNonNullPosition(field: String, levels: [Int]) but the spec uses field instead of position (or something else) in a lot of places so I kept Field for consistency even if it's a bit wrong.

nullability/v0.2/nullability-v0.2.graphql Outdated Show resolved Hide resolved
```

`field` is used for client applications that do not control the base schema and must use type extensions.
When used on a field definition, `field` must be null.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name nit: I find the word add to be a bit off for a directive because in a way all directives add something. semanticNonNullField works better for me but not a strong opinion.

@martinbonnin martinbonnin changed the title @semanticNonNull/@catch: default level to 0 instead of null @semanticNonNull/@catch: default levels to [0] instead of null Jan 15, 2024
Copy link

@captbaritone captbaritone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One optional minor nit, but otherwise looks good!

nullability/v0.2/nullability-v0.2.graphql Outdated Show resolved Hide resolved
@martinbonnin martinbonnin merged commit 2a22ccf into main Jan 29, 2024
7 checks passed
@martinbonnin martinbonnin deleted the level-0 branch January 29, 2024 09:49
@martinbonnin
Copy link
Contributor Author

Something I realized by making @catch non repeatable, we lose the possibility to have different CatchTo for different levels:

{
  user {
    # this is not possible
    # throw if the list errors but recover from item errors
    friends @catch(to: THROW, levels: [0]) @catch(to: NULL, levels: [1])
  }
}

Maybe not too bad but maybe the levels in @catch should behave differently than in @semanticNonNull

facebook-github-bot pushed a commit to facebook/relay that referenced this pull request Feb 5, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants