@vantreeseba/drizzle-graphql 15.0.0 → 15.1.1
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 +117 -68
- package/dist/index.cjs +204 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +204 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
- Pass your drizzle database instance
|
|
9
|
+
- Pass your drizzle database instance into the builder to generate a `{ schema, entities }` object. The tables are read from the `relations` you gave the drizzle constructor (`drizzle({ client, relations })`), so a table missing from your relations config is not part of the generated schema
|
|
10
10
|
- Use `schema` if pre-built schema already satisfies all your needs. It's compatible with any server that consumes `GraphQLSchema` class instance
|
|
11
11
|
|
|
12
12
|
Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
|
@@ -39,6 +39,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
39
39
|
|
|
40
40
|
// Schema contains 'Users' and 'Customers' tables
|
|
41
41
|
import { db } from './database'
|
|
42
|
+
import { users } from './schema'
|
|
42
43
|
|
|
43
44
|
const { entities } = buildSchema(db)
|
|
44
45
|
|
|
@@ -63,7 +64,7 @@ Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-
|
|
|
63
64
|
},
|
|
64
65
|
resolve: async (source, args, context, info) => {
|
|
65
66
|
// Your custom logic goes here...
|
|
66
|
-
const result = await db.select(
|
|
67
|
+
const result = await db.select().from(users).where(/* … */)
|
|
67
68
|
|
|
68
69
|
return result
|
|
69
70
|
}
|
|
@@ -168,8 +169,9 @@ const { schema } = buildSchema(db, {
|
|
|
168
169
|
turns the group-by queries off with it
|
|
169
170
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
170
171
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
171
|
-
- Turning off
|
|
172
|
-
`mutations: false`
|
|
172
|
+
- Turning off every mutation feature omits the `Mutation` type entirely, the same as
|
|
173
|
+
`mutations: false` — unless a table has `softDelete`, whose `restore` mutations are
|
|
174
|
+
generated regardless
|
|
173
175
|
- List and single queries are always generated, so `Query` is never empty
|
|
174
176
|
|
|
175
177
|
### Per-table operations
|
|
@@ -344,9 +346,7 @@ Together the two close the loop: `contextValues` decides which tenant a new row
|
|
|
344
346
|
|
|
345
347
|
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
346
348
|
build instead of quietly ceasing to be scoped.
|
|
347
|
-
|
|
348
|
-
> or an equivalent layer above the schema, which has the request context this library
|
|
349
|
-
> does not.
|
|
349
|
+
|
|
350
350
|
## Naming
|
|
351
351
|
|
|
352
352
|
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
@@ -463,7 +463,7 @@ Keyed by the **Drizzle schema key**, like `scope` and `exclude`. A function form
|
|
|
463
463
|
accepted, for a convention applied across the schema:
|
|
464
464
|
|
|
465
465
|
```Typescript
|
|
466
|
-
softDelete: (table, tableName) => ('deletedAt' in
|
|
466
|
+
softDelete: (table, tableName) => ('deletedAt' in getColumns(table) ? 'deletedAt' : undefined)
|
|
467
467
|
```
|
|
468
468
|
|
|
469
469
|
What a declared table gets:
|
|
@@ -612,6 +612,7 @@ generated SDL says what a field actually holds instead of falling back to `Strin
|
|
|
612
612
|
| `timestamp` / `datetime` (either `mode`) | `DateTime` | an ISO-8601 string |
|
|
613
613
|
| SQLite `integer({ mode: 'timestamp' })` / `'timestamp_ms'` | `DateTime` | an ISO-8601 string |
|
|
614
614
|
| `date` (either `mode`) | `Date` | a `YYYY-MM-DD` string |
|
|
615
|
+
| `numeric` / `decimal` | `Decimal` | a decimal string |
|
|
615
616
|
|
|
616
617
|
```graphql
|
|
617
618
|
mutation {
|
|
@@ -629,6 +630,8 @@ mutation {
|
|
|
629
630
|
`Number.MAX_SAFE_INTEGER` survive the round-trip. Integer literals are accepted on input;
|
|
630
631
|
floats and non-numeric strings are rejected
|
|
631
632
|
- **`UUID`** validates the format on the way in, including inside `where` filters
|
|
633
|
+
- **`Decimal`** is a decimal string in both directions, so the precision the column declares
|
|
634
|
+
is not lost to a JS float. Non-numeric and non-finite input is rejected
|
|
632
635
|
- **`Date`** is a calendar day — no time, no offset — and is the same type in every position
|
|
633
636
|
a `date` column appears in: the object field, the create and update inputs, the
|
|
634
637
|
`DateFilter` operands, aggregate `min` / `max` and group keys. It takes and returns an
|
|
@@ -656,7 +659,7 @@ mutation {
|
|
|
656
659
|
The scalars are exported if you need them in a hand-written schema:
|
|
657
660
|
|
|
658
661
|
```Typescript
|
|
659
|
-
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
662
|
+
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLDecimalString, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
660
663
|
```
|
|
661
664
|
|
|
662
665
|
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
@@ -781,7 +784,7 @@ only has to handle the cases it cares about:
|
|
|
781
784
|
```Typescript
|
|
782
785
|
const { schema } = buildSchema(db, {
|
|
783
786
|
// `schema` is the Postgres schema the enum was declared in, if any; `values` is its
|
|
784
|
-
// value list.
|
|
787
|
+
// value list. `enumName` and `schema` are undefined for an inline value list.
|
|
785
788
|
enumNameMapper: ({ enumName, schema }) =>
|
|
786
789
|
schema === 'reporting' ? `Reporting${enumName}Enum` : undefined,
|
|
787
790
|
})
|
|
@@ -861,9 +864,40 @@ const { schema } = buildSchema(db, {
|
|
|
861
864
|
|
|
862
865
|
- `describeColumn` reaches every place the column appears: the object type, the create and
|
|
863
866
|
update inputs, the filter input's field, `Min`/`Max` aggregates and `GroupKeys`
|
|
864
|
-
-
|
|
865
|
-
|
|
866
|
-
straight off
|
|
867
|
+
- `describeColumn` receives the drizzle column itself and `describeTable` the drizzle table
|
|
868
|
+
itself, so a project that keeps its documentation in the schema (a `WeakMap` keyed by
|
|
869
|
+
column/table, a custom property, a comment table) can read it straight off:
|
|
870
|
+
|
|
871
|
+
```Typescript
|
|
872
|
+
import type { Table } from 'drizzle-orm';
|
|
873
|
+
|
|
874
|
+
const docs = new WeakMap<object, string>();
|
|
875
|
+
|
|
876
|
+
// Register docs on the *built* table and its columns. `text('email')` is only a builder:
|
|
877
|
+
// pgTable creates a new column object from it, so keying on the builder never matches.
|
|
878
|
+
const described = <T extends Table>(
|
|
879
|
+
table: T,
|
|
880
|
+
description: string,
|
|
881
|
+
columns: Partial<Record<keyof T['_']['columns'], string>> = {},
|
|
882
|
+
): T => {
|
|
883
|
+
docs.set(table, description);
|
|
884
|
+
for (const [key, columnText] of Object.entries(columns)) {
|
|
885
|
+
docs.set((table as any)[key], columnText as string);
|
|
886
|
+
}
|
|
887
|
+
return table;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export const users = described(
|
|
891
|
+
pgTable('users', { id: integer('id').primaryKey(), email: text('email').notNull() }),
|
|
892
|
+
'A person who can sign in',
|
|
893
|
+
{ email: 'Primary login address' },
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
buildSchema(db, {
|
|
897
|
+
describeColumn: (column) => docs.get(column),
|
|
898
|
+
describeTable: (_tableName, table) => docs.get(table),
|
|
899
|
+
});
|
|
900
|
+
```
|
|
867
901
|
- `deprecateColumn` marks output fields and *optional* input fields. A required input field
|
|
868
902
|
is skipped: GraphQL forbids deprecating one, and emitting it would make the schema invalid
|
|
869
903
|
- Filter and `orderBy` fields are never deprecated — filtering on a deprecated column is how
|
|
@@ -1008,6 +1042,11 @@ The pattern operators are `like` / `notLike` / `ilike` / `notIlike`, the safe
|
|
|
1008
1042
|
`startsWith` / `endsWith` / `contains` and their `i`-prefixed forms, plus the `insensitive`
|
|
1009
1043
|
flag that modifies them.
|
|
1010
1044
|
|
|
1045
|
+
An operand of `null` leaves its operator out, as though it had not been sent. `false` is an
|
|
1046
|
+
ordinary value: `active: { eq: false }` matches the rows that are false, `ne: false` the rows
|
|
1047
|
+
that are true, and `has: false` the arrays holding a `false`. Only the flag operators —
|
|
1048
|
+
`isNull`, `isNotNull` and `isEmpty` — read `false` as "add no condition".
|
|
1049
|
+
|
|
1011
1050
|
The omissions are the cases where the operator cannot ask anything a caller means:
|
|
1012
1051
|
|
|
1013
1052
|
- **Pattern matching a timestamp, a boolean or an enum member** compares whatever the value
|
|
@@ -1090,7 +1129,8 @@ that column's `inArray` list instead of by the column's value:
|
|
|
1090
1129
|
## Filtering inside JSON documents
|
|
1091
1130
|
|
|
1092
1131
|
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
1093
|
-
document, alongside the whole-document `eq` / `ne`
|
|
1132
|
+
document, alongside the whole-document `eq` / `ne` operators and, on PostgreSQL and MySQL,
|
|
1133
|
+
structural `contains` (SQLite stores JSON as text and has no containment operator):
|
|
1094
1134
|
|
|
1095
1135
|
```graphql
|
|
1096
1136
|
{
|
|
@@ -1256,9 +1296,10 @@ columns you group by.
|
|
|
1256
1296
|
}
|
|
1257
1297
|
```
|
|
1258
1298
|
|
|
1259
|
-
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max`
|
|
1260
|
-
|
|
1261
|
-
|
|
1299
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` take one
|
|
1300
|
+
filter per numeric column (the comparison is numeric, so a `max` over a text or date
|
|
1301
|
+
column has no `having` entry even though the aggregate type returns it),
|
|
1302
|
+
`countNonNull` one per column, and `countDistinct` one per column `countDistinct` exposes
|
|
1262
1303
|
- Every entry in a `having` is ANDed together
|
|
1263
1304
|
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
1264
1305
|
whether or not you asked for `count`
|
|
@@ -1297,6 +1338,8 @@ can count or summarise related rows without fetching them:
|
|
|
1297
1338
|
- A parent with no related rows gets `count: 0` and `null` for every other aggregate
|
|
1298
1339
|
- To-one relations get no aggregate field — there is nothing to aggregate over
|
|
1299
1340
|
- The field is skipped if its name would collide with a column or another relation
|
|
1341
|
+
- Set `features: { relationAggregates: false }` (globally or per table) to leave these
|
|
1342
|
+
fields out
|
|
1300
1343
|
|
|
1301
1344
|
All parents in a selection are aggregated with a single
|
|
1302
1345
|
`SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
|
|
@@ -1320,7 +1363,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
|
|
|
1320
1363
|
```
|
|
1321
1364
|
|
|
1322
1365
|
- Which row survives each group is decided by `orderBy` — the first one wins. With no
|
|
1323
|
-
`orderBy`,
|
|
1366
|
+
`orderBy`, the table's [default ordering](#default-ordering) decides, and failing that the
|
|
1367
|
+
lowest primary key
|
|
1324
1368
|
- `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
|
|
1325
1369
|
**after**, so `limit: 10` returns ten distinct rows
|
|
1326
1370
|
- Several columns are treated as one combined key, not as independent ones
|
|
@@ -1339,7 +1383,7 @@ or **SQLite 3.25+**), and a table with no primary key cannot use it.
|
|
|
1339
1383
|
SQL gives no ordering guarantee for a query that has no `ORDER BY`, so paging through an
|
|
1340
1384
|
unordered result can return the same row twice or skip one entirely. To keep pages stable,
|
|
1341
1385
|
a query that returns only part of a table is ordered by its primary key when the request
|
|
1342
|
-
supplies no `orderBy
|
|
1386
|
+
supplies no `orderBy` and the table has no [default ordering](#default-ordering):
|
|
1343
1387
|
|
|
1344
1388
|
- list queries with `limit` and/or `offset`
|
|
1345
1389
|
- `<tableName>Single` queries, which are an implicit `limit 1`
|
|
@@ -1347,7 +1391,8 @@ supplies no `orderBy`:
|
|
|
1347
1391
|
`orderBy` you do supply, so per-parent slices are deterministic)
|
|
1348
1392
|
|
|
1349
1393
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
1350
|
-
key column, and an unpaginated list query
|
|
1394
|
+
key column, and an unpaginated list query of a table with no `defaults` entry is left
|
|
1395
|
+
unordered so no sort is paid for.
|
|
1351
1396
|
|
|
1352
1397
|
## Default ordering
|
|
1353
1398
|
|
|
@@ -1434,14 +1479,21 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1434
1479
|
window
|
|
1435
1480
|
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
1436
1481
|
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
1437
|
-
- `NULL`s in ordered columns
|
|
1438
|
-
|
|
1439
|
-
`asc`)
|
|
1440
|
-
through, not skipped
|
|
1441
|
-
|
|
1482
|
+
- `NULL`s in ordered columns sort where the entry's `nulls: first | last` says or, without
|
|
1483
|
+
it, at each dialect's native `ORDER BY` placement — PostgreSQL sorts them largest (last
|
|
1484
|
+
in `asc`), MySQL and SQLite smallest (first in `asc`). The predicate matches either way,
|
|
1485
|
+
so rows with `NULL` in an ordered column are paged through, not skipped, and `nulls` is
|
|
1486
|
+
part of the ordering a cursor is tied to
|
|
1487
|
+
- Dates and bigints round-trip losslessly through the cursor, at the column's own precision:
|
|
1488
|
+
a PostgreSQL `timestamp` / `timestamptz` (microseconds unless declared `precision: 3` or
|
|
1489
|
+
less) or a MySQL `datetime` / `timestamp` with `fsp` above 3 holds more than the
|
|
1490
|
+
millisecond JS `Date` that `mode: 'date'` reads it into, so the cursor carries the
|
|
1491
|
+
database's own text of the value instead, and rows a microsecond apart are still distinct
|
|
1492
|
+
pages
|
|
1442
1493
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1443
1494
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1444
|
-
- `after` cannot be combined with `distinct
|
|
1495
|
+
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
1496
|
+
that orders through a relation (`cursor` resolves to `null` there), and a table with
|
|
1445
1497
|
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
1446
1498
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
1447
1499
|
skipped
|
|
@@ -1595,8 +1647,10 @@ mutation {
|
|
|
1595
1647
|
}
|
|
1596
1648
|
```
|
|
1597
1649
|
|
|
1598
|
-
- `connect`, `disconnect` and `set` take the target's own `Filters` input
|
|
1599
|
-
|
|
1650
|
+
- `connect`, `disconnect` and `set` take the target's own `Filters` input — a list of them
|
|
1651
|
+
on a to-many relation, a single one that must match exactly one row on a to-one — so they
|
|
1652
|
+
select rows the same way a query does. A to-one `disconnect` is just `disconnect: true`,
|
|
1653
|
+
since there is only one row to detach. A filter that matches everything is rejected rather than
|
|
1600
1654
|
attaching the whole table
|
|
1601
1655
|
- `set` replaces the relation wholesale — everything attached is detached first, then the
|
|
1602
1656
|
filters attach. It runs before `create`, so a row created in the same write survives it
|
|
@@ -1847,6 +1901,10 @@ Two details worth stating plainly:
|
|
|
1847
1901
|
- **A rejection is the default because truncation lies.** Silently cutting a page down to
|
|
1848
1902
|
`maxLimit` tells a paginating client it has reached the end when it has not. `clampToMax:
|
|
1849
1903
|
true` opts into that when you would rather never fail a query.
|
|
1904
|
+
- **A negative `limit` or `offset` is always rejected** with `DRIZZLE_INVALID_INPUT_VALUE`,
|
|
1905
|
+
whether or not `limits` is configured — on root lists, single queries, relation fields and
|
|
1906
|
+
`resolveSelection`. SQLite reads `LIMIT -1` as "no limit", so a negative value would
|
|
1907
|
+
otherwise walk straight past `maxLimit`; `0` stays valid and returns no rows.
|
|
1850
1908
|
|
|
1851
1909
|
The policy applies to root list queries and to to-many relation fields, on both the eager and
|
|
1852
1910
|
the lazy relation paths. A relation takes the policy of the table it *reads*, not its parent's:
|
|
@@ -1944,8 +2002,8 @@ const yoga = createYoga({ schema, plugins: armor.protect().plugins });
|
|
|
1944
2002
|
|
|
1945
2003
|
Armor's `costLimit` is its own document-shape estimate and does not read the `complexity`
|
|
1946
2004
|
extensions above; the two compose, if you want a cheap static ceiling alongside row-count
|
|
1947
|
-
aware pricing. Neither one bounds an individual `limit` argument
|
|
1948
|
-
|
|
2005
|
+
aware pricing. Neither one bounds an individual `limit` argument — a list query with no
|
|
2006
|
+
`limit` is still a full table scan unless you set a [`limits`](#bounding-page-size) policy.
|
|
1949
2007
|
|
|
1950
2008
|
## Authorization
|
|
1951
2009
|
|
|
@@ -1987,21 +2045,17 @@ request context actually lives.
|
|
|
1987
2045
|
or through your own resolver wrapper, and let the ability answer the questions.
|
|
1988
2046
|
|
|
1989
2047
|
What neither can do is **narrow** a query. They permit or deny a field; they do not append
|
|
1990
|
-
`WHERE
|
|
1991
|
-
returns every tenant's rows.
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
wrapped;
|
|
1998
|
-
- put the scoped tables behind database row-level security and give the driver the
|
|
1999
|
-
request's identity, so the narrowing happens below Drizzle entirely.
|
|
2048
|
+
`WHERE org_id = …` to the SQL a generated resolver runs, so a permitted `posts` query still
|
|
2049
|
+
returns every tenant's rows. That is what [`scope`](#row-scoping-and-multi-tenancy) is for: a
|
|
2050
|
+
per-table predicate built from the request context and ANDed onto every read and write of
|
|
2051
|
+
that table, nested relation fields included. Pair it with
|
|
2052
|
+
[`contextValues`](#columns-the-server-owns) so new rows are stamped with the caller's tenant.
|
|
2053
|
+
The usual split is shield (or CASL) to decide who may reach a field, and `scope` to decide
|
|
2054
|
+
which rows they see through it.
|
|
2000
2055
|
|
|
2001
2056
|
None of this removes anything from introspection either — `deny` hides the data, not the
|
|
2002
|
-
shape. If the schema must not so much as mention a table,
|
|
2003
|
-
|
|
2004
|
-
cheap).
|
|
2057
|
+
shape. If the schema must not so much as mention a table or column, leave it out with
|
|
2058
|
+
[`exclude`](#excluding-tables-and-columns).
|
|
2005
2059
|
|
|
2006
2060
|
Nor is it a row bound: a complexity ceiling prices the `limit` a client asks for, it does not
|
|
2007
2061
|
cap it. Use [`limits`](#bounding-page-size) for that.
|
|
@@ -2074,7 +2128,8 @@ to match on prose:
|
|
|
2074
2128
|
`DrizzleErrorContext`.
|
|
2075
2129
|
|
|
2076
2130
|
A value the request could not supply is `DRIZZLE_INVALID_INPUT_VALUE` — a date that does not
|
|
2077
|
-
parse, a `BigInt` argument that is not an integer, a malformed JSON column value
|
|
2131
|
+
parse, a `BigInt` argument that is not an integer, a malformed JSON column value, a negative
|
|
2132
|
+
`limit` or `offset` — and a write
|
|
2078
2133
|
naming something that is not a column of its table is `DRIZZLE_UNKNOWN_COLUMN`. The mirror
|
|
2079
2134
|
image, a stored value the scalar transporting it cannot represent, is
|
|
2080
2135
|
`DRIZZLE_UNREPRESENTABLE_VALUE`; it says the data is out of range, not that the request was
|
|
@@ -2142,6 +2197,9 @@ are untouched.
|
|
|
2142
2197
|
- A document that mixes in mutation fields this build did not generate is left alone: their
|
|
2143
2198
|
completion cannot be tracked, so no transaction is opened
|
|
2144
2199
|
- It never nests — an executor supplied on the context wins, as below
|
|
2200
|
+
- A single mutation field can still run in a transaction of its own when it carries a
|
|
2201
|
+
nested write or its table has an `onWrite` hook — those open one regardless of this
|
|
2202
|
+
setting
|
|
2145
2203
|
|
|
2146
2204
|
### Supplying your own executor
|
|
2147
2205
|
|
|
@@ -2176,17 +2234,6 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
|
|
|
2176
2234
|
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
2177
2235
|
both end up loaded in one process.
|
|
2178
2236
|
|
|
2179
|
-
Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
|
|
2180
|
-
more than one generated mutation in a single transaction, committed when the last one
|
|
2181
|
-
completes and rolled back if any of them fails.
|
|
2182
|
-
|
|
2183
|
-
```Typescript
|
|
2184
|
-
buildSchema(db, { transactions: 'auto' })
|
|
2185
|
-
```
|
|
2186
|
-
|
|
2187
|
-
A document with one mutation, or one that selects a mutation field this build did not
|
|
2188
|
-
generate, still runs unwrapped — there is nothing to keep atomic.
|
|
2189
|
-
|
|
2190
2237
|
## Write hooks
|
|
2191
2238
|
|
|
2192
2239
|
`onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
|
|
@@ -2347,12 +2394,12 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2347
2394
|
relation in a single round-trip using Drizzle's relational query builder
|
|
2348
2395
|
(`with:`), including nested relations and per-relation `where` / `orderBy` /
|
|
2349
2396
|
`limit` / `offset` arguments.
|
|
2350
|
-
- **Mutations (PostgreSQL & SQLite)** — after an insert
|
|
2397
|
+
- **Mutations (PostgreSQL & SQLite)** — after an insert, upsert, update or `updateMany`, if the selection set
|
|
2351
2398
|
includes relation fields, the affected rows are re-fetched by primary key through one
|
|
2352
2399
|
relational query so their relations are eagerly loaded (single- and composite-column
|
|
2353
|
-
primary keys are supported). `delete` mutations keep using the
|
|
2354
|
-
|
|
2355
|
-
|
|
2400
|
+
primary keys are supported). `delete` and `restore` mutations keep using the
|
|
2401
|
+
request-scoped batch loader. MySQL mutations return only a success flag (no row
|
|
2402
|
+
payload), so this step does not apply there.
|
|
2356
2403
|
- **Custom schemas** — each relation field also has a standalone resolver, exported as
|
|
2357
2404
|
`entities.fieldResolvers[TableName][relationName]`. When you wire generated types
|
|
2358
2405
|
into your own schema and your root resolver does **not** pre-fetch relations, these
|
|
@@ -2369,9 +2416,8 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2369
2416
|
|
|
2370
2417
|
> [!IMPORTANT]
|
|
2371
2418
|
> Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
|
|
2372
|
-
> rely on SQL window functions. These require **PostgreSQL**,
|
|
2373
|
-
> **SQLite 3.25+**.
|
|
2374
|
-
> have no such requirement.
|
|
2419
|
+
> and the `distinct` argument rely on SQL window functions. These require **PostgreSQL**,
|
|
2420
|
+
> **MySQL 8.0+**, or **SQLite 3.25+**. Everything else has no such requirement.
|
|
2375
2421
|
|
|
2376
2422
|
### Relation nullability
|
|
2377
2423
|
|
|
@@ -2432,7 +2478,9 @@ const finalSchema = addResolversToSchema({
|
|
|
2432
2478
|
excludes that relation from `with:` (and from the mutation eager re-fetch).
|
|
2433
2479
|
|
|
2434
2480
|
Opting a relation out does **not** remove its field — it keeps resolving lazily via the
|
|
2435
|
-
request-scoped batch loader, so the field still works even before you override it.
|
|
2481
|
+
request-scoped batch loader, so the field still works even before you override it. A
|
|
2482
|
+
many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
|
|
2483
|
+
lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
|
|
2436
2484
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
2437
2485
|
of `entities.fieldResolvers`.
|
|
2438
2486
|
|
|
@@ -2475,9 +2523,10 @@ into its nested entry, and leaves `<relation>Aggregate` selections out — they
|
|
|
2475
2523
|
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
2476
2524
|
and returns the rows already remapped to their GraphQL transport forms.
|
|
2477
2525
|
|
|
2478
|
-
Both take
|
|
2479
|
-
|
|
2480
|
-
that renames its types has to say so.
|
|
2526
|
+
Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
|
|
2527
|
+
`typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
|
|
2528
|
+
published GraphQL type names, so a build that renames its types has to say so here too. If
|
|
2529
|
+
the build used `typeNameMapper: 'singularize'`, pass the exported `singularizeMapper`.
|
|
2481
2530
|
|
|
2482
2531
|
## Identifying generated fields
|
|
2483
2532
|
|
|
@@ -2548,7 +2597,7 @@ that explains it, where there is one.
|
|
|
2548
2597
|
| `mutations` | `true` | `false` omits the `Mutation` type entirely |
|
|
2549
2598
|
| `features` | see [Choosing what gets generated](#choosing-what-gets-generated) | which operations are generated, per operation |
|
|
2550
2599
|
| `exclude` | none | [tables and columns left out](#excluding-tables-and-columns) of the generated schema entirely |
|
|
2551
|
-
| `relationsDepthLimit` | unlimited |
|
|
2600
|
+
| `relationsDepthLimit` | unlimited | `0` generates no relation fields (a flat, columns-only schema); any other value currently behaves like the default |
|
|
2552
2601
|
| `prefixes` / `suffixes` / `typeNameMapper` | see [Naming](#naming) | operation and type names |
|
|
2553
2602
|
| `typeNamePrefix` / `typeNameSuffix` / `derivedTypeNameMapper` | none | [names of the derived types](#renaming-the-derived-types) — filters, inputs, aggregates, shared inputs |
|
|
2554
2603
|
| `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
|
|
@@ -2556,7 +2605,7 @@ that explains it, where there is one.
|
|
|
2556
2605
|
| `enumValueNameMapper` | the stored value | [names the enum's values](#enum-value-names); `'constant'` for CONSTANT_CASE |
|
|
2557
2606
|
| `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
|
|
2558
2607
|
| `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
|
|
2559
|
-
| `conflictDoNothing` | `false` | PostgreSQL
|
|
2608
|
+
| `conflictDoNothing` | `false` | **Deprecated** — use [`onConflict: { action: NOTHING }`](#upsert) per request. PostgreSQL and SQLite: `ON CONFLICT DO NOTHING` on every insert, which makes `create<Table>Single` nullable; ignored on MySQL |
|
|
2560
2609
|
| `complexity` | on | [cost hints](#query-cost) in field extensions; `false` turns them off |
|
|
2561
2610
|
| `transactions` | `'none'` | `'auto'` wraps a multi-mutation request in one [transaction](#transactions) |
|
|
2562
2611
|
| `limits` | unbounded | [default and maximum page size](#bounding-page-size), globally or per table |
|