fastify-auto-sqlapi 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/AGENTS_BACKEND.md +18 -14
  2. package/AGENTS_FRONTEND.md +78 -1
  3. package/README.md +117 -18
  4. package/dist/bin/generate-schema.js +3 -3
  5. package/dist/bin/generate-tables.js +3 -3
  6. package/dist/bin/utils.d.ts +1 -1
  7. package/dist/bin/utils.d.ts.map +1 -1
  8. package/dist/bin/utils.js +2 -2
  9. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  10. package/dist/lib/cli/mysql-introspect.js +9 -2
  11. package/dist/lib/cli/schema-codegen.d.ts +1 -1
  12. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  13. package/dist/lib/cli/schema-codegen.js +11 -4
  14. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  15. package/dist/lib/cli/tables-codegen.js +4 -3
  16. package/dist/lib/db.d.ts.map +1 -1
  17. package/dist/lib/db.js +10 -4
  18. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  19. package/dist/lib/engine/bulk/bulk-upsert.js +40 -36
  20. package/dist/lib/engine/rest/get.js +1 -1
  21. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  22. package/dist/lib/engine/rest/insert.js +24 -23
  23. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  24. package/dist/lib/engine/rest/update.js +41 -40
  25. package/dist/lib/engine/search/search.d.ts.map +1 -1
  26. package/dist/lib/engine/search/search.js +235 -39
  27. package/dist/lib/engine/write-helpers.js +4 -4
  28. package/dist/lib/naming.d.ts +11 -2
  29. package/dist/lib/naming.d.ts.map +1 -1
  30. package/dist/lib/naming.js +33 -5
  31. package/dist/lib/schema/search.d.ts.map +1 -1
  32. package/dist/lib/schema/search.js +16 -10
  33. package/dist/lib/table-helpers.d.ts.map +1 -1
  34. package/dist/lib/table-helpers.js +9 -1
  35. package/dist/types.d.ts +26 -7
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +8 -5
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IA6FlB,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAoB7B,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAuB7B,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA2B/B,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA+B/B,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,QAAQ,EACf,cAAc,CAAC,EAAE,gBAAgB,GAChC,OAAO,CAAC,MAAM,CAAC;IAuBZ,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,MAAM,CAAC;IAYZ,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACxC,SAAS,EACT,OAAa,EACb,KAAK,EACL,MAAM,EACN,KAAY,EACZ,OAAY,EACZ,KAAU,EACV,QAAgB,GACjB,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;CAchC;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAGb"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IA6FlB,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAoB7B,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAuB7B,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA+B/B,kBAAkB,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAiC/B,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,QAAQ,EACf,cAAc,CAAC,EAAE,gBAAgB,GAChC,OAAO,CAAC,MAAM,CAAC;IAuBZ,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,MAAM,CAAC;IAYZ,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACxC,SAAS,EACT,OAAa,EACb,KAAK,EACL,MAAM,EACN,KAAY,EACZ,OAAY,EACZ,KAAU,EACV,QAAgB,GACjB,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;CAchC;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAGb"}
package/dist/lib/db.js CHANGED
@@ -149,8 +149,12 @@ export class QueryClient {
149
149
  results.push(...result.rows);
150
150
  }
