fastify-auto-sqlapi 0.2.1 → 0.2.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.md +50 -0
- package/AGENTS_BACKEND.md +265 -7
- package/AGENTS_FRONTEND.md +3 -3
- package/BREAKING_CHANGES.md +156 -0
- package/CHANGELOG.md +265 -19
- package/README.md +260 -983
- package/dist/bin/generate-schema.js +49 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/agent/manifest.d.ts +4 -1
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +15 -3
- package/dist/lib/cli/mysql-introspect.d.ts +28 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +49 -16
- 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/tenant.js +7 -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 +24 -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/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,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/tenant.js
CHANGED
|
@@ -283,10 +283,16 @@ export async function assertTenantOwnsConflicts(db, tenant, tableName, conflictC
|
|
|
283
283
|
};
|
|
284
284
|
let mismatchSql;
|
|
285
285
|
if (tenantCols.length === 1) {
|
|
286
|
-
|
|
286
|
+
const col = tenantCols[0];
|
|
287
|
+
const notIn = `${col} NOT IN (${tenant.ids.map((id) => {
|
|
287
288
|
values.push(id);
|
|
288
289
|
return db.ph(values.length);
|
|
289
290
|
}).join(', ')})`;
|
|
291
|
+
// A NULL owner makes `col NOT IN (...)` return NULL, not TRUE, so an unowned row would slip
|
|
292
|
+
// past the probe and be claimable/overwritten by the upsert. Treat NULL as "not mine" — the
|
|
293
|
+
// same way the multi-column branch (NOT COALESCE(..., FALSE)) and the read side already do —
|
|
294
|
+
// so a NULL-owner conflict row is flagged as foreign.
|
|
295
|
+
mismatchSql = `(${col} IS NULL OR ${notIn})`;
|
|
290
296
|
}
|
|
291
297
|
else {
|
|
292
298
|
// A NULL party makes `IN` return NULL, and `NOT NULL` is not TRUE — a plain negation would
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
2
|
+
import type { ITable, SchemaDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Write visibility — the counterpart of `read-access.ts`.
|
|
5
|
+
*
|
|
6
|
+
* A field is excluded from writes for one of two reasons:
|
|
7
|
+
*
|
|
8
|
+
* - the database computes it (`GENERATED ALWAYS AS (<expr>)`), which the generated Schema
|
|
9
|
+
* publishes in `generatedFields`. Naming such a column in an INSERT or an UPDATE is an
|
|
10
|
+
* error on both engines, whoever put the value there, so this exclusion is a fact about
|
|
11
|
+
* the column rather than a decision about the caller;
|
|
12
|
+
* - the table config lists it in `writeExclude`, for a column the deployment keeps read-only
|
|
13
|
+
* for reasons of its own.
|
|
14
|
+
*
|
|
15
|
+
* Excluded fields are removed from the write body schemas — so an HTTP caller gets a 400 from
|
|
16
|
+
* `additionalProperties: false` rather than a driver error — and dropped again in the engines,
|
|
17
|
+
* which `sqlApi.*` reaches without passing through those schemas.
|
|
18
|
+
*
|
|
19
|
+
* This is not `excludeFromCreation`, which whitelists CLIENT input on insert only and lets a
|
|
20
|
+
* hook assign the field afterwards (ADR 0005); nor is it the place for per-caller field rules,
|
|
21
|
+
* which stay in `beforeUpdate` / `validate` (ADR 0011).
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeExcludedFields(tableConf: ITable | undefined, schema: SchemaDefinition | undefined): Set<string>;
|
|
24
|
+
/**
|
|
25
|
+
* A field map minus everything that may not be written. Used to build the write body schemas,
|
|
26
|
+
* where the removal is what turns a rejected column into a 400 instead of a 500.
|
|
27
|
+
*/
|
|
28
|
+
export declare function writableFields(fields: Record<string, TSchema>, tableConf: ITable | undefined, schema: SchemaDefinition | undefined): Record<string, TSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* Drop the non-writable fields from a camelCase record, in place.
|
|
31
|
+
*
|
|
32
|
+
* Call this AFTER the write hooks, unlike `excludeFromCreation`: a hook cannot be allowed to
|
|
33
|
+
* put back a value the database will refuse, so the last word has to belong to the exclusion.
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeWriteExcluded(record: Record<string, unknown>, tableConf: ITable | undefined, schema: SchemaDefinition | undefined): void;
|
|
36
|
+
//# sourceMappingURL=write-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-access.d.ts","sourceRoot":"","sources":["../../src/lib/write-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GAAG,SAAS,GACnC,GAAG,CAAC,MAAM,CAAC,CAIb;AAUD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GAAG,SAAS,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,gBAAgB,GAAG,SAAS,GACnC,IAAI,CAKN"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write visibility — the counterpart of `read-access.ts`.
|
|
3
|
+
*
|
|
4
|
+
* A field is excluded from writes for one of two reasons:
|
|
5
|
+
*
|
|
6
|
+
* - the database computes it (`GENERATED ALWAYS AS (<expr>)`), which the generated Schema
|
|
7
|
+
* publishes in `generatedFields`. Naming such a column in an INSERT or an UPDATE is an
|
|
8
|
+
* error on both engines, whoever put the value there, so this exclusion is a fact about
|
|
9
|
+
* the column rather than a decision about the caller;
|
|
10
|
+
* - the table config lists it in `writeExclude`, for a column the deployment keeps read-only
|
|
11
|
+
* for reasons of its own.
|
|
12
|
+
*
|
|
13
|
+
* Excluded fields are removed from the write body schemas — so an HTTP caller gets a 400 from
|
|
14
|
+
* `additionalProperties: false` rather than a driver error — and dropped again in the engines,
|
|
15
|
+
* which `sqlApi.*` reaches without passing through those schemas.
|
|
16
|
+
*
|
|
17
|
+
* This is not `excludeFromCreation`, which whitelists CLIENT input on insert only and lets a
|
|
18
|
+
* hook assign the field afterwards (ADR 0005); nor is it the place for per-caller field rules,
|
|
19
|
+
* which stay in `beforeUpdate` / `validate` (ADR 0011).
|
|
20
|
+
*/
|
|
21
|
+
export function writeExcludedFields(tableConf, schema) {
|
|
22
|
+
const excluded = new Set(schema?.generatedFields ?? []);
|
|
23
|
+
for (const field of tableConf?.writeExclude ?? [])
|
|
24
|
+
excluded.add(field);
|
|
25
|
+
return excluded;
|
|
26
|
+
}
|
|
27
|
+
/** True when the table has nothing to exclude — lets callers keep their existing object. */
|
|
28
|
+
function hasWriteExclusions(tableConf, schema) {
|
|
29
|
+
return (schema?.generatedFields?.length ?? 0) > 0 || (tableConf?.writeExclude?.length ?? 0) > 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A field map minus everything that may not be written. Used to build the write body schemas,
|
|
33
|
+
* where the removal is what turns a rejected column into a 400 instead of a 500.
|
|
34
|
+
*/
|
|
35
|
+
export function writableFields(fields, tableConf, schema) {
|
|
36
|
+
if (!hasWriteExclusions(tableConf, schema))
|
|
37
|
+
return fields;
|
|
38
|
+
const excluded = writeExcludedFields(tableConf, schema);
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
41
|
+
if (!excluded.has(name))
|
|
42
|
+
result[name] = value;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Drop the non-writable fields from a camelCase record, in place.
|
|
48
|
+
*
|
|
49
|
+
* Call this AFTER the write hooks, unlike `excludeFromCreation`: a hook cannot be allowed to
|
|
50
|
+
* put back a value the database will refuse, so the last word has to belong to the exclusion.
|
|
51
|
+
*/
|
|
52
|
+
export function removeWriteExcluded(record, tableConf, schema) {
|
|
53
|
+
if (!hasWriteExclusions(tableConf, schema))
|
|
54
|
+
return;
|
|
55
|
+
for (const field of writeExcludedFields(tableConf, schema)) {
|
|
56
|
+
delete record[field];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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;
|
|
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;AAI/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,CAkBf"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { GetTableResponse } from '../../lib/schema/get.js';
|
|
2
3
|
import { registerForAllTables } from './route-helpers.js';
|
|
3
4
|
export default async function getRoutes(fastify, options) {
|
|
4
5
|
await registerForAllTables(fastify, options, {
|
|
@@ -7,9 +8,9 @@ export default async function getRoutes(fastify, options) {
|
|
|
7
8
|
method: 'GET',
|
|
8
9
|
url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
|
|
9
10
|
successStatus: 200,
|
|
10
|
-
schemas: (
|
|
11
|
+
schemas: (db, table) => ({
|
|
11
12
|
params: Type.Object({ id: Type.String() }),
|
|
12
|
-
response:
|
|
13
|
+
response: GetTableResponse(db, table),
|
|
13
14
|
}),
|
|
14
15
|
summary: 'Get',
|
|
15
16
|
description: (name) => `Get a record from ${name} by primary key`,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fp from 'fastify-plugin';
|
|
2
2
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
3
3
|
import { getDialect } from '../../lib/dialect.js';
|
|
4
|
-
import { ensureSqlApiDecorator } from '
|
|
5
|
-
import { setupSwagger } from '
|
|
4
|
+
import { ensureSqlApiDecorator } from '../sql-api-decorator.js';
|
|
5
|
+
import { setupSwagger } from '../setup-swagger.js';
|
|
6
6
|
import searchRoutes from './search.routes.js';
|
|
7
7
|
import getRoutes from './get.routes.js';
|
|
8
8
|
import insertRoutes from './insert.routes.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureSqlApiDecorator } from '
|
|
1
|
+
import { ensureSqlApiDecorator } from '../sql-api-decorator.js';
|
|
2
2
|
import { sanitizeRouteError } from '../../lib/errors.js';
|
|
3
3
|
import { isCompositePrimary } from '../../types.js';
|
|
4
4
|
function mergeOnRequests(options, tableConf) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-swagger.d.ts","sourceRoot":"","sources":["../../src/routes/setup-swagger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,aAAa,CAAC;AAEvE,wBAAsB,YAAY,CAChC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC,CA6Bf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-api-decorator.d.ts","sourceRoot":"","sources":["../../src/routes/sql-api-decorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA2BN"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createQueryClient } from '
|
|
2
|
-
import { pgQueryable } from '
|
|
3
|
-
import { mysqlQueryable } from '
|
|
4
|
-
import { createSqlApi } from '
|
|
1
|
+
import { createQueryClient } from '../lib/db.js';
|
|
2
|
+
import { pgQueryable } from '../lib/adapters/pg-adapter.js';
|
|
3
|
+
import { mysqlQueryable } from '../lib/adapters/mysql-adapter.js';
|
|
4
|
+
import { createSqlApi } from '../lib/sql-api.js';
|
|
5
5
|
/**
|
|
6
6
|
* Decorates `fastify.sqlApi` (lazy getter) on the given instance unless the
|
|
7
7
|
* decorator is already present in scope. The main plugin calls this at the
|
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":"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"}
|
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,32 @@ 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
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the caller's tenant id(s) for this request. Returning one id (or an array, for a
|
|
19
|
+
* caller who belongs to several tenants) scopes every read and write to rows that tenant owns.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **Returning `null`/`undefined` means ADMIN — no tenant filtering at all, i.e. full
|
|
22
|
+
* cross-tenant access.** "Unauthenticated" and "admin" therefore collapse to the same value.
|
|
23
|
+
* The tenant identity is read from the request (typically `request.user`, populated by your own
|
|
24
|
+
* auth hook in {@link onRequests}), which this plugin does not set. If that hook is missing,
|
|
25
|
+
* mis-ordered, or fails, `request.user` is `undefined` and the common `?? null` pattern yields
|
|
26
|
+
* `null` → the request is served as admin. Nothing here fails closed.
|
|
27
|
+
*
|
|
28
|
+
* To fail closed, make this callback (or the auth hook) **reject when there is no authenticated
|
|
29
|
+
* principal** instead of returning `null`, and reserve `null` for a real, authorized superadmin:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* getTenantId: (req) => {
|
|
33
|
+
* if (!req.user) throw app.httpErrors.unauthorized(); // no principal → deny, never admin
|
|
34
|
+
* return req.user.isSuperadmin ? null : req.user.organizationId;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
getTenantId?: (request: ApiRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
|
|
18
39
|
debug?: boolean;
|
|
19
40
|
/**
|
|
20
41
|
* 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;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,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>>;
|