fastify-auto-sqlapi 0.0.5 → 0.1.1

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 (119) hide show
  1. package/AGENTS.md +15 -2
  2. package/AGENTS_BACKEND.md +144 -20
  3. package/AGENTS_FRONTEND.md +161 -67
  4. package/README.md +47 -26
  5. package/dist/bin/generate-schema.js +9 -26
  6. package/dist/bin/generate-tables.js +7 -30
  7. package/dist/bin/utils.d.ts +28 -0
  8. package/dist/bin/utils.d.ts.map +1 -1
  9. package/dist/bin/utils.js +69 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/lib/cli/load-dependency.d.ts +8 -0
  13. package/dist/lib/cli/load-dependency.d.ts.map +1 -0
  14. package/dist/lib/cli/load-dependency.js +21 -0
  15. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/mysql-introspect.js +2 -18
  17. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  18. package/dist/lib/cli/pg-introspect.js +2 -17
  19. package/dist/lib/db.d.ts +1 -0
  20. package/dist/lib/db.d.ts.map +1 -1
  21. package/dist/lib/db.js +33 -55
  22. package/dist/lib/dialect.d.ts +17 -0
  23. package/dist/lib/dialect.d.ts.map +1 -1
  24. package/dist/lib/dialect.js +32 -0
  25. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  26. package/dist/lib/engine/bulk/bulk-upsert.js +15 -43
  27. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  28. package/dist/lib/engine/rest/delete.js +5 -10
  29. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  30. package/dist/lib/engine/rest/get.js +3 -5
  31. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  32. package/dist/lib/engine/rest/insert.js +11 -31
  33. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/update.js +13 -46
  35. package/dist/lib/engine/search/search.d.ts.map +1 -1
  36. package/dist/lib/engine/search/search.js +553 -229
  37. package/dist/lib/engine/write-helpers.d.ts +39 -2
  38. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  39. package/dist/lib/engine/write-helpers.js +55 -12
  40. package/dist/lib/errors.d.ts +11 -0
  41. package/dist/lib/errors.d.ts.map +1 -0
  42. package/dist/lib/errors.js +12 -0
  43. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  44. package/dist/lib/schema/bulk-upsert.js +9 -25
  45. package/dist/lib/schema/helpers.d.ts +28 -1
  46. package/dist/lib/schema/helpers.d.ts.map +1 -1
  47. package/dist/lib/schema/helpers.js +38 -0
  48. package/dist/lib/schema/insert.d.ts.map +1 -1
  49. package/dist/lib/schema/insert.js +9 -19
  50. package/dist/lib/schema/search.d.ts.map +1 -1
  51. package/dist/lib/schema/search.js +138 -50
  52. package/dist/lib/schema/update.d.ts.map +1 -1
  53. package/dist/lib/schema/update.js +9 -25
  54. package/dist/lib/sql-api.d.ts +6 -6
  55. package/dist/lib/sql-api.d.ts.map +1 -1
  56. package/dist/lib/sql-api.js +8 -8
  57. package/dist/lib/table-helpers.d.ts +6 -1
  58. package/dist/lib/table-helpers.d.ts.map +1 -1
  59. package/dist/lib/table-helpers.js +42 -2
  60. package/dist/lib/tenant.d.ts +23 -0
  61. package/dist/lib/tenant.d.ts.map +1 -1
  62. package/dist/lib/tenant.js +57 -9
  63. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  64. package/dist/routes/auto/bulk-delete.routes.js +17 -23
  65. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  66. package/dist/routes/auto/bulk-upsert.routes.js +16 -22
  67. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  68. package/dist/routes/auto/delete.routes.js +18 -24
  69. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  70. package/dist/routes/auto/get.routes.js +16 -23
  71. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  72. package/dist/routes/auto/insert.routes.js +19 -26
  73. package/dist/routes/auto/route-helpers.d.ts +27 -2
  74. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  75. package/dist/routes/auto/route-helpers.js +39 -1
  76. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  77. package/dist/routes/auto/search.routes.js +47 -47
  78. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  79. package/dist/routes/auto/update.routes.js +20 -27
  80. package/dist/types/cli.d.ts +22 -0
  81. package/dist/types/cli.d.ts.map +1 -0
  82. package/dist/types/cli.js +1 -0
  83. package/dist/types/computed.d.ts +29 -0
  84. package/dist/types/computed.d.ts.map +1 -0
  85. package/dist/types/computed.js +1 -0
  86. package/dist/types/conditions.d.ts +16 -0
  87. package/dist/types/conditions.d.ts.map +1 -0
  88. package/dist/types/conditions.js +1 -0
  89. package/dist/types/db.d.ts +22 -0
  90. package/dist/types/db.d.ts.map +1 -0
  91. package/dist/types/db.js +1 -0
  92. package/dist/types/dml.d.ts +64 -0
  93. package/dist/types/dml.d.ts.map +1 -0
  94. package/dist/types/dml.js +1 -0
  95. package/dist/types/join.d.ts +44 -0
  96. package/dist/types/join.d.ts.map +1 -0
  97. package/dist/types/join.js +1 -0
  98. package/dist/types/plugin.d.ts +20 -0
  99. package/dist/types/plugin.d.ts.map +1 -0
  100. package/dist/types/plugin.js +1 -0
  101. package/dist/types/schema.d.ts +10 -0
  102. package/dist/types/schema.d.ts.map +1 -0
  103. package/dist/types/schema.js +1 -0
  104. package/dist/types/search.d.ts +45 -0
  105. package/dist/types/search.d.ts.map +1 -0
  106. package/dist/types/search.js +1 -0
  107. package/dist/types/table.d.ts +52 -0
  108. package/dist/types/table.d.ts.map +1 -0
  109. package/dist/types/table.js +7 -0
  110. package/dist/types/tenant.d.ts +19 -0
  111. package/dist/types/tenant.d.ts.map +1 -0
  112. package/dist/types/tenant.js +1 -0
  113. package/dist/types/validation.d.ts +21 -0
  114. package/dist/types/validation.d.ts.map +1 -0
  115. package/dist/types/validation.js +1 -0
  116. package/dist/types.d.ts +12 -277
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/types.js +12 -7
  119. package/package.json +3 -3
