@vantreeseba/drizzle-graphql 14.0.0 → 15.1.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 +196 -75
- package/dist/index.cjs +541 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.js +541 -156
- 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
|
-
- Pass your drizzle database instance
|
|
8
|
-
- Use `schema` if pre-built schema already satisfies all your
|
|
9
|
+
- Pass your drizzle database instance into the builder to generate a `{ schema, entities }` object. The tables are read from the `relations` you gave the drizzle constructor (`drizzle({ client, relations })`), so a table missing from your relations config is not part of the generated schema
|
|
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,10 +35,11 @@ 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'
|
|
42
|
+
import { users } from './schema'
|
|
39
43
|
|
|
40
44
|
const { entities } = buildSchema(db)
|
|
41
45
|
|
|
@@ -60,7 +64,7 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
|
|
|
60
64
|
},
|
|
61
65
|
resolve: async (source, args, context, info) => {
|
|
62
66
|
// Your custom logic goes here...
|
|
63
|
-
const result = await db.select(
|
|
67
|
+
const result = await db.select().from(users).where(/* … */)
|
|
64
68
|
|
|
65
69
|
return result
|
|
66
70
|
}
|
|
@@ -118,7 +122,7 @@ to leave that table with the default naming. The preset is exported as `singular
|
|
|
118
122
|
so it can be wrapped:
|
|
119
123
|
|
|
120
124
|
```Typescript
|
|
121
|
-
import { buildSchema, singularizeMapper } from 'drizzle-graphql'
|
|
125
|
+
import { buildSchema, singularizeMapper } from '@vantreeseba/drizzle-graphql'
|
|
122
126
|
|
|
123
127
|
const { schema } = buildSchema(db, {
|
|
124
128
|
typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
|
|
@@ -165,8 +169,9 @@ const { schema } = buildSchema(db, {
|
|
|
165
169
|
turns the group-by queries off with it
|
|
166
170
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
167
171
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
168
|
-
- Turning off
|
|
169
|
-
`mutations: false`
|
|
172
|
+
- Turning off every mutation feature omits the `Mutation` type entirely, the same as
|
|
173
|
+
`mutations: false` — unless a table has `softDelete`, whose `restore` mutations are
|
|
174
|
+
generated regardless
|
|
170
175
|
- List and single queries are always generated, so `Query` is never empty
|
|
171
176
|
|
|
172
177
|
### Per-table operations
|
|
@@ -341,9 +346,7 @@ Together the two close the loop: `contextValues` decides which tenant a new row
|
|
|
341
346
|
|
|
342
347
|
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
343
348
|
build instead of quietly ceasing to be scoped.
|
|
344
|
-
|
|
345
|
-
> or an equivalent layer above the schema, which has the request context this library
|
|
346
|
-
> does not.
|
|
349
|
+
|
|
347
350
|
## Naming
|
|
348
351
|
|
|
349
352
|
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
@@ -460,7 +463,7 @@ Keyed by the **Drizzle schema key**, like `scope` and `exclude`. A function form
|
|
|
460
463
|
accepted, for a convention applied across the schema:
|
|
461
464
|
|
|
462
465
|
```Typescript
|
|
463
|
-
softDelete: (table, tableName) => ('deletedAt' in
|
|
466
|
+
softDelete: (table, tableName) => ('deletedAt' in getColumns(table) ? 'deletedAt' : undefined)
|
|
464
467
|
```
|
|
465
468
|
|
|
466
469
|
What a declared table gets:
|
|
@@ -606,8 +609,10 @@ generated SDL says what a field actually holds instead of falling back to `Strin
|
|
|
606
609
|
| `json` / `jsonb` (and `mode: 'json'`) | `JSON` | the parsed value |
|
|
607
610
|
| `bigint` (and `mode: 'bigint'`) | `BigInt` | a decimal string |
|
|
608
611
|
| `uuid` | `UUID` | a validated UUID string |
|
|
609
|
-
| `timestamp` / `datetime`
|
|
610
|
-
| `
|
|
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 |
|
|
615
|
+
| `numeric` / `decimal` | `Decimal` | a decimal string |
|
|
611
616
|
|
|
612
617
|
```graphql
|
|
613
618
|
mutation {
|
|
@@ -625,15 +630,100 @@ mutation {
|
|
|
625
630
|
`Number.MAX_SAFE_INTEGER` survive the round-trip. Integer literals are accepted on input;
|
|
626
631
|
floats and non-numeric strings are rejected
|
|
627
632
|
- **`UUID`** validates the format on the way in, including inside `where` filters
|
|
633
|
+
- **`Decimal`** is a decimal string in both directions, so the precision the column declares
|
|
634
|
+
is not lost to a JS float. Non-numeric and non-finite input is rejected
|
|
635
|
+
- **`Date`** is a calendar day — no time, no offset — and is the same type in every position
|
|
636
|
+
a `date` column appears in: the object field, the create and update inputs, the
|
|
637
|
+
`DateFilter` operands, aggregate `min` / `max` and group keys. It takes and returns an
|
|
638
|
+
RFC 3339 `full-date` (`2026-09-15`) whichever `mode` the column is declared with; a
|
|
639
|
+
`mode: 'date'` column is read back from its JS `Date` as the UTC day, so no server timezone
|
|
640
|
+
shifts it. Input must name a real day: `2026-02-30`, `2026-13-45` and datetime strings like
|
|
641
|
+
`2026-09-15T10:00:00Z` are rejected. With `graphql-codegen`, map it as
|
|
642
|
+
`scalars: { Date: 'string' }`
|
|
643
|
+
- **`DateTime`** is an instant, and is likewise the same type in every position a Postgres
|
|
644
|
+
`timestamp`, MySQL `timestamp` / `datetime` or SQLite integer timestamp column appears in, with `DateTimeFilter` in
|
|
645
|
+
filters, whichever `mode` the column is declared with. It returns an ISO-8601 string in UTC.
|
|
646
|
+
MySQL stores these columns without an offset; the value is written and read as UTC
|
|
647
|
+
(`2026-09-15T10:00:00Z` is stored as `2026-09-15 10:00:00`) whatever the Node process's
|
|
648
|
+
timezone, the way drizzle itself treats them. Postgres `timestamp` without time zone is
|
|
649
|
+
treated the same way, in both modes; `timestamptz` stores the instant, and its text in
|
|
650
|
+
`mode: 'string'` (`2026-09-15 04:00:00-06`) is returned as the same instant in UTC. The
|
|
651
|
+
scalar carries milliseconds, so a `timestamp(6)` value's microseconds are truncated on read
|
|
652
|
+
and on write. A SQLite `integer({ mode: 'timestamp' })` or `integer({ mode: 'timestamp_ms' })`
|
|
653
|
+
column is `DateTime` too; it stores the instant as epoch seconds or milliseconds. Seconds
|
|
654
|
+
mode cannot hold a fraction, so it is floored (never rounded) on write —
|
|
655
|
+
`2026-09-15T10:00:00.999Z` is stored as `1789466400` and reads back as
|
|
656
|
+
`2026-09-15T10:00:00.000Z` — and filter operands on such a column are floored the same way,
|
|
657
|
+
so `eq` with the value you wrote finds the row you wrote it to
|
|
628
658
|
|
|
629
659
|
The scalars are exported if you need them in a hand-written schema:
|
|
630
660
|
|
|
631
661
|
```Typescript
|
|
632
|
-
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from 'drizzle-graphql'
|
|
662
|
+
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLDecimalString, GraphQLJSON, GraphQLUUID } from '@vantreeseba/drizzle-graphql'
|
|
633
663
|
```
|
|
634
664
|
|
|
635
665
|
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
636
|
-
called in SDL, to avoid clashing with the language's own `BigInt`.
|
|
666
|
+
called in SDL, to avoid clashing with the language's own `BigInt`. `GraphQLDate` is this
|
|
667
|
+
library's own `Date` scalar, not `graphql-scalars`' (whose `parseValue` returns a JS `Date`).
|
|
668
|
+
|
|
669
|
+
> **Upgrading — `date` columns.** A `date` column used to be typed differently depending on
|
|
670
|
+
> where it appeared and which `mode` it was declared with. Both modes are now `Date` everywhere:
|
|
671
|
+
>
|
|
672
|
+
> | Drizzle column | position | before | now |
|
|
673
|
+
> | --- | --- | --- | --- |
|
|
674
|
+
> | `date(…, { mode: 'string' })` | output | `Date` | `Date` |
|
|
675
|
+
> | | create / update input | `String` | `Date` |
|
|
676
|
+
> | | filter | `StringFilter` | `DateFilter` |
|
|
677
|
+
> | `date(…)` / `{ mode: 'date' }` (Postgres) | output, input, min/max, group keys | `DateTime` | `Date` |
|
|
678
|
+
> | | filter | `DateTimeFilter` | `DateFilter` |
|
|
679
|
+
> | `date(…, { mode: 'date' })` (MySQL) | output, input, min/max, group keys | `JSON` | `Date` |
|
|
680
|
+
> | | filter | `JSONFilter` | `DateFilter` |
|
|
681
|
+
> | `date(…, { mode: 'string' })` (MySQL) | output, input, min/max, group keys | `String` | `Date` |
|
|
682
|
+
> | | filter | `StringFilter` | `DateFilter` |
|
|
683
|
+
>
|
|
684
|
+
> What to change:
|
|
685
|
+
>
|
|
686
|
+
> - Variables typed `String` or `DateTime` for a date field become `Date`; filters on it are
|
|
687
|
+
> `DateFilter`, which has the `DateTimeFilter` operators (`eq` / `ne` / `lt` / `lte` / `gt` /
|
|
688
|
+
> `gte` / `inArray` / `notInArray` / `isNull` / `isNotNull`) and none of the string ones
|
|
689
|
+
> - Send `YYYY-MM-DD`. A datetime string such as `2026-09-15T00:00:00.000Z` used to be
|
|
690
|
+
> truncated to its date and is now rejected
|
|
691
|
+
> - A `mode: 'date'` column read `2026-09-15T00:00:00.000Z` and now reads `2026-09-15`
|
|
692
|
+
> - In `graphql-codegen`, map `scalars: { Date: 'string' }` — the value is a string in both
|
|
693
|
+
> directions
|
|
694
|
+
> - If you passed `graphql-scalars`' `GraphQLDate` as a column override, pass this library's
|
|
695
|
+
> exported `GraphQLDate` instead. They share the name `Date`, so a schema holding both
|
|
696
|
+
> fails to build with graphql's "multiple types named Date" error
|
|
697
|
+
|
|
698
|
+
> **Upgrading — MySQL `timestamp` / `datetime` columns.** These used to fall through to
|
|
699
|
+
> `JSON` in `mode: 'date'` and to `String` in `mode: 'string'`. All four are now `DateTime`, as
|
|
700
|
+
> Postgres `timestamp` already was:
|
|
701
|
+
>
|
|
702
|
+
> | Drizzle column (MySQL) | position | before | now |
|
|
703
|
+
> | --- | --- | --- | --- |
|
|
704
|
+
> | `timestamp(…)` / `datetime(…)` (`mode: 'date'`) | output, input, min/max, group keys | `JSON` | `DateTime` |
|
|
705
|
+
> | | filter | `JSONFilter` | `DateTimeFilter` |
|
|
706
|
+
> | `timestamp(…)` / `datetime(…)` with `{ mode: 'string' }` | output, input, min/max, group keys | `String` | `DateTime` |
|
|
707
|
+
> | | filter | `StringFilter` | `DateTimeFilter` |
|
|
708
|
+
>
|
|
709
|
+
> Variables for these fields become `DateTime` and their filters `DateTimeFilter`, which has no
|
|
710
|
+
> `JSONFilter` / `StringFilter` operators such as `contains` or `like`. Input must be an ISO-8601
|
|
711
|
+
> date-time (`2026-09-15T10:00:00Z`, or with an offset). A `mode: 'string'` column used to read
|
|
712
|
+
> MySQL's own `2026-09-15 10:00:00` and now reads `2026-09-15T10:00:00.000Z`. In
|
|
713
|
+
> `graphql-codegen`, map `DateTime` the way you already do for Postgres timestamps.
|
|
714
|
+
|
|
715
|
+
> **Upgrading — SQLite integer timestamps.** `integer({ mode: 'timestamp' })` and
|
|
716
|
+
> `integer({ mode: 'timestamp_ms' })` used to fall through to `JSON`. Both are now `DateTime`:
|
|
717
|
+
>
|
|
718
|
+
> | Drizzle column (SQLite) | position | before | now |
|
|
719
|
+
> | --- | --- | --- | --- |
|
|
720
|
+
> | `integer(…, { mode: 'timestamp' })` / `{ mode: 'timestamp_ms' }` | output, input, min/max, group keys | `JSON` | `DateTime` |
|
|
721
|
+
> | | filter | `JSONFilter` | `DateTimeFilter` |
|
|
722
|
+
>
|
|
723
|
+
> Variables for these fields become `DateTime`, and their filters `DateTimeFilter`, which swaps
|
|
724
|
+
> the JSON `path` operator for `lt` / `lte` / `gt` / `gte` / `inArray` / `notInArray`. Input must
|
|
725
|
+
> be an ISO-8601 date-time; a JSON value such as a bare number is rejected. Reads are unchanged:
|
|
726
|
+
> they were already ISO-8601 strings in UTC. The stored integers are unchanged too.
|
|
637
727
|
|
|
638
728
|
### Overriding a column's scalar
|
|
639
729
|
|
|
@@ -694,7 +784,7 @@ only has to handle the cases it cares about:
|
|
|
694
784
|
```Typescript
|
|
695
785
|
const { schema } = buildSchema(db, {
|
|
696
786
|
// `schema` is the Postgres schema the enum was declared in, if any; `values` is its
|
|
697
|
-
// value list.
|
|
787
|
+
// value list. `enumName` and `schema` are undefined for an inline value list.
|
|
698
788
|
enumNameMapper: ({ enumName, schema }) =>
|
|
699
789
|
schema === 'reporting' ? `Reporting${enumName}Enum` : undefined,
|
|
700
790
|
})
|
|
@@ -774,9 +864,40 @@ const { schema } = buildSchema(db, {
|
|
|
774
864
|
|
|
775
865
|
- `describeColumn` reaches every place the column appears: the object type, the create and
|
|
776
866
|
update inputs, the filter input's field, `Min`/`Max` aggregates and `GroupKeys`
|
|
777
|
-
-
|
|
778
|
-
|
|
779
|
-
straight off
|
|
867
|
+
- `describeColumn` receives the drizzle column itself and `describeTable` the drizzle table
|
|
868
|
+
itself, so a project that keeps its documentation in the schema (a `WeakMap` keyed by
|
|
869
|
+
column/table, a custom property, a comment table) can read it straight off:
|
|
870
|
+
|
|
871
|
+
```Typescript
|
|
872
|
+
import type { Table } from 'drizzle-orm';
|
|
873
|
+
|
|
874
|
+
const docs = new WeakMap<object, string>();
|
|
875
|
+
|
|
876
|
+
// Register docs on the *built* table and its columns. `text('email')` is only a builder:
|
|
877
|
+
// pgTable creates a new column object from it, so keying on the builder never matches.
|
|
878
|
+
const described = <T extends Table>(
|
|
879
|
+
table: T,
|
|
880
|
+
description: string,
|
|
881
|
+
columns: Partial<Record<keyof T['_']['columns'], string>> = {},
|
|
882
|
+
): T => {
|
|
883
|
+
docs.set(table, description);
|
|
884
|
+
for (const [key, columnText] of Object.entries(columns)) {
|
|
885
|
+
docs.set((table as any)[key], columnText as string);
|
|
886
|
+
}
|
|
887
|
+
return table;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export const users = described(
|
|
891
|
+
pgTable('users', { id: integer('id').primaryKey(), email: text('email').notNull() }),
|
|
892
|
+
'A person who can sign in',
|
|
893
|
+
{ email: 'Primary login address' },
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
buildSchema(db, {
|
|
897
|
+
describeColumn: (column) => docs.get(column),
|
|
898
|
+
describeTable: (_tableName, table) => docs.get(table),
|
|
899
|
+
});
|
|
900
|
+
```
|
|
780
901
|
- `deprecateColumn` marks output fields and *optional* input fields. A required input field
|
|
781
902
|
is skipped: GraphQL forbids deprecating one, and emitting it would make the schema invalid
|
|
782
903
|
- Filter and `orderBy` fields are never deprecated — filtering on a deprecated column is how
|
|
@@ -911,7 +1032,7 @@ no others. The set follows the column's data type, never its name:
|
|
|
911
1032
|
| column type | `eq` `ne` `inArray` `notInArray` `isNull` `isNotNull` | `lt` `lte` `gt` `gte` | pattern operators + `insensitive` |
|
|
912
1033
|
| --- | --- | --- | --- |
|
|
913
1034
|
| text / varchar | yes | yes | yes |
|
|
914
|
-
| timestamp, date | yes | yes | — |
|
|
1035
|
+
| timestamp (`DateTimeFilter`), date (`DateFilter`) | yes | yes | — |
|
|
915
1036
|
| int, float, bigint, decimal | yes | yes | — |
|
|
916
1037
|
| uuid | yes | yes | — |
|
|
917
1038
|
| boolean | yes | — | — |
|
|
@@ -1003,7 +1124,8 @@ that column's `inArray` list instead of by the column's value:
|
|
|
1003
1124
|
## Filtering inside JSON documents
|
|
1004
1125
|
|
|
1005
1126
|
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
1006
|
-
document, alongside the whole-document `eq` / `ne`
|
|
1127
|
+
document, alongside the whole-document `eq` / `ne` operators and, on PostgreSQL and MySQL,
|
|
1128
|
+
structural `contains` (SQLite stores JSON as text and has no containment operator):
|
|
1007
1129
|
|
|
1008
1130
|
```graphql
|
|
1009
1131
|
{
|
|
@@ -1169,9 +1291,10 @@ columns you group by.
|
|
|
1169
1291
|
}
|
|
1170
1292
|
```
|
|
1171
1293
|
|
|
1172
|
-
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max`
|
|
1173
|
-
|
|
1174
|
-
|
|
1294
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` take one
|
|
1295
|
+
filter per numeric column (the comparison is numeric, so a `max` over a text or date
|
|
1296
|
+
column has no `having` entry even though the aggregate type returns it),
|
|
1297
|
+
`countNonNull` one per column, and `countDistinct` one per column `countDistinct` exposes
|
|
1175
1298
|
- Every entry in a `having` is ANDed together
|
|
1176
1299
|
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
1177
1300
|
whether or not you asked for `count`
|
|
@@ -1210,6 +1333,8 @@ can count or summarise related rows without fetching them:
|
|
|
1210
1333
|
- A parent with no related rows gets `count: 0` and `null` for every other aggregate
|
|
1211
1334
|
- To-one relations get no aggregate field — there is nothing to aggregate over
|
|
1212
1335
|
- The field is skipped if its name would collide with a column or another relation
|
|
1336
|
+
- Set `features: { relationAggregates: false }` (globally or per table) to leave these
|
|
1337
|
+
fields out
|
|
1213
1338
|
|
|
1214
1339
|
All parents in a selection are aggregated with a single
|
|
1215
1340
|
`SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
|
|
@@ -1233,7 +1358,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
|
|
|
1233
1358
|
```
|
|
1234
1359
|
|
|
1235
1360
|
- Which row survives each group is decided by `orderBy` — the first one wins. With no
|
|
1236
|
-
`orderBy`,
|
|
1361
|
+
`orderBy`, the table's [default ordering](#default-ordering) decides, and failing that the
|
|
1362
|
+
lowest primary key
|
|
1237
1363
|
- `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
|
|
1238
1364
|
**after**, so `limit: 10` returns ten distinct rows
|
|
1239
1365
|
- Several columns are treated as one combined key, not as independent ones
|
|
@@ -1252,7 +1378,7 @@ or **SQLite 3.25+**), and a table with no primary key cannot use it.
|
|
|
1252
1378
|
SQL gives no ordering guarantee for a query that has no `ORDER BY`, so paging through an
|
|
1253
1379
|
unordered result can return the same row twice or skip one entirely. To keep pages stable,
|
|
1254
1380
|
a query that returns only part of a table is ordered by its primary key when the request
|
|
1255
|
-
supplies no `orderBy
|
|
1381
|
+
supplies no `orderBy` and the table has no [default ordering](#default-ordering):
|
|
1256
1382
|
|
|
1257
1383
|
- list queries with `limit` and/or `offset`
|
|
1258
1384
|
- `<tableName>Single` queries, which are an implicit `limit 1`
|
|
@@ -1260,7 +1386,8 @@ supplies no `orderBy`:
|
|
|
1260
1386
|
`orderBy` you do supply, so per-parent slices are deterministic)
|
|
1261
1387
|
|
|
1262
1388
|
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
1263
|
-
key column, and an unpaginated list query
|
|
1389
|
+
key column, and an unpaginated list query of a table with no `defaults` entry is left
|
|
1390
|
+
unordered so no sort is paid for.
|
|
1264
1391
|
|
|
1265
1392
|
## Default ordering
|
|
1266
1393
|
|
|
@@ -1347,14 +1474,16 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
1347
1474
|
window
|
|
1348
1475
|
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
1349
1476
|
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
1350
|
-
- `NULL`s in ordered columns
|
|
1351
|
-
|
|
1352
|
-
`asc`)
|
|
1353
|
-
through, not skipped
|
|
1477
|
+
- `NULL`s in ordered columns sort where the entry's `nulls: first | last` says or, without
|
|
1478
|
+
it, at each dialect's native `ORDER BY` placement — PostgreSQL sorts them largest (last
|
|
1479
|
+
in `asc`), MySQL and SQLite smallest (first in `asc`). The predicate matches either way,
|
|
1480
|
+
so rows with `NULL` in an ordered column are paged through, not skipped, and `nulls` is
|
|
1481
|
+
part of the ordering a cursor is tied to
|
|
1354
1482
|
- Dates and bigints round-trip losslessly through the cursor
|
|
1355
1483
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
1356
1484
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
1357
|
-
- `after` cannot be combined with `distinct
|
|
1485
|
+
- `after` cannot be combined with `distinct`, with `matchFilterOrder`, or with an `orderBy`
|
|
1486
|
+
that orders through a relation (`cursor` resolves to `null` there), and a table with
|
|
1358
1487
|
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
1359
1488
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
1360
1489
|
skipped
|
|
@@ -1508,8 +1637,10 @@ mutation {
|
|
|
1508
1637
|
}
|
|
1509
1638
|
```
|
|
1510
1639
|
|
|
1511
|
-
- `connect`, `disconnect` and `set` take the target's own `Filters` input
|
|
1512
|
-
|
|
1640
|
+
- `connect`, `disconnect` and `set` take the target's own `Filters` input — a list of them
|
|
1641
|
+
on a to-many relation, a single one that must match exactly one row on a to-one — so they
|
|
1642
|
+
select rows the same way a query does. A to-one `disconnect` is just `disconnect: true`,
|
|
1643
|
+
since there is only one row to detach. A filter that matches everything is rejected rather than
|
|
1513
1644
|
attaching the whole table
|
|
1514
1645
|
- `set` replaces the relation wholesale — everything attached is detached first, then the
|
|
1515
1646
|
filters attach. It runs before `create`, so a row created in the same write survives it
|
|
@@ -1857,8 +1988,8 @@ const yoga = createYoga({ schema, plugins: armor.protect().plugins });
|
|
|
1857
1988
|
|
|
1858
1989
|
Armor's `costLimit` is its own document-shape estimate and does not read the `complexity`
|
|
1859
1990
|
extensions above; the two compose, if you want a cheap static ceiling alongside row-count
|
|
1860
|
-
aware pricing. Neither one bounds an individual `limit` argument
|
|
1861
|
-
|
|
1991
|
+
aware pricing. Neither one bounds an individual `limit` argument — a list query with no
|
|
1992
|
+
`limit` is still a full table scan unless you set a [`limits`](#bounding-page-size) policy.
|
|
1862
1993
|
|
|
1863
1994
|
## Authorization
|
|
1864
1995
|
|
|
@@ -1900,21 +2031,17 @@ request context actually lives.
|
|
|
1900
2031
|
or through your own resolver wrapper, and let the ability answer the questions.
|
|
1901
2032
|
|
|
1902
2033
|
What neither can do is **narrow** a query. They permit or deny a field; they do not append
|
|
1903
|
-
`WHERE
|
|
1904
|
-
returns every tenant's rows.
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
wrapped;
|
|
1911
|
-
- put the scoped tables behind database row-level security and give the driver the
|
|
1912
|
-
request's identity, so the narrowing happens below Drizzle entirely.
|
|
2034
|
+
`WHERE org_id = …` to the SQL a generated resolver runs, so a permitted `posts` query still
|
|
2035
|
+
returns every tenant's rows. That is what [`scope`](#row-scoping-and-multi-tenancy) is for: a
|
|
2036
|
+
per-table predicate built from the request context and ANDed onto every read and write of
|
|
2037
|
+
that table, nested relation fields included. Pair it with
|
|
2038
|
+
[`contextValues`](#columns-the-server-owns) so new rows are stamped with the caller's tenant.
|
|
2039
|
+
The usual split is shield (or CASL) to decide who may reach a field, and `scope` to decide
|
|
2040
|
+
which rows they see through it.
|
|
1913
2041
|
|
|
1914
2042
|
None of this removes anything from introspection either — `deny` hides the data, not the
|
|
1915
|
-
shape. If the schema must not so much as mention a table,
|
|
1916
|
-
|
|
1917
|
-
cheap).
|
|
2043
|
+
shape. If the schema must not so much as mention a table or column, leave it out with
|
|
2044
|
+
[`exclude`](#excluding-tables-and-columns).
|
|
1918
2045
|
|
|
1919
2046
|
Nor is it a row bound: a complexity ceiling prices the `limit` a client asks for, it does not
|
|
1920
2047
|
cap it. Use [`limits`](#bounding-page-size) for that.
|
|
@@ -2055,6 +2182,9 @@ are untouched.
|
|
|
2055
2182
|
- A document that mixes in mutation fields this build did not generate is left alone: their
|
|
2056
2183
|
completion cannot be tracked, so no transaction is opened
|
|
2057
2184
|
- It never nests — an executor supplied on the context wins, as below
|
|
2185
|
+
- A single mutation field can still run in a transaction of its own when it carries a
|
|
2186
|
+
nested write or its table has an `onWrite` hook — those open one regardless of this
|
|
2187
|
+
setting
|
|
2058
2188
|
|
|
2059
2189
|
### Supplying your own executor
|
|
2060
2190
|
|
|
@@ -2062,7 +2192,7 @@ To control the boundary yourself, open a transaction and put it on the GraphQL c
|
|
|
2062
2192
|
the exported `drizzleExecutorKey`:
|
|
2063
2193
|
|
|
2064
2194
|
```Typescript
|
|
2065
|
-
import { buildSchema, drizzleExecutorKey } from 'drizzle-graphql'
|
|
2195
|
+
import { buildSchema, drizzleExecutorKey } from '@vantreeseba/drizzle-graphql'
|
|
2066
2196
|
|
|
2067
2197
|
const { schema } = buildSchema(db)
|
|
2068
2198
|
|
|
@@ -2089,17 +2219,6 @@ example a pooled connection bound to a tenant, or a logging proxy around `db`. B
|
|
|
2089
2219
|
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
2090
2220
|
both end up loaded in one process.
|
|
2091
2221
|
|
|
2092
|
-
Or let the library open one per request: `transactions: 'auto'` wraps a document that fires
|
|
2093
|
-
more than one generated mutation in a single transaction, committed when the last one
|
|
2094
|
-
completes and rolled back if any of them fails.
|
|
2095
|
-
|
|
2096
|
-
```Typescript
|
|
2097
|
-
buildSchema(db, { transactions: 'auto' })
|
|
2098
|
-
```
|
|
2099
|
-
|
|
2100
|
-
A document with one mutation, or one that selects a mutation field this build did not
|
|
2101
|
-
generate, still runs unwrapped — there is nothing to keep atomic.
|
|
2102
|
-
|
|
2103
2222
|
## Write hooks
|
|
2104
2223
|
|
|
2105
2224
|
`onWrite` runs a hook **inside the mutation's transaction**, so its writes commit or roll back
|
|
@@ -2260,12 +2379,12 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2260
2379
|
relation in a single round-trip using Drizzle's relational query builder
|
|
2261
2380
|
(`with:`), including nested relations and per-relation `where` / `orderBy` /
|
|
2262
2381
|
`limit` / `offset` arguments.
|
|
2263
|
-
- **Mutations (PostgreSQL & SQLite)** — after an insert
|
|
2382
|
+
- **Mutations (PostgreSQL & SQLite)** — after an insert, upsert, update or `updateMany`, if the selection set
|
|
2264
2383
|
includes relation fields, the affected rows are re-fetched by primary key through one
|
|
2265
2384
|
relational query so their relations are eagerly loaded (single- and composite-column
|
|
2266
|
-
primary keys are supported). `delete` mutations keep using the
|
|
2267
|
-
|
|
2268
|
-
|
|
2385
|
+
primary keys are supported). `delete` and `restore` mutations keep using the
|
|
2386
|
+
request-scoped batch loader. MySQL mutations return only a success flag (no row
|
|
2387
|
+
payload), so this step does not apply there.
|
|
2269
2388
|
- **Custom schemas** — each relation field also has a standalone resolver, exported as
|
|
2270
2389
|
`entities.fieldResolvers[TableName][relationName]`. When you wire generated types
|
|
2271
2390
|
into your own schema and your root resolver does **not** pre-fetch relations, these
|
|
@@ -2282,9 +2401,8 @@ Generated schemas resolve nested relations without N+1 query explosions:
|
|
|
2282
2401
|
|
|
2283
2402
|
> [!IMPORTANT]
|
|
2284
2403
|
> Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
|
|
2285
|
-
> rely on SQL window functions. These require **PostgreSQL**,
|
|
2286
|
-
> **SQLite 3.25+**.
|
|
2287
|
-
> have no such requirement.
|
|
2404
|
+
> and the `distinct` argument rely on SQL window functions. These require **PostgreSQL**,
|
|
2405
|
+
> **MySQL 8.0+**, or **SQLite 3.25+**. Everything else has no such requirement.
|
|
2288
2406
|
|
|
2289
2407
|
### Relation nullability
|
|
2290
2408
|
|
|
@@ -2345,7 +2463,9 @@ const finalSchema = addResolversToSchema({
|
|
|
2345
2463
|
excludes that relation from `with:` (and from the mutation eager re-fetch).
|
|
2346
2464
|
|
|
2347
2465
|
Opting a relation out does **not** remove its field — it keeps resolving lazily via the
|
|
2348
|
-
request-scoped batch loader, so the field still works even before you override it.
|
|
2466
|
+
request-scoped batch loader, so the field still works even before you override it. A
|
|
2467
|
+
many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
|
|
2468
|
+
lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
|
|
2349
2469
|
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
2350
2470
|
of `entities.fieldResolvers`.
|
|
2351
2471
|
|
|
@@ -2388,9 +2508,10 @@ into its nested entry, and leaves `<relation>Aggregate` selections out — they
|
|
|
2388
2508
|
`resolveSelection` additionally accepts `single`, `orderBy`, `limit`, `offset` and `after`,
|
|
2389
2509
|
and returns the rows already remapped to their GraphQL transport forms.
|
|
2390
2510
|
|
|
2391
|
-
Both take
|
|
2392
|
-
|
|
2393
|
-
that renames its types has to say so.
|
|
2511
|
+
Both take the naming options the build used — `typeNameMapper`, `derivedTypeNameMapper`,
|
|
2512
|
+
`typeNamePrefix` and `typeNameSuffix`. The translation matches selections against the
|
|
2513
|
+
published GraphQL type names, so a build that renames its types has to say so here too. If
|
|
2514
|
+
the build used `typeNameMapper: 'singularize'`, pass the exported `singularizeMapper`.
|
|
2394
2515
|
|
|
2395
2516
|
## Identifying generated fields
|
|
2396
2517
|
|
|
@@ -2461,7 +2582,7 @@ that explains it, where there is one.
|
|
|
2461
2582
|
| `mutations` | `true` | `false` omits the `Mutation` type entirely |
|
|
2462
2583
|
| `features` | see [Choosing what gets generated](#choosing-what-gets-generated) | which operations are generated, per operation |
|
|
2463
2584
|
| `exclude` | none | [tables and columns left out](#excluding-tables-and-columns) of the generated schema entirely |
|
|
2464
|
-
| `relationsDepthLimit` | unlimited |
|
|
2585
|
+
| `relationsDepthLimit` | unlimited | `0` generates no relation fields (a flat, columns-only schema); any other value currently behaves like the default |
|
|
2465
2586
|
| `prefixes` / `suffixes` / `typeNameMapper` | see [Naming](#naming) | operation and type names |
|
|
2466
2587
|
| `typeNamePrefix` / `typeNameSuffix` / `derivedTypeNameMapper` | none | [names of the derived types](#renaming-the-derived-types) — filters, inputs, aggregates, shared inputs |
|
|
2467
2588
|
| `scalars` / `mapColumnType` | built-in detection | [override a column's GraphQL type](#overriding-a-columns-scalar), by name or by rule |
|
|
@@ -2469,7 +2590,7 @@ that explains it, where there is one.
|
|
|
2469
2590
|
| `enumValueNameMapper` | the stored value | [names the enum's values](#enum-value-names); `'constant'` for CONSTANT_CASE |
|
|
2470
2591
|
| `describeColumn` / `describeTable` / `describeRelation` | none | [GraphQL descriptions](#documenting-the-schema) |
|
|
2471
2592
|
| `deprecateColumn` | none | `@deprecated` on a column's output and optional input fields |
|
|
2472
|
-
| `conflictDoNothing` | `false` | PostgreSQL
|
|
2593
|
+
| `conflictDoNothing` | `false` | **Deprecated** — use [`onConflict: { action: NOTHING }`](#upsert) per request. PostgreSQL and SQLite: `ON CONFLICT DO NOTHING` on every insert, which makes `create<Table>Single` nullable; ignored on MySQL |
|
|
2473
2594
|
| `complexity` | on | [cost hints](#query-cost) in field extensions; `false` turns them off |
|
|
2474
2595
|
| `transactions` | `'none'` | `'auto'` wraps a multi-mutation request in one [transaction](#transactions) |
|
|
2475
2596
|
| `limits` | unbounded | [default and maximum page size](#bounding-page-size), globally or per table |
|