@vantreeseba/drizzle-graphql 7.4.0 → 7.5.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 +99 -0
- package/dist/README.md +99 -0
- package/dist/index.cjs +435 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -1
- package/dist/index.d.ts +180 -1
- package/dist/index.js +429 -58
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1487,6 +1487,105 @@ request-scoped batch loader, so the field still works even before you override i
|
|
|
1487
1487
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
1488
1488
|
of `entities.fieldResolvers`.
|
|
1489
1489
|
|
|
1490
|
+
## Writing your own resolver over a generated type
|
|
1491
|
+
|
|
1492
|
+
When you replace or wrap a generated resolver — to add a permission check that needs the
|
|
1493
|
+
row, to run a write inside your own transaction, to enforce a rule the schema cannot
|
|
1494
|
+
express — you become responsible for returning a row shaped like the type you replaced.
|
|
1495
|
+
That means translating the GraphQL selection set into the relational query builder's
|
|
1496
|
+
`with:` tree: relation field resolvers only fill in relations the parent did not already
|
|
1497
|
+
provide.
|
|
1498
|
+
|
|
1499
|
+
Two exports do that for you, alongside the already-exported `extractFilters` and
|
|
1500
|
+
`extractOrderBy`:
|
|
1501
|
+
|
|
1502
|
+
```Typescript
|
|
1503
|
+
import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
1504
|
+
|
|
1505
|
+
// The whole read: selected columns, relation arguments, eager loading, cursor
|
|
1506
|
+
// pagination, and the transport remapping of the result — against any executor.
|
|
1507
|
+
const rows = await resolveSelection(info, {
|
|
1508
|
+
db,
|
|
1509
|
+
table: 'Users',
|
|
1510
|
+
where: { id: { eq: userId } },
|
|
1511
|
+
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
// Or just the `with:` tree, if you are building the query yourself.
|
|
1515
|
+
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
1516
|
+
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
`selectionToWith` resolves fragments and inline fragments against `info.fragments`,
|
|
1520
|
+
intersects the selection against the relations config (so scalars and any fields you added
|
|
1521
|
+
yourself drop out instead of becoming invalid `with:` keys), follows aliases back to the
|
|
1522
|
+
relation they select, compiles each relation's `where` / `orderBy` / `limit` / `offset`
|
|
1523
|
+
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
1524
|
+
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
1525
|
+
|
|
1526
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
1527
|
+
and returns the rows already remapped to their GraphQL transport forms.
|
|
1528
|
+
|
|
1529
|
+
Both take a `typeNameMapper` option. If the schema was built with one, pass the same
|
|
1530
|
+
function here — the translation matches selections against GraphQL type names, so a build
|
|
1531
|
+
that renames its types has to say so.
|
|
1532
|
+
|
|
1533
|
+
## Identifying generated fields
|
|
1534
|
+
|
|
1535
|
+
Every generated field and object type publishes what it is under `extensions.drizzle`, so
|
|
1536
|
+
wrappers — `graphql-middleware`, envelop, `graphql-shield`, an audit log, a cache
|
|
1537
|
+
invalidator — can ask the schema instead of parsing field names. Names are configurable
|
|
1538
|
+
(`prefixes`, `suffixes`, `typeNameMapper`), which makes name parsing ambiguous by
|
|
1539
|
+
construction; this is not.
|
|
1540
|
+
|
|
1541
|
+
```Typescript
|
|
1542
|
+
import { drizzleExtension, identifyRows } from '@vantreeseba/drizzle-graphql'
|
|
1543
|
+
|
|
1544
|
+
const field = info.parentType.getFields()[info.fieldName]
|
|
1545
|
+
const meta = drizzleExtension(field)
|
|
1546
|
+
// {
|
|
1547
|
+
// table: 'Users', // the Drizzle schema key
|
|
1548
|
+
// kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
|
|
1549
|
+
// operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
|
|
1550
|
+
// // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
|
|
1551
|
+
// // | 'relationAggregate'
|
|
1552
|
+
// single: true, // one row, or a list
|
|
1553
|
+
// targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live
|
|
1554
|
+
// primaryKey: ['id'], // key columns of `table`, in schema order
|
|
1555
|
+
// }
|
|
1556
|
+
|
|
1557
|
+
if (meta?.kind === 'mutation') await audit(meta.table, meta.operation, args[meta.targetArg])
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
`drizzleExtension` returns `undefined` for anything this library did not generate, which is
|
|
1561
|
+
the check to make before treating a field as one of ours. Relation fields (and
|
|
1562
|
+
`<relation>Aggregate` fields) additionally carry `relation` and `parentTable`, with `table`
|
|
1563
|
+
naming the relation's **target** — so `Users.posts` reads as
|
|
1564
|
+
`{ table: 'Posts', parentTable: 'Users', relation: 'posts' }`. Object types carry
|
|
1565
|
+
`{ table, kind: 'type', primaryKey }`, which is what a stitching layer needs to map a type
|
|
1566
|
+
back to its source.
|
|
1567
|
+
|
|
1568
|
+
Because the row identity of a mutation lives in a different argument for every operation,
|
|
1569
|
+
`identifyRows` reads whichever one applies and hands back primary-key values:
|
|
1570
|
+
|
|
1571
|
+
```Typescript
|
|
1572
|
+
const target = identifyRows(field, args)
|
|
1573
|
+
// { table: 'Users', primaryKey: ['id'], rows: [{ id: 3 }], complete: true }
|
|
1574
|
+
|
|
1575
|
+
if (target?.complete) await invalidate(target.table, target.rows)
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
It understands `values` (one object or a list), `where` (`eq` for one row, `inArray` for
|
|
1579
|
+
many, an equality on every column of a composite key), and the per-entry `where` inside a
|
|
1580
|
+
batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
|
|
1581
|
+
is `false` when the arguments do not pin the rows down — an insert whose key the database
|
|
1582
|
+
generates, a filter on something other than the primary key, an unbounded update, or a
|
|
1583
|
+
table with no primary key at all.
|
|
1584
|
+
|
|
1585
|
+
> [!NOTE]
|
|
1586
|
+
> `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
|
|
1587
|
+
> parsing names, which only holds if the shape does not move: fields are added to it, never
|
|
1588
|
+
> removed or repurposed.
|
|
1490
1589
|
## Configuration reference
|
|
1491
1590
|
|
|
1492
1591
|
Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
|
package/dist/README.md
CHANGED
|
@@ -1487,6 +1487,105 @@ request-scoped batch loader, so the field still works even before you override i
|
|
|
1487
1487
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
1488
1488
|
of `entities.fieldResolvers`.
|
|
1489
1489
|
|
|
1490
|
+
## Writing your own resolver over a generated type
|
|
1491
|
+
|
|
1492
|
+
When you replace or wrap a generated resolver — to add a permission check that needs the
|
|
1493
|
+
row, to run a write inside your own transaction, to enforce a rule the schema cannot
|
|
1494
|
+
express — you become responsible for returning a row shaped like the type you replaced.
|
|
1495
|
+
That means translating the GraphQL selection set into the relational query builder's
|
|
1496
|
+
`with:` tree: relation field resolvers only fill in relations the parent did not already
|
|
1497
|
+
provide.
|
|
1498
|
+
|
|
1499
|
+
Two exports do that for you, alongside the already-exported `extractFilters` and
|
|
1500
|
+
`extractOrderBy`:
|
|
1501
|
+
|
|
1502
|
+
```Typescript
|
|
1503
|
+
import { resolveSelection, selectionToWith } from '@vantreeseba/drizzle-graphql'
|
|
1504
|
+
|
|
1505
|
+
// The whole read: selected columns, relation arguments, eager loading, cursor
|
|
1506
|
+
// pagination, and the transport remapping of the result — against any executor.
|
|
1507
|
+
const rows = await resolveSelection(info, {
|
|
1508
|
+
db,
|
|
1509
|
+
table: 'Users',
|
|
1510
|
+
where: { id: { eq: userId } },
|
|
1511
|
+
executor: tx, // defaults to `db`; pass a transaction to see its own writes
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
// Or just the `with:` tree, if you are building the query yourself.
|
|
1515
|
+
const withTree = selectionToWith(info, { db, table: 'Users' })
|
|
1516
|
+
const row = await tx.query.Users.findFirst({ where: { id: userId }, with: withTree })
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
`selectionToWith` resolves fragments and inline fragments against `info.fragments`,
|
|
1520
|
+
intersects the selection against the relations config (so scalars and any fields you added
|
|
1521
|
+
yourself drop out instead of becoming invalid `with:` keys), follows aliases back to the
|
|
1522
|
+
relation they select, compiles each relation's `where` / `orderBy` / `limit` / `offset`
|
|
1523
|
+
into its nested entry, and leaves `<relation>Aggregate` selections out — they are not
|
|
1524
|
+
`with:` entries. It returns `undefined` when the selection asks for no relations.
|
|
1525
|
+
|
|
1526
|
+
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
1527
|
+
and returns the rows already remapped to their GraphQL transport forms.
|
|
1528
|
+
|
|
1529
|
+
Both take a `typeNameMapper` option. If the schema was built with one, pass the same
|
|
1530
|
+
function here — the translation matches selections against GraphQL type names, so a build
|
|
1531
|
+
that renames its types has to say so.
|
|
1532
|
+
|
|
1533
|
+
## Identifying generated fields
|
|
1534
|
+
|
|
1535
|
+
Every generated field and object type publishes what it is under `extensions.drizzle`, so
|
|
1536
|
+
wrappers — `graphql-middleware`, envelop, `graphql-shield`, an audit log, a cache
|
|
1537
|
+
invalidator — can ask the schema instead of parsing field names. Names are configurable
|
|
1538
|
+
(`prefixes`, `suffixes`, `typeNameMapper`), which makes name parsing ambiguous by
|
|
1539
|
+
construction; this is not.
|
|
1540
|
+
|
|
1541
|
+
```Typescript
|
|
1542
|
+
import { drizzleExtension, identifyRows } from '@vantreeseba/drizzle-graphql'
|
|
1543
|
+
|
|
1544
|
+
const field = info.parentType.getFields()[info.fieldName]
|
|
1545
|
+
const meta = drizzleExtension(field)
|
|
1546
|
+
// {
|
|
1547
|
+
// table: 'Users', // the Drizzle schema key
|
|
1548
|
+
// kind: 'mutation', // 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
|
|
1549
|
+
// operation: 'update', // 'select' | 'insert' | 'update' | 'updateMany' | 'upsert'
|
|
1550
|
+
// // | 'delete' | 'aggregate' | 'groupBy' | 'relation'
|
|
1551
|
+
// // | 'relationAggregate'
|
|
1552
|
+
// single: true, // one row, or a list
|
|
1553
|
+
// targetArg: 'where', // 'where' | 'values' | 'updates' — where the rows live
|
|
1554
|
+
// primaryKey: ['id'], // key columns of `table`, in schema order
|
|
1555
|
+
// }
|
|
1556
|
+
|
|
1557
|
+
if (meta?.kind === 'mutation') await audit(meta.table, meta.operation, args[meta.targetArg])
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
`drizzleExtension` returns `undefined` for anything this library did not generate, which is
|
|
1561
|
+
the check to make before treating a field as one of ours. Relation fields (and
|
|
1562
|
+
`<relation>Aggregate` fields) additionally carry `relation` and `parentTable`, with `table`
|
|
1563
|
+
naming the relation's **target** — so `Users.posts` reads as
|
|
1564
|
+
`{ table: 'Posts', parentTable: 'Users', relation: 'posts' }`. Object types carry
|
|
1565
|
+
`{ table, kind: 'type', primaryKey }`, which is what a stitching layer needs to map a type
|
|
1566
|
+
back to its source.
|
|
1567
|
+
|
|
1568
|
+
Because the row identity of a mutation lives in a different argument for every operation,
|
|
1569
|
+
`identifyRows` reads whichever one applies and hands back primary-key values:
|
|
1570
|
+
|
|
1571
|
+
```Typescript
|
|
1572
|
+
const target = identifyRows(field, args)
|
|
1573
|
+
// { table: 'Users', primaryKey: ['id'], rows: [{ id: 3 }], complete: true }
|
|
1574
|
+
|
|
1575
|
+
if (target?.complete) await invalidate(target.table, target.rows)
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
It understands `values` (one object or a list), `where` (`eq` for one row, `inArray` for
|
|
1579
|
+
many, an equality on every column of a composite key), and the per-entry `where` inside a
|
|
1580
|
+
batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
|
|
1581
|
+
is `false` when the arguments do not pin the rows down — an insert whose key the database
|
|
1582
|
+
generates, a filter on something other than the primary key, an unbounded update, or a
|
|
1583
|
+
table with no primary key at all.
|
|
1584
|
+
|
|
1585
|
+
> [!NOTE]
|
|
1586
|
+
> `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
|
|
1587
|
+
> parsing names, which only holds if the shape does not move: fields are added to it, never
|
|
1588
|
+
> removed or repurposed.
|
|
1490
1589
|
## Configuration reference
|
|
1491
1590
|
|
|
1492
1591
|
Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
|