fastify-auto-sqlapi 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +50 -0
  2. package/AGENTS_BACKEND.md +264 -6
  3. package/AGENTS_FRONTEND.md +1 -1
  4. package/BREAKING_CHANGES.md +51 -0
  5. package/CHANGELOG.md +113 -0
  6. package/README.md +254 -983
  7. package/dist/bin/generate-schema.js +49 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/lib/agent/manifest.d.ts +2 -0
  12. package/dist/lib/agent/manifest.d.ts.map +1 -1
  13. package/dist/lib/agent/manifest.js +4 -0
  14. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/mysql-introspect.js +15 -5
  16. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  17. package/dist/lib/cli/pg-introspect.js +53 -14
  18. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  19. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  20. package/dist/lib/cli/schema-codegen.js +17 -4
  21. package/dist/lib/cli/tables-codegen.d.ts +4 -0
  22. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  23. package/dist/lib/cli/tables-codegen.js +125 -27
  24. package/dist/lib/engine/bulk/bulk-upsert.js +2 -2
  25. package/dist/lib/engine/read-hooks.d.ts +19 -0
  26. package/dist/lib/engine/read-hooks.d.ts.map +1 -0
  27. package/dist/lib/engine/read-hooks.js +20 -0
  28. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  29. package/dist/lib/engine/rest/get.js +5 -2
  30. package/dist/lib/engine/rest/insert.js +1 -1
  31. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  32. package/dist/lib/engine/rest/update.js +14 -10
  33. package/dist/lib/engine/search/join-families.d.ts +11 -2
  34. package/dist/lib/engine/search/join-families.d.ts.map +1 -1
  35. package/dist/lib/engine/search/join-families.js +11 -13
  36. package/dist/lib/engine/search/search.d.ts.map +1 -1
  37. package/dist/lib/engine/search/search.js +9 -3
  38. package/dist/lib/engine/validate.d.ts +3 -3
  39. package/dist/lib/engine/validate.d.ts.map +1 -1
  40. package/dist/lib/engine/write-helpers.d.ts +7 -3
  41. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  42. package/dist/lib/engine/write-helpers.js +27 -28
  43. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  44. package/dist/lib/schema/bulk-upsert.js +2 -1
  45. package/dist/lib/schema/get.d.ts +14 -0
  46. package/dist/lib/schema/get.d.ts.map +1 -0
  47. package/dist/lib/schema/get.js +18 -0
  48. package/dist/lib/schema/helpers.d.ts +21 -3
  49. package/dist/lib/schema/helpers.d.ts.map +1 -1
  50. package/dist/lib/schema/helpers.js +37 -5
  51. package/dist/lib/schema/insert.d.ts.map +1 -1
  52. package/dist/lib/schema/insert.js +7 -3
  53. package/dist/lib/schema/search.d.ts.map +1 -1
  54. package/dist/lib/schema/search.js +3 -11
  55. package/dist/lib/schema/update.d.ts.map +1 -1
  56. package/dist/lib/schema/update.js +7 -3
  57. package/dist/lib/sql-api.d.ts +9 -9
  58. package/dist/lib/sql-api.d.ts.map +1 -1
  59. package/dist/lib/sql-api.js +2 -1
  60. package/dist/lib/table-helpers.d.ts.map +1 -1
  61. package/dist/lib/table-helpers.js +123 -2
  62. package/dist/lib/tenant.d.ts +2 -2
  63. package/dist/lib/tenant.d.ts.map +1 -1
  64. package/dist/lib/write-access.d.ts +36 -0
  65. package/dist/lib/write-access.d.ts.map +1 -0
  66. package/dist/lib/write-access.js +58 -0
  67. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  68. package/dist/routes/auto/get.routes.js +3 -2
  69. package/dist/routes/auto/plugin.js +2 -2
  70. package/dist/routes/auto/route-helpers.js +1 -1
  71. package/dist/routes/setup-swagger.d.ts.map +1 -0
  72. package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
  73. package/dist/routes/sql-api-decorator.d.ts.map +1 -0
  74. package/dist/{lib → routes}/sql-api-decorator.js +4 -4
  75. package/dist/types/cli.d.ts +27 -1
  76. package/dist/types/cli.d.ts.map +1 -1
  77. package/dist/types/dml.d.ts +6 -4
  78. package/dist/types/dml.d.ts.map +1 -1
  79. package/dist/types/plugin.d.ts +3 -3
  80. package/dist/types/plugin.d.ts.map +1 -1
  81. package/dist/types/request.d.ts +24 -0
  82. package/dist/types/request.d.ts.map +1 -0
  83. package/dist/types/request.js +1 -0
  84. package/dist/types/schema.d.ts +15 -0
  85. package/dist/types/schema.d.ts.map +1 -1
  86. package/dist/types/search.d.ts +3 -0
  87. package/dist/types/search.d.ts.map +1 -1
  88. package/dist/types/table.d.ts +84 -11
  89. package/dist/types/table.d.ts.map +1 -1
  90. package/dist/types/validation.d.ts +3 -3
  91. package/dist/types/validation.d.ts.map +1 -1
  92. package/dist/types.d.ts +1 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js +1 -0
  95. package/docs/adr/0004-updates-always-open.md +2 -1
  96. package/docs/adr/0014-what-follows-a-write-join.md +84 -0
  97. package/docs/adr/0015-non-writable-columns.md +86 -0
  98. package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
  99. package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
  100. package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
  101. package/docs/adr/README.md +5 -0
  102. package/package.json +1 -1
  103. package/dist/lib/setup-swagger.d.ts.map +0 -1
  104. package/dist/lib/sql-api-decorator.d.ts.map +0 -1
  105. /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
  106. /package/dist/{lib → routes}/setup-swagger.js +0 -0
