turbine-orm 0.34.0 → 0.36.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 +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/client.js +26 -4
- package/dist/cjs/dialect.js +2 -1
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mssql.js +27 -5
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/powdb.js +197 -25
- package/dist/cjs/powql.js +515 -51
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +361 -4508
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +4 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +28 -6
- package/dist/dialect.js +2 -1
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mssql.js +27 -5
- package/dist/mysql.js +4 -0
- package/dist/powdb.d.ts +135 -9
- package/dist/powdb.js +197 -25
- package/dist/powql.d.ts +166 -4
- package/dist/powql.js +516 -52
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +98 -830
- package/dist/query/builder.js +366 -4513
- package/dist/query/deferred.d.ts +13 -2
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +25 -6
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +4 -1
- package/package.json +4 -4
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm: aggregate / groupBy compilation (extracted from builder.ts)
|
|
4
|
+
*
|
|
5
|
+
* buildAggregate + buildGroupBy and their helpers (HAVING clauses, groupBy
|
|
6
|
+
* ordering, DISTINCT-ON sources, JSON-path aggregate targets). All functions
|
|
7
|
+
* take a {@link BuilderCtx} first argument; WHERE compilation is reused from
|
|
8
|
+
* where.ts (via `whereMod`), and the shared orderBy / row-parse primitives
|
|
9
|
+
* stay class-resident, reached through the ctx. See builder.ts for the thin
|
|
10
|
+
* delegating methods (buildGroupBy / buildAggregate).
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.buildGroupBy = buildGroupBy;
|
|
47
|
+
exports.buildGroupByOrderBy = buildGroupByOrderBy;
|
|
48
|
+
exports.resolveJsonPathTarget = resolveJsonPathTarget;
|
|
49
|
+
exports.buildDistinctOnSource = buildDistinctOnSource;
|
|
50
|
+
exports.buildHavingClauses = buildHavingClauses;
|
|
51
|
+
exports.buildHavingNumericClauses = buildHavingNumericClauses;
|
|
52
|
+
exports.buildAggregate = buildAggregate;
|
|
53
|
+
const errors_js_1 = require("../errors.js");
|
|
54
|
+
const schema_js_1 = require("../schema.js");
|
|
55
|
+
const filters_js_1 = require("./filters.js");
|
|
56
|
+
const whereMod = __importStar(require("./where.js"));
|
|
57
|
+
function buildGroupBy(qi, args) {
|
|
58
|
+
const meta = qi.schema.tables[qi.table];
|
|
59
|
+
if (meta) {
|
|
60
|
+
for (const key of args.by) {
|
|
61
|
+
if (typeof key === 'string' && !(key in meta.columnMap)) {
|
|
62
|
+
throw new errors_js_1.ValidationError(`Unknown column "${key}" in groupBy for table "${qi.table}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
qi.currentSkip = args.skipGlobalFilters;
|
|
67
|
+
const gbWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
68
|
+
const { sql: whereSql, params } = gbWhere
|
|
69
|
+
? whereMod.buildWhere(qi, gbWhere)
|
|
70
|
+
: { sql: '', params: [] };
|
|
71
|
+
// Row source. Plain: `"table"<WHERE>`. With `distinctOn` (PostgreSQL
|
|
72
|
+
// only), the groupBy runs over one representative row per column
|
|
73
|
+
// combination: the wrapper carries args.where INSIDE it (filter before
|
|
74
|
+
// picking) and is aliased as the table name so every outer expression is
|
|
75
|
+
// byte-identical either way.
|
|
76
|
+
const fromSql = args.distinctOn
|
|
77
|
+
? buildDistinctOnSource(qi, args.distinctOn, whereSql, params)
|
|
78
|
+
: `${qi.q(qi.table)}${whereSql}`;
|
|
79
|
+
// Group keys: plain columns and/or JSON-path keys. Output-name collisions
|
|
80
|
+
// are rejected up front — and the check runs over the EMITTED SQL output
|
|
81
|
+
// column names (snake_case column / JSON alias / `_agg_key` aggregate
|
|
82
|
+
// alias), not just the given arg keys: the driver keeps only the LAST
|
|
83
|
+
// duplicate field per row object, so a JSON alias equal to another key's
|
|
84
|
+
// snake_case column (or an aggregate output alias) would silently clobber
|
|
85
|
+
// that value in the results.
|
|
86
|
+
const groupExprs = [];
|
|
87
|
+
const selectExprs = [];
|
|
88
|
+
/** by entries in order: how to read each group key off the result row. */
|
|
89
|
+
const byReaders = [];
|
|
90
|
+
// ORDER BY registries: map each key the groupBy RESULT actually contains to
|
|
91
|
+
// the exact SELECT expression that produced it, so `orderBy` re-emits that
|
|
92
|
+
// expression (never a SELECT alias, since not every dialect accepts alias
|
|
93
|
+
// references in ORDER BY, and re-emitting mirrors HAVING's `jsonAggExprs`).
|
|
94
|
+
// `byOrderExprs`: plain by-field name / JSON group-key alias → column or
|
|
95
|
+
// extract expression. `aggOrderExprs`: `${aggKey}:${field}` → aggregate
|
|
96
|
+
// expression (including any already-bound JSON-path placeholder, reused
|
|
97
|
+
// exactly like HAVING since ORDER BY is appended after all other params).
|
|
98
|
+
const byOrderExprs = new Map();
|
|
99
|
+
const usedResultKeys = new Set();
|
|
100
|
+
const claimResultKey = (key, what) => {
|
|
101
|
+
if (key === '_count' || usedResultKeys.has(key)) {
|
|
102
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy output name "${key}" (${what}) collides with another output column on table ` +
|
|
103
|
+
`"${qi.table}": set an explicit \`alias\` (or rename the aggregate key) to disambiguate.`);
|
|
104
|
+
}
|
|
105
|
+
usedResultKeys.add(key);
|
|
106
|
+
};
|
|
107
|
+
for (const entry of args.by) {
|
|
108
|
+
if (typeof entry === 'string') {
|
|
109
|
+
const col = qi.toColumn(entry);
|
|
110
|
+
claimResultKey(entry, `column "${col}"`);
|
|
111
|
+
// The emitted output column is the snake_case name; claim it too (when
|
|
112
|
+
// it differs from the result key) so a JSON alias like 'created_at'
|
|
113
|
+
// cannot silently shadow the 'createdAt' group key on the wire.
|
|
114
|
+
if (col !== entry)
|
|
115
|
+
claimResultKey(col, `column "${col}"`);
|
|
116
|
+
groupExprs.push(qi.q(col));
|
|
117
|
+
selectExprs.push(qi.q(col));
|
|
118
|
+
byReaders.push({ resultKey: entry, rowKey: col, raw: false });
|
|
119
|
+
byOrderExprs.set(entry, qi.q(col));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const col = resolveJsonPathTarget(qi, 'group key', entry.field, entry.path);
|
|
123
|
+
params.push(whereMod.jsonPathParam(qi, entry.path));
|
|
124
|
+
const extract = qi.dialect.buildJsonPathExtract(qi.q(col), qi.p(params.length));
|
|
125
|
+
const alias = entry.alias ?? String(entry.path[entry.path.length - 1]);
|
|
126
|
+
claimResultKey(alias, `JSON path on "${entry.field}"`);
|
|
127
|
+
// Same expression (and the same $n placeholder) in SELECT and GROUP BY.
|
|
128
|
+
selectExprs.push(`(${extract}) AS ${qi.q(alias)}`);
|
|
129
|
+
groupExprs.push(extract);
|
|
130
|
+
byReaders.push({ resultKey: alias, rowKey: alias, raw: true });
|
|
131
|
+
// ORDER BY by this JSON alias re-emits the extract expression (with its
|
|
132
|
+
// already-bound $n): the same reuse HAVING does for JSON aggregates.
|
|
133
|
+
byOrderExprs.set(alias, extract);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// _count
|
|
137
|
+
const countSelected = args._count === true || args._count === undefined;
|
|
138
|
+
if (countSelected) {
|
|
139
|
+
// default: always include count
|
|
140
|
+
selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
141
|
+
}
|
|
142
|
+
// ORDER BY aggregate expressions, keyed `${aggKey}:${field}` (plus a bare
|
|
143
|
+
// `_count`). Populated alongside the SELECT list below so `orderBy` can only
|
|
144
|
+
// reference an aggregate that is actually requested. `COUNT(*)` (uncast) is
|
|
145
|
+
// the ordering expression (the SELECT cast is only for the returned value).
|
|
146
|
+
const aggOrderExprs = new Map();
|
|
147
|
+
if (countSelected)
|
|
148
|
+
aggOrderExprs.set('_count', 'COUNT(*)');
|
|
149
|
+
// _sum / _avg / _min / _max: `true` keeps the plain-column behavior; a
|
|
150
|
+
// {@link JsonPathAggregateTarget} aggregates a JSON path under the arg key
|
|
151
|
+
// as alias. `jsonAggFields` routes each JSON-aggregate row key back to its
|
|
152
|
+
// alias (and coercion kind) in the transform; `jsonAggExprs` lets HAVING
|
|
153
|
+
// reuse the exact aggregate expression (same placeholders) by alias.
|
|
154
|
+
const jsonAggFields = new Map();
|
|
155
|
+
const jsonAggExprs = new Map();
|
|
156
|
+
const buildAggregates = (aggKey, sqlFn, spec) => {
|
|
157
|
+
if (!spec)
|
|
158
|
+
return;
|
|
159
|
+
for (const [key, target] of Object.entries(spec)) {
|
|
160
|
+
if (!target)
|
|
161
|
+
continue;
|
|
162
|
+
if (target === true) {
|
|
163
|
+
const col = qi.toColumn(key);
|
|
164
|
+
// Aggregate output aliases share the same output-name namespace as
|
|
165
|
+
// the group keys: `_sum: { totalPrice: true, total_price: {json} }`
|
|
166
|
+
// would emit two "_sum_total_price" columns and silently drop one.
|
|
167
|
+
claimResultKey(`${aggKey}_${col}`, `${aggKey} of column "${col}"`);
|
|
168
|
+
const inner = `${sqlFn}(${qi.q(col)})`;
|
|
169
|
+
const expr = aggKey === '_avg' ? qi.castAgg(inner, 'float') : inner;
|
|
170
|
+
selectExprs.push(`${expr} AS ${qi.q(`${aggKey}_${col}`)}`);
|
|
171
|
+
aggOrderExprs.set(`${aggKey}:${key}`, expr);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const col = resolveJsonPathTarget(qi, `${aggKey} target "${key}"`, target.field, target.path);
|
|
175
|
+
const alwaysNumeric = aggKey === '_sum' || aggKey === '_avg';
|
|
176
|
+
if (alwaysNumeric && target.type === 'text') {
|
|
177
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy ${aggKey} target "${key}" on table "${qi.table}": ` +
|
|
178
|
+
`${aggKey} over a JSON path is always numeric: remove \`type: 'text'\`.`);
|
|
179
|
+
}
|
|
180
|
+
const numeric = alwaysNumeric || target.type === 'numeric';
|
|
181
|
+
claimResultKey(`${aggKey}_${key}`, `${aggKey} JSON target "${key}"`);
|
|
182
|
+
params.push(whereMod.jsonPathParam(qi, target.path));
|
|
183
|
+
const extract = qi.dialect.buildJsonPathExtract(qi.q(col), qi.p(params.length));
|
|
184
|
+
const inner = `${sqlFn}(${numeric ? whereMod.castJsonNumeric(qi, extract) : extract})`;
|
|
185
|
+
const expr = aggKey === '_avg' ? qi.castAgg(inner, 'float') : inner;
|
|
186
|
+
selectExprs.push(`${expr} AS ${qi.q(`${aggKey}_${key}`)}`);
|
|
187
|
+
jsonAggFields.set(`${aggKey}_${key}`, { field: key, numeric });
|
|
188
|
+
jsonAggExprs.set(`${key}:${aggKey}`, expr);
|
|
189
|
+
aggOrderExprs.set(`${aggKey}:${key}`, expr);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
buildAggregates('_sum', 'SUM', args._sum);
|
|
193
|
+
buildAggregates('_avg', 'AVG', args._avg);
|
|
194
|
+
buildAggregates('_min', 'MIN', args._min);
|
|
195
|
+
buildAggregates('_max', 'MAX', args._max);
|
|
196
|
+
let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
|
|
197
|
+
// HAVING — filter whole groups by their aggregate values.
|
|
198
|
+
// Appends to the same `params` array, so placeholders continue from the
|
|
199
|
+
// WHERE clause's parameter positions (qi.p(params.length) below).
|
|
200
|
+
if (args.having) {
|
|
201
|
+
const havingClauses = buildHavingClauses(qi, args.having, params, jsonAggExprs);
|
|
202
|
+
if (havingClauses.length > 0) {
|
|
203
|
+
sql += ` HAVING ${havingClauses.join(' AND ')}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ORDER BY, over the groupBy RESULT columns (by-fields, JSON aliases, and
|
|
207
|
+
// requested aggregates), not the table's physical columns.
|
|
208
|
+
if (args.orderBy) {
|
|
209
|
+
const orderSql = buildGroupByOrderBy(qi, args.orderBy, byOrderExprs, aggOrderExprs);
|
|
210
|
+
if (orderSql)
|
|
211
|
+
sql += ` ORDER BY ${orderSql}`;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
sql,
|
|
215
|
+
params,
|
|
216
|
+
transform: (result) => result.rows.map((row) => {
|
|
217
|
+
const parsed = qi.parseRow(row, qi.table);
|
|
218
|
+
// Restructure aggregate results into nested objects (Prisma-style)
|
|
219
|
+
const restructured = {};
|
|
220
|
+
// Copy group-by fields. JSON-path keys read their alias off the raw
|
|
221
|
+
// row (the alias is not a table column, so parseRow's snake→camel
|
|
222
|
+
// mapping must not touch it).
|
|
223
|
+
for (const reader of byReaders) {
|
|
224
|
+
restructured[reader.resultKey] = reader.raw ? row[reader.rowKey] : parsed[reader.resultKey];
|
|
225
|
+
}
|
|
226
|
+
// _count
|
|
227
|
+
if ('_count' in row) {
|
|
228
|
+
restructured._count = row._count;
|
|
229
|
+
}
|
|
230
|
+
else if ('count' in row) {
|
|
231
|
+
restructured._count = row.count;
|
|
232
|
+
}
|
|
233
|
+
// Collect aggregates into nested objects
|
|
234
|
+
const sumObj = {};
|
|
235
|
+
const avgObj = {};
|
|
236
|
+
const minObj = {};
|
|
237
|
+
const maxObj = {};
|
|
238
|
+
let hasSums = false, hasAvgs = false, hasMins = false, hasMaxs = false;
|
|
239
|
+
// JSON-path aggregates keep their arg key verbatim; plain-column
|
|
240
|
+
// aggregates keep the snake→camel field mapping.
|
|
241
|
+
const jsonAgg = (rawKey) => jsonAggFields.get(rawKey);
|
|
242
|
+
const fieldFor = (rawKey, col) => jsonAgg(rawKey)?.field ?? qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
243
|
+
for (const [rawKey, rawValue] of Object.entries(row)) {
|
|
244
|
+
if (rawKey.startsWith('_sum_')) {
|
|
245
|
+
sumObj[fieldFor(rawKey, rawKey.slice(5))] = rawValue !== null ? Number(rawValue) : null;
|
|
246
|
+
hasSums = true;
|
|
247
|
+
}
|
|
248
|
+
else if (rawKey.startsWith('_avg_')) {
|
|
249
|
+
avgObj[fieldFor(rawKey, rawKey.slice(5))] = rawValue !== null ? Number(rawValue) : null;
|
|
250
|
+
hasAvgs = true;
|
|
251
|
+
}
|
|
252
|
+
else if (rawKey.startsWith('_min_')) {
|
|
253
|
+
const j = jsonAgg(rawKey);
|
|
254
|
+
minObj[fieldFor(rawKey, rawKey.slice(5))] = j?.numeric && rawValue !== null ? Number(rawValue) : rawValue;
|
|
255
|
+
hasMins = true;
|
|
256
|
+
}
|
|
257
|
+
else if (rawKey.startsWith('_max_')) {
|
|
258
|
+
const j = jsonAgg(rawKey);
|
|
259
|
+
maxObj[fieldFor(rawKey, rawKey.slice(5))] = j?.numeric && rawValue !== null ? Number(rawValue) : rawValue;
|
|
260
|
+
hasMaxs = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (hasSums)
|
|
264
|
+
restructured._sum = sumObj;
|
|
265
|
+
if (hasAvgs)
|
|
266
|
+
restructured._avg = avgObj;
|
|
267
|
+
if (hasMins)
|
|
268
|
+
restructured._min = minObj;
|
|
269
|
+
if (hasMaxs)
|
|
270
|
+
restructured._max = maxObj;
|
|
271
|
+
return restructured;
|
|
272
|
+
}),
|
|
273
|
+
tag: `${qi.table}.groupBy`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Compile a groupBy `orderBy` into an ORDER BY body. Unlike findMany ORDER BY
|
|
278
|
+
* ({@link buildOrderBy}, which validates keys against the table's physical
|
|
279
|
+
* columns), groupBy ordering targets the columns the RESULT actually
|
|
280
|
+
* contains: plain by-fields, JSON group-key aliases, and requested aggregates
|
|
281
|
+
* (`_count` / `_sum` / `_avg` / `_min` / `_max`). Each key re-emits the exact
|
|
282
|
+
* SELECT expression that produced it (`byOrderExprs` / `aggOrderExprs`),
|
|
283
|
+
* mirroring how HAVING re-emits aggregate expressions, so no dialect ever has
|
|
284
|
+
* to accept a SELECT-alias reference in ORDER BY, and any already-bound
|
|
285
|
+
* JSON-path placeholder is reused verbatim (ORDER BY is the last clause, so
|
|
286
|
+
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
287
|
+
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
288
|
+
*/
|
|
289
|
+
function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
290
|
+
const aggBlocks = new Set(['_count', '_sum', '_avg', '_min', '_max']);
|
|
291
|
+
/** Human-readable list of every key this call can order by (for E003). */
|
|
292
|
+
const validKeys = () => {
|
|
293
|
+
const keys = [...byOrderExprs.keys()];
|
|
294
|
+
for (const k of aggOrderExprs.keys()) {
|
|
295
|
+
keys.push(k.includes(':') ? k.replace(':', '.') : k);
|
|
296
|
+
}
|
|
297
|
+
return keys.join(', ') || '(none)';
|
|
298
|
+
};
|
|
299
|
+
const parts = [];
|
|
300
|
+
for (const [key, value] of Object.entries(orderBy)) {
|
|
301
|
+
if (value === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
// Aggregate ordering blocks.
|
|
304
|
+
if (aggBlocks.has(key)) {
|
|
305
|
+
if (key === '_count') {
|
|
306
|
+
const expr = aggOrderExprs.get('_count');
|
|
307
|
+
if (!expr) {
|
|
308
|
+
throw new errors_js_1.ValidationError(`[turbine] Cannot order groupBy by "_count" on table "${qi.table}": _count is not selected. ` +
|
|
309
|
+
`Orderable keys: ${validKeys()}.`);
|
|
310
|
+
}
|
|
311
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
|
|
312
|
+
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// `_sum` / `_avg` / `_min` / `_max`: an object of field → direction/spec.
|
|
316
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
317
|
+
throw new errors_js_1.ValidationError(`[turbine] Invalid groupBy orderBy for "${key}" on table "${qi.table}": ` +
|
|
318
|
+
`expected a field map like { ${key}: { amount: 'desc' } }.`);
|
|
319
|
+
}
|
|
320
|
+
for (const [field, dirSpec] of Object.entries(value)) {
|
|
321
|
+
if (dirSpec === undefined)
|
|
322
|
+
continue;
|
|
323
|
+
const expr = aggOrderExprs.get(`${key}:${field}`);
|
|
324
|
+
if (!expr) {
|
|
325
|
+
throw new errors_js_1.ValidationError(`[turbine] Cannot order groupBy by "${key}.${field}" on table "${qi.table}": ` +
|
|
326
|
+
`that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
|
|
327
|
+
}
|
|
328
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirSpec);
|
|
329
|
+
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Plain by-field name or JSON group-key alias.
|
|
334
|
+
const expr = byOrderExprs.get(key);
|
|
335
|
+
if (!expr) {
|
|
336
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in groupBy orderBy on table "${qi.table}". ` +
|
|
337
|
+
`Orderable keys: ${validKeys()}.`);
|
|
338
|
+
}
|
|
339
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
|
|
340
|
+
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
341
|
+
}
|
|
342
|
+
return parts.join(', ');
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Validate a JSON-path target (group key or aggregate target) in groupBy:
|
|
346
|
+
* the field must resolve to a real json/jsonb column and the path must be a
|
|
347
|
+
* non-empty array of keys/indexes. Returns the resolved snake_case column.
|
|
348
|
+
*/
|
|
349
|
+
function resolveJsonPathTarget(qi, context, field, path) {
|
|
350
|
+
if (typeof field !== 'string') {
|
|
351
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy ${context} on table "${qi.table}" requires a string \`field\`.`);
|
|
352
|
+
}
|
|
353
|
+
const col = qi.toColumn(field);
|
|
354
|
+
if (!Array.isArray(path) ||
|
|
355
|
+
path.length === 0 ||
|
|
356
|
+
path.some((el) => typeof el !== 'string' && !(typeof el === 'number' && Number.isFinite(el)))) {
|
|
357
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy ${context} on "${field}" (table "${qi.table}") requires a non-empty \`path\` ` +
|
|
358
|
+
`array of keys/indexes (e.g. { field: '${field}', path: ['category'] }).`);
|
|
359
|
+
}
|
|
360
|
+
const colType = whereMod.pgTypeForColumn(qi, qi.tableMeta, col);
|
|
361
|
+
if (!whereMod.isJsonColumnType(qi, colType)) {
|
|
362
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy ${context} on "${field}": column "${col}" on table "${qi.table}" is not a JSON ` +
|
|
363
|
+
`column (actual type: ${colType}).`);
|
|
364
|
+
}
|
|
365
|
+
return col;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Build the `distinctOn` row source for groupBy (PostgreSQL only: other
|
|
369
|
+
* engines throw {@link UnsupportedFeatureError} E017):
|
|
370
|
+
*
|
|
371
|
+
* ```sql
|
|
372
|
+
* (SELECT DISTINCT ON ("c1") * FROM "table"<WHERE> ORDER BY "c1", <orderBy>) AS "table"
|
|
373
|
+
* ```
|
|
374
|
+
*
|
|
375
|
+
* The wrapper is aliased as the table name so every outer expression (group
|
|
376
|
+
* keys, aggregates, HAVING, ORDER BY) is byte-identical to the plain path.
|
|
377
|
+
* `distinctOn.orderBy` is required (it decides which row survives) and
|
|
378
|
+
* supports plain columns, {@link OrderBySpec} nulls, and JSON-path specs;
|
|
379
|
+
* JSON paths push their text[] param here, after the WHERE params.
|
|
380
|
+
*/
|
|
381
|
+
function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
|
|
382
|
+
if (qi.dialect.name !== 'postgresql') {
|
|
383
|
+
throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON row source (groupBy distinctOn)', qi.dialect.name, 'groupBy({ distinctOn }) requires PostgreSQL: SELECT DISTINCT ON is not portable.');
|
|
384
|
+
}
|
|
385
|
+
if (!Array.isArray(distinctOn.columns) || distinctOn.columns.length === 0) {
|
|
386
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy distinctOn on table "${qi.table}" requires a non-empty \`columns\` array.`);
|
|
387
|
+
}
|
|
388
|
+
const orderEntries = Object.entries(distinctOn.orderBy ?? {});
|
|
389
|
+
if (orderEntries.length === 0) {
|
|
390
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy distinctOn on table "${qi.table}" requires \`orderBy\` to pick ONE row per ` +
|
|
391
|
+
"column combination deterministically (e.g. orderBy: { createdAt: 'desc' }).");
|
|
392
|
+
}
|
|
393
|
+
const distinctCols = distinctOn.columns.map((c) => qi.q(qi.toColumn(c)));
|
|
394
|
+
// DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
|
|
395
|
+
// decides which row survives per combination.
|
|
396
|
+
const orderParts = [...distinctCols];
|
|
397
|
+
for (const [key, value] of orderEntries) {
|
|
398
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(value)) {
|
|
399
|
+
orderParts.push(qi.buildJsonPathOrderEntry(qi.table, qi.tableMeta, key, value, '', params));
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if ((0, filters_js_1.isVectorOrderBy)(value) || qi.isRelationOrderByValue(value)) {
|
|
403
|
+
throw new errors_js_1.ValidationError(`[turbine] groupBy distinctOn.orderBy on "${key}" (table "${qi.table}") supports plain columns, ` +
|
|
404
|
+
'sort specs, and JSON-path orderings only.');
|
|
405
|
+
}
|
|
406
|
+
const col = qi.resolveOrderByColumn(qi.table, qi.tableMeta, key);
|
|
407
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
|
|
408
|
+
orderParts.push(`${qi.q(col)} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
409
|
+
}
|
|
410
|
+
return (`(SELECT DISTINCT ON (${distinctCols.join(', ')}) * FROM ${qi.q(qi.table)}${whereSql} ` +
|
|
411
|
+
`ORDER BY ${orderParts.join(', ')}) AS ${qi.q(qi.table)}`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Build the SQL fragments for a {@link HavingClause}.
|
|
415
|
+
*
|
|
416
|
+
* Each aggregate expression (`COUNT(*)`, `SUM("col")`, etc.) is constructed
|
|
417
|
+
* from a **schema-validated, quoted** column identifier: `qi.toColumn()`
|
|
418
|
+
* throws {@link ValidationError} for unknown fields and `qi.q()` quotes via
|
|
419
|
+
* the dialect, so no unvalidated identifier ever reaches the SQL string. Every
|
|
420
|
+
* comparison value is pushed onto the shared `params` array and referenced by
|
|
421
|
+
* a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
|
|
422
|
+
* interpolation of user values.
|
|
423
|
+
*
|
|
424
|
+
* `jsonAggExprs` (from {@link buildGroupBy}) maps `alias:aggKey` to the
|
|
425
|
+
* exact aggregate expression a JSON-path aggregate emitted in SELECT
|
|
426
|
+
* (including its already-bound path placeholder), so HAVING on a JSON-path
|
|
427
|
+
* aggregate alias reuses the same expression instead of resolving the alias
|
|
428
|
+
* as a column.
|
|
429
|
+
*/
|
|
430
|
+
function buildHavingClauses(qi, having, params, jsonAggExprs) {
|
|
431
|
+
const clauses = [];
|
|
432
|
+
// Maps the per-field aggregate key to its SQL function name. The set of
|
|
433
|
+
// allowed keys is fixed here — any other key on a field's filter object is
|
|
434
|
+
// rejected by ValidationError below (never interpolated).
|
|
435
|
+
const aggFnByKey = {
|
|
436
|
+
_sum: 'SUM',
|
|
437
|
+
_avg: 'AVG',
|
|
438
|
+
_min: 'MIN',
|
|
439
|
+
_max: 'MAX',
|
|
440
|
+
_count: 'COUNT',
|
|
441
|
+
};
|
|
442
|
+
for (const [key, value] of Object.entries(having)) {
|
|
443
|
+
if (value === undefined)
|
|
444
|
+
continue;
|
|
445
|
+
// Top-level `_count` (no field) → COUNT(*) for the whole group.
|
|
446
|
+
if (key === '_count') {
|
|
447
|
+
clauses.push(...buildHavingNumericClauses(qi, 'COUNT(*)', value, params));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Otherwise `key` is a field name mapping to a per-aggregate filter object.
|
|
451
|
+
if (typeof value !== 'object' || value === null) {
|
|
452
|
+
throw new errors_js_1.ValidationError(`[turbine] Invalid having filter for field "${key}" on table "${qi.table}": ` +
|
|
453
|
+
`expected an aggregate object like { _sum: { gt: 100 } }.`);
|
|
454
|
+
}
|
|
455
|
+
// toColumn validates the field against schema metadata (throws
|
|
456
|
+
// ValidationError on unknown columns) and q() quotes the identifier — no
|
|
457
|
+
// unvalidated identifier ever reaches the SQL string. Resolution is lazy:
|
|
458
|
+
// a JSON-path aggregate alias is not a column, so it must not hit
|
|
459
|
+
// toColumn when every aggregate under it resolves via `jsonAggExprs`.
|
|
460
|
+
let quotedCol = null;
|
|
461
|
+
const columnExpr = () => {
|
|
462
|
+
quotedCol ??= qi.q(qi.toColumn(key));
|
|
463
|
+
return quotedCol;
|
|
464
|
+
};
|
|
465
|
+
for (const [aggKey, filter] of Object.entries(value)) {
|
|
466
|
+
if (filter === undefined)
|
|
467
|
+
continue;
|
|
468
|
+
const fn = aggFnByKey[aggKey];
|
|
469
|
+
if (!fn) {
|
|
470
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${qi.table}". ` +
|
|
471
|
+
`Supported: ${Object.keys(aggFnByKey).join(', ')}.`);
|
|
472
|
+
}
|
|
473
|
+
const expr = jsonAggExprs?.get(`${key}:${aggKey}`) ?? `${fn}(${columnExpr()})`;
|
|
474
|
+
clauses.push(...buildHavingNumericClauses(qi, expr, filter, params));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return clauses;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Convert a single having filter into one or more parameterized SQL
|
|
481
|
+
* comparisons against the given aggregate expression. A bare number is
|
|
482
|
+
* shorthand for equality. Unknown operator keys throw {@link ValidationError}.
|
|
483
|
+
*/
|
|
484
|
+
function buildHavingNumericClauses(qi, expr, filter, params) {
|
|
485
|
+
// Bare number → equality.
|
|
486
|
+
if (typeof filter === 'number') {
|
|
487
|
+
params.push(filter);
|
|
488
|
+
return [`${expr} = ${qi.p(params.length)}`];
|
|
489
|
+
}
|
|
490
|
+
if (typeof filter !== 'object' || filter === null) {
|
|
491
|
+
throw new errors_js_1.ValidationError(`[turbine] Invalid having filter on "${expr}" for table "${qi.table}": expected a number or operator object.`);
|
|
492
|
+
}
|
|
493
|
+
const op = filter;
|
|
494
|
+
const allowedKeys = new Set(['equals', 'not', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn']);
|
|
495
|
+
for (const k of Object.keys(op)) {
|
|
496
|
+
if (!allowedKeys.has(k)) {
|
|
497
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown having operator "${k}" on "${expr}" for table "${qi.table}". ` +
|
|
498
|
+
`Supported: ${[...allowedKeys].join(', ')}.`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const clauses = [];
|
|
502
|
+
if (op.equals !== undefined) {
|
|
503
|
+
params.push(op.equals);
|
|
504
|
+
clauses.push(`${expr} = ${qi.p(params.length)}`);
|
|
505
|
+
}
|
|
506
|
+
if (op.not !== undefined) {
|
|
507
|
+
params.push(op.not);
|
|
508
|
+
clauses.push(`${expr} != ${qi.p(params.length)}`);
|
|
509
|
+
}
|
|
510
|
+
if (op.gt !== undefined) {
|
|
511
|
+
params.push(op.gt);
|
|
512
|
+
clauses.push(`${expr} > ${qi.p(params.length)}`);
|
|
513
|
+
}
|
|
514
|
+
if (op.gte !== undefined) {
|
|
515
|
+
params.push(op.gte);
|
|
516
|
+
clauses.push(`${expr} >= ${qi.p(params.length)}`);
|
|
517
|
+
}
|
|
518
|
+
if (op.lt !== undefined) {
|
|
519
|
+
params.push(op.lt);
|
|
520
|
+
clauses.push(`${expr} < ${qi.p(params.length)}`);
|
|
521
|
+
}
|
|
522
|
+
if (op.lte !== undefined) {
|
|
523
|
+
params.push(op.lte);
|
|
524
|
+
clauses.push(`${expr} <= ${qi.p(params.length)}`);
|
|
525
|
+
}
|
|
526
|
+
if (op.in !== undefined) {
|
|
527
|
+
params.push(qi.inParam(op.in));
|
|
528
|
+
clauses.push(qi.inClause(expr, qi.p(params.length), false));
|
|
529
|
+
}
|
|
530
|
+
if (op.notIn !== undefined) {
|
|
531
|
+
params.push(qi.inParam(op.notIn));
|
|
532
|
+
clauses.push(qi.inClause(expr, qi.p(params.length), true));
|
|
533
|
+
}
|
|
534
|
+
return clauses;
|
|
535
|
+
}
|
|
536
|
+
function buildAggregate(qi, args) {
|
|
537
|
+
qi.currentSkip = args.skipGlobalFilters;
|
|
538
|
+
const aggWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
539
|
+
const { sql: whereSql, params } = aggWhere
|
|
540
|
+
? whereMod.buildWhere(qi, aggWhere)
|
|
541
|
+
: { sql: '', params: [] };
|
|
542
|
+
const meta = qi.schema.tables[qi.table];
|
|
543
|
+
if (meta) {
|
|
544
|
+
for (const group of [args._sum, args._avg, args._min, args._max]) {
|
|
545
|
+
if (group && typeof group === 'object') {
|
|
546
|
+
for (const key of Object.keys(group)) {
|
|
547
|
+
if (!(key in meta.columnMap)) {
|
|
548
|
+
throw new errors_js_1.ValidationError(`Unknown column "${key}" in aggregate for table "${qi.table}"`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (args._count && typeof args._count === 'object') {
|
|
554
|
+
for (const key of Object.keys(args._count)) {
|
|
555
|
+
if (!(key in meta.columnMap)) {
|
|
556
|
+
throw new errors_js_1.ValidationError(`Unknown column "${key}" in aggregate for table "${qi.table}"`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const selectExprs = [];
|
|
562
|
+
// _count
|
|
563
|
+
if (args._count === true) {
|
|
564
|
+
selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
565
|
+
}
|
|
566
|
+
else if (args._count && typeof args._count === 'object') {
|
|
567
|
+
for (const [field, enabled] of Object.entries(args._count)) {
|
|
568
|
+
if (enabled) {
|
|
569
|
+
const col = qi.toColumn(field);
|
|
570
|
+
selectExprs.push(`${qi.castAgg(`COUNT(${qi.q(col)})`, 'int')} AS ${qi.q(`_count_${col}`)}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// _sum
|
|
575
|
+
if (args._sum) {
|
|
576
|
+
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
577
|
+
if (enabled) {
|
|
578
|
+
const col = qi.toColumn(field);
|
|
579
|
+
selectExprs.push(`SUM(${qi.q(col)}) AS ${qi.q(`_sum_${col}`)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// _avg
|
|
584
|
+
if (args._avg) {
|
|
585
|
+
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
586
|
+
if (enabled) {
|
|
587
|
+
const col = qi.toColumn(field);
|
|
588
|
+
selectExprs.push(`${qi.castAgg(`AVG(${qi.q(col)})`, 'float')} AS ${qi.q(`_avg_${col}`)}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// _min
|
|
593
|
+
if (args._min) {
|
|
594
|
+
for (const [field, enabled] of Object.entries(args._min)) {
|
|
595
|
+
if (enabled) {
|
|
596
|
+
const col = qi.toColumn(field);
|
|
597
|
+
selectExprs.push(`MIN(${qi.q(col)}) AS ${qi.q(`_min_${col}`)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// _max
|
|
602
|
+
if (args._max) {
|
|
603
|
+
for (const [field, enabled] of Object.entries(args._max)) {
|
|
604
|
+
if (enabled) {
|
|
605
|
+
const col = qi.toColumn(field);
|
|
606
|
+
selectExprs.push(`MAX(${qi.q(col)}) AS ${qi.q(`_max_${col}`)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (selectExprs.length === 0) {
|
|
611
|
+
selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
612
|
+
}
|
|
613
|
+
const sql = `SELECT ${selectExprs.join(', ')} FROM ${qi.q(qi.table)}${whereSql}`;
|
|
614
|
+
return {
|
|
615
|
+
sql,
|
|
616
|
+
params,
|
|
617
|
+
transform: (result) => {
|
|
618
|
+
const row = result.rows[0];
|
|
619
|
+
const aggResult = {};
|
|
620
|
+
// _count
|
|
621
|
+
if (row._count !== undefined) {
|
|
622
|
+
aggResult._count = row._count;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Check for per-column counts
|
|
626
|
+
const countObj = {};
|
|
627
|
+
let hasCountFields = false;
|
|
628
|
+
for (const [key, val] of Object.entries(row)) {
|
|
629
|
+
if (key.startsWith('_count_')) {
|
|
630
|
+
const col = key.slice(7);
|
|
631
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
632
|
+
countObj[field] = val;
|
|
633
|
+
hasCountFields = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (hasCountFields)
|
|
637
|
+
aggResult._count = countObj;
|
|
638
|
+
}
|
|
639
|
+
// Build nested aggregate objects
|
|
640
|
+
const sumObj = {};
|
|
641
|
+
const avgObj = {};
|
|
642
|
+
const minObj = {};
|
|
643
|
+
const maxObj = {};
|
|
644
|
+
let hasSums = false, hasAvgs = false, hasMins = false, hasMaxs = false;
|
|
645
|
+
for (const [key, val] of Object.entries(row)) {
|
|
646
|
+
if (key.startsWith('_sum_')) {
|
|
647
|
+
const col = key.slice(5);
|
|
648
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
649
|
+
sumObj[field] = val !== null ? Number(val) : null;
|
|
650
|
+
hasSums = true;
|
|
651
|
+
}
|
|
652
|
+
else if (key.startsWith('_avg_')) {
|
|
653
|
+
const col = key.slice(5);
|
|
654
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
655
|
+
avgObj[field] = val !== null ? Number(val) : null;
|
|
656
|
+
hasAvgs = true;
|
|
657
|
+
}
|
|
658
|
+
else if (key.startsWith('_min_')) {
|
|
659
|
+
const col = key.slice(5);
|
|
660
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
661
|
+
minObj[field] = val;
|
|
662
|
+
hasMins = true;
|
|
663
|
+
}
|
|
664
|
+
else if (key.startsWith('_max_')) {
|
|
665
|
+
const col = key.slice(5);
|
|
666
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
667
|
+
maxObj[field] = val;
|
|
668
|
+
hasMaxs = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (hasSums)
|
|
672
|
+
aggResult._sum = sumObj;
|
|
673
|
+
if (hasAvgs)
|
|
674
|
+
aggResult._avg = avgObj;
|
|
675
|
+
if (hasMins)
|
|
676
|
+
aggResult._min = minObj;
|
|
677
|
+
if (hasMaxs)
|
|
678
|
+
aggResult._max = maxObj;
|
|
679
|
+
return aggResult;
|
|
680
|
+
},
|
|
681
|
+
tag: `${qi.table}.aggregate`,
|
|
682
|
+
};
|
|
683
|
+
}
|