fastify-auto-sqlapi 0.1.1 → 0.1.3

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 (73) hide show
  1. package/AGENTS_BACKEND.md +35 -1
  2. package/README.md +65 -4
  3. package/dist/bin/generate-schema.js +2 -2
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
  7. package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
  8. package/dist/lib/adapters/mysql-adapter.js +28 -17
  9. package/dist/lib/adapters/pg-adapter.d.ts +4 -0
  10. package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
  11. package/dist/lib/adapters/pg-adapter.js +18 -7
  12. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  13. package/dist/lib/cli/mysql-introspect.js +3 -1
  14. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/pg-introspect.js +13 -4
  16. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  17. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  18. package/dist/lib/cli/schema-codegen.js +17 -3
  19. package/dist/lib/cli/tables-codegen.d.ts +2 -0
  20. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/tables-codegen.js +43 -12
  22. package/dist/lib/db.d.ts +17 -0
  23. package/dist/lib/db.d.ts.map +1 -1
  24. package/dist/lib/db.js +47 -0
  25. package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
  26. package/dist/lib/engine/bulk/bulk-delete.js +33 -10
  27. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  28. package/dist/lib/engine/bulk/bulk-upsert.js +9 -8
  29. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  30. package/dist/lib/engine/rest/delete.js +18 -7
  31. package/dist/lib/engine/rest/get.js +1 -1
  32. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/insert.js +38 -33
  34. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/update.js +69 -55
  36. package/dist/lib/engine/search/search.d.ts.map +1 -1
  37. package/dist/lib/engine/search/search.js +49 -19
  38. package/dist/lib/sql-api.js +2 -2
  39. package/dist/lib/table-helpers.d.ts.map +1 -1
  40. package/dist/lib/table-helpers.js +10 -9
  41. package/dist/lib/tenant.d.ts +9 -2
  42. package/dist/lib/tenant.d.ts.map +1 -1
  43. package/dist/lib/tenant.js +31 -8
  44. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  45. package/dist/routes/auto/bulk-delete.routes.js +1 -0
  46. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  47. package/dist/routes/auto/bulk-upsert.routes.js +1 -0
  48. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  49. package/dist/routes/auto/delete.routes.js +1 -0
  50. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  51. package/dist/routes/auto/get.routes.js +1 -0
  52. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  53. package/dist/routes/auto/insert.routes.js +1 -0
  54. package/dist/routes/auto/plugin.d.ts.map +1 -1
  55. package/dist/routes/auto/plugin.js +40 -5
  56. package/dist/routes/auto/route-helpers.d.ts +3 -1
  57. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  58. package/dist/routes/auto/route-helpers.js +3 -0
  59. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  60. package/dist/routes/auto/search.routes.js +1 -0
  61. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  62. package/dist/routes/auto/update.routes.js +1 -0
  63. package/dist/types/cli.d.ts +6 -0
  64. package/dist/types/cli.d.ts.map +1 -1
  65. package/dist/types/db.d.ts +11 -0
  66. package/dist/types/db.d.ts.map +1 -1
  67. package/dist/types/dml.d.ts +2 -0
  68. package/dist/types/dml.d.ts.map +1 -1
  69. package/dist/types/schema.d.ts +2 -0
  70. package/dist/types/schema.d.ts.map +1 -1
  71. package/dist/types/table.d.ts +64 -2
  72. package/dist/types/table.d.ts.map +1 -1
  73. package/package.json +1 -1
@@ -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
  }
@@ -219,7 +234,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
219
234
  // Note: extraFilters declared on joinTableConf are not supported here for
220
235
  // joinLeft (they would require alias-aware handlers); only schema fields
221
236
  // and computed fields apply.
222
- const cb = new ConditionBuilder('AND');
237
+ const cb = new ConditionBuilder('AND', db.cbDialect);
223
238
  const joinTableConf = dbTables[joinSchema.tableName];
