@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 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
- Grouping (`groupBy`) is not supported yet.
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
- Grouping (`groupBy`) is not supported yet.
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