@vantreeseba/drizzle-graphql 13.1.1 → 15.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 +178 -15
- package/dist/index.cjs +726 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -8
- package/dist/index.d.ts +79 -8
- package/dist/index.js +726 -213
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Automatically create GraphQL schema or customizable schema config fields from Drizzle ORM schema
|
|
4
4
|
|
|
5
|
+
Site: [cubicecho.github.io/drizzle-graphql](https://cubicecho.github.io/drizzle-graphql/)
|
|
6
|
+
|
|
5
7
|
## Usage
|
|
6
8
|
|
|
7
9
|
- Pass your drizzle database instance and schema into builder to generate `{ schema, entities }` object
|
|
8
|
-
- Use `schema` if pre-built schema already satisfies all your
|
|
10
|
+
- Use `schema` if pre-built schema already satisfies all your needs. It's compatible with any server that consumes `GraphQLSchema` class instance
|
|
9
11
|
|
|
10
12
|
Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
|
11
13
|
|
|
12
14
|
```Typescript
|
|
13
15
|
import { createServer } from 'node:http'
|
|
14
16
|
import { createYoga } from 'graphql-yoga'
|
|
15
|
-
import { buildSchema } from 'drizzle-graphql'
|
|
17
|
+
import { buildSchema } from '@vantreeseba/drizzle-graphql'
|
|
16
18
|
|
|
17
19
|
// db - your drizzle instance
|
|
18
20
|
import { db } from './database'
|
|
@@ -20,6 +22,7 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
|
|
|
20
22
|
const { schema } = buildSchema(db)
|
|
21
23
|
|
|
22
24
|
const yoga = createYoga({ schema })
|
|
25
|
+
const server = createServer(yoga)
|
|
23
26
|
|
|
24
27
|
server.listen(4000, () => {
|
|
25
28
|
console.info('Server is running on http://localhost:4000/graphql')
|
|
@@ -32,7 +35,7 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
|
|
|
32
35
|
import { createServer } from 'node:http'
|
|
33
36
|
import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from 'graphql'
|
|
34
37
|
import { createYoga } from 'graphql-yoga'
|
|
35
|
-
import { buildSchema } from 'drizzle-graphql'
|
|
38
|
+
import { buildSchema } from '@vantreeseba/drizzle-graphql'
|
|
36
39
|
|
|
37
40
|
// Schema contains 'Users' and 'Customers' tables
|
|
38
41
|
import { db } from './database'
|
|
@@ -118,7 +121,7 @@ to leave that table with the default naming. The preset is exported as `singular
|
|
|
118
121
|
so it can be wrapped:
|
|
119
122
|
|
|
120
123
|
```Typescript
|
|
121
|
-
import { buildSchema, singularizeMapper } from 'drizzle-graphql'
|
|
124
|
+
import { buildSchema, singularizeMapper } from '@vantreeseba/drizzle-graphql'
|
|
122
125
|
|
|
123
126
|
const { schema } = buildSchema(db, {
|
|
124
127
|
typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
|
|
@@ -189,14 +192,16 @@ const { schema } = buildSchema(db, {
|
|
|
189
192
|
- The table is still fully readable — this decides which operations it offers, not whether
|
|
190
193
|
it is in the schema at all. For that, use `exclude` below
|
|
191
194
|
- `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
|
|
195
|
+
the whole relation graph, and a nested write reaches a second table by definition. What
|
|
196
|
+
it offers on each relation still follows the *target* table's flags — see
|
|
197
|
+
[Nested writes](#nested-writes)
|
|
193
198
|
- `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
|
|
194
199
|
- Some operations are built out of others, and the generator knows which. A table that
|
|
195
200
|
asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
|
|
196
201
|
path past the operation you turned off — warns on the console at build time, naming the
|
|
197
202
|
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
|
|
203
|
+
without `update`, `groupBy` without `aggregates`, or `countMutations` with neither plural
|
|
204
|
+
write. `nestedWrites` notes the nested inputs it left out because their table's write is off
|
|
200
205
|
|
|
201
206
|
## Excluding tables and columns
|
|
202
207
|
|
|
@@ -604,8 +609,9 @@ generated SDL says what a field actually holds instead of falling back to `Strin
|
|
|
604
609
|
| `json` / `jsonb` (and `mode: 'json'`) | `JSON` | the parsed value |
|
|
605
610
|
| `bigint` (and `mode: 'bigint'`) | `BigInt` | a decimal string |
|
|
606
611
|
| `uuid` | `UUID` | a validated UUID string |
|
|
607
|
-
| `timestamp` / `datetime`
|
|
608
|
-
| `
|
|
612
|
+
| `timestamp` / `datetime` (either `mode`) | `DateTime` | an ISO-8601 string |
|
|
613
|
+
| SQLite `integer({ mode: 'timestamp' })` / `'timestamp_ms'` | `DateTime` | an ISO-8601 string |
|
|
614
|
+
| `date` (either `mode`) | `Date` | a `YYYY-MM-DD` string |
|
|
609
615
|
|
|
610
616
|
```graphql
|
|
611
617
|
mutation {
|
|
@@ -623,15 +629,98 @@ mutation {
|
|
|
623
629
|
`Number.MAX_SAFE_INTEGER` survive the round-trip. Integer literals are accepted on input;
|
|
624
630
|
floats and non-numeric strings are rejected
|
|
625
631
|
- **`UUID`** validates the format on the way in, including inside `where` filters
|
|
632
|
+
- **`Date`** is a calendar day — no time, no offset — and is the same type in every position
|
|
633
|
+
a `date` column appears in: the object field, the create and update inputs, the
|
|
634
|
+
`DateFilter` operands, aggregate `min` / `max` and group keys. It takes and returns an
|
|
635
|
+
RFC 3339 `full-date` (`2026-09-15`) whichever `mode` the column is declared with; a
|
|
636
|
+
`mode: 'date'` column is read back from its JS `Date` as the UTC day, so no server timezone
|
|
637
|
+
shifts it. Input must name a real day: `2026-02-30`, `2026-13-45` and datetime strings like
|
|
638
|
+
`2026-09-15T10:00:00Z` are rejected. With `graphql-codegen`, map it as
|
|
639
|
+
`scalars: { Date: 'string' }`
|
|
640
|
+
- **`DateTime`** is an instant, and is likewise the same type in every position a Postgres
|
|
641
|
+
`timestamp`, MySQL `timestamp` / `datetime` or SQLite integer timestamp column appears in, with `DateTimeFilter` in
|
|
642
|
+
filters, whichever `mode` the column is declared with. It returns an ISO-8601 string in UTC.
|
|
643
|
+
MySQL stores these columns without an offset; the value is written and read as UTC
|
|
644
|
+
(`2026-09-15T10:00:00Z` is stored as `2026-09-15 10:00:00`) whatever the Node process's
|
|
645
|
+
timezone, the way drizzle itself treats them. Postgres `timestamp` without time zone is
|
|
646
|
+
treated the same way, in both modes; `timestamptz` stores the instant, and its text in
|
|
647
|
+
`mode: 'string'` (`2026-09-15 04:00:00-06`) is returned as the same instant in UTC. The
|
|
648
|
+
scalar carries milliseconds, so a `timestamp(6)` value's microseconds are truncated on read
|
|
649
|
+
and on write. A SQLite `integer({ mode: 'timestamp' })` or `integer({ mode: 'timestamp_ms' })`
|
|
650
|
+
column is `DateTime` too; it stores the instant as epoch seconds or milliseconds. Seconds
|
|
651
|
+
mode cannot hold a fraction, so it is floored (never rounded) on write —
|
|
652
|
+
`2026-09-15T10:00:00.999Z` is stored as `1789466400` and reads back as
|
|
653
|
+
`2026-09-15T10:00:00.000Z` — and filter operands on such a column are floored the same way,
|
|
654
|
+
so `eq` with the value you wrote finds the row you wrote it to
|
|
626
655
|
|
|
627
656
|
The scalars are exported if you need them in a hand-written schema:
|
|
628
657
|
|
|
629
658
|
```Typescript
|
|
630
|
-
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from 'drizzle-graphql'
|
|
659
|
+
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
631
660
|
```
|
|
632
661
|
|
|
633
662
|
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
634
|
-
called in SDL, to avoid clashing with the language's own `BigInt`.
|
|
663
|
+
called in SDL, to avoid clashing with the language's own `BigInt`. `GraphQLDate` is this
|
|
664
|
+
library's own `Date` scalar, not `graphql-scalars`' (whose `parseValue` returns a JS `Date`).
|
|
665
|
+
|
|
666
|
+
> **Upgrading — `date` columns.** A `date` column used to be typed differently depending on
|
|
667
|
+
> where it appeared and which `mode` it was declared with. Both modes are now `Date` everywhere:
|
|
668
|
+
>
|
|
669
|
+
> | Drizzle column | position | before | now |
|
|
670
|
+
> | --- | --- | --- | --- |
|
|
671
|
+
> | `date(…, { mode: 'string' })` | output | `Date` | `Date` |
|
|
672
|
+
> | | create / update input | `String` | `Date` |
|
|
673
|
+
> | | filter | `StringFilter` | `DateFilter` |
|
|
674
|
+
> | `date(…)` / `{ mode: 'date' }` (Postgres) | output, input, min/max, group keys | `DateTime` | `Date` |
|
|
675
|
+
> | | filter | `DateTimeFilter` | `DateFilter` |
|
|
676
|
+
> | `date(…, { mode: 'date' })` (MySQL) | output, input, min/max, group keys | `JSON` | `Date` |
|
|
677
|
+
> | | filter | `JSONFilter` | `DateFilter` |
|
|
678
|
+
> | `date(…, { mode: 'string' })` (MySQL) | output, input, min/max, group keys | `String` | `Date` |
|
|
679
|
+
> | | filter | `StringFilter` | `DateFilter` |
|
|
680
|
+
>
|
|
681
|
+
> What to change:
|
|
682
|
+
>
|
|
683
|
+
> - Variables typed `String` or `DateTime` for a date field become `Date`; filters on it are
|
|
684
|
+
> `DateFilter`, which has the `DateTimeFilter` operators (`eq` / `ne` / `lt` / `lte` / `gt` /
|
|
685
|
+
> `gte` / `inArray` / `notInArray` / `isNull` / `isNotNull`) and none of the string ones
|
|
686
|
+
> - Send `YYYY-MM-DD`. A datetime string such as `2026-09-15T00:00:00.000Z` used to be
|
|
687
|
+
> truncated to its date and is now rejected
|
|
688
|
+
> - A `mode: 'date'` column read `2026-09-15T00:00:00.000Z` and now reads `2026-09-15`
|
|
689
|
+
> - In `graphql-codegen`, map `scalars: { Date: 'string' }` — the value is a string in both
|
|
690
|
+
> directions
|
|
691
|
+
> - If you passed `graphql-scalars`' `GraphQLDate` as a column override, pass this library's
|
|
692
|
+
> exported `GraphQLDate` instead. They share the name `Date`, so a schema holding both
|
|
693
|
+
> fails to build with graphql's "multiple types named Date" error
|
|
694
|
+
|
|
695
|
+
> **Upgrading — MySQL `timestamp` / `datetime` columns.** These used to fall through to
|
|
696
|
+
> `JSON` in `mode: 'date'` and to `String` in `mode: 'string'`. All four are now `DateTime`, as
|
|
697
|
+
> Postgres `timestamp` already was:
|
|
698
|
+
>
|
|
699
|
+
> | Drizzle column (MySQL) | position | before | now |
|
|
700
|
+
> | --- | --- | --- | --- |
|
|
701
|
+
> | `timestamp(…)` / `datetime(…)` (`mode: 'date'`) | output, input, min/max, group keys | `JSON` | `DateTime` |
|
|
702
|
+
> | | filter | `JSONFilter` | `DateTimeFilter` |
|
|
703
|
+
> | `timestamp(…)` / `datetime(…)` with `{ mode: 'string' }` | output, input, min/max, group keys | `String` | `DateTime` |
|
|
704
|
+
> | | filter | `StringFilter` | `DateTimeFilter` |
|
|
705
|
+
>
|
|
706
|
+
> Variables for these fields become `DateTime` and their filters `DateTimeFilter`, which has no
|
|
707
|
+
> `JSONFilter` / `StringFilter` operators such as `contains` or `like`. Input must be an ISO-8601
|
|
708
|
+
> date-time (`2026-09-15T10:00:00Z`, or with an offset). A `mode: 'string'` column used to read
|
|
709
|
+
> MySQL's own `2026-09-15 10:00:00` and now reads `2026-09-15T10:00:00.000Z`. In
|
|
710
|
+
> `graphql-codegen`, map `DateTime` the way you already do for Postgres timestamps.
|
|
711
|
+
|
|
712
|
+
> **Upgrading — SQLite integer timestamps.** `integer({ mode: 'timestamp' })` and
|
|
713
|
+
> `integer({ mode: 'timestamp_ms' })` used to fall through to `JSON`. Both are now `DateTime`:
|
|
714
|
+
>
|
|
715
|
+
> | Drizzle column (SQLite) | position | before | now |
|
|
716
|
+
> | --- | --- | --- | --- |
|
|
717
|
+
> | `integer(…, { mode: 'timestamp' })` / `{ mode: 'timestamp_ms' }` | output, input, min/max, group keys | `JSON` | `DateTime` |
|
|
718
|
+
> | | filter | `JSONFilter` | `DateTimeFilter` |
|
|
719
|
+
>
|
|
720
|
+
> Variables for these fields become `DateTime`, and their filters `DateTimeFilter`, which swaps
|
|
721
|
+
> the JSON `path` operator for `lt` / `lte` / `gt` / `gte` / `inArray` / `notInArray`. Input must
|
|
722
|
+
> be an ISO-8601 date-time; a JSON value such as a bare number is rejected. Reads are unchanged:
|
|
723
|
+
> they were already ISO-8601 strings in UTC. The stored integers are unchanged too.
|
|
635
724
|
|
|
636
725
|
### Overriding a column's scalar
|
|
637
726
|
|
|
@@ -909,7 +998,7 @@ no others. The set follows the column's data type, never its name:
|
|
|
909
998
|
| column type | `eq` `ne` `inArray` `notInArray` `isNull` `isNotNull` | `lt` `lte` `gt` `gte` | pattern operators + `insensitive` |
|
|
910
999
|
| --- | --- | --- | --- |
|
|
911
1000
|
| text / varchar | yes | yes | yes |
|
|
912
|
-
| timestamp, date | yes | yes | — |
|
|
1001
|
+
| timestamp (`DateTimeFilter`), date (`DateFilter`) | yes | yes | — |
|
|
913
1002
|
| int, float, bigint, decimal | yes | yes | — |
|
|
914
1003
|
| uuid | yes | yes | — |
|
|
915
1004
|
| boolean | yes | — | — |
|
|
@@ -1531,6 +1620,32 @@ mutation {
|
|
|
1531
1620
|
- A `set` that carries only relation fields is a valid update: it changes what the row is
|
|
1532
1621
|
attached to without rewriting the row
|
|
1533
1622
|
|
|
1623
|
+
### The target table's own rules
|
|
1624
|
+
|
|
1625
|
+
A nested write is a write to another table, so it follows that table's rules, not just the
|
|
1626
|
+
root field's:
|
|
1627
|
+
|
|
1628
|
+
- **`scope`** narrows the rows `connect` / `disconnect` / `set` can reach, and
|
|
1629
|
+
**`contextValues`** stamp the rows a nested `create` inserts
|
|
1630
|
+
- **`features`**: each nested input is generated only when the table it writes has that
|
|
1631
|
+
write turned on. Whatever is left out is noted on the console at build time, and a
|
|
1632
|
+
relation with nothing left loses its field altogether
|
|
1633
|
+
|
|
1634
|
+
| Input | Needs, on the table it writes |
|
|
1635
|
+
| --- | --- |
|
|
1636
|
+
| `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) |
|
|
1637
|
+
| `connect` / `disconnect` / `set`, key on the target | the target's `update` |
|
|
1638
|
+
| `connect` / `disconnect`, key on the parent | nothing — only the parent's own row changes |
|
|
1639
|
+
| many-to-many `connect` | the junction's `insert` |
|
|
1640
|
+
| many-to-many `disconnect` | the junction's `delete` |
|
|
1641
|
+
| many-to-many `set` | the junction's `insert` and `delete` |
|
|
1642
|
+
|
|
1643
|
+
An excluded junction table takes its relation's inputs with it. A table whose writes are
|
|
1644
|
+
reserved for a hand-written mutation (`insert: false`) can therefore no longer be written
|
|
1645
|
+
by another table's nested `create` either
|
|
1646
|
+
- **`onWrite`**: the hooks of the table a nested statement writes run too — see
|
|
1647
|
+
[Nested writes and hooks](#nested-writes-and-hooks)
|
|
1648
|
+
|
|
1534
1649
|
### Many-to-many relations
|
|
1535
1650
|
|
|
1536
1651
|
A relation declared with `.through()` is written by inserting and deleting rows of its
|
|
@@ -2034,7 +2149,7 @@ To control the boundary yourself, open a transaction and put it on the GraphQL c
|
|
|
2034
2149
|
the exported `drizzleExecutorKey`:
|
|
2035
2150
|
|
|
2036
2151
|
```Typescript
|
|
2037
|
-
import { buildSchema, drizzleExecutorKey } from 'drizzle-graphql'
|
|
2152
|
+
import { buildSchema, drizzleExecutorKey } from '@vantreeseba/drizzle-graphql'
|
|
2038
2153
|
|
|
2039
2154
|
const { schema } = buildSchema(db)
|
|
2040
2155
|
|
|
@@ -2161,6 +2276,7 @@ The payload:
|
|
|
2161
2276
|
| `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) |
|
|
2162
2277
|
| `context`, `info` | the GraphQL context value and resolve info |
|
|
2163
2278
|
| `tx` | the executor the mutation ran on |
|
|
2279
|
+
| `nested` | `{ parentTable, relation, op }` when a [nested write](#nested-writes-and-hooks) wrote the rows; `undefined` at the mutation field's own table |
|
|
2164
2280
|
|
|
2165
2281
|
- **`tx` is the executor the mutation itself used** — one you supplied under
|
|
2166
2282
|
`drizzleExecutorKey`, the request's auto-transaction, or, for a field that would otherwise
|
|
@@ -2170,11 +2286,58 @@ The payload:
|
|
|
2170
2286
|
already does.
|
|
2171
2287
|
- **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
|
|
2172
2288
|
The `before` position and `args` still work there.
|
|
2173
|
-
-
|
|
2174
|
-
|
|
2289
|
+
- With `nestedWrites`, the hooks of **every table a nested write reaches** fire too —
|
|
2290
|
+
below.
|
|
2175
2291
|
|
|
2176
2292
|
A build with no `onWrite` opens exactly the transactions it did before.
|
|
2177
2293
|
|
|
2294
|
+
### Nested writes and hooks
|
|
2295
|
+
|
|
2296
|
+
A nested `create` / `connect` / `disconnect` / `set` runs the `before` and `after` hooks of the
|
|
2297
|
+
table its statement writes, on the same `tx`, with `payload.nested` saying where it came from:
|
|
2298
|
+
|
|
2299
|
+
```Typescript
|
|
2300
|
+
onWrite: {
|
|
2301
|
+
todos: {
|
|
2302
|
+
before: async ({ nested, args, tx }) => { /* runs ahead of the nested statement too */ },
|
|
2303
|
+
after: async ({ operation, rows, nested, tx }) => {
|
|
2304
|
+
// createProjectsSingle(values: { …, todos: { create: [ … ] } }) arrives here as
|
|
2305
|
+
// operation: 'insert', nested: { parentTable: 'projects', relation: 'todos', op: 'create' }
|
|
2306
|
+
for (const row of rows) await assertOwnsProject(tx, row.projectId)
|
|
2307
|
+
},
|
|
2308
|
+
columns: ['projectId'],
|
|
2309
|
+
},
|
|
2310
|
+
}
|
|
2311
|
+
```
|
|
2312
|
+
|
|
2313
|
+
| Nested input | `table` | `operation` |
|
|
2314
|
+
| --- | --- | --- |
|
|
2315
|
+
| `create` | the target | `insert` |
|
|
2316
|
+
| `connect` / `disconnect` / `set`, key on the target | the target | `update` (the foreign key moved; nothing is deleted) |
|
|
2317
|
+
| `connect` / `disconnect`, key on the parent | — | no target hook: only the parent's row changes, and its own hook sees that |
|
|
2318
|
+
| many-to-many `connect` | the junction | `insert` |
|
|
2319
|
+
| many-to-many `disconnect` | the junction | `delete` |
|
|
2320
|
+
| many-to-many `set` | the junction | `delete` (the old links), then `insert` (the new ones) |
|
|
2321
|
+
|
|
2322
|
+
- **`nested`** is `{ parentTable, relation, op }` — the Drizzle schema key of the table whose
|
|
2323
|
+
input carried the relation field, the relation's name, and which of its inputs this is.
|
|
2324
|
+
It is `undefined` at the mutation field's own table, so a hook that only cares about root
|
|
2325
|
+
writes starts with `if (payload.nested) return`
|
|
2326
|
+
- **One call per input and operation**, not per row: a nested `create` of three rows is one
|
|
2327
|
+
`insert` call with three rows, and an update whose `where` matched several parents hands
|
|
2328
|
+
the hook every child it wrote across all of them. A to-one `connect` that also detaches
|
|
2329
|
+
the row attached before is one `update` call holding both rows; a detached row's key is
|
|
2330
|
+
`null`
|
|
2331
|
+
- **`rows`** hold the table's primary key and the join column(s) the statement changed — a
|
|
2332
|
+
nested write has no client selection of its own — plus whatever the hook names in
|
|
2333
|
+
`columns`
|
|
2334
|
+
- **`args`, `context` and `info`** are the root mutation field's; **`single`** says whether the
|
|
2335
|
+
relation is a to-one
|
|
2336
|
+
- **`before`** runs ahead of that input's first statement, **`after`** once its last has
|
|
2337
|
+
returned, and both inside the transaction every nested write opens — so a hook on a child
|
|
2338
|
+
table can roll the whole tree back, the parent row included, even when the root field has
|
|
2339
|
+
no hook of its own
|
|
2340
|
+
|
|
2178
2341
|
|
|
2179
2342
|
## Relations & N+1 handling
|
|
2180
2343
|
|