224
239
  const computed = joinTableConf?.computedFields;
225
240
  if (ref.filters) {
@@ -328,7 +343,7 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
328
343
  };
329
344
  }
330
345
  // ─── joinMultiple (virtual fetch, child rows in side query) ─
331
- async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple) {
346
+ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
332
347
  const result = {};
333
348
  for (const [alias, ref] of Object.entries(joinMultiple)) {
334
349
  const joinDef = requireJoin(tableConf, alias, false);
@@ -342,6 +357,8 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
342
357
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
343
358
  const fkCol = joinSchema.col(joinField);
344
359
  cb.isIn(db.qi(fkCol), ids);
360
+ const tenantJoins = [];
361
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
345
362
  let where = cb.build(1, db.ph);
346
363
  let values = cb.getValues();
347
364
  // Side-channel: computed-field clauses on the join table
@@ -356,17 +373,19 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
356
373
  columns,
357
374
  where,
358
375
  values,
376
+ joins: tenantJoins.length > 0 ? tenantJoins : undefined,
359
377
  });
360
378
  result[alias] = mapRowsToCamelCase(rows, joinSchema);
361
379
  }
362
380
  return result;
363
381
  }
364
382
  // ─── joinLeft (parent fetch via PK IN side query) ───────────
365
- async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
383
+ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
366
384
  const result = {};
367
385
  for (const [alias, ref] of Object.entries(joinLeft)) {
368
386
  const joinDef = requireJoin(tableConf, alias, true);
369
387
  const { joinSchema, joinField, mainField, selection: defaultSelection } = joinDef;
388
+ const joinTableConf = dbTables[joinSchema.tableName];
370
389
  // For joinLeft (N:1), mainField on main is the FK pointing to joinField (PK) on parent.
371
390
  // We collect the FK values from the main results and look up parents by their PK.
372
391
  const ids = collectIds(mainResults, mainField);
@@ -375,8 +394,10 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
375
394
  continue;
376
395
  }
377
396
  const fkCol = joinSchema.col(joinField);
378
- const cb = new ConditionBuilder('AND');
397
+ const cb = new ConditionBuilder('AND', db.cbDialect);
379
398
  cb.isIn(db.qi(fkCol), ids);
399
+ const tenantJoins = [];
400
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
380
401
  const where = cb.build(1, db.ph);
381
402
  const values = cb.getValues();
382
403
  const selection = ref?.selection ?? defaultSelection;
@@ -386,6 +407,7 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
386
407
  columns,
387
408
  where,
388
409
  values,
410
+ joins: tenantJoins.length > 0 ? tenantJoins : undefined,
389
411
  });
390
412
  result[alias] = mapRowsToCamelCase(rows, joinSchema);
391
413
  }
@@ -412,7 +434,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
412
434
  err400(`Unknown field: ${by}`);
413
435
  }
414
436
  // ─── joinGroup (aggregations) ───────────────────────────────
