@vantreeseba/drizzle-graphql 7.7.0 → 7.8.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
@@ -1130,12 +1130,46 @@ mutation {
1130
1130
  `author: { create: … }` available, `authorId` is one of two ways to supply it
1131
1131
  - Nesting is one level deep: the row a nested `create` inserts takes columns only, and the
1132
1132
  join column is left off, since the write it is part of sets it
1133
- - Many-to-many (`.through()`) relations are not written through
1133
+ - Many-to-many (`.through()`) relations are written through their junction table — see
1134
+ below
1134
1135
  - The whole tree runs in one transaction — a savepoint when the request already carries one
1135
1136
  — so a failure anywhere leaves nothing behind
1136
1137
  - A `set` that carries only relation fields is a valid update: it changes what the row is
1137
1138
  attached to without rewriting the row
1138
1139
 
1140
+ ### Many-to-many relations
1141
+
1142
+ A relation declared with `.through()` is written by inserting and deleting rows of its
1143
+ junction table, so nothing about the two rows being linked changes:
1144
+
1145
+ ```graphql
1146
+ mutation {
1147
+ updatePostsSingle(
1148
+ where: { id: { eq: 1 } }
1149
+ set: { tags: { set: [{ name: { inArray: ["news", "release"] } }] } }
1150
+ ) {
1151
+ id
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ - It offers `connect` on the create input, and `connect`, `disconnect` and `set` on the
1157
+ update input. All three take the target's `Filters`, exactly as on a foreign-key relation
1158
+ - `disconnect` and `set` are offered unconditionally here: unlinking deletes a junction row
1159
+ rather than writing `NULL` into a column that might be `NOT NULL`
1160
+ - `connect` links every row its filters match and leaves already-linked rows alone, so
1161
+ re-connecting is a no-op rather than a unique-constraint error
1162
+ - `disconnect` deletes only the links, never the rows they pointed at
1163
+ - `set: []` unlinks everything and links nothing back, which is how a many-to-many relation
1164
+ is cleared
1165
+ - There is no `create`: inserting a row *and* linking it is a write to two tables, and the
1166
+ link is what the relation field is for. Create the row with its own mutation, then
1167
+ `connect` it
1168
+ - A relation is left out when its junction carries a further `NOT NULL` column with no
1169
+ default — a filter alone could not supply a value for it — and when it is declared as a
1170
+ to-one, whose "exactly one" guarantee lives in the junction's constraints rather than in
1171
+ the relation
1172
+
1139
1173
  Dialect support: **PostgreSQL**, and **SQLite** on an asynchronous driver (libsql, D1). A
1140
1174
  nested write reads back the key of the row it just wrote, which MySQL has no `RETURNING` for,
1141
1175
  and interleaves awaited statements inside a transaction, which a synchronous SQLite driver
@@ -1555,6 +1589,81 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
1555
1589
  key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
1556
1590
  both end up loaded in one process.
1557
1591
 
1592
+ Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
1593
+ more than one generated mutation in a single transaction, committed when the last one
1594
+ completes and rolled back if any of them fails.
1595
+
1596
+ ```Typescript
1597
+ buildSchema(db, { transactions: 'auto' })
1598
+ ```
1599
+
1600
+ A document with one mutation, or one that selects a mutation field this build did not
1601
+ generate, still runs unwrapped — there is nothing to keep atomic.
1602
+
1603
+ ## Write hooks
1604
+
1605
+ `onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
1606
+ with the mutation itself. That is the position an audit row, an outbox row or a denormalized
1607
+ counter needs, and the one a `graphql-middleware` wrapper cannot occupy — a wrapper sits
1608
+ outside the resolver, so its after-position is after the commit.
1609
+
1610
+ ```Typescript
1611
+ const { schema } = buildSchema(db, {
1612
+ onWrite: {
1613
+ posts: async ({ table, operation, rows, tx }) => {
1614
+ await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
1615
+ },
1616
+ },
1617
+ })
1618
+ ```
1619
+
1620
+ A bare function is the **`after`** hook — the position that has rows. `{ before, after }` names
1621
+ them explicitly:
1622
+
1623
+ ```Typescript
1624
+ onWrite: {
1625
+ posts: {
1626
+ // Reads inside the same snapshot the write will use.
1627
+ before: async ({ args, tx }) => {
1628
+ const [parent] = await tx.select().from(orgs).where(eq(orgs.id, args.values[0].orgId))
1629
+ if (!parent) throw new Error('org went away')
1630
+ },
1631
+ after: async ({ rows, tx }) => { /* … */ },
1632
+ },
1633
+ }
1634
+ ```
1635
+
1636
+ Registering a bare function (or a positions object) at the top level applies it to every table;
1637
+ anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
1638
+ matches nothing throws at build time.
1639
+
1640
+ The payload:
1641
+
1642
+ | Field | |
1643
+ | --- | --- |
1644
+ | `table` | Drizzle schema key of the table being written |
1645
+ | `operation` | `'insert' \| 'update' \| 'updateMany' \| 'upsert' \| 'delete' \| 'restore'` |
1646
+ | `position` | `'before'` or `'after'` |
1647
+ | `single` | whether the field writes one row or a list |
1648
+ | `args` | the field's GraphQL arguments, as the resolver received them |
1649
+ | `rows` | the post-write rows **as the database returned them**, before the output mapper |
1650
+ | `context`, `info` | the GraphQL context value and resolve info |
1651
+ | `tx` | the executor the mutation ran on |
1652
+
1653
+ - **`tx` is the executor the mutation itself used** — one you supplied under
1654
+ `drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
1655
+ have run unwrapped, a transaction opened for it *because* the hook is there. The hook does
1656
+ not have to know which.
1657
+ - **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
1658
+ already does.
1659
+ - **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
1660
+ The `before` position and `args` still work there.
1661
+ - The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
1662
+ another table do not fire that table's hook.
1663
+
1664
+ A build with no `onWrite` opens exactly the transactions it did before.
1665
+
1666
+
1558
1667
  ## Relations & N+1 handling
1559
1668
 
1560
1669
  Generated schemas resolve nested relations without N+1 query explosions:
package/dist/README.md CHANGED
@@ -1130,12 +1130,46 @@ mutation {
1130
1130
  `author: { create: … }` available, `authorId` is one of two ways to supply it
1131
1131
  - Nesting is one level deep: the row a nested `create` inserts takes columns only, and the
1132
1132
  join column is left off, since the write it is part of sets it
1133
- - Many-to-many (`.through()`) relations are not written through
1133
+ - Many-to-many (`.through()`) relations are written through their junction table — see
1134
+ below
1134
1135
  - The whole tree runs in one transaction — a savepoint when the request already carries one
1135
1136
  — so a failure anywhere leaves nothing behind
1136
1137
  - A `set` that carries only relation fields is a valid update: it changes what the row is
1137
1138
  attached to without rewriting the row
1138
1139
 
1140
+ ### Many-to-many relations
1141
+
1142
+ A relation declared with `.through()` is written by inserting and deleting rows of its
1143
+ junction table, so nothing about the two rows being linked changes:
1144
+
1145
+ ```graphql
1146
+ mutation {
1147
+ updatePostsSingle(
1148
+ where: { id: { eq: 1 } }
1149
+ set: { tags: { set: [{ name: { inArray: ["news", "release"] } }] } }
1150
+ ) {
1151
+ id
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ - It offers `connect` on the create input, and `connect`, `disconnect` and `set` on the
1157
+ update input. All three take the target's `Filters`, exactly as on a foreign-key relation
1158
+ - `disconnect` and `set` are offered unconditionally here: unlinking deletes a junction row
1159
+ rather than writing `NULL` into a column that might be `NOT NULL`
1160
+ - `connect` links every row its filters match and leaves already-linked rows alone, so
1161
+ re-connecting is a no-op rather than a unique-constraint error
1162
+ - `disconnect` deletes only the links, never the rows they pointed at
1163
+ - `set: []` unlinks everything and links nothing back, which is how a many-to-many relation
1164
+ is cleared
1165
+ - There is no `create`: inserting a row *and* linking it is a write to two tables, and the
1166
+ link is what the relation field is for. Create the row with its own mutation, then
1167
+ `connect` it
1168
+ - A relation is left out when its junction carries a further `NOT NULL` column with no
1169
+ default — a filter alone could not supply a value for it — and when it is declared as a
1170
+ to-one, whose "exactly one" guarantee lives in the junction's constraints rather than in
1171
+ the relation
1172
+
1139
1173
  Dialect support: **PostgreSQL**, and **SQLite** on an asynchronous driver (libsql, D1). A
1140
1174
  nested write reads back the key of the row it just wrote, which MySQL has no `RETURNING` for,
1141
1175
  and interleaves awaited statements inside a transaction, which a synchronous SQLite driver
@@ -1555,6 +1589,81 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
1555
1589
  key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
1556
1590
  both end up loaded in one process.
1557
1591
 
1592
+ Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
1593
+ more than one generated mutation in a single transaction, committed when the last one
1594
+ completes and rolled back if any of them fails.
1595
+
1596
+ ```Typescript
1597
+ buildSchema(db, { transactions: 'auto' })
1598
+ ```
1599
+
1600
+ A document with one mutation, or one that selects a mutation field this build did not
1601
+ generate, still runs unwrapped — there is nothing to keep atomic.
1602
+
1603
+ ## Write hooks
1604
+
1605
+ `onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
1606
+ with the mutation itself. That is the position an audit row, an outbox row or a denormalized
1607
+ counter needs, and the one a `graphql-middleware` wrapper cannot occupy — a wrapper sits
1608
+ outside the resolver, so its after-position is after the commit.
1609
+
1610
+ ```Typescript
1611
+ const { schema } = buildSchema(db, {
1612
+ onWrite: {
1613
+ posts: async ({ table, operation, rows, tx }) => {
1614
+ await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })))
1615
+ },
1616
+ },
1617
+ })
1618
+ ```
1619
+
1620
+ A bare function is the **`after`** hook — the position that has rows. `{ before, after }` names
1621
+ them explicitly:
1622
+
1623
+ ```Typescript
1624
+ onWrite: {
1625
+ posts: {
1626
+ // Reads inside the same snapshot the write will use.
1627
+ before: async ({ args, tx }) => {
1628
+ const [parent] = await tx.select().from(orgs).where(eq(orgs.id, args.values[0].orgId))
1629
+ if (!parent) throw new Error('org went away')
1630
+ },
1631
+ after: async ({ rows, tx }) => { /* … */ },
1632
+ },
1633
+ }
1634
+ ```
1635
+
1636
+ Registering a bare function (or a positions object) at the top level applies it to every table;
1637
+ anything else is read as a table map keyed by the **Drizzle schema key**, and a name that
1638
+ matches nothing throws at build time.
1639
+
1640
+ The payload:
1641
+
1642
+ | Field | |
1643
+ | --- | --- |
1644
+ | `table` | Drizzle schema key of the table being written |
1645
+ | `operation` | `'insert' \| 'update' \| 'updateMany' \| 'upsert' \| 'delete' \| 'restore'` |
1646
+ | `position` | `'before'` or `'after'` |
1647
+ | `single` | whether the field writes one row or a list |
1648
+ | `args` | the field's GraphQL arguments, as the resolver received them |
1649
+ | `rows` | the post-write rows **as the database returned them**, before the output mapper |
1650
+ | `context`, `info` | the GraphQL context value and resolve info |
1651
+ | `tx` | the executor the mutation ran on |
1652
+
1653
+ - **`tx` is the executor the mutation itself used** — one you supplied under
1654
+ `drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
1655
+ have run unwrapped, a transaction opened for it *because* the hook is there. The hook does
1656
+ not have to know which.
1657
+ - **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
1658
+ already does.
1659
+ - **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
1660
+ The `before` position and `args` still work there.
1661
+ - The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
1662
+ another table do not fire that table's hook.
1663
+
1664
+ A build with no `onWrite` opens exactly the transactions it did before.
1665
+
1666
+
1558
1667
  ## Relations & N+1 handling
1559
1668
 
1560
1669
  Generated schemas resolve nested relations without N+1 query explosions: