fastify-auto-sqlapi 0.0.4 → 0.0.5
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/AGENTS_BACKEND.md +18 -14
- package/AGENTS_FRONTEND.md +78 -1
- package/README.md +117 -18
- package/dist/bin/generate-schema.js +3 -3
- package/dist/bin/generate-tables.js +3 -3
- package/dist/bin/utils.d.ts +1 -1
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +2 -2
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +9 -2
- package/dist/lib/cli/schema-codegen.d.ts +1 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +11 -4
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +4 -3
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +10 -4
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +40 -36
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +24 -23
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +41 -40
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +235 -39
- package/dist/lib/engine/write-helpers.js +4 -4
- package/dist/lib/naming.d.ts +11 -2
- package/dist/lib/naming.d.ts.map +1 -1
- package/dist/lib/naming.js +33 -5
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +16 -10
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +9 -1
- package/dist/types.d.ts +26 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
|
@@ -10,19 +10,112 @@ function validateSchemaField(field, schema) {
|
|
|
10
10
|
}
|
|
11
11
|
return schema.col(field);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const AGG_FN_SQL = {
|
|
14
|
+
sum: 'SUM',
|
|
15
|
+
min: 'MIN',
|
|
16
|
+
max: 'MAX',
|
|
17
|
+
avg: 'AVG',
|
|
18
|
+
count: 'COUNT',
|
|
19
|
+
distinctCount: 'COUNT DISTINCT', // marker, handled specially
|
|
20
|
+
};
|
|
21
|
+
function buildAggOrderExpr(db, dbTables, tableConf, joinTableName, fn, field, joinGroups, startIdx) {
|
|
22
|
+
const err400 = (msg) => {
|
|
23
|
+
const e = new Error(msg);
|
|
24
|
+
e.statusCode = 400;
|
|
25
|
+
throw e;
|
|
26
|
+
};
|
|
27
|
+
// Whitelist fn
|
|
28
|
+
if (!(fn in AGG_FN_SQL)) {
|
|
29
|
+
err400(`Invalid aggregation function: ${fn}`);
|
|
30
|
+
}
|
|
31
|
+
// Require joinGroups declaration for the referenced table + fn + field
|
|
32
|
+
const groupReq = joinGroups?.[joinTableName];
|
|
33
|
+
if (!groupReq) {
|
|
34
|
+
err400(`orderBy references undeclared joinGroup: ${joinTableName}`);
|
|
35
|
+
}
|
|
36
|
+
const declaredFields = groupReq.aggregations[fn];
|
|
37
|
+
if (!Array.isArray(declaredFields) || !declaredFields.includes(field)) {
|
|
38
|
+
err400(`orderBy references undeclared aggregation: ${joinTableName}.${fn}.${field}`);
|
|
39
|
+
}
|
|
40
|
+
// Resolve join definition
|
|
41
|
+
const joinDef = tableConf.allowedReadJoins?.find(([js]) => js.tableName === joinTableName);
|
|
42
|
+
if (!joinDef) {
|
|
43
|
+
err400(`Unknown join table in orderBy: ${joinTableName}`);
|
|
44
|
+
}
|
|
45
|
+
const [joinSchema, joinField, mainField] = joinDef;
|
|
46
|
+
// If joinGroup has `by`, only accept it when `by` is the correlation FK (joinField).
|
|
47
|
+
// In that case each main row corresponds to exactly one group, so the scalar
|
|
48
|
+
// subquery is semantically equivalent to no-by. Any other `by` produces multiple
|
|
49
|
+
// groups per main row → not a single scalar, reject.
|
|
50
|
+
if (groupReq.aggregations.by && groupReq.aggregations.by !== joinField) {
|
|
51
|
+
err400(`Cannot order by aggregation on joinGroup with 'by' clause on non-FK column: ${joinTableName} (grouped by '${groupReq.aggregations.by}', correlation FK is '${joinField}')`);
|
|
52
|
+
}
|
|
53
|
+
// Validate field exists on join schema
|
|
54
|
+
const fieldCol = validateSchemaField(field, joinSchema);
|
|
55
|
+
// Build correlation: joinTable.fk = mainTable.main_pk
|
|
56
|
+
const mainColName = Array.isArray(mainField) ? mainField[0] : mainField;
|
|
57
|
+
const mainCol = db.qi(tableConf.Schema.col(mainColName));
|
|
58
|
+
const mainTable = db.qi(tableConf.Schema.tableName);
|
|
59
|
+
const joinTable = db.qi(joinSchema.tableName);
|
|
60
|
+
const fkCol = db.qi(joinSchema.col(joinField));
|
|
61
|
+
// Rebuild joinGroup filters + conditions with correct placeholder offset
|
|
62
|
+
let filterWhere = '';
|
|
63
|
+
let filterVals = [];
|
|
64
|
+
const joinTableConf = dbTables[joinTableName];
|
|
65
|
+
if ((groupReq.filters || groupReq.conditions?.length) && joinSchema) {
|
|
66
|
+
const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db);
|
|
67
|
+
const built = cb.build(startIdx, db.ph);
|
|
68
|
+
if (built) {
|
|
69
|
+
filterWhere = ` AND ${built}`;
|
|
70
|
+
filterVals = cb.getValues();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Generate SQL expression
|
|
74
|
+
const qField = db.qi(fieldCol);
|
|
75
|
+
const fnSql = AGG_FN_SQL[fn];
|
|
76
|
+
const aggExpr = fnSql === 'COUNT DISTINCT'
|
|
77
|
+
? `COUNT(DISTINCT ${joinTable}.${qField})`
|
|
78
|
+
: `${fnSql}(${joinTable}.${qField})`;
|
|
79
|
+
// COALESCE with 0 so main rows without matching joined records get a sortable
|
|
80
|
+
// numeric value instead of NULL. Works on all dialects (PG/MySQL/MariaDB) and
|
|
81
|
+
// keeps "no data" at the bottom on DESC (top-N queries), and at the top on ASC.
|
|
82
|
+
const expr = `COALESCE((SELECT ${aggExpr} FROM ${joinTable} WHERE ${joinTable}.${fkCol} = ${mainTable}.${mainCol}${filterWhere}), 0)`;
|
|
83
|
+
return { expr, values: filterVals };
|
|
84
|
+
}
|
|
85
|
+
function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroups, startIdx) {
|
|
86
|
+
const parts = orderBy.split(',');
|
|
87
|
+
const outParts = [];
|
|
88
|
+
const outValues = [];
|
|
89
|
+
let currentIdx = startIdx;
|
|
90
|
+
for (const part of parts) {
|
|
15
91
|
const trimmed = part.trim();
|
|
16
|
-
|
|
17
|
-
|
|
92
|
+
// Dotted notation: <joinTable>.<fn>.<field> [ASC|DESC]
|
|
93
|
+
const dottedMatch = trimmed.match(/^(\w+)\.(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
94
|
+
if (dottedMatch) {
|
|
95
|
+
if (tableConf.distinctResults) {
|
|
96
|
+
const err = new Error('Cannot combine distinctResults with aggregation orderBy');
|
|
97
|
+
err.statusCode = 400;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const [, joinTableName, fn, field, dir] = dottedMatch;
|
|
101
|
+
const { expr, values } = buildAggOrderExpr(db, dbTables, tableConf, joinTableName, fn, field, joinGroups, currentIdx);
|
|
102
|
+
outParts.push(`${expr} ${(dir || 'ASC').toUpperCase()}`);
|
|
103
|
+
outValues.push(...values);
|
|
104
|
+
currentIdx += values.length;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Plain field: <field> [ASC|DESC]
|
|
108
|
+
const plainMatch = trimmed.match(/^(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
109
|
+
if (!plainMatch) {
|
|
18
110
|
const err = new Error(`Invalid orderBy: ${trimmed}`);
|
|
19
111
|
err.statusCode = 400;
|
|
20
112
|
throw err;
|
|
21
113
|
}
|
|
22
|
-
const [, field, dir] =
|
|
114
|
+
const [, field, dir] = plainMatch;
|
|
23
115
|
const col = validateSchemaField(field, tableConf.Schema);
|
|
24
|
-
|
|
25
|
-
}
|
|
116
|
+
outParts.push(`${db.qi(col)} ${(dir || 'ASC').toUpperCase()}`);
|
|
117
|
+
}
|
|
118
|
+
return { sql: outParts.join(', '), values: outValues };
|
|
26
119
|
}
|
|
27
120
|
async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = []) {
|
|
28
121
|
const tableName = tableConf.Schema.tableName;
|
|
@@ -39,7 +132,7 @@ async function executeMainQuery(db, tableConf, where, values, orderBy, paginator
|
|
|
39
132
|
distinct: tableConf.distinctResults,
|
|
40
133
|
joins: extraJoins.length > 0 ? extraJoins : undefined,
|
|
41
134
|
});
|
|
42
|
-
return rows.map((r) => camelcaseObject(r));
|
|
135
|
+
return rows.map((r) => camelcaseObject(r, tableConf.Schema));
|
|
43
136
|
}
|
|
44
137
|
async function buildPagination(db, tableConf, where, values, paginator, extraJoins = [], computeMin, computeMax, computeSum, computeAvg) {
|
|
45
138
|
const tableName = tableConf.Schema.tableName;
|
|
@@ -72,7 +165,7 @@ function findJoinDefinition(tableConf, joinTableName) {
|
|
|
72
165
|
}
|
|
73
166
|
async function executeVirtualJoins(db, dbTables, tableConf, mainResults, joins) {
|
|
74
167
|
const result = {};
|
|
75
|
-
for (const [joinTableName,
|
|
168
|
+
for (const [joinTableName, joinRef] of Object.entries(joins)) {
|
|
76
169
|
const joinDef = findJoinDefinition(tableConf, joinTableName);
|
|
77
170
|
if (!joinDef)
|
|
78
171
|
continue;
|
|
@@ -84,22 +177,27 @@ async function executeVirtualJoins(db, dbTables, tableConf, mainResults, joins)
|
|
|
84
177
|
result[joinTableName] = [];
|
|
85
178
|
continue;
|
|
86
179
|
}
|
|
87
|
-
// Build join filters
|
|
88
|
-
const cb = joinTableConf
|
|
89
|
-
? joinTableConf.filters(joinOpts?.filters || {})
|
|
90
|
-
: new ConditionBuilder('AND');
|
|
180
|
+
// Build join filters (equality) + rich conditions
|
|
181
|
+
const cb = buildJoinRefCondition(joinTableConf, joinSchema, joinRef || {}, db);
|
|
91
182
|
const fkCol = joinSchema.col(joinField);
|
|
92
183
|
cb.isIn(db.qi(fkCol), ids);
|
|
93
184
|
const where = cb.build(1, db.ph);
|
|
94
185
|
const values = cb.getValues();
|
|
95
|
-
|
|
186
|
+
// selection is a comma-separated list of field names (camelCase API names).
|
|
187
|
+
// Resolve each to its DB column via schema.col() and quote it.
|
|
188
|
+
const columns = selection === '*'
|
|
189
|
+
? '*'
|
|
190
|
+
: selection.split(',').map((c) => {
|
|
191
|
+
const field = c.trim();
|
|
192
|
+
return db.qi(joinSchema.col(field));
|
|
193
|
+
}).join(', ');
|
|
96
194
|
const rows = await db.select({
|
|
97
195
|
tableName: joinSchema.tableName,
|
|
98
196
|
columns,
|
|
99
197
|
where,
|
|
100
198
|
values,
|
|
101
199
|
});
|
|
102
|
-
result[joinTableName] = rows.map((r) => camelcaseObject(r));
|
|
200
|
+
result[joinTableName] = rows.map((r) => camelcaseObject(r, joinSchema));
|
|
103
201
|
}
|
|
104
202
|
return result;
|
|
105
203
|
}
|
|
@@ -115,7 +213,7 @@ async function executeJoinGroups(db, dbTables, tableConf, mainResults, joinGroup
|
|
|
115
213
|
result[joinTableName] = {};
|
|
116
214
|
continue;
|
|
117
215
|
}
|
|
118
|
-
const { aggregations, filters: groupFilters } = groupReq;
|
|
216
|
+
const { aggregations, filters: groupFilters, conditions: groupConditions } = groupReq;
|
|
119
217
|
const selectParts = [];
|
|
120
218
|
const groupByParts = [];
|
|
121
219
|
if (aggregations.by) {
|
|
@@ -147,15 +245,25 @@ async function executeJoinGroups(db, dbTables, tableConf, mainResults, joinGroup
|
|
|
147
245
|
selectParts.push(`SUM(${db.qi(col)}) as "sum_${f}"`);
|
|
148
246
|
}
|
|
149
247
|
}
|
|
248
|
+
if (aggregations.avg) {
|
|
249
|
+
for (const f of aggregations.avg) {
|
|
250
|
+
const col = validateSchemaField(f, joinSchema);
|
|
251
|
+
selectParts.push(`AVG(${db.qi(col)}) as "avg_${f}"`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (aggregations.count) {
|
|
255
|
+
for (const f of aggregations.count) {
|
|
256
|
+
const col = validateSchemaField(f, joinSchema);
|
|
257
|
+
selectParts.push(`COUNT(${db.qi(col)}) as "count_${f}"`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
150
260
|
if (selectParts.length === 0) {
|
|
151
261
|
result[joinTableName] = {};
|
|
152
262
|
continue;
|
|
153
263
|
}
|
|
154
|
-
// Build WHERE clause
|
|
264
|
+
// Build WHERE clause: equality filters + rich conditions
|
|
155
265
|
const joinTableConf = dbTables[joinTableName];
|
|
156
|
-
const cb = joinTableConf
|
|
157
|
-
? joinTableConf.filters(groupFilters)
|
|
158
|
-
: new ConditionBuilder('AND');
|
|
266
|
+
const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupFilters, conditions: groupConditions }, db);
|
|
159
267
|
const fkCol = joinSchema.col(joinField);
|
|
160
268
|
cb.isIn(db.qi(fkCol), ids);
|
|
161
269
|
const where = cb.build(1, db.ph);
|
|
@@ -205,8 +313,47 @@ function collectIds(mainResults, mainField) {
|
|
|
205
313
|
}
|
|
206
314
|
// ─── Conditions (advanced filters) ──────────────────────────
|
|
207
315
|
import { ALLOWED_SET, SINGLE_VALUE_SET, BETWEEN_SET, IN_SET, NULL_SET, } from '../../condition-methods.js';
|
|
316
|
+
function dispatchConditionMethod(cb, method, colOrExpr, params) {
|
|
317
|
+
if (SINGLE_VALUE_SET.has(method)) {
|
|
318
|
+
cb[method](colOrExpr, params[0]);
|
|
319
|
+
}
|
|
320
|
+
else if (BETWEEN_SET.has(method)) {
|
|
321
|
+
cb[method](colOrExpr, params[0], params[1]);
|
|
322
|
+
}
|
|
323
|
+
else if (IN_SET.has(method)) {
|
|
324
|
+
cb[method](colOrExpr, params[0]);
|
|
325
|
+
}
|
|
326
|
+
else if (NULL_SET.has(method)) {
|
|
327
|
+
cb[method](colOrExpr, true);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Build a ConditionBuilder that applies both equality filters and rich conditions
|
|
332
|
+
* to a join schema. Used by joinFilters (EXISTS), virtual joins data fetching,
|
|
333
|
+
* and joinGroups (aggregation WHERE + orderBy scalar subquery).
|
|
334
|
+
*/
|
|
335
|
+
function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
|
|
336
|
+
const cb = (ref.filters && joinTableConf)
|
|
337
|
+
? joinTableConf.filters(ref.filters)
|
|
338
|
+
: new ConditionBuilder('AND');
|
|
339
|
+
if (ref.conditions?.length) {
|
|
340
|
+
for (const c of ref.conditions) {
|
|
341
|
+
if (!ALLOWED_SET.has(c.method)) {
|
|
342
|
+
const err = new Error(`Invalid condition method: ${c.method}`);
|
|
343
|
+
err.statusCode = 400;
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
const col = db.qi(validateSchemaField(c.field, joinSchema));
|
|
347
|
+
dispatchConditionMethod(cb, c.method, col, c.params ?? []);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return cb;
|
|
351
|
+
}
|
|
208
352
|
function applyConditions(condition, conditions, schema, db) {
|
|
209
353
|
for (const c of conditions) {
|
|
354
|
+
// Skip dot-notation fields — those become aggregation conditions processed later
|
|
355
|
+
if (c.field.includes('.'))
|
|
356
|
+
continue;
|
|
210
357
|
// Validate method — whitelist only, blocks prototype poisoning
|
|
211
358
|
if (!ALLOWED_SET.has(c.method)) {
|
|
212
359
|
const err = new Error(`Invalid condition method: ${c.method}`);
|
|
@@ -214,33 +361,61 @@ function applyConditions(condition, conditions, schema, db) {
|
|
|
214
361
|
throw err;
|
|
215
362
|
}
|
|
216
363
|
const col = db.qi(validateSchemaField(c.field, schema));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
364
|
+
dispatchConditionMethod(condition, c.method, col, c.params);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Append aggregation-based conditions (HAVING-style) to the main WHERE clause.
|
|
369
|
+
* For each condition with a dotted field like `session.count.id`, builds a
|
|
370
|
+
* correlated scalar subquery (reusing buildAggOrderExpr) and uses it as the
|
|
371
|
+
* left-hand side of a ConditionBuilder method.
|
|
372
|
+
*
|
|
373
|
+
* Runs after the base WHERE / joinFilters are built so placeholder offsets
|
|
374
|
+
* are correct.
|
|
375
|
+
*/
|
|
376
|
+
function appendAggConditions(currentWhere, currentValues, conditions, db, dbTables, tableConf, joinGroups) {
|
|
377
|
+
let where = currentWhere;
|
|
378
|
+
let values = [...currentValues];
|
|
379
|
+
for (const c of conditions) {
|
|
380
|
+
if (!c.field.includes('.'))
|
|
381
|
+
continue;
|
|
382
|
+
if (!ALLOWED_SET.has(c.method)) {
|
|
383
|
+
const err = new Error(`Invalid condition method: ${c.method}`);
|
|
384
|
+
err.statusCode = 400;
|
|
385
|
+
throw err;
|
|
225
386
|
}
|
|
226
|
-
|
|
227
|
-
|
|
387
|
+
const parts = c.field.split('.');
|
|
388
|
+
if (parts.length !== 3) {
|
|
389
|
+
const err = new Error(`Invalid dotted field in condition: ${c.field} (expected <joinTable>.<fn>.<field>)`);
|
|
390
|
+
err.statusCode = 400;
|
|
391
|
+
throw err;
|
|
228
392
|
}
|
|
393
|
+
const [joinTableName, fn, field] = parts;
|
|
394
|
+
// Build the scalar subquery; values for its filters are appended to the main values array.
|
|
395
|
+
const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, joinTableName, fn, field, joinGroups, values.length + 1);
|
|
396
|
+
values = [...values, ...exprValues];
|
|
397
|
+
// Use a temporary ConditionBuilder to generate "<expr> <op> <placeholder>" with
|
|
398
|
+
// the correct placeholder offset, then merge it back into the main values.
|
|
399
|
+
const tmpCb = new ConditionBuilder('AND');
|
|
400
|
+
dispatchConditionMethod(tmpCb, c.method, expr, c.params);
|
|
401
|
+
const startIdx = values.length + 1;
|
|
402
|
+
const clause = tmpCb.build(startIdx, db.ph);
|
|
403
|
+
where += ` AND ${clause}`;
|
|
404
|
+
values.push(...tmpCb.getValues());
|
|
229
405
|
}
|
|
406
|
+
return { where, values };
|
|
230
407
|
}
|
|
231
408
|
// ─── Join filters (EXISTS subquery) ─────────────────────────
|
|
232
409
|
function buildJoinFiltersExists(db, dbTables, tableConf, joinFilters, currentWhere, currentValues) {
|
|
233
410
|
let where = currentWhere;
|
|
234
411
|
const values = [...currentValues];
|
|
235
|
-
for (const [joinTableName,
|
|
412
|
+
for (const [joinTableName, ref] of Object.entries(joinFilters)) {
|
|
236
413
|
const joinDef = findJoinDefinition(tableConf, joinTableName);
|
|
237
414
|
if (!joinDef)
|
|
238
415
|
continue;
|
|
239
416
|
const [joinSchema, joinField, mainField] = joinDef;
|
|
240
417
|
const joinTableConf = dbTables[joinTableName];
|
|
241
|
-
|
|
242
|
-
continue;
|
|
243
|
-
const filterCondition = joinTableConf.filters(filterValues);
|
|
418
|
+
const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
|
|
244
419
|
const startIdx = values.length + 1;
|
|
245
420
|
const filterWhere = filterCondition.build(startIdx, db.ph);
|
|
246
421
|
const filterVals = filterCondition.getValues();
|
|
@@ -248,7 +423,11 @@ function buildJoinFiltersExists(db, dbTables, tableConf, joinFilters, currentWhe
|
|
|
248
423
|
const mainColName = Array.isArray(mainField) ? mainField[0] : mainField;
|
|
249
424
|
const mainCol = db.qi(tableConf.Schema.col(mainColName));
|
|
250
425
|
const mainTable = db.qi(tableConf.Schema.tableName);
|
|
251
|
-
|
|
426
|
+
// If there are no inner conditions, just the correlation suffices.
|
|
427
|
+
const innerWhere = filterWhere
|
|
428
|
+
? `${fkCol} = ${mainTable}.${mainCol} AND ${filterWhere}`
|
|
429
|
+
: `${fkCol} = ${mainTable}.${mainCol}`;
|
|
430
|
+
where += ` AND EXISTS (SELECT 1 FROM ${db.qi(joinSchema.tableName)} WHERE ${innerWhere})`;
|
|
252
431
|
values.push(...filterVals);
|
|
253
432
|
}
|
|
254
433
|
return { where, values };
|
|
@@ -275,11 +454,28 @@ export async function searchEngine(dbTables, params) {
|
|
|
275
454
|
if (joinFilters && Object.keys(joinFilters).length > 0) {
|
|
276
455
|
({ where, values } = buildJoinFiltersExists(db, dbTables, tableConf, joinFilters, where, values));
|
|
277
456
|
}
|
|
457
|
+
// Aggregation conditions (HAVING-style): dotted fields in `conditions` are
|
|
458
|
+
// translated into scalar subqueries using the joinGroup aggregation.
|
|
459
|
+
if (conditions?.length) {
|
|
460
|
+
const hasAggConditions = conditions.some((c) => c.field.includes('.'));
|
|
461
|
+
if (hasAggConditions) {
|
|
462
|
+
({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroups));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
278
465
|
// Validate and sanitize orderBy (user input -> SQL identifier)
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
466
|
+
// Note: orderBy aggregation values are kept separate from WHERE values,
|
|
467
|
+
// so that buildPagination (which doesn't use ORDER BY) only binds WHERE params.
|
|
468
|
+
let safeOrderBy;
|
|
469
|
+
let orderByValues = [];
|
|
470
|
+
if (orderBy) {
|
|
471
|
+
const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroups, values.length + 1);
|
|
472
|
+
safeOrderBy = obResult.sql;
|
|
473
|
+
orderByValues = obResult.values;
|
|
474
|
+
}
|
|
475
|
+
// Main query: bind WHERE values + orderBy aggregation values
|
|
476
|
+
const mainValues = [...values, ...orderByValues];
|
|
477
|
+
const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, tenantJoins);
|
|
478
|
+
// Pagination: bind only WHERE values (COUNT/compute queries don't use ORDER BY)
|
|
283
479
|
let pagination;
|
|
284
480
|
if (paginator) {
|
|
285
481
|
pagination = await buildPagination(db, tableConf, where, values, paginator, tenantJoins, computeMin, computeMax, computeSum, computeAvg);
|
|
@@ -35,7 +35,7 @@ export async function processSecondaries(db, tableConf, dbTables, mainRecord, se
|
|
|
35
35
|
? secondaryPk.map((f) => joinSchema.col(f))
|
|
36
36
|
: joinSchema.col(secondaryPk);
|
|
37
37
|
const preparedRecords = records.map((rec) => {
|
|
38
|
-
let prepared = snakecaseRecord(rec);
|
|
38
|
+
let prepared = snakecaseRecord(rec, joinSchema);
|
|
39
39
|
// Auto-fill FK from main record
|
|
40
40
|
const mainValue = Array.isArray(mainField)
|
|
41
41
|
? mainRecord[mainField[0]]
|
|
@@ -56,7 +56,7 @@ export async function processSecondaries(db, tableConf, dbTables, mainRecord, se
|
|
|
56
56
|
else {
|
|
57
57
|
pkRows = await db.bulkInsert(joinSchema.tableName, preparedRecords, secondaryPkCol);
|
|
58
58
|
}
|
|
59
|
-
results[joinTableName] = pkRows.map((r) => camelcaseObject(r));
|
|
59
|
+
results[joinTableName] = pkRows.map((r) => camelcaseObject(r, joinSchema));
|
|
60
60
|
}
|
|
61
61
|
return results;
|
|
62
62
|
}
|
|
@@ -71,10 +71,10 @@ export async function processDeletions(db, tableConf, deletions) {
|
|
|
71
71
|
const [joinSchema] = joinDef;
|
|
72
72
|
const deletedRows = [];
|
|
73
73
|
for (const rec of records) {
|
|
74
|
-
const snaked = snakecaseRecord(rec);
|
|
74
|
+
const snaked = snakecaseRecord(rec, joinSchema);
|
|
75
75
|
const affectedRows = await db.delete(joinSchema.tableName, snaked);
|
|
76
76
|
if (affectedRows > 0) {
|
|
77
|
-
deletedRows.push(camelcaseObject(snaked));
|
|
77
|
+
deletedRows.push(camelcaseObject(snaked, joinSchema));
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
results[joinTableName] = deletedRows;
|
package/dist/lib/naming.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import type { SchemaDefinition } from '../types.js';
|
|
1
2
|
export declare function toCamelCase(str: string): string;
|
|
2
3
|
export declare function toUnderscore(str: string): string;
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Convert API record (camelCase keys) to DB record (actual column names).
|
|
6
|
+
* Uses schema.colMap when available, falls back to toUnderscore.
|
|
7
|
+
*/
|
|
8
|
+
export declare function snakecaseRecord(obj: Record<string, unknown>, schema?: SchemaDefinition): Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Convert DB row (actual column names) to API record (camelCase field names).
|
|
11
|
+
* Uses schema.colMap when available, falls back to toCamelCase.
|
|
12
|
+
*/
|
|
13
|
+
export declare function camelcaseObject<T extends Record<string, unknown>>(obj: T, schema?: SchemaDefinition): Record<string, unknown>;
|
|
5
14
|
export declare function toSchemaName(tableName: string): string;
|
|
6
15
|
//# sourceMappingURL=naming.d.ts.map
|
package/dist/lib/naming.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../../src/lib/naming.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMhD;
|
|
1
|
+
{"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../../src/lib/naming.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMhD;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,CAAC,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,GAAG,EAAE,CAAC,EACN,MAAM,CAAC,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
package/dist/lib/naming.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function toCamelCase(str) {
|
|
2
|
-
return str.
|
|
2
|
+
return str.replace(/([-_])([a-zA-Z])/g, (_, _sep, char) => char.toUpperCase());
|
|
3
3
|
}
|
|
4
4
|
export function toUnderscore(str) {
|
|
5
5
|
return str
|
|
@@ -8,11 +8,39 @@ export function toUnderscore(str) {
|
|
|
8
8
|
.map((a, index) => (index === 0 ? a : a[0].toLowerCase() + a.substring(1)))
|
|
9
9
|
.join('_');
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Reverse colMap cache: colMap object → { column: field }
|
|
12
|
+
const reverseCache = new WeakMap();
|
|
13
|
+
function getReverseMap(colMap) {
|
|
14
|
+
let reverse = reverseCache.get(colMap);
|
|
15
|
+
if (!reverse) {
|
|
16
|
+
reverse = {};
|
|
17
|
+
for (const [field, col] of Object.entries(colMap)) {
|
|
18
|
+
reverse[col] = field;
|
|
19
|
+
}
|
|
20
|
+
reverseCache.set(colMap, reverse);
|
|
21
|
+
}
|
|
22
|
+
return reverse;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert API record (camelCase keys) to DB record (actual column names).
|
|
26
|
+
* Uses schema.colMap when available, falls back to toUnderscore.
|
|
27
|
+
*/
|
|
28
|
+
export function snakecaseRecord(obj, schema) {
|
|
29
|
+
const mapFn = schema?.colMap
|
|
30
|
+
? (k) => schema.colMap[k] ?? k
|
|
31
|
+
: (k) => toUnderscore(k);
|
|
32
|
+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [mapFn(k), v]));
|
|
13
33
|
}
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Convert DB row (actual column names) to API record (camelCase field names).
|
|
36
|
+
* Uses schema.colMap when available, falls back to toCamelCase.
|
|
37
|
+
*/
|
|
38
|
+
export function camelcaseObject(obj, schema) {
|
|
39
|
+
if (schema?.colMap) {
|
|
40
|
+
const reverse = getReverseMap(schema.colMap);
|
|
41
|
+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [reverse[k] ?? k, v]));
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [toCamelCase(k), v]));
|
|
16
44
|
}
|
|
17
45
|
export function toSchemaName(tableName) {
|
|
18
46
|
return ('Schema' +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAU/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAU/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsElF;AAED,eAAO,MAAM,sBAAsB;;;;;;;;EAQjC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAiClF"}
|
|
@@ -10,6 +10,13 @@ const JoinGroupResultItem = Type.Object({
|
|
|
10
10
|
export function SearchTableBodyPost(dbTables, tableName) {
|
|
11
11
|
const tableConf = dbTables[tableName];
|
|
12
12
|
const schema = tableConf.Schema;
|
|
13
|
+
// Condition item shape (shared across main, joinFilters, joins, joinGroups)
|
|
14
|
+
const methodEnum = Type.Union(ALLOWED_METHODS.map((m) => Type.Literal(m)));
|
|
15
|
+
const conditionItemSchema = Type.Object({
|
|
16
|
+
field: Type.String(),
|
|
17
|
+
method: methodEnum,
|
|
18
|
+
params: Type.Optional(Type.Array(Type.Any())),
|
|
19
|
+
});
|
|
13
20
|
// Filters: schema fields + extraFilters
|
|
14
21
|
const filterFields = {
|
|
15
22
|
...schema.fields,
|
|
@@ -27,10 +34,13 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
27
34
|
const joinFilterFields = joinTableConf
|
|
28
35
|
? { ...joinSchema.fields, ...joinTableConf.extraFilters }
|
|
29
36
|
: { ...joinSchema.fields };
|
|
30
|
-
|
|
37
|
+
// Shared { filters?, conditions? } shape for joinFilters, joins, and joinGroups inner filters
|
|
38
|
+
const joinRefShape = {
|
|
31
39
|
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
41
|
+
};
|
|
42
|
+
joinProperties[joinTableName] = Type.Object(joinRefShape);
|
|
43
|
+
joinFilterProperties[joinTableName] = Type.Object(joinRefShape);
|
|
34
44
|
joinGroupProperties[joinTableName] = Type.Object({
|
|
35
45
|
aggregations: Type.Object({
|
|
36
46
|
by: Type.Optional(Type.String()),
|
|
@@ -38,18 +48,14 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
38
48
|
min: Type.Optional(Type.Array(Type.String())),
|
|
39
49
|
max: Type.Optional(Type.Array(Type.String())),
|
|
40
50
|
sum: Type.Optional(Type.Array(Type.String())),
|
|
51
|
+
avg: Type.Optional(Type.Array(Type.String())),
|
|
52
|
+
count: Type.Optional(Type.Array(Type.String())),
|
|
41
53
|
}),
|
|
42
54
|
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
55
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
43
56
|
});
|
|
44
57
|
}
|
|
45
58
|
}
|
|
46
|
-
// Conditions: advanced filters with ConditionBuilder methods
|
|
47
|
-
const methodEnum = Type.Union(ALLOWED_METHODS.map((m) => Type.Literal(m)));
|
|
48
|
-
const conditionItemSchema = Type.Object({
|
|
49
|
-
field: Type.String(),
|
|
50
|
-
method: methodEnum,
|
|
51
|
-
params: Type.Optional(Type.Array(Type.Any())),
|
|
52
|
-
});
|
|
53
59
|
const bodyProperties = {
|
|
54
60
|
filters: filtersSchema,
|
|
55
61
|
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAYrB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAEX;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,SAAS,GAAE,MAAY,GACtB,cAAc,CAEhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
|
+
import { getDialect } from './dialect.js';
|
|
3
|
+
// Quote an identifier using the current global ConditionBuilder dialect.
|
|
4
|
+
// Needed so that DB columns with uppercase letters (e.g. betterauth "userId")
|
|
5
|
+
// are preserved on PostgreSQL (which folds unquoted identifiers to lowercase)
|
|
6
|
+
// and on MySQL (which is case-sensitive on Linux filesystems).
|
|
7
|
+
function qi(field) {
|
|
8
|
+
return getDialect(ConditionBuilder.DIALECT).qi(field);
|
|
9
|
+
}
|
|
2
10
|
export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
|
|
3
11
|
const filters = (filterValues) => {
|
|
4
12
|
const condition = new ConditionBuilder('AND');
|
|
5
13
|
// Only auto-match real schema fields (DB columns)
|
|
6
14
|
for (const field of Object.keys(Schema.fields)) {
|
|
7
15
|
if (field in filterValues && filterValues[field] !== null && filterValues[field] !== undefined) {
|
|
8
|
-
condition.isEqual(Schema.col(field), filterValues[field]);
|
|
16
|
+
condition.isEqual(qi(Schema.col(field)), filterValues[field]);
|
|
9
17
|
}
|
|
10
18
|
}
|
|
11
19
|
// extraFilters are handled exclusively by extendedCondition
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface SqlApiConfig {
|
|
|
8
8
|
outputDir: string;
|
|
9
9
|
schema?: string;
|
|
10
10
|
dialect?: DialectName;
|
|
11
|
+
envFile?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface ColumnInfo {
|
|
13
14
|
table_name: string;
|
|
@@ -20,6 +21,7 @@ export interface TableMap {
|
|
|
20
21
|
[schemaName: string]: {
|
|
21
22
|
name: string;
|
|
22
23
|
fields: Record<string, string>;
|
|
24
|
+
colMap: Record<string, string>;
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
export interface SqlResult<T = Record<string, unknown>> {
|
|
@@ -44,6 +46,7 @@ export interface SelectOptions {
|
|
|
44
46
|
}
|
|
45
47
|
export interface SchemaDefinition<T = Record<string, TSchema>> {
|
|
46
48
|
col(field: string): string;
|
|
49
|
+
colMap?: Record<string, string>;
|
|
47
50
|
fields: T;
|
|
48
51
|
validation: TObject;
|
|
49
52
|
tableName: string;
|
|
@@ -98,9 +101,15 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
98
101
|
schemaOverrides?: Partial<Record<string & keyof F, TSchema>>;
|
|
99
102
|
validate?: ValidatorFn<F>;
|
|
100
103
|
validateBulk?: BulkValidatorFn<F>;
|
|
101
|
-
beforeInsert?: (db: QueryClient, req: FastifyRequest, record:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
beforeInsert?: (db: QueryClient, req: FastifyRequest, record: {
|
|
105
|
+
[K in keyof F]?: Static<F[K]> | Expression | null;
|
|
106
|
+
}) => Promise<void>;
|
|
107
|
+
beforeUpdate?: (db: QueryClient, req: FastifyRequest, fields: {
|
|
108
|
+
[K in keyof F]?: Static<F[K]> | Expression | null;
|
|
109
|
+
}, secondaryFieldsFetcher?: unknown) => void | Promise<void>;
|
|
110
|
+
afterInsert?: (db: QueryClient, req: FastifyRequest, record: {
|
|
111
|
+
[K in keyof F]?: Static<F[K]>;
|
|
112
|
+
}, secondaryRecords?: unknown) => Promise<void>;
|
|
104
113
|
defaultOrder?: string;
|
|
105
114
|
excludeFromCreation?: (string & keyof F)[];
|
|
106
115
|
distinctResults?: boolean;
|
|
@@ -149,20 +158,30 @@ export interface AggregationRequest {
|
|
|
149
158
|
min?: string[];
|
|
150
159
|
max?: string[];
|
|
151
160
|
sum?: string[];
|
|
161
|
+
avg?: string[];
|
|
162
|
+
count?: string[];
|
|
152
163
|
}
|
|
153
164
|
export interface JoinGroupRequest {
|
|
154
165
|
aggregations: AggregationRequest;
|
|
155
166
|
filters?: FilterRecord;
|
|
167
|
+
conditions?: SearchCondition[];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reference filter for join-based existential filtering (joinFilters) and
|
|
171
|
+
* join-based virtual joins data fetching (joins). Combines equality-based filters
|
|
172
|
+
* with rich ConditionBuilder-powered conditions, both applied to the join schema.
|
|
173
|
+
*/
|
|
174
|
+
export interface JoinRefFilter {
|
|
175
|
+
filters?: FilterRecord;
|
|
176
|
+
conditions?: SearchCondition[];
|
|
156
177
|
}
|
|
157
178
|
export interface SearchParams {
|
|
158
179
|
db: QueryClient;
|
|
159
180
|
tableConf: ITable;
|
|
160
181
|
filters?: FilterRecord;
|
|
161
182
|
conditions?: SearchCondition[];
|
|
162
|
-
joinFilters?: Record<string,
|
|
163
|
-
joins?: Record<string,
|
|
164
|
-
filters?: FilterRecord;
|
|
165
|
-
}>;
|
|
183
|
+
joinFilters?: Record<string, JoinRefFilter>;
|
|
184
|
+
joins?: Record<string, JoinRefFilter>;
|
|
166
185
|
joinGroups?: Record<string, JoinGroupRequest>;
|
|
167
186
|
orderBy?: string;
|
|
168
187
|
paginator?: Paginator;
|