@@ -1,4 +1,6 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type, OptionalKind } from '@sinclair/typebox';
2
+ import { readableFieldNames } from '../read-access.js';
3
+ import { writableFields } from '../write-access.js';
2
4
  import { findSecondaryTableConf } from '../engine/write-helpers.js';
3
5
  /**
4
6
  * Build a PK-only schema for response shapes (single or composite PK).
@@ -19,7 +21,7 @@ export function pkSchema(tableConf, schema, fallback) {
19
21
  * - excludeFromCreation fields become Optional
20
22
  */
21
23
  function buildSecondaryFields(joinSchema, joinField, secondaryTableConf) {
22
- const fields = { ...joinSchema.fields };
24
+ const fields = writableFields({ ...joinSchema.fields }, secondaryTableConf, joinSchema);
23
25
  if (joinField in fields) {
24
26
  fields[joinField] = Type.Optional(fields[joinField]);
25
27
  }
@@ -32,12 +34,32 @@ function buildSecondaryFields(joinSchema, joinField, secondaryTableConf) {
32
34
  }
33
35
  return fields;
34
36
  }
37
+ /** The same schema without the Optional modifier, i.e. as a required property. */
38
+ export function asRequired(schema) {
39
+ if (!(OptionalKind in schema))
40
+ return schema;
41
+ return Type.Required(Type.Object({ value: schema })).properties.value;
42
+ }
35
43
  /**
36
- * Apply schemaOverrides to a fields record.
37
- * Replaces matching field schemas with the override.
44
+ * Apply schemaOverrides to a fields record: each matching field becomes the declared schema,
45
+ * verbatim.
46
+ *
47
+ * Write bodies only. An override is a rule about what this API accepts **from now on**, and
48
+ * the rows already in the table were written before it existed — a column narrowed to
49
+ * `format: 'email'` today holds whatever was accepted last year, and one made mandatory today
50
+ * is `NULL` on every pre-existing row. Describing the response with it would publish a promise
51
+ * about data nobody can retroactively make true. Responses stay on the generated type.
52
+ *
53
+ * Verbatim, because on the request side the schema is the rule: an override without
54
+ * `Type.Optional` makes the field mandatory and one without `Nullable` rejects an explicit
55
+ * `null`, whatever the column allows. That is how a column the DB had to leave nullable is
56
+ * made mandatory going forward. Write the modifiers when you want them.
57
+ *
58
+ * Replace-only: an override naming a field the table does not have is ignored, so the
59
+ * declaration can never introduce a property the engine will not produce.
38
60
  */
39
61
  export function applySchemaOverrides(fields, tableConf) {
40
- if (!tableConf.schemaOverrides)
62
+ if (!tableConf?.schemaOverrides)
41
63
  return fields;
42
64
  const result = { ...fields };
43
65
  for (const [field, schema] of Object.entries(tableConf.schemaOverrides)) {
@@ -88,3 +110,13 @@ export function attachWriteJoinSections(target, tableConf, dbTables, options) {
88
110
  export const writeJoinBodyFields = (j, sc) => buildSecondaryFields(j.joinSchema, j.joinField, sc);
89
111
  /** JoinFieldsBuilder for response schemas: PK only. */
90
112
  export const writeJoinResponseFields = (j, sc) => pkSchema(sc, j.joinSchema, j.joinField);
113
+ /** Schema fields minus the ones hidden by `readExclude`. */
114
+ export function readableFields(schema, tableConf) {
115
+ if (!tableConf?.readExclude?.length)
116
+ return { ...schema.fields };
117
+ const out = {};
118
+ for (const field of readableFieldNames(tableConf, schema)) {
119
+ out[field] = schema.fields[field];
120
+ }
121
+ return out;
122
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/insert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAU/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA+B9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -1,14 +1,18 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
+ import { writableFields } from '../write-access.js';
3
4
  import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
4
5
  export function InsertTableBody(dbTables, tableName) {
5
6
  const tableConf = dbTables[tableName];
6
7
  const schema = tableConf.Schema;
7
- // Main: full validation, but excludeFromCreation fields become Optional
8
- const mainFields = applySchemaOverrides({ ...schema.fields }, tableConf);
8
+ // Main: full validation, but excludeFromCreation fields become Optional. Fields the
9
+ // database computes are not offered at all — sending one is rejected here (400) rather
10
+ // than by the driver (500).
11
+ const mainFields = writableFields(applySchemaOverrides({ ...schema.fields }, tableConf), tableConf, schema);
9
12
  if (tableConf.excludeFromCreation) {
10
13
  for (const field of tableConf.excludeFromCreation) {
11
- mainFields[field] = Type.Optional(mainFields[field]);
14
+ if (field in mainFields)
15
+ mainFields[field] = Type.Optional(mainFields[field]);
12
16
  }
13
17
  }
14
18
  const bodyProperties = {
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,OAAO,KAAK,EAAE,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AA+JzE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyClF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,GAAE,MAAmC,GAAG,OAAO,CAe9F;AAED,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,kDAAqB,CAAC;AAEzD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyClF"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AAkJvD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAyClF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,GAAE,MAAmC,GAAG,OAAO,CAe9F;AAED,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,kDAAqB,CAAC;AAEzD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA2ClF"}
@@ -1,17 +1,7 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { ALLOWED_METHODS, MAX_CONDITIONS, MAX_ORDER_BY_LENGTH } from '../condition-methods.js';
3
- import { readableFieldNames } from '../read-access.js';
3
+ import { readableFields } from './helpers.js';
4
4
  import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
5
- /** Schema fields minus the ones hidden by `readExclude`. */
6
- function readableFields(schema, tableConf) {
7
- if (!tableConf?.readExclude?.length)
8
- return { ...schema.fields };
9
- const out = {};
10
- for (const field of readableFieldNames(tableConf, schema)) {
11
- out[field] = schema.fields[field];
12
- }
13
- return out;
14
- }
15
5
  /**
16
6
  * A `filters` map.
17
7
  *
@@ -192,6 +182,8 @@ export function SearchTableResponse(dbTables, tableName) {
192
182
  // Computed fields are present in main rows only when explicitly listed in
193
183
  // request body's selectComputed, so they're optional/Partial here.
194
184
  const computedTypes = computedFieldTypes(tableConf);
185
+ // Not narrowed by schemaOverrides: an override rules what this API accepts from now on,
186
+ // while the rows already stored predate it (see applySchemaOverrides).
195
187
  const mainReadable = readableFields(tableConf.Schema, tableConf);
196
188
  const mainItem = Object.keys(computedTypes).length > 0
197
189
  ? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAwB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAW/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA+B9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -1,14 +1,18 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
- import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
3
+ import { writableFields } from '../write-access.js';
4
+ import { pkSchema, applySchemaOverrides, asRequired, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
4
5
  export function UpdateTableBody(dbTables, tableName) {
5
6
  const tableConf = dbTables[tableName];
6
7
  const schema = tableConf.Schema;
7
8
  // Main: PK required, all other fields optional (overrides applied before Optional wrap)
8
- const baseFields = applySchemaOverrides({ ...schema.fields }, tableConf);
9
+ const baseFields = writableFields(applySchemaOverrides({ ...schema.fields }, tableConf), tableConf, schema);
9
10
  const mainFields = {};
11
+ const pk = primaryAsString(tableConf.primary);
10
12
  for (const [key, value] of Object.entries(baseFields)) {
11
- mainFields[key] = key === primaryAsString(tableConf.primary) ? value : Type.Optional(value);
13
+ // The PK identifies the row being updated, so it is mandatory here even when the column
14
+ // is optional on insert (a generated id) and the override carried that optionality over.
15
+ mainFields[key] = key === pk ? asRequired(value) : Type.Optional(value);
12
16
  }
13
17
  const bodyProperties = {
14
18
  // additionalProperties:false makes the schema the real write whitelist — unknown keys are
@@ -1,4 +1,4 @@
1
- import type { FastifyRequest } from 'fastify';
1
+ import type { ApiRequest } from '../types/request.js';
2
2
  import { QueryClient } from './db.js';
3
3
  import { type DialectName } from './dialect.js';
4
4
  import type { DbTables, Queryable, FilterRecord, SearchCondition, Paginator, JoinGroupRequest, JoinRefFilter, JoinFetchRequest, SearchResult, GetResult, InsertResult, UpdateResult, DeleteResult, BulkUpsertItem, BulkUpsertResult, BulkDeleteResult, TenantId } from '../types.js';
@@ -30,7 +30,7 @@ export interface SqlApiUpdateParams {
30
30
  }
31
31
  export interface SqlApiOptions {
32
32
  dialect?: DialectName;
33
- getTenantId?: (request: FastifyRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
33
+ getTenantId?: (request: ApiRequest) => TenantId | TenantId[] | null | undefined | Promise<TenantId | TenantId[] | null | undefined>;
34
34
  }
35
35
  export declare class SqlApi {
36
36
  private dbTables;
@@ -39,13 +39,13 @@ export declare class SqlApi {
39
39
  constructor(dbOrPool: QueryClient | Queryable, dbTables: DbTables, options?: SqlApiOptions);
40
40
  private getTableConf;
41
41
  private getTenant;
42
- search(tableName: string, params?: SqlApiSearchParams, request?: FastifyRequest): Promise<SearchResult>;
43
- get(tableName: string, id: string | number, request?: FastifyRequest): Promise<GetResult>;
44
- insert(tableName: string, params: SqlApiInsertParams, request?: FastifyRequest): Promise<InsertResult>;
45
- update(tableName: string, params: SqlApiUpdateParams, request?: FastifyRequest): Promise<UpdateResult>;
46
- delete(tableName: string, id: string | number, request?: FastifyRequest): Promise<DeleteResult>;
47
- bulkUpsert(tableName: string, items: BulkUpsertItem[], request?: FastifyRequest): Promise<BulkUpsertResult[]>;
48
- bulkDelete(tableName: string, ids: (string | number)[], request?: FastifyRequest): Promise<BulkDeleteResult[]>;
42
+ search(tableName: string, params?: SqlApiSearchParams, request?: ApiRequest): Promise<SearchResult>;
43
+ get(tableName: string, id: string | number, request?: ApiRequest): Promise<GetResult>;
44
+ insert(tableName: string, params: SqlApiInsertParams, request?: ApiRequest): Promise<InsertResult>;
45
+ update(tableName: string, params: SqlApiUpdateParams, request?: ApiRequest): Promise<UpdateResult>;
46
+ delete(tableName: string, id: string | number, request?: ApiRequest): Promise<DeleteResult>;
47
+ bulkUpsert(tableName: string, items: BulkUpsertItem[], request?: ApiRequest): Promise<BulkUpsertResult[]>;
48
+ bulkDelete(tableName: string, ids: (string | number)[], request?: ApiRequest): Promise<BulkDeleteResult[]>;
49
49
  }
50
50
  export declare function createSqlApi(dbOrPool: QueryClient | Queryable, dbTables: DbTables, options?: SqlApiOptions): SqlApi;
51
51
  //# sourceMappingURL=sql-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
@@ -63,12 +63,13 @@ export class SqlApi {
63
63
  selectComputed: params.selectComputed,
64
64
  maxRows: params.maxRows,
65
65
  tenant,
66
+ request,
66
67
  });
67
68
  }
68
69
  async get(tableName, id, request) {
69
70
  const tableConf = this.getTableConf(tableName);
70
71
  const tenant = await this.getTenant(tableName, request);
71
- return getEngine({ db: this.db, tableConf, id, tenant });
72
+ return getEngine({ db: this.db, tableConf, id, tenant, request });
72
73
  }
73
74
  async insert(tableName, params, request) {
74
75
  const tableConf = this.getTableConf(tableName);
@@ -1 +1 @@
1
- {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAQX;AAoJD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAgDD,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAWhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
1
+ {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EAEP,MAAM,aAAa,CAAC;AAcrB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAWX;AAgSD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAgDD,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAWhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
@@ -1,6 +1,7 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
2
  import { Type } from '@sinclair/typebox';
3
3
  import { getDialect } from './dialect.js';
4
+ import { hasOwnField } from './read-access.js';
4
5
  // Quote an identifier using the given ConditionBuilder dialect (or the global
5
6
  // default when not provided). Needed so that DB columns with uppercase letters
6
7
  // (e.g. betterauth "userId") are preserved on PostgreSQL (which folds unquoted
@@ -49,8 +50,11 @@ export function defineTable(config) {
49
50
  validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
50
51
  validateWriteJoinsUnrestricted(config.allowedWriteJoins);
51
52
  validateComputedFields(config);
53
+ validatePrimary(config);
52
54
  validateReadExclude(config);
55
+ validateWriteExclude(config);
53
56
  validateTenantScope(config);
57
+ validateTenantColumnNames(config);
54
58
  return config;
55
59
  }
56
60
  /**
@@ -59,9 +63,35 @@ export function defineTable(config) {
59
63
  * entry reached through a parent FK would need its own join per entry — and an empty `anyOf`
60
64
  * would fail closed on every row, which looks like a broken deployment rather than a rule.
61
65
  *
62
- * Column *names* are not checked against the schema: a tenant column may legitimately be kept
63
- * out of the generated schema (see ADR 0009) while still scoping the table.
66
+ * The columns it names are **real DB column names**, not camelCase schema fields — the one place
67
+ * in `defineTable` where the convention flips. That is not an oversight: a scope is applied to
68
+ * the SQL, after the payload has already been converted, and `through.foreignField` belongs to a
69
+ * parent table that may have no config, no `colMap`, or no generated schema at all. It is the
70
+ * same category as `db.*` and `extendedCondition`: names that go straight to the database.
71
+ *
72
+ * So existence cannot be checked — a hand-written schema may legitimately omit the column. What
73
+ * *is* checked is the mistake that exception invites: naming the schema **field** whose real
74
+ * column is something else. Left alone that surfaces as `column "organizationId" does not exist`
75
+ * on every request to the table — an opaque 500 (ADR 0006), failing closed but saying nothing.
76
+ */
77
+ /**
78
+ * Reject a tenant column that is really a schema field name mapping to a different column.
79
+ *
80
+ * The check is deliberately narrow, and it self-adjusts: on a camelCase database `col()` is the
81
+ * identity, so a camelCase name raises nothing. A name the schema does not carry raises nothing
82
+ * either — that is the hand-written-schema case, and there is nothing to compare it against.
64
83
  */
84
+ function assertNamesColumn(schema, name, where, table) {
85
+ if (!hasOwnField(schema.fields, name))
86
+ return;
87
+ const column = schema.col(name);
88
+ if (column === name)
89
+ return;
90
+ throw new Error(`defineTable: tenantScope ${where} on table '${table}' names '${name}', which is a ` +
91
+ `schema field, not a column: its column is '${column}'. tenantScope names real DB ` +
92
+ `columns — the scope is applied to the SQL, after the payload has been converted — so ` +
93
+ `write '${column}' here. Clients keep sending '${name}'.`);
94
+ }
65
95
  function validateTenantScope(config) {
66
96
  const scope = config.tenantScope;
67
97
  if (!scope)
@@ -100,6 +130,35 @@ function validateTenantScope(config) {
100
130
  `A scope with no owner column would filter nothing.`);
101
131
  }
102
132
  }
133
+ /**
134
+ * Every column a scope names, checked against the schema it belongs to. Runs after
135
+ * `validateTenantScope`, so the shape is already known to be one of the three valid forms.
136
+ */
137
+ function validateTenantColumnNames(config) {
138
+ const scope = config.tenantScope;
139
+ if (!scope)
140
+ return;
141
+ const table = config.Schema.tableName;
142
+ if ('anyOf' in scope) {
143
+ for (const entry of scope.anyOf) {
144
+ assertNamesColumn(config.Schema, entry, "'anyOf'", table);
145
+ }
146
+ return;
147
+ }
148
+ const direct = scope;
149
+ assertNamesColumn(config.Schema, direct.column, "'column'", table);
150
+ // `through.localField` is a column of THIS table; `through.foreignField` one of the parent's,
151
+ // which is why the parent schema travels in the scope.
152
+ const through = direct.through;
153
+ if (!through || typeof through !== 'object')
154
+ return;
155
+ if (typeof through.localField === 'string') {
156
+ assertNamesColumn(config.Schema, through.localField, "'through.localField'", table);
157
+ }
158
+ if (typeof through.foreignField === 'string' && typeof through.schema?.col === 'function') {
159
+ assertNamesColumn(through.schema, through.foreignField, "'through.foreignField'", table);
160
+ }
161
+ }
103
162
  /**
104
163
  * `fields` narrows a relation for reading; it has no meaning on a write join and would be
105
164
  * actively harmful there. Write paths resolve the secondary's upsert rule by looking the
@@ -133,6 +192,68 @@ function validateReadExclude(config) {
133
192
  }
134
193
  }
135
194
  }
195
+ /**
196
+ * Every operation but `search` addresses a row by its primary key, and a `primary` naming a
197
+ * field the schema does not have reaches the database as a column that may not exist — a raw
198
+ * SQL error, so a 500 (ADR 0006), on the first request instead of at startup. Views are where
199
+ * this bites: they carry no PRIMARY KEY constraint, so the generator writes a placeholder
200
+ * rather than inventing a key that would silently address the wrong rows.
201
+ *
202
+ * `search` is the deliberate exception: it reads `primary` only as the fallback for ordering,
203
+ * so a table that exposes nothing else and names its own `defaultOrder` never touches it. That
204
+ * is exactly the shape the generator emits for a view whose key it could not infer, and it is
205
+ * a working config — not a broken one waiting to be fixed.
206
+ */
207
+ function validatePrimary(config) {
208
+ const schemaFields = Object.keys(config.Schema.fields);
209
+ const pkFields = Array.isArray(config.primary) ? config.primary : [config.primary];
210
+ const table = config.Schema.tableName;
211
+ const missing = pkFields.filter((f) => !schemaFields.includes(f));
212
+ if (missing.length === 0)
213
+ return;
214
+ const searchOnly = config.operations?.length === 1 && config.operations[0] === 'search';
215
+ if (searchOnly) {
216
+ if (config.defaultOrder)
217
+ return;
218
+ throw new Error(`defineTable: table '${table}' exposes only 'search' with an unresolved primary key ` +
219
+ `('${missing[0]}'), so it must declare a 'defaultOrder' — otherwise the search orders ` +
220
+ `by that missing column.`);
221
+ }
222
+ const exposed = config.operations
223
+ ? `operations [${config.operations.join(', ')}]`
224
+ : 'every operation (no `operations` whitelist, so all routes are exposed)';
225
+ throw new Error(`defineTable: primary key field '${missing[0]}' is not a schema field on table ` +
226
+ `'${table}', and the table exposes ${exposed} — those address rows by the primary key. ` +
227
+ `Name a column that is unique in this relation, or restrict the table to ` +
228
+ `operations: ['search'] with an explicit defaultOrder.`);
229
+ }
230
+ /**
231
+ * `writeExclude` names schema fields that no write may carry. A field outside the schema is a
232
+ * typo; the primary key is refused because the update body identifies the row with it; and a
233
+ * field that is also `readExclude`d would be neither readable nor writable, which is what
234
+ * removing it from the Schema means — keeping it there only grows the surface for nothing.
235
+ */
236
+ function validateWriteExclude(config) {
237
+ const excluded = config.writeExclude;
238
+ if (!excluded?.length)
239
+ return;
240
+ const schemaFields = Object.keys(config.Schema.fields);
241
+ const pkFields = Array.isArray(config.primary) ? config.primary : [config.primary];
242
+ const table = config.Schema.tableName;
243
+ for (const field of excluded) {
244
+ if (!schemaFields.includes(field)) {
245
+ throw new Error(`defineTable: writeExclude field '${field}' is not a schema field on table '${table}'.`);
246
+ }
247
+ if (pkFields.includes(field)) {
248
+ throw new Error(`defineTable: writeExclude cannot cover the primary key field '${field}' on table ` +
249
+ `'${table}' — the update body identifies the row with it.`);
250
+ }
251
+ if (config.readExclude?.includes(field)) {
252
+ throw new Error(`defineTable: field '${field}' on table '${table}' is in both readExclude and ` +
253
+ `writeExclude, leaving it neither readable nor writable. Remove it from the Schema instead.`);
254
+ }
255
+ }
256
+ }
136
257
  function validateComputedFields(config) {
137
258
  const computed = config.computedFields;
138
259
  if (!computed)
@@ -1,8 +1,8 @@
1
- import type { FastifyRequest } from 'fastify';
1
+ import type { ApiRequest } from '../types/request.js';
2
2
  import type { QueryClient } from './db.js';
3
3
  import { ConditionBuilder, type ConditionValue, type ConditionValueOrUndefined } from 'node-condition-builder';
4
4
  import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext } from '../types.js';
5
- export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: FastifyRequest): Promise<TenantContext | undefined>;
5
+ export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: ApiRequest): Promise<TenantContext | undefined>;
6
6
  /**
7
7
  * The read predicate for a scope, as a fragment every caller appends to its own WHERE.
8
8
  *
@@ -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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"}
@@ -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