fastify-auto-sqlapi 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_BACKEND.md +35 -1
- package/README.md +65 -4
- package/dist/bin/generate-schema.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.d.ts +4 -0
- package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/mysql-adapter.js +28 -17
- package/dist/lib/adapters/pg-adapter.d.ts +4 -0
- package/dist/lib/adapters/pg-adapter.d.ts.map +1 -1
- package/dist/lib/adapters/pg-adapter.js +18 -7
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +3 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +13 -4
- package/dist/lib/cli/schema-codegen.d.ts +2 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +17 -3
- package/dist/lib/cli/tables-codegen.d.ts +2 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +43 -12
- package/dist/lib/db.d.ts +17 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +47 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +33 -10
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +9 -8
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +18 -7
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +38 -33
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +69 -55
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +49 -19
- package/dist/lib/sql-api.js +2 -2
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +10 -9
- package/dist/lib/tenant.d.ts +9 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +31 -8
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +1 -0
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +1 -0
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +1 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +1 -0
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +1 -0
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +40 -5
- package/dist/routes/auto/route-helpers.d.ts +3 -1
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +3 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +1 -0
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +1 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/db.d.ts +11 -0
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/dml.d.ts +2 -0
- package/dist/types/dml.d.ts.map +1 -1
- package/dist/types/schema.d.ts +2 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/table.d.ts +64 -2
- package/dist/types/table.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -219,7 +234,7 @@ function buildLeftJoinClauses(db, dbTables, tableConf, aliasesNeedingJoin, joinL
|
|
|
219
234
|
// Note: extraFilters declared on joinTableConf are not supported here for
|
|
220
235
|
// joinLeft (they would require alias-aware handlers); only schema fields
|
|
221
236
|
// and computed fields apply.
|
|
222
|
-
const cb = new ConditionBuilder('AND');
|
|
237
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
223
238
|
const joinTableConf = dbTables[joinSchema.tableName];
|
|
224
239
|
const computed = joinTableConf?.computedFields;
|
|
225
240
|
if (ref.filters) {
|
|
@@ -328,7 +343,7 @@ async function buildPagination(db, tableConf, where, values, paginator, extraJoi
|
|
|
328
343
|
};
|
|
329
344
|
}
|
|
330
345
|
// ─── joinMultiple (virtual fetch, child rows in side query) ─
|
|
331
|
-
async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple) {
|
|
346
|
+
async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
|
|
332
347
|
const result = {};
|
|
333
348
|
for (const [alias, ref] of Object.entries(joinMultiple)) {
|
|
334
349
|
const joinDef = requireJoin(tableConf, alias, false);
|
|
@@ -342,6 +357,8 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
|
|
|
342
357
|
const cb = buildJoinRefCondition(joinTableConf, joinSchema, ref || {}, db);
|
|
343
358
|
const fkCol = joinSchema.col(joinField);
|
|
344
359
|
cb.isIn(db.qi(fkCol), ids);
|
|
360
|
+
const tenantJoins = [];
|
|
361
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
345
362
|
let where = cb.build(1, db.ph);
|
|
346
363
|
let values = cb.getValues();
|
|
347
364
|
// Side-channel: computed-field clauses on the join table
|
|
@@ -356,17 +373,19 @@ async function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMul
|
|
|
356
373
|
columns,
|
|
357
374
|
where,
|
|
358
375
|
values,
|
|
376
|
+
joins: tenantJoins.length > 0 ? tenantJoins : undefined,
|
|
359
377
|
});
|
|
360
378
|
result[alias] = mapRowsToCamelCase(rows, joinSchema);
|
|
361
379
|
}
|
|
362
380
|
return result;
|
|
363
381
|
}
|
|
364
382
|
// ─── joinLeft (parent fetch via PK IN side query) ───────────
|
|
365
|
-
async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
383
|
+
async function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
|
|
366
384
|
const result = {};
|
|
367
385
|
for (const [alias, ref] of Object.entries(joinLeft)) {
|
|
368
386
|
const joinDef = requireJoin(tableConf, alias, true);
|
|
369
387
|
const { joinSchema, joinField, mainField, selection: defaultSelection } = joinDef;
|
|
388
|
+
const joinTableConf = dbTables[joinSchema.tableName];
|
|
370
389
|
// For joinLeft (N:1), mainField on main is the FK pointing to joinField (PK) on parent.
|
|
371
390
|
// We collect the FK values from the main results and look up parents by their PK.
|
|
372
391
|
const ids = collectIds(mainResults, mainField);
|
|
@@ -375,8 +394,10 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
|
375
394
|
continue;
|
|
376
395
|
}
|
|
377
396
|
const fkCol = joinSchema.col(joinField);
|
|
378
|
-
const cb = new ConditionBuilder('AND');
|
|
397
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
379
398
|
cb.isIn(db.qi(fkCol), ids);
|
|
399
|
+
const tenantJoins = [];
|
|
400
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
380
401
|
const where = cb.build(1, db.ph);
|
|
381
402
|
const values = cb.getValues();
|
|
382
403
|
const selection = ref?.selection ?? defaultSelection;
|
|
@@ -386,6 +407,7 @@ async function executeJoinLeft(db, tableConf, mainResults, joinLeft) {
|
|
|
386
407
|
columns,
|
|
387
408
|
where,
|
|
388
409
|
values,
|
|
410
|
+
joins: tenantJoins.length > 0 ? tenantJoins : undefined,
|
|
389
411
|
});
|
|
390
412
|
result[alias] = mapRowsToCamelCase(rows, joinSchema);
|
|
391
413
|
}
|
|
@@ -412,7 +434,7 @@ function buildByExpression(by, joinSchema, joinTableConf, db) {
|
|
|
412
434
|
err400(`Unknown field: ${by}`);
|
|
413
435
|
}
|
|
414
436
|
// ─── joinGroup (aggregations) ───────────────────────────────
|
|
415
|
-
async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup) {
|
|
437
|
+
async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup, tenant) {
|
|
416
438
|
const result = {};
|
|
417
439
|
for (const [alias, groupReq] of Object.entries(joinGroup)) {
|
|
418
440
|
const joinDef = requireJoin(tableConf, alias, false);
|
|
@@ -454,6 +476,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
|
|
|
454
476
|
const cb = buildJoinRefCondition(joinTableConf, joinSchema, groupRef, db);
|
|
455
477
|
const fkCol = joinSchema.col(joinField);
|
|
456
478
|
cb.isIn(db.qi(fkCol), ids);
|
|
479
|
+
const tenantJoins = [];
|
|
480
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, cb, tenantJoins);
|
|
457
481
|
let where = cb.build(1, db.ph);
|
|
458
482
|
let values = cb.getValues();
|
|
459
483
|
// Side-channel: computed-field clauses on the join table
|
|
@@ -462,7 +486,8 @@ async function executeJoinGroup(db, dbTables, tableConf, mainResults, joinGroup)
|
|
|
462
486
|
({ where, values } = appendComputedConditions(where, values, computedClauses, db));
|
|
463
487
|
}
|
|
464
488
|
const groupBy = groupByParts.length > 0 ? `GROUP BY ${groupByParts.join(', ')}` : '';
|
|
465
|
-
const
|
|
489
|
+
const fromJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
|
|
490
|
+
const sql = `SELECT ${selectParts.join(', ')} FROM ${db.qi(joinSchema.tableName)}${fromJoins} WHERE ${where} ${groupBy}`;
|
|
466
491
|
const queryResult = await db.query(sql, values);
|
|
467
492
|
const rows = queryResult.rows;
|
|
468
493
|
const formatted = {};
|
|
@@ -520,8 +545,8 @@ function dispatchConditionMethod(cb, method, colOrExpr, params) {
|
|
|
520
545
|
}
|
|
521
546
|
function buildJoinRefCondition(joinTableConf, joinSchema, ref, db) {
|
|
522
547
|
const cb = (ref.filters && joinTableConf)
|
|
523
|
-
? joinTableConf.filters(ref.filters)
|
|
524
|
-
: new ConditionBuilder('AND');
|
|
548
|
+
? joinTableConf.filters(ref.filters, db.cbDialect)
|
|
549
|
+
: new ConditionBuilder('AND', db.cbDialect);
|
|
525
550
|
if (ref.conditions?.length) {
|
|
526
551
|
for (const c of ref.conditions) {
|
|
527
552
|
if (!ALLOWED_SET.has(c.method)) {
|
|
@@ -641,7 +666,7 @@ function appendComputedConditions(currentWhere, currentValues, clauses, db) {
|
|
|
641
666
|
// Append the computed's own bound values first; the expr placeholders
|
|
642
667
|
// (if any) reference them via stable indices set by the consumer.
|
|
643
668
|
values = [...values, ...c.values];
|
|
644
|
-
const tmpCb = new ConditionBuilder('AND');
|
|
669
|
+
const tmpCb = new ConditionBuilder('AND', db.cbDialect);
|
|
645
670
|
dispatchConditionMethod(tmpCb, c.method, c.expr, c.params);
|
|
646
671
|
const startIdx = values.length + 1;
|
|
647
672
|
const clauseSql = tmpCb.build(startIdx, db.ph);
|
|
@@ -666,7 +691,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
|
|
|
666
691
|
const [alias, fn, field] = parts;
|
|
667
692
|
const { expr, values: exprValues } = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, values.length + 1);
|
|
668
693
|
values = [...values, ...exprValues];
|
|
669
|
-
const tmpCb = new ConditionBuilder('AND');
|
|
694
|
+
const tmpCb = new ConditionBuilder('AND', db.cbDialect);
|
|
670
695
|
dispatchConditionMethod(tmpCb, c.method, expr, c.params);
|
|
671
696
|
const startIdx = values.length + 1;
|
|
672
697
|
const clause = tmpCb.build(startIdx, db.ph);
|
|
@@ -676,7 +701,7 @@ function appendAggConditions(currentWhere, currentValues, conditions, db, dbTabl
|
|
|
676
701
|
return { where, values };
|
|
677
702
|
}
|
|
678
703
|
// ─── joinMustExist (EXISTS subquery filter on main) ─────────
|
|
679
|
-
function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues) {
|
|
704
|
+
function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, currentWhere, currentValues, tenant) {
|
|
680
705
|
let where = currentWhere;
|
|
681
706
|
const values = [...currentValues];
|
|
682
707
|
for (const [alias, ref] of Object.entries(joinMustExist)) {
|
|
@@ -685,6 +710,8 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
|
|
|
685
710
|
const joinTableConf = dbTables[joinSchema.tableName];
|
|
686
711
|
const refs = extractJoinRefs(db, tableConf, joinDef);
|
|
687
712
|
const filterCondition = buildJoinRefCondition(joinTableConf, joinSchema, ref, db);
|
|
713
|
+
const tenantJoins = [];
|
|
714
|
+
appendJoinTenantScope(db, joinTableConf, tenant, joinSchema.tableName, filterCondition, tenantJoins);
|
|
688
715
|
const startIdx = values.length + 1;
|
|
689
716
|
let filterWhere = filterCondition.build(startIdx, db.ph);
|
|
690
717
|
let filterVals = [...filterCondition.getValues()];
|
|
@@ -699,10 +726,13 @@ function buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, curre
|
|
|
699
726
|
filterVals = cvalues.slice(values.length);
|
|
700
727
|
filterWhere = cwhere;
|
|
701
728
|
}
|
|
729
|
+
// Qualify the FK with the join table: the EXISTS FROM may include the tenant
|
|
730
|
+
// through-join, so a bare column could become ambiguous.
|
|
702
731
|
const innerWhere = filterWhere
|
|
703
|
-
? `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
|
|
704
|
-
: `${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
|
|
705
|
-
|
|
732
|
+
? `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol} AND ${filterWhere}`
|
|
733
|
+
: `${refs.joinTable}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}`;
|
|
734
|
+
const existsJoins = tenantJoins.length > 0 ? ` ${tenantJoins.join(' ')}` : '';
|
|
735
|
+
where += ` AND EXISTS (SELECT 1 FROM ${refs.joinTable}${existsJoins} WHERE ${innerWhere})`;
|
|
706
736
|
values.push(...filterVals);
|
|
707
737
|
}
|
|
708
738
|
return { where, values };
|
|
@@ -724,7 +754,7 @@ export async function searchEngine(dbTables, params) {
|
|
|
724
754
|
for (const a of Object.keys(joinLeft))
|
|
725
755
|
requireJoin(tableConf, a, true);
|
|
726
756
|
// Build main condition
|
|
727
|
-
const condition = tableConf.filters(filters || {});
|
|
757
|
+
const condition = tableConf.filters(filters || {}, db.cbDialect);
|
|
728
758
|
// Filters that target computed fields (declared on tableConf.computedFields)
|
|
729
759
|
// are applied via the side-channel pattern further down.
|
|
730
760
|
let computedFilterClauses = [];
|
|
@@ -755,7 +785,7 @@ export async function searchEngine(dbTables, params) {
|
|
|
755
785
|
}
|
|
756
786
|
// joinMustExist (EXISTS)
|
|
757
787
|
if (joinMustExist && Object.keys(joinMustExist).length > 0) {
|
|
758
|
-
({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values));
|
|
788
|
+
({ where, values } = buildJoinMustExistClauses(db, dbTables, tableConf, joinMustExist, where, values, tenant));
|
|
759
789
|
}
|
|
760
790
|
// Aggregation conditions (HAVING-style)
|
|
761
791
|
if (conditions?.length) {
|
|
@@ -805,13 +835,13 @@ export async function searchEngine(dbTables, params) {
|
|
|
805
835
|
}
|
|
806
836
|
const result = { main };
|
|
807
837
|
if (joinMultiple && Object.keys(joinMultiple).length > 0) {
|
|
808
|
-
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple);
|
|
838
|
+
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
|
|
809
839
|
}
|
|
810
840
|
if (joinLeft && Object.keys(joinLeft).length > 0) {
|
|
811
|
-
result.joinLeft = await executeJoinLeft(db, tableConf, main, joinLeft);
|
|
841
|
+
result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
|
|
812
842
|
}
|
|
813
843
|
if (joinGroup && Object.keys(joinGroup).length > 0) {
|
|
814
|
-
result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup);
|
|
844
|
+
result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
|
|
815
845
|
}
|
|
816
846
|
if (pagination) {
|
|
817
847
|
result.pagination = pagination;
|
package/dist/lib/sql-api.js
CHANGED
|
@@ -99,7 +99,7 @@ export class SqlApi {
|
|
|
99
99
|
async delete(tableName, id, request) {
|
|
100
100
|
const tableConf = this.getTableConf(tableName);
|
|
101
101
|
const tenant = await this.getTenant(tableName, request);
|
|
102
|
-
return deleteEngine({ db: this.db, tableConf, id, tenant });
|
|
102
|
+
return deleteEngine({ db: this.db, tableConf, id, tenant, request });
|
|
103
103
|
}
|
|
104
104
|
async bulkUpsert(tableName, items, request) {
|
|
105
105
|
const tableConf = this.getTableConf(tableName);
|
|
@@ -116,7 +116,7 @@ export class SqlApi {
|
|
|
116
116
|
async bulkDelete(tableName, ids, request) {
|
|
117
117
|
const tableConf = this.getTableConf(tableName);
|
|
118
118
|
const tenant = await this.getTenant(tableName, request);
|
|
119
|
-
return bulkDeleteEngine({ db: this.db, tableConf, ids, tenant });
|
|
119
|
+
return bulkDeleteEngine({ db: this.db, tableConf, ids, tenant, request });
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
export function createSqlApi(dbOrPool, dbTables, options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAKX;AA2CD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
2
|
import { getDialect } from './dialect.js';
|
|
3
|
-
// Quote an identifier using the
|
|
4
|
-
// Needed so that DB columns with uppercase letters
|
|
5
|
-
// are preserved on PostgreSQL (which folds unquoted
|
|
6
|
-
// and on MySQL (which is case-sensitive on Linux
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// Quote an identifier using the given ConditionBuilder dialect (or the global
|
|
4
|
+
// default when not provided). Needed so that DB columns with uppercase letters
|
|
5
|
+
// (e.g. betterauth "userId") are preserved on PostgreSQL (which folds unquoted
|
|
6
|
+
// identifiers to lowercase) and on MySQL (which is case-sensitive on Linux
|
|
7
|
+
// filesystems).
|
|
8
|
+
function qi(field, dialect) {
|
|
9
|
+
return getDialect(dialect ?? ConditionBuilder.DIALECT).qi(field);
|
|
9
10
|
}
|
|
10
11
|
export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
|
|
11
|
-
const filters = (filterValues) => {
|
|
12
|
-
const condition = new ConditionBuilder('AND');
|
|
12
|
+
const filters = (filterValues, dialect) => {
|
|
13
|
+
const condition = new ConditionBuilder('AND', dialect);
|
|
13
14
|
// Only auto-match real schema fields (DB columns)
|
|
14
15
|
for (const field of Object.keys(Schema.fields)) {
|
|
15
16
|
if (field in filterValues && filterValues[field] !== null && filterValues[field] !== undefined) {
|
|
16
|
-
condition.isEqual(qi(Schema.col(field)), filterValues[field]);
|
|
17
|
+
condition.isEqual(qi(Schema.col(field), dialect), filterValues[field]);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
// extraFilters are handled exclusively by extendedCondition
|
package/dist/lib/tenant.d.ts
CHANGED
|
@@ -32,11 +32,18 @@ export declare function enforceTenantOnWrites(db: QueryClient, tenant: TenantCon
|
|
|
32
32
|
* Returns undefined for indirect or no-tenant scenarios — those are handled separately
|
|
33
33
|
* (indirect uses pre-check via `assertTenantOwnership`).
|
|
34
34
|
*/
|
|
35
|
-
export declare function buildTenantUpdateExtra(tenant: TenantContext | undefined): ConditionBuilder | undefined;
|
|
35
|
+
export declare function buildTenantUpdateExtra(db: QueryClient, tenant: TenantContext | undefined): ConditionBuilder | undefined;
|
|
36
36
|
/**
|
|
37
37
|
* For update with indirect tenant: pre-check ownership via `SELECT 1 ... INNER JOIN ... LIMIT 1`.
|
|
38
38
|
* Throws 404 if the record either does not exist or does not belong to the tenant.
|
|
39
39
|
* No-op for direct or no-tenant — those are enforced via `extraCondition` in the UPDATE itself.
|
|
40
40
|
*/
|
|
41
|
-
export declare function assertTenantOwnership(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string, pkValue: ConditionValue): Promise<void>;
|
|
41
|
+
export declare function assertTenantOwnership(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string | string[], pkValue: ConditionValue | ConditionValue[]): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Assert that EVERY pk in `ids` exists and belongs to the tenant, for both direct and indirect
|
|
44
|
+
* scopes. Throws 404 if any id is missing or not owned. No-op when `tenant` is undefined or `ids`
|
|
45
|
+
* is empty. Used by the delete engines to enforce tenant ownership BEFORE running a `beforeDelete`/
|
|
46
|
+
* `beforeBulkDelete` hook, so user code never runs against rows the caller cannot access.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertTenantOwnsAll(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string, ids: ConditionValue[]): Promise<void>;
|
|
42
49
|
//# sourceMappingURL=tenant.d.ts.map
|
package/dist/lib/tenant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,gBAAgB,CAclB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
package/dist/lib/tenant.js
CHANGED
|
@@ -24,7 +24,7 @@ export function buildTenantCondition(db, scope, tenantIds) {
|
|
|
24
24
|
else {
|
|
25
25
|
qualifier = db.qi(col);
|
|
26
26
|
}
|
|
27
|
-
const cb = new ConditionBuilder('AND');
|
|
27
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
28
28
|
cb.isIn(qualifier, tenantIds);
|
|
29
29
|
return cb;
|
|
30
30
|
}
|
|
@@ -59,7 +59,7 @@ export async function validateTenantFK(db, scope, tenantIds, fkValues) {
|
|
|
59
59
|
const throughTable = scope.through.schema.tableName;
|
|
60
60
|
const foreignField = scope.through.foreignField;
|
|
61
61
|
const tenantCol = scope.column;
|
|
62
|
-
const cb = new ConditionBuilder('AND');
|
|
62
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
63
63
|
cb.isIn(db.qi(foreignField), uniqueFKs);
|
|
64
64
|
cb.isNotIn(db.qi(tenantCol), tenantIds);
|
|
65
65
|
const where = cb.build(1, db.ph);
|
|
@@ -77,7 +77,7 @@ export async function validateTenantFK(db, scope, tenantIds, fkValues) {
|
|
|
77
77
|
*/
|
|
78
78
|
export function buildTenantDeleteWhere(db, tableName, pkCol, pkValue, tenant) {
|
|
79
79
|
if (isIndirect(tenant.scope)) {
|
|
80
|
-
const innerCb = new ConditionBuilder('AND');
|
|
80
|
+
const innerCb = new ConditionBuilder('AND', db.cbDialect);
|
|
81
81
|
innerCb.isIn(`${db.qi(tableName)}.${db.qi(pkCol)}`, (Array.isArray(pkValue) ? pkValue : [pkValue]));
|
|
82
82
|
innerCb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
|
|
83
83
|
const innerWhere = innerCb.build(1, db.ph);
|
|
@@ -86,7 +86,7 @@ export function buildTenantDeleteWhere(db, tableName, pkCol, pkValue, tenant) {
|
|
|
86
86
|
const where = `${db.qi(pkCol)} IN (SELECT ${db.qi(tableName)}.${db.qi(pkCol)} FROM ${db.qi(tableName)} ${joinSql} WHERE ${innerWhere})`;
|
|
87
87
|
return { where, values };
|
|
88
88
|
}
|
|
89
|
-
const cb = new ConditionBuilder('AND');
|
|
89
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
90
90
|
if (Array.isArray(pkValue)) {
|
|
91
91
|
cb.isIn(db.qi(pkCol), pkValue);
|
|
92
92
|
}
|
|
@@ -129,10 +129,10 @@ export async function enforceTenantOnWrites(db, tenant, records) {
|
|
|
129
129
|
* Returns undefined for indirect or no-tenant scenarios — those are handled separately
|
|
130
130
|
* (indirect uses pre-check via `assertTenantOwnership`).
|
|
131
131
|
*/
|
|
132
|
-
export function buildTenantUpdateExtra(tenant) {
|
|
132
|
+
export function buildTenantUpdateExtra(db, tenant) {
|
|
133
133
|
if (!tenant || isIndirect(tenant.scope))
|
|
134
134
|
return undefined;
|
|
135
|
-
const cb = new ConditionBuilder('AND');
|
|
135
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
136
136
|
if (tenant.ids.length === 1)
|
|
137
137
|
cb.isEqual(tenant.scope.column, tenant.ids[0]);
|
|
138
138
|
else
|
|
@@ -147,8 +147,10 @@ export function buildTenantUpdateExtra(tenant) {
|
|
|
147
147
|
export async function assertTenantOwnership(db, tenant, tableName, pkCol, pkValue) {
|
|
148
148
|
if (!tenant || !isIndirect(tenant.scope))
|
|
149
149
|
return;
|
|
150
|
-
const cb = new ConditionBuilder('AND');
|
|
151
|
-
|
|
150
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
151
|
+
const cols = Array.isArray(pkCol) ? pkCol : [pkCol];
|
|
152
|
+
const vals = Array.isArray(pkValue) ? pkValue : [pkValue];
|
|
153
|
+
cols.forEach((c, i) => cb.isEqual(`${db.qi(tableName)}.${db.qi(c)}`, vals[i]));
|
|
152
154
|
cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
|
|
153
155
|
const sql = `SELECT 1 FROM ${db.qi(tableName)} ${buildTenantJoin(db, tenant.scope, tableName)} WHERE ${cb.build(1, db.ph)} LIMIT 1`;
|
|
154
156
|
const r = await db.query(sql, cb.getValues());
|
|
@@ -156,3 +158,24 @@ export async function assertTenantOwnership(db, tenant, tableName, pkCol, pkValu
|
|
|
156
158
|
throw httpError(404, 'Record not found');
|
|
157
159
|
}
|
|
158
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Assert that EVERY pk in `ids` exists and belongs to the tenant, for both direct and indirect
|
|
163
|
+
* scopes. Throws 404 if any id is missing or not owned. No-op when `tenant` is undefined or `ids`
|
|
164
|
+
* is empty. Used by the delete engines to enforce tenant ownership BEFORE running a `beforeDelete`/
|
|
165
|
+
* `beforeBulkDelete` hook, so user code never runs against rows the caller cannot access.
|
|
166
|
+
*/
|
|
167
|
+
export async function assertTenantOwnsAll(db, tenant, tableName, pkCol, ids) {
|
|
168
|
+
if (!tenant || !ids.length)
|
|
169
|
+
return;
|
|
170
|
+
const qualifiedPk = `${db.qi(tableName)}.${db.qi(pkCol)}`;
|
|
171
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
172
|
+
cb.isIn(qualifiedPk, ids);
|
|
173
|
+
cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
|
|
174
|
+
const join = isIndirect(tenant.scope) ? ` ${buildTenantJoin(db, tenant.scope, tableName)}` : '';
|
|
175
|
+
const sql = `SELECT DISTINCT ${qualifiedPk} AS pk FROM ${db.qi(tableName)}${join} WHERE ${cb.build(1, db.ph)}`;
|
|
176
|
+
const r = await db.query(sql, cb.getValues());
|
|
177
|
+
const uniqueRequested = new Set(ids.map((v) => String(v))).size;
|
|
178
|
+
if (r.rows.length < uniqueRequested) {
|
|
179
|
+
throw httpError(404, 'Record not found');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
|
|
@@ -3,6 +3,7 @@ import { primaryAsString } from '../../types.js';
|
|
|
3
3
|
import { registerForAllTables } from './route-helpers.js';
|
|
4
4
|
export default async function bulkDeleteRoutes(fastify, options) {
|
|
5
5
|
await registerForAllTables(fastify, options, {
|
|
6
|
+
operation: 'bulkDelete',
|
|
6
7
|
method: 'POST',
|
|
7
8
|
url: (tc) => `/bulk/${tc.Schema.tableName}/delete`,
|
|
8
9
|
successStatus: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -2,6 +2,7 @@ import { BulkUpsertTableBody, BulkUpsertTableResponse } from '../../lib/schema/b
|
|
|
2
2
|
import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
|
|
3
3
|
export default async function bulkUpsertRoutes(fastify, options) {
|
|
4
4
|
await registerForAllTables(fastify, options, {
|
|
5
|
+
operation: 'bulkUpsert',
|
|
5
6
|
method: 'PUT',
|
|
6
7
|
url: (tc) => `/bulk/${tc.Schema.tableName}`,
|
|
7
8
|
successStatus: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -3,6 +3,7 @@ import { primaryAsString } from '../../types.js';
|
|
|
3
3
|
import { registerForAllTables } from './route-helpers.js';
|
|
4
4
|
export default async function deleteRoutes(fastify, options) {
|
|
5
5
|
await registerForAllTables(fastify, options, {
|
|
6
|
+
operation: 'delete',
|
|
6
7
|
method: 'DELETE',
|
|
7
8
|
url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
|
|
8
9
|
successStatus: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -2,6 +2,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
2
2
|
import { registerForAllTables } from './route-helpers.js';
|
|
3
3
|
export default async function getRoutes(fastify, options) {
|
|
4
4
|
await registerForAllTables(fastify, options, {
|
|
5
|
+
operation: 'get',
|
|
5
6
|
method: 'GET',
|
|
6
7
|
url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
|
|
7
8
|
successStatus: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|
|
@@ -2,6 +2,7 @@ import { InsertTableBody, InsertTableResponse } from '../../lib/schema/insert.js
|
|
|
2
2
|
import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
|
|
3
3
|
export default async function insertRoutes(fastify, options) {
|
|
4
4
|
await registerForAllTables(fastify, options, {
|
|
5
|
+
operation: 'insert',
|
|
5
6
|
method: 'POST',
|
|
6
7
|
url: (tc) => `/rest/${tc.Schema.tableName}`,
|
|
7
8
|
successStatus: 201,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;kCAuDU,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBAiFoC"}
|
|
@@ -13,6 +13,45 @@ import updateRoutes from './update.routes.js';
|
|
|
13
13
|
import deleteRoutes from './delete.routes.js';
|
|
14
14
|
import bulkUpsertRoutes from './bulk-upsert.routes.js';
|
|
15
15
|
import bulkDeleteRoutes from './bulk-delete.routes.js';
|
|
16
|
+
// Schema combinators for which Ajv reports one error per failed branch plus a
|
|
17
|
+
// single combinator-level summary error, all targeting the same path.
|
|
18
|
+
const COMBINATOR_KEYWORDS = new Set(['anyOf', 'oneOf']);
|
|
19
|
+
/**
|
|
20
|
+
* Converts Ajv validation errors into response fields, stripping the noise Ajv
|
|
21
|
+
* emits for schema combinators.
|
|
22
|
+
*
|
|
23
|
+
* An enum modelled as `anyOf` of `const`s fails every branch when invalid, so Ajv
|
|
24
|
+
* produces one `const` error per branch plus an `anyOf` error — all on the same
|
|
25
|
+
* path. We keep only the combinator error for those paths and drop the per-branch
|
|
26
|
+
* errors, then deduplicate any remaining identical entries. `required` errors are
|
|
27
|
+
* re-pointed from the parent object to the actually missing property.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeAjvErrors(errors) {
|
|
30
|
+
const toPath = (v) => {
|
|
31
|
+
const path = (v.instancePath || '').replace(/\//g, '.').replace(/^\./, '');
|
|
32
|
+
const missing = v.params?.missingProperty;
|
|
33
|
+
if (v.keyword === 'required' && missing) {
|
|
34
|
+
return path ? `${path}.${missing}` : missing;
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
};
|
|
38
|
+
const mapped = errors.map((v) => ({
|
|
39
|
+
path: toPath(v),
|
|
40
|
+
message: v.message || 'invalid',
|
|
41
|
+
code: v.keyword || 'unknown',
|
|
42
|
+
}));
|
|
43
|
+
const combinatorPaths = new Set(mapped.filter((f) => COMBINATOR_KEYWORDS.has(f.code)).map((f) => f.path));
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
return mapped.filter((f) => {
|
|
46
|
+
if (combinatorPaths.has(f.path) && !COMBINATOR_KEYWORDS.has(f.code))
|
|
47
|
+
return false;
|
|
48
|
+
const key = `${f.path}|${f.code}|${f.message}`;
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
return false;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
16
55
|
export default fp(async function fastifyAutoSqlApi(fastify, options) {
|
|
17
56
|
// Set ConditionBuilder dialect globally
|
|
18
57
|
const dialect = getDialect(options.dialect || 'postgres');
|
|
@@ -49,11 +88,7 @@ export default fp(async function fastifyAutoSqlApi(fastify, options) {
|
|
|
49
88
|
instance.setErrorHandler((error, request, reply) => {
|
|
50
89
|
// Schema validation errors (Ajv)
|
|
51
90
|
if (error.validation) {
|
|
52
|
-
const fields = error.validation
|
|
53
|
-
path: (v.instancePath || '').replace(/\//g, '.').replace(/^\./, ''),
|
|
54
|
-
message: v.message || 'invalid',
|
|
55
|
-
code: v.keyword || 'unknown',
|
|
56
|
-
}));
|
|
91
|
+
const fields = normalizeAjvErrors(error.validation);
|
|
57
92
|
return reply.status(400).send({
|
|
58
93
|
statusCode: 400,
|
|
59
94
|
error: 'Bad Request',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
-
import type { ITable, SqlApiPluginOptions, DbTables } from '../../types.js';
|
|
2
|
+
import type { ITable, SqlApiPluginOptions, DbTables, TableOperation } from '../../types.js';
|
|
3
3
|
type RequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>;
|
|
4
4
|
export declare function mergeOnRequests(options: SqlApiPluginOptions, tableConf: ITable): RequestHook[];
|
|
5
5
|
export declare function buildWriteDescription(action: string, tableName: string, tableConf: ITable): string;
|
|
@@ -10,6 +10,8 @@ interface RouteSchema {
|
|
|
10
10
|
response?: any;
|
|
11
11
|
}
|
|
12
12
|
export interface AutoRouteSpec {
|
|
13
|
+
/** Operation key matched against `ITable.operations` to decide whether to register the route. */
|
|
14
|
+
operation: TableOperation;
|
|
13
15
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
14
16
|
/** URL builder receiving the table configuration to read `Schema.tableName`. */
|
|
15
17
|
url: (tableConf: ITable) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -18,6 +18,9 @@ export function buildWriteDescription(action, tableName, tableConf) {
|
|
|
18
18
|
export async function registerForAllTables(fastify, options, spec) {
|
|
19
19
|
const { DbTables } = options;
|
|
20
20
|
for (const [tableName, tableConf] of Object.entries(DbTables)) {
|
|
21
|
+
// operations acts as a whitelist; when omitted every operation is exposed.
|
|
22
|
+
if (tableConf.operations && !tableConf.operations.includes(spec.operation))
|
|
23
|
+
continue;
|
|
21
24
|
const schemas = spec.schemas(DbTables, tableName, tableConf);
|
|
22
25
|
// Only include schema keys that are actually defined: Fastify checks for key
|
|
23
26
|
// *presence* (not undefined value) to decide whether to emit FSTWRN001.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkDf"}
|
|
@@ -2,6 +2,7 @@ import { SearchTableBodyPost, SearchTableQueryString, SearchTableResponse } from
|
|
|
2
2
|
import { registerForAllTables } from './route-helpers.js';
|
|
3
3
|
export default async function searchRoutes(fastify, options) {
|
|
4
4
|
await registerForAllTables(fastify, options, {
|
|
5
|
+
operation: 'search',
|
|
5
6
|
method: 'POST',
|
|
6
7
|
url: (tc) => `/search/${tc.Schema.tableName}`,
|
|
7
8
|
successStatus: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/update.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"update.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/update.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -2,6 +2,7 @@ import { UpdateTableBody, UpdateTableResponse } from '../../lib/schema/update.js
|
|
|
2
2
|
import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
|
|
3
3
|
export default async function updateRoutes(fastify, options) {
|
|
4
4
|
await registerForAllTables(fastify, options, {
|
|
5
|
+
operation: 'update',
|
|
5
6
|
method: 'PUT',
|
|
6
7
|
url: (tc) => `/rest/${tc.Schema.tableName}`,
|
|
7
8
|
successStatus: 200,
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -11,12 +11,18 @@ export interface ColumnInfo {
|
|
|
11
11
|
udt_name: string;
|
|
12
12
|
column_default: string | null;
|
|
13
13
|
is_nullable: string;
|
|
14
|
+
/** True when the column is part of the table's PRIMARY KEY (from information_schema). */
|
|
15
|
+
is_primary?: boolean;
|
|
16
|
+
/** True for auto-generated columns without a visible default (mysql AUTO_INCREMENT). */
|
|
17
|
+
is_auto_increment?: boolean;
|
|
14
18
|
}
|
|
15
19
|
export interface TableMap {
|
|
16
20
|
[schemaName: string]: {
|
|
17
21
|
name: string;
|
|
18
22
|
fields: Record<string, string>;
|
|
19
23
|
colMap: Record<string, string>;
|
|
24
|
+
/** PRIMARY KEY fields (camelCase), in table column order. Empty when unknown. */
|
|
25
|
+
primary: string[];
|
|
20
26
|
};
|
|
21
27
|
}
|
|
22
28
|
//# sourceMappingURL=cli.d.ts.map
|