@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 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