@vantreeseba/drizzle-graphql 7.1.0 → 7.3.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 +174 -2
- package/dist/README.md +174 -2
- package/dist/index.cjs +317 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -2
- package/dist/index.d.ts +56 -2
- package/dist/index.js +318 -41
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,60 @@ const { schema } = buildSchema(db, {
|
|
|
124
124
|
`mutations: false`
|
|
125
125
|
- List and single queries are always generated, so `Query` is never empty
|
|
126
126
|
|
|
127
|
+
## Excluding tables and columns
|
|
128
|
+
|
|
129
|
+
`features` decides which *kinds* of operation exist. `exclude` decides which tables and
|
|
130
|
+
columns exist at all — for the table holding session tokens, or the column holding a
|
|
131
|
+
password hash:
|
|
132
|
+
|
|
133
|
+
```Typescript
|
|
134
|
+
const { schema } = buildSchema(db, {
|
|
135
|
+
exclude: {
|
|
136
|
+
tables: ['magicLinks', 'sessions'],
|
|
137
|
+
columns: { apiKeys: ['keyHash'] },
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Both lists are keyed by the **Drizzle schema key**, not the database name and not the
|
|
143
|
+
generated GraphQL name.
|
|
144
|
+
|
|
145
|
+
An excluded **table** generates nothing: no object type, no queries, no mutations, no
|
|
146
|
+
aggregates, and no relation field on any other table that points at it. `Users.sessions`
|
|
147
|
+
is gone, and so is `users(where: { sessions: { … } })` — the table is unreachable, not
|
|
148
|
+
merely unnamed.
|
|
149
|
+
|
|
150
|
+
An excluded **column** disappears from every surface at once:
|
|
151
|
+
|
|
152
|
+
| Surface | Effect |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `<Type>` object type | field removed |
|
|
155
|
+
| `Create<Type>Input` / `Update<Type>Input` | field removed (nested-write payloads too) |
|
|
156
|
+
| `<Type>Filters` | field removed |
|
|
157
|
+
| `<Type>OrderBy` | field removed |
|
|
158
|
+
| `<Type>MinAggregate` / `<Type>MaxAggregate` / … | field removed |
|
|
159
|
+
| `<Type>DistinctColumn` / `<Type>GroupByColumn` | value removed |
|
|
160
|
+
| `<Type>ConflictColumn` (upsert targets) | value removed |
|
|
161
|
+
|
|
162
|
+
Two details worth stating plainly:
|
|
163
|
+
|
|
164
|
+
- **The exclusion is all-or-nothing on purpose.** A column you can filter on but not
|
|
165
|
+
select is an oracle: `where: { keyHash: { eq: '…' } }` confirms a value without ever
|
|
166
|
+
returning it. Ordering and grouping leak the same way, more slowly. There is no
|
|
167
|
+
read-only or write-only variant of `exclude` for this reason.
|
|
168
|
+
- **Nothing changes below the schema.** An excluded column is still written by its
|
|
169
|
+
default, still read by the primary-key and cursor machinery, and still usable by your
|
|
170
|
+
own resolvers. Only the generated schema stops mentioning it.
|
|
171
|
+
|
|
172
|
+
Names that match nothing throw at build time, so renaming a column fails the build instead
|
|
173
|
+
of quietly un-hiding it. Excluding a `NOT NULL` column with no default is allowed — the
|
|
174
|
+
table becomes readable but not insertable — and warns on the console, since that is more
|
|
175
|
+
often a mistake than a plan.
|
|
176
|
+
|
|
177
|
+
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
178
|
+
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
179
|
+
> or an equivalent layer above the schema, which has the request context this library
|
|
180
|
+
> does not.
|
|
127
181
|
## Naming
|
|
128
182
|
|
|
129
183
|
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
@@ -348,6 +402,124 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
348
402
|
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
349
403
|
to-many relation
|
|
350
404
|
|
|
405
|
+
## Matching lists of values
|
|
406
|
+
|
|
407
|
+
`inArray` / `notInArray` take a list of candidate values and compile to SQL `IN` / `NOT IN`.
|
|
408
|
+
Two things about them are worth stating explicitly.
|
|
409
|
+
|
|
410
|
+
**An empty list is answered, not rejected.** A caller building a filter from a variable often
|
|
411
|
+
ends up with an empty list, and that list has a well-defined meaning: nothing is `IN ()` and
|
|
412
|
+
everything is `NOT IN ()`.
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
{
|
|
416
|
+
# no rows
|
|
417
|
+
posts(where: { id: { inArray: [] } }) {
|
|
418
|
+
id
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# every row
|
|
422
|
+
posts(where: { id: { notInArray: [] } }) {
|
|
423
|
+
id
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The same rule applies to the Postgres array-column operators: `hasSome: []` matches nothing
|
|
429
|
+
(overlapping with no elements is never true) and `hasEvery: []` matches everything (every
|
|
430
|
+
array contains all zero of them).
|
|
431
|
+
|
|
432
|
+
**The list's own order can be the sort order.** SQL returns `IN` matches in whatever order it
|
|
433
|
+
likes, so a caller that fetches rows by a ranked list of ids normally has to re-sort them
|
|
434
|
+
client-side. Setting `matchFilterOrder` on an `orderBy` entry sorts by the row's position in
|
|
435
|
+
that column's `inArray` list instead of by the column's value:
|
|
436
|
+
|
|
437
|
+
```graphql
|
|
438
|
+
{
|
|
439
|
+
posts(
|
|
440
|
+
where: { id: { inArray: [7, 3, 12] } }
|
|
441
|
+
orderBy: { id: { direction: asc, priority: 1, matchFilterOrder: true } }
|
|
442
|
+
) {
|
|
443
|
+
id # → 7, 3, 12
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
- `direction: asc` keeps the list's order; `desc` reverses it
|
|
449
|
+
- It needs an `inArray` filter on the same column, at the top level of `where` — an
|
|
450
|
+
`inArray` buried inside an `OR` branch raises an error rather than being guessed at
|
|
451
|
+
- It interleaves with the other `orderBy` entries by `priority` like any ordering key, and
|
|
452
|
+
works on relation fields as well as root lists
|
|
453
|
+
- It compiles to a `CASE` ladder over the list values, so it behaves identically on all
|
|
454
|
+
three dialects and every value stays a bound parameter
|
|
455
|
+
- It cannot be combined with `after` or `distinct`: a row's position in the request's own
|
|
456
|
+
filter list is not a value of the row, so it cannot be encoded into a cursor or rebuilt
|
|
457
|
+
against a subquery. Selecting `cursor` under it yields `null` rather than an error
|
|
458
|
+
|
|
459
|
+
## Filtering inside JSON documents
|
|
460
|
+
|
|
461
|
+
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
462
|
+
document, alongside the whole-document `eq` / `ne` / `contains` operators:
|
|
463
|
+
|
|
464
|
+
```graphql
|
|
465
|
+
{
|
|
466
|
+
users(where: { meta: { path: { path: ["profile", "level"], gt: 2 } } }) {
|
|
467
|
+
id
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`path` walks the document key by key from the root; an all-digits key indexes an array
|
|
473
|
+
(`["tags", "0"]`). Several predicates may be given at once and are `AND`ed:
|
|
474
|
+
|
|
475
|
+
```graphql
|
|
476
|
+
{
|
|
477
|
+
users(
|
|
478
|
+
where: {
|
|
479
|
+
meta: {
|
|
480
|
+
path: [
|
|
481
|
+
{ path: ["profile", "role"], eq: "admin" }
|
|
482
|
+
{ path: ["profile", "level"], gte: 3 }
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
) {
|
|
487
|
+
id
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Available operators are `eq` / `ne` / `lt` / `lte` / `gt` / `gte`, the safe string matchers
|
|
493
|
+
`startsWith` / `endsWith` / `contains` (plus their case-insensitive `i`-prefixed forms), and
|
|
494
|
+
`isNull` / `isNotNull`. A missing key and a JSON `null` both read as null.
|
|
495
|
+
|
|
496
|
+
**How the value is compared** is decided by the operand: a GraphQL number compares
|
|
497
|
+
numerically, a boolean as a boolean, and anything else as text. Pass `as: TEXT | NUMBER |
|
|
498
|
+
BOOLEAN` when the operand's type doesn't match the document's — comparing a numeric field
|
|
499
|
+
against a `String` variable, say:
|
|
500
|
+
|
|
501
|
+
```graphql
|
|
502
|
+
{
|
|
503
|
+
users(where: { meta: { path: { path: ["profile", "level"], as: NUMBER, eq: "3" } } }) {
|
|
504
|
+
id
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
A value of the wrong shape never matches rather than failing the query: `gt: 0` against a
|
|
510
|
+
document whose path holds `"admin"` returns no rows on every dialect. That takes a guard on
|
|
511
|
+
each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts a non-numeric
|
|
512
|
+
string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
|
|
513
|
+
number.
|
|
514
|
+
|
|
515
|
+
Note that `contains` means different things at the two levels, because a path names a scalar:
|
|
516
|
+
on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
|
|
517
|
+
inside a `path` filter it is substring matching on the extracted value.
|
|
518
|
+
|
|
519
|
+
Path filters combine with `AND` / `OR` / `NOT` like any other operator, and are accepted
|
|
520
|
+
everywhere a filter is — list, single and aggregate queries, `update` / `delete`, and the
|
|
521
|
+
`where` argument on a relation field.
|
|
522
|
+
|
|
351
523
|
## Aggregate queries
|
|
352
524
|
|
|
353
525
|
Every table also gets an aggregate query field — `<tableName>Aggregate` (e.g. `usersAggregate`),
|
|
@@ -588,8 +760,8 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
588
760
|
- Dates and bigints round-trip losslessly through the cursor
|
|
589
761
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
590
762
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
591
|
-
- `after` cannot be combined with `distinct`, and a table with
|
|
592
|
-
cursor pagination (its `cursor` field resolves to `null`)
|
|
763
|
+
- `after` cannot be combined with `distinct` or with `matchFilterOrder`, and a table with
|
|
764
|
+
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
593
765
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
594
766
|
skipped
|
|
595
767
|
|
package/dist/README.md
CHANGED
|
@@ -124,6 +124,60 @@ const { schema } = buildSchema(db, {
|
|
|
124
124
|
`mutations: false`
|
|
125
125
|
- List and single queries are always generated, so `Query` is never empty
|
|
126
126
|
|
|
127
|
+
## Excluding tables and columns
|
|
128
|
+
|
|
129
|
+
`features` decides which *kinds* of operation exist. `exclude` decides which tables and
|
|
130
|
+
columns exist at all — for the table holding session tokens, or the column holding a
|
|
131
|
+
password hash:
|
|
132
|
+
|
|
133
|
+
```Typescript
|
|
134
|
+
const { schema } = buildSchema(db, {
|
|
135
|
+
exclude: {
|
|
136
|
+
tables: ['magicLinks', 'sessions'],
|
|
137
|
+
columns: { apiKeys: ['keyHash'] },
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Both lists are keyed by the **Drizzle schema key**, not the database name and not the
|
|
143
|
+
generated GraphQL name.
|
|
144
|
+
|
|
145
|
+
An excluded **table** generates nothing: no object type, no queries, no mutations, no
|
|
146
|
+
aggregates, and no relation field on any other table that points at it. `Users.sessions`
|
|
147
|
+
is gone, and so is `users(where: { sessions: { … } })` — the table is unreachable, not
|
|
148
|
+
merely unnamed.
|
|
149
|
+
|
|
150
|
+
An excluded **column** disappears from every surface at once:
|
|
151
|
+
|
|
152
|
+
| Surface | Effect |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `<Type>` object type | field removed |
|
|
155
|
+
| `Create<Type>Input` / `Update<Type>Input` | field removed (nested-write payloads too) |
|
|
156
|
+
| `<Type>Filters` | field removed |
|
|
157
|
+
| `<Type>OrderBy` | field removed |
|
|
158
|
+
| `<Type>MinAggregate` / `<Type>MaxAggregate` / … | field removed |
|
|
159
|
+
| `<Type>DistinctColumn` / `<Type>GroupByColumn` | value removed |
|
|
160
|
+
| `<Type>ConflictColumn` (upsert targets) | value removed |
|
|
161
|
+
|
|
162
|
+
Two details worth stating plainly:
|
|
163
|
+
|
|
164
|
+
- **The exclusion is all-or-nothing on purpose.** A column you can filter on but not
|
|
165
|
+
select is an oracle: `where: { keyHash: { eq: '…' } }` confirms a value without ever
|
|
166
|
+
returning it. Ordering and grouping leak the same way, more slowly. There is no
|
|
167
|
+
read-only or write-only variant of `exclude` for this reason.
|
|
168
|
+
- **Nothing changes below the schema.** An excluded column is still written by its
|
|
169
|
+
default, still read by the primary-key and cursor machinery, and still usable by your
|
|
170
|
+
own resolvers. Only the generated schema stops mentioning it.
|
|
171
|
+
|
|
172
|
+
Names that match nothing throw at build time, so renaming a column fails the build instead
|
|
173
|
+
of quietly un-hiding it. Excluding a `NOT NULL` column with no default is allowed — the
|
|
174
|
+
table becomes readable but not insertable — and warns on the console, since that is more
|
|
175
|
+
often a mistake than a plan.
|
|
176
|
+
|
|
177
|
+
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
178
|
+
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
179
|
+
> or an equivalent layer above the schema, which has the request context this library
|
|
180
|
+
> does not.
|
|
127
181
|
## Naming
|
|
128
182
|
|
|
129
183
|
Type and field names are derived from the Drizzle schema keys. Three options change them.
|
|
@@ -348,6 +402,124 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
348
402
|
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
349
403
|
to-many relation
|
|
350
404
|
|
|
405
|
+
## Matching lists of values
|
|
406
|
+
|
|
407
|
+
`inArray` / `notInArray` take a list of candidate values and compile to SQL `IN` / `NOT IN`.
|
|
408
|
+
Two things about them are worth stating explicitly.
|
|
409
|
+
|
|
410
|
+
**An empty list is answered, not rejected.** A caller building a filter from a variable often
|
|
411
|
+
ends up with an empty list, and that list has a well-defined meaning: nothing is `IN ()` and
|
|
412
|
+
everything is `NOT IN ()`.
|
|
413
|
+
|
|
414
|
+
```graphql
|
|
415
|
+
{
|
|
416
|
+
# no rows
|
|
417
|
+
posts(where: { id: { inArray: [] } }) {
|
|
418
|
+
id
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# every row
|
|
422
|
+
posts(where: { id: { notInArray: [] } }) {
|
|
423
|
+
id
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The same rule applies to the Postgres array-column operators: `hasSome: []` matches nothing
|
|
429
|
+
(overlapping with no elements is never true) and `hasEvery: []` matches everything (every
|
|
430
|
+
array contains all zero of them).
|
|
431
|
+
|
|
432
|
+
**The list's own order can be the sort order.** SQL returns `IN` matches in whatever order it
|
|
433
|
+
likes, so a caller that fetches rows by a ranked list of ids normally has to re-sort them
|
|
434
|
+
client-side. Setting `matchFilterOrder` on an `orderBy` entry sorts by the row's position in
|
|
435
|
+
that column's `inArray` list instead of by the column's value:
|
|
436
|
+
|
|
437
|
+
```graphql
|
|
438
|
+
{
|
|
439
|
+
posts(
|
|
440
|
+
where: { id: { inArray: [7, 3, 12] } }
|
|
441
|
+
orderBy: { id: { direction: asc, priority: 1, matchFilterOrder: true } }
|
|
442
|
+
) {
|
|
443
|
+
id # → 7, 3, 12
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
- `direction: asc` keeps the list's order; `desc` reverses it
|
|
449
|
+
- It needs an `inArray` filter on the same column, at the top level of `where` — an
|
|
450
|
+
`inArray` buried inside an `OR` branch raises an error rather than being guessed at
|
|
451
|
+
- It interleaves with the other `orderBy` entries by `priority` like any ordering key, and
|
|
452
|
+
works on relation fields as well as root lists
|
|
453
|
+
- It compiles to a `CASE` ladder over the list values, so it behaves identically on all
|
|
454
|
+
three dialects and every value stays a bound parameter
|
|
455
|
+
- It cannot be combined with `after` or `distinct`: a row's position in the request's own
|
|
456
|
+
filter list is not a value of the row, so it cannot be encoded into a cursor or rebuilt
|
|
457
|
+
against a subquery. Selecting `cursor` under it yields `null` rather than an error
|
|
458
|
+
|
|
459
|
+
## Filtering inside JSON documents
|
|
460
|
+
|
|
461
|
+
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
462
|
+
document, alongside the whole-document `eq` / `ne` / `contains` operators:
|
|
463
|
+
|
|
464
|
+
```graphql
|
|
465
|
+
{
|
|
466
|
+
users(where: { meta: { path: { path: ["profile", "level"], gt: 2 } } }) {
|
|
467
|
+
id
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`path` walks the document key by key from the root; an all-digits key indexes an array
|
|
473
|
+
(`["tags", "0"]`). Several predicates may be given at once and are `AND`ed:
|
|
474
|
+
|
|
475
|
+
```graphql
|
|
476
|
+
{
|
|
477
|
+
users(
|
|
478
|
+
where: {
|
|
479
|
+
meta: {
|
|
480
|
+
path: [
|
|
481
|
+
{ path: ["profile", "role"], eq: "admin" }
|
|
482
|
+
{ path: ["profile", "level"], gte: 3 }
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
) {
|
|
487
|
+
id
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Available operators are `eq` / `ne` / `lt` / `lte` / `gt` / `gte`, the safe string matchers
|
|
493
|
+
`startsWith` / `endsWith` / `contains` (plus their case-insensitive `i`-prefixed forms), and
|
|
494
|
+
`isNull` / `isNotNull`. A missing key and a JSON `null` both read as null.
|
|
495
|
+
|
|
496
|
+
**How the value is compared** is decided by the operand: a GraphQL number compares
|
|
497
|
+
numerically, a boolean as a boolean, and anything else as text. Pass `as: TEXT | NUMBER |
|
|
498
|
+
BOOLEAN` when the operand's type doesn't match the document's — comparing a numeric field
|
|
499
|
+
against a `String` variable, say:
|
|
500
|
+
|
|
501
|
+
```graphql
|
|
502
|
+
{
|
|
503
|
+
users(where: { meta: { path: { path: ["profile", "level"], as: NUMBER, eq: "3" } } }) {
|
|
504
|
+
id
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
A value of the wrong shape never matches rather than failing the query: `gt: 0` against a
|
|
510
|
+
document whose path holds `"admin"` returns no rows on every dialect. That takes a guard on
|
|
511
|
+
each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts a non-numeric
|
|
512
|
+
string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
|
|
513
|
+
number.
|
|
514
|
+
|
|
515
|
+
Note that `contains` means different things at the two levels, because a path names a scalar:
|
|
516
|
+
on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
|
|
517
|
+
inside a `path` filter it is substring matching on the extracted value.
|
|
518
|
+
|
|
519
|
+
Path filters combine with `AND` / `OR` / `NOT` like any other operator, and are accepted
|
|
520
|
+
everywhere a filter is — list, single and aggregate queries, `update` / `delete`, and the
|
|
521
|
+
`where` argument on a relation field.
|
|
522
|
+
|
|
351
523
|
## Aggregate queries
|
|
352
524
|
|
|
353
525
|
Every table also gets an aggregate query field — `<tableName>Aggregate` (e.g. `usersAggregate`),
|
|
@@ -588,8 +760,8 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
588
760
|
- Dates and bigints round-trip losslessly through the cursor
|
|
589
761
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
590
762
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
591
|
-
- `after` cannot be combined with `distinct`, and a table with
|
|
592
|
-
cursor pagination (its `cursor` field resolves to `null`)
|
|
763
|
+
- `after` cannot be combined with `distinct` or with `matchFilterOrder`, and a table with
|
|
764
|
+
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
593
765
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
594
766
|
skipped
|
|
595
767
|
|