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,5 +1,6 @@
1
1
  import { camelcaseObject, snakecaseRecord } from '../naming.js';
2
2
  import { runValidation } from './validate.js';
3
+ import { removeWriteExcluded } from '../write-access.js';
3
4
  import { tenantForTable, buildTenantRowGuard, enforceTenantOnWrites, assertTenantOwnsConflicts, tenantImmutableColumns, } from '../tenant.js';
4
5
  /**
5
6
  * Resolve the write join an alias names, or undefined when the table does not declare it.
@@ -14,26 +15,11 @@ function findWriteJoin(tableConf, alias) {
14
15
  return tableConf.allowedWriteJoins?.find((j) => j.alias === alias);
15
16
  }
16
17
  /**
17
- * Drop `excludeFromCreation` columns from a snake_case (DB-format) record.
18
+ * Drop `excludeFromCreation` fields from a camelCase record, in place.
18
19
  *
19
- * Exclusion is a whitelist on CLIENT input: call this on the client payload only,
20
- * BEFORE any server-side value is assigned (beforeInsert mutations, FK auto-fill),
21
- * so engine/hook-generated values on those fields reach the SQL.
22
- */
23
- function removeExcludedFields(record, tableConf) {
24
- if (!tableConf.excludeFromCreation?.length)
25
- return record;
26
- const result = { ...record };
27
- for (const field of tableConf.excludeFromCreation) {
28
- const col = tableConf.Schema.col(field);
29
- delete result[col];
30
- }
31
- return result;
32
- }
33
- /**
34
- * Same whitelist as `removeExcludedFields`, but for a camelCase record (schema
35
- * field names) and in place. Used on the client payload before the beforeInsert
36
- * hook runs.
20
+ * Exclusion is a whitelist on CLIENT input: call this on the client payload only, BEFORE any
21
+ * server-side value is assigned (beforeInsert mutations, FK auto-fill), so engine- and
22
+ * hook-generated values on those fields reach the SQL (ADR 0005).
37
23
  */
