@vantreeseba/drizzle-graphql 4.0.0 → 4.2.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 +115 -1
- package/dist/README.md +115 -1
- package/dist/index.cjs +320 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.js +340 -29
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ reach:
|
|
|
96
96
|
const { schema } = buildSchema(db, {
|
|
97
97
|
features: {
|
|
98
98
|
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
+
groupBy: false, // <plural>GroupBy root queries
|
|
99
100
|
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
100
101
|
distinct: false, // the `distinct` argument on list queries
|
|
101
102
|
insert: false, // create<Table> / create<Table>Single mutations
|
|
@@ -110,6 +111,8 @@ const { schema } = buildSchema(db, {
|
|
|
110
111
|
changes nothing else
|
|
111
112
|
- `upsert` is the exception: it defaults to `false`, so the upsert mutations and their
|
|
112
113
|
conflict input only exist if you ask for them
|
|
114
|
+
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
115
|
+
turns the group-by queries off with it
|
|
113
116
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
114
117
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
115
118
|
- Turning off all three mutation features omits the `Mutation` type entirely, the same as
|
|
@@ -253,7 +256,72 @@ The optional `where` argument takes the same filter input as the table's list qu
|
|
|
253
256
|
applied to every aggregate in the selection. All requested aggregates are computed in a
|
|
254
257
|
single `SELECT`, and on an empty result set `count` is `0` while the other values are `null`.
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
Add `groupBy` to get the same numbers one row per group — see [Group by](#group-by).
|
|
260
|
+
|
|
261
|
+
## Group by
|
|
262
|
+
|
|
263
|
+
Every table with aggregates also gets a `<tableName>GroupBy` query (e.g. `postsGroupBy`): the
|
|
264
|
+
same aggregates as `<tableName>Aggregate`, computed once per distinct combination of the
|
|
265
|
+
columns you group by.
|
|
266
|
+
|
|
267
|
+
```graphql
|
|
268
|
+
{
|
|
269
|
+
postsGroupBy(
|
|
270
|
+
groupBy: [authorId, published]
|
|
271
|
+
where: { createdAt: { gte: "2024-01-01T00:00:00Z" } }
|
|
272
|
+
having: { count: { gte: 5 } }
|
|
273
|
+
) {
|
|
274
|
+
group {
|
|
275
|
+
authorId
|
|
276
|
+
published
|
|
277
|
+
}
|
|
278
|
+
count
|
|
279
|
+
avg {
|
|
280
|
+
views
|
|
281
|
+
}
|
|
282
|
+
max {
|
|
283
|
+
createdAt
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
290
|
+
enum. It holds the orderable columns plus booleans — anything the database can group on.
|
|
291
|
+
An empty list is an error, and repeated columns are ignored
|
|
292
|
+
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
293
|
+
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
294
|
+
a column whose grouped value really is `NULL` is indistinguishable from
|
|
295
|
+
- Every other field is the same one `<tableName>Aggregate` returns — `count`, `avg`, `sum`,
|
|
296
|
+
`min`, `max`, `countNonNull`, `countDistinct` — with the identical types and rules
|
|
297
|
+
- `where` filters rows before grouping; `having` filters groups after aggregating
|
|
298
|
+
- The result is `[<Type>GroupBy!]!`, one row per group, in whatever order the database
|
|
299
|
+
returns them. Add your own ordering client-side if you need it
|
|
300
|
+
|
|
301
|
+
`having` mirrors the aggregate selection, with an `AggregateNumberFilter` (`eq`, `ne`, `gt`,
|
|
302
|
+
`gte`, `lt`, `lte`, all `Float`) in place of each value:
|
|
303
|
+
|
|
304
|
+
```graphql
|
|
305
|
+
{
|
|
306
|
+
postsGroupBy(groupBy: [authorId], having: { count: { gt: 3 }, avg: { views: { gte: 100 } } }) {
|
|
307
|
+
group {
|
|
308
|
+
authorId
|
|
309
|
+
}
|
|
310
|
+
count
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` /
|
|
316
|
+
`countNonNull` / `countDistinct` take one filter per column, over the same columns the
|
|
317
|
+
matching aggregate type exposes
|
|
318
|
+
- Every entry in a `having` is ANDed together
|
|
319
|
+
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
320
|
+
whether or not you asked for `count`
|
|
321
|
+
|
|
322
|
+
The whole thing is one `SELECT … GROUP BY … HAVING …`. Set `features: { groupBy: false }` to
|
|
323
|
+
leave these queries (and their `<Type>GroupBy`, `<Type>GroupKeys`, `<Type>GroupByColumn` and
|
|
324
|
+
`<Type>Having` types) out of the schema; turning off `aggregates` removes them too.
|
|
257
325
|
|
|
258
326
|
## Relation aggregates
|
|
259
327
|
|
|
@@ -397,6 +465,52 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
|
|
|
397
465
|
`create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
|
|
398
466
|
the per-request replacement.
|
|
399
467
|
|
|
468
|
+
## Query cost
|
|
469
|
+
|
|
470
|
+
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
471
|
+
[`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity)'s
|
|
472
|
+
`fieldExtensionsEstimator`. The generator knows which fields are paginated and which are
|
|
473
|
+
aggregates, so the cost tracks the rows a query can actually pull rather than the number of
|
|
474
|
+
fields it mentions:
|
|
475
|
+
|
|
476
|
+
| Field | Cost |
|
|
477
|
+
| --- | --- |
|
|
478
|
+
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
479
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
480
|
+
| Everything else | no hint — your estimator's default applies |
|
|
481
|
+
|
|
482
|
+
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
483
|
+
same query without limits costs `10 * (1 + 10 * 1)` = 110.
|
|
484
|
+
|
|
485
|
+
The hints do nothing until you install a complexity rule, so they are generated by default:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { createComplexityRule, fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity';
|
|
489
|
+
|
|
490
|
+
const rule = createComplexityRule({
|
|
491
|
+
maximumComplexity: 1000,
|
|
492
|
+
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Tune the two assumptions, or turn the hints off entirely:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
buildSchema(db, {
|
|
500
|
+
complexity: { defaultListSize: 25, aggregateCost: 50 },
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
buildSchema(db, { complexity: false });
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Depth
|
|
507
|
+
|
|
508
|
+
Cost is not a depth bound. A cyclic relation graph (`user -> posts -> author -> posts -> …`)
|
|
509
|
+
lets a client nest as deep as it likes, and a deep query over cheap fields can stay under any
|
|
510
|
+
complexity ceiling. Put a depth limit in front of a publicly exposed schema as well — e.g.
|
|
511
|
+
[`graphql-depth-limit`](https://github.com/stems/graphql-depth-limit) — or set
|
|
512
|
+
`relationsDepthLimit: 0` to generate no relation fields at all.
|
|
513
|
+
|
|
400
514
|
## Error handling
|
|
401
515
|
|
|
402
516
|
Database drivers put a lot into an error message. Drizzle rethrows them with the full SQL
|
package/dist/README.md
CHANGED
|
@@ -96,6 +96,7 @@ reach:
|
|
|
96
96
|
const { schema } = buildSchema(db, {
|
|
97
97
|
features: {
|
|
98
98
|
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
+
groupBy: false, // <plural>GroupBy root queries
|
|
99
100
|
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
100
101
|
distinct: false, // the `distinct` argument on list queries
|
|
101
102
|
insert: false, // create<Table> / create<Table>Single mutations
|
|
@@ -110,6 +111,8 @@ const { schema } = buildSchema(db, {
|
|
|
110
111
|
changes nothing else
|
|
111
112
|
- `upsert` is the exception: it defaults to `false`, so the upsert mutations and their
|
|
112
113
|
conflict input only exist if you ask for them
|
|
114
|
+
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
115
|
+
turns the group-by queries off with it
|
|
113
116
|
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
114
117
|
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
115
118
|
- Turning off all three mutation features omits the `Mutation` type entirely, the same as
|
|
@@ -253,7 +256,72 @@ The optional `where` argument takes the same filter input as the table's list qu
|
|
|
253
256
|
applied to every aggregate in the selection. All requested aggregates are computed in a
|
|
254
257
|
single `SELECT`, and on an empty result set `count` is `0` while the other values are `null`.
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
Add `groupBy` to get the same numbers one row per group — see [Group by](#group-by).
|
|
260
|
+
|
|
261
|
+
## Group by
|
|
262
|
+
|
|
263
|
+
Every table with aggregates also gets a `<tableName>GroupBy` query (e.g. `postsGroupBy`): the
|
|
264
|
+
same aggregates as `<tableName>Aggregate`, computed once per distinct combination of the
|
|
265
|
+
columns you group by.
|
|
266
|
+
|
|
267
|
+
```graphql
|
|
268
|
+
{
|
|
269
|
+
postsGroupBy(
|
|
270
|
+
groupBy: [authorId, published]
|
|
271
|
+
where: { createdAt: { gte: "2024-01-01T00:00:00Z" } }
|
|
272
|
+
having: { count: { gte: 5 } }
|
|
273
|
+
) {
|
|
274
|
+
group {
|
|
275
|
+
authorId
|
|
276
|
+
published
|
|
277
|
+
}
|
|
278
|
+
count
|
|
279
|
+
avg {
|
|
280
|
+
views
|
|
281
|
+
}
|
|
282
|
+
max {
|
|
283
|
+
createdAt
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
290
|
+
enum. It holds the orderable columns plus booleans — anything the database can group on.
|
|
291
|
+
An empty list is an error, and repeated columns are ignored
|
|
292
|
+
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
293
|
+
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
294
|
+
a column whose grouped value really is `NULL` is indistinguishable from
|
|
295
|
+
- Every other field is the same one `<tableName>Aggregate` returns — `count`, `avg`, `sum`,
|
|
296
|
+
`min`, `max`, `countNonNull`, `countDistinct` — with the identical types and rules
|
|
297
|
+
- `where` filters rows before grouping; `having` filters groups after aggregating
|
|
298
|
+
- The result is `[<Type>GroupBy!]!`, one row per group, in whatever order the database
|
|
299
|
+
returns them. Add your own ordering client-side if you need it
|
|
300
|
+
|
|
301
|
+
`having` mirrors the aggregate selection, with an `AggregateNumberFilter` (`eq`, `ne`, `gt`,
|
|
302
|
+
`gte`, `lt`, `lte`, all `Float`) in place of each value:
|
|
303
|
+
|
|
304
|
+
```graphql
|
|
305
|
+
{
|
|
306
|
+
postsGroupBy(groupBy: [authorId], having: { count: { gt: 3 }, avg: { views: { gte: 100 } } }) {
|
|
307
|
+
group {
|
|
308
|
+
authorId
|
|
309
|
+
}
|
|
310
|
+
count
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` /
|
|
316
|
+
`countNonNull` / `countDistinct` take one filter per column, over the same columns the
|
|
317
|
+
matching aggregate type exposes
|
|
318
|
+
- Every entry in a `having` is ANDed together
|
|
319
|
+
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
320
|
+
whether or not you asked for `count`
|
|
321
|
+
|
|
322
|
+
The whole thing is one `SELECT … GROUP BY … HAVING …`. Set `features: { groupBy: false }` to
|
|
323
|
+
leave these queries (and their `<Type>GroupBy`, `<Type>GroupKeys`, `<Type>GroupByColumn` and
|
|
324
|
+
`<Type>Having` types) out of the schema; turning off `aggregates` removes them too.
|
|
257
325
|
|
|
258
326
|
## Relation aggregates
|
|
259
327
|
|
|
@@ -397,6 +465,52 @@ The build-wide `conflictDoNothing` option is deprecated in favour of this: it ap
|
|
|
397
465
|
`create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
|
|
398
466
|
the per-request replacement.
|
|
399
467
|
|
|
468
|
+
## Query cost
|
|
469
|
+
|
|
470
|
+
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
471
|
+
[`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity)'s
|
|
472
|
+
`fieldExtensionsEstimator`. The generator knows which fields are paginated and which are
|
|
473
|
+
aggregates, so the cost tracks the rows a query can actually pull rather than the number of
|
|
474
|
+
fields it mentions:
|
|
475
|
+
|
|
476
|
+
| Field | Cost |
|
|
477
|
+
| --- | --- |
|
|
478
|
+
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
479
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
480
|
+
| Everything else | no hint — your estimator's default applies |
|
|
481
|
+
|
|
482
|
+
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
483
|
+
same query without limits costs `10 * (1 + 10 * 1)` = 110.
|
|
484
|
+
|
|
485
|
+
The hints do nothing until you install a complexity rule, so they are generated by default:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { createComplexityRule, fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity';
|
|
489
|
+
|
|
490
|
+
const rule = createComplexityRule({
|
|
491
|
+
maximumComplexity: 1000,
|
|
492
|
+
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Tune the two assumptions, or turn the hints off entirely:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
buildSchema(db, {
|
|
500
|
+
complexity: { defaultListSize: 25, aggregateCost: 50 },
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
buildSchema(db, { complexity: false });
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Depth
|
|
507
|
+
|
|
508
|
+
Cost is not a depth bound. A cyclic relation graph (`user -> posts -> author -> posts -> …`)
|
|
509
|
+
lets a client nest as deep as it likes, and a deep query over cheap fields can stay under any
|
|
510
|
+
complexity ceiling. Put a depth limit in front of a publicly exposed schema as well — e.g.
|
|
511
|
+
[`graphql-depth-limit`](https://github.com/stems/graphql-depth-limit) — or set
|
|
512
|
+
`relationsDepthLimit: 0` to generate no relation fields at all.
|
|
513
|
+
|
|
400
514
|
## Error handling
|
|
401
515
|
|
|
402
516
|
Database drivers put a lot into an error message. Drizzle rethrows them with the full SQL
|