@vantreeseba/drizzle-graphql 4.1.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 +70 -2
- package/dist/README.md +70 -2
- package/dist/index.cjs +264 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +284 -14
- 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
|
|
|
@@ -408,7 +476,7 @@ fields it mentions:
|
|
|
408
476
|
| Field | Cost |
|
|
409
477
|
| --- | --- |
|
|
410
478
|
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
411
|
-
| Aggregate query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
479
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
412
480
|
| Everything else | no hint — your estimator's default applies |
|
|
413
481
|
|
|
414
482
|
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
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
|
|
|
@@ -408,7 +476,7 @@ fields it mentions:
|
|
|
408
476
|
| Field | Cost |
|
|
409
477
|
| --- | --- |
|
|
410
478
|
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
411
|
-
| Aggregate query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
479
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
412
480
|
| Everything else | no hint — your estimator's default applies |
|
|
413
481
|
|
|
414
482
|
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
package/dist/index.cjs
CHANGED
|
@@ -1720,6 +1720,7 @@ var computeResolverFieldNames = (tableName, typeNameMapper, prefixes, suffixes)
|
|
|
1720
1720
|
const listFieldName = (mapped?.plural ?? uncapitalize(tableName)) + suffixes.list;
|
|
1721
1721
|
const singleFieldName = mapped?.singular ?? uncapitalize(tableName) + suffixes.single;
|
|
1722
1722
|
const aggregateFieldName = `${mapped?.plural ?? uncapitalize(tableName)}Aggregate`;
|
|
1723
|
+
const groupByFieldName = `${mapped?.plural ?? uncapitalize(tableName)}GroupBy`;
|
|
1723
1724
|
const createArrayFieldName = `${prefixes.insert}${mapped ? capitalize(mapped.plural) : capitalize(tableName)}`;
|
|
1724
1725
|
const createSingleFieldName = mapped ? `${prefixes.insert}${capitalize(mapped.singular)}` : `${prefixes.insert}${capitalize(tableName)}${suffixes.single}`;
|
|
1725
1726
|
const upsertPrefix = prefixes.upsert ?? "upsert";
|
|
@@ -1732,6 +1733,7 @@ var computeResolverFieldNames = (tableName, typeNameMapper, prefixes, suffixes)
|
|
|
1732
1733
|
listFieldName,
|
|
1733
1734
|
singleFieldName,
|
|
1734
1735
|
aggregateFieldName,
|
|
1736
|
+
groupByFieldName,
|
|
1735
1737
|
createArrayFieldName,
|
|
1736
1738
|
createSingleFieldName,
|
|
1737
1739
|
upsertArrayFieldName,
|
|
@@ -2006,9 +2008,9 @@ var aggregateTarget = (table, tableName, typeName) => {
|
|
|
2006
2008
|
)
|
|
2007
2009
|
};
|
|
2008
2010
|
};
|
|
2009
|
-
var parseAggregateRequest = (info, target) => {
|
|
2011
|
+
var parseAggregateRequest = (info, target, rootTypeName = `${target.typeName}Aggregate`) => {
|
|
2010
2012
|
const parsedInfo = (0, import_graphql_parse_resolve_info.parseResolveInfo)(info, { deep: true });
|
|
2011
|
-
const selectionTree = parsedInfo.fieldsByTypeName[
|
|
2013
|
+
const selectionTree = parsedInfo.fieldsByTypeName[rootTypeName] ?? {};
|
|
2012
2014
|
const request = {
|
|
2013
2015
|
count: false,
|
|
2014
2016
|
ops: { avg: [], sum: [], min: [], max: [], countNonNull: [], countDistinct: [] },
|
|
@@ -2146,6 +2148,184 @@ var createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, filt
|
|
|
2146
2148
|
return { type, resolve };
|
|
2147
2149
|
};
|
|
2148
2150
|
};
|
|
2151
|
+
var GROUP_COL = `group${SEP}`;
|
|
2152
|
+
var HAVING_OPS = { eq: import_drizzle_orm4.eq, ne: import_drizzle_orm4.ne, gt: import_drizzle_orm4.gt, gte: import_drizzle_orm4.gte, lt: import_drizzle_orm4.lt, lte: import_drizzle_orm4.lte };
|
|
2153
|
+
var groupableColumns = (table, tableName) => {
|
|
2154
|
+
const { orderable } = classifyAggregateColumns(table, tableName);
|
|
2155
|
+
const groupable = { ...orderable };
|
|
2156
|
+
for (const [columnName, column] of Object.entries((0, import_drizzle_orm4.getColumns)(table))) {
|
|
2157
|
+
if (groupable[columnName]) {
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
const { type: dataType } = (0, import_drizzle_orm4.extractExtendedColumnType)(column);
|
|
2161
|
+
if (dataType !== "boolean") {
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
groupable[columnName] = {
|
|
2165
|
+
column,
|
|
2166
|
+
converted: drizzleColumnToGraphQLType(column, columnName, tableName, true, false, false)
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
return groupable;
|
|
2170
|
+
};
|
|
2171
|
+
var generateGroupByEnum = (table, tableName, typeName) => {
|
|
2172
|
+
const groupable = groupableColumns(table, tableName);
|
|
2173
|
+
return generateColumnEnum(
|
|
2174
|
+
table,
|
|
2175
|
+
`${typeName}GroupByColumn`,
|
|
2176
|
+
`Columns of ${typeName} that a query can group by`,
|
|
2177
|
+
(_column, columnName) => Boolean(groupable[columnName])
|
|
2178
|
+
);
|
|
2179
|
+
};
|
|
2180
|
+
var aggregateNumberFilter = new import_graphql5.GraphQLInputObjectType({
|
|
2181
|
+
name: "AggregateNumberFilter",
|
|
2182
|
+
description: "Compares an aggregated value. Several operators in one filter are ANDed together.",
|
|
2183
|
+
fields: Object.fromEntries(Object.keys(HAVING_OPS).map((op) => [op, { type: import_graphql5.GraphQLFloat }]))
|
|
2184
|
+
});
|
|
2185
|
+
var generateHavingInput = (table, tableName, typeName) => {
|
|
2186
|
+
const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
|
|
2187
|
+
const fields = {
|
|
2188
|
+
count: { type: aggregateNumberFilter, description: "Filters groups by how many rows they contain" }
|
|
2189
|
+
};
|
|
2190
|
+
const opColumns = {
|
|
2191
|
+
avg: numeric,
|
|
2192
|
+
sum: numeric,
|
|
2193
|
+
min: numeric,
|
|
2194
|
+
max: numeric,
|
|
2195
|
+
countNonNull: all,
|
|
2196
|
+
countDistinct: orderable
|
|
2197
|
+
};
|
|
2198
|
+
for (const [op, columns] of Object.entries(opColumns)) {
|
|
2199
|
+
const columnNames = Object.keys(columns);
|
|
2200
|
+
if (!columnNames.length) {
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
fields[op] = {
|
|
2204
|
+
type: new import_graphql5.GraphQLInputObjectType({
|
|
2205
|
+
name: `${typeName}${capitalize(op)}Having`,
|
|
2206
|
+
fields: Object.fromEntries(columnNames.map((columnName) => [columnName, { type: aggregateNumberFilter }]))
|
|
2207
|
+
})
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
return new import_graphql5.GraphQLInputObjectType({
|
|
2211
|
+
name: `${typeName}Having`,
|
|
2212
|
+
description: `Filters ${typeName} groups by their aggregated values`,
|
|
2213
|
+
fields
|
|
2214
|
+
});
|
|
2215
|
+
};
|
|
2216
|
+
var generateGroupByType = (table, tableName, typeName, cacheCtx) => {
|
|
2217
|
+
const groupable = groupableColumns(table, tableName);
|
|
2218
|
+
if (!Object.keys(groupable).length) {
|
|
2219
|
+
return void 0;
|
|
2220
|
+
}
|
|
2221
|
+
const keysType = new import_graphql5.GraphQLObjectType({
|
|
2222
|
+
name: `${typeName}GroupKeys`,
|
|
2223
|
+
description: `The grouped column values of one ${typeName} group. A column the query did not group by is null.`,
|
|
2224
|
+
fields: Object.fromEntries(
|
|
2225
|
+
Object.entries(groupable).map(([columnName, { converted }]) => [
|
|
2226
|
+
columnName,
|
|
2227
|
+
{ type: converted.type, description: converted.description }
|
|
2228
|
+
])
|
|
2229
|
+
)
|
|
2230
|
+
});
|
|
2231
|
+
const aggregateType = generateAggregateTypes(table, tableName, typeName, cacheCtx);
|
|
2232
|
+
const aggregateFields = Object.fromEntries(
|
|
2233
|
+
Object.values(aggregateType.getFields()).map((field) => [
|
|
2234
|
+
field.name,
|
|
2235
|
+
{ type: field.type, description: field.description ?? void 0 }
|
|
2236
|
+
])
|
|
2237
|
+
);
|
|
2238
|
+
return new import_graphql5.GraphQLObjectType({
|
|
2239
|
+
name: `${typeName}GroupBy`,
|
|
2240
|
+
fields: { group: { type: new import_graphql5.GraphQLNonNull(keysType) }, ...aggregateFields }
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
2243
|
+
var havingComparisons = (expression, filter) => Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](expression, value));
|
|
2244
|
+
var buildHavingCondition = (having, columns) => {
|
|
2245
|
+
if (!having) {
|
|
2246
|
+
return void 0;
|
|
2247
|
+
}
|
|
2248
|
+
const conditions = [];
|
|
2249
|
+
for (const [key, value] of Object.entries(having)) {
|
|
2250
|
+
if (value == null) {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
if (key === "count") {
|
|
2254
|
+
conditions.push(...havingComparisons((0, import_drizzle_orm4.count)(), value));
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
if (!AGGREGATE_OPS.includes(key)) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
for (const [columnName, filter] of Object.entries(value)) {
|
|
2261
|
+
const column = columns[columnName];
|
|
2262
|
+
if (!column || filter == null) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
conditions.push(...havingComparisons(OP_FNS[key](column), filter));
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return conditions.length ? (0, import_drizzle_orm4.and)(...conditions) : void 0;
|
|
2269
|
+
};
|
|
2270
|
+
var generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs, groupByEnum, havingInput, filterCtx) => {
|
|
2271
|
+
const target = aggregateTarget(table, tableName, typeName);
|
|
2272
|
+
const groupable = groupableColumns(table, tableName);
|
|
2273
|
+
const columns = (0, import_drizzle_orm4.getColumns)(table);
|
|
2274
|
+
const rootTypeName = `${typeName}GroupBy`;
|
|
2275
|
+
const queryArgs = {
|
|
2276
|
+
groupBy: {
|
|
2277
|
+
type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(groupByEnum))),
|
|
2278
|
+
description: "Columns to group by. One result row per distinct combination of their values."
|
|
2279
|
+
},
|
|
2280
|
+
where: { type: filterArgs, description: "Filters the rows before they are grouped." },
|
|
2281
|
+
having: { type: havingInput, description: "Filters the groups after they are aggregated." }
|
|
2282
|
+
};
|
|
2283
|
+
return {
|
|
2284
|
+
name: fieldName,
|
|
2285
|
+
resolver: async (_source, args, context, info) => {
|
|
2286
|
+
try {
|
|
2287
|
+
const requestedKeys = [...new Set(args.groupBy ?? [])];
|
|
2288
|
+
if (!requestedKeys.length) {
|
|
2289
|
+
throw new import_graphql5.GraphQLError("At least one column to group by is required!");
|
|
2290
|
+
}
|
|
2291
|
+
const keyColumns = requestedKeys.map((columnName) => {
|
|
2292
|
+
const groupableColumn = groupable[columnName];
|
|
2293
|
+
if (!groupableColumn) {
|
|
2294
|
+
throw new import_graphql5.GraphQLError(`Cannot group ${typeName} by ${columnName}!`);
|
|
2295
|
+
}
|
|
2296
|
+
return [columnName, groupableColumn.column];
|
|
2297
|
+
});
|
|
2298
|
+
const request = parseAggregateRequest(info, target, rootTypeName);
|
|
2299
|
+
const selection = {
|
|
2300
|
+
...Object.fromEntries(keyColumns.map(([columnName, column]) => [`${GROUP_COL}${columnName}`, column])),
|
|
2301
|
+
...request.selection
|
|
2302
|
+
};
|
|
2303
|
+
let query = resolveExecutor(db, context).select(selection).from(table);
|
|
2304
|
+
if (args.where) {
|
|
2305
|
+
query = query.where(extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName)));
|
|
2306
|
+
}
|
|
2307
|
+
query = query.groupBy(...keyColumns.map(([, column]) => column));
|
|
2308
|
+
const havingCondition = buildHavingCondition(args.having, columns);
|
|
2309
|
+
if (havingCondition) {
|
|
2310
|
+
query = query.having(havingCondition);
|
|
2311
|
+
}
|
|
2312
|
+
const rows = await query;
|
|
2313
|
+
return rows.map((row) => {
|
|
2314
|
+
const group = {};
|
|
2315
|
+
for (const [columnName, column] of keyColumns) {
|
|
2316
|
+
const value = row[`${GROUP_COL}${columnName}`];
|
|
2317
|
+
const decoded = typeof value === "string" && target.dateTimeColumns.has(columnName) ? parseDriverDateTime(value) : value;
|
|
2318
|
+
group[columnName] = value == null ? null : remapToGraphQLCore(columnName, decoded, tableName, column);
|
|
2319
|
+
}
|
|
2320
|
+
return { group, ...assembleAggregateRow(row, request, target) };
|
|
2321
|
+
});
|
|
2322
|
+
} catch (e) {
|
|
2323
|
+
throw toGraphQLError(e);
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
args: queryArgs
|
|
2327
|
+
};
|
|
2328
|
+
};
|
|
2149
2329
|
|
|
2150
2330
|
// src/util/builders/mysql.ts
|
|
2151
2331
|
var generateSelectArray = (db, tableName, tables, relationMap, orderArgs, filterArgs, fieldName, typeName, typeNameMapper, filterCtx, distinctEnabled = true) => {
|
|
@@ -2443,6 +2623,7 @@ var generateSchemaData = (db, schema, relations, options) => {
|
|
|
2443
2623
|
listFieldName,
|
|
2444
2624
|
singleFieldName,
|
|
2445
2625
|
aggregateFieldName,
|
|
2626
|
+
groupByFieldName,
|
|
2446
2627
|
createArrayFieldName,
|
|
2447
2628
|
createSingleFieldName,
|
|
2448
2629
|
upsertArrayFieldName,
|
|
@@ -2506,6 +2687,20 @@ var generateSchemaData = (db, schema, relations, options) => {
|
|
|
2506
2687
|
tableFilters,
|
|
2507
2688
|
filterCtx
|
|
2508
2689
|
) : void 0;
|
|
2690
|
+
const groupByType = features.aggregates && features.groupBy ? generateGroupByType(schema[tableName], tableName, typeName, cacheCtx) : void 0;
|
|
2691
|
+
const groupByEnum = groupByType ? generateGroupByEnum(schema[tableName], tableName, typeName) : void 0;
|
|
2692
|
+
const havingInput = groupByEnum ? generateHavingInput(schema[tableName], tableName, typeName) : void 0;
|
|
2693
|
+
const groupByGenerated = groupByType && groupByEnum && havingInput ? generateGroupBy(
|
|
2694
|
+
db,
|
|
2695
|
+
tableName,
|
|
2696
|
+
schema[tableName],
|
|
2697
|
+
typeName,
|
|
2698
|
+
groupByFieldName,
|
|
2699
|
+
tableFilters,
|
|
2700
|
+
groupByEnum,
|
|
2701
|
+
havingInput,
|
|
2702
|
+
filterCtx
|
|
2703
|
+
) : void 0;
|
|
2509
2704
|
queries[selectArrGenerated.name] = {
|
|
2510
2705
|
type: selectArrOutput,
|
|
2511
2706
|
args: selectArrGenerated.args,
|
|
@@ -2525,6 +2720,14 @@ var generateSchemaData = (db, schema, relations, options) => {
|
|
|
2525
2720
|
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
2526
2721
|
};
|
|
2527
2722
|
}
|
|
2723
|
+
if (groupByGenerated && groupByType) {
|
|
2724
|
+
queries[groupByGenerated.name] = {
|
|
2725
|
+
type: new import_graphql6.GraphQLNonNull(new import_graphql6.GraphQLList(new import_graphql6.GraphQLNonNull(groupByType))),
|
|
2726
|
+
args: groupByGenerated.args,
|
|
2727
|
+
resolve: groupByGenerated.resolver,
|
|
2728
|
+
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2528
2731
|
for (const generated of [
|
|
2529
2732
|
insertArrGenerated,
|
|
2530
2733
|
insertSingleGenerated,
|
|
@@ -2556,6 +2759,10 @@ var generateSchemaData = (db, schema, relations, options) => {
|
|
|
2556
2759
|
if (aggregateType) {
|
|
2557
2760
|
outputs[aggregateType.name] = aggregateType;
|
|
2558
2761
|
}
|
|
2762
|
+
if (groupByType && havingInput) {
|
|
2763
|
+
outputs[groupByType.name] = groupByType;
|
|
2764
|
+
inputs[havingInput.name] = havingInput;
|
|
2765
|
+
}
|
|
2559
2766
|
}
|
|
2560
2767
|
const fieldResolvers = {};
|
|
2561
2768
|
for (const [tableName, tableRelations] of Object.entries(namedRelations)) {
|
|
@@ -3015,6 +3222,7 @@ function generateSchemaData2(db, schema, relations, options) {
|
|
|
3015
3222
|
listFieldName,
|
|
3016
3223
|
singleFieldName,
|
|
3017
3224
|
aggregateFieldName,
|
|
3225
|
+
groupByFieldName,
|
|
3018
3226
|
createArrayFieldName,
|
|
3019
3227
|
createSingleFieldName,
|
|
3020
3228
|
upsertArrayFieldName,
|
|
@@ -3142,6 +3350,20 @@ function generateSchemaData2(db, schema, relations, options) {
|
|
|
3142
3350
|
tableFilters,
|
|
3143
3351
|
filterCtx
|
|
3144
3352
|
) : void 0;
|
|
3353
|
+
const groupByType = features.aggregates && features.groupBy ? generateGroupByType(schema[tableName], tableName, typeName, cacheCtx) : void 0;
|
|
3354
|
+
const groupByEnum = groupByType ? generateGroupByEnum(schema[tableName], tableName, typeName) : void 0;
|
|
3355
|
+
const havingInput = groupByEnum ? generateHavingInput(schema[tableName], tableName, typeName) : void 0;
|
|
3356
|
+
const groupByGenerated = groupByType && groupByEnum && havingInput ? generateGroupBy(
|
|
3357
|
+
db,
|
|
3358
|
+
tableName,
|
|
3359
|
+
schema[tableName],
|
|
3360
|
+
typeName,
|
|
3361
|
+
groupByFieldName,
|
|
3362
|
+
tableFilters,
|
|
3363
|
+
groupByEnum,
|
|
3364
|
+
havingInput,
|
|
3365
|
+
filterCtx
|
|
3366
|
+
) : void 0;
|
|
3145
3367
|
queries[selectArrGenerated.name] = {
|
|
3146
3368
|
type: selectArrOutput,
|
|
3147
3369
|
args: selectArrGenerated.args,
|
|
@@ -3161,6 +3383,14 @@ function generateSchemaData2(db, schema, relations, options) {
|
|
|
3161
3383
|
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
3162
3384
|
};
|
|
3163
3385
|
}
|
|
3386
|
+
if (groupByGenerated && groupByType) {
|
|
3387
|
+
queries[groupByGenerated.name] = {
|
|
3388
|
+
type: new import_graphql7.GraphQLNonNull(new import_graphql7.GraphQLList(new import_graphql7.GraphQLNonNull(groupByType))),
|
|
3389
|
+
args: groupByGenerated.args,
|
|
3390
|
+
resolve: groupByGenerated.resolver,
|
|
3391
|
+
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3164
3394
|
if (insertArrGenerated) {
|
|
3165
3395
|
mutations[insertArrGenerated.name] = {
|
|
3166
3396
|
type: arrTableItemOutput,
|
|
@@ -3219,6 +3449,10 @@ function generateSchemaData2(db, schema, relations, options) {
|
|
|
3219
3449
|
if (aggregateType) {
|
|
3220
3450
|
outputs[aggregateType.name] = aggregateType;
|
|
3221
3451
|
}
|
|
3452
|
+
if (groupByType && havingInput) {
|
|
3453
|
+
outputs[groupByType.name] = groupByType;
|
|
3454
|
+
inputs[havingInput.name] = havingInput;
|
|
3455
|
+
}
|
|
3222
3456
|
}
|
|
3223
3457
|
const fieldResolvers = {};
|
|
3224
3458
|
for (const [tableName, tableRelations] of Object.entries(namedRelations)) {
|
|
@@ -3617,6 +3851,7 @@ var generateSchemaData3 = (db, schema, relations, options) => {
|
|
|
3617
3851
|
listFieldName,
|
|
3618
3852
|
singleFieldName,
|
|
3619
3853
|
aggregateFieldName,
|
|
3854
|
+
groupByFieldName,
|
|
3620
3855
|
createArrayFieldName,
|
|
3621
3856
|
createSingleFieldName,
|
|
3622
3857
|
upsertArrayFieldName,
|
|
@@ -3744,6 +3979,20 @@ var generateSchemaData3 = (db, schema, relations, options) => {
|
|
|
3744
3979
|
tableFilters,
|
|
3745
3980
|
filterCtx
|
|
3746
3981
|
) : void 0;
|
|
3982
|
+
const groupByType = features.aggregates && features.groupBy ? generateGroupByType(schema[tableName], tableName, typeName, cacheCtx) : void 0;
|
|
3983
|
+
const groupByEnum = groupByType ? generateGroupByEnum(schema[tableName], tableName, typeName) : void 0;
|
|
3984
|
+
const havingInput = groupByEnum ? generateHavingInput(schema[tableName], tableName, typeName) : void 0;
|
|
3985
|
+
const groupByGenerated = groupByType && groupByEnum && havingInput ? generateGroupBy(
|
|
3986
|
+
db,
|
|
3987
|
+
tableName,
|
|
3988
|
+
schema[tableName],
|
|
3989
|
+
typeName,
|
|
3990
|
+
groupByFieldName,
|
|
3991
|
+
tableFilters,
|
|
3992
|
+
groupByEnum,
|
|
3993
|
+
havingInput,
|
|
3994
|
+
filterCtx
|
|
3995
|
+
) : void 0;
|
|
3747
3996
|
queries[selectArrGenerated.name] = {
|
|
3748
3997
|
type: selectArrOutput,
|
|
3749
3998
|
args: selectArrGenerated.args,
|
|
@@ -3763,6 +4012,14 @@ var generateSchemaData3 = (db, schema, relations, options) => {
|
|
|
3763
4012
|
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
3764
4013
|
};
|
|
3765
4014
|
}
|
|
4015
|
+
if (groupByGenerated && groupByType) {
|
|
4016
|
+
queries[groupByGenerated.name] = {
|
|
4017
|
+
type: new import_graphql8.GraphQLNonNull(new import_graphql8.GraphQLList(new import_graphql8.GraphQLNonNull(groupByType))),
|
|
4018
|
+
args: groupByGenerated.args,
|
|
4019
|
+
resolve: groupByGenerated.resolver,
|
|
4020
|
+
...complexity ? { extensions: { complexity: aggregateFieldComplexity(complexity) } } : {}
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
3766
4023
|
if (insertArrGenerated) {
|
|
3767
4024
|
mutations[insertArrGenerated.name] = {
|
|
3768
4025
|
type: arrTableItemOutput,
|
|
@@ -3821,6 +4078,10 @@ var generateSchemaData3 = (db, schema, relations, options) => {
|
|
|
3821
4078
|
if (aggregateType) {
|
|
3822
4079
|
outputs[aggregateType.name] = aggregateType;
|
|
3823
4080
|
}
|
|
4081
|
+
if (groupByType && havingInput) {
|
|
4082
|
+
outputs[groupByType.name] = groupByType;
|
|
4083
|
+
inputs[havingInput.name] = havingInput;
|
|
4084
|
+
}
|
|
3824
4085
|
}
|
|
3825
4086
|
const fieldResolvers = {};
|
|
3826
4087
|
for (const [tableName, tableRelations] of Object.entries(namedRelations)) {
|
|
@@ -3863,6 +4124,7 @@ var buildSchema = (db, config) => {
|
|
|
3863
4124
|
const typeNameMapper = config?.typeNameMapper;
|
|
3864
4125
|
const features = {
|
|
3865
4126
|
aggregates: config?.features?.aggregates ?? true,
|
|
4127
|
+
groupBy: config?.features?.groupBy ?? true,
|
|
3866
4128
|
relationAggregates: config?.features?.relationAggregates ?? true,
|
|
3867
4129
|
distinct: config?.features?.distinct ?? true,
|
|
3868
4130
|
insert: config?.features?.insert ?? true,
|