@vantreeseba/drizzle-graphql 15.1.6 → 16.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 +172 -21
- package/dist/index.cjs +706 -300
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +709 -303
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ const { schema } = buildSchema(db, {
|
|
|
154
154
|
nestedWrites: true, // relation fields on the create/update inputs (off by default)
|
|
155
155
|
fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
|
|
156
156
|
countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
|
|
157
|
-
requireWhere: true, //
|
|
157
|
+
requireWhere: true, // require a `where` on plural update/delete/updateMany (off by default)
|
|
158
158
|
uniqueKeyFilters: true, // a `where` field per compound unique constraint (off by default)
|
|
159
159
|
},
|
|
160
160
|
})
|
|
@@ -238,7 +238,7 @@ An excluded **column** disappears from every surface at once:
|
|
|
238
238
|
| `<Type>OrderBy` | field removed |
|
|
239
239
|
| `<Type>MinAggregate` / `<Type>MaxAggregate` / … | field removed |
|
|
240
240
|
| `<Type>DistinctColumn` / `<Type>GroupByColumn` | value removed |
|
|
241
|
-
| `<Type>
|
|
241
|
+
| `<Type>ConflictTarget` / `<Type>UpdateColumn` (upsert `onConflict`) | value removed |
|
|
242
242
|
|
|
243
243
|
Two details worth stating plainly:
|
|
244
244
|
|
|
@@ -313,7 +313,9 @@ Two things it deliberately does not do:
|
|
|
313
313
|
|
|
314
314
|
A relation filter or an ordering through a relation sees exactly the rows the relation field
|
|
315
315
|
would return. Through a `.through()` relation, the junction table's own `scope` and soft delete
|
|
316
|
-
apply as well
|
|
316
|
+
apply as well — to the relation field, its `<relation>Aggregate` and a relation filter alike,
|
|
317
|
+
so a link row the caller cannot read links nothing on any of them (see
|
|
318
|
+
[junction rows](#junction-rows)). A scope that returns a filter object is the server's own predicate, so the
|
|
317
319
|
relations it reaches through are read without their targets' scopes.
|
|
318
320
|
|
|
319
321
|
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
@@ -566,6 +568,27 @@ field shows. They take no `deleted` argument of their own.
|
|
|
566
568
|
`scope` changes reads only. A nested `connect` / `disconnect` / `set` still refuses to attach a
|
|
567
569
|
marked row under either setting, and the table's own writes are unaffected.
|
|
568
570
|
|
|
571
|
+
#### Junction rows
|
|
572
|
+
|
|
573
|
+
A `.through()` relation links only through the junction rows the junction table's own `scope`
|
|
574
|
+
and soft delete keep, on every path that reads it: the relation field (eager or batched,
|
|
575
|
+
paginated or `distinct`), `<relation>Aggregate`, and a relation filter. A marked junction row is
|
|
576
|
+
a removed link, so the junction is always read at `EXCLUDE`:
|
|
577
|
+
|
|
578
|
+
- The relation's `deleted` argument is about the **target** rows. `tags(deleted: INCLUDE)`
|
|
579
|
+
brings back marked tags that are still linked, never a tag whose link was marked.
|
|
580
|
+
- The junction's own `softDelete.scope` only decides what the junction table's own fields
|
|
581
|
+
return. `'root'` keeps a retired row rendering where a relation points *at* it; a relation
|
|
582
|
+
passes *through* a junction row and never renders it, so there is nothing for `'root'` to
|
|
583
|
+
keep, and a marked link stays hidden under either setting.
|
|
584
|
+
|
|
585
|
+
Drizzle's `with:` joins the junction itself and offers no way to add a condition to it, so a
|
|
586
|
+
relation whose junction has a `scope` or soft delete is loaded by its field resolver's batch
|
|
587
|
+
loader rather than with its parent — still one query per relation per request, not one per
|
|
588
|
+
parent. A junction joined on more than one column per side cannot be loaded that way, so
|
|
589
|
+
selecting such a relation while its junction has a policy fails with
|
|
590
|
+
`DRIZZLE_UNSUPPORTED_RELATION` instead of returning links the policy hides.
|
|
591
|
+
|
|
569
592
|
### Purging a row
|
|
570
593
|
|
|
571
594
|
Soft delete and purge are not alternatives — emptying the trash, reclaiming a unique key and
|
|
@@ -681,6 +704,36 @@ mutation {
|
|
|
681
704
|
`2026-09-15T10:00:00.000Z` — and filter operands on such a column are floored the same way,
|
|
682
705
|
so `eq` with the value you wrote finds the row you wrote it to
|
|
683
706
|
|
|
707
|
+
Some columns map to built-in GraphQL types rather than a custom scalar:
|
|
708
|
+
|
|
709
|
+
| Drizzle column | GraphQL type | Transported as |
|
|
710
|
+
| --- | --- | --- |
|
|
711
|
+
| SQLite `integer()` (number mode) | `Float` | a JS number, exact up to 2^53 |
|
|
712
|
+
| Postgres `bytea`, SQLite `blob({ mode: 'buffer' })` | `[Int!]` | the bytes, each 0–255 |
|
|
713
|
+
| Postgres `point` / `line` (tuple mode), PostGIS `geometry` (tuple mode), `vector`, `halfvec` | `[Float!]` | the tuple or vector |
|
|
714
|
+
| Postgres `point({ mode: 'xy' })`, `line({ mode: 'abc' })` | `JSON` | the object, `{ x, y }` / `{ a, b, c }` |
|
|
715
|
+
|
|
716
|
+
- **SQLite `integer()`** holds any 64-bit integer, a rowid primary key included, so it is a
|
|
717
|
+
`Float`: GraphQL's `Int` is 32-bit and cannot serialize anything past 2^31. Drizzle reads
|
|
718
|
+
the column as a JS number, so values are exact up to `Number.MAX_SAFE_INTEGER`; declare it
|
|
719
|
+
`blob({ mode: 'bigint' })` (a `BigInt`) for more. Its `'boolean'` and `'timestamp'` modes
|
|
720
|
+
keep `Boolean` and `DateTime`
|
|
721
|
+
- A **tuple** must have its type's length — 2 for a point, 3 for a line — or the write is
|
|
722
|
+
rejected
|
|
723
|
+
- An **array column** (`bigint().array()`, `timestamp().array()`, …) is a list of its element
|
|
724
|
+
type, and each element is converted as a single value would be, in writes and in the `has` /
|
|
725
|
+
`hasSome` / `hasEvery` operands alike
|
|
726
|
+
|
|
727
|
+
> **Upgrading — SQLite `integer()` columns.** A SQLite `integer()` column in number mode (the
|
|
728
|
+
> default) used to be `Int`, and reading a value past 2^31 failed with "Int cannot represent
|
|
729
|
+
> non 32-bit signed integer value". It is now `Float` in every position: the object field, the
|
|
730
|
+
> create / update inputs, primary-key and unique-key arguments, `FloatFilter` operands, aggregate
|
|
731
|
+
> `min` / `max` and group keys, and `FloatFieldUpdate`. Variables typed `Int` for these fields
|
|
732
|
+
> become `Float` (an `Int` variable is a validation error in a `Float` position); literals and
|
|
733
|
+
> the values themselves are unchanged. `integer({ mode: 'boolean' })` and the timestamp modes are
|
|
734
|
+
> not affected. A SQLite `blob({ mode: 'buffer' })` column, which used to be `JSON` and could
|
|
735
|
+
> not be written, is now `[Int!]`.
|
|
736
|
+
|
|
684
737
|
The scalars are exported if you need them in a hand-written schema:
|
|
685
738
|
|
|
686
739
|
```Typescript
|
|
@@ -797,11 +850,16 @@ type Posts {
|
|
|
797
850
|
|
|
798
851
|
- One type, one filter input (`StatusEnumFilter`), so a client variable typed `StatusEnum!`
|
|
799
852
|
is passable to either table
|
|
800
|
-
- The declared name is PascalCased and suffixed: `user_status` → `UserStatusEnum
|
|
853
|
+
- The declared name is PascalCased and suffixed: `user_status` → `UserStatusEnum`. A name
|
|
854
|
+
that would start with a digit gets a leading underscore: `2fa_mode` → `_2faModeEnum`
|
|
801
855
|
- A column that lists its values inline (`text('tier', { enum: ['free', 'paid'] })`) has no
|
|
802
856
|
declared name to share, so it keeps a per-column type — `UsersTierEnum`
|
|
803
857
|
- Two different enums that would land on the same GraphQL name (`status` in two Postgres
|
|
804
858
|
schemas) fail the build rather than silently sharing a type
|
|
859
|
+
- Ordering by an enum column, its `min` / `max`, and cursor pages over it follow the
|
|
860
|
+
database's own rule. PostgreSQL and MySQL sort an enum by declaration order (`active` before
|
|
861
|
+
`archived` because it is declared first). SQLite has no enum type — the column is `text`
|
|
862
|
+
with the values checked by Drizzle — so there it sorts alphabetically
|
|
805
863
|
|
|
806
864
|
`enumNameMapper` overrides the name. Returning `undefined` keeps the default, so a mapper
|
|
807
865
|
only has to handle the cases it cares about:
|
|
@@ -831,8 +889,10 @@ lists is the build-time error above.
|
|
|
831
889
|
### Enum value names
|
|
832
890
|
|
|
833
891
|
A value's GraphQL name is the stored value itself, or `Option<index>` when the stored value
|
|
834
|
-
is not a usable GraphQL name
|
|
835
|
-
|
|
892
|
+
is not a usable GraphQL name — one with a character outside `[_a-zA-Z0-9]` (`hot-fix`), one
|
|
893
|
+
that starts with a digit (`2xl`), or `true` / `false` / `null`. `enumValueNameMapper` renames
|
|
894
|
+
the values — most often to follow GraphQL's SCREAMING_CASE convention while the database keeps
|
|
895
|
+
its lowercase values:
|
|
836
896
|
|
|
837
897
|
```Typescript
|
|
838
898
|
export const Runs = pgTable('runs', {
|
|
@@ -1052,6 +1112,27 @@ by hand. Under `insensitive` the operand stays a literal and stays a bound param
|
|
|
1052
1112
|
from the numeric, uuid, boolean, timestamp and enum filters (see
|
|
1053
1113
|
[Which operators a column gets](#which-operators-a-column-gets))
|
|
1054
1114
|
|
|
1115
|
+
**On MySQL, plain comparisons may already ignore case.** MySQL compares strings under the
|
|
1116
|
+
column's collation, and its default (`utf8mb4_0900_ai_ci`, like every `*_ai_ci` collation) is
|
|
1117
|
+
case- **and** accent-insensitive. So without `insensitive`, `eq: "dan"` matches `Dan` and `dän`
|
|
1118
|
+
there, and the same holds for `ne`, the ordering comparisons, `inArray`, `orderBy`, `min` /
|
|
1119
|
+
`max`, `distinct`, `groupBy` and the `like` / `startsWith` / `endsWith` / `contains` operators.
|
|
1120
|
+
PostgreSQL compares exactly. That is the database's configured behaviour, not something this
|
|
1121
|
+
library adds: give a column a binary or `*_as_cs` collation (`varchar(…) COLLATE utf8mb4_bin`)
|
|
1122
|
+
where it has to compare exactly.
|
|
1123
|
+
|
|
1124
|
+
### Case sensitivity of the pattern operators on SQLite
|
|
1125
|
+
|
|
1126
|
+
`like` / `notLike` and the safe `startsWith` / `endsWith` / `contains` are case-sensitive, as
|
|
1127
|
+
they are on PostgreSQL: `contains: "App"` matches `Apple` and not `apple`. SQLite's own `LIKE`
|
|
1128
|
+
folds ASCII case, so on SQLite they compile to `GLOB` instead, with `%` / `_` translated to its
|
|
1129
|
+
`*` / `?` and every other wildcard in the operand matched literally. The `i`-prefixed operators
|
|
1130
|
+
and `insensitive: true` fold case on SQLite as everywhere else.
|
|
1131
|
+
|
|
1132
|
+
> **Upgrading — SQLite.** These operators used to compile to `LIKE` on SQLite and so matched
|
|
1133
|
+
> case-insensitively. Queries that relied on that should use the `i`-prefixed operator or
|
|
1134
|
+
> `insensitive: true`.
|
|
1135
|
+
|
|
1055
1136
|
## Which operators a column gets
|
|
1056
1137
|
|
|
1057
1138
|
A filter input carries the operators that can express a question about that column type, and
|
|
@@ -1068,7 +1149,8 @@ no others. The set follows the column's data type, never its name:
|
|
|
1068
1149
|
|
|
1069
1150
|
The pattern operators are `like` / `notLike` / `ilike` / `notIlike`, the safe
|
|
1070
1151
|
`startsWith` / `endsWith` / `contains` and their `i`-prefixed forms, plus the `insensitive`
|
|
1071
|
-
flag that modifies them.
|
|
1152
|
+
flag that modifies them. MySQL and SQLite have no `ILIKE`, so there `ilike` / `notIlike`
|
|
1153
|
+
compile to `lower(column) LIKE lower(pattern)`, the same form `insensitive` uses.
|
|
1072
1154
|
|
|
1073
1155
|
An operand of `null` leaves its operator out, as though it had not been sent. `false` is an
|
|
1074
1156
|
ordinary value: `active: { eq: false }` matches the rows that are false, `ne: false` the rows
|
|
@@ -1211,6 +1293,23 @@ each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts
|
|
|
1211
1293
|
string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
|
|
1212
1294
|
number.
|
|
1213
1295
|
|
|
1296
|
+
SQLite has no boolean type, and its `json_extract` hands a JSON `true` / `false` back as `1` /
|
|
1297
|
+
`0`, so its path reads check `json_type` as well: `eq: true` matches only a JSON `true`, never
|
|
1298
|
+
the number `1`, a number operand never matches a boolean, and a text read sees `"true"` /
|
|
1299
|
+
`"false"`, as PostgreSQL's does.
|
|
1300
|
+
|
|
1301
|
+
**Whole-document `eq` / `ne`** compare documents, not their text. Object key order and
|
|
1302
|
+
whitespace do not matter; array order does, and so does the difference between `1` and `true`.
|
|
1303
|
+
PostgreSQL compares as `jsonb` (so a plain `json` column, which has no `=` of its own, works
|
|
1304
|
+
too), MySQL against `CAST(? AS JSON)`, and SQLite by matching the two documents' nodes both ways.
|
|
1305
|
+
Like `=` / `<>` on any column, neither matches a row whose column is SQL `NULL`.
|
|
1306
|
+
|
|
1307
|
+
**JSON columns cannot be ordered.** A JSON document has no meaningful order — PostgreSQL's
|
|
1308
|
+
`json` has none at all, and MySQL's JSON ordering is not one a cursor can resume from — so
|
|
1309
|
+
JSON columns are left out of `<Type>OrderBy` (and so out of cursor pagination and every relation
|
|
1310
|
+
`orderBy`) and out of `<Type>DistinctColumn`, and naming one in `defaults.<table>.orderBy` fails
|
|
1311
|
+
the build. Order by a generated column or a path you extract yourself instead.
|
|
1312
|
+
|
|
1214
1313
|
Note that `contains` means different things at the two levels, because a path names a scalar:
|
|
1215
1314
|
on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
|
|
1216
1315
|
inside a `path` filter it is substring matching on the extracted value.
|
|
@@ -1252,13 +1351,19 @@ following the same naming rules as the other generated queries:
|
|
|
1252
1351
|
```
|
|
1253
1352
|
|
|
1254
1353
|
- `count` — number of matching rows (`Int!`)
|
|
1255
|
-
- `avg` / `sum` — one nullable `Float` field per numeric column
|
|
1354
|
+
- `avg` / `sum` — one nullable `Float` field per numeric column. On MySQL `avg` is computed
|
|
1355
|
+
as a `DOUBLE`, since MySQL's own `AVG` over an integer column is a `DECIMAL` rounded to four
|
|
1356
|
+
places
|
|
1256
1357
|
- `min` / `max` — one field per orderable column (numbers, strings, enums, dates, bigints),
|
|
1257
|
-
typed exactly like that column is in the table's own type
|
|
1358
|
+
typed exactly like that column is in the table's own type. PostgreSQL `uuid`, `macaddr`,
|
|
1359
|
+
`macaddr8` and `bit` columns are left out, because PostgreSQL has no `min` / `max` for them,
|
|
1360
|
+
and so is a SQLite `blob({ mode: 'bigint' })` column, whose bytes compare as text (`9` above
|
|
1361
|
+
`10`)
|
|
1258
1362
|
- `countNonNull` — `Int!` per column: how many matching rows have a non-null value there.
|
|
1259
1363
|
Every column qualifies, since `count(col)` is valid whatever the type
|
|
1260
1364
|
- `countDistinct` — `Int!` per column: how many distinct non-null values there are. Limited to
|
|
1261
|
-
the
|
|
1365
|
+
the columns with an equality operator: the `min` / `max` columns plus the ones listed above
|
|
1366
|
+
that compare but have no `min` / `max`
|
|
1262
1367
|
|
|
1263
1368
|
Columns that have no meaningful ordering — booleans, arrays, JSON, buffers, and geometry —
|
|
1264
1369
|
are left out of these types, and the `avg` / `sum` / `min` / `max` fields themselves are
|
|
@@ -1299,7 +1404,7 @@ columns you group by.
|
|
|
1299
1404
|
```
|
|
1300
1405
|
|
|
1301
1406
|
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
1302
|
-
enum. It holds the
|
|
1407
|
+
enum. It holds the `countDistinct` columns plus booleans — anything the database can group on.
|
|
1303
1408
|
An empty list is an error, and repeated columns are ignored
|
|
1304
1409
|
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
1305
1410
|
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
@@ -1328,6 +1433,9 @@ columns you group by.
|
|
|
1328
1433
|
filter per numeric column (the comparison is numeric, so a `max` over a text or date
|
|
1329
1434
|
column has no `having` entry even though the aggregate type returns it),
|
|
1330
1435
|
`countNonNull` one per column, and `countDistinct` one per column `countDistinct` exposes
|
|
1436
|
+
- The operand is a `Float` whatever the aggregate's own type, so `count: { gt: 1.5 }` works:
|
|
1437
|
+
on PostgreSQL, where an integer aggregate would otherwise give the parameter its integer
|
|
1438
|
+
type, the aggregate is compared as `numeric`
|
|
1331
1439
|
- Every entry in a `having` is ANDed together
|
|
1332
1440
|
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
1333
1441
|
whether or not you asked for `count`
|
|
@@ -1406,6 +1514,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
|
|
|
1406
1514
|
distinct value **per parent** — the window is partitioned by the foreign key as well as
|
|
1407
1515
|
the requested columns
|
|
1408
1516
|
- `distinct` is not on single queries — those return one row either way
|
|
1517
|
+
- JSON columns are not in `<Type>DistinctColumn` (see
|
|
1518
|
+
[Filtering inside JSON documents](#filtering-inside-json-documents))
|
|
1409
1519
|
|
|
1410
1520
|
It runs as an extra `row_number() over (partition by …)` query that picks the surviving
|
|
1411
1521
|
rows' primary keys, after which the main query is narrowed to them — so it needs the same
|
|
@@ -1422,12 +1532,25 @@ supplies no `orderBy` and the table has no [default ordering](#default-ordering)
|
|
|
1422
1532
|
- list queries with `limit` and/or `offset`
|
|
1423
1533
|
- `<tableName>Single` queries, which are an implicit `limit 1`
|
|
1424
1534
|
- to-many relation fields with `limit` and/or `offset` (as a tiebreak appended to any
|
|
1425
|
-
`orderBy` you do supply
|
|
1535
|
+
`orderBy` you do supply or [default ordering](#default-ordering) the target has — and under
|
|
1536
|
+
`orderBy: {}` too — so per-parent slices are deterministic and the eager and lazy paths
|
|
1537
|
+
return the same slice)
|
|
1426
1538
|
|
|
1427
1539
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
1428
1540
|
key column, and an unpaginated list query of a table with no `defaults` entry is left
|
|
1429
1541
|
unordered so no sort is paid for.
|
|
1430
1542
|
|
|
1543
|
+
`offset` works without `limit` on every dialect. SQLite and MySQL only accept `OFFSET` after a
|
|
1544
|
+
`LIMIT`, so there an offset-only page is sent with a limit meaning "no limit" (`-1` on SQLite,
|
|
1545
|
+
2^53 - 1 on MySQL).
|
|
1546
|
+
|
|
1547
|
+
`<Type>OrderBy` offers every column that has a meaningful order. JSON columns (see
|
|
1548
|
+
[Filtering inside JSON documents](#filtering-inside-json-documents)) and SQLite
|
|
1549
|
+
`blob({ mode: 'bigint' })` columns, whose bytes sort as text, are left out. A table left with
|
|
1550
|
+
nothing to order by — no such column, and no to-one relation to a table that has one — gets no
|
|
1551
|
+
`<Type>OrderBy` and no `orderBy` argument, on its lists or on relation fields pointing at it, and
|
|
1552
|
+
likewise no `<Type>DistinctColumn` or `distinct` argument when no column qualifies for that.
|
|
1553
|
+
|
|
1431
1554
|
## Default ordering
|
|
1432
1555
|
|
|
1433
1556
|
A table usually has one ordering that is right nearly every time it is read — newest first,
|
|
@@ -1615,7 +1738,10 @@ mutation {
|
|
|
1615
1738
|
exclusion constraint matching" error
|
|
1616
1739
|
- `target` can be a partial unique index — the `WHERE deleted_at IS NULL` companion of
|
|
1617
1740
|
[soft delete](#soft-delete), say. Its predicate goes into `ON CONFLICT (…) WHERE …`, so a
|
|
1618
|
-
conflict is detected only against the rows the index covers
|
|
1741
|
+
conflict is detected only against the rows the index covers. On SQLite the predicate is
|
|
1742
|
+
written with its values inline (SQLite matches it to the index as written, so
|
|
1743
|
+
`status = 'active'` works), and `action: NOTHING` on such a target is spelled as an update
|
|
1744
|
+
that matches no row, since SQLite cannot parse the predicate before `DO NOTHING`
|
|
1619
1745
|
- `contextValues` columns are never overwritten on conflict — see
|
|
1620
1746
|
[columns the server owns](#columns-the-server-owns)
|
|
1621
1747
|
- `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
|
|
@@ -1655,7 +1781,9 @@ Dialect differences:
|
|
|
1655
1781
|
no predicate, so `<Type>OnConflict` there has only `action` and `update`. Because the
|
|
1656
1782
|
conflict can be on any unique key, the default overwrite leaves out the primary key as well
|
|
1657
1783
|
— copying a new row's key over the conflicting row's would move it. `action: NOTHING`
|
|
1658
|
-
becomes `
|
|
1784
|
+
becomes a no-op `ON DUPLICATE KEY UPDATE <key> = <key>` rather than `INSERT IGNORE`, so
|
|
1785
|
+
only a duplicate key is skipped: a value too long for its column or a foreign-key violation
|
|
1786
|
+
still fails the write. The mutations return `MutationReturn` like every other MySQL
|
|
1659
1787
|
mutation
|
|
1660
1788
|
|
|
1661
1789
|
The build-wide `conflictDoNothing` option is deprecated in favour of this: it applies to every
|
|
@@ -1728,6 +1856,11 @@ mutation {
|
|
|
1728
1856
|
- When the parent write produces no row — an update whose `where` matched nothing, an
|
|
1729
1857
|
insert or upsert a conflict turned into a no-op — whatever a parent-side `create` inserted
|
|
1730
1858
|
for it is rolled back too, so no orphan is left. In a batch only that entry is undone
|
|
1859
|
+
- An upsert whose conflict branch does not write the parent's join column (an
|
|
1860
|
+
`onConflict.update` that leaves it out) would update the existing row without attaching
|
|
1861
|
+
what a parent-side `create` or `connect` produced. That fails with
|
|
1862
|
+
`DRIZZLE_NESTED_WRITE_INVALID` and rolls the write back; list the column in
|
|
1863
|
+
`onConflict.update`, or leave `update` unset so every supplied column is written
|
|
1731
1864
|
- The returned rows reflect the nested write: on a self-referencing relation
|
|
1732
1865
|
(`reports: { connect: … }` on the row itself), where a child-side statement rewrites the
|
|
1733
1866
|
parent row, the parent rows are read back after it
|
|
@@ -1740,7 +1873,9 @@ A relation declared with a `where` (`publishedPosts: r.many.posts({ …, where:
|
|
|
1740
1873
|
is written through the same way it is read: only the rows it shows.
|
|
1741
1874
|
|
|
1742
1875
|
- `disconnect`, `set`, and the detaching a to-one attach does, reach only rows that satisfy
|
|
1743
|
-
the `where` — `publishedPosts: { set: [] }` leaves the drafts attached
|
|
1876
|
+
the `where` — `publishedPosts: { set: [] }` leaves the drafts attached. On a to-one whose
|
|
1877
|
+
key is on the parent, `disconnect` leaves a key that points at a row the `where` hides,
|
|
1878
|
+
and a `connect` that would replace such a key fails with `DRIZZLE_NESTED_WRITE_INVALID`
|
|
1744
1879
|
- `connect` and `set` attach only rows that satisfy it, so the relation shows what was
|
|
1745
1880
|
attached. A to-one `connect` must match exactly one such row
|
|
1746
1881
|
- There is no `create`: the inserted row is attached whether or not it satisfies the
|
|
@@ -1749,7 +1884,9 @@ is written through the same way it is read: only the rows it shows.
|
|
|
1749
1884
|
- A many-to-many `set` unlinks only the links to rows the `where` shows
|
|
1750
1885
|
- A relation that inherits its `where` from the other side (declared without `from` / `to`)
|
|
1751
1886
|
filters the parent rather than the target. Writing through it on a parent row it is
|
|
1752
|
-
hidden for fails with `DRIZZLE_NESTED_WRITE_INVALID
|
|
1887
|
+
hidden for fails with `DRIZZLE_NESTED_WRITE_INVALID`. When the key is on the parent
|
|
1888
|
+
(`connect`, `create` or `disconnect` on a to-one), the row is judged as the write leaves
|
|
1889
|
+
it, so a write that also makes the row visible goes through
|
|
1753
1890
|
|
|
1754
1891
|
### The target table's own rules
|
|
1755
1892
|
|
|
@@ -1945,7 +2082,9 @@ deliberate rather than accidental:
|
|
|
1945
2082
|
The plural mutations still accept a missing `where` (a full-table write) by default. To rule
|
|
1946
2083
|
that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
|
|
1947
2084
|
`update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
|
|
1948
|
-
Single variants. It
|
|
2085
|
+
Single variants. It also covers `update<Table>Many`: an entry without a `where`, or with one
|
|
2086
|
+
that has no filters, rejects the whole batch with `DRIZZLE_WHERE_REQUIRED` before any entry
|
|
2087
|
+
runs. It defaults to `false` for backwards compatibility.
|
|
1949
2088
|
|
|
1950
2089
|
## Bounding page size
|
|
1951
2090
|
|
|
@@ -2014,8 +2153,8 @@ So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 1
|
|
|
2014
2153
|
same query without limits costs `10 * (1 + 10 * 1)` = 110.
|
|
2015
2154
|
|
|
2016
2155
|
A [limit policy](#bounding-page-size) feeds into the hint: a field with no `limit` is priced at
|
|
2017
|
-
its `defaultLimit`
|
|
2018
|
-
cost matches the rows the query can actually pull.
|
|
2156
|
+
its `defaultLimit` (or at `maxLimit` when the policy sets only that) rather than `defaultListSize`,
|
|
2157
|
+
and nothing is priced above `maxLimit`, so the cost matches the rows the query can actually pull.
|
|
2019
2158
|
|
|
2020
2159
|
The hints do nothing until you install a complexity rule, so they are generated by default:
|
|
2021
2160
|
|
|
@@ -2274,7 +2413,13 @@ are untouched.
|
|
|
2274
2413
|
does this) — it keys the per-request transaction state
|
|
2275
2414
|
- `timeoutMs` (default `30000`) rolls back an abandoned transaction, e.g. when the host
|
|
2276
2415
|
server stopped calling resolvers because of a non-null completion error, instead of
|
|
2277
|
-
leaking the connection
|
|
2416
|
+
leaking the connection. It must be a positive integer up to `2147483647` (the longest
|
|
2417
|
+
delay `setTimeout` honours), or `Infinity` to turn the timer off; `buildSchema` throws for
|
|
2418
|
+
anything else. With the timer off an abandoned transaction keeps its connection, so prefer
|
|
2419
|
+
a finite value. Fields after a timeout fail with `DRIZZLE_TRANSACTION_ABORTED`, naming the
|
|
2420
|
+
timeout as the cause
|
|
2421
|
+
- A root field whose arguments fail to coerce is answered with an error without its resolver
|
|
2422
|
+
running, so it is not waited for: the transaction commits once the other fields complete
|
|
2278
2423
|
- A document that mixes in mutation fields this build did not generate is left alone: their
|
|
2279
2424
|
completion cannot be tracked, so no transaction is opened
|
|
2280
2425
|
- It never nests — an executor supplied on the context wins, as below
|
|
@@ -2412,6 +2557,10 @@ The payload:
|
|
|
2412
2557
|
not have to know which.
|
|
2413
2558
|
- **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
|
|
2414
2559
|
already does.
|
|
2560
|
+
- **SQLite needs an asynchronous driver** (libsql, D1). A synchronous one (better-sqlite3,
|
|
2561
|
+
`node:sqlite`) commits before an awaited hook runs, so a throw could not roll anything
|
|
2562
|
+
back; `buildSchema` throws for `onWrite` on one, as it does for `nestedWrites` and
|
|
2563
|
+
`transactions: 'auto'`.
|
|
2415
2564
|
- **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
|
|
2416
2565
|
The `before` position and `args` still work there.
|
|
2417
2566
|
- With `nestedWrites`, the hooks of **every table a nested write reaches** fire too —
|
|
@@ -2569,7 +2718,9 @@ const finalSchema = addResolversToSchema({
|
|
|
2569
2718
|
excludes that relation from `with:` (and from the mutation eager re-fetch).
|
|
2570
2719
|
|
|
2571
2720
|
Opting a relation out does **not** remove its field — it keeps resolving lazily via the
|
|
2572
|
-
request-scoped batch loader, so the field still works even before you override it
|
|
2721
|
+
request-scoped batch loader, so the field still works even before you override it — in a
|
|
2722
|
+
mutation payload as well, where the write returns the join column the relation correlates on
|
|
2723
|
+
even though the selection never named it. A
|
|
2573
2724
|
many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
|
|
2574
2725
|
lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
|
|
2575
2726
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|