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
package/dist/types/cli.d.ts
CHANGED
|
@@ -19,8 +19,30 @@ export interface ColumnInfo {
|
|
|
19
19
|
is_nullable: string;
|
|
20
20
|
/** True when the column is part of the table's PRIMARY KEY (from information_schema). */
|
|
21
21
|
is_primary?: boolean;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* True for columns the database fills in by itself without a visible `column_default`:
|
|
24
|
+
* MySQL `AUTO_INCREMENT` and PostgreSQL `GENERATED ... AS IDENTITY`. Both behave like a
|
|
25
|
+
* default — omit the column and the value appears — so the generated field is Optional and
|
|
26
|
+
* the CLI lists it under `excludeFromCreation`.
|
|
27
|
+
*/
|
|
23
28
|
is_auto_increment?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* True for a column computed from an expression: `GENERATED ALWAYS AS (<expr>)`, STORED on
|
|
31
|
+
* PostgreSQL, VIRTUAL or STORED on MySQL. Unlike an identity column this one is never
|
|
32
|
+
* writable at all — naming it in an INSERT or an UPDATE is an error on both engines — so it
|
|
33
|
+
* is read-only for the whole API, not just at creation time.
|
|
34
|
+
*/
|
|
35
|
+
is_generated?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* True when the relation is a view rather than a base table — `information_schema.tables`
|
|
38
|
+
* on both engines, plus `pg_class.relkind = 'm'` for PostgreSQL materialized views, which
|
|
39
|
+
* `information_schema` does not describe at all.
|
|
40
|
+
*
|
|
41
|
+
* A view is still a table config: what changes is what may be assumed about it. It carries
|
|
42
|
+
* no PRIMARY KEY constraint, so `is_primary` is false on every column, and only a view
|
|
43
|
+
* simple enough for the engine to make updatable accepts a write.
|
|
44
|
+
*/
|
|
45
|
+
is_view?: boolean;
|
|
24
46
|
}
|
|
25
47
|
export interface TableMap {
|
|
26
48
|
[schemaName: string]: {
|
|
@@ -29,6 +51,10 @@ export interface TableMap {
|
|
|
29
51
|
colMap: Record<string, string>;
|
|
30
52
|
/** PRIMARY KEY fields (camelCase), in table column order. Empty when unknown. */
|
|
31
53
|
primary: string[];
|
|
54
|
+
/** Database-computed fields (camelCase), in table column order. Never writable. */
|
|
55
|
+
generated: string[];
|
|
56
|
+
/** True when this relation is a view (or a materialized view), not a base table. */
|
|
57
|
+
isView: boolean;
|
|
32
58
|
};
|
|
33
59
|
}
|
|
34
60
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/types/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/types/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,UAAU,EAAE,MAAM,GAAG;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,iFAAiF;QACjF,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,mFAAmF;QACnF,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,oFAAoF;QACpF,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;CACH"}
|
package/dist/types/dml.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApiRequest } from './request.js';
|
|
2
2
|
import type { QueryClient } from '../lib/db.js';
|
|
3
3
|
import type { ITable, DbTables } from './table.js';
|
|
4
4
|
import type { TenantContext } from './tenant.js';
|
|
@@ -9,7 +9,7 @@ interface DmlBaseParams {
|
|
|
9
9
|
}
|
|
10
10
|
interface DmlWriteBaseParams extends DmlBaseParams {
|
|
11
11
|
dbTables: DbTables;
|
|
12
|
-
request:
|
|
12
|
+
request: ApiRequest;
|
|
13
13
|
}
|
|
14
14
|
export interface InsertParams extends DmlWriteBaseParams {
|
|
15
15
|
record: Record<string, unknown>;
|
|
@@ -31,13 +31,15 @@ export interface UpdateResult {
|
|
|
31
31
|
}
|
|
32
32
|
export interface GetParams extends DmlBaseParams {
|
|
33
33
|
id: string | number;
|
|
34
|
+
/** Handed to the `afterRead` hook; absent for a programmatic caller that did not pass one. */
|
|
35
|
+
request?: ApiRequest;
|
|
34
36
|
}
|
|
35
37
|
export interface GetResult {
|
|
36
38
|
main: Record<string, unknown>;
|
|
37
39
|
}
|
|
38
40
|
export interface DeleteParams extends DmlBaseParams {
|
|
39
41
|
id: string | number;
|
|
40
|
-
request?:
|
|
42
|
+
request?: ApiRequest;
|
|
41
43
|
}
|
|
42
44
|
export interface DeleteResult {
|
|
43
45
|
main: Record<string, unknown>;
|
|
@@ -57,7 +59,7 @@ export interface BulkUpsertResult {
|
|
|
57
59
|
}
|
|
58
60
|
export interface BulkDeleteParams extends DmlBaseParams {
|
|
59
61
|
ids: (string | number)[];
|
|
60
|
-
request?:
|
|
62
|
+
request?: ApiRequest;
|
|
61
63
|
}
|
|
62
64
|
export interface BulkDeleteResult {
|
|
63
65
|
main: Record<string, unknown>;
|
package/dist/types/dml.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dml.d.ts","sourceRoot":"","sources":["../../src/types/dml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"dml.d.ts","sourceRoot":"","sources":["../../src/types/dml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,UAAU,aAAa;IACrB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,UAAU,kBAAmB,SAAQ,aAAa;IAChD,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,UAAU,CAAC;CACrB;AAID,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACzD;AAID,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACvD;AAID,MAAM,WAAW,SAAU,SAAQ,aAAa;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAID,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAiB,SAAQ,kBAAkB;IAC1D,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACvD;AAID,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,GAAG,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B"}
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApiRequest, ApiReply } from './request.js';
|
|
2
2
|
import type { DialectName } from '../lib/dialect.js';
|
|
3
3
|
import type { DbTables } from './table.js';
|
|
4
4
|
import type { TenantId } from './tenant.js';
|
|
@@ -10,11 +10,11 @@ export interface SwaggerOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export interface SqlApiPluginOptions {
|
|
12
12
|
DbTables: DbTables;
|
|
13
|
-
onRequests?: ((request:
|
|
13
|
+
onRequests?: ((request: ApiRequest, reply: ApiReply) => Promise<void | ApiReply>)[];
|
|
14
14
|
prefix?: string;
|
|
15
15
|
swagger?: boolean | SwaggerOptions;
|
|
16
16
|
dialect?: DialectName;
|
|
17
|
-
getTenantId?: (request:
|
|
17
|
+
getTenantId?: (request: ApiRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
|
|
18
18
|
debug?: boolean;
|
|
19
19
|
/**
|
|
20
20
|
* Hard cap on the search page size (the `itemsPerPage` query param), and the row LIMIT applied
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/types/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC3E,OAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,qGAAqG;AACrG,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,OAAO,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
/**
|
|
3
|
+
* The request the plugin carries from a route down to consumer code.
|
|
4
|
+
*
|
|
5
|
+
* Every hook, `validate` and `getTenantId` receive it; **nothing inside `src/lib/` ever reads a
|
|
6
|
+
* property off it**. It is an opaque token that the HTTP layer produces and consumer code
|
|
7
|
+
* consumes, and the engines only forward — which is why the engines, the schema builders and
|
|
8
|
+
* the write pipeline hold no dependency on a web framework at all, only on this name.
|
|
9
|
+
*
|
|
10
|
+
* That is the point of the alias: it is the single seam where the plugin's core meets Fastify.
|
|
11
|
+
* A second HTTP adapter — or a core published apart from one — changes this file and nothing
|
|
12
|
+
* else, and `.dependency-cruiser.mjs` keeps it that way by refusing a `fastify` import anywhere
|
|
13
|
+
* under `src/lib/` or `src/types/` but here.
|
|
14
|
+
*
|
|
15
|
+
* It is a plain alias rather than a generic parameter on `ITable`: that type appears bare in
|
|
16
|
+
* over a hundred places, and threading `<Req>` through all of them — plus `DbTables`, plus
|
|
17
|
+
* every `*Params` — would change the public type surface today for a benefit that only
|
|
18
|
+
* materialises once the core ships as its own package. See
|
|
19
|
+
* [ADR 0018](../../docs/adr/0018-one-seam-to-the-http-framework.md).
|
|
20
|
+
*/
|
|
21
|
+
export type ApiRequest = FastifyRequest;
|
|
22
|
+
/** The reply, which only the HTTP layer's own options need to name. */
|
|
23
|
+
export type ApiReply = FastifyReply;
|
|
24
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/types/request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AAExC,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/schema.d.ts
CHANGED
|
@@ -8,5 +8,20 @@ export interface SchemaDefinition<T = Record<string, TSchema>> {
|
|
|
8
8
|
partialValidation: TObject;
|
|
9
9
|
/** PRIMARY KEY fields (camelCase) as introspected from the DB. Set by generated schemas. */
|
|
10
10
|
primaryKey?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Fields (camelCase) the database computes from an expression — `GENERATED ALWAYS AS (...)`.
|
|
13
|
+
* Set by generated schemas. They are readable like any other column but every write path
|
|
14
|
+
* drops them: the engine rejects a value for them, so offering the field could only ever
|
|
15
|
+
* turn a client mistake into a driver error.
|
|
16
|
+
*/
|
|
17
|
+
generatedFields?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* True when the underlying relation is a view. Set by generated schemas.
|
|
20
|
+
*
|
|
21
|
+
* A view is a table config like any other — this only records that the database had no
|
|
22
|
+
* PRIMARY KEY to report, so an absent `primaryKey` means "not declared" rather than
|
|
23
|
+
* "this data has no key".
|
|
24
|
+
*/
|
|
25
|
+
isView?: boolean;
|
|
11
26
|
}
|
|
12
27
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/types/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,CAAC,CAAC;IACV,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/types/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,CAAC,CAAC;IACV,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
|
package/dist/types/search.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApiRequest } from './request.js';
|
|
1
2
|
import type { QueryClient } from '../lib/db.js';
|
|
2
3
|
import type { ITable, FilterRecord } from './table.js';
|
|
3
4
|
import type { TenantContext } from './tenant.js';
|
|
@@ -35,6 +36,8 @@ export interface SearchParams {
|
|
|
35
36
|
* sets it from `maxItemsPerPage`.
|
|
36
37
|
*/
|
|
37
38
|
maxRows?: number;
|
|
39
|
+
/** Handed to the `afterRead` hook; absent for a programmatic caller that did not pass one. */
|
|
40
|
+
request?: ApiRequest;
|
|
38
41
|
}
|
|
39
42
|
export interface PaginationResult {
|
|
40
43
|
total: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B"}
|
package/dist/types/table.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Expression, ConditionBuilder, ConditionValueOrUndefined, DialectName as CbDialect } from 'node-condition-builder';
|
|
2
2
|
import type { TSchema, Static } from '@sinclair/typebox';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ApiRequest, ApiReply } from './request.js';
|
|
4
4
|
import type { QueryClient } from '../lib/db.js';
|
|
5
5
|
import type { SchemaDefinition } from './schema.js';
|
|
6
6
|
import type { ComputedFieldFn } from './computed.js';
|
|
@@ -23,6 +23,14 @@ export type ExtendedConditionFn = (condition: ConditionBuilder, filters: FilterR
|
|
|
23
23
|
* resolve against the OUTER query.
|
|
24
24
|
*/
|
|
25
25
|
export type TableFilterFn = (filters: FilterRecord, dialect?: CbDialect, qualifier?: string) => ConditionBuilder;
|
|
26
|
+
/** Which read path produced the rows an `afterRead` hook is handed. */
|
|
27
|
+
export type ReadSource = 'get' | 'search' | 'joinMultiple' | 'joinLeft';
|
|
28
|
+
/** Where the rows an `afterRead` hook receives came from. */
|
|
29
|
+
export interface AfterReadContext {
|
|
30
|
+
source: ReadSource;
|
|
31
|
+
/** The relation's alias, on the two join sources; absent on `get` and `search`. */
|
|
32
|
+
alias?: string;
|
|
33
|
+
}
|
|
26
34
|
export interface ITable<F extends Record<string, TSchema> = Record<string, TSchema>> {
|
|
27
35
|
primary: (string & keyof F) | (string & keyof F)[];
|
|
28
36
|
Schema: SchemaDefinition<F>;
|
|
@@ -41,16 +49,37 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
41
49
|
allowedReadJoins?: JoinDefinition[];
|
|
42
50
|
allowedWriteJoins?: JoinDefinition[];
|
|
43
51
|
upsertMap?: Map<SchemaDefinition, string[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Narrow a field's generated type where the DB could only describe it loosely: a column
|
|
54
|
+
* Postgres calls `text` is an email, a UUID, a URL; an `integer` has a range. The generated
|
|
55
|
+
* schema is as precise as `information_schema` allows, and this is where the rest goes.
|
|
56
|
+
*
|
|
57
|
+
* **Write bodies only.** An override is a rule about what this API accepts from now on, and
|
|
58
|
+
* the rows already in the table were written before it existed: a column narrowed to
|
|
59
|
+
* `format: 'email'` today holds whatever was accepted last year. Narrowing the response
|
|
60
|
+
* would publish a promise about data nobody can retroactively make true, so responses and
|
|
61
|
+
* the `filters` map stay on the generated type.
|
|
62
|
+
*
|
|
63
|
+
* The override is taken **verbatim**, because on the request side the schema is the rule: no
|
|
64
|
+
* `Type.Optional` means the field is mandatory and no `Nullable` means an explicit `null` is
|
|
65
|
+
* rejected, whatever the column allows. That is how a column the database had to leave
|
|
66
|
+
* nullable, because it was added to a populated table, is made mandatory going forward.
|
|
67
|
+
*
|
|
68
|
+
* It is not a re-typing mechanism. Nothing stops an override from turning a `number` into a
|
|
69
|
+
* `string`, but the value crossing the wire is still whatever the driver read from the
|
|
70
|
+
* column, so the declaration would simply be false. Transforming a value between storage and
|
|
71
|
+
* API is what `afterRead` (out) and `beforeInsert`/`beforeUpdate` (in) are for.
|
|
72
|
+
*/
|
|
44
73
|
schemaOverrides?: Partial<Record<string & keyof F, TSchema>>;
|
|
45
74
|
validate?: ValidatorFn<F>;
|
|
46
75
|
validateBulk?: BulkValidatorFn<F>;
|
|
47
|
-
beforeInsert?: (db: QueryClient, req:
|
|
76
|
+
beforeInsert?: (db: QueryClient, req: ApiRequest, record: {
|
|
48
77
|
[K in keyof F]?: Static<F[K]> | Expression | null;
|
|
49
78
|
}) => Promise<void>;
|
|
50
|
-
beforeUpdate?: (db: QueryClient, req:
|
|
79
|
+
beforeUpdate?: (db: QueryClient, req: ApiRequest, fields: {
|
|
51
80
|
[K in keyof F]?: Static<F[K]> | Expression | null;
|
|
52
81
|
}, secondaryFieldsFetcher?: unknown) => void | Promise<void>;
|
|
53
|
-
afterInsert?: (db: QueryClient, req:
|
|
82
|
+
afterInsert?: (db: QueryClient, req: ApiRequest, record: {
|
|
54
83
|
[K in keyof F]?: Static<F[K]>;
|
|
55
84
|
}, secondaryRecords?: unknown) => Promise<void>;
|
|
56
85
|
/**
|
|
@@ -59,9 +88,34 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
59
88
|
* adapter supports transactions). Receives the camelCase input record (including the
|
|
60
89
|
* PK) plus the secondaries/deletions results, mirroring `afterInsert`.
|
|
61
90
|
*/
|
|
62
|
-
afterUpdate?: (db: QueryClient, req:
|
|
91
|
+
afterUpdate?: (db: QueryClient, req: ApiRequest, record: {
|
|
63
92
|
[K in keyof F]?: Static<F[K]> | Expression | null;
|
|
64
93
|
}, secondaryRecords?: unknown, deletionRecords?: unknown) => void | Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Runs on the rows a read returned, once per result set, before they become the response.
|
|
96
|
+
* The read-side counterpart of `beforeInsert`/`beforeUpdate`: the place to turn a stored
|
|
97
|
+
* representation back into the API one (decrypting a column, unpacking a blob).
|
|
98
|
+
*
|
|
99
|
+
* `rows` are camelCase records and are mutated **in place** — the return value is ignored.
|
|
100
|
+
* Rows cannot be added or removed: the pagination `COUNT` has already run, so a hook that
|
|
101
|
+
* dropped rows would report a `total` that does not match what it returned. Narrowing what
|
|
102
|
+
* a caller sees is `filters`, `tenantScope` and `readExclude`; hiding a field is
|
|
103
|
+
* `readExclude`.
|
|
104
|
+
*
|
|
105
|
+
* Declared on the table that OWNS the column, and it follows a join: the hook runs whenever
|
|
106
|
+
* that table's rows surface, including through a `joinMultiple` or `joinLeft` declared on
|
|
107
|
+
* another table (`ctx.source` and `ctx.alias` say which). It does not run on `joinGroup`,
|
|
108
|
+
* whose rows are aggregates rather than table rows.
|
|
109
|
+
*
|
|
110
|
+
* Not called when the read returned no rows. Whatever the hook writes is serialized against
|
|
111
|
+
* the response schema, so a transform that changes a field's JSON type needs a matching
|
|
112
|
+
* `schemaOverrides` entry — without it `fast-json-stringify` coerces the value silently.
|
|
113
|
+
*
|
|
114
|
+
* `req` is the request that triggered the read. Same caveat as `beforeDelete`:
|
|
115
|
+
* present through the auto-generated HTTP routes, `undefined` when a programmatic caller
|
|
116
|
+
* invokes `sqlApi.search()`/`sqlApi.get()` without passing one.
|
|
117
|
+
*/
|
|
118
|
+
afterRead?: (db: QueryClient, req: ApiRequest | undefined, rows: Record<string, unknown>[], ctx: AfterReadContext) => void | Promise<void>;
|
|
65
119
|
/**
|
|
66
120
|
* Runs before a single record is deleted (DELETE /rest/:id). Throw to abort the
|
|
67
121
|
* deletion (the thrown error's `statusCode`/`message` are surfaced to the client).
|
|
@@ -70,17 +124,17 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
70
124
|
* For tenant-scoped tables the hook only runs once ownership has been verified, so it
|
|
71
125
|
* never fires for rows the caller cannot access.
|
|
72
126
|
*
|
|
73
|
-
* `req` is the
|
|
127
|
+
* `req` is the request that triggered the operation. It is always present when
|
|
74
128
|
* the delete comes through the auto-generated HTTP route. It is `undefined` only if you
|
|
75
129
|
* call `sqlApi.delete(table, id)` programmatically without passing a request — in that
|
|
76
130
|
* case any hook that reads request context (e.g. `req.user`) must guard for it.
|
|
77
131
|
*/
|
|
78
|
-
beforeDelete?: (db: QueryClient, req:
|
|
132
|
+
beforeDelete?: (db: QueryClient, req: ApiRequest, id: string | number) => void | Promise<void>;
|
|
79
133
|
/**
|
|
80
134
|
* Runs after a single record has been deleted (DELETE /rest/:id). Not called when the
|
|
81
135
|
* record was not found (404). Same `req` caveat as `beforeDelete`.
|
|
82
136
|
*/
|
|
83
|
-
afterDelete?: (db: QueryClient, req:
|
|
137
|
+
afterDelete?: (db: QueryClient, req: ApiRequest, id: string | number) => void | Promise<void>;
|
|
84
138
|
/**
|
|
85
139
|
* Bulk counterpart of `beforeDelete`, invoked ONCE with all ids before a bulk delete
|
|
86
140
|
* (POST /bulk/:table/delete). Called once — not per id — to preserve the single-query
|
|
@@ -90,13 +144,13 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
90
144
|
* Same `req` caveat as `beforeDelete`: present via the HTTP route, `undefined` only when
|
|
91
145
|
* `sqlApi.bulkDelete(table, ids)` is called programmatically without a request.
|
|
92
146
|
*/
|
|
93
|
-
beforeBulkDelete?: (db: QueryClient, req:
|
|
147
|
+
beforeBulkDelete?: (db: QueryClient, req: ApiRequest, ids: (string | number)[]) => void | Promise<void>;
|
|
94
148
|
/**
|
|
95
149
|
* Bulk counterpart of `afterDelete`, invoked ONCE after a bulk delete with the ids that
|
|
96
150
|
* were ACTUALLY deleted (which may be a subset of the requested ids). Not called when
|
|
97
151
|
* nothing was deleted. Same `req` caveat as `beforeBulkDelete`.
|
|
98
152
|
*/
|
|
99
|
-
afterBulkDelete?: (db: QueryClient, req:
|
|
153
|
+
afterBulkDelete?: (db: QueryClient, req: ApiRequest, deletedIds: (string | number)[]) => void | Promise<void>;
|
|
100
154
|
/**
|
|
101
155
|
* Whitelist of auto-generated HTTP routes for this table. When omitted, ALL operations
|
|
102
156
|
* are exposed (search, get, insert, update, delete, bulkUpsert, bulkDelete) — the
|
|
@@ -120,10 +174,29 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
120
174
|
* password hash). Primary-key fields cannot be excluded.
|
|
121
175
|
*/
|
|
122
176
|
readExclude?: (string & keyof F)[];
|
|
177
|
+
/**
|
|
178
|
+
* Fields that no write may carry: removed from the insert, update and bulk-upsert bodies
|
|
179
|
+
* (so sending one is a 400, not a driver error) and dropped again in the engines, which
|
|
180
|
+
* `sqlApi.*` reaches without the HTTP schemas. Reads are untouched — the field stays
|
|
181
|
+
* projected, filterable and orderable.
|
|
182
|
+
*
|
|
183
|
+
* It is the write-side counterpart of `readExclude`, and it exists for columns the database
|
|
184
|
+
* refuses to be told about: a `GENERATED ALWAYS AS (<expr>)` column is already excluded
|
|
185
|
+
* automatically through the Schema's `generatedFields`, and this key covers the rest — a
|
|
186
|
+
* column a trigger owns, one a migration is about to drop.
|
|
187
|
+
*
|
|
188
|
+
* Not a per-caller permission: it is static and applies to everyone, admins included.
|
|
189
|
+
* Field-level rules that depend on who is asking belong in `beforeUpdate` (silent strip) or
|
|
190
|
+
* `validate` (loud 400). Not `excludeFromCreation` either — that one whitelists client input
|
|
191
|
+
* on insert only, and deliberately lets a hook assign the field afterwards.
|
|
192
|
+
*
|
|
193
|
+
* The primary key cannot be excluded: the update body identifies the row with it.
|
|
194
|
+
*/
|
|
195
|
+
writeExclude?: (string & keyof F)[];
|
|
123
196
|
defaultOrder?: string;
|
|
124
197
|
excludeFromCreation?: (string & keyof F)[];
|
|
125
198
|
distinctResults?: boolean;
|
|
126
|
-
onRequests?: ((request:
|
|
199
|
+
onRequests?: ((request: ApiRequest, reply: ApiReply) => Promise<void | ApiReply>)[];
|
|
127
200
|
tenantScope?: TenantScope;
|
|
128
201
|
}
|
|
129
202
|
export type DbTables = Record<string, ITable<any>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,WAAW,IAAI,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAChI,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/types/table.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ApiRequest } from './request.js';
|
|
3
3
|
import type { QueryClient } from '../lib/db.js';
|
|
4
4
|
/**
|
|
5
5
|
* Validation error tuple: [field, code] or [field, code, message].
|
|
@@ -8,7 +8,7 @@ import type { QueryClient } from '../lib/db.js';
|
|
|
8
8
|
* - message: human-readable description (defaults to code if omitted)
|
|
9
9
|
*/
|
|
10
10
|
export type ValidationError = [field: string, code: string] | [field: string, code: string, message: string];
|
|
11
|
-
export type ValidatorFn<F extends Record<string, TSchema> = Record<string, TSchema>> = (db: QueryClient, req:
|
|
11
|
+
export type ValidatorFn<F extends Record<string, TSchema> = Record<string, TSchema>> = (db: QueryClient, req: ApiRequest, main: {
|
|
12
12
|
[K in keyof F]?: Static<F[K]>;
|
|
13
13
|
}, secondaries?: Record<string, Record<string, unknown>[]>) => Promise<ValidationError[]> | ValidationError[];
|
|
14
14
|
export interface BulkValidatorItem<F extends Record<string, TSchema> = Record<string, TSchema>> {
|
|
@@ -17,5 +17,5 @@ export interface BulkValidatorItem<F extends Record<string, TSchema> = Record<st
|
|
|
17
17
|
};
|
|
18
18
|
secondaries?: Record<string, Record<string, unknown>[]>;
|
|
19
19
|
}
|
|
20
|
-
export type BulkValidatorFn<F extends Record<string, TSchema> = Record<string, TSchema>> = (db: QueryClient, req:
|
|
20
|
+
export type BulkValidatorFn<F extends Record<string, TSchema> = Record<string, TSchema>> = (db: QueryClient, req: ApiRequest, items: BulkValidatorItem<F>[]) => Promise<ValidationError[]> | ValidationError[];
|
|
21
21
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/types/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/types/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAC7B,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAEnD,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACrF,EAAE,EAAE,WAAW,EACf,GAAG,EAAE,UAAU,EACf,IAAI,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,EACvC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,KACpD,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAC;AAEpD,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5F,IAAI,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CACzD;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACzF,EAAE,EAAE,WAAW,EACf,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,KAC1B,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAC"}
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC"}
|
package/dist/types.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# 0014. A secondary runs the child table's transform, not its verdicts
|
|
2
|
+
|
|
3
|
+
- **Status**: accepted
|
|
4
|
+
- **Date**: 2026-08-23
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
[ADR 0010](./0010-joins-do-not-run-route-guards.md) drew the line for reads: route-level
|
|
9
|
+
controls (`onRequests`, `operations`) do not follow a join, configuration-level ones
|
|
10
|
+
(`readExclude`, `tenantScope`, the relation's schema) do. The write side never had that line
|
|
11
|
+
drawn, and it had drifted into an inconsistent middle.
|
|
12
|
+
|
|
13
|
+
`processSecondaries` already consulted the child's configuration for two things — it stripped
|
|
14
|
+
the child's `excludeFromCreation` and enforced the child's `tenantScope` — while its
|
|
15
|
+
`beforeInsert` was silently skipped. The gap was invisible until a table started using that
|
|
16
|
+
hook to change a value's *representation* rather than to add a value: a column encrypted on
|
|
17
|
+
the way in was stored as ciphertext through `POST /rest/patient`, and as plaintext through
|
|
18
|
+
`POST /rest/visit` with the same row sent under `secondaries.patient`. Nothing failed; the
|
|
19
|
+
data was simply wrong in a way only a later read would show, and `afterRead` would then
|
|
20
|
+
"decrypt" a value that was never encrypted.
|
|
21
|
+
|
|
22
|
+
The reverse extension is not obviously right either. `validate` and `afterInsert` are also
|
|
23
|
+
per-table configuration, and running them for children would be the symmetric move — but they
|
|
24
|
+
do not transform a record, they *decide* about it: `validate` rejects the request, and
|
|
25
|
+
`afterInsert` fires side effects. Both would change when and how a host's write fails, for
|
|
26
|
+
consumers that never opted in.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
**A secondary is a write like any other for everything that shapes the record, and not for
|
|
31
|
+
anything that renders a verdict on it.** The child's configuration is applied in this order,
|
|
32
|
+
mirroring the single-insert pipeline of [ADR 0005](./0005-insert-pipeline-sanitize-before-hooks.md):
|
|
33
|
+
|
|
34
|
+
1. the child's `excludeFromCreation` strip (client-input whitelist);
|
|
35
|
+
2. the child's `beforeInsert` (may mutate, including on excluded fields);
|
|
36
|
+
3. conversion to DB column format;
|
|
37
|
+
4. the engine's FK auto-fill, which is authoritative over both the payload and the hook;
|
|
38
|
+
5. the child's `tenantScope` enforcement.
|
|
39
|
+
|
|
40
|
+
The child's `validate`, `validateBulk`, `afterInsert`, `beforeDelete` and `afterDelete` do not
|
|
41
|
+
run. `processDeletions` is unchanged: it removes rows, it does not shape them.
|
|
42
|
+
|
|
43
|
+
| Child-table config | Applies to a secondary |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `excludeFromCreation` | yes |
|
|
46
|
+
| `beforeInsert` | yes |
|
|
47
|
+
| `tenantScope` | yes |
|
|
48
|
+
| `upsertMap` | yes (via the host's `upsertMap`) |
|
|
49
|
+
| `validate` / `validateBulk` | no |
|
|
50
|
+
| `afterInsert` | no |
|
|
51
|
+
| `onRequests` / `operations` | no (ADR 0010) |
|
|
52
|
+
|
|
53
|
+
The host's own `validate` receives the secondaries and is the place to reject them.
|
|
54
|
+
|
|
55
|
+
## Alternatives considered
|
|
56
|
+
|
|
57
|
+
- **Leave `beforeInsert` skipped and document it** — rejected: the failure is silent and
|
|
58
|
+
writes bad data. A hook that assigns `createdBy` merely produced a null column when skipped;
|
|
59
|
+
a hook that encodes a value produced a row that reads back as garbage, and no error marks
|
|
60
|
+
the moment it happened.
|
|
61
|
+
- **Run the child's full pipeline (`validate` + `afterInsert`)** — rejected for now, not on
|
|
62
|
+
principle. `validate` would make a host's insert start failing on child rows that were
|
|
63
|
+
accepted the day before, and `afterInsert` would fire N side effects inside the host's
|
|
64
|
+
transaction with no ordering contract against the host's own `afterInsert`. Both are
|
|
65
|
+
behaviour changes that need their own decision; the host's `validate` already receives the
|
|
66
|
+
secondaries.
|
|
67
|
+
- **A separate `beforeSecondaryInsert` hook on the host** — rejected: a second name for
|
|
68
|
+
"prepare a row of this table", declared on the wrong table. The transform belongs to the
|
|
69
|
+
table that owns the column, which is the same rule `afterRead` follows on the read side.
|
|
70
|
+
- **Run the child hook before the exclusion strip** — rejected: it inverts ADR 0005 and lets
|
|
71
|
+
a client value survive on an excluded field.
|
|
72
|
+
|
|
73
|
+
## Consequences
|
|
74
|
+
|
|
75
|
+
- Behaviour change (CHANGELOG + BREAKING_CHANGES): a child table's `beforeInsert` now runs for
|
|
76
|
+
rows written as secondaries, where it was silently skipped. A hook that was written assuming
|
|
77
|
+
it only ever saw the table's own route — one that reads `req.params`, or increments a counter
|
|
78
|
+
per call — now runs in a context it did not expect.
|
|
79
|
+
- The hook receives the **transaction** connection, not the pool: what it writes rolls back
|
|
80
|
+
with the host's write.
|
|
81
|
+
- `beforeInsert` is now the one place where "how this table's rows are shaped on the way in"
|
|
82
|
+
is written, on both entry paths — the dual of `afterRead` on the way out.
|
|
83
|
+
- The asymmetry between transform and verdict is now a stated boundary, so the next reader
|
|
84
|
+
finds a decision instead of what looks like the same oversight in a different hook.
|