fastify-auto-sqlapi 0.1.3 → 0.1.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 +14 -2
- package/AGENTS_FRONTEND.md +7 -5
- package/README.md +57 -2
- package/dist/bin/generate-tables.js +19 -5
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +6 -4
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +3 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +2 -0
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +3 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +5 -2
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +107 -29
- package/dist/lib/read-access.d.ts +31 -0
- package/dist/lib/read-access.d.ts.map +1 -0
- package/dist/lib/read-access.js +53 -0
- 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/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +17 -5
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +4 -1
- package/dist/lib/sql-api-decorator.d.ts +18 -0
- package/dist/lib/sql-api-decorator.d.ts.map +1 -0
- package/dist/lib/sql-api-decorator.js +40 -0
- 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 +18 -0
- package/dist/lib/tenant.d.ts +22 -0
- 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 +2 -1
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +2 -1
- package/dist/routes/auto/plugin.d.ts +0 -6
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +2 -28
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +4 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +8 -0
- 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/search.d.ts +7 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/table.d.ts +13 -0
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
2
|
import { camelcaseObject } from '../../naming.js';
|
|
3
3
|
import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
|
|
4
|
+
import { assertReadable, assertFiltersReadable, readableSelectColumns } from '../../read-access.js';
|
|
4
5
|
import { primaryAsString } from '../../../types.js';
|
|
5
6
|
// ─── Schema field validation ────────────────────────────────
|
|
6
|
-
function validateSchemaField(field, schema) {
|
|
7
|
+
function validateSchemaField(field, schema, tableConf) {
|
|
7
8
|
if (!(field in schema.fields)) {
|
|
8
9
|
const err = new Error(`Unknown field: ${field}`);
|
|
9
10
|
err.statusCode = 400;
|
|
10
11
|
throw err;
|
|
11
12
|
}
|
|
13
|
+
assertReadable(tableConf, field);
|
|
12
14
|
return schema.col(field);
|
|
13
15
|
}
|
|
14
16
|
function err400(msg) {
|
|
@@ -37,6 +39,7 @@ function buildComputedContext(db, schema, alias) {
|
|
|
37
39
|
}
|
|
38
40
|
function resolveFieldRef(field, schema, tableConf, db, alias) {
|
|
39
41
|
if (field in schema.fields) {
|
|
42
|
+
assertReadable(tableConf, field);
|
|
40
43
|
const col = db.qi(schema.col(field));
|
|
41
44
|
const expr = alias ? `${db.qi(alias)}.${col}` : col;
|
|
42
45
|
return { expr, values: [], computed: false };
|
|
@@ -121,11 +124,19 @@ function mapRowsToCamelCase(rows, schema, computedFieldNames) {
|
|
|
121
124
|
return camel;
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Build the SELECT columns list for a join fetch: '*' or comma-separated quoted columns.
|
|
129
|
+
* A default '*' selection narrows to the join table's readable columns when it declares
|
|
130
|
+
* `readExclude`; an explicit selection naming an excluded field is rejected with 400.
|
|
131
|
+
*/
|
|
132
|
+
function buildSelectionColumns(selection, joinSchema, db, joinTableConf) {
|
|
133
|
+
if (selection === '*') {
|
|
134
|
+
return readableSelectColumns(joinTableConf, joinSchema, db) ?? '*';
|
|
135
|
+
}
|
|
136
|
+
return selection
|
|
137
|
+
.split(',')
|
|
138
|
+
.map((c) => db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf)))
|
|
139
|
+
.join(', ');
|
|
129
140
|
}
|
|
130
141
|
const AGG_FN = {
|
|
131
142
|
sum: { sql: 'SUM', distinct: false },
|
|
@@ -171,6 +182,28 @@ function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup,
|
|
|
171
182
|
const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} WHERE ${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${filterWhere}), 0)`;
|
|
172
183
|
return { expr, values: filterVals };
|
|
173
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Pre-scan `orderBy` for the joinLeft aliases referenced in 2-parti notation (`<alias>.<field>`).
|
|
187
|
+
* Needed BEFORE building the LEFT JOIN clauses (which must know their aliases) while the actual
|
|
188
|
+
* orderBy SQL — with its parameter placeholders — is only baked later, once the LEFT JOIN value
|
|
189
|
+
* count is known. Validates each alias against `allowedReadJoins` (throws 400), mirroring
|
|
190
|
+
* `validateOrderBy`; 3-parti aggregation entries are ignored (they use joinGroup, not a LEFT JOIN).
|
|
191
|
+
*/
|
|
192
|
+
function collectOrderByLeftAliases(orderBy, tableConf) {
|
|
193
|
+
const aliases = new Set();
|
|
194
|
+
for (const part of orderBy.split(',')) {
|
|
195
|
+
const trimmed = part.trim();
|
|
196
|
+
if (/^(\w+)\.(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i.test(trimmed))
|
|
197
|
+
continue; // 3-parti aggregation
|
|
198
|
+
const m = trimmed.match(/^(\w+)\.(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
199
|
+
if (m) {
|
|
200
|
+
const alias = m[1];
|
|
201
|
+
requireJoin(tableConf, alias, true);
|
|
202
|
+
aliases.add(alias);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return aliases;
|
|
206
|
+
}
|
|
174
207
|
function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx) {
|
|
175
208
|
const parts = orderBy.split(',');
|
|
176
209
|
const outParts = [];
|
|
@@ -197,7 +230,7 @@ function validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, startIdx)
|
|
|
197
230
|
if (dotted2) {
|
|
198
231
|
const [, alias, field, dir] = dotted2;
|
|
199
232
|
const joinDef = requireJoin(tableConf, alias, true);
|
|
200
|
-
const col = validateSchemaField(field, joinDef.joinSchema);
|
|
233
|
+
const col = validateSchemaField(field, joinDef.joinSchema, dbTables[joinDef.joinSchema.tableName]);
|
|
201
234
|
// Reference the LEFT JOIN'd table via its alias (SQL identifier).
|
|
202
235
|
outParts.push(`${db.qi(alias)}.${db.qi(col)} ${(dir || 'ASC').toUpperCase()}`);
|
|
203
236
|
leftJoinAliases.add(alias);
|
|
@@ -237,6 +270,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
237
270
|
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
238
271
|
const joinTableConf = dbTables[joinSchema.tableName];
|
|
239
272
|
const computed = joinTableConf?.computedFields;
|
|
273
|
+
assertFiltersReadable(ref.filters, joinTableConf);
|
|
240
274
|
if (ref.filters) {
|
|
241
275
|
for (const [field, value] of Object.entries(ref.filters)) {
|
|
242
276
|
if (value === null || value === undefined)
|
|
@@ -257,6 +291,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
257
291
|
err400(`Invalid condition method: ${c.method}`);
|
|
258
292
|
}
|
|
259
293
|
if (c.field in joinSchema.fields) {
|
|
294
|
+
assertReadable(dbTables[joinSchema.tableName], c.field);
|
|
260
295
|
const col = `${aliasIdent}.${db.qi(joinSchema.col(c.field))}`;
|
|
261
296
|
dispatchConditionMethod(cb, c.method, col, c.params ?? []);
|
|
262
297
|
}
|
|
@@ -281,17 +316,52 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
281
316
|
return { joinClauses, whereExtras, values };
|
|
282
317
|
}
|
|
283
318
|
// ─── Main query execution ───────────────────────────────────
|
|
284
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Converts a configured order (`defaultOrder` or the primary-key fallback) to SQL.
|
|
321
|
+
* Unlike the request `orderBy` (strictly validated by `validateOrderBy`), this is
|
|
322
|
+
* lenient for backward compatibility: tokens matching a camelCase schema field are
|
|
323
|
+
* mapped to their quoted DB column, computed fields (without bound values) expand
|
|
324
|
+
* to their expression, and anything else passes through unchanged as raw SQL.
|
|
325
|
+
*/
|
|
326
|
+
function convertConfiguredOrder(order, tableConf, db) {
|
|
327
|
+
return order
|
|
328
|
+
.split(',')
|
|
329
|
+
.map((part) => {
|
|
330
|
+
const trimmed = part.trim();
|
|
331
|
+
const match = trimmed.match(/^(\w+)(?:\s+(ASC|DESC))?$/i);
|
|
332
|
+
if (match) {
|
|
333
|
+
const [, field, dir] = match;
|
|
334
|
+
const suffix = dir ? ` ${dir.toUpperCase()}` : '';
|
|
335
|
+
if (field in tableConf.Schema.fields) {
|
|
336
|
+
return `${db.qi(tableConf.Schema.col(field))}${suffix}`;
|
|
337
|
+
}
|
|
338
|
+
const computed = tableConf.computedFields?.[field];
|
|
339
|
+
if (computed) {
|
|
340
|
+
const ev = evaluateComputedField(computed, tableConf.Schema, db, undefined, `Computed field '${field}' with bound values cannot be used in defaultOrder`);
|
|
341
|
+
return `${ev.expr}${suffix}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return trimmed;
|
|
345
|
+
})
|
|
346
|
+
.join(', ');
|
|
347
|
+
}
|
|
348
|
+
async function executeMainQuery(db, tableConf, where, values, orderBy, paginator, extraJoins = [], selectComputed, maxRows) {
|
|
285
349
|
const tableName = tableConf.Schema.tableName;
|
|
286
|
-
|
|
350
|
+
// Request orderBy arrives already validated and mapped by validateOrderBy;
|
|
351
|
+
// configured fallbacks (defaultOrder / primary key) are converted leniently here.
|
|
352
|
+
const order = orderBy || convertConfiguredOrder(tableConf.defaultOrder || primaryAsString(tableConf.primary), tableConf, db);
|
|
353
|
+
// With a paginator, the page size governs the LIMIT. Without one, apply `maxRows` (if set) so an
|
|
354
|
+
// unbounded search cannot dump the whole table; a plain integer is a safe, non-injectable LIMIT.
|
|
287
355
|
const limit = paginator
|
|
288
356
|
? `${paginator.itemsPerPage} OFFSET ${(paginator.page - 1) * paginator.itemsPerPage}`
|
|
289
|
-
: null;
|
|
357
|
+
: (maxRows != null ? String(maxRows) : null);
|
|
358
|
+
// Base projection: '*' unless the table hides fields via readExclude.
|
|
359
|
+
const readableColumns = readableSelectColumns(tableConf, tableConf.Schema, db);
|
|
290
360
|
// Optional computed projections — bound values, if any, are NOT yet supported
|
|
291
361
|
// here (would require placeholder-aware composition with WHERE values).
|
|
292
|
-
let columns;
|
|
362
|
+
let columns = readableColumns;
|
|
293
363
|
if (selectComputed?.length) {
|
|
294
|
-
const projections = ['*'];
|
|
364
|
+
const projections = [readableColumns ?? '*'];
|
|
295
365
|
for (const name of selectComputed) {
|
|
296
366
|
const fn = tableConf.computedFields?.[name];
|
|
297
367
|
if (!fn)
|
|
@@ -367,7 +437,7 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
|
|
|
367
437
|
({ where, values } = appendComputedConditions(where, values, computedClauses, db));
|
|
368
438
|
}
|
|
369
439
|
const selection = ref?.selection ?? defaultSelection;
|
|
370
|
-
const columns = buildSelectionColumns(selection, joinSchema, db);
|
|
440
|
+
const columns = buildSelectionColumns(selection, joinSchema, db, joinTableConf);
|
|
371
441
|
const rows = await db.select({
|
|
372
442
|
tableName: joinSchema.tableName,
|
|
373
443
|
columns,
|
|
@@ -401,7 +471,7 @@ async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, t
|
|
|
401
471
|
const where = cb.build(1, db.ph);
|
|
402
472
|
const values = cb.getValues();
|
|
403
473
|
const selection = ref?.selection ?? defaultSelection;
|
|
404
|
-
const columns = buildSelectionColumns(selection, joinSchema, db);
|
|
474
|
+
const columns = buildSelectionColumns(selection, joinSchema, db, joinTableConf);
|
|
405
475
|
const rows = await db.select({
|
|
406
476
|
tableName: joinSchema.tableName,
|
|
407
477
|
columns,
|
|
@@ -458,7 +528,7 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup,
|
|
|
458
528
|
return;
|
|
459
529
|
const fn = AGG_FN[kind];
|
|
460
530
|
for (const f of fields) {
|
|
461
|
-
const col = validateSchemaField(f, joinSchema);
|
|
531
|
+
const col = validateSchemaField(f, joinSchema, joinTableConf);
|
|
462
532
|
selectParts.push(`${aggExpr(fn, db.qi(col))} as "${kind}_${f}"`);
|
|
463
533
|
}
|
|
464
534
|
};
|
|
@@ -544,6 +614,7 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
|
|
|
544
614
|
}
|
|
545
615
|
}
|
|
546
616
|
function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
|
|
617
|
+
assertFiltersReadable(ref.filters, joinTableConf);
|
|
547
618
|
const cb = (ref.filters && joinTableConf)
|
|
548
619
|
? joinTableConf.filters(ref.filters, db.cbDialect)
|
|
549
620
|
: new ConditionBuilder('AND', db.cbDialect);
|
|
@@ -557,7 +628,7 @@ function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
|
|
|
557
628
|
// for an LHS expression.
|
|
558
629
|
if (joinTableConf?.computedFields?.[c.field])
|
|
559
630
|
continue;
|
|
560
|
-
const col = db.qi(validateSchemaField(c.field, joinSchema));
|
|
631
|
+
const col = db.qi(validateSchemaField(c.field, joinSchema, joinTableConf));
|
|
561
632
|
dispatchConditionMethod(cb, c.method, col, c.params ?? []);
|
|
562
633
|
}
|
|
563
634
|
}
|
|
@@ -754,6 +825,7 @@ export async function searchEngine(dbTables, params) {
|
|
|
754
825
|
for (const a of Object.keys(joinLeft))
|
|
755
826
|
requireJoin(tableConf, a, true);
|
|
756
827
|
// Build main condition
|
|
828
|
+
assertFiltersReadable(filters, tableConf);
|
|
757
829
|
const condition = tableConf.filters(filters || {}, db.cbDialect);
|
|
758
830
|
// Filters that target computed fields (declared on tableConf.computedFields)
|
|
759
831
|
// are applied via the side-channel pattern further down.
|
|
@@ -794,27 +866,23 @@ export async function searchEngine(dbTables, params) {
|
|
|
794
866
|
({ where, values } = appendAggConditions(where, values, conditions, db, dbTables, tableConf, joinGroup));
|
|
795
867
|
}
|
|
796
868
|
}
|
|
797
|
-
// orderBy parsing — collects 2-parti aliases that need LEFT JOIN on the main query
|
|
798
|
-
let safeOrderBy;
|
|
799
|
-
let orderByValues = [];
|
|
800
|
-
let orderByLeftAliases = new Set();
|
|
801
|
-
if (orderBy) {
|
|
802
|
-
const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + 1);
|
|
803
|
-
safeOrderBy = obResult.sql;
|
|
804
|
-
orderByValues = obResult.values;
|
|
805
|
-
orderByLeftAliases = obResult.leftJoinAliases;
|
|
806
|
-
}
|
|
807
869
|
// Determine which joinLeft aliases need a LEFT JOIN on the main query:
|
|
808
870
|
// - any alias used in 2-parti orderBy
|
|
809
871
|
// - any alias in joinLeft body that has filters or conditions on the parent
|
|
810
|
-
const aliasesNeedingLeftJoin = new Set(
|
|
872
|
+
const aliasesNeedingLeftJoin = new Set();
|
|
873
|
+
if (orderBy) {
|
|
874
|
+
for (const a of collectOrderByLeftAliases(orderBy, tableConf))
|
|
875
|
+
aliasesNeedingLeftJoin.add(a);
|
|
876
|
+
}
|
|
811
877
|
if (joinLeft) {
|
|
812
878
|
for (const [alias, ref] of Object.entries(joinLeft)) {
|
|
813
879
|
if (ref?.filters || ref?.conditions?.length)
|
|
814
880
|
aliasesNeedingLeftJoin.add(alias);
|
|
815
881
|
}
|
|
816
882
|
}
|
|
817
|
-
// Build LEFT JOIN clauses + extra WHERE for filtered parents
|
|
883
|
+
// Build LEFT JOIN clauses + extra WHERE for filtered parents. Their placeholders start right
|
|
884
|
+
// after the WHERE values; the same clause SQL is reused by the pagination COUNT/compute queries,
|
|
885
|
+
// whose value array is [...WHERE, ...leftJoin] — so this base MUST stay `values.length + 1`.
|
|
818
886
|
const extraJoinClauses = [...tenantJoins];
|
|
819
887
|
let leftJoinValues = [];
|
|
820
888
|
if (aliasesNeedingLeftJoin.size > 0) {
|
|
@@ -824,11 +892,21 @@ export async function searchEngine(dbTables, params) {
|
|
|
824
892
|
for (const w of lj.whereExtras)
|
|
825
893
|
where += ` AND ${w}`;
|
|
826
894
|
}
|
|
895
|
+
// orderBy is parsed AFTER the LEFT JOIN values so aggregation-orderBy placeholders start past
|
|
896
|
+
// them (main query binds [...WHERE, ...leftJoin, ...orderBy]). Parsing it earlier with base
|
|
897
|
+
// `values.length + 1` misbinds when BOTH an aggregation orderBy and a filtered joinLeft exist.
|
|
898
|
+
let safeOrderBy;
|
|
899
|
+
let orderByValues = [];
|
|
900
|
+
if (orderBy) {
|
|
901
|
+
const obResult = validateOrderBy(orderBy, tableConf, db, dbTables, joinGroup, values.length + leftJoinValues.length + 1);
|
|
902
|
+
safeOrderBy = obResult.sql;
|
|
903
|
+
orderByValues = obResult.values;
|
|
904
|
+
}
|
|
827
905
|
// Bind WHERE values + LEFT JOIN values + orderBy aggregation values for main query.
|
|
828
906
|
// Pagination COUNT/compute include WHERE + LEFT JOIN values (no orderBy).
|
|
829
907
|
const whereAndJoinValues = [...values, ...leftJoinValues];
|
|
830
908
|
const mainValues = [...whereAndJoinValues, ...orderByValues];
|
|
831
|
-
const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed);
|
|
909
|
+
const main = await executeMainQuery(db, tableConf, where, mainValues, safeOrderBy, paginator, extraJoinClauses, params.selectComputed, params.maxRows);
|
|
832
910
|
let pagination;
|
|
833
911
|
if (paginator) {
|
|
834
912
|
pagination = await buildPagination(db, tableConf, where, whereAndJoinValues, paginator, extraJoinClauses, computeMin, computeMax, computeSum, computeAvg);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { QueryClient } from './db.js';
|
|
2
|
+
import type { ITable, SchemaDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Read visibility (`ITable.readExclude`).
|
|
5
|
+
*
|
|
6
|
+
* Excluded fields are never projected by any read query and cannot be referenced
|
|
7
|
+
* from filters, conditions, orderBy, aggregations or join selections: allowing a
|
|
8
|
+
* field to be filtered while hiding it from the output would still leak its value
|
|
9
|
+
* by bisection. Write paths (insert/update/upsert) are deliberately unaffected —
|
|
10
|
+
* a field can be writable but never readable (e.g. a password hash).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isReadExcluded(tableConf: ITable | undefined, field: string): boolean;
|
|
13
|
+
/** Throws 400 when `field` is read-excluded on `tableConf`. */
|
|
14
|
+
export declare function assertReadable(tableConf: ITable | undefined, field: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Throws 400 when a filter map targets a read-excluded field. Keys that are not
|
|
17
|
+
* excluded pass through untouched (extraFilters and computed names included).
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertFiltersReadable(filters: Record<string, unknown> | undefined, tableConf: ITable | undefined): void;
|
|
20
|
+
/** Schema field names that may be read, in schema declaration order. */
|
|
21
|
+
export declare function readableFieldNames(tableConf: ITable | undefined, schema: SchemaDefinition): string[];
|
|
22
|
+
/**
|
|
23
|
+
* SELECT column list for a table with read exclusions, or `undefined` when the
|
|
24
|
+
* table has none (callers keep their existing `*` projection).
|
|
25
|
+
*
|
|
26
|
+
* Columns are qualified with the table name: read queries may carry a tenant
|
|
27
|
+
* `through` INNER JOIN, where a bare column name shared with the through table
|
|
28
|
+
* would be ambiguous.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readableSelectColumns(tableConf: ITable | undefined, schema: SchemaDefinition, db: QueryClient): string | undefined;
|
|
31
|
+
//# sourceMappingURL=read-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-access.d.ts","sourceRoot":"","sources":["../../src/lib/read-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG5D;;;;;;;;GAQG;AAEH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,IAAI,CAKN;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GACvB,MAAM,EAAE,CAIV;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,EACxB,EAAE,EAAE,WAAW,GACd,MAAM,GAAG,SAAS,CAMpB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { httpError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read visibility (`ITable.readExclude`).
|
|
4
|
+
*
|
|
5
|
+
* Excluded fields are never projected by any read query and cannot be referenced
|
|
6
|
+
* from filters, conditions, orderBy, aggregations or join selections: allowing a
|
|
7
|
+
* field to be filtered while hiding it from the output would still leak its value
|
|
8
|
+
* by bisection. Write paths (insert/update/upsert) are deliberately unaffected —
|
|
9
|
+
* a field can be writable but never readable (e.g. a password hash).
|
|
10
|
+
*/
|
|
11
|
+
export function isReadExcluded(tableConf, field) {
|
|
12
|
+
return tableConf?.readExclude?.includes(field) ?? false;
|
|
13
|
+
}
|
|
14
|
+
/** Throws 400 when `field` is read-excluded on `tableConf`. */
|
|
15
|
+
export function assertReadable(tableConf, field) {
|
|
16
|
+
if (isReadExcluded(tableConf, field)) {
|
|
17
|
+
throw httpError(400, `Field is not readable: ${field}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Throws 400 when a filter map targets a read-excluded field. Keys that are not
|
|
22
|
+
* excluded pass through untouched (extraFilters and computed names included).
|
|
23
|
+
*/
|
|
24
|
+
export function assertFiltersReadable(filters, tableConf) {
|
|
25
|
+
if (!filters || !tableConf?.readExclude?.length)
|
|
26
|
+
return;
|
|
27
|
+
for (const key of Object.keys(filters)) {
|
|
28
|
+
assertReadable(tableConf, key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Schema field names that may be read, in schema declaration order. */
|
|
32
|
+
export function readableFieldNames(tableConf, schema) {
|
|
33
|
+
const fields = Object.keys(schema.fields);
|
|
34
|
+
if (!tableConf?.readExclude?.length)
|
|
35
|
+
return fields;
|
|
36
|
+
return fields.filter((f) => !isReadExcluded(tableConf, f));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* SELECT column list for a table with read exclusions, or `undefined` when the
|
|
40
|
+
* table has none (callers keep their existing `*` projection).
|
|
41
|
+
*
|
|
42
|
+
* Columns are qualified with the table name: read queries may carry a tenant
|
|
43
|
+
* `through` INNER JOIN, where a bare column name shared with the through table
|
|
44
|
+
* would be ambiguous.
|
|
45
|
+
*/
|
|
46
|
+
export function readableSelectColumns(tableConf, schema, db) {
|
|
47
|
+
if (!tableConf?.readExclude?.length)
|
|
48
|
+
return undefined;
|
|
49
|
+
const table = db.qi(schema.tableName);
|
|
50
|
+
return readableFieldNames(tableConf, schema)
|
|
51
|
+
.map((f) => `${table}.${db.qi(schema.col(f))}`)
|
|
52
|
+
.join(', ');
|
|
53
|
+
}
|
|
@@ -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":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAGrE,OAAO,KAAK,EAAE,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AAyBzE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAuHlF;AAED,eAAO,MAAM,sBAAsB;;;;;;;;EASjC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsElF"}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { ALLOWED_METHODS } from '../condition-methods.js';
|
|
3
|
+
import { readableFieldNames } from '../read-access.js';
|
|
4
|
+
/** Schema fields minus the ones hidden by `readExclude`. */
|
|
5
|
+
function readableFields(schema, tableConf) {
|
|
6
|
+
if (!tableConf?.readExclude?.length)
|
|
7
|
+
return { ...schema.fields };
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const field of readableFieldNames(tableConf, schema)) {
|
|
10
|
+
out[field] = schema.fields[field];
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
3
14
|
const JoinGroupResultItem = Type.Object({
|
|
4
15
|
sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
5
16
|
min: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
@@ -38,7 +49,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
const filterFields = {
|
|
41
|
-
...schema
|
|
52
|
+
...readableFields(schema, tableConf),
|
|
42
53
|
...tableConf.extraFilters,
|
|
43
54
|
...computedTypes,
|
|
44
55
|
};
|
|
@@ -71,7 +82,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
const joinFilterFields = joinTableConf
|
|
74
|
-
? { ...joinSchema
|
|
85
|
+
? { ...readableFields(joinSchema, joinTableConf), ...joinTableConf.extraFilters, ...joinComputedTypes }
|
|
75
86
|
: { ...joinSchema.fields };
|
|
76
87
|
const joinRefShape = {
|
|
77
88
|
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
@@ -150,16 +161,17 @@ export function SearchTableResponse(dbTables, tableName) {
|
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
163
|
}
|
|
164
|
+
const mainReadable = readableFields(tableConf.Schema, tableConf);
|
|
153
165
|
const mainItem = Object.keys(computedTypes).length > 0
|
|
154
|
-
? Type.Partial(Type.Object({ ...
|
|
155
|
-
: Type.Partial(Type.Object(
|
|
166
|
+
? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
|
|
167
|
+
: Type.Partial(Type.Object(mainReadable));
|
|
156
168
|
const joinMultipleProps = {};
|
|
157
169
|
const joinLeftProps = {};
|
|
158
170
|
const joinGroupProps = {};
|
|
159
171
|
if (tableConf.allowedReadJoins) {
|
|
160
172
|
for (const joinDef of tableConf.allowedReadJoins) {
|
|
161
173
|
const { joinSchema, alias, unique } = joinDef;
|
|
162
|
-
const itemArray = Type.Array(Type.Partial(Type.Object(joinSchema.
|
|
174
|
+
const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
|
|
163
175
|
if (unique) {
|
|
164
176
|
joinLeftProps[alias] = itemArray;
|
|
165
177
|
}
|
|
@@ -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"}
|
|
@@ -11,11 +11,14 @@ export function UpdateTableBody(dbTables, tableName) {
|
|
|
11
11
|
mainFields[key] = key === primaryAsString(tableConf.primary) ? value : Type.Optional(value);
|
|
12
12
|
}
|
|
13
13
|
const bodyProperties = {
|
|
14
|
-
|
|
14
|
+
// additionalProperties:false makes the schema the real write whitelist — unknown keys are
|
|
15
|
+
// rejected (400) instead of silently reaching the UPDATE as columns (mass assignment).
|
|
16
|
+
main: Type.Object(mainFields, { additionalProperties: false }),
|
|
15
17
|
};
|
|
16
18
|
attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
|
|
17
19
|
withDeletions: true,
|
|
18
20
|
secondaryFields: writeJoinBodyFields,
|
|
21
|
+
strictItems: true,
|
|
19
22
|
});
|
|
20
23
|
return Type.Object(bodyProperties);
|
|
21
24
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { type SqlApi } from './sql-api.js';
|
|
3
|
+
import type { SqlApiPluginOptions } from '../types.js';
|
|
4
|
+
declare module 'fastify' {
|
|
5
|
+
interface FastifyInstance {
|
|
6
|
+
sqlApi: SqlApi;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Decorates `fastify.sqlApi` (lazy getter) on the given instance unless the
|
|
11
|
+
* decorator is already present in scope. The main plugin calls this at the
|
|
12
|
+
* fp level (parent scope); granular route plugins call it through
|
|
13
|
+
* `registerForAllTables` so they also work standalone, each within its own
|
|
14
|
+
* encapsulation context. The underlying pool (`fastify.pg` / `fastify.mysql`)
|
|
15
|
+
* is shared either way.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensureSqlApiDecorator(fastify: FastifyInstance, options: SqlApiPluginOptions): void;
|
|
18
|
+
//# sourceMappingURL=sql-api-decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-api-decorator.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api-decorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA2BN"}
|