@vantreeseba/drizzle-graphql 7.3.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
@@ -85,6 +85,39 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
85
85
  })
86
86
  ```
87
87
 
88
+ ## Naming the generated types and fields
89
+
90
+ By default every name comes from the table's key in the Drizzle schema: `tasks` gives type
91
+ `Tasks`, queries `tasks` / `tasksSingle`, mutations `createTasks` / `createTasksSingle`.
92
+ Most schemas use plural table keys and want singular type names, so that pairing is shipped
93
+ as a preset:
94
+
95
+ ```Typescript
96
+ const { schema } = buildSchema(db, { typeNameMapper: 'singularize' })
97
+ ```
98
+
99
+ `tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), mutations
100
+ `createTasks` (array) and `createTask` (single), `updateTask`, `deleteTask`. Irregular
101
+ plurals go through `pluralize`, so `people` gives `Person` / `people` / `person` and
102
+ `addresses` gives `Address` rather than the `addresss` a naive suffix would produce. A table
103
+ key that is already singular is pluralized for the list side, and a capitalized key is
104
+ uncapitalized for the field side, so `tasks`, `task` and `Tasks` all land on the same names.
105
+
106
+ For anything else, pass your own function — it is asked per table and may return `undefined`
107
+ to leave that table with the default naming. The preset is exported as `singularizeMapper`
108
+ so it can be wrapped:
109
+
110
+ ```Typescript
111
+ import { buildSchema, singularizeMapper } from 'drizzle-graphql'
112
+
113
+ const { schema } = buildSchema(db, {
114
+ typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
115
+ })
116
+ ```
117
+
118
+ With a mapper in play the singular/plural forms are what keep the list and single fields
119
+ apart, so `suffixes` may be empty on both sides. Without one, they must still differ.
120
+
88
121
  ## Choosing what gets generated
89
122
 
90
123
  Every generated operation is on by default. `features` turns individual ones off, which