151
151
  else {
152
- for (const rec of chunk) {
153
- results.push(this.#mysqlPkResult(pkCol, rec, result.insertId));
152
+ // MySQL: insertId is the FIRST generated ID of the multi-row insert.
153
+ // Subsequent rows follow sequentially assuming auto_increment_increment=1 (default).
154
+ const firstId = result.insertId;
155
+ for (let j = 0; j < chunk.length; j++) {
156
+ const id = typeof firstId === 'number' ? firstId + j : undefined;
157
+ results.push(this.#mysqlPkResult(pkCol, chunk[j], id));
154
158
  }
155
159
  }
156
160
  }
@@ -174,8 +178,10 @@ export class QueryClient {
174
178
  results.push(...result.rows);
175
179
  }
176
180
  else {
177
- for (const rec of chunk) {
178
- results.push(this.#mysqlPkResult(pkCol, rec, result.insertId));
181
+ const firstId = result.insertId;
182
+ for (let j = 0; j < chunk.length; j++) {
183
+ const id = typeof firstId === 'number' ? firstId + j : undefined;
184
+ results.push(this.#mysqlPkResult(pkCol, chunk[j], id));
179
185
  }
180
186
  }
181
187
  }
@@ -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,CAgG5F"}
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,4 +1,4 @@
1
- import { camelcaseObject, snakecaseRecord } from '../../naming.js';
1
+ import { snakecaseRecord } from '../../naming.js';
2
2
  import { removeExcludedFields, processSecondaries, processDeletions } from '../write-helpers.js';
3
3
  import { injectTenantValue, validateTenantFK } from '../../tenant.js';
4
4
  import { runValidation, runBulkValidation } from '../validate.js';
@@ -7,18 +7,35 @@ export async function bulkUpsertEngine(params) {
7
7
  const { db, tableConf, dbTables, request, items, tenant } = params;
8
8
  if (!items.length)
9
9
  return [];
10
+ const schema = tableConf.Schema;
10
11
  const pk = primaryAsString(tableConf.primary);
11
- const pkCol = tableConf.Schema.col(pk);
12
- // 1. Prepare all main records
13
- const preparedMains = items.map((item) => {
14
- let rec = snakecaseRecord(item.main);
15
- rec = removeExcludedFields(rec, tableConf);
16
- return rec;
12
+ 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)
16
+ if (tableConf.validateBulk) {
17
+ await runBulkValidation(db, request, tableConf, items);
18
+ }
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;
17
35
  });
18
- // 1b. Tenant: inject or validate FK
36
+ // 5. Tenant: inject or validate FK (enforced after user mutations)
19
37
  if (tenant) {
20
38
  if ('through' in tenant.scope) {
21
- // Indirect: collect all FK values, validate in batch
22
39
  const scope = tenant.scope;
23
40
  const fkCol = scope.through.localField;
24
41
  const fkValues = preparedMains.map((rec) => rec[fkCol]);
@@ -30,40 +47,27 @@ export async function bulkUpsertEngine(params) {
30
47
  }
31
48
  }
32
49
  }
33
- // 2. Custom validation (receives original camelCase records)
34
- if (tableConf.validateBulk) {
35
- await runBulkValidation(db, request, tableConf, items);
36
- }
37
- else {
38
- for (let i = 0; i < items.length; i++) {
39
- await runValidation(db, request, tableConf, items[i].main, items[i].secondaries);
40
- }
41
- }
42
- // 3. beforeInsert hook per record
43
- if (tableConf.beforeInsert) {
44
- for (const rec of preparedMains) {
45
- await tableConf.beforeInsert(db, request, rec);
46
- }
47
- }
48
- // 4. Bulk upsert all mains in one query → returns PK-only
49
- const upsertKeys = tableConf.upsertMap?.get(tableConf.Schema);
50
+ // 6. Bulk upsert all mains in one query → returns PK-only
51
+ const upsertKeys = tableConf.upsertMap?.get(schema);
50
52
  let pkRows;
51
53
  if (upsertKeys) {
52
- const conflictCols = upsertKeys.map((k) => tableConf.Schema.col(k));
53
- pkRows = await db.bulkInsertOrUpdate(tableConf.Schema.tableName, preparedMains, conflictCols, pkCol);
54
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
55
+ pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
54
56
  }
55
57
  else {
56
- pkRows = await db.bulkInsert(tableConf.Schema.tableName, preparedMains, pkCol);
58
+ pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
57
59
  }
58
- const pksCamel = pkRows.map((r) => camelcaseObject(r));
59
- // 5. Process secondaries and deletions per item
60
+ // 7. Process secondaries, deletions, afterInsert per item (all camelCase)
60
61
  const results = [];
61
62
  for (let i = 0; i < items.length; i++) {
62
63
  const item = items[i];
63
- const mainPk = pksCamel[i];
64
- const result = { main: mainPk };
65
- // For FK auto-fill in secondaries, merge input record + PK
66
- const mainForFK = { ...camelcaseObject(preparedMains[i]), ...mainPk };
64
+ const mainPk = pkRows[i]; // PK-only (already in camelCase field name since PK uses camelCase field)
65
+ // pkRows entries come from SQL RETURNING or insertId — keys are DB column names.
66
+ // 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
+ const result = { main: { [pk]: mainPk[pkCol] ?? mainPk[pk] } };
69
+ // FK auto-fill: input camelCase + generated PK
70
+ const mainForFK = { ...inputMains[i], [pk]: mainPk[pkCol] ?? mainPk[pk] };
67
71
  if (item.secondaries && Object.keys(item.secondaries).length > 0) {
68
72
  const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
69
73
  if (Object.keys(sec).length > 0)
@@ -74,7 +78,7 @@ export async function bulkUpsertEngine(params) {
74
78
  if (Object.keys(del).length > 0)
75
79
  result.deletions = del;
76
80
  }
77
- // 6. afterInsert hook per item
81
+ // 8. afterInsert hook per item (camelCase)
78
82
  if (tableConf.afterInsert) {
79
83
  await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
80
84
  }
@@ -28,5 +28,5 @@ export async function getEngine(params) {
28
28
  err.statusCode = 404;
29
29
  throw err;
30
30
  }
31
- return { main: camelcaseObject(rows[0]) };
31
+ return { main: camelcaseObject(rows[0], tableConf.Schema) };
32
32
  }
@@ -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,CAyE9E"}
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"}
@@ -5,15 +5,23 @@ import { runValidation } from '../validate.js';
5
5
  import { primaryAsString } from '../../../types.js';
6
6
  export async function insertEngine(params) {
7
7
  const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
8
+ const schema = tableConf.Schema;
8
9
  const pk = primaryAsString(tableConf.primary);
9
- const pkCol = tableConf.Schema.col(pk);
10
- // 1. Prepare main record
11
- let mainRecord = snakecaseRecord(record);
10
+ 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);
12
21
  mainRecord = removeExcludedFields(mainRecord, tableConf);
13
- // 1b. Tenant: inject or validate
22
+ // 5. Tenant: inject or validate (enforced after user mutations for security)
14
23
  if (tenant) {
15
24
  if ('through' in tenant.scope) {
16
- // Indirect: validate FK belongs to tenant
17
25
  const scope = tenant.scope;
18
26
  const fkCol = scope.through.localField;
19
27
  const fkValue = mainRecord[fkCol];
@@ -23,36 +31,29 @@ export async function insertEngine(params) {
23
31
  injectTenantValue(mainRecord, tenant.scope, tenant.ids);
24
32
  }
25
33
  }
26
- // 2. Custom validation (receives original camelCase record)
27
- await runValidation(db, request, tableConf, record, secondaries);
28
- // 3. beforeInsert hook
29
- if (tableConf.beforeInsert) {
30
- await tableConf.beforeInsert(db, request, mainRecord);
31
- }
32
- // 4. Insert main → returns PK-only
34
+ // 6. Insert main → returns PK-only
33
35
  let pkResult;
34
- const upsertKeys = tableConf.upsertMap?.get(tableConf.Schema);
36
+ const upsertKeys = tableConf.upsertMap?.get(schema);
35
37
  if (upsertKeys) {
36
- const conflictCols = upsertKeys.map((k) => tableConf.Schema.col(k));
37
- pkResult = await db.insertOrUpdate(tableConf.Schema.tableName, mainRecord, conflictCols, pkCol);
38
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
39
+ pkResult = await db.insertOrUpdate(schema.tableName, mainRecord, conflictCols, pkCol);
38
40
  }
39
41
  else {
40
- pkResult = await db.insert(tableConf.Schema.tableName, mainRecord, pkCol);
42
+ pkResult = await db.insert(schema.tableName, mainRecord, pkCol);
41
43
  }
42
- const mainPkCamel = camelcaseObject(pkResult);
43
- // 5. Secondaries: need full record for FK auto-fill
44
+ const mainPkCamel = camelcaseObject(pkResult, schema);
45
+ // 7. Secondaries: need full record for FK auto-fill (camelCase)
44
46
  let secondaryResults;
45
47
  if (secondaries && Object.keys(secondaries).length > 0) {
46
- // Merge input record + PK for FK auto-fill
47
- const mainForFK = { ...camelcaseObject(mainRecord), ...mainPkCamel };
48
+ const mainForFK = { ...inputRecord, ...mainPkCamel };
48
49
  secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
49
50
  }
50
- // 6. afterInsert hook
51
+ // 8. afterInsert hook (camelCase — input merged with generated PK)
51
52
  if (tableConf.afterInsert) {
52
- const mainForHook = { ...camelcaseObject(mainRecord), ...mainPkCamel };
53
+ const mainForHook = { ...inputRecord, ...mainPkCamel };
53
54
  await tableConf.afterInsert(db, request, mainForHook, secondaryResults);
54
55
  }
55
- // 7. Return PK-only
56
+ // 9. Return PK-only
56
57
  const result = { main: mainPkCamel };
57
58
  if (secondaryResults && Object.keys(secondaryResults).length > 0) {
58
59
  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,CAgI9E"}
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,4 +1,4 @@
1
- import { camelcaseObject, snakecaseRecord } from '../../naming.js';
1
+ import { snakecaseRecord } from '../../naming.js';
2
2
  import { processSecondaries, processDeletions } from '../write-helpers.js';
3
3
  import { stripTenantColumn, buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
@@ -6,51 +6,53 @@ import { ConditionBuilder } from 'node-condition-builder';
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;
9
- // 1. Prepare: extract PK, snakecase, build update fields
9
+ const schema = tableConf.Schema;
10
10
  const pk = primaryAsString(tableConf.primary);
11
- const pkCol = tableConf.Schema.col(pk);
12
- const snaked = snakecaseRecord(record);
13
- const pkValue = snaked[pkCol];
11
+ const pkCol = schema.col(pk);
12
+ const pkValue = record[pk];
14
13
  if (pkValue == null) {
15
14
  const error = new Error(`Primary key "${pk}" is required`);
16
15
  error.statusCode = 400;
17
16
  throw error;
18
17
  }
18
+ // 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
+ }
36
+ // 2. Mutable copy of input in camelCase
37
+ const inputRecord = { ...record };
38
+ // 3. Custom validation (camelCase)
39
+ await runValidation(db, request, tableConf, inputRecord, secondaries);
40
+ // 4. beforeUpdate hook (camelCase — user can mutate with schema field names)
41
+ if (tableConf.beforeUpdate) {
42
+ await tableConf.beforeUpdate(db, request, inputRecord);
43
+ }
44
+ // 5. Convert to DB format (after all user mutations)
45
+ const snaked = snakecaseRecord(inputRecord, schema);
19
46
  const updateFields = { ...snaked };
20
47
  delete updateFields[pkCol];
21
- // 1b. Tenant: strip tenant column from update fields + validate ownership
48
+ // 6. Strip tenant column (user cannot change tenant of an existing record)
22
49
  if (tenant) {
23
50
  stripTenantColumn(updateFields, tenant.scope);
24
- if ('through' in tenant.scope) {
25
- // Indirect: verify the record belongs to tenant via subquery
26
- const scope = tenant.scope;
27
- const tableName = tableConf.Schema.tableName;
28
- const cb = new ConditionBuilder('AND');
29
- cb.isEqual(`${db.qi(tableName)}.${db.qi(pkCol)}`, pkValue);
30
- cb.append(buildTenantCondition(db, scope, tenant.ids));
31
- const checkWhere = cb.build(1, db.ph);
32
- const checkValues = cb.getValues();
33
- const joinSql = buildTenantJoin(db, scope, tableName);
34
- const checkSql = `SELECT 1 FROM ${db.qi(tableName)} ${joinSql} WHERE ${checkWhere} LIMIT 1`;
35
- const checkResult = await db.query(checkSql, checkValues);
36
- if (checkResult.rows.length === 0) {
37
- const error = new Error('Record not found');
38
- error.statusCode = 404;
39
- throw error;
40
- }
41
- }
42
- }
43
- // 2. Custom validation (receives original camelCase record)
44
- await runValidation(db, request, tableConf, record, secondaries);
45
- // 3. beforeUpdate hook
46
- if (tableConf.beforeUpdate) {
47
- await tableConf.beforeUpdate(db, request, updateFields);
48
51
  }
49
- // 4. Update main
52
+ // 7. Update main
50
53
  const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
51
54
  let extraCondition;
52
55
  if (tenant && !('through' in tenant.scope)) {
53
- // Direct: add tenant to WHERE
54
56
  extraCondition = new ConditionBuilder('AND');
55
57
  if (tenant.ids.length === 1) {
56
58
  extraCondition.isEqual(tenant.scope.column, tenant.ids[0]);
@@ -60,7 +62,7 @@ export async function updateEngine(params) {
60
62
  }
61
63
  }
62
64
  if (hasFieldsToUpdate) {
63
- const affectedRows = await db.update(tableConf.Schema.tableName, updateFields, { [pkCol]: pkValue }, extraCondition);
65
+ const affectedRows = await db.update(schema.tableName, updateFields, { [pkCol]: pkValue }, extraCondition);
64
66
  if (affectedRows === 0) {
65
67
  const error = new Error(`Record not found`);
66
68
  error.statusCode = 404;
@@ -69,14 +71,14 @@ export async function updateEngine(params) {
69
71
  }
70
72
  else {
71
73
  // No fields to update: verify the record exists (for secondaries/deletions)
72
- let whereSql = `${db.qi(tableConf.Schema.tableName)}.${db.qi(pkCol)} = ${db.ph(1)}`;
74
+ let whereSql = `${db.qi(schema.tableName)}.${db.qi(pkCol)} = ${db.ph(1)}`;
73
75
  const whereValues = [pkValue];
74
76
  if (extraCondition) {
75
77
  whereSql += ` AND ${extraCondition.build(2, db.ph)}`;
76
78
  whereValues.push(...extraCondition.getValues());
77
79
  }
78
80
  const rows = await db.select({
79
- tableName: tableConf.Schema.tableName,
81
+ tableName: schema.tableName,
80
82
  where: whereSql,
81
83
  values: whereValues,
82
84
  limit: '1',
@@ -89,19 +91,18 @@ export async function updateEngine(params) {
89
91
  }
90
92
  // Build the main response (PK-only)
91
93
  const mainResult = { [pk]: pkValue };
92
- // 5. Secondaries (upsert/insert with FK auto-fill)
94
+ // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
93
95
  let secondaryResults;
94
96
  if (secondaries && Object.keys(secondaries).length > 0) {
95
- // Merge input fields + PK for FK auto-fill
96
- const mainForFK = { ...camelcaseObject(snaked) };
97
+ const mainForFK = { ...inputRecord };
97
98
  secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
98
99
  }
99
- // 6. Deletions
100
+ // 9. Deletions
100
101
  let deletionResults;
101
102
  if (deletions && Object.keys(deletions).length > 0) {
102
103
  deletionResults = await processDeletions(db, tableConf, deletions);
103
104
  }
104
- // 7. Return PK-only
105
+ // 10. Return PK-only
105
106
  const result = { main: mainResult };
106
107
  if (secondaryResults && Object.keys(secondaryResults).length > 0) {
107
108
  result.secondaries = secondaryResults;
@@ -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,EASb,MAAM,mBAAmB,CAAC;AA+V3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAyDvB"}
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"}