38
24
  function removeExcludedFieldsCamel(record, tableConf) {
39
25
  if (!tableConf.excludeFromCreation?.length)
@@ -71,7 +57,9 @@ function resolveWriteJoinTarget(joinDef, dbTables, mainRecord, tenant) {
71
57
  * 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
72
58
  * hook so values it assigns — e.g. a server-generated id — reach the INSERT)
73
59
  * 4. beforeInsert hook (caller can mutate the camelCase copy)
74
- * 5. snakecaseRecord → DB format
60
+ * 5. drop non-writable fields (generated columns, writeExclude) — AFTER the hook, because
61
+ * the database refuses those values whoever assigned them
62
+ * 6. snakecaseRecord → DB format
75
63
  *
76
64
  * Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
77
65
  * `snake` (DB-ready record). The original `input` is left intact.
@@ -88,6 +76,7 @@ export async function prepareInsertRecord(ctx, input, secondaries, options) {
88
76
  if (ctx.tableConf.beforeInsert) {
89
77
  await ctx.tableConf.beforeInsert(ctx.db, ctx.request, camel);
90
78
  }
79
+ removeWriteExcluded(camel, ctx.tableConf, ctx.tableConf.Schema);
91
80
  const snake = snakecaseRecord(camel, ctx.tableConf.Schema);
92
81
  return { camel, snake };
93
82
  }
@@ -121,23 +110,33 @@ async function forEachWriteJoin(pass, payload, writeAlias) {
121
110
  * where the child is upserted, hijack one of that tenant's existing rows via the conflict key.
122
111
  */
123
112
  export function processSecondaries(pass, secondaries) {
124
- const { db, tableConf } = pass;
113
+ const { db, tableConf, request } = pass;
125
114
  return forEachWriteJoin(pass, secondaries, async (records, target, joinDef) => {
126
115
  const { joinSchema, joinCol, mainValue, childConf, childTenant } = target;
127
116
  const childPk = childConf?.primary || joinDef.joinField;
128
117
  const childPkCol = Array.isArray(childPk)
129
118
  ? childPk.map((f) => joinSchema.col(f))
130
119
  : joinSchema.col(childPk);
131
- const preparedRecords = records.map((rec) => {
132
- let prepared = snakecaseRecord(rec, joinSchema);
133
- // Remove excluded fields from the client payload BEFORE the FK auto-fill:
134
- // an excluded FK column must not strip the engine-injected value below.
120
+ // Same order as `prepareInsertRecord` (ADR 0005): the client whitelist runs first, then
121
+ // the child's own `beforeInsert`, then the DB format — so a value the hook assigns to an
122
+ // excluded field still reaches the INSERT. A secondary is a write like any other, and its
123
+ // table's transform has to run here too or a column that table encodes on the way in is
124
+ // stored raw whenever its rows arrive as another table's children.
125
+ const preparedRecords = [];
126
+ for (const rec of records) {
127
+ const camel = { ...rec };
135
128
  if (childConf) {
136
- prepared = removeExcludedFields(prepared, childConf);
129
+ removeExcludedFieldsCamel(camel, childConf);
130
+ if (childConf.beforeInsert) {
131
+ await childConf.beforeInsert(db, request, camel);
132
+ }
137
133
  }
134
+ removeWriteExcluded(camel, childConf, joinSchema);
135
+ const prepared = snakecaseRecord(camel, joinSchema);
136
+ // FK auto-fill last: the engine's own value, not the client's and not the hook's.
138
137
  prepared[joinCol] = mainValue;
139
- return prepared;
140
- });
138
+ preparedRecords.push(prepared);
139
+ }
141
140
  // Tenant enforcement runs after the FK auto-fill: for an indirect scope the through-FK
142
141
  // the engine just injected is the value being validated.
143
142
  await enforceTenantOnWrites(db, childTenant, preparedRecords);
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAoB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
1
+ {"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/bulk-upsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAU/C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,oGAuB3F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oGAa5E"}
@@ -1,11 +1,12 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
+ import { writableFields } from '../write-access.js';
3
4
  import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
4
5
  export function BulkUpsertTableBody(dbTables, tableName, maxItems) {
5
6
  const tableConf = dbTables[tableName];
6
7
  const schema = tableConf.Schema;
7
8
  // additionalProperties:false makes the schema the real write whitelist (no mass assignment).
8
- const mainSchema = Type.Partial(Type.Object(applySchemaOverrides({ ...schema.fields }, tableConf), { additionalProperties: false }));
9
+ const mainSchema = Type.Partial(Type.Object(writableFields(applySchemaOverrides({ ...schema.fields }, tableConf), tableConf, schema), { additionalProperties: false }));
9
10
  const itemProperties = {
10
11
  main: mainSchema,
11
12
  };
@@ -0,0 +1,14 @@
1
+ import { type TObject } from '@sinclair/typebox';
2
+ import type { DbTables } from '../../types.js';
3
+ /**
4
+ * Response shape of `GET /rest/:table/:id`.
5
+ *
6
+ * Lives here rather than inline in the route so it is built from the same pieces as every
7
+ * other operation: it drifted while it was inline, documenting `readExclude`d fields the
8
+ * engine never selects.
9
+ *
10
+ * `Partial` because a record is returned column by column: a nullable column the driver
11
+ * reports as absent must not fail serialization.
12
+ */
13
+ export declare function GetTableResponse(dbTables: DbTables, tableName: string): TObject;
14
+ //# sourceMappingURL=get.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/get.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -0,0 +1,18 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { readableFields } from './helpers.js';
3
+ /**
4
+ * Response shape of `GET /rest/:table/:id`.
5
+ *
6
+ * Lives here rather than inline in the route so it is built from the same pieces as every
7
+ * other operation: it drifted while it was inline, documenting `readExclude`d fields the
8
+ * engine never selects.
9
+ *
10
+ * `Partial` because a record is returned column by column: a nullable column the driver
11
+ * reports as absent must not fail serialization.
12
+ */
13
+ export function GetTableResponse(dbTables, tableName) {
14
+ const tableConf = dbTables[tableName];
15
+ return Type.Object({
16
+ main: Type.Partial(Type.Object(readableFields(tableConf.Schema, tableConf))),
17
+ });
18
+ }
@@ -5,11 +5,27 @@ import type { ITable, SchemaDefinition, DbTables, JoinDefinition } from '../../t
5
5
  * Falls back to `fallback` field if tableConf is undefined.
6
6
  */
7
7
  export declare function pkSchema(tableConf: ITable | undefined, schema: SchemaDefinition, fallback: string): Record<string, TSchema>;
8
+ /** The same schema without the Optional modifier, i.e. as a required property. */
9
+ export declare function asRequired(schema: TSchema): TSchema;
8
10
  /**
9
- * Apply schemaOverrides to a fields record.
10
- * Replaces matching field schemas with the override.
11
+ * Apply schemaOverrides to a fields record: each matching field becomes the declared schema,
12
+ * verbatim.
13
+ *
14
+ * Write bodies only. An override is a rule about what this API accepts **from now on**, and
15
+ * the rows already in the table were written before it existed — a column narrowed to
16
+ * `format: 'email'` today holds whatever was accepted last year, and one made mandatory today
17
+ * is `NULL` on every pre-existing row. Describing the response with it would publish a promise
18
+ * about data nobody can retroactively make true. Responses stay on the generated type.
19
+ *
20
+ * Verbatim, because on the request side the schema is the rule: an override without
21
+ * `Type.Optional` makes the field mandatory and one without `Nullable` rejects an explicit
22
+ * `null`, whatever the column allows. That is how a column the DB had to leave nullable is
23
+ * made mandatory going forward. Write the modifiers when you want them.
24
+ *
25
+ * Replace-only: an override naming a field the table does not have is ignored, so the
26
+ * declaration can never introduce a property the engine will not produce.
11
27
  */
12
- export declare function applySchemaOverrides(fields: Record<string, TSchema>, tableConf: ITable): Record<string, TSchema>;
28
+ export declare function applySchemaOverrides(fields: Record<string, TSchema>, tableConf: ITable | undefined): Record<string, TSchema>;
13
29
  type JoinFieldsBuilder = (join: JoinDefinition, secondaryConf: ITable | undefined) => Record<string, TSchema>;
14
30
  /**
15
31
  * Attach `secondaries` (and optionally `deletions`) sections to the given target object,
@@ -29,5 +45,7 @@ export declare function attachWriteJoinSections(target: Record<string, TSchema>,
29
45
  export declare const writeJoinBodyFields: JoinFieldsBuilder;
30
46
  /** JoinFieldsBuilder for response schemas: PK only. */
31
47
  export declare const writeJoinResponseFields: JoinFieldsBuilder;
48
+ /** Schema fields minus the ones hidden by `readExclude`. */
49
+ export declare function readableFields(schema: SchemaDefinition, tableConf: ITable | undefined): Record<string, TSchema>;
32
50
  export {};
33
51
  //# sourceMappingURL=helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AA6BD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAwB7B;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,IAAI,CA8BN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKzF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3H;AAiCD,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAGnD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,KAAK,iBAAiB,GAAG,CACvB,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,MAAM,GAAG,SAAS,KAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAwB7B;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,iBAAiB,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7F,IAAI,CA8BN;AAED,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,iBACmB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,EAAE,iBACG,CAAC;AAE1C,4DAA4D;AAC5D,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB"}
@@ -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
  *