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.
- package/AGENTS_BACKEND.md +18 -14
- package/AGENTS_FRONTEND.md +78 -1
- package/README.md +117 -18
- package/dist/bin/generate-schema.js +3 -3
- package/dist/bin/generate-tables.js +3 -3
- package/dist/bin/utils.d.ts +1 -1
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +2 -2
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +9 -2
- package/dist/lib/cli/schema-codegen.d.ts +1 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +11 -4
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +4 -3
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +10 -4
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +40 -36
- 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 +24 -23
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +41 -40
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +235 -39
- package/dist/lib/engine/write-helpers.js +4 -4
- package/dist/lib/naming.d.ts +11 -2
- package/dist/lib/naming.d.ts.map +1 -1
- package/dist/lib/naming.js +33 -5
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +16 -10
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +9 -1
- package/dist/types.d.ts +26 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
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;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;
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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,
|
|
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 {
|
|
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 =
|
|
12
|
-
// 1.
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
34
|
-
|
|
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) =>
|
|
53
|
-
pkRows = await db.bulkInsertOrUpdate(
|
|
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(
|
|
58
|
+
pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
|
|
57
59
|
}
|
|
58
|
-
|
|
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 =
|
|
64
|
-
|
|
65
|
-
// For
|
|
66
|
-
|
|
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
|
-
//
|
|
81
|
+
// 8. afterInsert hook per item (camelCase)
|
|
78
82
|
if (tableConf.afterInsert) {
|
|
79
83
|
await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
|
|
80
84
|
}
|
|
@@ -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,
|
|
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 =
|
|
10
|
-
// 1.
|
|
11
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
36
|
+
const upsertKeys = tableConf.upsertMap?.get(schema);
|
|
35
37
|
if (upsertKeys) {
|
|
36
|
-
const conflictCols = upsertKeys.map((k) =>
|
|
37
|
-
pkResult = await db.insertOrUpdate(
|
|
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(
|
|
42
|
+
pkResult = await db.insert(schema.tableName, mainRecord, pkCol);
|
|
41
43
|
}
|
|
42
|
-
const mainPkCamel = camelcaseObject(pkResult);
|
|
43
|
-
//
|
|
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
|
-
|
|
47
|
-
const mainForFK = { ...camelcaseObject(mainRecord), ...mainPkCamel };
|
|
48
|
+
const mainForFK = { ...inputRecord, ...mainPkCamel };
|
|
48
49
|
secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
|
|
49
50
|
}
|
|
50
|
-
//
|
|
51
|
+
// 8. afterInsert hook (camelCase — input merged with generated PK)
|
|
51
52
|
if (tableConf.afterInsert) {
|
|
52
|
-
const mainForHook = { ...
|
|
53
|
+
const mainForHook = { ...inputRecord, ...mainPkCamel };
|
|
53
54
|
await tableConf.afterInsert(db, request, mainForHook, secondaryResults);
|
|
54
55
|
}
|
|
55
|
-
//
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
9
|
+
const schema = tableConf.Schema;
|
|
10
10
|
const pk = primaryAsString(tableConf.primary);
|
|
11
|
-
const pkCol =
|
|
12
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
//
|
|
94
|
+
// 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
|
|
93
95
|
let secondaryResults;
|
|
94
96
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
95
|
-
|
|
96
|
-
const mainForFK = { ...camelcaseObject(snaked) };
|
|
97
|
+
const mainForFK = { ...inputRecord };
|
|
97
98
|
secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
|
|
98
99
|
}
|
|
99
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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"}
|