@@ -124,6 +157,35 @@ const { schema } = buildSchema(db, {
124
157
  `mutations: false`
125
158
  - List and single queries are always generated, so `Query` is never empty
126
159
 
160
+ ### Per-table operations
161
+
162
+ The choice is rarely build-wide: a schema usually has a handful of tables that must not be
163
+ deleted through the API, a handful whose writes belong to a hand-written resolver that
164
+ enforces something, and a majority that want everything. Any flag can be a predicate
165
+ instead of a boolean, asked once per table with the table's key in the Drizzle schema:
166
+
167
+ ```Typescript
168
+ const { schema } = buildSchema(db, {
169
+ features: {
170
+ delete: (table) => table !== 'auditLog',
171
+ update: (table) => !ownedByCustomResolver.has(table),
172
+ upsert: (table) => table === 'settings',
173
+ },
174
+ })
175
+ ```
176
+
177
+ - The table is still fully readable — this decides which operations it offers, not whether
178
+ it is in the schema at all. For that, use `exclude` below
179
+ - `nestedWrites` is the one flag that stays build-wide: its plans are computed once over
180
+ the whole relation graph, and a nested write reaches a second table by definition
181
+ - `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
182
+ - Some operations are built out of others, and the generator knows which. A table that
183
+ asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
184
+ path past the operation you turned off — warns on the console at build time, naming the
185
+ tables, rather than leaving that to be discovered later. So does asking for `updateMany`
186
+ without `update`, `groupBy` without `aggregates`, `countMutations` with neither plural
187
+ write, or `nestedWrites` alongside a table whose own writes are off
188
+
127
189
  ## Excluding tables and columns
128
190
 
129
191
  `features` decides which *kinds* of operation exist. `exclude` decides which tables and
@@ -1425,6 +1487,105 @@ request-scoped batch loader, so the field still works even before you override i
1425
1487
  and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
1426
1488
  of `entities.fieldResolvers`.
1427
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.
1428
1589
  ## Configuration reference
1429
1590
 
1430
1591
  Every key of the second argument to `buildSchema(db, config?)`. Each links to the section
package/dist/README.md CHANGED
@@ -85,6 +85,39 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
85
85
  })
86
86
  ```
87
87
 
88
+ ## Naming the generated types and fields
89
+
90
+ By default every name comes from the table's key in the Drizzle schema: `tasks` gives type
91
+ `Tasks`, queries `tasks` / `tasksSingle`, mutations `createTasks` / `createTasksSingle`.
92
+ Most schemas use plural table keys and want singular type names, so that pairing is shipped
93
+ as a preset:
94
+
95
+ ```Typescript
96
+ const { schema } = buildSchema(db, { typeNameMapper: 'singularize' })
97
+ ```
98
+
99
+ `tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), mutations
100
+ `createTasks` (array) and `createTask` (single), `updateTask`, `deleteTask`. Irregular
101
+ plurals go through `pluralize`, so `people` gives `Person` / `people` / `person` and
102
+ `addresses` gives `Address` rather than the `addresss` a naive suffix would produce. A table
103
+ key that is already singular is pluralized for the list side, and a capitalized key is
104
+ uncapitalized for the field side, so `tasks`, `task` and `Tasks` all land on the same names.
105
+
106
+ For anything else, pass your own function — it is asked per table and may return `undefined`
107
+ to leave that table with the default naming. The preset is exported as `singularizeMapper`
108
+ so it can be wrapped:
109
+
110
+ ```Typescript
111
+ import { buildSchema, singularizeMapper } from 'drizzle-graphql'
112
+
113
+ const { schema } = buildSchema(db, {
114
+ typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
115
+ })
116
+ ```
117
+
118
+ With a mapper in play the singular/plural forms are what keep the list and single fields
119
+ apart, so `suffixes` may be empty on both sides. Without one, they must still differ.
120
+
88
121
  ## Choosing what gets generated
89
122
 
90
123
  Every generated operation is on by default. `features` turns individual ones off, which
@@ -124,6 +157,35 @@ const { schema } = buildSchema(db, {
124
157
  `mutations: false`
125
158
  - List and single queries are always generated, so `Query` is never empty
126
159
 
160
+ ### Per-table operations
161
+
162
+ The choice is rarely build-wide: a schema usually has a handful of tables that must not be
163
+ deleted through the API, a handful whose writes belong to a hand-written resolver that
164
+ enforces something, and a majority that want everything. Any flag can be a predicate
165
+ instead of a boolean, asked once per table with the table's key in the Drizzle schema:
166
+
167
+ ```Typescript
168
+ const { schema } = buildSchema(db, {
169
+ features: {
170
+ delete: (table) => table !== 'auditLog',
171
+ update: (table) => !ownedByCustomResolver.has(table),
172
+ upsert: (table) => table === 'settings',
173
+ },
174
+ })
175
+ ```
176
+
177
+ - The table is still fully readable — this decides which operations it offers, not whether
178
+ it is in the schema at all. For that, use `exclude` below
179
+ - `nestedWrites` is the one flag that stays build-wide: its plans are computed once over
180
+ the whole relation graph, and a nested write reaches a second table by definition
181
+ - `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
182
+ - Some operations are built out of others, and the generator knows which. A table that
183
+ asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
184
+ path past the operation you turned off — warns on the console at build time, naming the
185
+ tables, rather than leaving that to be discovered later. So does asking for `updateMany`
186
+ without `update`, `groupBy` without `aggregates`, `countMutations` with neither plural
187
+ write, or `nestedWrites` alongside a table whose own writes are off
188
+
127
189
  ## Excluding tables and columns
128
190
 
129
191
  `features` decides which *kinds* of operation exist. `exclude` decides which tables and
@@ -1425,6 +1487,105 @@ request-scoped batch loader, so the field still works even before you override i
1425
1487
  and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
1426
1488
  of `entities.fieldResolvers`.
1427
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.
1428
1589
  ## Configuration reference
1429
1590
 
1430
1591
  Every key of the second argument to `buildSchema(db, config?)`. Each links to the section