@@ -9,10 +9,36 @@ const PostgresDialect = {
9
9
  return `ON CONFLICT (${quotedConflictKeys}) DO NOTHING`;
10
10
  return `ON CONFLICT (${quotedConflictKeys}) DO UPDATE SET ${quotedUpdateSet}`;
11
11
  },
12
+ upsertUpdateSet(fields, conflictKeys) {
13
+ const updateCols = fields.filter((f) => !conflictKeys.includes(f));
14
+ if (!updateCols.length)
15
+ return '';
16
+ return updateCols.map((f) => `${this.qi(f)} = EXCLUDED.${this.qi(f)}`).join(', ');
17
+ },
12
18
  returningPk(quotedPkCol) {
13
19
  return ` RETURNING ${quotedPkCol}`;
14
20
  },
21
+ dateTrunc(unit, col) {
22
+ const fmt = unit === 'hour' ? `'YYYY-MM-DD"T"HH24:00:00'` : `'YYYY-MM-DD'`;
23
+ // DATE_TRUNC accepts a literal unit string, never user input — `unit` is whitelisted upstream.
24
+ return `TO_CHAR(DATE_TRUNC('${unit}', ${col}), ${fmt})`;
25
+ },
15
26
  };
27
+ function mysqlDateTrunc(unit, col) {
28
+ switch (unit) {
29
+ case 'year': return `DATE_FORMAT(${col}, '%Y-01-01')`;
30
+ case 'quarter': return `CONCAT(YEAR(${col}), '-', LPAD((QUARTER(${col})-1)*3+1, 2, '0'), '-01')`;
31
+ case 'month': return `DATE_FORMAT(${col}, '%Y-%m-01')`;
32
+ case 'day': return `DATE_FORMAT(${col}, '%Y-%m-%d')`;
33
+ case 'hour': return `DATE_FORMAT(${col}, '%Y-%m-%dT%H:00:00')`;
34
+ }
35
+ }
36
+ function mysqlUpsertUpdateSet(fields, conflictKeys) {
37
+ const updateCols = fields.filter((f) => !conflictKeys.includes(f));
38
+ if (!updateCols.length)
39
+ return '';
40
+ return updateCols.map((f) => `${this.qi(f)} = VALUES(${this.qi(f)})`).join(', ');
41
+ }
16
42
  const MysqlDialect = {
17
43
  name: 'mysql',
18
44
  qi: (id) => `\`${id.replace(/`/g, '``')}\``,
