fastify-auto-sqlapi 0.2.1 → 0.2.2
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.md +50 -0
- package/AGENTS_BACKEND.md +264 -6
- package/AGENTS_FRONTEND.md +1 -1
- package/BREAKING_CHANGES.md +51 -0
- package/CHANGELOG.md +113 -0
- package/README.md +254 -983
- package/dist/bin/generate-schema.js +49 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/agent/manifest.d.ts +2 -0
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +4 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +15 -5
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +53 -14
- 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 -4
- package/dist/lib/cli/tables-codegen.d.ts +4 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +125 -27
- package/dist/lib/engine/bulk/bulk-upsert.js +2 -2
- package/dist/lib/engine/read-hooks.d.ts +19 -0
- package/dist/lib/engine/read-hooks.d.ts.map +1 -0
- package/dist/lib/engine/read-hooks.js +20 -0
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +5 -2
- package/dist/lib/engine/rest/insert.js +1 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +14 -10
- package/dist/lib/engine/search/join-families.d.ts +11 -2
- package/dist/lib/engine/search/join-families.d.ts.map +1 -1
- package/dist/lib/engine/search/join-families.js +11 -13
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +9 -3
- package/dist/lib/engine/validate.d.ts +3 -3
- package/dist/lib/engine/validate.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.d.ts +7 -3
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +27 -28
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +2 -1
- package/dist/lib/schema/get.d.ts +14 -0
- package/dist/lib/schema/get.d.ts.map +1 -0
- package/dist/lib/schema/get.js +18 -0
- package/dist/lib/schema/helpers.d.ts +21 -3
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +37 -5
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +7 -3
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +3 -11
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +7 -3
- package/dist/lib/sql-api.d.ts +9 -9
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +2 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +123 -2
- package/dist/lib/tenant.d.ts +2 -2
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/write-access.d.ts +36 -0
- package/dist/lib/write-access.d.ts.map +1 -0
- package/dist/lib/write-access.js +58 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +3 -2
- package/dist/routes/auto/plugin.js +2 -2
- package/dist/routes/auto/route-helpers.js +1 -1
- package/dist/routes/setup-swagger.d.ts.map +1 -0
- package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
- package/dist/routes/sql-api-decorator.d.ts.map +1 -0
- package/dist/{lib → routes}/sql-api-decorator.js +4 -4
- package/dist/types/cli.d.ts +27 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/dml.d.ts +6 -4
- package/dist/types/dml.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +3 -3
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/request.d.ts +24 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +1 -0
- package/dist/types/schema.d.ts +15 -0
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/search.d.ts +3 -0
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/table.d.ts +84 -11
- package/dist/types/table.d.ts.map +1 -1
- package/dist/types/validation.d.ts +3 -3
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/docs/adr/0004-updates-always-open.md +2 -1
- package/docs/adr/0014-what-follows-a-write-join.md +84 -0
- package/docs/adr/0015-non-writable-columns.md +86 -0
- package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
- package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
- package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
- package/docs/adr/README.md +5 -0
- package/package.json +1 -1
- package/dist/lib/setup-swagger.d.ts.map +0 -1
- package/dist/lib/sql-api-decorator.d.ts.map +0 -1
- /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
- /package/dist/{lib → routes}/setup-swagger.js +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ApiRequest } from '../../types/request.js';
|
|
2
|
+
import type { QueryClient } from '../db.js';
|
|
3
|
+
import type { ITable, ReadSource } from '../../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Run a table's `afterRead` hook over the rows a read produced.
|
|
6
|
+
*
|
|
7
|
+
* Every read path funnels through this one helper so the hook has a single meaning: it is
|
|
8
|
+
* handed camelCase rows of `tableConf`'s own table, in one batch, and mutates them in place.
|
|
9
|
+
* Batching is the contract, not an optimisation — a hook that decrypts through a remote KMS
|
|
10
|
+
* gets one call per result set instead of one per row.
|
|
11
|
+
*
|
|
12
|
+
* `tableConf` is resolved by the caller, which on a join is the JOINED table's config
|
|
13
|
+
* (`dbTables[joinSchema.tableName]`). That is what makes the hook follow a relation without
|
|
14
|
+
* being declared on the host, the way `readExclude` and `tenantScope` do (ADR 0010).
|
|
15
|
+
*
|
|
16
|
+
* An empty result set is skipped: there is nothing to transform, and the hook may be costly.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runAfterRead(db: QueryClient, request: ApiRequest | undefined, rows: Record<string, unknown>[], tableConf: ITable | undefined, source: ReadSource, alias?: string): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=read-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-hooks.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/read-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAoB,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,UAAU,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a table's `afterRead` hook over the rows a read produced.
|
|
3
|
+
*
|
|
4
|
+
* Every read path funnels through this one helper so the hook has a single meaning: it is
|
|
5
|
+
* handed camelCase rows of `tableConf`'s own table, in one batch, and mutates them in place.
|
|
6
|
+
* Batching is the contract, not an optimisation — a hook that decrypts through a remote KMS
|
|
7
|
+
* gets one call per result set instead of one per row.
|
|
8
|
+
*
|
|
9
|
+
* `tableConf` is resolved by the caller, which on a join is the JOINED table's config
|
|
10
|
+
* (`dbTables[joinSchema.tableName]`). That is what makes the hook follow a relation without
|
|
11
|
+
* being declared on the host, the way `readExclude` and `tenantScope` do (ADR 0010).
|
|
12
|
+
*
|
|
13
|
+
* An empty result set is skipped: there is nothing to transform, and the hook may be costly.
|
|
14
|
+
*/
|
|
15
|
+
export async function runAfterRead(db, request, rows, tableConf, source, alias) {
|
|
16
|
+
if (!tableConf?.afterRead || rows.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const ctx = alias === undefined ? { source } : { source, alias };
|
|
19
|
+
await tableConf.afterRead(db, request, rows, ctx);
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA0CrE"}
|
|
@@ -3,9 +3,10 @@ import { camelcaseObject } from '../../naming.js';
|
|
|
3
3
|
import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
|
|
4
4
|
import { httpError } from '../../errors.js';
|
|
5
5
|
import { readableSelectColumns } from '../../read-access.js';
|
|
6
|
+
import { runAfterRead } from '../read-hooks.js';
|
|
6
7
|
import { primaryAsString, isCompositePrimary } from '../../../types.js';
|
|
7
8
|
export async function getEngine(params) {
|
|
8
|
-
const { db, tableConf, id, tenant } = params;
|
|
9
|
+
const { db, tableConf, id, tenant, request } = params;
|
|
9
10
|
if (isCompositePrimary(tableConf.primary)) {
|
|
10
11
|
throw httpError(400, `Table "${tableConf.Schema.tableName}" has a composite primary key: get-by-id would match on "${primaryAsString(tableConf.primary)}" alone. Use search with all PK fields.`);
|
|
11
12
|
}
|
|
@@ -34,5 +35,7 @@ export async function getEngine(params) {
|
|
|
34
35
|
});
|
|
35
36
|
if (rows.length === 0)
|
|
36
37
|
throw httpError(404, `Record not found: ${id}`);
|
|
37
|
-
|
|
38
|
+
const main = camelcaseObject(rows[0], tableConf.Schema);
|
|
39
|
+
await runAfterRead(db, request, [main], tableConf, 'get');
|
|
40
|
+
return { main };
|
|
38
41
|
}
|
|
@@ -32,7 +32,7 @@ export async function insertEngine(params) {
|
|
|
32
32
|
let secondaryResults;
|
|
33
33
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
34
34
|
const mainForFK = { ...inputRecord, ...mainPkCamel };
|
|
35
|
-
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
|
|
35
|
+
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, secondaries);
|
|
36
36
|
}
|
|
37
37
|
// 5. afterInsert hook (camelCase — input merged with generated PK)
|
|
38
38
|
if (tableConf.afterInsert) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA+G9E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { snakecaseRecord } from '../../naming.js';
|
|
2
|
+
import { removeWriteExcluded } from '../../write-access.js';
|
|
2
3
|
import { processSecondaries, processDeletions } from '../write-helpers.js';
|
|
3
4
|
import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
|
|
4
5
|
import { runValidation } from '../validate.js';
|
|
@@ -46,22 +47,25 @@ export async function updateEngine(params) {
|
|
|
46
47
|
if (tableConf.beforeUpdate) {
|
|
47
48
|
await tableConf.beforeUpdate(db, request, inputRecord);
|
|
48
49
|
}
|
|
49
|
-
// 5.
|
|
50
|
+
// 5. Drop what the database will not be told about (generated columns, writeExclude).
|
|
51
|
+
// After the hook on purpose: the column refuses a value whoever assigned it.
|
|
52
|
+
removeWriteExcluded(inputRecord, tableConf, schema);
|
|
53
|
+
// 6. Convert to DB format (after all user mutations)
|
|
50
54
|
const snaked = snakecaseRecord(inputRecord, schema);
|
|
51
55
|
const updateFields = { ...snaked };
|
|
52
56
|
for (const c of pkCols)
|
|
53
57
|
delete updateFields[c];
|
|
54
|
-
//
|
|
58
|
+
// 7. Strip tenant column (user cannot change tenant of an existing record).
|
|
55
59
|
// For indirect scopes the tenant link is the through-FK (localField): it may be changed,
|
|
56
60
|
// but only to another value the caller owns — re-validate the new FK against the tenant.
|
|
57
61
|
if (tenant?.scope) {
|
|
58
62
|
stripTenantColumn(updateFields, tenant.scope);
|
|
59
63
|
await enforceTenantFKOnUpdate(db, tenant, updateFields);
|
|
60
64
|
}
|
|
61
|
-
// Steps
|
|
65
|
+
// Steps 8-10 are atomic: a failure in secondaries or deletions rolls back the main
|
|
62
66
|
// update too. Degrades to non-transactional when the adapter has no connect().
|
|
63
67
|
return db.withTransaction(async (tx) => {
|
|
64
|
-
//
|
|
68
|
+
// 8. Update main
|
|
65
69
|
const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
|
|
66
70
|
const extraCondition = buildTenantUpdateExtra(tx, tenant);
|
|
67
71
|
// WHERE matching every primary key column (single or composite).
|
|
@@ -76,23 +80,23 @@ export async function updateEngine(params) {
|
|
|
76
80
|
}
|
|
77
81
|
// Build the main response (PK-only — every PK field)
|
|
78
82
|
const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
|
|
79
|
-
//
|
|
83
|
+
// 9. Secondaries (upsert/insert with FK auto-fill — camelCase)
|
|
80
84
|
let secondaryResults;
|
|
81
85
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
82
86
|
const mainForFK = { ...inputRecord };
|
|
83
|
-
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
|
|
87
|
+
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, secondaries);
|
|
84
88
|
}
|
|
85
|
-
//
|
|
89
|
+
// 10. Deletions (FK auto-fill from main like secondaries)
|
|
86
90
|
let deletionResults;
|
|
87
91
|
if (deletions && Object.keys(deletions).length > 0) {
|
|
88
92
|
const mainForFK = { ...inputRecord };
|
|
89
|
-
deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, deletions);
|
|
93
|
+
deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, deletions);
|
|
90
94
|
}
|
|
91
|
-
//
|
|
95
|
+
// 11. afterUpdate hook (inside the transaction: throwing rolls everything back)
|
|
92
96
|
if (tableConf.afterUpdate) {
|
|
93
97
|
await tableConf.afterUpdate(tx, request, inputRecord, secondaryResults, deletionResults);
|
|
94
98
|
}
|
|
95
|
-
//
|
|
99
|
+
// 12. Return PK-only
|
|
96
100
|
const result = { main: mainResult };
|
|
97
101
|
if (secondaryResults && Object.keys(secondaryResults).length > 0) {
|
|
98
102
|
result.secondaries = secondaryResults;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four join families, each executed against the rows the main query returned.
|
|
3
|
+
*
|
|
4
|
+
* joinMustExist child -> main, 1:N EXISTS filter on the main query (returns no rows)
|
|
5
|
+
* joinMultiple child -> main, 1:N side query, rows under result.joinMultiple.<alias>
|
|
6
|
+
* joinGroup child -> main, 1:N aggregations under result.joinGroup.<alias>
|
|
7
|
+
* joinLeft parent -> main, N:1 real LEFT JOIN on demand + side query for the rows
|
|
8
|
+
*/
|
|
9
|
+
import type { ApiRequest } from '../../../types/request.js';
|
|
1
10
|
import type { QueryClient } from '../../db.js';
|
|
2
11
|
import type { QueryParams } from '../query-params.js';
|
|
3
12
|
import type { DbTables, ITable, JoinFetchRequest, JoinGroupRequest, JoinRefFilter, TenantContext } from '../../../types.js';
|
|
@@ -26,9 +35,9 @@ export declare function buildLeftJoinOnClauses(db: QueryClient, dbTables: DbTabl
|
|
|
26
35
|
/** The WHERE fragments a `joinLeft` request's own filters and conditions contribute. */
|
|
27
36
|
export declare function buildLeftJoinFilters(db: QueryClient, dbTables: DbTables, tableConf: ITable, aliasesNeedingJoin: Set<string>, joinLeft: Record<string, JoinFetchRequest> | undefined, startIdx: number): LeftJoinFilterBuild;
|
|
28
37
|
/** joinMultiple: 1:N children, returned as rows under `result.joinMultiple.<alias>`. */
|
|
29
|
-
export declare function executeJoinMultiple(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinMultiple: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
|
|
38
|
+
export declare function executeJoinMultiple(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinMultiple: Record<string, JoinFetchRequest>, tenant?: TenantContext, request?: ApiRequest): Promise<Record<string, Record<string, unknown>[]>>;
|
|
30
39
|
/** joinLeft: N:1 parents, returned as rows under `result.joinLeft.<alias>`. */
|
|
31
|
-
export declare function executeJoinLeft(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinLeft: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
|
|
40
|
+
export declare function executeJoinLeft(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinLeft: Record<string, JoinFetchRequest>, tenant?: TenantContext, request?: ApiRequest): Promise<Record<string, Record<string, unknown>[]>>;
|
|
32
41
|
export declare function executeJoinGroup(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinGroup: Record<string, JoinGroupRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>>>;
|
|
33
42
|
export declare function buildJoinMustExistClauses(params: QueryParams, db: QueryClient, dbTables: DbTables, tableConf: ITable, joinMustExist: Record<string, JoinRefFilter>, tenant?: TenantContext): string;
|
|
34
43
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"join-families.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/join-families.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"join-families.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/join-families.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The four join families, each executed against the rows the main query returned.
|
|
3
|
-
*
|
|
4
|
-
* joinMustExist child -> main, 1:N EXISTS filter on the main query (returns no rows)
|
|
5
|
-
* joinMultiple child -> main, 1:N side query, rows under result.joinMultiple.<alias>
|
|
6
|
-
* joinGroup child -> main, 1:N aggregations under result.joinGroup.<alias>
|
|
7
|
-
* joinLeft parent -> main, N:1 real LEFT JOIN on demand + side query for the rows
|
|
8
|
-
*/
|
|
9
1
|
import { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
10
2
|
import { assertReadable, assertFiltersReadable, hasOwnField, ownField } from '../../read-access.js';
|
|
11
3
|
import { ALLOWED_SET } from '../../condition-methods.js';
|
|
12
4
|
import { err400, validateSchemaField, evaluateComputedField, mapRowsToCamelCase, buildSelectionColumns, } from './fields.js';
|
|
5
|
+
import { runAfterRead } from '../read-hooks.js';
|
|
13
6
|
import { appendJoinTenantScope, subqueryAlias, requireJoin, extractJoinRefs, } from './joins.js';
|
|
14
7
|
import { tenantForTable, buildTenantRowGuard } from '../../tenant.js';
|
|
15
8
|
import { dispatchConditionMethod, buildJoinRefCondition } from './conditions.js';
|
|
@@ -152,7 +145,7 @@ function scopeJoinSideQuery(cb, db, joinSchema, joinField, ids, joinTableConf, t
|
|
|
152
145
|
const where = cb.build(1, db.ph);
|
|
153
146
|
return { where, values: cb.getValues(), tenantJoins };
|
|
154
147
|
}
|
|
155
|
-
async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mode, tenant) {
|
|
148
|
+
async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mode, tenant, request) {
|
|
156
149
|
const result = {};
|
|
157
150
|
for (const [alias, ref] of Object.entries(requests)) {
|
|
158
151
|
const joinDef = requireJoin(tableConf, alias, mode.requireUnique);
|
|
@@ -182,22 +175,27 @@ async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mod
|
|
|
182
175
|
joins: tenantJoins.length > 0 ? tenantJoins : undefined,
|
|
183
176
|
});
|
|
184
177
|
result[alias] = mapRowsToCamelCase(rows, joinSchema);
|
|
178
|
+
// The joined table's own hook, not the host's: a transform belongs to the table that
|
|
179
|
+
// owns the column, wherever its rows surface.
|
|
180
|
+
await runAfterRead(db, request, result[alias], joinTableConf, mode.source, alias);
|
|
185
181
|
}
|
|
186
182
|
return result;
|
|
187
183
|
}
|
|
188
184
|
/** joinMultiple: 1:N children, returned as rows under `result.joinMultiple.<alias>`. */
|
|
189
|
-
export function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
|
|
185
|
+
export function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant, request) {
|
|
190
186
|
return fetchJoinRows(db, dbTables, tableConf, mainResults, joinMultiple, {
|
|
187
|
+
source: 'joinMultiple',
|
|
191
188
|
requireUnique: false,
|
|
192
189
|
applyRefCondition: true,
|
|
193
|
-
}, tenant);
|
|
190
|
+
}, tenant, request);
|
|
194
191
|
}
|
|
195
192
|
/** joinLeft: N:1 parents, returned as rows under `result.joinLeft.<alias>`. */
|
|
196
|
-
export function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
|
|
193
|
+
export function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant, request) {
|
|
197
194
|
return fetchJoinRows(db, dbTables, tableConf, mainResults, joinLeft, {
|
|
195
|
+
source: 'joinLeft',
|
|
198
196
|
requireUnique: true,
|
|
199
197
|
applyRefCondition: false,
|
|
200
|
-
}, tenant);
|
|
198
|
+
}, tenant, request);
|
|
201
199
|
}
|
|
202
200
|
/**
|
|
203
201
|
* Resolve `aggregations.by` to a SQL expression. Accepts:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAyP3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4FvB"}
|
|
@@ -5,6 +5,7 @@ import { MAX_CONDITIONS, MAX_ORDER_BY_PARTS } from '../../condition-methods.js';
|
|
|
5
5
|
import { primaryAsString } from '../../../types.js';
|
|
6
6
|
import { err400, resolveFieldRef, evaluateComputedField, refValues, renderRef, mapRowsToCamelCase, } from './fields.js';
|
|
7
7
|
import { assertJoinFilterKeys, requireJoin } from './joins.js';
|
|
8
|
+
import { runAfterRead } from '../read-hooks.js';
|
|
8
9
|
import { collectOrderByLeftAliases, validateOrderBy, convertConfiguredOrder } from './order-by.js';
|
|
9
10
|
import { applyConditions, applyComputedFilters } from './conditions.js';
|
|
10
11
|
import { appendAggConditions } from './aggregations.js';
|
|
@@ -187,12 +188,12 @@ function collectLeftJoinAliases(params) {
|
|
|
187
188
|
* it returned.
|
|
188
189
|
*/
|
|
189
190
|
async function attachJoinResults(result, dbTables, params, main) {
|
|
190
|
-
const { db, tableConf, joinMultiple, joinLeft, joinGroup, tenant } = params;
|
|
191
|
+
const { db, tableConf, joinMultiple, joinLeft, joinGroup, tenant, request } = params;
|
|
191
192
|
if (joinMultiple && Object.keys(joinMultiple).length > 0) {
|
|
192
|
-
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
|
|
193
|
+
result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant, request);
|
|
193
194
|
}
|
|
194
195
|
if (joinLeft && Object.keys(joinLeft).length > 0) {
|
|
195
|
-
result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
|
|
196
|
+
result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant, request);
|
|
196
197
|
}
|
|
197
198
|
if (joinGroup && Object.keys(joinGroup).length > 0) {
|
|
198
199
|
result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
|
|
@@ -248,6 +249,11 @@ export async function searchEngine(dbTables, params) {
|
|
|
248
249
|
}
|
|
249
250
|
const result = { main };
|
|
250
251
|
await attachJoinResults(result, dbTables, params, main);
|
|
252
|
+
// After the joins, not before: they correlate to the main rows by the values the DB
|
|
253
|
+
// returned (`fk IN (ids)`), and a hook that rewrites the column holding that key would
|
|
254
|
+
// have them look for a value no child row carries. `main` is the same array either way,
|
|
255
|
+
// so the response still carries the hook's mutations.
|
|
256
|
+
await runAfterRead(db, params.request, main, tableConf, 'search');
|
|
251
257
|
if (pagination) {
|
|
252
258
|
result.pagination = pagination;
|
|
253
259
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApiRequest } from '../../types/request.js';
|
|
2
2
|
import type { QueryClient } from '../db.js';
|
|
3
3
|
import type { ITable, BulkValidatorItem } from '../../types.js';
|
|
4
|
-
export declare function runValidation(db: QueryClient, request:
|
|
5
|
-
export declare function runBulkValidation(db: QueryClient, request:
|
|
4
|
+
export declare function runValidation(db: QueryClient, request: ApiRequest, tableConf: ITable, main: Record<string, unknown>, secondaries?: Record<string, Record<string, unknown>[]>): Promise<void>;
|
|
5
|
+
export declare function runBulkValidation(db: QueryClient, request: ApiRequest, tableConf: ITable, items: BulkValidatorItem[]): Promise<void>;
|
|
6
6
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAmB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAgBjF,wBAAsB,aAAa,CACjC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACtD,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAOf"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApiRequest } from '../../types/request.js';
|
|
2
2
|
import type { QueryClient } from '../db.js';
|
|
3
3
|
import type { ITable, DbTables, TenantContext } from '../../types.js';
|
|
4
4
|
export declare function findSecondaryTableConf(dbTables: DbTables, joinTableName: string): ITable | undefined;
|
|
@@ -13,11 +13,13 @@ export interface WriteJoinPass {
|
|
|
13
13
|
/** Main record (camelCase), the source of the FK auto-filled into every child row. */
|
|
14
14
|
mainRecord: Record<string, unknown>;
|
|
15
15
|
tenant?: TenantContext;
|
|
16
|
+
/** The request that triggered the write, handed to the child table's `beforeInsert`. */
|
|
17
|
+
request: ApiRequest;
|
|
16
18
|
}
|
|
17
19
|
interface PrepareCtx {
|
|
18
20
|
db: QueryClient;
|
|
19
21
|
tableConf: ITable;
|
|
20
|
-
request:
|
|
22
|
+
request: ApiRequest;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Prepare a single record for INSERT/UPSERT:
|
|
@@ -26,7 +28,9 @@ interface PrepareCtx {
|
|
|
26
28
|
* 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
|
|
27
29
|
* hook so values it assigns — e.g. a server-generated id — reach the INSERT)
|
|
28
30
|
* 4. beforeInsert hook (caller can mutate the camelCase copy)
|
|
29
|
-
* 5.
|
|
31
|
+
* 5. drop non-writable fields (generated columns, writeExclude) — AFTER the hook, because
|
|
32
|
+
* the database refuses those values whoever assigned them
|
|
33
|
+
* 6. snakecaseRecord → DB format
|
|
30
34
|
*
|
|
31
35
|
* Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
|
|
32
36
|
* `snake` (DB-ready record). The original `input` is left intact.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAU5C,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EAGR,aAAa,EACd,MAAM,gBAAgB,CAAC;AAmCxB,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAKpB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,wFAAwF;IACxF,OAAO,EAAE,UAAU,CAAC;CACrB;AAmCD,UAAU,UAAU;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EACvD,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAgB7E;AAkCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACrD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CA0DpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACnD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAoBpD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { camelcaseObject, snakecaseRecord } from '../naming.js';
|
|
2
2
|
import { runValidation } from './validate.js';
|
|
3
|
+
import { removeWriteExcluded } from '../write-access.js';
|
|
3
4
|
import { tenantForTable, buildTenantRowGuard, enforceTenantOnWrites, assertTenantOwnsConflicts, tenantImmutableColumns, } from '../tenant.js';
|
|
4
5
|
/**
|
|
5
6
|
* Resolve the write join an alias names, or undefined when the table does not declare it.
|
|
@@ -14,26 +15,11 @@ function findWriteJoin(tableConf, alias) {
|
|
|
14
15
|
return tableConf.allowedWriteJoins?.find((j) => j.alias === alias);
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
* Drop `excludeFromCreation`
|
|
18
|
+
* Drop `excludeFromCreation` fields from a camelCase record, in place.
|
|
18
19
|
*
|
|
19
|
-
* Exclusion is a whitelist on CLIENT input: call this on the client payload only,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
function removeExcludedFields(record, tableConf) {
|
|
24
|
-
if (!tableConf.excludeFromCreation?.length)
|
|
25
|
-
return record;
|
|
26
|
-
const result = { ...record };
|
|
27
|
-
for (const field of tableConf.excludeFromCreation) {
|
|
28
|
-
const col = tableConf.Schema.col(field);
|
|
29
|
-
delete result[col];
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Same whitelist as `removeExcludedFields`, but for a camelCase record (schema
|
|
35
|
-
* field names) and in place. Used on the client payload before the beforeInsert
|
|
36
|
-
* hook runs.
|
|
20
|
+
* Exclusion is a whitelist on CLIENT input: call this on the client payload only, BEFORE any
|
|
21
|
+
* server-side value is assigned (beforeInsert mutations, FK auto-fill), so engine- and
|
|
22
|
+
* hook-generated values on those fields reach the SQL (ADR 0005).
|
|
37
23
|
*/
|
|
38
24
|
function removeExcludedFieldsCamel(record, tableConf) {
|
|
39
25
|
if (!tableConf.excludeFromCreation?.length)
|
|
@@ -71,7 +57,9 @@ function resolveWriteJoinTarget(joinDef, dbTables, mainRecord, tenant) {
|
|
|
71
57
|
* 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
|
|
72
58
|
* hook so values it assigns — e.g. a server-generated id — reach the INSERT)
|
|
73
59
|
* 4. beforeInsert hook (caller can mutate the camelCase copy)
|
|
74
|
-
* 5.
|
|
60
|
+
* 5. drop non-writable fields (generated columns, writeExclude) — AFTER the hook, because
|
|
61
|
+
* the database refuses those values whoever assigned them
|
|
62
|
+
* 6. snakecaseRecord → DB format
|
|
75
63
|
*
|
|
76
64
|
* Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
|
|
77
65
|
* `snake` (DB-ready record). The original `input` is left intact.
|
|
@@ -88,6 +76,7 @@ export async function prepareInsertRecord(ctx, input, secondaries, options) {
|
|
|
88
76
|
if (ctx.tableConf.beforeInsert) {
|
|
89
77
|
await ctx.tableConf.beforeInsert(ctx.db, ctx.request, camel);
|
|
90
78
|
}
|
|
79
|
+
removeWriteExcluded(camel, ctx.tableConf, ctx.tableConf.Schema);
|
|
91
80
|
const snake = snakecaseRecord(camel, ctx.tableConf.Schema);
|
|
92
81
|
return { camel, snake };
|
|
93
82
|
}
|
|
@@ -121,23 +110,33 @@ async function forEachWriteJoin(pass, payload, writeAlias) {
|
|
|
121
110
|
* where the child is upserted, hijack one of that tenant's existing rows via the conflict key.
|
|
122
111
|
*/
|
|
123
112
|
export function processSecondaries(pass, secondaries) {
|
|
124
|
-
const { db, tableConf } = pass;
|
|
113
|
+
const { db, tableConf, request } = pass;
|
|
125
114
|
return forEachWriteJoin(pass, secondaries, async (records, target, joinDef) => {
|
|
126
115
|
const { joinSchema, joinCol, mainValue, childConf, childTenant } = target;
|
|
127
116
|
const childPk = childConf?.primary || joinDef.joinField;
|
|
128
117
|
const childPkCol = Array.isArray(childPk)
|
|
129
118
|
? childPk.map((f) => joinSchema.col(f))
|
|
130
119
|
: joinSchema.col(childPk);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
// Same order as `prepareInsertRecord` (ADR 0005): the client whitelist runs first, then
|
|
121
|
+
// the child's own `beforeInsert`, then the DB format — so a value the hook assigns to an
|
|
122
|
+
// excluded field still reaches the INSERT. A secondary is a write like any other, and its
|
|
123
|
+
// table's transform has to run here too or a column that table encodes on the way in is
|
|
124
|
+
// stored raw whenever its rows arrive as another table's children.
|
|
125
|
+
const preparedRecords = [];
|
|
126
|
+
for (const rec of records) {
|
|
127
|
+
const camel = { ...rec };
|
|
135
128
|
if (childConf) {
|
|
136
|
-
|
|
129
|
+
removeExcludedFieldsCamel(camel, childConf);
|
|
130
|
+
if (childConf.beforeInsert) {
|
|
131
|
+
await childConf.beforeInsert(db, request, camel);
|
|
132
|
+
}
|
|
137
133
|
}
|
|
134
|
+
removeWriteExcluded(camel, childConf, joinSchema);
|
|
135
|
+
const prepared = snakecaseRecord(camel, joinSchema);
|
|
136
|
+
// FK auto-fill last: the engine's own value, not the client's and not the hook's.
|
|
138
137
|
prepared[joinCol] = mainValue;
|
|
139
|
-
|
|
140
|
-
}
|
|
138
|
+
preparedRecords.push(prepared);
|
|
139
|
+
}
|
|
141
140
|
// Tenant enforcement runs after the FK auto-fill: for an indirect scope the through-FK
|
|
142
141
|
// the engine just injected is the value being validated.
|
|
143
142
|
await enforceTenantOnWrites(db, childTenant, preparedRecords);
|
|
@@ -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;
|
|
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;AAU/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAuB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { primaryAsString } from '../../types.js';
|
|
3
|
+
import { writableFields } from '../write-access.js';
|
|
3
4
|
import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
|
|
4
5
|
export function BulkUpsertTableBody(dbTables, tableName, maxItems) {
|
|
5
6
|
const tableConf = dbTables[tableName];
|
|
6
7
|
const schema = tableConf.Schema;
|
|
7
8
|
// 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 }));
|
|
9
|
+
const mainSchema = Type.Partial(Type.Object(writableFields(applySchemaOverrides({ ...schema.fields }, tableConf), tableConf, schema), { additionalProperties: false }));
|
|
9
10
|
const itemProperties = {
|
|
10
11
|
main: mainSchema,
|
|
11
12
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TObject } from '@sinclair/typebox';
|
|
2
|
+
import type { DbTables } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Response shape of `GET /rest/:table/:id`.
|
|
5
|
+
*
|
|
6
|
+
* Lives here rather than inline in the route so it is built from the same pieces as every
|
|
7
|
+
* other operation: it drifted while it was inline, documenting `readExclude`d fields the
|
|
8
|
+
* engine never selects.
|
|
9
|
+
*
|
|
10
|
+
* `Partial` because a record is returned column by column: a nullable column the driver
|
|
11
|
+
* reports as absent must not fail serialization.
|
|
12
|
+
*/
|
|
13
|
+
export declare function GetTableResponse(dbTables: DbTables, tableName: string): TObject;
|
|
14
|
+
//# sourceMappingURL=get.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/get.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAK/E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { readableFields } from './helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Response shape of `GET /rest/:table/:id`.
|
|
5
|
+
*
|
|
6
|
+
* Lives here rather than inline in the route so it is built from the same pieces as every
|
|
7
|
+
* other operation: it drifted while it was inline, documenting `readExclude`d fields the
|
|
8
|
+
* engine never selects.
|
|
9
|
+
*
|
|
10
|
+
* `Partial` because a record is returned column by column: a nullable column the driver
|
|
11
|
+
* reports as absent must not fail serialization.
|
|
12
|
+
*/
|
|
13
|
+
export function GetTableResponse(dbTables, tableName) {
|
|
14
|
+
const tableConf = dbTables[tableName];
|
|
15
|
+
return Type.Object({
|
|
16
|
+
main: Type.Partial(Type.Object(readableFields(tableConf.Schema, tableConf))),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -5,11 +5,27 @@ import type { ITable, SchemaDefinition, DbTables, JoinDefinition } from '../../t
|
|
|
5
5
|
* Falls back to `fallback` field if tableConf is undefined.
|
|
6
6
|
*/
|
|
7
7
|
export declare function pkSchema(tableConf: ITable | undefined, schema: SchemaDefinition, fallback: string): Record<string, TSchema>;
|
|
8
|
+
/** The same schema without the Optional modifier, i.e. as a required property. */
|
|
9
|
+
export declare function asRequired(schema: TSchema): TSchema;
|
|
8
10
|
/**
|
|
9
|
-
* Apply schemaOverrides to a fields record
|
|
10
|
-
*
|
|
11
|
+
* Apply schemaOverrides to a fields record: each matching field becomes the declared schema,
|
|
12
|
+
* verbatim.
|
|
13
|
+
*
|
|
14
|
+
* Write bodies only. An override is a rule about what this API accepts **from now on**, and
|
|
15
|
+
* the rows already in the table were written before it existed — a column narrowed to
|
|
16
|
+
* `format: 'email'` today holds whatever was accepted last year, and one made mandatory today
|
|
17
|
+
* is `NULL` on every pre-existing row. Describing the response with it would publish a promise
|
|
18
|
+
* about data nobody can retroactively make true. Responses stay on the generated type.
|
|
19
|
+
*
|
|
20
|
+
* Verbatim, because on the request side the schema is the rule: an override without
|
|
21
|
+
* `Type.Optional` makes the field mandatory and one without `Nullable` rejects an explicit
|
|
22
|
+
* `null`, whatever the column allows. That is how a column the DB had to leave nullable is
|
|
23
|
+
* made mandatory going forward. Write the modifiers when you want them.
|
|
24
|
+
*
|
|
25
|
+
* Replace-only: an override naming a field the table does not have is ignored, so the
|
|
26
|
+
* declaration can never introduce a property the engine will not produce.
|
|
11
27
|
*/
|
|
12
|
-
export declare function applySchemaOverrides(fields: Record<string, TSchema>, tableConf: ITable): Record<string, TSchema>;
|
|
28
|
+
export declare function applySchemaOverrides(fields: Record<string, TSchema>, tableConf: ITable | undefined): Record<string, TSchema>;
|
|
13
29
|
type JoinFieldsBuilder = (join: JoinDefinition, secondaryConf: ITable | undefined) => Record<string, TSchema>;
|
|
14
30
|
/**
|
|
15
31
|
* Attach `secondaries` (and optionally `deletions`) sections to the given target object,
|
|
@@ -29,5 +45,7 @@ export declare function attachWriteJoinSections(target: Record<string, TSchema>,
|
|
|
29
45
|
export declare const writeJoinBodyFields: JoinFieldsBuilder;
|
|
30
46
|
/** JoinFieldsBuilder for response schemas: PK only. */
|
|
31
47
|
export declare const writeJoinResponseFields: JoinFieldsBuilder;
|
|
48
|
+
/** Schema fields minus the ones hidden by `readExclude`. */
|
|
49
|
+
export declare function readableFields(schema: SchemaDefinition, tableConf: ITable | undefined): Record<string, TSchema>;
|
|
32
50
|
export {};
|
|
33
51
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKzF;;;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;AAiCD,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAGnD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,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;AAwB7B;;;;;;;;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,CA8BN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC;AAE1C,4DAA4D;AAC5D,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB"}
|