fastify-auto-sqlapi 0.1.2 → 0.1.4

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 (90) hide show
  1. package/AGENTS_BACKEND.md +41 -2
  2. package/AGENTS_FRONTEND.md +7 -5
  3. package/README.md +105 -5
  4. package/dist/bin/generate-schema.js +2 -2
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
  8. package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
  9. package/dist/lib/adapters/mysql-adapter.js +28 -17
  10. package/dist/lib/adapters/pg-adapter.d.ts +4 -0
  11. package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
  12. package/dist/lib/adapters/pg-adapter.js +18 -7
  13. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  14. package/dist/lib/cli/mysql-introspect.js +3 -1
  15. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/pg-introspect.js +13 -4
  17. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  18. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  19. package/dist/lib/cli/schema-codegen.js +17 -3
  20. package/dist/lib/cli/tables-codegen.d.ts +2 -0
  21. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  22. package/dist/lib/cli/tables-codegen.js +43 -12
  23. package/dist/lib/db.d.ts +17 -0
  24. package/dist/lib/db.d.ts.map +1 -1
  25. package/dist/lib/db.js +47 -0
  26. package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
  27. package/dist/lib/engine/bulk/bulk-delete.js +25 -8
  28. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  29. package/dist/lib/engine/bulk/bulk-upsert.js +12 -9
  30. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  31. package/dist/lib/engine/rest/delete.js +9 -5
  32. package/dist/lib/engine/rest/get.js +1 -1
  33. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/insert.js +41 -34
  35. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  36. package/dist/lib/engine/rest/update.js +74 -57
  37. package/dist/lib/engine/search/search.d.ts.map +1 -1
  38. package/dist/lib/engine/search/search.js +94 -34
  39. package/dist/lib/schema/bulk-delete.d.ts +1 -1
  40. package/dist/lib/schema/bulk-delete.d.ts.map +1 -1
  41. package/dist/lib/schema/bulk-delete.js +2 -2
  42. package/dist/lib/schema/bulk-upsert.d.ts +1 -1
  43. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  44. package/dist/lib/schema/bulk-upsert.js +5 -3
  45. package/dist/lib/schema/helpers.d.ts +2 -0
  46. package/dist/lib/schema/helpers.d.ts.map +1 -1
  47. package/dist/lib/schema/helpers.js +3 -2
  48. package/dist/lib/schema/insert.d.ts.map +1 -1
  49. package/dist/lib/schema/insert.js +4 -1
  50. package/dist/lib/schema/update.d.ts.map +1 -1
  51. package/dist/lib/schema/update.js +4 -1
  52. package/dist/lib/sql-api.d.ts +2 -0
  53. package/dist/lib/sql-api.d.ts.map +1 -1
  54. package/dist/lib/sql-api.js +1 -0
  55. package/dist/lib/table-helpers.d.ts.map +1 -1
  56. package/dist/lib/table-helpers.js +10 -9
  57. package/dist/lib/tenant.d.ts +24 -2
  58. package/dist/lib/tenant.d.ts.map +1 -1
  59. package/dist/lib/tenant.js +0 -0
  60. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  61. package/dist/routes/auto/bulk-delete.routes.js +3 -1
  62. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  63. package/dist/routes/auto/bulk-upsert.routes.js +3 -1
  64. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  65. package/dist/routes/auto/delete.routes.js +1 -0
  66. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  67. package/dist/routes/auto/get.routes.js +1 -0
  68. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  69. package/dist/routes/auto/insert.routes.js +1 -0
  70. package/dist/routes/auto/route-helpers.d.ts +3 -1
  71. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  72. package/dist/routes/auto/route-helpers.js +3 -0
  73. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  74. package/dist/routes/auto/search.routes.js +9 -0
  75. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  76. package/dist/routes/auto/update.routes.js +1 -0
  77. package/dist/types/cli.d.ts +6 -0
  78. package/dist/types/cli.d.ts.map +1 -1
  79. package/dist/types/db.d.ts +11 -0
  80. package/dist/types/db.d.ts.map +1 -1
  81. package/dist/types/plugin.d.ts +16 -0
  82. package/dist/types/plugin.d.ts.map +1 -1
  83. package/dist/types/plugin.js +4 -1
  84. package/dist/types/schema.d.ts +2 -0
  85. package/dist/types/schema.d.ts.map +1 -1
  86. package/dist/types/search.d.ts +7 -0
  87. package/dist/types/search.d.ts.map +1 -1
  88. package/dist/types/table.d.ts +40 -2
  89. package/dist/types/table.d.ts.map +1 -1
  90. package/package.json +2 -2
