Skip to content

Commit 16b9ce2

Browse files
authored
Add @catch support for responseBased codegen (#6697) (#6698)
* Add `@catch` support for `responseBased` codegen (#6697) * hopefully unbreak integration tests
1 parent 5e809a7 commit 16b9ce2

File tree

19 files changed

+833
-78
lines changed

19 files changed

+833
-78
lines changed

docs/source/advanced/nullability.mdx

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -90,30 +90,51 @@ fun User(user: GetUserQuery.User) {
9090

9191
When there are a lot of fields, handling the `null` case on every one of them becomes really tedious.
9292

93-
Wouldn't it be nice if instead the UI could decide to handle errors more globally and display a general error if any field in an `User` fails?
94-
9593
Apollo Kotlin offers nullability directives to deal with this situation:
96-
* [`@semanticNonNull`](#semanticnonnull)
97-
* [`@catch`](#handle-errors-and-receive-partial-data-with-catch)
94+
* [`@semanticNonNull`](#semanticnonnull) (schema directive)
95+
* [`@catch`](#handle-errors-and-receive-partial-data-with-catch) (client directive)
9896

9997
These tools change the GraphQL default from "handle every field error" to "opt-in the errors you want to handle".
10098

101-
## Import the nullability directives
99+
## Enabling error aware parsing
102100

103-
Nullability directives are experimental. You need to import them using the [`@link` directive](https://specs.apollo.dev/link/v1.0/):
101+
To make the Apollo generated parsers aware of errors, import the [nullability directives](https://specs.apollo.dev/nullability/v0.4/) using the [`@link` directive](https://specs.apollo.dev/link/v1.0/):
104102

105103
```graphql
106104
extend schema @link(
107105
url: "https://specs.apollo.dev/nullability/v0.4",
108-
# Note: other directives are needed later on and added here for convenience
109106
import: ["@semanticNonNull", "@semanticNonNullField", "@catch", "CatchTo", "@catchByDefault"]
110107
)
111108
```
112109

113-
<Note>
110+
And define the default behavior when an error happens.
114111

115-
You will also need to opt in a default catch but more on that [later](#catchbydefault).
116-
</Note>
112+
You may catch the error and expose it as a `FieldResult`:
113+
114+
```graphql
115+
# Catch the error and expose it as a FieldResult<T> in the generated models.
116+
extend schema @catchByDefault(to: RESULT)
117+
```
118+
119+
or re-throw the error:
120+
121+
```graphql
122+
# Re-throw the error. If no parent field catches it, `response.exception` contains an instance of `ApolloGraphQLException`.
123+
extend schema @catchByDefault(to: THROW)
124+
```
125+
126+
or coerce the error to `null`, like the current GraphQL default:
127+
128+
```graphql
129+
# Coerce the error to null. The caller must read `response.errors` to disambiguate a null vs error field.
130+
extend schema @catchByDefault(to: NULL)
131+
```
132+
133+
Adding `@catchByDefault(to: NULL)` is a no-op for codegen that unlocks using `@catch` in your operations.
134+
135+
Because errors can never happen on non-null fields (`String!` and others), `@catchByDefault` only influences the nullable fields in your schema.
136+
137+
Some of those fields are only nullable for error reasons. For those cases, Apollo Kotlin supports `@semanticNonNull`.
117138

118139
## `@semanticNonNull`
119140

@@ -149,7 +170,7 @@ type User {
149170
}
150171
```
151172

152-
With `@semanticNonNull`, a frontend developer knows that a given field will never be null in regular operation and can therefore act accordingly. No need to guess anymore!
173+
With `@semanticNonNull`, a frontend developer knows that a given field will never be null in regular operation and can therefore act accordingly. The Apollo Kotlin codegen generates `@semanticNonNull` fields as non-null Kotlin properties. No need to guess anymore!
153174

154175
Ideally, your backend team annotates their schema with `@semanticNonNull` directives so that different frontend teams can benefit from the new type information.
155176

@@ -281,34 +302,7 @@ class User(
281302

282303
The error is thrown during parsing but still caught before it reaches your UI code. If no parent field catches it, the Apollo Kotlin runtime does and exposes the exception in `ApolloResponse.exception`.
283304

284-
</Note>
285-
286-
## `@catchByDefault`
287-
288-
In order to use the nullability directives, you need to opt in a default catch behaviour for nullable GraphQL fields using `@catchByDefault`.
289-
290-
You can choose to map nullable fields to `FieldResult`:
291-
292-
```graphql
293-
# Errors stop the parsing.
294-
extend schema @catchByDefault(to: RESULT)
295-
```
296-
297-
Or throw errors:
298-
299-
```graphql
300-
# Errors stop the parsing.
301-
extend schema @catchByDefault(to: THROW)
302-
```
303-
304-
Or coerce errors to `null`, like the current GraphQL default:
305-
306-
```graphql
307-
# Coerce errors to null by default.
308-
extend schema @catchByDefault(to: NULL)
309-
```
310-
311-
(Adding `@catchByDefault(to: NULL)` is a no-op for codegen that unlocks using `@catch` in your operations.)
305+
</Note>
312306

313307
## Migrate to semantic nullability
314308

@@ -329,20 +323,19 @@ If you were using `@nonnull` before, you can now use `@semanticNonNull`.
329323

330324
`@semanticNonNull`, coupled with `@catch` is more flexible and also more in line with other frameworks.
331325

332-
**For usages in executable documents**:
326+
Catch to NULL by default:
327+
333328
```graphql
334-
# Replace
335-
query GetFoo {
336-
foo @nonnull
337-
}
329+
extend schema @link(
330+
url: "https://specs.apollo.dev/nullability/v0.4",
331+
import: ["@semanticNonNull", "@semanticNonNullField", "@catch", "CatchTo", "@catchByDefault"]
332+
)
338333

339-
# With
340-
query GetFoo {
341-
foo @catch(to: THROW)
342-
}
334+
extend schema @catchByDefault(to: NULL)
343335
```
344336

345-
**For usages in schema documents**:
337+
Replace `@nonnull` with `@semanticNonNullField`:
338+
346339
```graphql
347340
# Replace
348341
extend type Foo @nonnull(fields: "bar")
@@ -351,11 +344,31 @@ extend type Foo @nonnull(fields: "bar")
351344
extend type Foo @semanticNonNullField(name: "bar")
352345
```
353346

354-
If your schema is configured with `@catchByDefault(to: NULL)`, you'll also need to update the usages in your executable documents:
347+
In your queries, use `@catch(to: THROW)` to generate those fields as non-nullable:
355348

356349
```graphql
357350
# Add `@catch(to: THROW)`
358351
query GetFoo {
359352
foo @catch(to: THROW)
360353
}
361354
```
355+
356+
357+
**For usages in executable documents**
358+
359+
Because nullability is a schema concern, `@semanticNonNull` cannot be used in executable documents. Instead, define your fields as `@semanticNonNull`:
360+
361+
```graphql
362+
# Replace
363+
query GetFoo {
364+
foo @nonnull
365+
}
366+
367+
# With
368+
query GetFoo {
369+
foo @catch(to: THROW)
370+
}
371+
372+
# and make foo `@semanticNonNull` in your schema
373+
extends type Query @semanticNonNullField(name: "foo")
374+
```

libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ir/IrOperationsBuilder.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,15 @@ internal class IrOperationsBuilder(
326326

327327
val inferredVariables = inferFragmentVariables(this)
328328

329+
val defaultCatchTo = findCatchByDefault(schema)
329330
val interfaceModelGroup = builder.buildFragmentInterface(
330-
fragmentName = name
331+
fragmentName = name,
332+
defaultCatchTo = defaultCatchTo
331333
)
332334

333335
val (dataProperty, dataModelGroup) = builder.buildFragmentData(
334336
fragmentName = name,
335-
defaultCatchTo = findCatchByDefault(schema)
337+
defaultCatchTo = defaultCatchTo
336338
)
337339

338340
// Add the root type to use from the fragment selections
@@ -550,6 +552,12 @@ internal class IrOperationsBuilder(
550552
check(fieldsWithSameResponseName.map { it.type }.distinctBy { it.pretty() }.size == 1)
551553

552554
val first = fieldsWithSameResponseName.first()
555+
if (defaultCatchTo != null) {
556+
check(fieldsWithSameResponseName.map { it.catch }.distinct().size == 1) {
557+
// TODO: move to a validation rule
558+
"Merged field '${first.responseName} has different `@catch` directives"
559+
}
560+
}
553561
val childSelections = fieldsWithSameResponseName.flatMap { it.selections }
554562

555563
val forceOptional = fieldsWithSameResponseName.any {

libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ir/ModelGroupBuilder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal interface ModelGroupBuilder {
1313

1414
fun buildFragmentInterface(
1515
fragmentName: String,
16+
defaultCatchTo: CatchTo?
1617
): IrModelGroup?
1718

1819
fun buildFragmentData(

libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ir/OperationBasedModelGroupBuilder.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.apollographql.apollo.compiler.ir
22

3-
import com.apollographql.apollo.ast.Catch
43
import com.apollographql.apollo.ast.CatchTo
54
import com.apollographql.apollo.ast.GQLField
65
import com.apollographql.apollo.ast.GQLFragmentDefinition
@@ -11,10 +10,10 @@ import com.apollographql.apollo.ast.GQLNonNullType
1110
import com.apollographql.apollo.ast.GQLSelection
1211
import com.apollographql.apollo.ast.Schema
1312
import com.apollographql.apollo.compiler.capitalizeFirstLetter
14-
import com.apollographql.apollo.compiler.lowerCamelCaseIgnoringNonLetters
1513
import com.apollographql.apollo.compiler.codegen.modelName
1614
import com.apollographql.apollo.compiler.decapitalizeFirstLetter
1715
import com.apollographql.apollo.compiler.internal.escapeKotlinReservedWord
16+
import com.apollographql.apollo.compiler.lowerCamelCaseIgnoringNonLetters
1817

1918
internal class OperationBasedModelGroupBuilder(
2019
private val schema: Schema,
@@ -45,7 +44,7 @@ internal class OperationBasedModelGroupBuilder(
4544
return field.toProperty() to field.toModelGroup()!!
4645
}
4746

48-
override fun buildFragmentInterface(fragmentName: String): IrModelGroup? {
47+
override fun buildFragmentInterface(fragmentName: String, defaultCatchTo: CatchTo?): IrModelGroup? {
4948
return null
5049
}
5150

libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ir/OperationBasedWithInterfacesModelGroupBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal class OperationBasedWithInterfacesModelGroupBuilder(
5959
return field.toProperty() to field.toModelGroup()!!
6060
}
6161

62-
override fun buildFragmentInterface(fragmentName: String): IrModelGroup? {
62+
override fun buildFragmentInterface(fragmentName: String, defaultCatchTo: CatchTo?): IrModelGroup? {
6363
return null
6464
}
6565

0 commit comments

Comments
 (0)