@vantreeseba/drizzle-graphql 7.2.0 → 7.4.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 +182 -2
- package/dist/README.md +182 -2
- package/dist/index.cjs +394 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -24
- package/dist/index.d.ts +101 -24
- package/dist/index.js +392 -126
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,39 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
|
|
|
85
85
|
})
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Naming the generated types and fields
|
|
89
|
+
|
|
90
|
+
By default every name comes from the table's key in the Drizzle schema: `tasks` gives type
|
|
91
|
+
`Tasks`, queries `tasks` / `tasksSingle`, mutations `createTasks` / `createTasksSingle`.
|
|
92
|
+
Most schemas use plural table keys and want singular type names, so that pairing is shipped
|
|
93
|
+
as a preset:
|
|
94
|
+
|
|
95
|
+
```Typescript
|
|
96
|
+
const { schema } = buildSchema(db, { typeNameMapper: 'singularize' })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), mutations
|
|
100
|
+
`createTasks` (array) and `createTask` (single), `updateTask`, `deleteTask`. Irregular
|
|
101
|
+
plurals go through `pluralize`, so `people` gives `Person` / `people` / `person` and
|
|
102
|
+
`addresses` gives `Address` rather than the `addresss` a naive suffix would produce. A table
|
|
103
|
+
key that is already singular is pluralized for the list side, and a capitalized key is
|
|
104
|
+
uncapitalized for the field side, so `tasks`, `task` and `Tasks` all land on the same names.
|
|
105
|
+
|
|
106
|
+
For anything else, pass your own function — it is asked per table and may return `undefined`
|
|
107
|
+
to leave that table with the default naming. The preset is exported as `singularizeMapper`
|
|
108
|
+
so it can be wrapped:
|
|
109
|
+
|
|
110
|
+
```Typescript
|
|
111
|
+
import { buildSchema, singularizeMapper } from 'drizzle-graphql'
|
|
112
|
+
|
|
113
|
+
const { schema } = buildSchema(db, {
|
|
114
|
+
typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
With a mapper in play the singular/plural forms are what keep the list and single fields
|
|
119
|
+
apart, so `suffixes` may be empty on both sides. Without one, they must still differ.
|
|
120
|
+
|
|
88
121
|
## Choosing what gets generated
|
|
89
122
|
|
|
90
123
|
Every generated operation is on by default. `features` turns individual ones off, which
|
|
@@ -124,6 +157,35 @@ const { schema } = buildSchema(db, {
|
|
|
124
157
|
`mutations: false`
|
|
125
158
|
- List and single queries are always generated, so `Query` is never empty
|
|
126
159
|
|
|
160
|
+
### Per-table operations
|
|
161
|
+
|
|
162
|
+
The choice is rarely build-wide: a schema usually has a handful of tables that must not be
|
|
163
|
+
deleted through the API, a handful whose writes belong to a hand-written resolver that
|
|
164
|
+
enforces something, and a majority that want everything. Any flag can be a predicate
|
|
165
|
+
instead of a boolean, asked once per table with the table's key in the Drizzle schema:
|
|
166
|
+
|
|
167
|
+
```Typescript
|
|
168
|
+
const { schema } = buildSchema(db, {
|
|
169
|
+
features: {
|
|
170
|
+
delete: (table) => table !== 'auditLog',
|
|
171
|
+
update: (table) => !ownedByCustomResolver.has(table),
|
|
172
|
+
upsert: (table) => table === 'settings',
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- The table is still fully readable — this decides which operations it offers, not whether
|
|
178
|
+
it is in the schema at all. For that, use `exclude` below
|
|
179
|
+
- `nestedWrites` is the one flag that stays build-wide: its plans are computed once over
|
|
180
|
+
the whole relation graph, and a nested write reaches a second table by definition
|
|
181
|
+
- `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
|
|
182
|
+
- Some operations are built out of others, and the generator knows which. A table that
|
|
183
|
+
asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
|
|
184
|
+
path past the operation you turned off — warns on the console at build time, naming the
|
|
185
|
+
tables, rather than leaving that to be discovered later. So does asking for `updateMany`
|
|
186
|
+
without `update`, `groupBy` without `aggregates`, `countMutations` with neither plural
|
|
187
|
+
write, or `nestedWrites` alongside a table whose own writes are off
|
|
188
|
+
|
|
127
189
|
## Excluding tables and columns
|
|
128
190
|
|
|
129
191
|
`features` decides which *kinds* of operation exist. `exclude` decides which tables and
|
|
@@ -402,6 +464,124 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
402
464
|
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
403
465
|
to-many relation
|
|
404
466
|
|
|
467
|
+
## Matching lists of values
|
|
468
|
+
|
|
469
|
+
`inArray` / `notInArray` take a list of candidate values and compile to SQL `IN` / `NOT IN`.
|
|
470
|
+
Two things about them are worth stating explicitly.
|
|
471
|
+
|
|
472
|
+
**An empty list is answered, not rejected.** A caller building a filter from a variable often
|
|
473
|
+
ends up with an empty list, and that list has a well-defined meaning: nothing is `IN ()` and
|
|
474
|
+
everything is `NOT IN ()`.
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
{
|
|
478
|
+
# no rows
|
|
479
|
+
posts(where: { id: { inArray: [] } }) {
|
|
480
|
+
id
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# every row
|
|
484
|
+
posts(where: { id: { notInArray: [] } }) {
|
|
485
|
+
id
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
The same rule applies to the Postgres array-column operators: `hasSome: []` matches nothing
|
|
491
|
+
(overlapping with no elements is never true) and `hasEvery: []` matches everything (every
|
|
492
|
+
array contains all zero of them).
|
|
493
|
+
|
|
494
|
+
**The list's own order can be the sort order.** SQL returns `IN` matches in whatever order it
|
|
495
|
+
likes, so a caller that fetches rows by a ranked list of ids normally has to re-sort them
|
|
496
|
+
client-side. Setting `matchFilterOrder` on an `orderBy` entry sorts by the row's position in
|
|
497
|
+
that column's `inArray` list instead of by the column's value:
|
|
498
|
+
|
|
499
|
+
```graphql
|
|
500
|
+
{
|
|
501
|
+
posts(
|
|
502
|
+
where: { id: { inArray: [7, 3, 12] } }
|
|
503
|
+
orderBy: { id: { direction: asc, priority: 1, matchFilterOrder: true } }
|
|
504
|
+
) {
|
|
505
|
+
id # → 7, 3, 12
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- `direction: asc` keeps the list's order; `desc` reverses it
|
|
511
|
+
- It needs an `inArray` filter on the same column, at the top level of `where` — an
|
|
512
|
+
`inArray` buried inside an `OR` branch raises an error rather than being guessed at
|
|
513
|
+
- It interleaves with the other `orderBy` entries by `priority` like any ordering key, and
|
|
514
|
+
works on relation fields as well as root lists
|
|
515
|
+
- It compiles to a `CASE` ladder over the list values, so it behaves identically on all
|
|
516
|
+
three dialects and every value stays a bound parameter
|
|
517
|
+
- It cannot be combined with `after` or `distinct`: a row's position in the request's own
|
|
518
|
+
filter list is not a value of the row, so it cannot be encoded into a cursor or rebuilt
|
|
519
|
+
against a subquery. Selecting `cursor` under it yields `null` rather than an error
|
|
520
|
+
|
|
521
|
+
## Filtering inside JSON documents
|
|
522
|
+
|
|
523
|
+
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
524
|
+
document, alongside the whole-document `eq` / `ne` / `contains` operators:
|
|
525
|
+
|
|
526
|
+
```graphql
|
|
527
|
+
{
|
|
528
|
+
users(where: { meta: { path: { path: ["profile", "level"], gt: 2 } } }) {
|
|
529
|
+
id
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`path` walks the document key by key from the root; an all-digits key indexes an array
|
|
535
|
+
(`["tags", "0"]`). Several predicates may be given at once and are `AND`ed:
|
|
536
|
+
|
|
537
|
+
```graphql
|
|
538
|
+
{
|
|
539
|
+
users(
|
|
540
|
+
where: {
|
|
541
|
+
meta: {
|
|
542
|
+
path: [
|
|
543
|
+
{ path: ["profile", "role"], eq: "admin" }
|
|
544
|
+
{ path: ["profile", "level"], gte: 3 }
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
) {
|
|
549
|
+
id
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Available operators are `eq` / `ne` / `lt` / `lte` / `gt` / `gte`, the safe string matchers
|
|
555
|
+
`startsWith` / `endsWith` / `contains` (plus their case-insensitive `i`-prefixed forms), and
|
|
556
|
+
`isNull` / `isNotNull`. A missing key and a JSON `null` both read as null.
|
|
557
|
+
|
|
558
|
+
**How the value is compared** is decided by the operand: a GraphQL number compares
|
|
559
|
+
numerically, a boolean as a boolean, and anything else as text. Pass `as: TEXT | NUMBER |
|
|
560
|
+
BOOLEAN` when the operand's type doesn't match the document's — comparing a numeric field
|
|
561
|
+
against a `String` variable, say:
|
|
562
|
+
|
|
563
|
+
```graphql
|
|
564
|
+
{
|
|
565
|
+
users(where: { meta: { path: { path: ["profile", "level"], as: NUMBER, eq: "3" } } }) {
|
|
566
|
+
id
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
A value of the wrong shape never matches rather than failing the query: `gt: 0` against a
|
|
572
|
+
document whose path holds `"admin"` returns no rows on every dialect. That takes a guard on
|
|
573
|
+
each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts a non-numeric
|
|
574
|
+
string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
|
|
575
|
+
number.
|
|
576
|
+
|
|
577
|
+
Note that `contains` means different things at the two levels, because a path names a scalar:
|
|
578
|
+
on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
|
|
579
|
+
inside a `path` filter it is substring matching on the extracted value.
|
|
580
|
+
|
|
581
|
+
Path filters combine with `AND` / `OR` / `NOT` like any other operator, and are accepted
|
|
582
|
+
everywhere a filter is — list, single and aggregate queries, `update` / `delete`, and the
|
|
583
|
+
`where` argument on a relation field.
|
|
584
|
+
|
|
405
585
|
## Aggregate queries
|
|
406
586
|
|
|
407
587
|
Every table also gets an aggregate query field — `<tableName>Aggregate` (e.g. `usersAggregate`),
|
|
@@ -642,8 +822,8 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
642
822
|
- Dates and bigints round-trip losslessly through the cursor
|
|
643
823
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
644
824
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
645
|
-
- `after` cannot be combined with `distinct`, and a table with
|
|
646
|
-
cursor pagination (its `cursor` field resolves to `null`)
|
|
825
|
+
- `after` cannot be combined with `distinct` or with `matchFilterOrder`, and a table with
|
|
826
|
+
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
647
827
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
648
828
|
skipped
|
|
649
829
|
|
package/dist/README.md
CHANGED
|
@@ -85,6 +85,39 @@ Automatically create GraphQL schema or customizable schema config fields from Dr
|
|
|
85
85
|
})
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Naming the generated types and fields
|
|
89
|
+
|
|
90
|
+
By default every name comes from the table's key in the Drizzle schema: `tasks` gives type
|
|
91
|
+
`Tasks`, queries `tasks` / `tasksSingle`, mutations `createTasks` / `createTasksSingle`.
|
|
92
|
+
Most schemas use plural table keys and want singular type names, so that pairing is shipped
|
|
93
|
+
as a preset:
|
|
94
|
+
|
|
95
|
+
```Typescript
|
|
96
|
+
const { schema } = buildSchema(db, { typeNameMapper: 'singularize' })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`tasks` then gives type `Task`, queries `tasks` (list) and `task` (single), mutations
|
|
100
|
+
`createTasks` (array) and `createTask` (single), `updateTask`, `deleteTask`. Irregular
|
|
101
|
+
plurals go through `pluralize`, so `people` gives `Person` / `people` / `person` and
|
|
102
|
+
`addresses` gives `Address` rather than the `addresss` a naive suffix would produce. A table
|
|
103
|
+
key that is already singular is pluralized for the list side, and a capitalized key is
|
|
104
|
+
uncapitalized for the field side, so `tasks`, `task` and `Tasks` all land on the same names.
|
|
105
|
+
|
|
106
|
+
For anything else, pass your own function — it is asked per table and may return `undefined`
|
|
107
|
+
to leave that table with the default naming. The preset is exported as `singularizeMapper`
|
|
108
|
+
so it can be wrapped:
|
|
109
|
+
|
|
110
|
+
```Typescript
|
|
111
|
+
import { buildSchema, singularizeMapper } from 'drizzle-graphql'
|
|
112
|
+
|
|
113
|
+
const { schema } = buildSchema(db, {
|
|
114
|
+
typeNameMapper: (table) => (table === 'auditLog' ? undefined : singularizeMapper(table)),
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
With a mapper in play the singular/plural forms are what keep the list and single fields
|
|
119
|
+
apart, so `suffixes` may be empty on both sides. Without one, they must still differ.
|
|
120
|
+
|
|
88
121
|
## Choosing what gets generated
|
|
89
122
|
|
|
90
123
|
Every generated operation is on by default. `features` turns individual ones off, which
|
|
@@ -124,6 +157,35 @@ const { schema } = buildSchema(db, {
|
|
|
124
157
|
`mutations: false`
|
|
125
158
|
- List and single queries are always generated, so `Query` is never empty
|
|
126
159
|
|
|
160
|
+
### Per-table operations
|
|
161
|
+
|
|
162
|
+
The choice is rarely build-wide: a schema usually has a handful of tables that must not be
|
|
163
|
+
deleted through the API, a handful whose writes belong to a hand-written resolver that
|
|
164
|
+
enforces something, and a majority that want everything. Any flag can be a predicate
|
|
165
|
+
instead of a boolean, asked once per table with the table's key in the Drizzle schema:
|
|
166
|
+
|
|
167
|
+
```Typescript
|
|
168
|
+
const { schema } = buildSchema(db, {
|
|
169
|
+
features: {
|
|
170
|
+
delete: (table) => table !== 'auditLog',
|
|
171
|
+
update: (table) => !ownedByCustomResolver.has(table),
|
|
172
|
+
upsert: (table) => table === 'settings',
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- The table is still fully readable — this decides which operations it offers, not whether
|
|
178
|
+
it is in the schema at all. For that, use `exclude` below
|
|
179
|
+
- `nestedWrites` is the one flag that stays build-wide: its plans are computed once over
|
|
180
|
+
the whole relation graph, and a nested write reaches a second table by definition
|
|
181
|
+
- `Mutation` is omitted when *no* table generates a mutation, and kept when one still does
|
|
182
|
+
- Some operations are built out of others, and the generator knows which. A table that
|
|
183
|
+
asks for `upsert` while its `insert` or `update` is off — an upsert is a second write
|
|
184
|
+
path past the operation you turned off — warns on the console at build time, naming the
|
|
185
|
+
tables, rather than leaving that to be discovered later. So does asking for `updateMany`
|
|
186
|
+
without `update`, `groupBy` without `aggregates`, `countMutations` with neither plural
|
|
187
|
+
write, or `nestedWrites` alongside a table whose own writes are off
|
|
188
|
+
|
|
127
189
|
## Excluding tables and columns
|
|
128
190
|
|
|
129
191
|
`features` decides which *kinds* of operation exist. `exclude` decides which tables and
|
|
@@ -402,6 +464,124 @@ A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
|
402
464
|
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
403
465
|
to-many relation
|
|
404
466
|
|
|
467
|
+
## Matching lists of values
|
|
468
|
+
|
|
469
|
+
`inArray` / `notInArray` take a list of candidate values and compile to SQL `IN` / `NOT IN`.
|
|
470
|
+
Two things about them are worth stating explicitly.
|
|
471
|
+
|
|
472
|
+
**An empty list is answered, not rejected.** A caller building a filter from a variable often
|
|
473
|
+
ends up with an empty list, and that list has a well-defined meaning: nothing is `IN ()` and
|
|
474
|
+
everything is `NOT IN ()`.
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
{
|
|
478
|
+
# no rows
|
|
479
|
+
posts(where: { id: { inArray: [] } }) {
|
|
480
|
+
id
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# every row
|
|
484
|
+
posts(where: { id: { notInArray: [] } }) {
|
|
485
|
+
id
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
The same rule applies to the Postgres array-column operators: `hasSome: []` matches nothing
|
|
491
|
+
(overlapping with no elements is never true) and `hasEvery: []` matches everything (every
|
|
492
|
+
array contains all zero of them).
|
|
493
|
+
|
|
494
|
+
**The list's own order can be the sort order.** SQL returns `IN` matches in whatever order it
|
|
495
|
+
likes, so a caller that fetches rows by a ranked list of ids normally has to re-sort them
|
|
496
|
+
client-side. Setting `matchFilterOrder` on an `orderBy` entry sorts by the row's position in
|
|
497
|
+
that column's `inArray` list instead of by the column's value:
|
|
498
|
+
|
|
499
|
+
```graphql
|
|
500
|
+
{
|
|
501
|
+
posts(
|
|
502
|
+
where: { id: { inArray: [7, 3, 12] } }
|
|
503
|
+
orderBy: { id: { direction: asc, priority: 1, matchFilterOrder: true } }
|
|
504
|
+
) {
|
|
505
|
+
id # → 7, 3, 12
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- `direction: asc` keeps the list's order; `desc` reverses it
|
|
511
|
+
- It needs an `inArray` filter on the same column, at the top level of `where` — an
|
|
512
|
+
`inArray` buried inside an `OR` branch raises an error rather than being guessed at
|
|
513
|
+
- It interleaves with the other `orderBy` entries by `priority` like any ordering key, and
|
|
514
|
+
works on relation fields as well as root lists
|
|
515
|
+
- It compiles to a `CASE` ladder over the list values, so it behaves identically on all
|
|
516
|
+
three dialects and every value stays a bound parameter
|
|
517
|
+
- It cannot be combined with `after` or `distinct`: a row's position in the request's own
|
|
518
|
+
filter list is not a value of the row, so it cannot be encoded into a cursor or rebuilt
|
|
519
|
+
against a subquery. Selecting `cursor` under it yields `null` rather than an error
|
|
520
|
+
|
|
521
|
+
## Filtering inside JSON documents
|
|
522
|
+
|
|
523
|
+
`json` / `jsonb` columns get a `path` filter that compares the value at one path inside the
|
|
524
|
+
document, alongside the whole-document `eq` / `ne` / `contains` operators:
|
|
525
|
+
|
|
526
|
+
```graphql
|
|
527
|
+
{
|
|
528
|
+
users(where: { meta: { path: { path: ["profile", "level"], gt: 2 } } }) {
|
|
529
|
+
id
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`path` walks the document key by key from the root; an all-digits key indexes an array
|
|
535
|
+
(`["tags", "0"]`). Several predicates may be given at once and are `AND`ed:
|
|
536
|
+
|
|
537
|
+
```graphql
|
|
538
|
+
{
|
|
539
|
+
users(
|
|
540
|
+
where: {
|
|
541
|
+
meta: {
|
|
542
|
+
path: [
|
|
543
|
+
{ path: ["profile", "role"], eq: "admin" }
|
|
544
|
+
{ path: ["profile", "level"], gte: 3 }
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
) {
|
|
549
|
+
id
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Available operators are `eq` / `ne` / `lt` / `lte` / `gt` / `gte`, the safe string matchers
|
|
555
|
+
`startsWith` / `endsWith` / `contains` (plus their case-insensitive `i`-prefixed forms), and
|
|
556
|
+
`isNull` / `isNotNull`. A missing key and a JSON `null` both read as null.
|
|
557
|
+
|
|
558
|
+
**How the value is compared** is decided by the operand: a GraphQL number compares
|
|
559
|
+
numerically, a boolean as a boolean, and anything else as text. Pass `as: TEXT | NUMBER |
|
|
560
|
+
BOOLEAN` when the operand's type doesn't match the document's — comparing a numeric field
|
|
561
|
+
against a `String` variable, say:
|
|
562
|
+
|
|
563
|
+
```graphql
|
|
564
|
+
{
|
|
565
|
+
users(where: { meta: { path: { path: ["profile", "level"], as: NUMBER, eq: "3" } } }) {
|
|
566
|
+
id
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
A value of the wrong shape never matches rather than failing the query: `gt: 0` against a
|
|
572
|
+
document whose path holds `"admin"` returns no rows on every dialect. That takes a guard on
|
|
573
|
+
each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts a non-numeric
|
|
574
|
+
string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
|
|
575
|
+
number.
|
|
576
|
+
|
|
577
|
+
Note that `contains` means different things at the two levels, because a path names a scalar:
|
|
578
|
+
on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
|
|
579
|
+
inside a `path` filter it is substring matching on the extracted value.
|
|
580
|
+
|
|
581
|
+
Path filters combine with `AND` / `OR` / `NOT` like any other operator, and are accepted
|
|
582
|
+
everywhere a filter is — list, single and aggregate queries, `update` / `delete`, and the
|
|
583
|
+
`where` argument on a relation field.
|
|
584
|
+
|
|
405
585
|
## Aggregate queries
|
|
406
586
|
|
|
407
587
|
Every table also gets an aggregate query field — `<tableName>Aggregate` (e.g. `usersAggregate`),
|
|
@@ -642,8 +822,8 @@ field, and passing it back as `after` resumes strictly after that row.
|
|
|
642
822
|
- Dates and bigints round-trip losslessly through the cursor
|
|
643
823
|
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
644
824
|
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
645
|
-
- `after` cannot be combined with `distinct`, and a table with
|
|
646
|
-
cursor pagination (its `cursor` field resolves to `null`)
|
|
825
|
+
- `after` cannot be combined with `distinct` or with `matchFilterOrder`, and a table with
|
|
826
|
+
no primary key cannot use cursor pagination (its `cursor` field resolves to `null`)
|
|
647
827
|
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
648
828
|
skipped
|
|
649
829
|
|