fastify-auto-sqlapi 0.1.2 → 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 (68) 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 +25 -8
  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 +9 -5
  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/table-helpers.d.ts.map +1 -1
  39. package/dist/lib/table-helpers.js +10 -9
  40. package/dist/lib/tenant.d.ts +2 -2
  41. package/dist/lib/tenant.d.ts.map +1 -1
  42. package/dist/lib/tenant.js +11 -9
  43. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  44. package/dist/routes/auto/bulk-delete.routes.js +1 -0
  45. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  46. package/dist/routes/auto/bulk-upsert.routes.js +1 -0
  47. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  48. package/dist/routes/auto/delete.routes.js +1 -0
  49. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  50. package/dist/routes/auto/get.routes.js +1 -0
  51. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  52. package/dist/routes/auto/insert.routes.js +1 -0
  53. package/dist/routes/auto/route-helpers.d.ts +3 -1
  54. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  55. package/dist/routes/auto/route-helpers.js +3 -0
  56. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  57. package/dist/routes/auto/search.routes.js +1 -0
  58. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  59. package/dist/routes/auto/update.routes.js +1 -0
  60. package/dist/types/cli.d.ts +6 -0
  61. package/dist/types/cli.d.ts.map +1 -1
  62. package/dist/types/db.d.ts +11 -0
  63. package/dist/types/db.d.ts.map +1 -1
  64. package/dist/types/schema.d.ts +2 -0
  65. package/dist/types/schema.d.ts.map +1 -1
  66. package/dist/types/table.d.ts +40 -2
  67. package/dist/types/table.d.ts.map +1 -1
  68. package/package.json +1 -1
@@ -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;AAQ/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,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;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;IAmGlB,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;IAgB7B,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;IAkB7B,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;IAoB/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;IAuB/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":"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"}
package/dist/lib/db.js CHANGED
@@ -23,6 +23,18 @@ export class QueryClient {
23
23
  get dialectName() {
24
24
  return this.dialect.name;
25
25
  }
26
+ /** ConditionBuilder dialect ('postgres' | 'mysql') for per-instance builders. */
27
+ get cbDialect() {
28
+ return this.dialect.cbDialect;
29
+ }
30
+ /** Whether the dialect supports `RETURNING` (postgres/mariadb: yes, mysql: no). */
31
+ get supportsReturning() {
32
+ return this.dialect.supportsReturning;
33
+ }
34
+ /** Build the ` RETURNING <quoted pk>` suffix ('' when unsupported). */
35
+ get returningPk() {
36
+ return this.dialect.returningPk.bind(this.dialect);
37
+ }
26
38
  get dateTrunc() {
27
39
  return this.dialect.dateTrunc.bind(this.dialect);
28
40
  }
@@ -39,6 +51,41 @@ export class QueryClient {
39
51
  }
40
52
  return this.client.query(text, values);
41
53
  }
54
+ /**
55
+ * Run `fn` inside a DB transaction on a dedicated connection (BEGIN/COMMIT, with
56
+ * ROLLBACK + rethrow on error). Requires the underlying Queryable to expose
57
+ * `connect()`; when it does not (legacy custom adapters), `fn` runs directly on
58
+ * this client without transactional guarantees.
59
+ *
60
+ * Nested calls join the outer transaction: the tx QueryClient wraps a dedicated
61
+ * connection that has no `connect()`, so an inner `withTransaction` is a no-op
62
+ * wrapper around the same connection.
63
+ */
64
+ async withTransaction(fn) {
65
+ if (!this.client.connect)
66
+ return fn(this);
67
+ const conn = await this.client.connect();
68
+ const tx = new QueryClient(conn, this.dialect);
69
+ tx.setDebug(this.debug);
70
+ try {
71
+ await conn.query('BEGIN');
72
+ const result = await fn(tx);
73
+ await conn.query('COMMIT');
74
+ return result;
75
+ }
76
+ catch (err) {
77
+ try {
78
+ await conn.query('ROLLBACK');
79
+ }
80
+ catch {
81
+ // The connection is broken: surface the original error, not the rollback failure.
82
+ }
83
+ throw err;
84
+ }
85
+ finally {
86
+ await conn.release();
87
+ }
88
+ }
42
89
  // Quote a list of fields
43
90
  #q(fields) {
44
91
  return fields.map((f) => this.dialect.qi(f)).join(', ');
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAwC5F"}
1
+ {"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiE5F"}
@@ -20,17 +20,34 @@ export async function bulkDeleteEngine(params) {
20
20
  ({ where, values } = buildTenantDeleteWhere(db, tableName, pkCol, ids, tenant));
21
21
  }
