@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 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 neeeds. It's compatible witn any server that consumes `GraphQLSchema` class instance
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, or `nestedWrites` alongside a table whose own writes are off
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` | `DateTime` | an ISO-8601 string |
608
- | `date` | `Date` | a `YYYY-MM-DD` string |
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
- - The hook fires for the **mutation field's own table**. Rows a nested `create` writes into
2174
- another table do not fire that table's hook.
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