@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 +110 -1
- package/dist/README.md +110 -1
- package/dist/index.cjs +1500 -835
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -3
- package/dist/index.d.ts +88 -3
- package/dist/index.js +1501 -836
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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:
|