@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 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 not written through
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 not written through
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: