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.
Files changed (37) hide show
  1. package/AGENTS_BACKEND.md +18 -14
  2. package/AGENTS_FRONTEND.md +78 -1
  3. package/README.md +117 -18
  4. package/dist/bin/generate-schema.js +3 -3
  5. package/dist/bin/generate-tables.js +3 -3
  6. package/dist/bin/utils.d.ts +1 -1
  7. package/dist/bin/utils.d.ts.map +1 -1
  8. package/dist/bin/utils.js +2 -2
  9. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  10. package/dist/lib/cli/mysql-introspect.js +9 -2
  11. package/dist/lib/cli/schema-codegen.d.ts +1 -1
  12. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  13. package/dist/lib/cli/schema-codegen.js +11 -4
  14. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  15. package/dist/lib/cli/tables-codegen.js +4 -3
  16. package/dist/lib/db.d.ts.map +1 -1
  17. package/dist/lib/db.js +10 -4
  18. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  19. package/dist/lib/engine/bulk/bulk-upsert.js +40 -36
  20. package/dist/lib/engine/rest/get.js +1 -1
  21. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  22. package/dist/lib/engine/rest/insert.js +24 -23
  23. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  24. package/dist/lib/engine/rest/update.js +41 -40
  25. package/dist/lib/engine/search/search.d.ts.map +1 -1
  26. package/dist/lib/engine/search/search.js +235 -39
  27. package/dist/lib/engine/write-helpers.js +4 -4
  28. package/dist/lib/naming.d.ts +11 -2
  29. package/dist/lib/naming.d.ts.map +1 -1
  30. package/dist/lib/naming.js +33 -5
  31. package/dist/lib/schema/search.d.ts.map +1 -1
  32. package/dist/lib/schema/search.js +16 -10
  33. package/dist/lib/table-helpers.d.ts.map +1 -1
  34. package/dist/lib/table-helpers.js +9 -1
  35. package/dist/types.d.ts +26 -7
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +8 -5
@@ -10,19 +10,112 @@ function validateSchemaField(field, schema) {
10
10
  }
11
11
  return schema.col(field);
12
12
  }
13
- function validateOrderBy(orderBy, tableConf, db) {
14
- return orderBy.split(',').map((part) => {
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
- const match = trimmed.match(/^(\w+)(?:\s+(ASC|DESC))?$/i);
17
- if (!match) {
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] = match;
114
+ const [, field, dir] = plainMatch;
23
115
  const col = validateSchemaField(field, tableConf.Schema);
24
- return `${db.qi(col)} ${(dir || 'ASC').toUpperCase()}`;
25
- }).join(', ');
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, joinOpts] of Object.entries(joins)) {
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
- const columns = selection === '*' ? '*' : selection;
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 && groupFilters
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
- if (SINGLE_VALUE_SET.has(c.method)) {
218
- condition[c.method](col, c.params[0]);
219
- }
220
- else if (BETWEEN_SET.has(c.method)) {
221
- condition[c.method](col, c.params[0], c.params[1]);
222
- }
223
- else if (IN_SET.has(c.method)) {
224
- condition[c.method](col, c.params[0]);
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
- else if (NULL_SET.has(c.method)) {
227
- condition[c.method](col, true);
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, filterValues] of Object.entries(joinFilters)) {
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
- if (!joinTableConf)
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
- where += ` AND EXISTS (SELECT 1 FROM ${db.qi(joinSchema.tableName)} WHERE ${fkCol} = ${mainTable}.${mainCol} AND ${filterWhere})`;
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
- const safeOrderBy = orderBy ? validateOrderBy(orderBy, tableConf, db) : undefined;
280
- // Main query
281
- const main = await executeMainQuery(db, tableConf, where, values, safeOrderBy, paginator, tenantJoins);
282
- // Pagination
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;
@@ -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
- export declare function camelcaseObject<T extends Record<string, unknown>>(obj: T): Record<string, unknown>;
4
- export declare function snakecaseRecord(obj: Record<string, unknown>): Record<string, unknown>;
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
@@ -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;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,GAAG,EAAE,CAAC,GACL,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQtD"}
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"}
@@ -1,5 +1,5 @@
1
1
  export function toCamelCase(str) {
2
- return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', ''));
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
- export function camelcaseObject(obj) {
12
- return Object.fromEntries(Object.entries(obj).map(([k, v]) => [toCamelCase(k), v]));
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
- export function snakecaseRecord(obj) {
15
- return Object.fromEntries(Object.entries(obj).map(([k, v]) => [toUnderscore(k), v]));
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,CAgElF;AAED,eAAO,MAAM,sBAAsB;;;;;;;;EAQjC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAiClF"}
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
- joinProperties[joinTableName] = Type.Object({
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
- joinFilterProperties[joinTableName] = Type.Partial(Type.Object(joinFilterFields));
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;AAGrB,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
+ {"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: Record<string, unknown>) => Promise<void>;
102
- beforeUpdate?: (db: QueryClient, req: FastifyRequest, fields: Record<string, unknown>, secondaryFieldsFetcher?: unknown) => void | Promise<void>;
103
- afterInsert?: (db: QueryClient, req: FastifyRequest, record: Record<string, unknown>, secondaryRecords?: unknown) => Promise<void>;
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, FilterRecord>;
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;