415
- async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup) {
437
+ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup, tenant) {
416
438
  const result = {};
417
439
  for (const [alias, groupReq] of Object.entries(joinGroup)) {
418
440
  const joinDef = requireJoin(tableConf, alias, false);
@@ -454,6 +476,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
454
476
  const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
455
477
  const fkCol = joinSchema.col(joinField);
456
478
  cb.isIn(db.qi(fkCol), ids);
479
+ const tenantJoins = [];
480
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
457
481
  let where = cb.build(1, db.ph);
458
482
  let values = cb.getValues();
459
483
  // Side-channel: computed-field clauses on the join table
@@ -462,7 +486,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
462
486
  ({ where, values } = appendComputedConditions(where, values, computedClauses, db));
463
487
  }
464
488
  const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
465
- const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)} WHERE ${where} ${groupBy}`;
489
+ const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
490
+ const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
466
491
  const queryResult = await db.query(sql, values);
467
492
  const rows = queryResult.rows;
468
493
  const formatted = {};
@@ -520,8 +545,8 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
520
545
  }
521
546
  function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
522
547
  const cb = (ref.filters && joinTableConf)
523
- ? joinTableConf.filters(ref.filters)
524
- : new ConditionBuilder('AND');
548
+ ? joinTableConf.filters(ref.filters, db.cbDialect)
549
+ : new ConditionBuilder('AND', db.cbDialect);
525
550
  if (ref.conditions?.length) {
526
551
  for (const c of ref.conditions) {
527
552
  if (!ALLOWED_SET.has(c.method)) {
@@ -641,7 +666,7 @@ function appendComputedConditions(currentWhere, currentValues, clauses, db) {
641
666
  // Append the computed's own bound values first; the expr placeholders
642
667
  // (if any) reference them via stable indices set by the consumer.
643
668
  values = [...values, ...c.values];
644
- const tmpCb = new ConditionBuilder('AND');
669
+ const tmpCb = new ConditionBuilder('AND', db.cbDialect);
645
670
  dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
646
671
  const startIdx = values.length + 1;
647
672
  const clauseSql = tmpCb.build(startIdx, db.ph);
@@ -666,7 +691,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
666
691
  const [alias, fn, field] = parts;
667
692
  const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
668
693
  values = [...values, ...exprValues];
669
- const tmpCb = new ConditionBuilder('AND');
694
+ const tmpCb = new ConditionBuilder('AND', db.cbDialect);
670
695
  dispatchConditionMethod(tmpCb, c.method, expr, c.params);
671
696
  const startIdx = values.length + 1;
672
697
  const clause = tmpCb.build(startIdx, db.ph);
@@ -676,7 +701,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
676
701
  return { where, values };
677
702
  }
678
703
  // ─── joinMustExist (EXISTS subquery filter on main) ─────────
679
- function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues) {
704
+ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
680
705
  let where = currentWhere;
681
706
  const values = [...currentValues];
682
707
  for (const [alias, ref] of Object.entries(joinMustExist)) {
@@ -685,6 +710,8 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
685
710
  const joinTableConf = dbTables[joinSchema.tableName];
686
711
  const refs = extractJoinRefs(db, tableConf, joinDef);
687
712
  const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
713
+ const tenantJoins = [];
714
+ appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
688
715
  const startIdx = values.length + 1;
689
716
  let filterWhere = filterCondition.build(startIdx, db.ph);
690
717
  let filterVals = [...filterCondition.getValues()];
@@ -699,10 +726,13 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
699
726
  filterVals = cvalues.slice(values.length);
700
727
  filterWhere = cwhere;
701
728
  }
729
+ // Qualify the FK with the join table: the EXISTS FROM may include the tenant
730
+ // through-join, so a bare column could become ambiguous.
702
731
  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})`;
