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.
- package/AGENTS_BACKEND.md +35 -1
- package/README.md +65 -4
- package/dist/bin/generate-schema.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
- package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.js +28 -17
- package/dist/lib/adapters/pg-adapter.d.ts +4 -0
- package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/pg-adapter.js +18 -7
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +3 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +13 -4
- package/dist/lib/cli/schema-codegen.d.ts +2 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +17 -3
- package/dist/lib/cli/tables-codegen.d.ts +2 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +43 -12
- package/dist/lib/db.d.ts +17 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +47 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +33 -10
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +9 -8
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +18 -7
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +38 -33
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +69 -55
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +49 -19
- package/dist/lib/sql-api.js +2 -2
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +10 -9
- package/dist/lib/tenant.d.ts +9 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +31 -8
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +1 -0
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +1 -0
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +1 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +1 -0
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +1 -0
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +40 -5
- package/dist/routes/auto/route-helpers.d.ts +3 -1
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +3 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +1 -0
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +1 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/db.d.ts +11 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/dml.d.ts +2 -0
- package/dist/types/dml.d.ts.map +1 -1
- package/dist/types/schema.d.ts +2 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/table.d.ts +64 -2
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -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;
|
|
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":"
|
|
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"}
|
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
|
-
import { buildTenantDeleteWhere } from '../../tenant.js';
|
|
2
|
+
import { buildTenantDeleteWhere, assertTenantOwnsAll } from '../../tenant.js';
|
|
3
3
|
import { primaryAsString } from '../../../types.js';
|
|
4
4
|
export async function bulkDeleteEngine(params) {
|
|
5
|
-
const { db, tableConf, ids, tenant } = params;
|
|
5
|
+
const { db, tableConf, ids, tenant, request } = params;
|
|
6
6
|
if (!ids.length)
|
|
7
7
|
return [];
|
|
8
8
|
const pk = primaryAsString(tableConf.primary);
|
|
9
9
|
const pkCol = tableConf.Schema.col(pk);
|
|
10
10
|
const tableName = tableConf.Schema.tableName;
|
|
11
|
+
if (tableConf.beforeBulkDelete) {
|
|
12
|
+
// Enforce tenant ownership of every id before the hook (single batched SELECT), so it
|
|
13
|
+
// never runs for rows the tenant cannot access. Called once with all ids — no loop.
|
|
14
|
+
await assertTenantOwnsAll(db, tenant, tableName, pkCol, ids);
|
|
15
|
+
await tableConf.beforeBulkDelete(db, request, ids);
|
|
16
|
+
}
|
|
11
17
|
let where;
|
|
12
18
|
let values;
|
|
13
19
|
if (tenant) {
|
|
14
20
|
({ where, values } = buildTenantDeleteWhere(db, tableName, pkCol, ids, tenant));
|
|
15
21
|
}
|
|
16
22
|
else {
|
|
17
|
-
const cb = new ConditionBuilder('AND');
|
|
23
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
18
24
|
cb.isIn(db.qi(pkCol), ids);
|
|
19
25
|
where = cb.build(1, db.ph);
|
|
20
26
|
values = cb.getValues();
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|
|
39
|
+
}
|
|
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);
|
|
26
51
|
}
|
|
27
|
-
|
|
28
|
-
const deletedCount = result.affectedRows;
|
|
29
|
-
return ids.slice(0, deletedCount).map((id) => ({ main: { [pk]: id } }));
|
|
52
|
+
return results;
|
|
30
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
-
|
|
11
|
-
const pkCol = schema.col(
|
|
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
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
const result = { main:
|
|
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],
|
|
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":"
|
|
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"}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import { buildTenantDeleteWhere } from '../../tenant.js';
|
|
1
|
+
import { buildTenantDeleteWhere, assertTenantOwnsAll } from '../../tenant.js';
|
|
2
2
|
import { httpError } from '../../errors.js';
|
|
3
3
|
import { primaryAsString } from '../../../types.js';
|
|
4
4
|
export async function deleteEngine(params) {
|
|
5
|
-
const { db, tableConf, id, tenant } = params;
|
|
5
|
+
const { db, tableConf, id, tenant, request } = params;
|
|
6
6
|
const pk = primaryAsString(tableConf.primary);
|
|
7
7
|
const pkCol = tableConf.Schema.col(pk);
|
|
8
8
|
const tableName = tableConf.Schema.tableName;
|
|
9
|
+
if (tableConf.beforeDelete) {
|
|
10
|
+
// Enforce tenant ownership before the hook so it never runs for rows the tenant
|
|
11
|
+
// cannot access. (When no hook is defined, the tenant-scoped DELETE below already
|
|
12
|
+
// prevents deleting non-owned rows, so the extra SELECT is skipped.)
|
|
13
|
+
await assertTenantOwnsAll(db, tenant, tableName, pkCol, [id]);
|
|
14
|
+
await tableConf.beforeDelete(db, request, id);
|
|
15
|
+
}
|
|
9
16
|
if (!tenant) {
|
|
10
17
|
const affectedRows = await db.delete(tableName, { [pkCol]: id });
|
|
11
18
|
if (affectedRows === 0)
|
|
12
19
|
throw httpError(404, `Record not found: ${id}`);
|
|
13
|
-
return { main: { [pk]: id } };
|
|
14
20
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
}
|
|
19
30
|
return { main: { [pk]: id } };
|
|
20
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,
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
secondaryResults
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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":"
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
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"}
|