@@ -20,13 +46,17 @@ const MysqlDialect = {
20
46
  cbDialect: 'mysql',
21
47
  supportsReturning: false,
22
48
  upsertSql(_quotedConflictKeys, quotedUpdateSet) {
49
+ // MySQL requires at least one assignment in ON DUPLICATE KEY UPDATE; when there's nothing
50
+ // user-meaningful to update, assign the first conflict key to itself (no-op).
23
51
  if (!quotedUpdateSet)
24
52
  return `ON DUPLICATE KEY UPDATE ${_quotedConflictKeys.split(', ')[0]} = ${_quotedConflictKeys.split(', ')[0]}`;
25
53
  return `ON DUPLICATE KEY UPDATE ${quotedUpdateSet}`;
26
54
  },
55
+ upsertUpdateSet: mysqlUpsertUpdateSet,
27
56
  returningPk() {
28
57
  return '';
29
58
  },
59
+ dateTrunc: mysqlDateTrunc,
30
60
  };
31
61
  const MariadbDialect = {
32
62
  name: 'mariadb',
@@ -39,9 +69,11 @@ const MariadbDialect = {
39
69
  return `ON DUPLICATE KEY UPDATE ${_quotedConflictKeys.split(', ')[0]} = ${_quotedConflictKeys.split(', ')[0]}`;
40
70
  return `ON DUPLICATE KEY UPDATE ${quotedUpdateSet}`;
41
71
  },
72
+ upsertUpdateSet: mysqlUpsertUpdateSet,
42
73
  returningPk(quotedPkCol) {
43
74
  return ` RETURNING ${quotedPkCol}`;
44
75
  },
76
+ dateTrunc: mysqlDateTrunc,
45
77
  };
46
78
  const dialects = {
47
79
  postgres: PostgresDialect,
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoG5F"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkF5F"}
@@ -1,7 +1,6 @@
1
- import { snakecaseRecord } from '../../naming.js';
2
- import { removeExcludedFields, processSecondaries, processDeletions } from '../write-helpers.js';
3
- import { injectTenantValue, validateTenantFK } from '../../tenant.js';
4
- import { runValidation, runBulkValidation } from '../validate.js';
1
+ import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
2
+ import { enforceTenantOnWrites } from '../../tenant.js';
3
+ import { runBulkValidation } from '../validate.js';
5
4
  import { primaryAsString } from '../../../types.js';
