@vantreeseba/drizzle-graphql 13.1.0 → 14.0.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
@@ -189,14 +189,16 @@ const { schema } = buildSchema(db, {
189
189
  - The table is still fully readable — this decides which operations it offers, not whether
190
190
  it is in the schema at all. For that, use `exclude` below
191
191
  - `nestedWrites` is the one flag that stays build-wide: its plans are computed once over
192
- the whole relation graph, and a nested write reaches a second table by definition
192
+ the whole relation graph, and a nested write reaches a second table by definition. What
193
+ it offers on each relation still follows the *target* table's flags — see
194
+ [Nested writes](#nested-writes)
193
195
  - `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
194
196
  - Some operations are built out of others, and the generator knows which. A table that
195
197
  asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
196
198
  path past the operation you turned off — warns on the console at build time, naming the
197
199
  tables, rather than leaving that to be discovered later. So does asking for `updateMany`
198
- without `update`, `groupBy` without `aggregates`, `countMutations` with neither plural
199
- write, or `nestedWrites` alongside a table whose own writes are off
200
+ without `update`, `groupBy` without `aggregates`, or `countMutations` with neither plural
201
+ write. `nestedWrites` notes the nested inputs it left out because their table's write is off
200
202
 
201
203
  ## Excluding tables and columns
202
204
 
@@ -1531,6 +1533,32 @@ mutation {
1531
1533
  - A `set` that carries only relation fields is a valid update: it changes what the row is
1532
1534
  attached to without rewriting the row
1533
1535
 
1536
+ ### The target table's own rules
1537
+
1538
+ A nested write is a write to another table, so it follows that table's rules, not just the
1539
+ root field's:
1540
+
1541
+ - **`scope`** narrows the rows `connect` / `disconnect` / `set` can reach, and
1542
+ **`contextValues`** stamp the rows a nested `create` inserts
1543
+ - **`features`**: each nested input is generated only when the table it writes has that
1544
+ write turned on. Whatever is left out is noted on the console at build time, and a
1545
+ relation with nothing left loses its field altogether
1546
+
1547
+ | Input | Needs, on the table it writes |
1548
+ | --- | --- |
1549
+ | `create` | the target's `insert` (and its `update` too on a to-one whose nullable key lives on the target, since attaching detaches the previous row) |
1550
+ | `connect` / `disconnect` / `set`, key on the target | the target's `update` |
1551
+ | `connect` / `disconnect`, key on the parent | nothing — only the parent's own row changes |
1552
+ | many-to-many `connect` | the junction's `insert` |
1553
+ | many-to-many `disconnect` | the junction's `delete` |
1554
+ | many-to-many `set` | the junction's `insert` and `delete` |
1555
+
1556
+ An excluded junction table takes its relation's inputs with it. A table whose writes are
1557
+ reserved for a hand-written mutation (`insert: false`) can therefore no longer be written
1558
+ by another table's nested `create` either
1559
+ - **`onWrite`**: the hooks of the table a nested statement writes run too — see
1560
+ [Nested writes and hooks](#nested-writes-and-hooks)
1561
+
1534
1562
  ### Many-to-many relations
1535
1563
 
1536
1564
  A relation declared with `.through()` is written by inserting and deleting rows of its
@@ -1623,9 +1651,27 @@ mutation {
1623
1651
  - Both return `Int!` — the number of rows the write affected — and add no new output type
1624
1652
  to the schema
1625
1653
  - Arguments are exactly the plural mutation's, so a client swaps one field name for the
1626
- other; `features.requireWhere` applies to them the same way
1654
+ other; `features.requireWhere` applies to them the same way, and on a soft-deleting
1655
+ table that opted into `hardDelete` the delete takes `hard` too
1627
1656
  - They follow the feature switch of the write they mirror: `updateCount` needs
1628
1657
  `features.update`, `deleteCount` needs `features.delete`
1658
+ - They obey the same row policies as the plural write — only the answer differs:
1659
+ - [`scope`](#row-scoping-and-multi-tenancy) is ANDed into the `where`, so they reach,
1660
+ and count, only in-scope rows
1661
+ - on a [soft-deleting](#soft-delete) table `delete<Table>Count` marks the rows rather
1662
+ than removing them, and neither mutation reaches a row that is already marked
1663
+ - a [`contextValues`](#row-scoping-and-multi-tenancy) column is never reassigned by the
1664
+ update
1665
+ - [`onWrite`](#write-hooks) hooks run `before` and `after`, in a transaction, with the
1666
+ plural write's payload (`operation: 'update'` / `'delete'`, `single: false`). An
1667
+ `after` hook is handed rows, so a table with one gives up the returnless write: the
1668
+ statement `RETURNING`s the hook's `columns`, or just the primary key when it names
1669
+ none (every column, on a table without a key), and the count is the number of rows
1670
+ returned. Without an `after` hook there is no `RETURNING` at all. On MySQL the hook
1671
+ gets `rows: []`, as it does on every MySQL mutation
1672
+ - Their [`extensions.drizzle`](#identifying-generated-fields) is the plural write's
1673
+ (`operation: 'update'` / `'delete'`, `targetArg: 'where'`) plus `count: true`, so a
1674
+ wrapper that gates writes on `operation` covers them too
1629
1675
  - **MySQL** gets them too, and they are arguably most useful there: MySQL's other mutations
1630
1676
  return only `MutationReturn { isSuccess }`, so a row count is strictly more information
1631
1677
  - `update<Table>Count` **rejects** nested writes with a `GraphQLError` — a nested write
@@ -2122,6 +2168,9 @@ onWrite: {
2122
2168
  works.
2123
2169
  - The columns are added to `RETURNING` for every write the hook is attached to: insert, upsert,
2124
2170
  update, `updateMany`, delete and restore. A delete's rows are the rows it removed.
2171
+ - The [count mutations](#row-counts-instead-of-rows) fire the hooks as `update` / `delete`.
2172
+ The client selects nothing there, so an `after` hook's rows hold exactly its `columns`, or
2173
+ just the primary key when it names none — the one `RETURNING` those mutations pay for.
2125
2174
  - **The client's response is unchanged.** Columns the hook asked for but the client did not
2126
2175
  select are taken back off before the rows are mapped, so they reach neither the response nor
2127
2176
  a custom resolver calling the generated one. The hook gets its own copy of the rows.
@@ -2140,6 +2189,7 @@ The payload:
2140
2189
  | `rows` | the post-write rows **as the database returned them**, before the output mapper — the selected columns plus the hook's [`columns`](#which-columns-rows-holds) |
2141
2190
  | `context`, `info` | the GraphQL context value and resolve info |
2142
2191
  | `tx` | the executor the mutation ran on |
2192
+ | `nested` | `{ parentTable, relation, op }` when a [nested write](#nested-writes-and-hooks) wrote the rows; `undefined` at the mutation field's own table |
2143
2193
 
2144
2194
  - **`tx` is the executor the mutation itself used** — one you supplied under
2145
2195
  `drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
@@ -2149,11 +2199,58 @@ The payload:
2149
2199
  already does.
2150
2200
  - **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
2151
2201
  The `before` position and `args` still work there.
2152
- - The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
2153
- another table do not fire that table's hook.
2202
+ - With `nestedWrites`, the hooks of **every table a nested write reaches** fire too —
2203
+ below.
2154
2204
 
2155
2205
  A build with no `onWrite` opens exactly the transactions it did before.
2156
2206
 
2207
+ ### Nested writes and hooks
2208
+
2209
+ A nested `create` / `connect` / `disconnect` / `set` runs the `before` and `after` hooks of the
2210
+ table its statement writes, on the same `tx`, with `payload.nested` saying where it came from:
2211
+
2212
+ ```Typescript
2213
+ onWrite: {
2214
+ todos: {
2215
+ before: async ({ nested, args, tx }) => { /* runs ahead of the nested statement too */ },
2216
+ after: async ({ operation, rows, nested, tx }) => {
2217
+ // createProjectsSingle(values: { …, todos: { create: [ … ] } }) arrives here as
2218
+ // operation: 'insert', nested: { parentTable: 'projects', relation: 'todos', op: 'create' }
2219
+ for (const row of rows) await assertOwnsProject(tx, row.projectId)
2220
+ },
2221
+ columns: ['projectId'],
2222
+ },
2223
+ }
2224
+ ```
2225
+
2226
+ | Nested input | `table` | `operation` |
2227
+ | --- | --- | --- |
2228
+ | `create` | the target | `insert` |
2229
+ | `connect` / `disconnect` / `set`, key on the target | the target | `update` (the foreign key moved; nothing is deleted) |
2230
+ | `connect` / `disconnect`, key on the parent | — | no target hook: only the parent's row changes, and its own hook sees that |
2231
+ | many-to-many `connect` | the junction | `insert` |
2232
+ | many-to-many `disconnect` | the junction | `delete` |
2233
+ | many-to-many `set` | the junction | `delete` (the old links), then `insert` (the new ones) |
2234
+
2235
+ - **`nested`** is `{ parentTable, relation, op }` — the Drizzle schema key of the table whose
2236
+ input carried the relation field, the relation's name, and which of its inputs this is.
2237
+ It is `undefined` at the mutation field's own table, so a hook that only cares about root
2238
+ writes starts with `if (payload.nested) return`
2239
+ - **One call per input and operation**, not per row: a nested `create` of three rows is one
2240
+ `insert` call with three rows, and an update whose `where` matched several parents hands
2241
+ the hook every child it wrote across all of them. A to-one `connect` that also detaches
2242
+ the row attached before is one `update` call holding both rows; a detached row's key is
2243
+ `null`
2244
+ - **`rows`** hold the table's primary key and the join column(s) the statement changed — a
2245
+ nested write has no client selection of its own — plus whatever the hook names in
2246
+ `columns`
2247
+ - **`args`, `context` and `info`** are the root mutation field's; **`single`** says whether the
2248
+ relation is a to-one
2249
+ - **`before`** runs ahead of that input's first statement, **`after`** once its last has
2250
+ returned, and both inside the transaction every nested write opens — so a hook on a child
2251
+ table can roll the whole tree back, the parent row included, even when the root field has
2252
+ no hook of its own
2253
+
2157
2254
 
2158
2255
  ## Relations & N+1 handling
2159
2256
 
@@ -2329,7 +2426,9 @@ the check to make before treating a field as one of ours. Relation fields (and
2329
2426
  naming the relation's **target** — so `Users.posts` reads as
2330
2427
  `{ table: 'Posts', parentTable: 'Users', relation: 'posts' }`. Object types carry
2331
2428
  `{ table, kind: 'type', primaryKey }`, which is what a stitching layer needs to map a type
2332
- back to its source.
2429
+ back to its source. The [count mutations](#row-counts-instead-of-rows) carry their plural
2430
+ write's `operation` plus `count: true` — they write the same rows but resolve to an `Int!`, so
2431
+ a wrapper that reads the result as rows should check `count` first.
2333
2432
 
2334
2433
  Because the row identity of a mutation lives in a different argument for every operation,
2335
2434
  `identifyRows` reads whichever one applies and hands back primary-key values: