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.
Files changed (108) hide show
  1. package/AGENTS.md +50 -0
  2. package/AGENTS_BACKEND.md +265 -7
  3. package/AGENTS_FRONTEND.md +3 -3
  4. package/BREAKING_CHANGES.md +156 -0
  5. package/CHANGELOG.md +265 -19
  6. package/README.md +260 -983
  7. package/dist/bin/generate-schema.js +49 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/lib/agent/manifest.d.ts +4 -1
  12. package/dist/lib/agent/manifest.d.ts.map +1 -1
  13. package/dist/lib/agent/manifest.js +15 -3
  14. package/dist/lib/cli/mysql-introspect.d.ts +28 -0
  15. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/mysql-introspect.js +49 -16
  17. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  18. package/dist/lib/cli/pg-introspect.js +53 -14
  19. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  20. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/schema-codegen.js +17 -4
  22. package/dist/lib/cli/tables-codegen.d.ts +4 -0
  23. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  24. package/dist/lib/cli/tables-codegen.js +125 -27
  25. package/dist/lib/engine/bulk/bulk-upsert.js +2 -2
  26. package/dist/lib/engine/read-hooks.d.ts +19 -0
  27. package/dist/lib/engine/read-hooks.d.ts.map +1 -0
  28. package/dist/lib/engine/read-hooks.js +20 -0
  29. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  30. package/dist/lib/engine/rest/get.js +5 -2
  31. package/dist/lib/engine/rest/insert.js +1 -1
  32. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/update.js +14 -10
  34. package/dist/lib/engine/search/join-families.d.ts +11 -2
  35. package/dist/lib/engine/search/join-families.d.ts.map +1 -1
  36. package/dist/lib/engine/search/join-families.js +11 -13
  37. package/dist/lib/engine/search/search.d.ts.map +1 -1
  38. package/dist/lib/engine/search/search.js +9 -3
  39. package/dist/lib/engine/validate.d.ts +3 -3
  40. package/dist/lib/engine/validate.d.ts.map +1 -1
  41. package/dist/lib/engine/write-helpers.d.ts +7 -3
  42. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  43. package/dist/lib/engine/write-helpers.js +27 -28
  44. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  45. package/dist/lib/schema/bulk-upsert.js +2 -1
  46. package/dist/lib/schema/get.d.ts +14 -0
  47. package/dist/lib/schema/get.d.ts.map +1 -0
  48. package/dist/lib/schema/get.js +18 -0
  49. package/dist/lib/schema/helpers.d.ts +21 -3
  50. package/dist/lib/schema/helpers.d.ts.map +1 -1
  51. package/dist/lib/schema/helpers.js +37 -5
  52. package/dist/lib/schema/insert.d.ts.map +1 -1
  53. package/dist/lib/schema/insert.js +7 -3
  54. package/dist/lib/schema/search.d.ts.map +1 -1
  55. package/dist/lib/schema/search.js +3 -11
  56. package/dist/lib/schema/update.d.ts.map +1 -1
  57. package/dist/lib/schema/update.js +7 -3
  58. package/dist/lib/sql-api.d.ts +9 -9
  59. package/dist/lib/sql-api.d.ts.map +1 -1
  60. package/dist/lib/sql-api.js +2 -1
  61. package/dist/lib/table-helpers.d.ts.map +1 -1
  62. package/dist/lib/table-helpers.js +123 -2
  63. package/dist/lib/tenant.d.ts +2 -2
  64. package/dist/lib/tenant.d.ts.map +1 -1
  65. package/dist/lib/tenant.js +7 -1
  66. package/dist/lib/write-access.d.ts +36 -0
  67. package/dist/lib/write-access.d.ts.map +1 -0
  68. package/dist/lib/write-access.js +58 -0
  69. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  70. package/dist/routes/auto/get.routes.js +3 -2
  71. package/dist/routes/auto/plugin.js +2 -2
  72. package/dist/routes/auto/route-helpers.js +1 -1
  73. package/dist/routes/setup-swagger.d.ts.map +1 -0
  74. package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
  75. package/dist/routes/sql-api-decorator.d.ts.map +1 -0
  76. package/dist/{lib → routes}/sql-api-decorator.js +4 -4
  77. package/dist/types/cli.d.ts +27 -1
  78. package/dist/types/cli.d.ts.map +1 -1
  79. package/dist/types/dml.d.ts +6 -4
  80. package/dist/types/dml.d.ts.map +1 -1
  81. package/dist/types/plugin.d.ts +24 -3
  82. package/dist/types/plugin.d.ts.map +1 -1
  83. package/dist/types/request.d.ts +24 -0
  84. package/dist/types/request.d.ts.map +1 -0
  85. package/dist/types/request.js +1 -0
  86. package/dist/types/schema.d.ts +15 -0
  87. package/dist/types/schema.d.ts.map +1 -1
  88. package/dist/types/search.d.ts +3 -0
  89. package/dist/types/search.d.ts.map +1 -1
  90. package/dist/types/table.d.ts +84 -11
  91. package/dist/types/table.d.ts.map +1 -1
  92. package/dist/types/validation.d.ts +3 -3
  93. package/dist/types/validation.d.ts.map +1 -1
  94. package/dist/types.d.ts +1 -0
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js +1 -0
  97. package/docs/adr/0004-updates-always-open.md +2 -1
  98. package/docs/adr/0014-what-follows-a-write-join.md +84 -0
  99. package/docs/adr/0015-non-writable-columns.md +86 -0
  100. package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
  101. package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
  102. package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
  103. package/docs/adr/README.md +5 -0
  104. package/package.json +1 -1
  105. package/dist/lib/setup-swagger.d.ts.map +0 -1
  106. package/dist/lib/sql-api-decorator.d.ts.map +0 -1
  107. /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
  108. /package/dist/{lib → routes}/setup-swagger.js +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