6
5
  export async function bulkUpsertEngine(params) {
7
6
  const { db, tableConf, dbTables, request, items, tenant } = params;
@@ -10,44 +9,18 @@ export async function bulkUpsertEngine(params) {
10
9
  const schema = tableConf.Schema;
11
10
  const pk = primaryAsString(tableConf.primary);
12
11
  const pkCol = schema.col(pk);
13
- // 1. Mutable camelCase copies of each item.main
14
- const inputMains = items.map((item) => ({ ...item.main }));
15
- // 2. Custom validation (camelCase)
12
+ // 1. Bulk validation runs once for all items (skips per-item validate). Otherwise per-item
13
+ // validation happens inside prepareInsertRecord below.
16
14
  if (tableConf.validateBulk) {
17
15
  await runBulkValidation(db, request, tableConf, items);
18
16
  }
19
- else {
20
- for (let i = 0; i < items.length; i++) {
21
- await runValidation(db, request, tableConf, inputMains[i], items[i].secondaries);
22
- }
23
- }
24
- // 3. beforeInsert hook per record (camelCase)
25
- if (tableConf.beforeInsert) {
26
- for (const rec of inputMains) {
27
- await tableConf.beforeInsert(db, request, rec);
28
- }
29
- }
30
- // 4. Convert to DB format (after user mutations)
31
- const preparedMains = inputMains.map((rec) => {
32
- let converted = snakecaseRecord(rec, schema);
33
- converted = removeExcludedFields(converted, tableConf);
34
- return converted;
35
- });
36
- // 5. Tenant: inject or validate FK (enforced after user mutations)
37
- if (tenant) {
38
- if ('through' in tenant.scope) {
39
- const scope = tenant.scope;
40
- const fkCol = scope.through.localField;
41
- const fkValues = preparedMains.map((rec) => rec[fkCol]);
42
- await validateTenantFK(db, scope, tenant.ids, fkValues);
43
- }
44
- else {
45
- for (const rec of preparedMains) {
46
- injectTenantValue(rec, tenant.scope, tenant.ids);
47
- }
48
- }
49
- }
50
- // 6. Bulk upsert all mains in one query → returns PK-only
17
+ // 2. For each item: validate (unless validateBulk ran) → beforeInsert → snake → remove excluded.
18
+ const prepared = await Promise.all(items.map((item) => prepareInsertRecord({ db, tableConf, request }, item.main, item.secondaries, { skipValidate: !!tableConf.validateBulk })));
19
+ const inputMains = prepared.map((p) => p.camel);
20
+ const preparedMains = prepared.map((p) => p.snake);
21
+ // 3. Tenant: inject (direct) or batch-validate FK (indirect) — enforced after user mutations
22
+ await enforceTenantOnWrites(db, tenant, preparedMains);
23
+ // 4. Bulk upsert all mains in one query → returns PK-only
51
24
  const upsertKeys = tableConf.upsertMap?.get(schema);
52
25
  let pkRows;
53
26
  if (upsertKeys) {
@@ -57,14 +30,13 @@ export async function bulkUpsertEngine(params) {
57
30
  else {
58
31
  pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
59
32
  }
60
- // 7. Process secondaries, deletions, afterInsert per item (all camelCase)
33
+ // 5. Process secondaries, deletions, afterInsert per item (all camelCase)
61
34
  const results = [];
62
35
  for (let i = 0; i < items.length; i++) {
63
36
  const item = items[i];
64
37
  const mainPk = pkRows[i]; // PK-only (already in camelCase field name since PK uses camelCase field)
65
38
  // pkRows entries come from SQL RETURNING or insertId — keys are DB column names.
66
39
  // For simple PK named the same in API and DB (like 'id') this is transparent.
67
- // If PK name differs (rare), the user should not rely on mainPk for FK auto-fill — it's the merged inputMain that drives FK.
68
40
  const result = { main: { [pk]: mainPk[pkCol] ?? mainPk[pk] } };
69
41
  // FK auto-fill: input camelCase + generated PK
70
42
  const mainForFK = { ...inputMains[i], [pk]: mainPk[pkCol] ?? mainPk[pk] };
@@ -74,11 +46,11 @@ export async function bulkUpsertEngine(params) {
74
46
  result.secondaries = sec;
75
47
  }
76
48
  if (item.deletions && Object.keys(item.deletions).length > 0) {
77
- const del = await processDeletions(db, tableConf, item.deletions);
49
+ const del = await processDeletions(db, tableConf, mainForFK, item.deletions);
78
50
  if (Object.keys(del).length > 0)
79
51
  result.deletions = del;
80
52
  }
81
- // 8. afterInsert hook per item (camelCase)
53
+ // 6. afterInsert hook per item (camelCase)
82
54
  if (tableConf.afterInsert) {
83
55
  await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
84
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA8B9E"}
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAsB9E"}
@@ -1,4 +1,5 @@
1
1
  import { buildTenantDeleteWhere } from '../../tenant.js';
2
+ import { httpError } from '../../errors.js';
2
3
  import { primaryAsString } from '../../../types.js';
3
4
  export async function deleteEngine(params) {
4
5
  const { db, tableConf, id, tenant } = params;
@@ -7,19 +8,13 @@ export async function deleteEngine(params) {
7
8
  const tableName = tableConf.Schema.tableName;
8
9
  if (!tenant) {
9
10
  const affectedRows = await db.delete(tableName, { [pkCol]: id });
10
- if (affectedRows === 0) {
11
- const err = new Error(`Record not found: ${id}`);
12
- err.statusCode = 404;
13
- throw err;
14
- }
11
+ if (affectedRows === 0)
12
+ throw httpError(404, `Record not found: ${id}`);
15
13
  return { main: { [pk]: id } };
16
14
  }
17
15
  const { where, values } = buildTenantDeleteWhere(db, tableName, pkCol, id, tenant);
18
16
  const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
19
- if (result.affectedRows === 0) {
20
- const err = new Error(`Record not found: ${id}`);
21
- err.statusCode = 404;
22
- throw err;
23
- }
17
+ if (result.affectedRows === 0)
18
+ throw httpError(404, `Record not found: ${id}`);
24
19
  return { main: { [pk]: id } };
25
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAiCrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAuB,MAAM,mBAAmB,CAAC;AAEnF,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA6BrE"}
@@ -1,6 +1,7 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
2
  import { camelcaseObject } from '../../naming.js';
3
3
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
+ import { httpError } from '../../errors.js';
4
5
  import { primaryAsString } from '../../../types.js';
5
6
  export async function getEngine(params) {
6
7
  const { db, tableConf, id, tenant } = params;
@@ -23,10 +24,7 @@ export async function getEngine(params) {
23
24
  limit: '1',
24
25
  joins: joins.length > 0 ? joins : undefined,
25
26
  });
26
- if (rows.length === 0) {
27
- const err = new Error(`Record not found: ${id}`);
28
- err.statusCode = 404;
29
- throw err;
30
- }
27
+ if (rows.length === 0)
28
+ throw httpError(404, `Record not found: ${id}`);
31
29
  return { main: camelcaseObject(rows[0], tableConf.Schema) };
32
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2E9E"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA0D9E"}
@@ -1,37 +1,17 @@
1
- import { camelcaseObject, snakecaseRecord } from '../../naming.js';
2
- import { removeExcludedFields, processSecondaries } from '../write-helpers.js';
3
- import { injectTenantValue, validateTenantFK } from '../../tenant.js';
4
- import { runValidation } from '../validate.js';
1
+ import { camelcaseObject } from '../../naming.js';
2
+ import { processSecondaries, prepareInsertRecord } from '../write-helpers.js';
3
+ import { enforceTenantOnWrites } from '../../tenant.js';
5
4
  import { primaryAsString } from '../../../types.js';
6
5
  export async function insertEngine(params) {
7
6
  const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
8
7
  const schema = tableConf.Schema;
9
8
  const pk = primaryAsString(tableConf.primary);
10
9
  const pkCol = schema.col(pk);
11
- // 1. Mutable copy of input in camelCase (user-facing format)
12
- const inputRecord = { ...record };
13
- // 2. Custom validation (camelCase)
14
- await runValidation(db, request, tableConf, inputRecord, secondaries);
15
- // 3. beforeInsert hook (camelCase — user can mutate with schema field names)
16
- if (tableConf.beforeInsert) {
17
- await tableConf.beforeInsert(db, request, inputRecord);
18
- }
19
- // 4. Convert to DB format (after all user mutations)
20
- let mainRecord = snakecaseRecord(inputRecord, schema);
21
- mainRecord = removeExcludedFields(mainRecord, tableConf);
22
- // 5. Tenant: inject or validate (enforced after user mutations for security)
23
- if (tenant) {
24
- if ('through' in tenant.scope) {
25
- const scope = tenant.scope;
26
- const fkCol = scope.through.localField;
27
- const fkValue = mainRecord[fkCol];
28
- await validateTenantFK(db, scope, tenant.ids, [fkValue]);
29
- }
30
- else {
31
- injectTenantValue(mainRecord, tenant.scope, tenant.ids);
32
- }
33
- }
34
- // 6. Insert main → returns PK-only
10
+ // 1. Validate, run beforeInsert hook, snakecase, drop excluded fields.
11
+ const { camel: inputRecord, snake: mainRecord } = await prepareInsertRecord({ db, tableConf, request }, record, secondaries);
12
+ // 2. Tenant: inject (direct) or validate FK (indirect) — enforced after user mutations
13
+ await enforceTenantOnWrites(db, tenant, [mainRecord]);
14
+ // 3. Insert main → returns PK-only
35
15
  let pkResult;
36
16
  const upsertKeys = tableConf.upsertMap?.get(schema);
37
17
  if (upsertKeys) {
@@ -42,18 +22,18 @@ export async function insertEngine(params) {
42
22
  pkResult = await db.insert(schema.tableName, mainRecord, pkCol);
43
23
  }
44
24
  const mainPkCamel = camelcaseObject(pkResult, schema);
45
- // 7. Secondaries: need full record for FK auto-fill (camelCase)
25
+ // 4. Secondaries: need full record for FK auto-fill (camelCase)
46
26
  let secondaryResults;
47
27
  if (secondaries && Object.keys(secondaries).length > 0) {
48
28
  const mainForFK = { ...inputRecord, ...mainPkCamel };
49
29
  secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
50
30
  }
51
- // 8. afterInsert hook (camelCase — input merged with generated PK)
31
+ // 5. afterInsert hook (camelCase — input merged with generated PK)
52
32
  if (tableConf.afterInsert) {
53
33
  const mainForHook = { ...inputRecord, ...mainPkCamel };
54
34
  await tableConf.afterInsert(db, request, mainForHook, secondaryResults);
55
35
  }
56
- // 9. Return PK-only
36
+ // 6. Return PK-only
57
37
  const result = { main: mainPkCamel };
58
38
  if (secondaryResults && Object.keys(secondaryResults).length > 0) {
59
39
  result.secondaries = secondaryResults;
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAkI9E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA+F9E"}
@@ -1,8 +1,8 @@
1
1
  import { snakecaseRecord } from '../../naming.js';
2
2
  import { processSecondaries, processDeletions } from '../write-helpers.js';
3
- import { stripTenantColumn, buildTenantCondition, buildTenantJoin } from '../../tenant.js';
3
+ import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
5
- import { ConditionBuilder } from 'node-condition-builder';
5
+ import { httpError } from '../../errors.js';
6
6
  import { primaryAsString } from '../../../types.js';
7
7
  export async function updateEngine(params) {
8
8
  const { db, tableConf, dbTables, request, record, secondaries, deletions, tenant } = params;
@@ -10,29 +10,10 @@ export async function updateEngine(params) {
10
10
  const pk = primaryAsString(tableConf.primary);
11
11
  const pkCol = schema.col(pk);
12
12
  const pkValue = record[pk];
13
- if (pkValue == null) {
14
- const error = new Error(`Primary key "${pk}" is required`);
15
- error.statusCode = 400;
16
- throw error;
17
- }
13
+ if (pkValue == null)
14
+ throw httpError(400, `Primary key "${pk}" is required`);
18
15
  // 1. Tenant ownership check (indirect) — do it early so user mutations don't run on records they can't access
19
- if (tenant && 'through' in tenant.scope) {
20
- const scope = tenant.scope;
21
- const tableName = schema.tableName;
22
- const cb = new ConditionBuilder('AND');
23
- cb.isEqual(`${db.qi(tableName)}.${db.qi(pkCol)}`, pkValue);
24
- cb.append(buildTenantCondition(db, scope, tenant.ids));
25
- const checkWhere = cb.build(1, db.ph);
26
- const checkValues = cb.getValues();
27
- const joinSql = buildTenantJoin(db, scope, tableName);
28
- const checkSql = `SELECT 1 FROM ${db.qi(tableName)} ${joinSql} WHERE ${checkWhere} LIMIT 1`;
29
- const checkResult = await db.query(checkSql, checkValues);
30
- if (checkResult.rows.length === 0) {
31
- const error = new Error('Record not found');
32
- error.statusCode = 404;
33
- throw error;
34
- }
35
- }
16
+ await assertTenantOwnership(db, tenant, schema.tableName, pkCol, pkValue);
36
17
  // 2. Mutable copy of input in camelCase
37
18
  const inputRecord = { ...record };
38
19
  // 3. Custom validation (camelCase)
@@ -51,23 +32,11 @@ export async function updateEngine(params) {
51
32
  }
52
33
  // 7. Update main
53
34
  const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
54
- let extraCondition;
55
- if (tenant && !('through' in tenant.scope)) {
56
- extraCondition = new ConditionBuilder('AND');
57
- if (tenant.ids.length === 1) {
58
- extraCondition.isEqual(tenant.scope.column, tenant.ids[0]);
59
- }
60
- else {
61
- extraCondition.isIn(tenant.scope.column, tenant.ids);
62
- }
63
- }
35
+ const extraCondition = buildTenantUpdateExtra(tenant);
64
36
  if (hasFieldsToUpdate) {
65
37
  const affectedRows = await db.update(schema.tableName, updateFields, { [pkCol]: pkValue }, extraCondition);
66
- if (affectedRows === 0) {
67
- const error = new Error(`Record not found`);
68
- error.statusCode = 404;
69
- throw error;
70
- }
38
+ if (affectedRows === 0)
39
+ throw httpError(404, `Record not found`);
71
40
  }
72
41
  else {
73
42
  // No fields to update: verify the record exists (for secondaries/deletions)
@@ -83,11 +52,8 @@ export async function updateEngine(params) {
83
52
  values: whereValues,
84
53
  limit: '1',
85
54
  });
86
- if (rows.length === 0) {
87
- const error = new Error(`Record not found`);
88
- error.statusCode = 404;
89
- throw error;
90
- }
55
+ if (rows.length === 0)
56
+ throw httpError(404, `Record not found`);
91
57
  }
92
58
  // Build the main response (PK-only)
93
59
  const mainResult = { [pk]: pkValue };
@@ -97,10 +63,11 @@ export async function updateEngine(params) {
97
63
  const mainForFK = { ...inputRecord };
98
64
  secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
99
65
  }
100
- // 9. Deletions
66
+ // 9. Deletions (FK auto-fill from main like secondaries)
101
67
  let deletionResults;
102
68
  if (deletions && Object.keys(deletions).length > 0) {
103
- deletionResults = await processDeletions(db, tableConf, deletions);
69
+ const mainForFK = { ...inputRecord };
70
+ deletionResults = await processDeletions(db, tableConf, mainForFK, deletions);
104
71
  }
105
72
  // 10. Return PK-only
106
73
  const result = { main: mainResult };
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EAER,YAAY,EACZ,YAAY,EAUb,MAAM,mBAAmB,CAAC;AA+lB3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA2EvB"}
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"}