fastify-auto-sqlapi 0.1.2 → 0.1.4
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 +41 -2
- package/AGENTS_FRONTEND.md +7 -5
- package/README.md +105 -5
- 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 +25 -8
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +12 -9
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +9 -5
- 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 +41 -34
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +74 -57
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +94 -34
- package/dist/lib/schema/bulk-delete.d.ts +1 -1
- package/dist/lib/schema/bulk-delete.d.ts.map +1 -1
- package/dist/lib/schema/bulk-delete.js +2 -2
- package/dist/lib/schema/bulk-upsert.d.ts +1 -1
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +5 -3
- package/dist/lib/schema/helpers.d.ts +2 -0
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +3 -2
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +4 -1
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +4 -1
- package/dist/lib/sql-api.d.ts +2 -0
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +1 -0
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +10 -9
- package/dist/lib/tenant.d.ts +24 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +0 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +3 -1
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +3 -1
- 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/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 +9 -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/plugin.d.ts +16 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/plugin.js +4 -1
- package/dist/types/schema.d.ts +2 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/search.d.ts +7 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/table.d.ts +40 -2
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { snakecaseRecord } from '../../naming.js';
|
|
2
2
|
import { processSecondaries, processDeletions } from '../write-helpers.js';
|
|
3
|
-
import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, } from '../../tenant.js';
|
|
3
|
+
import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } 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,72 @@ 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
|
-
|
|
29
|
-
|
|
30
|
+
for (const c of pkCols)
|
|
31
|
+
delete updateFields[c];
|
|
32
|
+
// 6. Strip tenant column (user cannot change tenant of an existing record).
|
|
33
|
+
// For indirect scopes the tenant link is the through-FK (localField): it may be changed,
|
|
34
|
+
// but only to another value the caller owns — re-validate the new FK against the tenant.
|
|
30
35
|
if (tenant) {
|
|
31
36
|
stripTenantColumn(updateFields, tenant.scope);
|
|
37
|
+
await enforceTenantFKOnUpdate(db, tenant, updateFields);
|
|
32
38
|
}
|
|
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());
|
|
39
|
+
// Steps 7-9 are atomic: a failure in secondaries or deletions rolls back the main
|
|
40
|
+
// update too. Degrades to non-transactional when the adapter has no connect().
|
|
41
|
+
return db.withTransaction(async (tx) => {
|
|
42
|
+
// 7. Update main
|
|
43
|
+
const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
|
|
44
|
+
const extraCondition = buildTenantUpdateExtra(tx, tenant);
|
|
45
|
+
// WHERE matching every primary key column (single or composite).
|
|
46
|
+
const whereByPk = Object.fromEntries(pkCols.map((c, i) => [c, pkValues[i]]));
|
|
47
|
+
if (hasFieldsToUpdate) {
|
|
48
|
+
const affectedRows = await tx.update(schema.tableName, updateFields, whereByPk, extraCondition);
|
|
49
|
+
if (affectedRows === 0)
|
|
50
|
+
throw httpError(404, `Record not found`);
|
|
48
51
|
}
|
|
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
|
-
|
|
52
|
+
else {
|
|
53
|
+
// No fields to update: verify the record exists (for secondaries/deletions)
|
|
54
|
+
const whereParts = pkCols.map((c, i) => `${tx.qi(schema.tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
|
|
55
|
+
let whereSql = whereParts.join(' AND ');
|
|
56
|
+
const whereValues = [...pkValues];
|
|
57
|
+
if (extraCondition) {
|
|
58
|
+
whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
|
|
59
|
+
whereValues.push(...extraCondition.getValues());
|
|
60
|
+
}
|
|
61
|
+
const rows = await tx.select({
|
|
62
|
+
tableName: schema.tableName,
|
|
63
|
+
where: whereSql,
|
|
64
|
+
values: whereValues,
|
|
65
|
+
limit: '1',
|
|
66
|
+
});
|
|
67
|
+
if (rows.length === 0)
|
|
68
|
+
throw httpError(404, `Record not found`);
|
|
69
|
+
}
|
|
70
|
+
// Build the main response (PK-only — every PK field)
|
|
71
|
+
const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
|
|
72
|
+
// 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
|
|
73
|
+
let secondaryResults;
|
|
74
|
+
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
75
|
+
const mainForFK = { ...inputRecord };
|
|
76
|
+
secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
|
|
77
|
+
}
|
|
78
|
+
// 9. Deletions (FK auto-fill from main like secondaries)
|
|
79
|
+
let deletionResults;
|
|
80
|
+
if (deletions && Object.keys(deletions).length > 0) {
|
|
81
|
+
const mainForFK = { ...inputRecord };
|
|
82
|
+
deletionResults = await processDeletions(tx, tableConf, mainForFK, deletions);
|
|
83
|
+
}
|
|
84
|
+
// 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
|
|
85
|
+
if (tableConf.afterUpdate) {
|
|
86
|
+
await tableConf.afterUpdate(tx, request, inputRecord, secondaryResults, deletionResults);
|
|
87
|
+
}
|
|
88
|
+
// 11. Return PK-only
|
|
89
|
+
const result = { main: mainResult };
|
|
90
|
+
if (secondaryResults && Object.keys(secondaryResults).length > 0) {
|
|
91
|
+
result.secondaries = secondaryResults;
|
|
92
|
+
}
|
|
93
|
+
if (deletionResults && Object.keys(deletionResults).length > 0) {
|
|
94
|
+
result.deletions = deletionResults;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
});
|
|
81
98
|
}
|
|
@@ -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;AA4jC3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA2IvB"}
|
|
@@ -50,6 +50,21 @@ function resolveFieldRef(field, schema, tableConf, db, alias) {
|
|
|
50
50
|
err400(`Unknown field: ${field}`);
|
|
51
51
|
}
|
|
52
52
|
// ─── Join lookup ────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Tenant-scope a join side-query. When the joined table declares its own `tenantScope`,
|
|
55
|
+
* appends the tenant condition to `cb` (direct: `col IN ids`; indirect: condition on the
|
|
56
|
+
* through table) and, for indirect scopes, pushes the INNER JOIN clause into `joins`.
|
|
57
|
+
* No-op without a tenant context (admin) or when the join table is not tenant-scoped.
|
|
58
|
+
*/
|
|
59
|
+
function appendJoinTenantScope(db, joinTableConf, tenant, joinTableName, cb, joins) {
|
|
60
|
+
const scope = joinTableConf?.tenantScope;
|
|
61
|
+
if (!tenant || !scope)
|
|
62
|
+
return;
|
|
63
|
+
cb.append(buildTenantCondition(db, scope, tenant.ids));
|
|
64
|
+
if ('through' in scope) {
|
|
65
|
+
joins?.push(buildTenantJoin(db, scope, joinTableName));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
53
68
|
function findJoinByAlias(tableConf, alias) {
|
|
54
69
|
return tableConf.allowedReadJoins?.find((j) => j.alias === alias);
|
|
55
70
|
}
|
|
@@ -156,6 +171,28 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
|
|
|
156
171
|
const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
|
|
157
172
|
return { expr, values: filterVals };
|
|
158
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
|
|
176
|
+
* Needed BEFORE building the LEFT JOIN clauses (which must know their aliases) while the actual
|
|
177
|
+
* orderBy SQL — with its parameter placeholders — is only baked later, once the LEFT JOIN value
|
|
178
|
+
* count is known. Validates each alias against `allowedReadJoins` (throws 400), mirroring
|
|
179
|
+
* `validateOrderBy`; 3-parti aggregation entries are ignored (they use joinGroup, not a LEFT JOIN).
|
|
180
|
+
*/
|
|
181
|
+
function collectOrderByLeftAliases(orderBy, tableConf) {
|
|
182
|
+
const aliases = new Set();
|
|
183
|
+
for (const part of orderBy.split(',')) {
|
|
184
|
+
const trimmed = part.trim();
|
|
185
|
+
if (/^(\w+)\.(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i.test(trimmed))
|
|
186
|
+
continue; // 3-parti aggregation
|
|
187
|
+
const m = trimmed.match(/^(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
188
|
+
if (m) {
|
|
189
|
+
const alias = m[1];
|
|
190
|
+
requireJoin(tableConf, alias, true);
|
|
191
|
+
aliases.add(alias);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return aliases;
|
|
195
|
+
}
|
|
159
196
|
function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx) {
|
|
160
197
|
const parts = orderBy.split(',');
|
|
161
198
|
const outParts = [];
|
|
@@ -219,7 +256,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
219
256
|
// Note: extraFilters declared on joinTableConf are not supported here for
|
|
220
257
|
// joinLeft (they would require alias-aware handlers); only schema fields
|
|
221
258
|
// and computed fields apply.
|
|
222
|
-
const cb = new ConditionBuilder('AND');
|
|
259
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
223
260
|
const joinTableConf = dbTables[joinSchema.tableName];
|
|
224
261
|
const computed = joinTableConf?.computedFields;
|
|
225
262
|
if (ref.filters) {
|
|
@@ -266,12 +303,14 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
266
303
|
return { joinClauses, whereExtras, values };
|
|
267
304
|
}
|
|
268
305
|
// ─── Main query execution ───────────────────────────────────
|
|
269
|
-
async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed) {
|
|
306
|
+
async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed, maxRows) {
|
|
270
307
|
const tableName = tableConf.Schema.tableName;
|
|
271
308
|
const order = orderBy || tableConf.defaultOrder || primaryAsString(tableConf.primary);
|
|
309
|
+
// With a paginator, the page size governs the LIMIT. Without one, apply `maxRows` (if set) so an
|
|
310
|
+
// unbounded search cannot dump the whole table; a plain integer is a safe, non-injectable LIMIT.
|
|
272
311
|
const limit = paginator
|
|
273
312
|
? `${paginator.itemsPerPage} OFFSET ${(paginator.page - 1) * paginator.itemsPerPage}`
|
|
274
|
-
: null;
|
|
313
|
+
: (maxRows != null ? String(maxRows) : null);
|
|
275
314
|
// Optional computed projections — bound values, if any, are NOT yet supported
|
|
276
315
|
// here (would require placeholder-aware composition with WHERE values).
|
|
277
316
|
let columns;
|
|
@@ -328,7 +367,7 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
|
|
|
328
367
|
};
|
|
329
368
|
}
|
|
330
369
|
// ─── joinMultiple (virtual fetch, child rows in side query) ─
|
|
331
|
-
async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple) {
|
|
370
|
+
async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
|
|
332
371
|
const result = {};
|
|
333
372
|
for (const [alias, ref] of Object.entries(joinMultiple)) {
|
|
334
373
|
const joinDef = requireJoin(tableConf, alias, false);
|
|
@@ -342,6 +381,8 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
|
|
|
342
381
|
const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
|
|
343
382
|
const fkCol = joinSchema.col(joinField);
|
|
344
383
|
cb.isIn(db.qi(fkCol), ids);
|
|
384
|
+
const tenantJoins = [];
|
|
385
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
345
386
|
let where = cb.build(1, db.ph);
|
|
346
387
|
let values = cb.getValues();
|
|
347
388
|
// Side-channel: computed-field clauses on the join table
|
|
@@ -356,17 +397,19 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
|
|
|
356
397
|
columns,
|
|
357
398
|
where,
|
|
358
399
|
values,
|
|
400
|
+
joins: tenantJoins.length > 0 ? tenantJoins : undefined,
|
|
359
401
|
});
|
|
360
402
|
result[alias] = mapRowsToCamelCase(rows, joinSchema);
|
|
361
403
|
}
|
|
362
404
|
return result;
|
|
363
405
|
}
|
|
364
406
|
// ─── joinLeft (parent fetch via PK IN side query) ───────────
|
|
365
|
-
async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
407
|
+
async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
|
|
366
408
|
const result = {};
|
|
367
409
|
for (const [alias, ref] of Object.entries(joinLeft)) {
|
|
368
410
|
const joinDef = requireJoin(tableConf, alias, true);
|
|
369
411
|
const { joinSchema, joinField, mainField, selection: defaultSelection } = joinDef;
|
|
412
|
+
const joinTableConf = dbTables[joinSchema.tableName];
|
|
370
413
|
// For joinLeft (N:1), mainField on main is the FK pointing to joinField (PK) on parent.
|
|
371
414
|
// We collect the FK values from the main results and look up parents by their PK.
|
|
372
415
|
const ids = collectIds(mainResults, mainField);
|
|
@@ -375,8 +418,10 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
|
375
418
|
continue;
|
|
376
419
|
}
|
|
377
420
|
const fkCol = joinSchema.col(joinField);
|
|
378
|
-
const cb = new ConditionBuilder('AND');
|
|
421
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
379
422
|
cb.isIn(db.qi(fkCol), ids);
|
|
423
|
+
const tenantJoins = [];
|
|
424
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
380
425
|
const where = cb.build(1, db.ph);
|
|
381
426
|
const values = cb.getValues();
|
|
382
427
|
const selection = ref?.selection ?? defaultSelection;
|
|
@@ -386,6 +431,7 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
|
386
431
|
columns,
|
|
387
432
|
where,
|
|
388
433
|
values,
|
|
434
|
+
joins: tenantJoins.length > 0 ? tenantJoins : undefined,
|
|
389
435
|
});
|
|
390
436
|
result[alias] = mapRowsToCamelCase(rows, joinSchema);
|
|
391
437
|
}
|
|
@@ -412,7 +458,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
|
|
|
412
458
|
err400(`Unknown field: ${by}`);
|
|
413
459
|
}
|
|
414
460
|
// ─── joinGroup (aggregations) ───────────────────────────────
|
|
415
|
-
async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup) {
|
|
461
|
+
async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup, tenant) {
|
|
416
462
|
const result = {};
|
|
417
463
|
for (const [alias, groupReq] of Object.entries(joinGroup)) {
|
|
418
464
|
const joinDef = requireJoin(tableConf, alias, false);
|
|
@@ -454,6 +500,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
|
|
|
454
500
|
const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
|
|
455
501
|
const fkCol = joinSchema.col(joinField);
|
|
456
502
|
cb.isIn(db.qi(fkCol), ids);
|
|
503
|
+
const tenantJoins = [];
|
|
504
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
457
505
|
let where = cb.build(1, db.ph);
|
|
458
506
|
let values = cb.getValues();
|
|
459
507
|
// Side-channel: computed-field clauses on the join table
|
|
@@ -462,7 +510,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
|
|
|
462
510
|
({ where, values } = appendComputedConditions(where, values, computedClauses, db));
|
|
463
511
|
}
|
|
464
512
|
const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
|
|
465
|
-
const
|
|
513
|
+
const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
|
|
514
|
+
const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
|
|
466
515
|
const queryResult = await db.query(sql, values);
|
|
467
516
|
const rows = queryResult.rows;
|
|
468
517
|
const formatted = {};
|
|
@@ -520,8 +569,8 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
|
|
|
520
569
|
}
|
|
521
570
|
function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
|
|
522
571
|
const cb = (ref.filters && joinTableConf)
|
|
523
|
-
? joinTableConf.filters(ref.filters)
|
|
524
|
-
: new ConditionBuilder('AND');
|
|
572
|
+
? joinTableConf.filters(ref.filters, db.cbDialect)
|
|
573
|
+
: new ConditionBuilder('AND', db.cbDialect);
|
|
525
574
|
if (ref.conditions?.length) {
|
|
526
575
|
for (const c of ref.conditions) {
|
|
527
576
|
if (!ALLOWED_SET.has(c.method)) {
|
|
@@ -641,7 +690,7 @@ function appendComputedConditions(currentWhere, currentValues, clauses, db) {
|
|
|
641
690
|
// Append the computed's own bound values first; the expr placeholders
|
|
642
691
|
// (if any) reference them via stable indices set by the consumer.
|
|
643
692
|
values = [...values, ...c.values];
|
|
644
|
-
const tmpCb = new ConditionBuilder('AND');
|
|
693
|
+
const tmpCb = new ConditionBuilder('AND', db.cbDialect);
|
|
645
694
|
dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
|
|
646
695
|
const startIdx = values.length + 1;
|
|
647
696
|
const clauseSql = tmpCb.build(startIdx, db.ph);
|
|
@@ -666,7 +715,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
|
|
|
666
715
|
const [alias, fn, field] = parts;
|
|
667
716
|
const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
|
|
668
717
|
values = [...values, ...exprValues];
|
|
669
|
-
const tmpCb = new ConditionBuilder('AND');
|
|
718
|
+
const tmpCb = new ConditionBuilder('AND', db.cbDialect);
|
|
670
719
|
dispatchConditionMethod(tmpCb, c.method, expr, c.params);
|
|
671
720
|
const startIdx = values.length + 1;
|
|
672
721
|
const clause = tmpCb.build(startIdx, db.ph);
|
|
@@ -676,7 +725,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
|
|
|
676
725
|
return { where, values };
|
|
677
726
|
}
|
|
678
727
|
// ─── joinMustExist (EXISTS subquery filter on main) ─────────
|
|
679
|
-
function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues) {
|
|
728
|
+
function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
|
|
680
729
|
let where = currentWhere;
|
|
681
730
|
const values = [...currentValues];
|
|
682
731
|
for (const [alias, ref] of Object.entries(joinMustExist)) {
|
|
@@ -685,6 +734,8 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
|
|
|
685
734
|
const joinTableConf = dbTables[joinSchema.tableName];
|
|
686
735
|
const refs = extractJoinRefs(db, tableConf, joinDef);
|
|
687
736
|
const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
|
|
737
|
+
const tenantJoins = [];
|
|
738
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
|
|
688
739
|
const startIdx = values.length + 1;
|
|
689
740
|
let filterWhere = filterCondition.build(startIdx, db.ph);
|
|
690
741
|
let filterVals = [...filterCondition.getValues()];
|
|
@@ -699,10 +750,13 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
|
|
|
699
750
|
filterVals = cvalues.slice(values.length);
|
|
700
751
|
filterWhere = cwhere;
|
|
701
752
|
}
|
|
753
|
+
// Qualify the FK with the join table: the EXISTS FROM may include the tenant
|
|
754
|
+
// through-join, so a bare column could become ambiguous.
|
|
702
755
|
const innerWhere = filterWhere
|
|
703
|
-
? `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
|
|
704
|
-
: `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
|
|
705
|
-
|
|
756
|
+
? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
|
|
757
|
+
: `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
|
|
758
|
+
const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
|
|
759
|
+
where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
|
|
706
760
|
values.push(...filterVals);
|
|
707
761
|
}
|
|
708
762
|
return { where, values };
|
|
@@ -724,7 +778,7 @@ export async function searchEngine(dbTables, params) {
|
|
|
724
778
|
for (const a of Object.keys(joinLeft))
|
|
725
779
|
requireJoin(tableConf, a, true);
|
|
726
780
|
// Build main condition
|
|
727
|
-
const condition = tableConf.filters(filters || {});
|
|
781
|
+
const condition = tableConf.filters(filters || {}, db.cbDialect);
|
|
728
782
|
// Filters that target computed fields (declared on tableConf.computedFields)
|
|
729
783
|
// are applied via the side-channel pattern further down.
|
|
730
784
|
let computedFilterClauses = [];
|
|
@@ -755,7 +809,7 @@ export async function searchEngine(dbTables, params) {
|
|
|
755
809
|
}
|
|
756
810
|
// joinMustExist (EXISTS)
|
|
757
811
|
if (joinMustExist && Object.keys(joinMustExist).length > 0) {
|
|
758
|
-
({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values));
|
|
812
|
+
({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
|
|
759
813
|
}
|
|
760
814
|
// Aggregation conditions (HAVING-style)
|
|
761
815
|
if (conditions?.length) {
|
|
@@ -764,27 +818,23 @@ export async function searchEngine(dbTables, params) {
|
|
|
764
818
|
({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
|
|
765
819
|
}
|
|
766
820
|
}
|
|
767
|
-
// orderBy parsing — collects 2-parti aliases that need LEFT JOIN on the main query
|
|
768
|
-
let safeOrderBy;
|
|
769
|
-
let orderByValues = [];
|
|
770
|
-
let orderByLeftAliases = new Set();
|
|
771
|
-
if (orderBy) {
|
|
772
|
-
const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + 1);
|
|
773
|
-
safeOrderBy = obResult.sql;
|
|
774
|
-
orderByValues = obResult.values;
|
|
775
|
-
orderByLeftAliases = obResult.leftJoinAliases;
|
|
776
|
-
}
|
|
777
821
|
// Determine which joinLeft aliases need a LEFT JOIN on the main query:
|
|
778
822
|
// - any alias used in 2-parti orderBy
|
|
779
823
|
// - any alias in joinLeft body that has filters or conditions on the parent
|
|
780
|
-
const aliasesNeedingLeftJoin = new Set(
|
|
824
|
+
const aliasesNeedingLeftJoin = new Set();
|
|
825
|
+
if (orderBy) {
|
|
826
|
+
for (const a of collectOrderByLeftAliases(orderBy, tableConf))
|
|
827
|
+
aliasesNeedingLeftJoin.add(a);
|
|
828
|
+
}
|
|
781
829
|
if (joinLeft) {
|
|
782
830
|
for (const [alias, ref] of Object.entries(joinLeft)) {
|
|
783
831
|
if (ref?.filters || ref?.conditions?.length)
|
|
784
832
|
aliasesNeedingLeftJoin.add(alias);
|
|
785
833
|
}
|
|
786
834
|
}
|
|
787
|
-
// Build LEFT JOIN clauses + extra WHERE for filtered parents
|
|
835
|
+
// Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
|
|
836
|
+
// after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
|
|
837
|
+
// whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
|
|
788
838
|
const extraJoinClauses = [...tenantJoins];
|
|
789
839
|
let leftJoinValues = [];
|
|
790
840
|
if (aliasesNeedingLeftJoin.size > 0) {
|
|
@@ -794,24 +844,34 @@ export async function searchEngine(dbTables, params) {
|
|
|
794
844
|
for (const w of lj.whereExtras)
|
|
795
845
|
where += ` AND ${w}`;
|
|
796
846
|
}
|
|
847
|
+
// orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
|
|
848
|
+
// them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
|
|
849
|
+
// `values.length + 1` misbinds when BOTH an aggregation orderBy and a filtered joinLeft exist.
|
|
850
|
+
let safeOrderBy;
|
|
851
|
+
let orderByValues = [];
|
|
852
|
+
if (orderBy) {
|
|
853
|
+
const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
|
|
854
|
+
safeOrderBy = obResult.sql;
|
|
855
|
+
orderByValues = obResult.values;
|
|
856
|
+
}
|
|
797
857
|
// Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
|
|
798
858
|
// Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
|
|
799
859
|
const whereAndJoinValues = [...values, ...leftJoinValues];
|
|
800
860
|
const mainValues = [...whereAndJoinValues, ...orderByValues];
|
|
801
|
-
const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed);
|
|
861
|
+
const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
|
|
802
862
|
let pagination;
|
|
803
863
|
if (paginator) {
|
|
804
864
|
pagination = await buildPagination(db, tableConf, where, whereAndJoinValues, paginator, extraJoinClauses, computeMin, computeMax, computeSum, computeAvg);
|
|
805
865
|
}
|
|
806
866
|
const result = { main };
|
|
807
867
|
if (joinMultiple && Object.keys(joinMultiple).length > 0) {
|
|
808
|
-
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple);
|
|
868
|
+
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
|
|
809
869
|
}
|
|
810
870
|
if (joinLeft && Object.keys(joinLeft).length > 0) {
|
|
811
|
-
result.joinLeft = await executeJoinLeft(db, tableConf, main, joinLeft);
|
|
871
|
+
result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
|
|
812
872
|
}
|
|
813
873
|
if (joinGroup && Object.keys(joinGroup).length > 0) {
|
|
814
|
-
result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup);
|
|
874
|
+
result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
|
|
815
875
|
}
|
|
816
876
|
if (pagination) {
|
|
817
877
|
result.pagination = pagination;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbTables } from '../../types.js';
|
|
2
|
-
export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
2
|
+
export declare function BulkDeleteTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
3
3
|
[x: string]: any;
|
|
4
4
|
}>>;
|
|
5
5
|
export declare function BulkDeleteTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;
|
|
1
|
+
{"version":3,"file":"bulk-delete.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-delete.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;;IAS3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM;;;;IAU5E"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { primaryAsString } from '../../types.js';
|
|
3
|
-
export function BulkDeleteTableBody(dbTables, tableName) {
|
|
3
|
+
export function BulkDeleteTableBody(dbTables, tableName, maxItems) {
|
|
4
4
|
const tableConf = dbTables[tableName];
|
|
5
5
|
const pkField = primaryAsString(tableConf.primary);
|
|
6
6
|
const pkType = tableConf.Schema.fields[pkField];
|
|
7
|
-
return Type.Array(Type.Object({ [pkField]: pkType }));
|
|
7
|
+
return Type.Array(Type.Object({ [pkField]: pkType }, { additionalProperties: false }), maxItems != null ? { maxItems } : {});
|
|
8
8
|
}
|
|
9
9
|
export function BulkDeleteTableResponse(dbTables, tableName) {
|
|
10
10
|
const tableConf = dbTables[tableName];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TSchema } from '@sinclair/typebox';
|
|
2
2
|
import type { DbTables } from '../../types.js';
|
|
3
|
-
export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
|
|
3
|
+
export declare function BulkUpsertTableBody(dbTables: DbTables, tableName: string, maxItems?: number): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
|
|
4
4
|
export declare function BulkUpsertTableResponse(dbTables: DbTables, tableName: string): import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<Record<string, TSchema>>>;
|
|
5
5
|
//# sourceMappingURL=bulk-upsert.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAoB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { primaryAsString } from '../../types.js';
|
|
3
3
|
import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
|
|
4
|
-
export function BulkUpsertTableBody(dbTables, tableName) {
|
|
4
|
+
export function BulkUpsertTableBody(dbTables, tableName, maxItems) {
|
|
5
5
|
const tableConf = dbTables[tableName];
|
|
6
6
|
const schema = tableConf.Schema;
|
|
7
|
-
|
|
7
|
+
// additionalProperties:false makes the schema the real write whitelist (no mass assignment).
|
|
8
|
+
const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf), { additionalProperties: false }));
|
|
8
9
|
const itemProperties = {
|
|
9
10
|
main: mainSchema,
|
|
10
11
|
};
|
|
11
12
|
attachWriteJoinSections(itemProperties, tableConf, dbTables, {
|
|
12
13
|
withDeletions: true,
|
|
13
14
|
secondaryFields: writeJoinBodyFields,
|
|
15
|
+
strictItems: true,
|
|
14
16
|
});
|
|
15
|
-
return Type.Array(Type.Object(itemProperties));
|
|
17
|
+
return Type.Array(Type.Object(itemProperties), maxItems != null ? { maxItems } : {});
|
|
16
18
|
}
|
|
17
19
|
export function BulkUpsertTableResponse(dbTables, tableName) {
|
|
18
20
|
const tableConf = dbTables[tableName];
|
|
@@ -24,6 +24,7 @@ type JoinFieldsBuilder = (join: JoinDefinition, secondaryConf: ITable | undefine
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables, build: JoinFieldsBuilder, options?: {
|
|
26
26
|
itemsPartial?: boolean;
|
|
27
|
+
strictItems?: boolean;
|
|
27
28
|
}): Record<string, TSchema>;
|
|
28
29
|
/**
|
|
29
30
|
* Attach `secondaries` (and optionally `deletions`) sections to the given target object,
|
|
@@ -37,6 +38,7 @@ export declare function buildJoinAliasMap(tableConf: ITable, dbTables: DbTables,
|
|
|
37
38
|
export declare function attachWriteJoinSections(target: Record<string, TSchema>, tableConf: ITable, dbTables: DbTables, options: {
|
|
38
39
|
withDeletions: boolean;
|
|
39
40
|
secondaryFields: JoinFieldsBuilder;
|
|
41
|
+
strictItems?: boolean;
|
|
40
42
|
}): void;
|
|
41
43
|
/** JoinFieldsBuilder for body schemas: includes all join fields with FK + excluded made Optional. */
|
|
42
44
|
export declare const writeJoinBodyFields: JoinFieldsBuilder;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,IAAI,CAmBN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
|
|
@@ -56,9 +56,10 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
|
|
|
56
56
|
const out = {};
|
|
57
57
|
if (!tableConf.allowedWriteJoins?.length)
|
|
58
58
|
return out;
|
|
59
|
+
const objOpts = options.strictItems ? { additionalProperties: false } : {};
|
|
59
60
|
for (const j of tableConf.allowedWriteJoins) {
|
|
60
61
|
const sc = findSecondaryTableConf(dbTables, j.joinSchema.tableName);
|
|
61
|
-
const item = Type.Object(build(j, sc));
|
|
62
|
+
const item = Type.Object(build(j, sc), objOpts);
|
|
62
63
|
out[j.alias] = Type.Array(options.itemsPartial ? Type.Partial(item) : item);
|
|
63
64
|
}
|
|
64
65
|
return out;
|
|
@@ -75,7 +76,7 @@ export function buildJoinAliasMap(tableConf, dbTables, build, options = {}) {
|
|
|
75
76
|
export function attachWriteJoinSections(target, tableConf, dbTables, options) {
|
|
76
77
|
if (!tableConf.allowedWriteJoins?.length)
|
|
77
78
|
return;
|
|
78
|
-
target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields))));
|
|
79
|
+
target.secondaries = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, options.secondaryFields, { strictItems: options.strictItems }))));
|
|
79
80
|
if (options.withDeletions) {
|
|
80
81
|
target.deletions = Type.Optional(Type.Partial(Type.Object(buildJoinAliasMap(tableConf, dbTables, ({ joinSchema }) => joinSchema.fields, { itemsPartial: true }))));
|
|
81
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
|
|
@@ -12,11 +12,14 @@ export function InsertTableBody(dbTables, tableName) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
const bodyProperties = {
|
|
15
|
-
|
|
15
|
+
// additionalProperties:false makes the schema the real write whitelist — unknown keys are
|
|
16
|
+
// rejected (400) instead of silently reaching the INSERT as columns (mass assignment).
|
|
17
|
+
main: Type.Object(mainFields, { additionalProperties: false }),
|
|
16
18
|
};
|
|
17
19
|
attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
|
|
18
20
|
withDeletions: false,
|
|
19
21
|
secondaryFields: writeJoinBodyFields,
|
|
22
|
+
strictItems: true,
|
|
20
23
|
});
|
|
21
24
|
return Type.Object(bodyProperties);
|
|
22
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAwB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
|