@@ -1,19 +1,21 @@
1
1
  import { snakecaseRecord } from '../../naming.js';
2
2
  import { processSecondaries, processDeletions } from '../write-helpers.js';
3
- import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, } from '../../tenant.js';
3
+ import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
5
5
  import { httpError } from '../../errors.js';
6
- import { primaryAsString } from '../../../types.js';
7
6
  export async function updateEngine(params) {
8
7
  const { db, tableConf, dbTables, request, record, secondaries, deletions, tenant } = params;
9
8
  const schema = tableConf.Schema;
10
- const pk = primaryAsString(tableConf.primary);
11
- const pkCol = schema.col(pk);
12
- const pkValue = record[pk];
13
- if (pkValue == null)
14
- throw httpError(400, `Primary key "${pk}" is required`);
9
+ // Every primary key field/column: composite PKs must match on ALL columns, otherwise an
10
+ // UPDATE keyed on the first column alone would hit every row sharing that value.
11
+ const pkFields = Array.isArray(tableConf.primary) ? tableConf.primary : [tableConf.primary];
12
+ const pkCols = pkFields.map((f) => schema.col(f));
13
+ const pkValues = pkFields.map((f) => record[f]);
14
+ const missingIdx = pkValues.findIndex((v) => v == null);
15
+ if (missingIdx !== -1)
16
+ throw httpError(400, `Primary key "${pkFields[missingIdx]}" is required`);
15
17
  // 1. Tenant ownership check (indirect) — do it early so user mutations don't run on records they can't access
16
- await assertTenantOwnership(db, tenant, schema.tableName, pkCol, pkValue);
18
+ await assertTenantOwnership(db, tenant, schema.tableName, pkCols, pkValues);
17
19
  // 2. Mutable copy of input in camelCase
18
20
  const inputRecord = { ...record };
19
21
  // 3. Custom validation (camelCase)
@@ -25,57 +27,72 @@ export async function updateEngine(params) {
25
27
  // 5. Convert to DB format (after all user mutations)
26
28
  const snaked = snakecaseRecord(inputRecord, schema);
27
29
  const updateFields = { ...snaked };
28
- delete updateFields[pkCol];
29
- // 6. Strip tenant column (user cannot change tenant of an existing record)
30
+ for (const c of pkCols)
31
+ delete updateFields[c];
32
+ // 6. Strip tenant column (user cannot change tenant of an existing record).
33
+ // For indirect scopes the tenant link is the through-FK (localField): it may be changed,
34
+ // but only to another value the caller owns — re-validate the new FK against the tenant.
30
35
  if (tenant) {
31
36
  stripTenantColumn(updateFields, tenant.scope);
37
+ await enforceTenantFKOnUpdate(db, tenant, updateFields);
32
38
  }
33
- // 7. Update main
34
- const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
35
- const extraCondition = buildTenantUpdateExtra(tenant);
36
- if (hasFieldsToUpdate) {
37
- const affectedRows = await db.update(schema.tableName, updateFields, { [pkCol]: pkValue }, extraCondition);
38
- if (affectedRows === 0)
39
- throw httpError(404, `Record not found`);
40
- }
41
- else {
42
- // No fields to update: verify the record exists (for secondaries/deletions)
43
- let whereSql = `${db.qi(schema.tableName)}.${db.qi(pkCol)} = ${db.ph(1)}`;
44
- const whereValues = [pkValue];
45
- if (extraCondition) {
46
- whereSql += ` AND ${extraCondition.build(2, db.ph)}`;
47
- whereValues.push(...extraCondition.getValues());
39
+ // Steps 7-9 are atomic: a failure in secondaries or deletions rolls back the main
40
+ // update too. Degrades to non-transactional when the adapter has no connect().
41
+ return db.withTransaction(async (tx) => {
42
+ // 7. Update main
43
+ const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
44
+ const extraCondition = buildTenantUpdateExtra(tx, tenant);
45
+ // WHERE matching every primary key column (single or composite).
46
+ const whereByPk = Object.fromEntries(pkCols.map((c, i) => [c, pkValues[i]]));
47
+ if (hasFieldsToUpdate) {
48
+ const affectedRows = await tx.update(schema.tableName, updateFields, whereByPk, extraCondition);
49
+ if (affectedRows === 0)
50
+ throw httpError(404, `Record not found`);
48
51
  }
49
- const rows = await db.select({
50
- tableName: schema.tableName,
51
- where: whereSql,
52
- values: whereValues,
53
- limit: '1',
54
- });
55
- if (rows.length === 0)
56
- throw httpError(404, `Record not found`);
57
- }
58
- // Build the main response (PK-only)
59
- const mainResult = { [pk]: pkValue };
60
- // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
61
- let secondaryResults;
62
- if (secondaries && Object.keys(secondaries).length > 0) {
63
- const mainForFK = { ...inputRecord };
64
- secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
65
- }
66
- // 9. Deletions (FK auto-fill from main like secondaries)
67
- let deletionResults;
68
- if (deletions && Object.keys(deletions).length > 0) {
69
- const mainForFK = { ...inputRecord };
70
- deletionResults = await processDeletions(db, tableConf, mainForFK, deletions);
71
- }
72
- // 10. Return PK-only
73
- const result = { main: mainResult };
74
- if (secondaryResults && Object.keys(secondaryResults).length > 0) {
75
- result.secondaries = secondaryResults;
76
- }
77
- if (deletionResults && Object.keys(deletionResults).length > 0) {
78
- result.deletions = deletionResults;
79
- }
80
- return result;
52
+ else {
53
+ // No fields to update: verify the record exists (for secondaries/deletions)
54
+ const whereParts = pkCols.map((c, i) => `${tx.qi(schema.tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
55
+ let whereSql = whereParts.join(' AND ');
56
+ const whereValues = [...pkValues];
57
+ if (extraCondition) {
58
+ whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
59
+ whereValues.push(...extraCondition.getValues());
60
+ }
61
+ const rows = await tx.select({
62
+ tableName: schema.tableName,
63
+ where: whereSql,
64
+ values: whereValues,
65
+ limit: '1',
66
+ });
67
+ if (rows.length === 0)
68
+ throw httpError(404, `Record not found`);
69
+ }
70
+ // Build the main response (PK-only — every PK field)
71
+ const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
72
+ // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
73
+ let secondaryResults;
74
+ if (secondaries && Object.keys(secondaries).length > 0) {
75
+ const mainForFK = { ...inputRecord };
76
+ secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
77
+ }
78
+ // 9. Deletions (FK auto-fill from main like secondaries)
79
+ let deletionResults;
80
+ if (deletions && Object.keys(deletions).length > 0) {
81
+ const mainForFK = { ...inputRecord };
82
+ deletionResults = await processDeletions(tx, tableConf, mainForFK, deletions);
83
+ }
84
+ // 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
85
+ if (tableConf.afterUpdate) {
86
+ await tableConf.afterUpdate(tx, request, inputRecord, secondaryResults, deletionResults);
87
+ }
88
+ // 11. Return PK-only
89
+ const result = { main: mainResult };
90
+ if (secondaryResults && Object.keys(secondaryResults).length > 0) {
91
+ result.secondaries = secondaryResults;
92
+ }
93
+ if (deletionResults && Object.keys(deletionResults).length > 0) {
94
+ result.deletions = deletionResults;
95
+ }
96
+ return result;
97
+ });
81
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAab,MAAM,mBAAmB,CAAC;AAy/B3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAoIvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AA4jC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA2IvB"}
@@ -50,6 +50,21 @@ function resolveFieldRef(field, schema, tableConf, db, alias) {
50
50
  err400(`Unknown field: ${field}`);
51
51
  }
52
52
  // ─── Join lookup ────────────────────────────────────────────
53
+ /**
54
+ * Tenant-scope a join side-query. When the joined table declares its own `tenantScope`,
55
+ * appends the tenant condition to `cb` (direct: `col IN ids`; indirect: condition on the
56
+ * through table) and, for indirect scopes, pushes the INNER JOIN clause into `joins`.
57
+ * No-op without a tenant context (admin) or when the join table is not tenant-scoped.
58
+ */
59
+ function appendJoinTenantScope(db, joinTableConf, tenant, joinTableName, cb, joins) {
60
+ const scope = joinTableConf?.tenantScope;
61
+ if (!tenant || !scope)
62
+ return;
63
+ cb.append(buildTenantCondition(db, scope, tenant.ids));
64
+ if ('through' in scope) {
65
+ joins?.push(buildTenantJoin(db, scope, joinTableName));
66
+ }
67
+ }
53
68
  function findJoinByAlias(tableConf, alias) {
54
69
  return tableConf.allowedReadJoins?.find((j) => j.alias === alias);
55
70
  }
@@ -156,6 +171,28 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
156
171
  const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
157
172
  return { expr, values: filterVals };
158
173
  }
174
+ /**
175
+ * Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
176
+ * Needed BEFORE building the LEFT JOIN clauses (which must know their aliases) while the actual
177
+ * orderBy SQL — with its parameter placeholders — is only baked later, once the LEFT JOIN value
178
+ * count is known. Validates each alias against `allowedReadJoins` (throws 400), mirroring
179
+ * `validateOrderBy`; 3-parti aggregation entries are ignored (they use joinGroup, not a LEFT JOIN).
180
+ */
181
+ function collectOrderByLeftAliases(orderBy, tableConf) {
182
+ const aliases = new Set();
183
+ for (const part of orderBy.split(',')) {
184
+ const trimmed = part.trim();
185
+ if (/^(\w+)\.(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i.test(trimmed))
186
+ continue; // 3-parti aggregation
187
+ const m = trimmed.match(/^(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i);
188
+ if (m) {
189
+ const alias = m[1];
190
+ requireJoin(tableConf, alias, true);
191
+ aliases.add(alias);
192
+ }
193
+ }
194
+ return aliases;
195
+ }
159
196
  function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx) {
160
197
  const parts = orderBy.split(',');
161
198
  const outParts = [];
@@ -219,7 +256,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
219
256
  // Note: extraFilters declared on joinTableConf are not supported here for
220
257
  // joinLeft (they would require alias-aware handlers); only schema fields
221
258
  // and computed fields apply.
222
- const cb = new ConditionBuilder('AND');
259
+ const cb = new ConditionBuilder('AND', db.cbDialect);
223
260
  const joinTableConf = dbTables[joinSchema.tableName];
224
261
  const computed = joinTableConf?.computedFields;
225
262
  if (ref.filters) {
@@ -266,12 +303,14 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
266
303
  return { joinClauses, whereExtras, values };
267
304
  }
268
305
  // ─── Main query execution ───────────────────────────────────
269
- async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed) {
306
+ async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed, maxRows) {
270
307
  const tableName = tableConf.Schema.tableName;
271
308
  const order = orderBy || tableConf.defaultOrder || primaryAsString(tableConf.primary);
309
+ // With a paginator, the page size governs the LIMIT. Without one, apply `maxRows` (if set) so an
310
+ // unbounded search cannot dump the whole table; a plain integer is a safe, non-injectable LIMIT.
272
311
  const limit = paginator
273
312
  ? `${paginator.itemsPerPage} OFFSET ${(paginator.page - 1) * paginator.itemsPerPage}`
274
- : null;
313
+ : (maxRows != null ? String(maxRows) : null);
275
314
  // Optional computed projections — bound values, if any, are NOT yet supported
276
315
  // here (would require placeholder-aware composition with WHERE values).
277
316
  let columns;
@@ -328,7 +367,7 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
328
367
  };
329
368
  }
330
369
  // ─── joinMultiple (virtual fetch, child rows in side query) ─
331
- async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple) {
370
+ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
332
371
  const result = {};
333
372
  for (const [alias, ref] of Object.entries(joinMultiple)) {
334
373
  const joinDef = requireJoin(tableConf, alias, false);
@@ -342,6 +381,8 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
342
381
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
343
382
  const fkCol = joinSchema.col(joinField);
344
383
  cb.isIn(db.qi(fkCol), ids);
384
+ const tenantJoins = [];
385
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
345
386
  let where = cb.build(1, db.ph);
346
387
  let values = cb.getValues();
347
388
  // Side-channel: computed-field clauses on the join table
@@ -356,17 +397,19 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
356
397
  columns,
357
398
  where,
358
399
  values,
400
+ joins: tenantJoins.length > 0 ? tenantJoins : undefined,
359
401
  });
360
402
  result[alias] = mapRowsToCamelCase(rows, joinSchema);
361
403
  }
362
404
  return result;
363
405
  }
364
406
  // ─── joinLeft (parent fetch via PK IN side query) ───────────
365
- async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
407
+ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
366
408
  const result = {};
367
409
  for (const [alias, ref] of Object.entries(joinLeft)) {
368
410
  const joinDef = requireJoin(tableConf, alias, true);
369
411
  const { joinSchema, joinField, mainField, selection: defaultSelection } = joinDef;
412
+ const joinTableConf = dbTables[joinSchema.tableName];
370
413
  // For joinLeft (N:1), mainField on main is the FK pointing to joinField (PK) on parent.
371
414
  // We collect the FK values from the main results and look up parents by their PK.
372
415
  const ids = collectIds(mainResults, mainField);
@@ -375,8 +418,10 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
375
418
  continue;
376
419
  }
377
420
  const fkCol = joinSchema.col(joinField);
378
- const cb = new ConditionBuilder('AND');
421
+ const cb = new ConditionBuilder('AND', db.cbDialect);
379
422
  cb.isIn(db.qi(fkCol), ids);
423
+ const tenantJoins = [];
424
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
380
425
  const where = cb.build(1, db.ph);
381
426
  const values = cb.getValues();
382
427
  const selection = ref?.selection ?? defaultSelection;
@@ -386,6 +431,7 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
386
431
  columns,
387
432
  where,
388
433
  values,
434
+ joins: tenantJoins.length > 0 ? tenantJoins : undefined,
389
435
  });
390
436
  result[alias] = mapRowsToCamelCase(rows, joinSchema);
391
437
  }
@@ -412,7 +458,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
412
458
  err400(`Unknown field: ${by}`);
413
459
  }
414
460
  // ─── joinGroup (aggregations) ───────────────────────────────
415
- async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup) {
461
+ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup, tenant) {
416
462
  const result = {};
417
463
  for (const [alias, groupReq] of Object.entries(joinGroup)) {
418
464
  const joinDef = requireJoin(tableConf, alias, false);
@@ -454,6 +500,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
454
500
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
455
501
  const fkCol = joinSchema.col(joinField);
456
502
  cb.isIn(db.qi(fkCol), ids);
503
+ const tenantJoins = [];
504
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
457
505
  let where = cb.build(1, db.ph);
458
506
  let values = cb.getValues();
459
507
  // Side-channel: computed-field clauses on the join table
@@ -462,7 +510,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
462
510
  ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
463
511
  }
464
512
  const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
465
- const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)} WHERE ${where} ${groupBy}`;
513
+ const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
514
+ const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
466
515
  const queryResult = await db.query(sql, values);
467
516
  const rows = queryResult.rows;
468
517
  const formatted = {};
@@ -520,8 +569,8 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
520
569
  }
521
570
  function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
522
571
  const cb = (ref.filters && joinTableConf)
523
- ? joinTableConf.filters(ref.filters)
524
- : new ConditionBuilder('AND');
572
+ ? joinTableConf.filters(ref.filters, db.cbDialect)
573
+ : new ConditionBuilder('AND', db.cbDialect);
525
574
  if (ref.conditions?.length) {
526
575
  for (const c of ref.conditions) {
527
576
  if (!ALLOWED_SET.has(c.method)) {
@@ -641,7 +690,7 @@ function appendComputedConditions(currentWhere, currentValues, clauses, db) {
641
690
  // Append the computed's own bound values first; the expr placeholders
642
691
  // (if any) reference them via stable indices set by the consumer.
643
692
  values = [...values, ...c.values];
644
- const tmpCb = new ConditionBuilder('AND');
693
+ const tmpCb = new ConditionBuilder('AND', db.cbDialect);
645
694
  dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
646
695
  const startIdx = values.length + 1;
647
696
  const clauseSql = tmpCb.build(startIdx, db.ph);
@@ -666,7 +715,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
666
715
  const [alias, fn, field] = parts;
667
716
  const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
668
717
  values = [...values, ...exprValues];
669
- const tmpCb = new ConditionBuilder('AND');
718
+ const tmpCb = new ConditionBuilder('AND', db.cbDialect);
670
719
  dispatchConditionMethod(tmpCb, c.method, expr, c.params);
671
720
  const startIdx = values.length + 1;
672
721
  const clause = tmpCb.build(startIdx, db.ph);
@@ -676,7 +725,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
676
725
  return { where, values };
677
726
  }
678
727
  // ─── joinMustExist (EXISTS subquery filter on main) ─────────
679
- function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues) {
728
+ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
680
729
  let where = currentWhere;
681
730
  const values = [...currentValues];
682
731
  for (const [alias, ref] of Object.entries(joinMustExist)) {
@@ -685,6 +734,8 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
685
734
  const joinTableConf = dbTables[joinSchema.tableName];
686
735
  const refs = extractJoinRefs(db, tableConf, joinDef);
687
736
  const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
737
+ const tenantJoins = [];
738
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
688
739
  const startIdx = values.length + 1;
689
740
  let filterWhere = filterCondition.build(startIdx, db.ph);
690
741
  let filterVals = [...filterCondition.getValues()];
@@ -699,10 +750,13 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
699
750
  filterVals = cvalues.slice(values.length);
700
751
  filterWhere = cwhere;
701
752
  }
753
+ // Qualify the FK with the join table: the EXISTS FROM may include the tenant
754
+ // through-join, so a bare column could become ambiguous.
702
755
  const innerWhere = filterWhere
703
- ? `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
704
- : `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
705
- where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable} WHERE ${innerWhere})`;
756
+ ? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
757
+ : `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
758
+ const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
759
+ where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
706
760
  values.push(...filterVals);
707
761
  }
708
762
  return { where, values };
@@ -724,7 +778,7 @@ export async function searchEngine(dbTables, params) {
724
778
  for (const a of Object.keys(joinLeft))
725
779
  requireJoin(tableConf, a, true);
726
780
  // Build main condition
727
- const condition = tableConf.filters(filters || {});
781
+ const condition = tableConf.filters(filters || {}, db.cbDialect);
728
782
  // Filters that target computed fields (declared on tableConf.computedFields)
729
783
  // are applied via the side-channel pattern further down.
730
784
  let computedFilterClauses = [];
@@ -755,7 +809,7 @@ export async function searchEngine(dbTables, params) {
755
809
  }
756
810
  // joinMustExist (EXISTS)
757
811
  if (joinMustExist && Object.keys(joinMustExist).length > 0) {
758
- ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values));
812
+ ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
759
813
  }
760
814
  // Aggregation conditions (HAVING-style)
761
815
  if (conditions?.length) {
@@ -764,27 +818,23 @@ export async function searchEngine(dbTables, params) {
764
818
  ({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
765
819
  }
766
820
  }
767
- // orderBy parsing — collects 2-parti aliases that need LEFT JOIN on the main query
768
- let safeOrderBy;
769
- let orderByValues = [];
770
- let orderByLeftAliases = new Set();
771
- if (orderBy) {
772
- const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + 1);
773
- safeOrderBy = obResult.sql;
774
- orderByValues = obResult.values;
775
- orderByLeftAliases = obResult.leftJoinAliases;
776
- }
777
821
  // Determine which joinLeft aliases need a LEFT JOIN on the main query:
778
822
  // - any alias used in 2-parti orderBy
779
823
  // - any alias in joinLeft body that has filters or conditions on the parent
780
- const aliasesNeedingLeftJoin = new Set(orderByLeftAliases);
824
+ const aliasesNeedingLeftJoin = new Set();
825
+ if (orderBy) {
826
+ for (const a of collectOrderByLeftAliases(orderBy, tableConf))
827
+ aliasesNeedingLeftJoin.add(a);
828
+ }
781
829
  if (joinLeft) {
782
830
  for (const [alias, ref] of Object.entries(joinLeft)) {
783
831
  if (ref?.filters || ref?.conditions?.length)
784
832
  aliasesNeedingLeftJoin.add(alias);
785
833
  }
786
834
  }
787
- // Build LEFT JOIN clauses + extra WHERE for filtered parents
835
+ // Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
836
+ // after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
837
+ // whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
788
838
  const extraJoinClauses = [...tenantJoins];
789
839
  let leftJoinValues = [];
790
840
  if (aliasesNeedingLeftJoin.size > 0) {
@@ -794,24 +844,34 @@ export async function searchEngine(dbTables, params) {
794
844
  for (const w of lj.whereExtras)
795
845
  where += ` AND ${w}`;
796
846
  }
847
+ // orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
848
+ // them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
849
+ // `values.length + 1` misbinds when BOTH an aggregation orderBy and a filtered joinLeft exist.
850
+ let safeOrderBy;
851
+ let orderByValues = [];
852
+ if (orderBy) {
853
+ const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
854
+ safeOrderBy = obResult.sql;
855
+ orderByValues = obResult.values;
856
+ }
797
857
  // Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
798
858
  // Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
799
859
  const whereAndJoinValues = [...values, ...leftJoinValues];
800
860
  const mainValues = [...whereAndJoinValues, ...orderByValues];
801
- const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed);
861
+ const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
802
862
  let pagination;
803
863
  if (paginator) {
804
864
  pagination = await buildPagination(db, tableConf, where, whereAndJoinValues, paginator, extraJoinClauses, computeMin, computeMax, computeSum, computeAvg);
805
865
  }
806
866
  const result = { main };
807
867
  if (joinMultiple && Object.keys(joinMultiple).length > 0) {
808
- result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple);
868
+ result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
809
869
  }
810
870
  if (joinLeft && Object.keys(joinLeft).length > 0) {
811
- result.joinLeft = await executeJoinLeft(db, tableConf, main, joinLeft);
871
+ result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
812
872
  }
813
873
  if (joinGroup && Object.keys(joinGroup).length > 0) {
814
- result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup);
874
+ result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
815
875
  }
816
876
  if (pagination) {
817
877
  result.pagination = pagination;
@@ -1,5 +1,5 @@
1
1
  import type { DbTables } from '../../types.js';
2
- export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
2
+ export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
3
3
  [x: string]: any;
4
4
  }>>;
5
5
  export declare function BulkDeleteTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;IAMxE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;;;IAU5E"}
1
+ {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;;IAS3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;;;IAU5E"}
@@ -1,10 +1,10 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
- export function BulkDeleteTableBody(dbTables, tableName) {
3
+ export function BulkDeleteTableBody(dbTables, tableName, maxItems) {
4
4
  const tableConf = dbTables[tableName];
5
5
  const pkField = primaryAsString(tableConf.primary);
6
6
  const pkType = tableConf.Schema.fields[pkField];
7
- return Type.Array(Type.Object({ [pkField]: pkType }));
7
+ return Type.Array(Type.Object({ [pkField]: pkType }, { additionalProperties: false }), maxItems != null ? { maxItems } : {});
8
8
  }
9
9
  export function BulkDeleteTableResponse(dbTables, tableName) {
10
10
  const tableConf = dbTables[tableName];
@@ -1,5 +1,5 @@
1
1
  import { type TSchema } from '@sinclair/typebox';
2
2
  import type { DbTables } from '../../types.js';
3
- export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
3
+ export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
4
4
  export declare function BulkUpsertTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
5
5
  //# sourceMappingURL=bulk-upsert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAgBxE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAoB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
@@ -1,18 +1,20 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
3
  import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
4
- export function BulkUpsertTableBody(dbTables, tableName) {
4
+ export function BulkUpsertTableBody(dbTables, tableName, maxItems) {
5
5
  const tableConf = dbTables[tableName];
6
6
  const schema = tableConf.Schema;
7
- const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf)));
7
+ // additionalProperties:false makes the schema the real write whitelist (no mass assignment).
8
+ const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf), { additionalProperties: false }));
8
9
  const itemProperties = {
9
10
  main: mainSchema,
10
11
  };
11
12
  attachWriteJoinSections(itemProperties, tableConf, dbTables, {
12
13
  withDeletions: true,
13
14
  secondaryFields: writeJoinBodyFields,
15
+ strictItems: true,
14
16
  });
15
- return Type.Array(Type.Object(itemProperties));
17
+ return Type.Array(Type.Object(itemProperties), maxItems != null ? { maxItems } : {});
16
18
  }
17
19
  export function BulkUpsertTableResponse(dbTables, tableName) {
18
20
  const tableConf = dbTables[tableName];
@@ -24,6 +24,7 @@ type JoinFieldsBuilder = (join: JoinDefinition, secondaryConf: ITable | undefine
24
24
  */
25
25
  export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables, build: JoinFieldsBuilder, options?: {
26
26
  itemsPartial?: boolean;
27
+ strictItems?: boolean;
27
28
  }): Record<string, TSchema>;
28
29
  /**
29
30
  * Attach `secondaries` (and optionally `deletions`) sections to the given target object,
@@ -37,6 +38,7 @@ export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables,
37
38
  export declare function attachWriteJoinSections(target: Record<string, TSchema>, tableConf: ITable, dbTables: DbTables, options: {
38
39
  withDeletions: boolean;
39
40
  secondaryFields: JoinFieldsBuilder;
41
+ strictItems?: boolean;
40
42
  }): void;
41
43
  /** JoinFieldsBuilder for body schemas: includes all join fields with FK + excluded made Optional. */
42
44
  export declare const writeJoinBodyFields: JoinFieldsBuilder;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAA;CAAE,GACtE,IAAI,CAmBN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,IAAI,CAmBN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
@@ -56,9 +56,10 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
56
56
  const out = {};
57
57
  if (!tableConf.allowedWriteJoins?.length)
58
58
  return out;
59
+ const objOpts = options.strictItems ? { additionalProperties: false } : {};
59
60
  for (const j of tableConf.allowedWriteJoins) {
60
61
  const sc = findSecondaryTableConf(dbTables, j.joinSchema.tableName);
61
- const item = Type.Object(build(j, sc));
62
+ const item = Type.Object(build(j, sc), objOpts);
62
63
  out[j.alias] = Type.Array(options.itemsPartial ? Type.Partial(item) : item);
63
64
  }
64
65
  return out;
@@ -75,7 +76,7 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
75
76
  export function attachWriteJoinSections(target, tableConf, dbTables, options) {
76
77
  if (!tableConf.allowedWriteJoins?.length)
77
78
  return;
78
- target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields))));
79
+ target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields, { strictItems: options.strictItems }))));
79
80
  if (options.withDeletions) {
80
81
  target.deletions = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, ({ joinSchema }) => joinSchema.fields, { itemsPartial: true }))));
81
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -12,11 +12,14 @@ export function InsertTableBody(dbTables, tableName) {
12
12
  }
13
13
  }
14
14
  const bodyProperties = {
15
- main: Type.Object(mainFields),
15
+ // additionalProperties:false makes the schema the real write whitelist — unknown keys are
16
+ // rejected (400) instead of silently reaching the INSERT as columns (mass assignment).
17
+ main: Type.Object(mainFields, { additionalProperties: false }),
16
18
  };
17
19
  attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
18
20
  withDeletions: false,
19
21
  secondaryFields: writeJoinBodyFields,
22
+ strictItems: true,
20
23
  });
21
24
  return Type.Object(bodyProperties);
22
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAqB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAwB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}