732
+ ? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
733
+ : `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
734
+ const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
735
+ where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
706
736
  values.push(...filterVals);
707
737
  }
708
738
  return { where, values };
@@ -724,7 +754,7 @@ export async function searchEngine(dbTables, params) {
724
754
  for (const a of Object.keys(joinLeft))
725
755
  requireJoin(tableConf, a, true);
726
756
  // Build main condition
727
- const condition = tableConf.filters(filters || {});
757
+ const condition = tableConf.filters(filters || {}, db.cbDialect);
728
758
  // Filters that target computed fields (declared on tableConf.computedFields)
729
759
  // are applied via the side-channel pattern further down.
730
760
  let computedFilterClauses = [];
@@ -755,7 +785,7 @@ export async function searchEngine(dbTables, params) {
755
785
  }
756
786
  // joinMustExist (EXISTS)
757
787
  if (joinMustExist && Object.keys(joinMustExist).length > 0) {
758
- ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values));
788
+ ({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
759
789
  }
760
790
  // Aggregation conditions (HAVING-style)
761
791
  if (conditions?.length) {
@@ -805,13 +835,13 @@ export async function searchEngine(dbTables, params) {
805
835
  }
806
836
  const result = { main };
807
837
  if (joinMultiple && Object.keys(joinMultiple).length > 0) {
808
- result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple);
838
+ result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
809
839
  }
810
840
  if (joinLeft && Object.keys(joinLeft).length > 0) {
811
- result.joinLeft = await executeJoinLeft(db, tableConf, main, joinLeft);
841
+ result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
812
842
  }
813
843
  if (joinGroup && Object.keys(joinGroup).length > 0) {
814
- result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup);
844
+ result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
815
845
  }
816
846
  if (pagination) {
817
847
  result.pagination = pagination;
@@ -99,7 +99,7 @@ export class SqlApi {
99
99
  async delete(tableName, id, request) {
100
100
  const tableConf = this.getTableConf(tableName);
101
101
  const tenant = await this.getTenant(tableName, request);
102
- return deleteEngine({ db: this.db, tableConf, id, tenant });
102
+ return deleteEngine({ db: this.db, tableConf, id, tenant, request });
103
103
  }
104
104
  async bulkUpsert(tableName, items, request) {
105
105
  const tableConf = this.getTableConf(tableName);
@@ -116,7 +116,7 @@ export class SqlApi {
116
116
  async bulkDelete(tableName, ids, request) {
117
117
  const tableConf = this.getTableConf(tableName);
118
118
  const tenant = await this.getTenant(tableName, request);
119
- return bulkDeleteEngine({ db: this.db, tableConf, ids, tenant });
119
+ return bulkDeleteEngine({ db: this.db, tableConf, ids, tenant, request });
120
120
  }
121
121
  }
122
122
  export function createSqlApi(dbOrPool, dbTables, options) {
@@ -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;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,CAKX;AA2CD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;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,EAAiC,MAAM,wBAAwB,CAAC;AACzH,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;AAarB,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,CAKX;AA2CD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;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,19 +1,20 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
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);
3
+ // Quote an identifier using the given ConditionBuilder dialect (or the global
4
+ // default when not provided). Needed so that DB columns with uppercase letters
5
+ // (e.g. betterauth "userId") are preserved on PostgreSQL (which folds unquoted
6
+ // identifiers to lowercase) and on MySQL (which is case-sensitive on Linux
7
+ // filesystems).
8
+ function qi(field, dialect) {
9
+ return getDialect(dialect ?? ConditionBuilder.DIALECT).qi(field);
9
10
  }
10
11
  export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
11
- const filters = (filterValues) => {
12
- const condition = new ConditionBuilder('AND');
12
+ const filters = (filterValues, dialect) => {
13
+ const condition = new ConditionBuilder('AND', dialect);
13
14
  // Only auto-match real schema fields (DB columns)
14
15
  for (const field of Object.keys(Schema.fields)) {
15
16
  if (field in filterValues && filterValues[field] !== null && filterValues[field] !== undefined) {
16
- condition.isEqual(qi(Schema.col(field)), filterValues[field]);
17
+ condition.isEqual(qi(Schema.col(field), dialect), filterValues[field]);
17
18
  }
18
19
  }
19
20
  // extraFilters are handled exclusively by extendedCondition
@@ -32,11 +32,18 @@ export declare function enforceTenantOnWrites(db: QueryClient, tenant: TenantCon
32
32
  * Returns undefined for indirect or no-tenant scenarios — those are handled separately
33
33
  * (indirect uses pre-check via `assertTenantOwnership`).
34
34
  */
35
- export declare function buildTenantUpdateExtra(tenant: TenantContext | undefined): ConditionBuilder | undefined;
35
+ export declare function buildTenantUpdateExtra(db: QueryClient, tenant: TenantContext | undefined): ConditionBuilder | undefined;
36
36
  /**
37
37
  * For update with indirect tenant: pre-check ownership via `SELECT 1 ... INNER JOIN ... LIMIT 1`.
38
38
  * Throws 404 if the record either does not exist or does not belong to the tenant.
39
39
  * No-op for direct or no-tenant — those are enforced via `extraCondition` in the UPDATE itself.
40
40
  */
41
- export declare function assertTenantOwnership(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string, pkValue: ConditionValue): Promise<void>;
41
+ export declare function assertTenantOwnership(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string | string[], pkValue: ConditionValue | ConditionValue[]): Promise<void>;
42
+ /**
43
+ * Assert that EVERY pk in `ids` exists and belongs to the tenant, for both direct and indirect
44
+ * scopes. Throws 404 if any id is missing or not owned. No-op when `tenant` is undefined or `ids`
45
+ * is empty. Used by the delete engines to enforce tenant ownership BEFORE running a `beforeDelete`/
46
+ * `beforeBulkDelete` hook, so user code never runs against rows the caller cannot access.
47
+ */
48
+ export declare function assertTenantOwnsAll(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string, ids: ConditionValue[]): Promise<void>;
42
49
  //# sourceMappingURL=tenant.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
@@ -24,7 +24,7 @@ export function buildTenantCondition(db, scope, tenantIds) {
24
24
  else {
25
25
  qualifier = db.qi(col);
26
26
  }
27
- const cb = new ConditionBuilder('AND');
27
+ const cb = new ConditionBuilder('AND', db.cbDialect);
28
28
  cb.isIn(qualifier, tenantIds);
29
29
  return cb;
30
30
  }
@@ -59,7 +59,7 @@ export async function validateTenantFK(db, scope, tenantIds, fkValues) {
59
59
  const throughTable = scope.through.schema.tableName;
60
60
  const foreignField = scope.through.foreignField;
61
61
  const tenantCol = scope.column;
62
- const cb = new ConditionBuilder('AND');
62
+ const cb = new ConditionBuilder('AND', db.cbDialect);
63
63
  cb.isIn(db.qi(foreignField), uniqueFKs);
64
64
  cb.isNotIn(db.qi(tenantCol), tenantIds);
65
65
  const where = cb.build(1, db.ph);
@@ -77,7 +77,7 @@ export async function validateTenantFK(db, scope, tenantIds, fkValues) {
77
77
  */
78
78
  export function buildTenantDeleteWhere(db, tableName, pkCol, pkValue, tenant) {
79
79
  if (isIndirect(tenant.scope)) {
80
- const innerCb = new ConditionBuilder('AND');
80
+ const innerCb = new ConditionBuilder('AND', db.cbDialect);
81
81
  innerCb.isIn(`${db.qi(tableName)}.${db.qi(pkCol)}`, (Array.isArray(pkValue) ? pkValue : [pkValue]));
82
82
  innerCb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
83
83
  const innerWhere = innerCb.build(1, db.ph);
@@ -86,7 +86,7 @@ export function buildTenantDeleteWhere(db, tableName, pkCol, pkValue, tenant) {
86
86
  const where = `${db.qi(pkCol)} IN (SELECT ${db.qi(tableName)}.${db.qi(pkCol)} FROM ${db.qi(tableName)} ${joinSql} WHERE ${innerWhere})`;
87
87
  return { where, values };
88
88
  }
89
- const cb = new ConditionBuilder('AND');
89
+ const cb = new ConditionBuilder('AND', db.cbDialect);
90
90
  if (Array.isArray(pkValue)) {
91
91
  cb.isIn(db.qi(pkCol), pkValue);
92
92
  }
@@ -129,10 +129,10 @@ export async function enforceTenantOnWrites(db, tenant, records) {
129
129
  * Returns undefined for indirect or no-tenant scenarios — those are handled separately
130
130
  * (indirect uses pre-check via `assertTenantOwnership`).
131
131
  */
132
- export function buildTenantUpdateExtra(tenant) {
132
+ export function buildTenantUpdateExtra(db, tenant) {
133
133
  if (!tenant || isIndirect(tenant.scope))
134
134
  return undefined;
135
- const cb = new ConditionBuilder('AND');
135
+ const cb = new ConditionBuilder('AND', db.cbDialect);
136
136
  if (tenant.ids.length === 1)
137
137
  cb.isEqual(tenant.scope.column, tenant.ids[0]);
138
138
  else
@@ -147,8 +147,10 @@ export function buildTenantUpdateExtra(tenant) {
147
147
  export async function assertTenantOwnership(db, tenant, tableName, pkCol, pkValue) {
148
148
  if (!tenant || !isIndirect(tenant.scope))
149
149
  return;
150
- const cb = new ConditionBuilder('AND');
151
- cb.isEqual(`${db.qi(tableName)}.${db.qi(pkCol)}`, pkValue);
150
+ const cb = new ConditionBuilder('AND', db.cbDialect);
151
+ const cols = Array.isArray(pkCol) ? pkCol : [pkCol];
152
+ const vals = Array.isArray(pkValue) ? pkValue : [pkValue];
153
+ cols.forEach((c, i) => cb.isEqual(`${db.qi(tableName)}.${db.qi(c)}`, vals[i]));
152
154
  cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
153
155
  const sql = `SELECT 1 FROM ${db.qi(tableName)} ${buildTenantJoin(db, tenant.scope, tableName)} WHERE ${cb.build(1, db.ph)} LIMIT 1`;
154
156
  const r = await db.query(sql, cb.getValues());
@@ -156,3 +158,24 @@ export async function assertTenantOwnership(db, tenant, tableName, pkCol, pkValu
156
158
  throw httpError(404, 'Record not found');
157
159
  }
158
160
  }
161
+ /**
162
+ * Assert that EVERY pk in `ids` exists and belongs to the tenant, for both direct and indirect
163
+ * scopes. Throws 404 if any id is missing or not owned. No-op when `tenant` is undefined or `ids`
164
+ * is empty. Used by the delete engines to enforce tenant ownership BEFORE running a `beforeDelete`/
165
+ * `beforeBulkDelete` hook, so user code never runs against rows the caller cannot access.
166
+ */
167
+ export async function assertTenantOwnsAll(db, tenant, tableName, pkCol, ids) {
168
+ if (!tenant || !ids.length)
169
+ return;
170
+ const qualifiedPk = `${db.qi(tableName)}.${db.qi(pkCol)}`;
171
+ const cb = new ConditionBuilder('AND', db.cbDialect);
172
+ cb.isIn(qualifiedPk, ids);
173
+ cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
174
+ const join = isIndirect(tenant.scope) ? ` ${buildTenantJoin(db, tenant.scope, tableName)}` : '';
175
+ const sql = `SELECT DISTINCT ${qualifiedPk} AS pk FROM ${db.qi(tableName)}${join} WHERE ${cb.build(1, db.ph)}`;
176
+ const r = await db.query(sql, cb.getValues());
177
+ const uniqueRequested = new Set(ids.map((v) => String(v))).size;
178
+ if (r.rows.length < uniqueRequested) {
179
+ throw httpError(404, 'Record not found');
180
+ }
181
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
1
+ {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -3,6 +3,7 @@ import { primaryAsString } from '../../types.js';
3
3
  import { registerForAllTables } from './route-helpers.js';
4
4
  export default async function bulkDeleteRoutes(fastify, options) {
5
5
  await registerForAllTables(fastify, options, {
6
+ operation: 'bulkDelete',
6
7
  method: 'POST',
7
8
  url: (tc) => `/bulk/${tc.Schema.tableName}/delete`,
8
9
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -2,6 +2,7 @@ import { BulkUpsertTableBody, BulkUpsertTableResponse } from '../../lib/schema/b
2
2
  import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
3
3
  export default async function bulkUpsertRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
+ operation: 'bulkUpsert',
5
6
  method: 'PUT',
6
7
  url: (tc) => `/bulk/${tc.Schema.tableName}`,
7
8
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
1
+ {"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -3,6 +3,7 @@ import { primaryAsString } from '../../types.js';
3
3
  import { registerForAllTables } from './route-helpers.js';
4
4
  export default async function deleteRoutes(fastify, options) {
5
5
  await registerForAllTables(fastify, options, {
6
+ operation: 'delete',
6
7
  method: 'DELETE',
7
8
  url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
8
9
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -2,6 +2,7 @@ import { Type } from '@sinclair/typebox';
2
2
  import { registerForAllTables } from './route-helpers.js';
3
3
  export default async function getRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
+ operation: 'get',
5
6
  method: 'GET',
6
7
  url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
7
8
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsBf"}
1
+ {"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAuBf"}
@@ -2,6 +2,7 @@ import { InsertTableBody, InsertTableResponse } from '../../lib/schema/insert.js
2
2
  import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
3
3
  export default async function insertRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
+ operation: 'insert',
5
6
  method: 'POST',
6
7
  url: (tc) => `/rest/${tc.Schema.tableName}`,
7
8
  successStatus: 201,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;kCAGU,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBAqFoC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;kCAuDU,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBAiFoC"}
@@ -13,6 +13,45 @@ import updateRoutes from './update.routes.js';
13
13
  import deleteRoutes from './delete.routes.js';
14
14
  import bulkUpsertRoutes from './bulk-upsert.routes.js';
15
15
  import bulkDeleteRoutes from './bulk-delete.routes.js';
16
+ // Schema combinators for which Ajv reports one error per failed branch plus a
17
+ // single combinator-level summary error, all targeting the same path.
18
+ const COMBINATOR_KEYWORDS = new Set(['anyOf', 'oneOf']);
19
+ /**
20
+ * Converts Ajv validation errors into response fields, stripping the noise Ajv
21
+ * emits for schema combinators.
22
+ *
23
+ * An enum modelled as `anyOf` of `const`s fails every branch when invalid, so Ajv
24
+ * produces one `const` error per branch plus an `anyOf` error — all on the same
25
+ * path. We keep only the combinator error for those paths and drop the per-branch
26
+ * errors, then deduplicate any remaining identical entries. `required` errors are
27
+ * re-pointed from the parent object to the actually missing property.
28
+ */
29
+ function normalizeAjvErrors(errors) {
30
+ const toPath = (v) => {
31
+ const path = (v.instancePath || '').replace(/\//g, '.').replace(/^\./, '');
32
+ const missing = v.params?.missingProperty;
33
+ if (v.keyword === 'required' && missing) {
34
+ return path ? `${path}.${missing}` : missing;
35
+ }
36
+ return path;
37
+ };
38
+ const mapped = errors.map((v) => ({
39
+ path: toPath(v),
40
+ message: v.message || 'invalid',
41
+ code: v.keyword || 'unknown',
42
+ }));
43
+ const combinatorPaths = new Set(mapped.filter((f) => COMBINATOR_KEYWORDS.has(f.code)).map((f) => f.path));
44
+ const seen = new Set();
45
+ return mapped.filter((f) => {
46
+ if (combinatorPaths.has(f.path) && !COMBINATOR_KEYWORDS.has(f.code))
47
+ return false;
48
+ const key = `${f.path}|${f.code}|${f.message}`;
49
+ if (seen.has(key))
50
+ return false;
51
+ seen.add(key);
52
+ return true;
53
+ });
54
+ }
16
55
  export default fp(async function fastifyAutoSqlApi(fastify, options) {
17
56
  // Set ConditionBuilder dialect globally
18
57
  const dialect = getDialect(options.dialect || 'postgres');
@@ -49,11 +88,7 @@ export default fp(async function fastifyAutoSqlApi(fastify, options) {
49
88
  instance.setErrorHandler((error, request, reply) => {
50
89
  // Schema validation errors (Ajv)
51
90
  if (error.validation) {
52
- const fields = error.validation.map((v) => ({
53
- path: (v.instancePath || '').replace(/\//g, '.').replace(/^\./, ''),
54
- message: v.message || 'invalid',
55
- code: v.keyword || 'unknown',
56
- }));
91
+ const fields = normalizeAjvErrors(error.validation);
57
92
  return reply.status(400).send({
58
93
  statusCode: 400,
59
94
  error: 'Bad Request',
@@ -1,5 +1,5 @@
1
1
  import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
- import type { ITable, SqlApiPluginOptions, DbTables } from '../../types.js';
2
+ import type { ITable, SqlApiPluginOptions, DbTables, TableOperation } from '../../types.js';
3
3
  type RequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>;
4
4
  export declare function mergeOnRequests(options: SqlApiPluginOptions, tableConf: ITable): RequestHook[];
5
5
  export declare function buildWriteDescription(action: string, tableName: string, tableConf: ITable): string;
@@ -10,6 +10,8 @@ interface RouteSchema {
10
10
  response?: any;
11
11
  }
12
12
  export interface AutoRouteSpec {
13
+ /** Operation key matched against `ITable.operations` to decide whether to register the route. */
14
+ operation: TableOperation;
13
15
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
14
16
  /** URL builder receiving the table configuration to read `Schema.tableName`. */
15
17
  url: (tableConf: ITable) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE5E,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -18,6 +18,9 @@ export function buildWriteDescription(action, tableName, tableConf) {
18
18
  export async function registerForAllTables(fastify, options, spec) {
19
19
  const { DbTables } = options;
20
20
  for (const [tableName, tableConf] of Object.entries(DbTables)) {
21
+ // operations acts as a whitelist; when omitted every operation is exposed.
22
+ if (tableConf.operations && !tableConf.operations.includes(spec.operation))
23
+ continue;
21
24
  const schemas = spec.schemas(DbTables, tableName, tableConf);
22
25
  // Only include schema keys that are actually defined: Fastify checks for key
23
26
  // *presence* (not undefined value) to decide whether to emit FSTWRN001.
@@ -1 +1 @@
1
- {"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiDf"}
1
+ {"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -2,6 +2,7 @@ import { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from
2
2
  import { registerForAllTables } from './route-helpers.js';
3
3
  export default async function searchRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
+ operation: 'search',
5
6
  method: 'POST',
6
7
  url: (tc) => `/search/${tc.Schema.tableName}`,
7
8
  successStatus: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"update.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/update.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAwBf"}
1
+ {"version":3,"file":"update.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/update.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -2,6 +2,7 @@ import { UpdateTableBody, UpdateTableResponse } from '../../lib/schema/update.js
2
2
  import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
3
3
  export default async function updateRoutes(fastify, options) {
4
4
  await registerForAllTables(fastify, options, {
5
+ operation: 'update',
5
6
  method: 'PUT',
6
7
  url: (tc) => `/rest/${tc.Schema.tableName}`,
7
8
  successStatus: 200,
@@ -11,12 +11,18 @@ export interface ColumnInfo {
11
11
  udt_name: string;
12
12
  column_default: string | null;
13
13
  is_nullable: string;
14
+ /** True when the column is part of the table's PRIMARY KEY (from information_schema). */
15
+ is_primary?: boolean;
16
+ /** True for auto-generated columns without a visible default (mysql AUTO_INCREMENT). */
17
+ is_auto_increment?: boolean;
14
18
  }
15
19
  export interface TableMap {
16
20
  [schemaName: string]: {
17
21
  name: string;
18
22
  fields: Record<string, string>;
19
23
  colMap: Record<string, string>;
24
+ /** PRIMARY KEY fields (camelCase), in table column order. Empty when unknown. */
25
+ primary: string[];
20
26
  };
21
27
  }
22
28
  //# sourceMappingURL=cli.d.ts.map