@vantreeseba/drizzle-graphql 7.7.0 → 7.9.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 +160 -1
- package/dist/README.md +160 -1
- package/dist/index.cjs +1589 -855
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -3
- package/dist/index.d.ts +148 -3
- package/dist/index.js +1590 -856
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -945,6 +945,56 @@ supplies no `orderBy`:
|
|
|
945
945
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
946
946
|
key column, and an unpaginated list query is left unordered so no sort is paid for.
|
|
947
947
|
|
|
948
|
+
## Default ordering
|
|
949
|
+
|
|
950
|
+
A table usually has one ordering that is right nearly every time it is read — newest first,
|
|
951
|
+
priority first, name alphabetical. `defaults` names that ordering once, and every query that
|
|
952
|
+
does not ask for its own gets it:
|
|
953
|
+
|
|
954
|
+
```ts
|
|
955
|
+
buildSchema(db, {
|
|
956
|
+
defaults: {
|
|
957
|
+
Todos: { orderBy: { priority: 'desc', createdAt: 'desc' } },
|
|
958
|
+
Users: { orderBy: { name: 'asc' } },
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
```graphql
|
|
964
|
+
# ordered by priority desc, then createdAt desc — nothing in the query says so
|
|
965
|
+
{ todos { id title } }
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Keys are the table's key in your Drizzle schema and the column's property name, so a typo in
|
|
969
|
+
either fails at `buildSchema` time rather than silently doing nothing. Each column takes
|
|
970
|
+
`'asc'`, `'desc'`, or a `{ direction, priority? }` object mirroring the `orderBy` argument.
|
|
971
|
+
Columns sort highest `priority` first, and the shorthand leaves it unset, so the shorthand
|
|
972
|
+
falls back to the order the keys are written in — the object form is there for when you would
|
|
973
|
+
rather state the precedence than depend on that:
|
|
974
|
+
|
|
975
|
+
```ts
|
|
976
|
+
defaults: {
|
|
977
|
+
Todos: { orderBy: { createdAt: { direction: 'desc', priority: 1 }, priority: { direction: 'desc', priority: 2 } } },
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
It applies to list queries, `<tableName>Single` queries, and to-many relation fields on both
|
|
982
|
+
the eager and the lazy relation paths — a relation takes the default of the table it *reads*,
|
|
983
|
+
not its parent's. To-one relation fields are a single row and take no ordering.
|
|
984
|
+
|
|
985
|
+
Two details worth stating plainly:
|
|
986
|
+
|
|
987
|
+
- **Any `orderBy` in the request wins outright.** The default is a fallback for a missing
|
|
988
|
+
argument, not a base that client ordering is appended to. To ask for *no* ordering, pass
|
|
989
|
+
`orderBy: {}` — an empty object is still an argument, so it suppresses the default.
|
|
990
|
+
- **A default ordering makes an unpaginated list query pay for a sort.** That is the point,
|
|
991
|
+
but it is a cost a table with no `defaults` entry does not pay; a build with no `defaults`
|
|
992
|
+
behaves exactly as before.
|
|
993
|
+
|
|
994
|
+
Cursor pagination composes with it: a default-ordered page is ordered by those columns plus
|
|
995
|
+
the primary key tiebreak, and its `cursor` values encode that same total order, so paging works
|
|
996
|
+
without the client restating the ordering.
|
|
997
|
+
|
|
948
998
|
## Cursor pagination
|
|
949
999
|
|
|
950
1000
|
`OFFSET` pagination degrades linearly and shifts under concurrent writes. List queries also
|
|
@@ -1130,12 +1180,46 @@ mutation {
|
|
|
1130
1180
|
`author: { create: … }` available, `authorId` is one of two ways to supply it
|
|
1131
1181
|
- Nesting is one level deep: the row a nested `create` inserts takes columns only, and the
|
|
1132
1182
|
join column is left off, since the write it is part of sets it
|
|
1133
|
-
- Many-to-many (`.through()`) relations are
|
|
1183
|
+
- Many-to-many (`.through()`) relations are written through their junction table — see
|
|
1184
|
+
below
|
|
1134
1185
|
- The whole tree runs in one transaction — a savepoint when the request already carries one
|
|
1135
1186
|
— so a failure anywhere leaves nothing behind
|
|
1136
1187
|
- A `set` that carries only relation fields is a valid update: it changes what the row is
|
|
1137
1188
|
attached to without rewriting the row
|
|
1138
1189
|
|
|
1190
|
+
### Many-to-many relations
|
|
1191
|
+
|
|
1192
|
+
A relation declared with `.through()` is written by inserting and deleting rows of its
|
|
1193
|
+
junction table, so nothing about the two rows being linked changes:
|
|
1194
|
+
|
|
1195
|
+
```graphql
|
|
1196
|
+
mutation {
|
|
1197
|
+
updatePostsSingle(
|
|
1198
|
+
where: { id: { eq: 1 } }
|
|
1199
|
+
set: { tags: { set: [{ name: { inArray: ["news", "release"] } }] } }
|
|
1200
|
+
) {
|
|
1201
|
+
id
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
- It offers `connect` on the create input, and `connect`, `disconnect` and `set` on the
|
|
1207
|
+
update input. All three take the target's `Filters`, exactly as on a foreign-key relation
|
|
1208
|
+
- `disconnect` and `set` are offered unconditionally here: unlinking deletes a junction row
|
|
1209
|
+
rather than writing `NULL` into a column that might be `NOT NULL`
|
|
1210
|
+
- `connect` links every row its filters match and leaves already-linked rows alone, so
|
|
1211
|
+
re-connecting is a no-op rather than a unique-constraint error
|
|
1212
|
+
- `disconnect` deletes only the links, never the rows they pointed at
|
|
1213
|
+
- `set: []` unlinks everything and links nothing back, which is how a many-to-many relation
|
|
1214
|
+
is cleared
|
|
1215
|
+
- There is no `create`: inserting a row *and* linking it is a write to two tables, and the
|
|
1216
|
+
link is what the relation field is for. Create the row with its own mutation, then
|
|
1217
|
+
`connect` it
|
|
1218
|
+
- A relation is left out when its junction carries a further `NOT NULL` column with no
|
|
1219
|
+
default — a filter alone could not supply a value for it — and when it is declared as a
|
|
1220
|
+
to-one, whose "exactly one" guarantee lives in the junction's constraints rather than in
|
|
1221
|
+
the relation
|
|
1222
|
+
|
|
1139
1223
|
Dialect support: **PostgreSQL**, and **SQLite** on an asynchronous driver (libsql, D1). A
|
|
1140
1224
|
nested write reads back the key of the row it just wrote, which MySQL has no `RETURNING` for,
|
|
1141
1225
|
and interleaves awaited statements inside a transaction, which a synchronous SQLite driver
|
|
@@ -1555,6 +1639,81 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
|
|
|
1555
1639
|
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
1556
1640
|
both end up loaded in one process.
|
|
1557
1641
|
|
|
1642
|
+
Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
|
|
1643
|
+
more than one generated mutation in a single transaction, committed when the last one
|
|
1644
|
+
completes and rolled back if any of them fails.
|
|
1645
|
+
|
|
1646
|
+
```Typescript
|
|
1647
|
+
buildSchema(db, { transactions: 'auto' })
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
A document with one mutation, or one that selects a mutation field this build did not
|
|
1651
|
+
generate, still runs unwrapped — there is nothing to keep atomic.
|
|
1652
|
+
|
|
1653
|
+
## Write hooks
|
|
1654
|
+
|
|
1655
|
+
`onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
|
|
1656
|
+
with the mutation itself. That is the position an audit row, an outbox row or a denormalized
|
|
1657
|
+
counter needs, and the one a `graphql-middleware` wrapper cannot occupy — a wrapper sits
|
|
1658
|
+
outside the resolver, so its after-position is after the commit.
|
|
1659
|
+
|
|
1660
|
+
```Typescript
|
|
1661
|
+
const { schema } = buildSchema(db, {
|
|
1662
|
+
onWrite: {
|
|
1663
|
+
posts: async ({ table, operation, rows, tx }) => {
|
|
1664
|
+
await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
})
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
A bare function is the **`after`** hook — the position that has rows. `{ before, after }` names
|
|
1671
|
+
them explicitly:
|
|
1672
|
+
|
|
1673
|
+
```Typescript
|
|
1674
|
+
onWrite: {
|
|
1675
|
+
posts: {
|
|
1676
|
+
// Reads inside the same snapshot the write will use.
|
|
1677
|
+
before: async ({ args, tx }) => {
|
|
1678
|
+
const [parent] = await tx.select().from(orgs).where(eq(orgs.id, args.values[0].orgId))
|
|
1679
|
+
if (!parent) throw new Error('org went away')
|
|
1680
|
+
},
|
|
1681
|
+
after: async ({ rows, tx }) => { /* … */ },
|
|
1682
|
+
},
|
|
1683
|
+
}
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
Registering a bare function (or a positions object) at the top level applies it to every table;
|
|
1687
|
+
anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
|
|
1688
|
+
matches nothing throws at build time.
|
|
1689
|
+
|
|
1690
|
+
The payload:
|
|
1691
|
+
|
|
1692
|
+
| Field | |
|
|
1693
|
+
| --- | --- |
|
|
1694
|
+
| `table` | Drizzle schema key of the table being written |
|
|
1695
|
+
| `operation` | `'insert' \| 'update' \| 'updateMany' \| 'upsert' \| 'delete' \| 'restore'` |
|
|
1696
|
+
| `position` | `'before'` or `'after'` |
|
|
1697
|
+
| `single` | whether the field writes one row or a list |
|
|
1698
|
+
| `args` | the field's GraphQL arguments, as the resolver received them |
|
|
1699
|
+
| `rows` | the post-write rows **as the database returned them**, before the output mapper |
|
|
1700
|
+
| `context`, `info` | the GraphQL context value and resolve info |
|
|
1701
|
+
| `tx` | the executor the mutation ran on |
|
|
1702
|
+
|
|
1703
|
+
- **`tx` is the executor the mutation itself used** — one you supplied under
|
|
1704
|
+
`drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
|
|
1705
|
+
have run unwrapped, a transaction opened for it *because* the hook is there. The hook does
|
|
1706
|
+
not have to know which.
|
|
1707
|
+
- **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
|
|
1708
|
+
already does.
|
|
1709
|
+
- **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
|
|
1710
|
+
The `before` position and `args` still work there.
|
|
1711
|
+
- The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
|
|
1712
|
+
another table do not fire that table's hook.
|
|
1713
|
+
|
|
1714
|
+
A build with no `onWrite` opens exactly the transactions it did before.
|
|
1715
|
+
|
|
1716
|
+
|
|
1558
1717
|
## Relations & N+1 handling
|
|
1559
1718
|
|
|
1560
1719
|
Generated schemas resolve nested relations without N+1 query explosions:
|
package/dist/README.md
CHANGED
|
@@ -945,6 +945,56 @@ supplies no `orderBy`:
|
|
|
945
945
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
946
946
|
key column, and an unpaginated list query is left unordered so no sort is paid for.
|
|
947
947
|
|
|
948
|
+
## Default ordering
|
|
949
|
+
|
|
950
|
+
A table usually has one ordering that is right nearly every time it is read — newest first,
|
|
951
|
+
priority first, name alphabetical. `defaults` names that ordering once, and every query that
|
|
952
|
+
does not ask for its own gets it:
|
|
953
|
+
|
|
954
|
+
```ts
|
|
955
|
+
buildSchema(db, {
|
|
956
|
+
defaults: {
|
|
957
|
+
Todos: { orderBy: { priority: 'desc', createdAt: 'desc' } },
|
|
958
|
+
Users: { orderBy: { name: 'asc' } },
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
```graphql
|
|
964
|
+
# ordered by priority desc, then createdAt desc — nothing in the query says so
|
|
965
|
+
{ todos { id title } }
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Keys are the table's key in your Drizzle schema and the column's property name, so a typo in
|
|
969
|
+
either fails at `buildSchema` time rather than silently doing nothing. Each column takes
|
|
970
|
+
`'asc'`, `'desc'`, or a `{ direction, priority? }` object mirroring the `orderBy` argument.
|
|
971
|
+
Columns sort highest `priority` first, and the shorthand leaves it unset, so the shorthand
|
|
972
|
+
falls back to the order the keys are written in — the object form is there for when you would
|
|
973
|
+
rather state the precedence than depend on that:
|
|
974
|
+
|
|
975
|
+
```ts
|
|
976
|
+
defaults: {
|
|
977
|
+
Todos: { orderBy: { createdAt: { direction: 'desc', priority: 1 }, priority: { direction: 'desc', priority: 2 } } },
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
It applies to list queries, `<tableName>Single` queries, and to-many relation fields on both
|
|
982
|
+
the eager and the lazy relation paths — a relation takes the default of the table it *reads*,
|
|
983
|
+
not its parent's. To-one relation fields are a single row and take no ordering.
|
|
984
|
+
|
|
985
|
+
Two details worth stating plainly:
|
|
986
|
+
|
|
987
|
+
- **Any `orderBy` in the request wins outright.** The default is a fallback for a missing
|
|
988
|
+
argument, not a base that client ordering is appended to. To ask for *no* ordering, pass
|
|
989
|
+
`orderBy: {}` — an empty object is still an argument, so it suppresses the default.
|
|
990
|
+
- **A default ordering makes an unpaginated list query pay for a sort.** That is the point,
|
|
991
|
+
but it is a cost a table with no `defaults` entry does not pay; a build with no `defaults`
|
|
992
|
+
behaves exactly as before.
|
|
993
|
+
|
|
994
|
+
Cursor pagination composes with it: a default-ordered page is ordered by those columns plus
|
|
995
|
+
the primary key tiebreak, and its `cursor` values encode that same total order, so paging works
|
|
996
|
+
without the client restating the ordering.
|
|
997
|
+
|
|
948
998
|
## Cursor pagination
|
|
949
999
|
|
|
950
1000
|
`OFFSET` pagination degrades linearly and shifts under concurrent writes. List queries also
|
|
@@ -1130,12 +1180,46 @@ mutation {
|
|
|
1130
1180
|
`author: { create: … }` available, `authorId` is one of two ways to supply it
|
|
1131
1181
|
- Nesting is one level deep: the row a nested `create` inserts takes columns only, and the
|
|
1132
1182
|
join column is left off, since the write it is part of sets it
|
|
1133
|
-
- Many-to-many (`.through()`) relations are
|
|
1183
|
+
- Many-to-many (`.through()`) relations are written through their junction table — see
|
|
1184
|
+
below
|
|
1134
1185
|
- The whole tree runs in one transaction — a savepoint when the request already carries one
|
|
1135
1186
|
— so a failure anywhere leaves nothing behind
|
|
1136
1187
|
- A `set` that carries only relation fields is a valid update: it changes what the row is
|
|
1137
1188
|
attached to without rewriting the row
|
|
1138
1189
|
|
|
1190
|
+
### Many-to-many relations
|
|
1191
|
+
|
|
1192
|
+
A relation declared with `.through()` is written by inserting and deleting rows of its
|
|
1193
|
+
junction table, so nothing about the two rows being linked changes:
|
|
1194
|
+
|
|
1195
|
+
```graphql
|
|
1196
|
+
mutation {
|
|
1197
|
+
updatePostsSingle(
|
|
1198
|
+
where: { id: { eq: 1 } }
|
|
1199
|
+
set: { tags: { set: [{ name: { inArray: ["news", "release"] } }] } }
|
|
1200
|
+
) {
|
|
1201
|
+
id
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
- It offers `connect` on the create input, and `connect`, `disconnect` and `set` on the
|
|
1207
|
+
update input. All three take the target's `Filters`, exactly as on a foreign-key relation
|
|
1208
|
+
- `disconnect` and `set` are offered unconditionally here: unlinking deletes a junction row
|
|
1209
|
+
rather than writing `NULL` into a column that might be `NOT NULL`
|
|
1210
|
+
- `connect` links every row its filters match and leaves already-linked rows alone, so
|
|
1211
|
+
re-connecting is a no-op rather than a unique-constraint error
|
|
1212
|
+
- `disconnect` deletes only the links, never the rows they pointed at
|
|
1213
|
+
- `set: []` unlinks everything and links nothing back, which is how a many-to-many relation
|
|
1214
|
+
is cleared
|
|
1215
|
+
- There is no `create`: inserting a row *and* linking it is a write to two tables, and the
|
|
1216
|
+
link is what the relation field is for. Create the row with its own mutation, then
|
|
1217
|
+
`connect` it
|
|
1218
|
+
- A relation is left out when its junction carries a further `NOT NULL` column with no
|
|
1219
|
+
default — a filter alone could not supply a value for it — and when it is declared as a
|
|
1220
|
+
to-one, whose "exactly one" guarantee lives in the junction's constraints rather than in
|
|
1221
|
+
the relation
|
|
1222
|
+
|
|
1139
1223
|
Dialect support: **PostgreSQL**, and **SQLite** on an asynchronous driver (libsql, D1). A
|
|
1140
1224
|
nested write reads back the key of the row it just wrote, which MySQL has no `RETURNING` for,
|
|
1141
1225
|
and interleaves awaited statements inside a transaction, which a synchronous SQLite driver
|
|
@@ -1555,6 +1639,81 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
|
|
|
1555
1639
|
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
1556
1640
|
both end up loaded in one process.
|
|
1557
1641
|
|
|
1642
|
+
Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
|
|
1643
|
+
more than one generated mutation in a single transaction, committed when the last one
|
|
1644
|
+
completes and rolled back if any of them fails.
|
|
1645
|
+
|
|
1646
|
+
```Typescript
|
|
1647
|
+
buildSchema(db, { transactions: 'auto' })
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
A document with one mutation, or one that selects a mutation field this build did not
|
|
1651
|
+
generate, still runs unwrapped — there is nothing to keep atomic.
|
|
1652
|
+
|
|
1653
|
+
## Write hooks
|
|
1654
|
+
|
|
1655
|
+
`onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
|
|
1656
|
+
with the mutation itself. That is the position an audit row, an outbox row or a denormalized
|
|
1657
|
+
counter needs, and the one a `graphql-middleware` wrapper cannot occupy — a wrapper sits
|
|
1658
|
+
outside the resolver, so its after-position is after the commit.
|
|
1659
|
+
|
|
1660
|
+
```Typescript
|
|
1661
|
+
const { schema } = buildSchema(db, {
|
|
1662
|
+
onWrite: {
|
|
1663
|
+
posts: async ({ table, operation, rows, tx }) => {
|
|
1664
|
+
await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
})
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
A bare function is the **`after`** hook — the position that has rows. `{ before, after }` names
|
|
1671
|
+
them explicitly:
|
|
1672
|
+
|
|
1673
|
+
```Typescript
|
|
1674
|
+
onWrite: {
|
|
1675
|
+
posts: {
|
|
1676
|
+
// Reads inside the same snapshot the write will use.
|
|
1677
|
+
before: async ({ args, tx }) => {
|
|
1678
|
+
const [parent] = await tx.select().from(orgs).where(eq(orgs.id, args.values[0].orgId))
|
|
1679
|
+
if (!parent) throw new Error('org went away')
|
|
1680
|
+
},
|
|
1681
|
+
after: async ({ rows, tx }) => { /* … */ },
|
|
1682
|
+
},
|
|
1683
|
+
}
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
Registering a bare function (or a positions object) at the top level applies it to every table;
|
|
1687
|
+
anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
|
|
1688
|
+
matches nothing throws at build time.
|
|
1689
|
+
|
|
1690
|
+
The payload:
|
|
1691
|
+
|
|
1692
|
+
| Field | |
|
|
1693
|
+
| --- | --- |
|
|
1694
|
+
| `table` | Drizzle schema key of the table being written |
|
|
1695
|
+
| `operation` | `'insert' \| 'update' \| 'updateMany' \| 'upsert' \| 'delete' \| 'restore'` |
|
|
1696
|
+
| `position` | `'before'` or `'after'` |
|
|
1697
|
+
| `single` | whether the field writes one row or a list |
|
|
1698
|
+
| `args` | the field's GraphQL arguments, as the resolver received them |
|
|
1699
|
+
| `rows` | the post-write rows **as the database returned them**, before the output mapper |
|
|
1700
|
+
| `context`, `info` | the GraphQL context value and resolve info |
|
|
1701
|
+
| `tx` | the executor the mutation ran on |
|
|
1702
|
+
|
|
1703
|
+
- **`tx` is the executor the mutation itself used** — one you supplied under
|
|
1704
|
+
`drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
|
|
1705
|
+
have run unwrapped, a transaction opened for it *because* the hook is there. The hook does
|
|
1706
|
+
not have to know which.
|
|
1707
|
+
- **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
|
|
1708
|
+
already does.
|
|
1709
|
+
- **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
|
|
1710
|
+
The `before` position and `args` still work there.
|
|
1711
|
+
- The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
|
|
1712
|
+
another table do not fire that table's hook.
|
|
1713
|
+
|
|
1714
|
+
A build with no `onWrite` opens exactly the transactions it did before.
|
|
1715
|
+
|
|
1716
|
+
|
|
1558
1717
|
## Relations & N+1 handling
|
|
1559
1718
|
|
|
1560
1719
|
Generated schemas resolve nested relations without N+1 query explosions:
|