@@ -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
- mismatchSql = `${tenantCols[0]} NOT IN (${tenant.ids.map((id) => {
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;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
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: (_db, _table, tc) => ({
11
+ schemas: (db, table) => ({
11
12
  params: Type.Object({ id: Type.String() }),
12
- response: Type.Object({ main: Type.Partial(Type.Object(tc.Schema.fields)) }),
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 '../../lib/sql-api-decorator.js';
5
- import { setupSwagger } from '../../lib/setup-swagger.js';
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 '../../lib/sql-api-decorator.js';
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"}
@@ -1,5 +1,5 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
- import { type SqlApi } from './sql-api.js';
2
+ import { type SqlApi } from '../lib/sql-api.js';
3
3
  import type { SqlApiPluginOptions } from '../types.js';
4
4
  declare module 'fastify' {
5
5
  interface FastifyInstance {
@@ -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 './db.js';
2
- import { pgQueryable } from './adapters/pg-adapter.js';
3
- import { mysqlQueryable } from './adapters/mysql-adapter.js';
4
- import { createSqlApi } from './sql-api.js';
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
@@ -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
- /** True for auto-generated columns without a visible default (mysql AUTO_INCREMENT). */
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
@@ -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,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;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;KACnB,CAAC;CACH"}
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"}
@@ -1,4 +1,4 @@
1
- import type { FastifyRequest } from 'fastify';
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: FastifyRequest;
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?: FastifyRequest;
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?: FastifyRequest;
62
+ request?: ApiRequest;
61
63
  }
62
64
  export interface BulkDeleteResult {
63
65
  main: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"dml.d.ts","sourceRoot":"","sources":["../../src/types/dml.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dml.d.ts","sourceRoot":"","sources":["../../src/types/dml.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { FastifyRequest, FastifyReply } from 'fastify';
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: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>)[];
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: FastifyRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
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,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5D,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,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;IAChG,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,cAAc,KAAK,QAAQ,GAAG,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,GAC/E,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"}
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 {};
@@ -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;CACvB"}
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"}
@@ -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;CAClB;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"}
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"}
@@ -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 { FastifyRequest, FastifyReply } from 'fastify';
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: FastifyRequest, record: {
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: FastifyRequest, fields: {
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: FastifyRequest, record: {
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: FastifyRequest, record: {
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 Fastify request that triggered the operation. It is always present when
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: FastifyRequest, id: string | number) => void | Promise<void>;
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: FastifyRequest, id: string | number) => void | Promise<void>;
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: FastifyRequest, ids: (string | number)[]) => void | Promise<void>;
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: FastifyRequest, deletedIds: (string | number)[]) => void | Promise<void>;
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: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>)[];
199
+ onRequests?: ((request: ApiRequest, reply: ApiReply) => Promise<void | ApiReply>)[];
127
200
  tenantScope?: TenantScope;
128
201
  }
129
202
  export type DbTables = Record<string, ITable<any>>;