@vantreeseba/drizzle-graphql 7.4.0 → 7.6.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 +189 -0
- package/dist/README.md +189 -0
- package/dist/index.cjs +997 -247
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -2
- package/dist/index.d.ts +283 -2
- package/dist/index.js +992 -246
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,96 @@ table becomes readable but not insertable — and warns on the console, since th
|
|
|
237
237
|
often a mistake than a plan.
|
|
238
238
|
|
|
239
239
|
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
240
|
+
> request *which fields* a caller may reach is a job for
|
|
241
|
+
> [`graphql-shield`](https://the-guild.dev/graphql/shield) or an equivalent layer above the
|
|
242
|
+
> schema. Deciding per request *which rows* a caller may reach is `scope`, below — that one a
|
|
243
|
+
> layer above cannot do, because a nested relation field never passes through a root resolver.
|
|
244
|
+
|
|
245
|
+
## Row scoping and multi-tenancy
|
|
246
|
+
|
|
247
|
+
`scope` attaches a predicate, derived from the request context, to every read and write of a
|
|
248
|
+
table:
|
|
249
|
+
|
|
250
|
+
```Typescript
|
|
251
|
+
import { and, eq } from 'drizzle-orm'
|
|
252
|
+
|
|
253
|
+
const { schema } = buildSchema(db, {
|
|
254
|
+
scope: {
|
|
255
|
+
posts: (ctx, table) => eq(table.orgId, ctx.orgId),
|
|
256
|
+
// A filter object works too — same shape as the generated `where` argument:
|
|
257
|
+
comments: (ctx) => ({ post: { orgId: { eq: ctx.orgId } } }),
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Keyed by the **Drizzle schema key**. `ctx` is your GraphQL context value; `table` is the table
|
|
263
|
+
the current statement runs against — for a relational read that is Drizzle's *aliased* copy, so
|
|
264
|
+
always build the predicate from that argument rather than from the imported table object.
|
|
265
|
+
|
|
266
|
+
The predicate is ANDed on **last**, after whatever the client sent, so a client filter can only
|
|
267
|
+
narrow the scope, never widen it. It reaches every path that touches the table:
|
|
268
|
+
|
|
269
|
+
| Path | Effect |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `posts`, `postsSingle` | out-of-scope rows are not returned |
|
|
272
|
+
| `postsAggregate`, `postsGroupBy` | counted over in-scope rows only |
|
|
273
|
+
| `user.posts`, `user.postsAggregate` | scoped on both the batched and the eager (`with:`) path |
|
|
274
|
+
| cursor pages | the keyset page is taken within the scope |
|
|
275
|
+
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
276
|
+
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
277
|
+
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
278
|
+
|
|
279
|
+
Two things it deliberately does not do:
|
|
280
|
+
|
|
281
|
+
- **It does not refuse.** A scoped `deletePosts(where: { id: { eq: 4 } })` on another
|
|
282
|
+
tenant's row returns `[]`, the same answer as a row that does not exist. Telling the caller
|
|
283
|
+
"that row exists but is not yours" is the leak the scope is there to close.
|
|
284
|
+
- **It does not apply to a plain insert.** There is no existing row to filter. `contextValues`
|
|
285
|
+
is the write-side half.
|
|
286
|
+
|
|
287
|
+
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
288
|
+
administrator through:
|
|
289
|
+
|
|
290
|
+
```Typescript
|
|
291
|
+
scope: {
|
|
292
|
+
posts: (ctx, table) => (ctx.isAdmin ? undefined : eq(table.orgId, ctx.orgId)),
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A table with no entry is unscoped, and a build with no `scope` at all emits exactly the SQL it
|
|
297
|
+
did before.
|
|
298
|
+
|
|
299
|
+
### Columns the server owns
|
|
300
|
+
|
|
301
|
+
`contextValues` supplies a column's value from the request context instead of accepting it from
|
|
302
|
+
the client:
|
|
303
|
+
|
|
304
|
+
```Typescript
|
|
305
|
+
const { schema } = buildSchema(db, {
|
|
306
|
+
contextValues: {
|
|
307
|
+
posts: {
|
|
308
|
+
orgId: (ctx) => ctx.orgId,
|
|
309
|
+
authorId: (ctx) => ctx.userId,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- The column is **removed from `CreatePostsInput` and `UpdatePostsInput`**, so a client cannot
|
|
316
|
+
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
317
|
+
object type.
|
|
318
|
+
- Every insert stamps it, including rows created by a nested `create`.
|
|
319
|
+
- Updates never write it, so an update cannot hand a row to another owner.
|
|
320
|
+
|
|
321
|
+
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
322
|
+
`scope` decides which rows a request can see and change afterwards.
|
|
323
|
+
|
|
324
|
+
> **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so a MySQL upsert's conflict
|
|
325
|
+
> branch cannot be scoped. Scope the table's `update` and rely on `contextValues` for the insert
|
|
326
|
+
> half.
|
|
327
|
+
|
|
328
|
+
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
329
|
+
build instead of quietly ceasing to be scoped.
|
|
240
330
|
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
241
331
|
> or an equivalent layer above the schema, which has the request context this library
|
|
242
332
|
> does not.
|
|
@@ -1487,6 +1577,105 @@ request-scoped batch loader, so the field still works even before you override i
|
|
|
1487
1577
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
1488
1578
|
of `entities.fieldResolvers`.
|
|
1489
1579
|
|
|
1580
|
+
## Writing your own resolver over a generated type
|
|
1581
|
+
|
|
1582
|
+
When you replace or wrap a generated resolver — to add a permission check that needs the
|
|
1583
|
+
row, to run a write inside your own transaction, to enforce a rule the schema cannot
|
|
1584
|
+
express — you become responsible for returning a row shaped like the type you replaced.
|
|
1585
|
+
That means translating the GraphQL selection set into the relational query builder's
|
|
1586
|
+
`with:` tree: relation field resolvers only fill in relations the parent did not already
|
|
1587
|
+
provide.
|
|
1588
|
+
|
|
1589
|
+
Two exports do that for you, alongside the already-exported `extractFilters` and
|
|
1590
|
+
`extractOrderBy`:
|
|
1591
|
+
|
|
1592
|
+
```Typescript
|
|
1593
|
+
import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
1594
|
+
|
|
1595
|
+
// The whole read: selected columns, relation arguments, eager loading, cursor
|
|
1596
|
+
// pagination, and the transport remapping of the result — against any executor.
|
|
1597
|
+
const rows = await resolveSelection(info, {
|
|
1598
|
+
db,
|
|
1599
|
+
table: 'Users',
|
|
1600
|
+
where: { id: { eq: userId } },
|
|
1601
|
+
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
// Or just the `with:` tree, if you are building the query yourself.
|
|
1605
|
+
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
1606
|
+
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
`selectionToWith` resolves fragments and inline fragments against `info.fragments`,
|
|
1610
|
+
intersects the selection against the relations config (so scalars and any fields you added
|
|
1611
|
+
yourself drop out instead of becoming invalid `with:` keys), follows aliases back to the
|
|
1612
|
+
relation they select, compiles each relation's `where` / `orderBy` / `limit` / `offset`
|
|
1613
|
+
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
1614
|
+
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
1615
|
+
|
|
1616
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
1617
|
+
and returns the rows already remapped to their GraphQL transport forms.
|
|
1618
|
+
|
|
1619
|
+
Both take a `typeNameMapper` option. If the schema was built with one, pass the same
|
|
1620
|
+
function here — the translation matches selections against GraphQL type names, so a build
|
|
1621
|
+
that renames its types has to say so.
|
|
1622
|
+
|
|
1623
|
+
## Identifying generated fields
|
|
1624
|
+
|
|
1625
|
+
Every generated field and object type publishes what it is under `extensions.drizzle`, so
|
|
1626
|
+
wrappers — `graphql-middleware`, envelop, `graphql-shield`, an audit log, a cache
|
|
1627
|
+
invalidator — can ask the schema instead of parsing field names. Names are configurable
|
|
1628
|
+
(`prefixes`, `suffixes`, `typeNameMapper`), which makes name parsing ambiguous by
|
|
1629
|
+
construction; this is not.
|
|
1630
|
+
|
|
1631
|
+
```Typescript
|
|
1632
|
+
import { drizzleExtension, identifyRows } from '@vantreeseba/drizzle-graphql'
|
|
1633
|
+
|
|
1634
|
+
const field = info.parentType.getFields()[info.fieldName]
|
|
1635
|
+
const meta = drizzleExtension(field)
|
|
1636
|
+
// {
|
|
1637
|
+
// table: 'Users', // the Drizzle schema key
|
|
1638
|
+
// kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
|
|
1639
|
+
// operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
|
|
1640
|
+
// // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
|
|
1641
|
+
// // | 'relationAggregate'
|
|
1642
|
+
// single: true, // one row, or a list
|
|
1643
|
+
// targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live
|
|
1644
|
+
// primaryKey: ['id'], // key columns of `table`, in schema order
|
|
1645
|
+
// }
|
|
1646
|
+
|
|
1647
|
+
if (meta?.kind === 'mutation') await audit(meta.table, meta.operation, args[meta.targetArg])
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
`drizzleExtension` returns `undefined` for anything this library did not generate, which is
|
|
1651
|
+
the check to make before treating a field as one of ours. Relation fields (and
|
|
1652
|
+
`<relation>Aggregate` fields) additionally carry `relation` and `parentTable`, with `table`
|
|
1653
|
+
naming the relation's **target** — so `Users.posts` reads as
|
|
1654
|
+
`{ table: 'Posts', parentTable: 'Users', relation: 'posts' }`. Object types carry
|
|
1655
|
+
`{ table, kind: 'type', primaryKey }`, which is what a stitching layer needs to map a type
|
|
1656
|
+
back to its source.
|
|
1657
|
+
|
|
1658
|
+
Because the row identity of a mutation lives in a different argument for every operation,
|
|
1659
|
+
`identifyRows` reads whichever one applies and hands back primary-key values:
|
|
1660
|
+
|
|
1661
|
+
```Typescript
|
|
1662
|
+
const target = identifyRows(field, args)
|
|
1663
|
+
// { table: 'Users', primaryKey: ['id'], rows: [{ id: 3 }], complete: true }
|
|
1664
|
+
|
|
1665
|
+
if (target?.complete) await invalidate(target.table, target.rows)
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
It understands `values` (one object or a list), `where` (`eq` for one row, `inArray` for
|
|
1669
|
+
many, an equality on every column of a composite key), and the per-entry `where` inside a
|
|
1670
|
+
batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
|
|
1671
|
+
is `false` when the arguments do not pin the rows down — an insert whose key the database
|
|
1672
|
+
generates, a filter on something other than the primary key, an unbounded update, or a
|
|
1673
|
+
table with no primary key at all.
|
|
1674
|
+
|
|
1675
|
+
> [!NOTE]
|
|
1676
|
+
> `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
|
|
1677
|
+
> parsing names, which only holds if the shape does not move: fields are added to it, never
|
|
1678
|
+
> removed or repurposed.
|
|
1490
1679
|
## Configuration reference
|
|
1491
1680
|
|
|
1492
1681
|
Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
|
package/dist/README.md
CHANGED
|
@@ -237,6 +237,96 @@ table becomes readable but not insertable — and warns on the console, since th
|
|
|
237
237
|
often a mistake than a plan.
|
|
238
238
|
|
|
239
239
|
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
240
|
+
> request *which fields* a caller may reach is a job for
|
|
241
|
+
> [`graphql-shield`](https://the-guild.dev/graphql/shield) or an equivalent layer above the
|
|
242
|
+
> schema. Deciding per request *which rows* a caller may reach is `scope`, below — that one a
|
|
243
|
+
> layer above cannot do, because a nested relation field never passes through a root resolver.
|
|
244
|
+
|
|
245
|
+
## Row scoping and multi-tenancy
|
|
246
|
+
|
|
247
|
+
`scope` attaches a predicate, derived from the request context, to every read and write of a
|
|
248
|
+
table:
|
|
249
|
+
|
|
250
|
+
```Typescript
|
|
251
|
+
import { and, eq } from 'drizzle-orm'
|
|
252
|
+
|
|
253
|
+
const { schema } = buildSchema(db, {
|
|
254
|
+
scope: {
|
|
255
|
+
posts: (ctx, table) => eq(table.orgId, ctx.orgId),
|
|
256
|
+
// A filter object works too — same shape as the generated `where` argument:
|
|
257
|
+
comments: (ctx) => ({ post: { orgId: { eq: ctx.orgId } } }),
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Keyed by the **Drizzle schema key**. `ctx` is your GraphQL context value; `table` is the table
|
|
263
|
+
the current statement runs against — for a relational read that is Drizzle's *aliased* copy, so
|
|
264
|
+
always build the predicate from that argument rather than from the imported table object.
|
|
265
|
+
|
|
266
|
+
The predicate is ANDed on **last**, after whatever the client sent, so a client filter can only
|
|
267
|
+
narrow the scope, never widen it. It reaches every path that touches the table:
|
|
268
|
+
|
|
269
|
+
| Path | Effect |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `posts`, `postsSingle` | out-of-scope rows are not returned |
|
|
272
|
+
| `postsAggregate`, `postsGroupBy` | counted over in-scope rows only |
|
|
273
|
+
| `user.posts`, `user.postsAggregate` | scoped on both the batched and the eager (`with:`) path |
|
|
274
|
+
| cursor pages | the keyset page is taken within the scope |
|
|
275
|
+
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
276
|
+
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
277
|
+
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
278
|
+
|
|
279
|
+
Two things it deliberately does not do:
|
|
280
|
+
|
|
281
|
+
- **It does not refuse.** A scoped `deletePosts(where: { id: { eq: 4 } })` on another
|
|
282
|
+
tenant's row returns `[]`, the same answer as a row that does not exist. Telling the caller
|
|
283
|
+
"that row exists but is not yours" is the leak the scope is there to close.
|
|
284
|
+
- **It does not apply to a plain insert.** There is no existing row to filter. `contextValues`
|
|
285
|
+
is the write-side half.
|
|
286
|
+
|
|
287
|
+
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
288
|
+
administrator through:
|
|
289
|
+
|
|
290
|
+
```Typescript
|
|
291
|
+
scope: {
|
|
292
|
+
posts: (ctx, table) => (ctx.isAdmin ? undefined : eq(table.orgId, ctx.orgId)),
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A table with no entry is unscoped, and a build with no `scope` at all emits exactly the SQL it
|
|
297
|
+
did before.
|
|
298
|
+
|
|
299
|
+
### Columns the server owns
|
|
300
|
+
|
|
301
|
+
`contextValues` supplies a column's value from the request context instead of accepting it from
|
|
302
|
+
the client:
|
|
303
|
+
|
|
304
|
+
```Typescript
|
|
305
|
+
const { schema } = buildSchema(db, {
|
|
306
|
+
contextValues: {
|
|
307
|
+
posts: {
|
|
308
|
+
orgId: (ctx) => ctx.orgId,
|
|
309
|
+
authorId: (ctx) => ctx.userId,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- The column is **removed from `CreatePostsInput` and `UpdatePostsInput`**, so a client cannot
|
|
316
|
+
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
317
|
+
object type.
|
|
318
|
+
- Every insert stamps it, including rows created by a nested `create`.
|
|
319
|
+
- Updates never write it, so an update cannot hand a row to another owner.
|
|
320
|
+
|
|
321
|
+
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
322
|
+
`scope` decides which rows a request can see and change afterwards.
|
|
323
|
+
|
|
324
|
+
> **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so a MySQL upsert's conflict
|
|
325
|
+
> branch cannot be scoped. Scope the table's `update` and rely on `contextValues` for the insert
|
|
326
|
+
> half.
|
|
327
|
+
|
|
328
|
+
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
329
|
+
build instead of quietly ceasing to be scoped.
|
|
240
330
|
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
241
331
|
> or an equivalent layer above the schema, which has the request context this library
|
|
242
332
|
> does not.
|
|
@@ -1487,6 +1577,105 @@ request-scoped batch loader, so the field still works even before you override i
|
|
|
1487
1577
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
1488
1578
|
of `entities.fieldResolvers`.
|
|
1489
1579
|
|
|
1580
|
+
## Writing your own resolver over a generated type
|
|
1581
|
+
|
|
1582
|
+
When you replace or wrap a generated resolver — to add a permission check that needs the
|
|
1583
|
+
row, to run a write inside your own transaction, to enforce a rule the schema cannot
|
|
1584
|
+
express — you become responsible for returning a row shaped like the type you replaced.
|
|
1585
|
+
That means translating the GraphQL selection set into the relational query builder's
|
|
1586
|
+
`with:` tree: relation field resolvers only fill in relations the parent did not already
|
|
1587
|
+
provide.
|
|
1588
|
+
|
|
1589
|
+
Two exports do that for you, alongside the already-exported `extractFilters` and
|
|
1590
|
+
`extractOrderBy`:
|
|
1591
|
+
|
|
1592
|
+
```Typescript
|
|
1593
|
+
import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
1594
|
+
|
|
1595
|
+
// The whole read: selected columns, relation arguments, eager loading, cursor
|
|
1596
|
+
// pagination, and the transport remapping of the result — against any executor.
|
|
1597
|
+
const rows = await resolveSelection(info, {
|
|
1598
|
+
db,
|
|
1599
|
+
table: 'Users',
|
|
1600
|
+
where: { id: { eq: userId } },
|
|
1601
|
+
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
// Or just the `with:` tree, if you are building the query yourself.
|
|
1605
|
+
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
1606
|
+
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
`selectionToWith` resolves fragments and inline fragments against `info.fragments`,
|
|
1610
|
+
intersects the selection against the relations config (so scalars and any fields you added
|
|
1611
|
+
yourself drop out instead of becoming invalid `with:` keys), follows aliases back to the
|
|
1612
|
+
relation they select, compiles each relation's `where` / `orderBy` / `limit` / `offset`
|
|
1613
|
+
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
1614
|
+
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
1615
|
+
|
|
1616
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
1617
|
+
and returns the rows already remapped to their GraphQL transport forms.
|
|
1618
|
+
|
|
1619
|
+
Both take a `typeNameMapper` option. If the schema was built with one, pass the same
|
|
1620
|
+
function here — the translation matches selections against GraphQL type names, so a build
|
|
1621
|
+
that renames its types has to say so.
|
|
1622
|
+
|
|
1623
|
+
## Identifying generated fields
|
|
1624
|
+
|
|
1625
|
+
Every generated field and object type publishes what it is under `extensions.drizzle`, so
|
|
1626
|
+
wrappers — `graphql-middleware`, envelop, `graphql-shield`, an audit log, a cache
|
|
1627
|
+
invalidator — can ask the schema instead of parsing field names. Names are configurable
|
|
1628
|
+
(`prefixes`, `suffixes`, `typeNameMapper`), which makes name parsing ambiguous by
|
|
1629
|
+
construction; this is not.
|
|
1630
|
+
|
|
1631
|
+
```Typescript
|
|
1632
|
+
import { drizzleExtension, identifyRows } from '@vantreeseba/drizzle-graphql'
|
|
1633
|
+
|
|
1634
|
+
const field = info.parentType.getFields()[info.fieldName]
|
|
1635
|
+
const meta = drizzleExtension(field)
|
|
1636
|
+
// {
|
|
1637
|
+
// table: 'Users', // the Drizzle schema key
|
|
1638
|
+
// kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
|
|
1639
|
+
// operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
|
|
1640
|
+
// // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
|
|
1641
|
+
// // | 'relationAggregate'
|
|
1642
|
+
// single: true, // one row, or a list
|
|
1643
|
+
// targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live
|
|
1644
|
+
// primaryKey: ['id'], // key columns of `table`, in schema order
|
|
1645
|
+
// }
|
|
1646
|
+
|
|
1647
|
+
if (meta?.kind === 'mutation') await audit(meta.table, meta.operation, args[meta.targetArg])
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
`drizzleExtension` returns `undefined` for anything this library did not generate, which is
|
|
1651
|
+
the check to make before treating a field as one of ours. Relation fields (and
|
|
1652
|
+
`<relation>Aggregate` fields) additionally carry `relation` and `parentTable`, with `table`
|
|
1653
|
+
naming the relation's **target** — so `Users.posts` reads as
|
|
1654
|
+
`{ table: 'Posts', parentTable: 'Users', relation: 'posts' }`. Object types carry
|
|
1655
|
+
`{ table, kind: 'type', primaryKey }`, which is what a stitching layer needs to map a type
|
|
1656
|
+
back to its source.
|
|
1657
|
+
|
|
1658
|
+
Because the row identity of a mutation lives in a different argument for every operation,
|
|
1659
|
+
`identifyRows` reads whichever one applies and hands back primary-key values:
|
|
1660
|
+
|
|
1661
|
+
```Typescript
|
|
1662
|
+
const target = identifyRows(field, args)
|
|
1663
|
+
// { table: 'Users', primaryKey: ['id'], rows: [{ id: 3 }], complete: true }
|
|
1664
|
+
|
|
1665
|
+
if (target?.complete) await invalidate(target.table, target.rows)
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
It understands `values` (one object or a list), `where` (`eq` for one row, `inArray` for
|
|
1669
|
+
many, an equality on every column of a composite key), and the per-entry `where` inside a
|
|
1670
|
+
batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
|
|
1671
|
+
is `false` when the arguments do not pin the rows down — an insert whose key the database
|
|
1672
|
+
generates, a filter on something other than the primary key, an unbounded update, or a
|
|
1673
|
+
table with no primary key at all.
|
|
1674
|
+
|
|
1675
|
+
> [!NOTE]
|
|
1676
|
+
> `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
|
|
1677
|
+
> parsing names, which only holds if the shape does not move: fields are added to it, never
|
|
1678
|
+
> removed or repurposed.
|
|
1490
1679
|
## Configuration reference
|
|
1491
1680
|
|
|
1492
1681
|
Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
|