22
22
  else {
23
- const cb = new ConditionBuilder('AND');
23
+ const cb = new ConditionBuilder('AND', db.cbDialect);
24
24
  cb.isIn(db.qi(pkCol), ids);
25
25
  where = cb.build(1, db.ph);
26
26
  values = cb.getValues();
27
27
  }
28
- const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
29
- // Return PK-only for each deleted record
30
- if (result.affectedRows === ids.length) {
31
- return ids.map((id) => ({ main: { [pk]: id } }));
28
+ let results;
29
+ if (db.supportsReturning) {
30
+ const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}${db.returningPk(db.qi(pkCol))}`, values);
31
+ // Happy path: everything requested was deleted
32
+ if (result.affectedRows === ids.length) {
33
+ results = ids.map((id) => ({ main: { [pk]: id } }));
34
+ }
35
+ else {
36
+ // Partial delete: RETURNING tells exactly which rows went away
37
+ results = result.rows.map((r) => ({ main: { [pk]: r[pkCol] } }));
38
+ }
32
39
  }
33
- // Partial delete: best effort (no RETURNING on MySQL)
34
- const deletedCount = result.affectedRows;
35
- return ids.slice(0, deletedCount).map((id) => ({ main: { [pk]: id } }));
40
+ else {
41
+ // No RETURNING (mysql): affectedRows alone cannot tell WHICH ids were deleted on a
42
+ // partial match, so look up the rows visible through the same WHERE before deleting.
43
+ const existing = await db.query(`SELECT ${db.qi(pkCol)} AS pk FROM ${db.qi(tableName)} WHERE ${where}`, values);
44
+ await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
45
+ results = existing.rows.map((r) => ({ main: { [pk]: r.pk } }));
46
+ }
47
+ // Called once with the ACTUALLY deleted ids (possibly a subset of the requested ones)
48
+ if (tableConf.afterBulkDelete && results.length > 0) {
49
+ const deletedIds = results.map((r) => r.main[pk]);
50
+ await tableConf.afterBulkDelete(db, request, deletedIds);
51
+ }
52
+ return results;
36
53
  }
@@ -1 +1 @@
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
+ {"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,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkF5F"}
@@ -1,14 +1,15 @@
1
+ import { camelcaseObject } from '../../naming.js';
1
2
  import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
2
3
  import { enforceTenantOnWrites } from '../../tenant.js';
3
4
  import { runBulkValidation } from '../validate.js';
4
- import { primaryAsString } from '../../../types.js';
5
+ import { primaryAsCols } from '../../../types.js';
5
6
  export async function bulkUpsertEngine(params) {
6
7
  const { db, tableConf, dbTables, request, items, tenant } = params;
7
8
  if (!items.length)
8
9
  return [];
9
10
  const schema = tableConf.Schema;
10
- const pk = primaryAsString(tableConf.primary);
11
- const pkCol = schema.col(pk);
11
+ // Full primary key columns — composite PKs need every column (see insertEngine).
12
+ const pkCol = primaryAsCols(tableConf.primary, (f) => schema.col(f));
12
13
  // 1. Bulk validation runs once for all items (skips per-item validate). Otherwise per-item
13
14
  // validation happens inside prepareInsertRecord below.
14
15
  if (tableConf.validateBulk) {
@@ -34,12 +35,12 @@ export async function bulkUpsertEngine(params) {
34
35
  const results = [];
35
36
  for (let i = 0; i < items.length; i++) {
36
37
  const item = items[i];
37
- const mainPk = pkRows[i]; // PK-only (already in camelCase field name since PK uses camelCase field)
38
- // pkRows entries come from SQL RETURNING or insertId — keys are DB column names.
39
- // For simple PK named the same in API and DB (like 'id') this is transparent.
40
- const result = { main: { [pk]: mainPk[pkCol] ?? mainPk[pk] } };
38
+ // pkRows entries come from SQL RETURNING or mysql insertId synthesis — keys are DB
39
+ // column names; camelcaseObject maps them back to schema fields (full composite PK).
40
+ const mainPkCamel = camelcaseObject(pkRows[i], schema);
41
+ const result = { main: mainPkCamel };
41
42
  // FK auto-fill: input camelCase + generated PK
42
- const mainForFK = { ...inputMains[i], [pk]: mainPk[pkCol] ?? mainPk[pk] };
43
+ const mainForFK = { ...inputMains[i], ...mainPkCamel };
43
44
  if (item.secondaries && Object.keys(item.secondaries).length > 0) {
44
45
  const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
45
46
  if (Object.keys(sec).length > 0)
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAKA,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":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAY,MAAM,mBAAmB,CAAC;AAE9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAiC9E"}
@@ -17,11 +17,15 @@ export async function deleteEngine(params) {
17
17
  const affectedRows = await db.delete(tableName, { [pkCol]: id });
18
18
  if (affectedRows === 0)
19
19
  throw httpError(404, `Record not found: ${id}`);
20
- return { main: { [pk]: id } };
21
20
  }
22
- const { where, values } = buildTenantDeleteWhere(db, tableName, pkCol, id, tenant);
23
- const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
24
- if (result.affectedRows === 0)
25
- throw httpError(404, `Record not found: ${id}`);
21
+ else {
22
+ const { where, values } = buildTenantDeleteWhere(db, tableName, pkCol, id, tenant);
23
+ const result = await db.query(`DELETE FROM ${db.qi(tableName)} WHERE ${where}`, values);
24
+ if (result.affectedRows === 0)
25
+ throw httpError(404, `Record not found: ${id}`);
26
+ }
27
+ if (tableConf.afterDelete) {
28
+ await tableConf.afterDelete(db, request, id);
29
+ }
26
30
  return { main: { [pk]: id } };
27
31
  }
@@ -6,7 +6,7 @@ import { primaryAsString } from '../../../types.js';
6
6
  export async function getEngine(params) {
7
7
  const { db, tableConf, id, tenant } = params;
8
8
  const pkCol = tableConf.Schema.col(primaryAsString(tableConf.primary));
9
- const cb = new ConditionBuilder('AND');
9
+ const cb = new ConditionBuilder('AND', db.cbDialect);
10
10
  cb.isEqual(db.qi(pkCol), id);
11
11
  const joins = [];
12
12
  if (tenant) {
@@ -1 +1 @@
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
+ {"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,CA+D9E"}
@@ -1,42 +1,47 @@
1
1
  import { camelcaseObject } from '../../naming.js';
2
2
  import { processSecondaries, prepareInsertRecord } from '../write-helpers.js';
3
3
  import { enforceTenantOnWrites } from '../../tenant.js';
4
- import { primaryAsString } from '../../../types.js';
4
+ import { primaryAsCols } from '../../../types.js';
5
5
  export async function insertEngine(params) {
6
6
  const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
7
7
  const schema = tableConf.Schema;
8
- const pk = primaryAsString(tableConf.primary);
9
- const pkCol = schema.col(pk);
8
+ // Full primary key columns (composite PKs need every column for RETURNING /
9
+ // mysql synthesis, otherwise the response is missing PK fields).
10
+ const pkCol = primaryAsCols(tableConf.primary, (f) => schema.col(f));
10
11
  // 1. Validate, run beforeInsert hook, snakecase, drop excluded fields.
11
12
  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
15
- let pkResult;
16
- const upsertKeys = tableConf.upsertMap?.get(schema);
17
- if (upsertKeys) {
18
- const conflictCols = upsertKeys.map((k) => schema.col(k));
19
- pkResult = await db.insertOrUpdate(schema.tableName, mainRecord, conflictCols, pkCol);
20
- }
21
- else {
22
- pkResult = await db.insert(schema.tableName, mainRecord, pkCol);
23
- }
24
- const mainPkCamel = camelcaseObject(pkResult, schema);
25
- // 4. Secondaries: need full record for FK auto-fill (camelCase)
26
- let secondaryResults;
27
- if (secondaries && Object.keys(secondaries).length > 0) {
28
- const mainForFK = { ...inputRecord, ...mainPkCamel };
29
- secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
30
- }
31
- // 5. afterInsert hook (camelCase — input merged with generated PK)
32
- if (tableConf.afterInsert) {
33
- const mainForHook = { ...inputRecord, ...mainPkCamel };
34
- await tableConf.afterInsert(db, request, mainForHook, secondaryResults);
35
- }
36
- // 6. Return PK-only
37
- const result = { main: mainPkCamel };
38
- if (secondaryResults && Object.keys(secondaryResults).length > 0) {
39
- result.secondaries = secondaryResults;
40
- }
41
- return result;
13
+ // Steps 2-5 are atomic: a failure in secondaries or afterInsert rolls back the
14
+ // main insert too. Degrades to non-transactional when the adapter has no connect().
15
+ return db.withTransaction(async (tx) => {
16
+ // 2. Tenant: inject (direct) or validate FK (indirect) — enforced after user mutations
17
+ await enforceTenantOnWrites(tx, tenant, [mainRecord]);
18
+ // 3. Insert main → returns PK-only
19
+ let pkResult;
20
+ const upsertKeys = tableConf.upsertMap?.get(schema);
21
+ if (upsertKeys) {
22
+ const conflictCols = upsertKeys.map((k) => schema.col(k));
23
+ pkResult = await tx.insertOrUpdate(schema.tableName, mainRecord, conflictCols, pkCol);
24
+ }
25
+ else {
26
+ pkResult = await tx.insert(schema.tableName, mainRecord, pkCol);
27
+ }
28
+ const mainPkCamel = camelcaseObject(pkResult, schema);
29
+ // 4. Secondaries: need full record for FK auto-fill (camelCase)
30
+ let secondaryResults;
31
+ if (secondaries && Object.keys(secondaries).length > 0) {
32
+ const mainForFK = { ...inputRecord, ...mainPkCamel };
33
+ secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
34
+ }
35
+ // 5. afterInsert hook (camelCase — input merged with generated PK)
36
+ if (tableConf.afterInsert) {
37
+ const mainForHook = { ...inputRecord, ...mainPkCamel };
38
+ await tableConf.afterInsert(tx, request, mainForHook, secondaryResults);
39
+ }
40
+ // 6. Return PK-only
41
+ const result = { main: mainPkCamel };
42
+ if (secondaryResults && Object.keys(secondaryResults).length > 0) {
43
+ result.secondaries = secondaryResults;
44
+ }
45
+ return result;
46
+ });
42
47
  }
@@ -1 +1 @@
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
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAqH9E"}
@@ -3,17 +3,19 @@ import { processSecondaries, processDeletions } from '../write-helpers.js';
3
3
  import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, } from '../../tenant.js';
4
4
  import { runValidation } from '../validate.js';
5
5
  import { httpError } from '../../errors.js';
6
- import { primaryAsString } from '../../../types.js';
7
6
  export async function updateEngine(params) {
8
7
  const { db, tableConf, dbTables, request, record, secondaries, deletions, tenant } = params;
9
8
  const schema = tableConf.Schema;
10
- const pk = primaryAsString(tableConf.primary);
11
- const pkCol = schema.col(pk);
12
- const pkValue = record[pk];
13
- if (pkValue == null)
14
- throw httpError(400, `Primary key "${pk}" is required`);
9
+ // Every primary key field/column: composite PKs must match on ALL columns, otherwise an
10
+ // UPDATE keyed on the first column alone would hit every row sharing that value.
11
+ const pkFields = Array.isArray(tableConf.primary) ? tableConf.primary : [tableConf.primary];
12
+ const pkCols = pkFields.map((f) => schema.col(f));
13
+ const pkValues = pkFields.map((f) => record[f]);
14
+ const missingIdx = pkValues.findIndex((v) => v == null);
15
+ if (missingIdx !== -1)
16
+ throw httpError(400, `Primary key "${pkFields[missingIdx]}" is required`);
15
17
  // 1. Tenant ownership check (indirect) — do it early so user mutations don't run on records they can't access
16
- await assertTenantOwnership(db, tenant, schema.tableName, pkCol, pkValue);
18
+ await assertTenantOwnership(db, tenant, schema.tableName, pkCols, pkValues);
17
19
  // 2. Mutable copy of input in camelCase
18
20
  const inputRecord = { ...record };
19
21
  // 3. Custom validation (camelCase)
@@ -25,57 +27,69 @@ export async function updateEngine(params) {
25
27
  // 5. Convert to DB format (after all user mutations)
26
28
  const snaked = snakecaseRecord(inputRecord, schema);
27
29
  const updateFields = { ...snaked };
28
- delete updateFields[pkCol];
30
+ for (const c of pkCols)
31
+ delete updateFields[c];
29
32
  // 6. Strip tenant column (user cannot change tenant of an existing record)
30
33
  if (tenant) {
31
34
  stripTenantColumn(updateFields, tenant.scope);
32
35
  }
33
- // 7. Update main
34
- const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
35
- const extraCondition = buildTenantUpdateExtra(tenant);
36
- if (hasFieldsToUpdate) {
37
- const affectedRows = await db.update(schema.tableName, updateFields, { [pkCol]: pkValue }, extraCondition);
38
- if (affectedRows === 0)
39
- throw httpError(404, `Record not found`);
40
- }
41
- else {
42
- // No fields to update: verify the record exists (for secondaries/deletions)
43
- let whereSql = `${db.qi(schema.tableName)}.${db.qi(pkCol)} = ${db.ph(1)}`;
44
- const whereValues = [pkValue];
45
- if (extraCondition) {
46
- whereSql += ` AND ${extraCondition.build(2, db.ph)}`;
47
- whereValues.push(...extraCondition.getValues());
36
+ // Steps 7-9 are atomic: a failure in secondaries or deletions rolls back the main
37
+ // update too. Degrades to non-transactional when the adapter has no connect().
38
+ return db.withTransaction(async (tx) => {
39
+ // 7. Update main
40
+ const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
41
+ const extraCondition = buildTenantUpdateExtra(tx, tenant);
42
+ // WHERE matching every primary key column (single or composite).
43
+ const whereByPk = Object.fromEntries(pkCols.map((c, i) => [c, pkValues[i]]));
44
+ if (hasFieldsToUpdate) {
45
+ const affectedRows = await tx.update(schema.tableName, updateFields, whereByPk, extraCondition);
46
+ if (affectedRows === 0)
47
+ throw httpError(404, `Record not found`);
48
48
  }
49
- const rows = await db.select({
50
- tableName: schema.tableName,
51
- where: whereSql,
52
- values: whereValues,
53
- limit: '1',
54
- });
55
- if (rows.length === 0)
56
- throw httpError(404, `Record not found`);
57
- }
58
- // Build the main response (PK-only)
59
- const mainResult = { [pk]: pkValue };
60
- // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
61
- let secondaryResults;
62
- if (secondaries && Object.keys(secondaries).length > 0) {
63
- const mainForFK = { ...inputRecord };
64
- secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
65
- }
66
- // 9. Deletions (FK auto-fill from main like secondaries)
67
- let deletionResults;
68
- if (deletions && Object.keys(deletions).length > 0) {
69
- const mainForFK = { ...inputRecord };
70
- deletionResults = await processDeletions(db, tableConf, mainForFK, deletions);
71
- }
72
- // 10. Return PK-only
73
- const result = { main: mainResult };
74
- if (secondaryResults && Object.keys(secondaryResults).length > 0) {
75
- result.secondaries = secondaryResults;
76
- }
77
- if (deletionResults && Object.keys(deletionResults).length > 0) {
78
- result.deletions = deletionResults;
79
- }
80
- return result;
49
+ else {
50
+ // No fields to update: verify the record exists (for secondaries/deletions)
51
+ const whereParts = pkCols.map((c, i) => `${tx.qi(schema.tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
52
+ let whereSql = whereParts.join(' AND ');
53
+ const whereValues = [...pkValues];
54
+ if (extraCondition) {
55
+ whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
56
+ whereValues.push(...extraCondition.getValues());
57
+ }
58
+ const rows = await tx.select({
59
+ tableName: schema.tableName,
60
+ where: whereSql,
61
+ values: whereValues,
62
+ limit: '1',
63
+ });
64
+ if (rows.length === 0)
65
+ throw httpError(404, `Record not found`);
66
+ }
67
+ // Build the main response (PK-only — every PK field)
68
+ const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
69
+ // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
70
+ let secondaryResults;
71
+ if (secondaries && Object.keys(secondaries).length > 0) {
72
+ const mainForFK = { ...inputRecord };
73
+ secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
74
+ }
75
+ // 9. Deletions (FK auto-fill from main like secondaries)
76
+ let deletionResults;
77
+ if (deletions && Object.keys(deletions).length > 0) {
78
+ const mainForFK = { ...inputRecord };
79
+ deletionResults = await processDeletions(tx, tableConf, mainForFK, deletions);
80
+ }
81
+ // 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
82
+ if (tableConf.afterUpdate) {
83
+ await tableConf.afterUpdate(tx, request, inputRecord, secondaryResults, deletionResults);
84
+ }
85
+ // 11. Return PK-only
86
+ const result = { main: mainResult };
87
+ if (secondaryResults && Object.keys(secondaryResults).length > 0) {
88
+ result.secondaries = secondaryResults;
89
+ }
90
+ if (deletionResults && Object.keys(deletionResults).length > 0) {
91
+ result.deletions = deletionResults;
92
+ }
93
+ return result;
94
+ });
81
95
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAab,MAAM,mBAAmB,CAAC;AAy/B3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAoIvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAcb,MAAM,mBAAmB,CAAC;AAmiC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAoIvB"}