@vantreeseba/drizzle-graphql 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -6
- package/dist/README.md +130 -6
- package/dist/index.cjs +909 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -5
- package/dist/index.d.ts +99 -5
- package/dist/index.js +941 -148
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,12 +96,14 @@ reach:
|
|
|
96
96
|
const { schema } = buildSchema(db, {
|
|
97
97
|
features: {
|
|
98
98
|
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
+
groupBy: false, // <plural>GroupBy root queries
|
|
99
100
|
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
100
101
|
distinct: false, // the `distinct` argument on list queries
|
|
101
102
|
insert: false, // create<Table> / create<Table>Single mutations
|
|
102
|
-
update: false, // update<Table> mutations
|
|
103
|
-
delete: false, // delete<Table> mutations
|
|
103
|
+
update: false, // update<Table> / update<Table>Single mutations
|
|
104
|
+
delete: false, // delete<Table> / delete<Table>Single mutations
|
|
104
105
|
upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
|
|
106
|
+
requireWhere: true, // make `where` non-null on plural update/delete (off by default)
|
|
105
107
|
},
|
|
106
108
|
})
|
|
107
109
|
```
|
|
@@ -110,6 +112,8 @@ const { schema } = buildSchema(db, {
|
|
|
110
112
|
changes nothing else
|
|
111
113
|
- `upsert` is the exception: it defaults to `false`, so the upsert mutations and their
|
|
112
114
|
conflict input only exist if you ask for them
|
|
115
|
+
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
116
|
+
turns the group-by queries off with it
|
|
113
117
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
114
118
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
115
119
|
- Turning off all three mutation features omits the `Mutation` type entirely, the same as
|
|
@@ -201,8 +205,10 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
201
205
|
- `some: {}` means "at least one related row exists"; `none: {}` means "none exist"
|
|
202
206
|
- Several modes may be given at once and are `AND`ed together
|
|
203
207
|
- A relation whose name collides with a column name is skipped — the column keeps the field
|
|
204
|
-
- Many-to-many relations declared with `.through()` are
|
|
205
|
-
|
|
208
|
+
- Many-to-many relations declared with `.through()` are filterable too: the `EXISTS`
|
|
209
|
+
subquery joins the junction table to the target, so
|
|
210
|
+
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
211
|
+
to-many relation
|
|
206
212
|
|
|
207
213
|
## Aggregate queries
|
|
208
214
|
|
|
@@ -253,7 +259,72 @@ The optional `where` argument takes the same filter input as the table's list qu
|
|
|
253
259
|
applied to every aggregate in the selection. All requested aggregates are computed in a
|
|
254
260
|
single `SELECT`, and on an empty result set `count` is `0` while the other values are `null`.
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
Add `groupBy` to get the same numbers one row per group — see [Group by](#group-by).
|
|
263
|
+
|
|
264
|
+
## Group by
|
|
265
|
+
|
|
266
|
+
Every table with aggregates also gets a `<tableName>GroupBy` query (e.g. `postsGroupBy`): the
|
|
267
|
+
same aggregates as `<tableName>Aggregate`, computed once per distinct combination of the
|
|
268
|
+
columns you group by.
|
|
269
|
+
|
|
270
|
+
```graphql
|
|
271
|
+
{
|
|
272
|
+
postsGroupBy(
|
|
273
|
+
groupBy: [authorId, published]
|
|
274
|
+
where: { createdAt: { gte: "2024-01-01T00:00:00Z" } }
|
|
275
|
+
having: { count: { gte: 5 } }
|
|
276
|
+
) {
|
|
277
|
+
group {
|
|
278
|
+
authorId
|
|
279
|
+
published
|
|
280
|
+
}
|
|
281
|
+
count
|
|
282
|
+
avg {
|
|
283
|
+
views
|
|
284
|
+
}
|
|
285
|
+
max {
|
|
286
|
+
createdAt
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
293
|
+
enum. It holds the orderable columns plus booleans — anything the database can group on.
|
|
294
|
+
An empty list is an error, and repeated columns are ignored
|
|
295
|
+
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
296
|
+
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
297
|
+
a column whose grouped value really is `NULL` is indistinguishable from
|
|
298
|
+
- Every other field is the same one `<tableName>Aggregate` returns — `count`, `avg`, `sum`,
|
|
299
|
+
`min`, `max`, `countNonNull`, `countDistinct` — with the identical types and rules
|
|
300
|
+
- `where` filters rows before grouping; `having` filters groups after aggregating
|
|
301
|
+
- The result is `[<Type>GroupBy!]!`, one row per group, in whatever order the database
|
|
302
|
+
returns them. Add your own ordering client-side if you need it
|
|
303
|
+
|
|
304
|
+
`having` mirrors the aggregate selection, with an `AggregateNumberFilter` (`eq`, `ne`, `gt`,
|
|
305
|
+
`gte`, `lt`, `lte`, all `Float`) in place of each value:
|
|
306
|
+
|
|
307
|
+
```graphql
|
|
308
|
+
{
|
|
309
|
+
postsGroupBy(groupBy: [authorId], having: { count: { gt: 3 }, avg: { views: { gte: 100 } } }) {
|
|
310
|
+
group {
|
|
311
|
+
authorId
|
|
312
|
+
}
|
|
313
|
+
count
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` /
|
|
319
|
+
`countNonNull` / `countDistinct` take one filter per column, over the same columns the
|
|
320
|
+
matching aggregate type exposes
|
|
321
|
+
- Every entry in a `having` is ANDed together
|
|
322
|
+
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
323
|
+
whether or not you asked for `count`
|
|
324
|
+
|
|
325
|
+
The whole thing is one `SELECT … GROUP BY … HAVING …`. Set `features: { groupBy: false }` to
|
|
326
|
+
leave these queries (and their `<Type>GroupBy`, `<Type>GroupKeys`, `<Type>GroupByColumn` and
|
|
327
|
+
`<Type>Having` types) out of the schema; turning off `aggregates` removes them too.
|
|
257
328
|
|
|
258
329
|
## Relation aggregates
|
|
259
330
|
|
|
@@ -397,6 +468,37 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
|
|
|
397
468
|
`create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
|
|
398
469
|
the per-request replacement.
|
|
399
470
|
|
|
471
|
+
## Single-row update & delete
|
|
472
|
+
|
|
473
|
+
Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
|
|
474
|
+
`update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
mutation {
|
|
478
|
+
updateUsersSingle(where: { id: { eq: 1 } }, set: { name: "Dan" }) {
|
|
479
|
+
id
|
|
480
|
+
name
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
- `where` is **non-null** and must contain at least one filter — `where: {}` is rejected at
|
|
486
|
+
resolve time, so a Single write can never become an unbounded one
|
|
487
|
+
- The return type is the single (nullable) row type, not a list: the affected row comes back
|
|
488
|
+
directly, and **no match returns `null`** instead of an empty list
|
|
489
|
+
- If `where` matches **more than one row**, the mutation throws a `GraphQLError` and writes
|
|
490
|
+
nothing — the match is checked (`LIMIT 2`) before the write runs
|
|
491
|
+
- **MySQL** cannot return the affected row (no `RETURNING`), so its Single variants keep the
|
|
492
|
+
single-match and non-empty-`where` guarantees but return `MutationReturn` like every other
|
|
493
|
+
MySQL mutation
|
|
494
|
+
- The variants are generated under `features.update` / `features.delete`, share the plural
|
|
495
|
+
mutations' input types, and appear in `entities.mutations` for custom schemas
|
|
496
|
+
|
|
497
|
+
The plural mutations still accept a missing `where` (a full-table write) by default. To rule
|
|
498
|
+
that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
|
|
499
|
+
`update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
|
|
500
|
+
Single variants. It defaults to `false` for backwards compatibility.
|
|
501
|
+
|
|
400
502
|
## Query cost
|
|
401
503
|
|
|
402
504
|
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
@@ -408,7 +510,7 @@ fields it mentions:
|
|
|
408
510
|
| Field | Cost |
|
|
409
511
|
| --- | --- |
|
|
410
512
|
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
411
|
-
| Aggregate query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
513
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
412
514
|
| Everything else | no hint — your estimator's default applies |
|
|
413
515
|
|
|
414
516
|
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
@@ -555,6 +657,28 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
555
657
|
> **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
|
|
556
658
|
> have no such requirement.
|
|
557
659
|
|
|
660
|
+
### Relation nullability
|
|
661
|
+
|
|
662
|
+
A **to-many** relation field is always `[Target!]!`. A **to-one** relation field is
|
|
663
|
+
nullable by default, but honors the relation's declared optionality: declaring
|
|
664
|
+
`optional: false` on a Drizzle `one` relation — the assertion that the related row always
|
|
665
|
+
exists, i.e. a `NOT NULL` foreign key — emits the field as `Target!`:
|
|
666
|
+
|
|
667
|
+
```Typescript
|
|
668
|
+
const relations = buildRelations({ Users, Posts }, {
|
|
669
|
+
Posts: {
|
|
670
|
+
// posts.author_id is NOT NULL → author: Users!
|
|
671
|
+
author: r.one.Users({ from: r.Posts.authorId, to: r.Users.id, optional: false }),
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Only the explicit `optional: false` declaration is honored — required-ness is never
|
|
677
|
+
inferred from column nullability, since a `NOT NULL` `from` column does not guarantee a
|
|
678
|
+
related row exists when the constraint lives on the other side of the join. Note that on a
|
|
679
|
+
required relation, a `where` argument that filters out the related row (or a dangling
|
|
680
|
+
foreign key) resolves to `null` and therefore surfaces as a GraphQL non-null error.
|
|
681
|
+
|
|
558
682
|
### Overriding a relation's resolver without overfetching
|
|
559
683
|
|
|
560
684
|
By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
|
package/dist/README.md
CHANGED
|
@@ -96,12 +96,14 @@ reach:
|
|
|
96
96
|
const { schema } = buildSchema(db, {
|
|
97
97
|
features: {
|
|
98
98
|
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
+
groupBy: false, // <plural>GroupBy root queries
|
|
99
100
|
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
100
101
|
distinct: false, // the `distinct` argument on list queries
|
|
101
102
|
insert: false, // create<Table> / create<Table>Single mutations
|
|
102
|
-
update: false, // update<Table> mutations
|
|
103
|
-
delete: false, // delete<Table> mutations
|
|
103
|
+
update: false, // update<Table> / update<Table>Single mutations
|
|
104
|
+
delete: false, // delete<Table> / delete<Table>Single mutations
|
|
104
105
|
upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
|
|
106
|
+
requireWhere: true, // make `where` non-null on plural update/delete (off by default)
|
|
105
107
|
},
|
|
106
108
|
})
|
|
107
109
|
```
|
|
@@ -110,6 +112,8 @@ const { schema } = buildSchema(db, {
|
|
|
110
112
|
changes nothing else
|
|
111
113
|
- `upsert` is the exception: it defaults to `false`, so the upsert mutations and their
|
|
112
114
|
conflict input only exist if you ask for them
|
|
115
|
+
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
116
|
+
turns the group-by queries off with it
|
|
113
117
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
114
118
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
115
119
|
- Turning off all three mutation features omits the `Mutation` type entirely, the same as
|
|
@@ -201,8 +205,10 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
201
205
|
- `some: {}` means "at least one related row exists"; `none: {}` means "none exist"
|
|
202
206
|
- Several modes may be given at once and are `AND`ed together
|
|
203
207
|
- A relation whose name collides with a column name is skipped — the column keeps the field
|
|
204
|
-
- Many-to-many relations declared with `.through()` are
|
|
205
|
-
|
|
208
|
+
- Many-to-many relations declared with `.through()` are filterable too: the `EXISTS`
|
|
209
|
+
subquery joins the junction table to the target, so
|
|
210
|
+
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
211
|
+
to-many relation
|
|
206
212
|
|
|
207
213
|
## Aggregate queries
|
|
208
214
|
|
|
@@ -253,7 +259,72 @@ The optional `where` argument takes the same filter input as the table's list qu
|
|
|
253
259
|
applied to every aggregate in the selection. All requested aggregates are computed in a
|
|
254
260
|
single `SELECT`, and on an empty result set `count` is `0` while the other values are `null`.
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
Add `groupBy` to get the same numbers one row per group — see [Group by](#group-by).
|
|
263
|
+
|
|
264
|
+
## Group by
|
|
265
|
+
|
|
266
|
+
Every table with aggregates also gets a `<tableName>GroupBy` query (e.g. `postsGroupBy`): the
|
|
267
|
+
same aggregates as `<tableName>Aggregate`, computed once per distinct combination of the
|
|
268
|
+
columns you group by.
|
|
269
|
+
|
|
270
|
+
```graphql
|
|
271
|
+
{
|
|
272
|
+
postsGroupBy(
|
|
273
|
+
groupBy: [authorId, published]
|
|
274
|
+
where: { createdAt: { gte: "2024-01-01T00:00:00Z" } }
|
|
275
|
+
having: { count: { gte: 5 } }
|
|
276
|
+
) {
|
|
277
|
+
group {
|
|
278
|
+
authorId
|
|
279
|
+
published
|
|
280
|
+
}
|
|
281
|
+
count
|
|
282
|
+
avg {
|
|
283
|
+
views
|
|
284
|
+
}
|
|
285
|
+
max {
|
|
286
|
+
createdAt
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
293
|
+
enum. It holds the orderable columns plus booleans — anything the database can group on.
|
|
294
|
+
An empty list is an error, and repeated columns are ignored
|
|
295
|
+
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
296
|
+
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
297
|
+
a column whose grouped value really is `NULL` is indistinguishable from
|
|
298
|
+
- Every other field is the same one `<tableName>Aggregate` returns — `count`, `avg`, `sum`,
|
|
299
|
+
`min`, `max`, `countNonNull`, `countDistinct` — with the identical types and rules
|
|
300
|
+
- `where` filters rows before grouping; `having` filters groups after aggregating
|
|
301
|
+
- The result is `[<Type>GroupBy!]!`, one row per group, in whatever order the database
|
|
302
|
+
returns them. Add your own ordering client-side if you need it
|
|
303
|
+
|
|
304
|
+
`having` mirrors the aggregate selection, with an `AggregateNumberFilter` (`eq`, `ne`, `gt`,
|
|
305
|
+
`gte`, `lt`, `lte`, all `Float`) in place of each value:
|
|
306
|
+
|
|
307
|
+
```graphql
|
|
308
|
+
{
|
|
309
|
+
postsGroupBy(groupBy: [authorId], having: { count: { gt: 3 }, avg: { views: { gte: 100 } } }) {
|
|
310
|
+
group {
|
|
311
|
+
authorId
|
|
312
|
+
}
|
|
313
|
+
count
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` /
|
|
319
|
+
`countNonNull` / `countDistinct` take one filter per column, over the same columns the
|
|
320
|
+
matching aggregate type exposes
|
|
321
|
+
- Every entry in a `having` is ANDed together
|
|
322
|
+
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
323
|
+
whether or not you asked for `count`
|
|
324
|
+
|
|
325
|
+
The whole thing is one `SELECT … GROUP BY … HAVING …`. Set `features: { groupBy: false }` to
|
|
326
|
+
leave these queries (and their `<Type>GroupBy`, `<Type>GroupKeys`, `<Type>GroupByColumn` and
|
|
327
|
+
`<Type>Having` types) out of the schema; turning off `aggregates` removes them too.
|
|
257
328
|
|
|
258
329
|
## Relation aggregates
|
|
259
330
|
|
|
@@ -397,6 +468,37 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
|
|
|
397
468
|
`create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
|
|
398
469
|
the per-request replacement.
|
|
399
470
|
|
|
471
|
+
## Single-row update & delete
|
|
472
|
+
|
|
473
|
+
Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
|
|
474
|
+
`update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
mutation {
|
|
478
|
+
updateUsersSingle(where: { id: { eq: 1 } }, set: { name: "Dan" }) {
|
|
479
|
+
id
|
|
480
|
+
name
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
- `where` is **non-null** and must contain at least one filter — `where: {}` is rejected at
|
|
486
|
+
resolve time, so a Single write can never become an unbounded one
|
|
487
|
+
- The return type is the single (nullable) row type, not a list: the affected row comes back
|
|
488
|
+
directly, and **no match returns `null`** instead of an empty list
|
|
489
|
+
- If `where` matches **more than one row**, the mutation throws a `GraphQLError` and writes
|
|
490
|
+
nothing — the match is checked (`LIMIT 2`) before the write runs
|
|
491
|
+
- **MySQL** cannot return the affected row (no `RETURNING`), so its Single variants keep the
|
|
492
|
+
single-match and non-empty-`where` guarantees but return `MutationReturn` like every other
|
|
493
|
+
MySQL mutation
|
|
494
|
+
- The variants are generated under `features.update` / `features.delete`, share the plural
|
|
495
|
+
mutations' input types, and appear in `entities.mutations` for custom schemas
|
|
496
|
+
|
|
497
|
+
The plural mutations still accept a missing `where` (a full-table write) by default. To rule
|
|
498
|
+
that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
|
|
499
|
+
`update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
|
|
500
|
+
Single variants. It defaults to `false` for backwards compatibility.
|
|
501
|
+
|
|
400
502
|
## Query cost
|
|
401
503
|
|
|
402
504
|
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
@@ -408,7 +510,7 @@ fields it mentions:
|
|
|
408
510
|
| Field | Cost |
|
|
409
511
|
| --- | --- |
|
|
410
512
|
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
411
|
-
| Aggregate query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
513
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
412
514
|
| Everything else | no hint — your estimator's default applies |
|
|
413
515
|
|
|
414
516
|
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
@@ -555,6 +657,28 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
555
657
|
> **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
|
|
556
658
|
> have no such requirement.
|
|
557
659
|
|
|
660
|
+
### Relation nullability
|
|
661
|
+
|
|
662
|
+
A **to-many** relation field is always `[Target!]!`. A **to-one** relation field is
|
|
663
|
+
nullable by default, but honors the relation's declared optionality: declaring
|
|
664
|
+
`optional: false` on a Drizzle `one` relation — the assertion that the related row always
|
|
665
|
+
exists, i.e. a `NOT NULL` foreign key — emits the field as `Target!`:
|
|
666
|
+
|
|
667
|
+
```Typescript
|
|
668
|
+
const relations = buildRelations({ Users, Posts }, {
|
|
669
|
+
Posts: {
|
|
670
|
+
// posts.author_id is NOT NULL → author: Users!
|
|
671
|
+
author: r.one.Users({ from: r.Posts.authorId, to: r.Users.id, optional: false }),
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Only the explicit `optional: false` declaration is honored — required-ness is never
|
|
677
|
+
inferred from column nullability, since a `NOT NULL` `from` column does not guarantee a
|
|
678
|
+
related row exists when the constraint lives on the other side of the join. Note that on a
|
|
679
|
+
required relation, a `where` argument that filters out the related row (or a dangling
|
|
680
|
+
foreign key) resolves to `null` and therefore surfaces as a GraphQL non-null error.
|
|
681
|
+
|
|
558
682
|
### Overriding a relation's resolver without overfetching
|
|
559
683
|
|
|
560
684
|
By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
|