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
@@ -20,43 +20,91 @@ export function parseSchemaFile(content) {
20
20
  }
21
21
  if (fields.length === 0)
22
22
  return null;
23
- // primaryKey: ["code"] — emitted by generate-schema from DB introspection
24
- let primary;
25
- const pkMatch = /primaryKey:\s*\[([^\]]*)\]/.exec(content);
26
- if (pkMatch) {
27
- primary = pkMatch[1]
28
- .split(',')
29
- .map((s) => s.trim().replace(/^["']|["']$/g, ''))
30
- .filter(Boolean);
31
- if (primary.length === 0)
32
- primary = undefined;
33
- }
23
+ // primaryKey: ["code"] / generatedFields: ["total"] — emitted by generate-schema from
24
+ // DB introspection. Both are absent on hand-written schema files.
25
+ const primary = parseFieldList(content, 'primaryKey');
26
+ const generated = parseFieldList(content, 'generatedFields');
27
+ const isView = /isView:\s*true/.test(content);
34
28
  return {
35
29
  schemaName: exportMatch[1],
36
30
  tableName: tableNameMatch[1],
37
31
  fields,
38
32
  fieldTypes,
39
33
  primary,
34
+ generated,
35
+ ...(isView ? { isView: true } : {}),
40
36
  };
41
37
  }
38
+ /** Read a `<key>: ["a", "b"]` list out of a generated schema file. */
39
+ function parseFieldList(content, key) {
40
+ const match = new RegExp(`${key}:\\s*\\[([^\\]]*)\\]`).exec(content);
41
+ if (!match)
42
+ return undefined;
43
+ const values = match[1]
44
+ .split(',')
45
+ .map((s) => s.trim().replace(/^["']|["']$/g, ''))
46
+ .filter(Boolean);
47
+ return values.length ? values : undefined;
48
+ }
42
49
  // ─── Detection ───────────────────────────────────────────────
50
+ /** Stands in for a primary key nobody can infer. `defineTable` refuses it beyond search. */
51
+ const PK_PLACEHOLDER = 'TODO_pick_a_unique_column';
43
52
  function detectPrimaryKey(schema) {
44
53
  // Preferred source: the PK introspected from the DB (primaryKey in the schema file).
45
54
  if (schema.primary?.length) {
46
55
  const pk = schema.primary.length === 1 ? schema.primary[0] : schema.primary;
47
56
  const first = schema.primary[0];
48
- return { pk, autoIncrement: schema.fieldTypes[first]?.includes('Optional') ?? false };
57
+ return {
58
+ pk,
59
+ autoIncrement: schema.fieldTypes[first]?.includes('Optional') ?? false,
60
+ source: 'declared',
61
+ };
62
+ }
63
+ // A view carries no PRIMARY KEY constraint, so an absent one says nothing about the data.
64
+ // Only `id` is a strong enough signal to act on; the type-based fallback below would take
65
+ // any integer column — a count, a year — and hand back a key that is not unique, which
66
+ // `get/:id`, update and delete would then address rows by. Guessing is the one thing that
67
+ // must not happen quietly here.
68
+ if (schema.isView) {
69
+ return schema.fields.includes('id')
70
+ ? { pk: 'id', autoIncrement: false, source: 'inferred' }
71
+ : { pk: PK_PLACEHOLDER, autoIncrement: false, source: 'unresolved' };
49
72
  }
50
73
  // Heuristic fallback for hand-written/legacy schema files without primaryKey.
51
74
  if (schema.fields.includes('id')) {
52
- return { pk: 'id', autoIncrement: schema.fieldTypes['id']?.includes('Optional') ?? false };
75
+ return {
76
+ pk: 'id',
77
+ autoIncrement: schema.fieldTypes['id']?.includes('Optional') ?? false,
78
+ source: 'inferred',
79
+ };
53
80
  }
54
81
  for (const field of schema.fields) {
55
82
  if (schema.fieldTypes[field]?.includes('Integer')) {
56
- return { pk: field, autoIncrement: schema.fieldTypes[field].includes('Optional') };
83
+ return {
84
+ pk: field,
85
+ autoIncrement: schema.fieldTypes[field].includes('Optional'),
86
+ source: 'inferred',
87
+ };
57
88
  }
58
89
  }
59
- return { pk: schema.fields[0], autoIncrement: false };
90
+ return { pk: schema.fields[0], autoIncrement: false, source: 'inferred' };
91
+ }
92
+ /**
93
+ * The `writeExclude` slot, plus the list of columns the database computes for this table.
94
+ *
95
+ * Those are excluded from every write by the runtime already — the Schema publishes them in
96
+ * `generatedFields` — so the line stays commented: it is here to say *why* the fields are
97
+ * missing from the write bodies, and to give a place to add fields of your own.
98
+ */
99
+ function writeExcludeLines(schema) {
100
+ if (!schema.generated?.length) {
101
+ return [' // writeExclude: [],'];
102
+ }
103
+ return [
104
+ ` // Computed by the database (GENERATED ALWAYS AS ...): ${schema.generated.join(', ')}`,
105
+ ' // — readable, never writable. Already excluded from every write body.',
106
+ ' // writeExclude: [],',
107
+ ];
60
108
  }
61
109
  /** First PK field: used for defaultOrder, relations and example snippets. */
62
110
  function pkFirst(pk) {
@@ -157,9 +205,48 @@ function allowedReadJoinsLines(parentRels) {
157
205
  lines.push(` // ],`);
158
206
  return lines;
159
207
  }
208
+ // ─── Views ───────────────────────────────────────────────────
209
+ /**
210
+ * The header a view's config gets: what the generator could not read from the database, and
211
+ * what it therefore assumed.
212
+ */
213
+ function viewNotice(schema, source) {
214
+ if (!schema.isView)
215
+ return [];
216
+ const lines = [
217
+ `// "${schema.tableName}" is a VIEW. It is a table config like any other, but the database`,
218
+ `// reports no PRIMARY KEY for it and only a view the engine can make updatable accepts a`,
219
+ `// write — an aggregating one rejects every INSERT and UPDATE outright.`,
220
+ ];
221
+ if (source === 'inferred') {
222
+ lines.push(`// The primary key below was inferred from the column names, NOT read from the database:`, `// check that it is actually unique in this view before exposing get/update/delete.`);
223
+ }
224
+ else {
225
+ lines.push(`// No column was a plausible primary key, so none was invented. As generated this config`, `// serves search only, which never needs one. To expose get/update/delete, replace the`, `// placeholder with a column that is unique in this view and widen \`operations\`.`);
226
+ }
227
+ return lines;
228
+ }
229
+ /**
230
+ * A view's route whitelist. Reads only: writing through a view works solely when the engine
231
+ * can make that view updatable, which is a property of the view's own SQL rather than of
232
+ * anything declared here — so the generated starting point is the one that always works, and
233
+ * widening it is a deliberate edit. This is not the runtime default (ADR 0002), which stays
234
+ * open: it is what the generator writes into a file the developer then owns.
235
+ */
236
+ function viewOperationsLine(schema, source) {
237
+ if (!schema.isView)
238
+ return [];
239
+ return source === 'unresolved'
240
+ ? [` operations: ['search'],`]
241
+ : [` operations: ['search', 'get'],`];
242
+ }
243
+ /** Ordering must not fall back to a primary key that does not exist. */
244
+ function defaultOrderField(schema, pk, source) {
245
+ return source === 'unresolved' ? schema.fields[0] : pkFirst(pk);
246
+ }
160
247
  export function generateSingleTableFile(schema, allSchemas) {
161
248
  const { byParent, byChild } = indexRelations(detectRelations(allSchemas));
162
- const { pk, autoIncrement } = detectPrimaryKey(schema);
249
+ const { pk, autoIncrement, source } = detectPrimaryKey(schema);
163
250
  const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
164
251
  const parentRels = byParent.get(schema.schemaName) ?? [];
165
252
  const childRels = byChild.get(schema.schemaName) ?? [];
@@ -180,6 +267,7 @@ export function generateSingleTableFile(schema, allSchemas) {
180
267
  lines.push(`// });`);
181
268
  // Table definition
182
269
  lines.push(``);
270
+ lines.push(...viewNotice(schema, source));
183
271
  lines.push(`// Fields: ${schema.fields.join(', ')}`);
184
272
  lines.push(`export const ${tableVarName} = defineTable({`);
185
273
  lines.push(` primary: ${formatPrimary(pk)},`);
@@ -194,15 +282,21 @@ export function generateSingleTableFile(schema, allSchemas) {
194
282
  lines.push(` // if (filters.q) condition.isILike(\`\${Schema.tableName}.\${Schema.col('${schema.fields.includes('name') ? 'name' : schema.fields[0]}')}\`, \`%\${filters.q}%\`);`);
195
283
  lines.push(` // }`);
196
284
  lines.push(` // ),`);
197
- lines.push(` defaultOrder: '${pkFirst(pk)}',`);
198
- if (autoIncrement) {
199
- lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
200
- }
201
- else {
202
- lines.push(` // excludeFromCreation: [],`);
285
+ lines.push(` defaultOrder: '${defaultOrderField(schema, pk, source)}',`);
286
+ lines.push(...viewOperationsLine(schema, source));
287
+ // A view is never inserted into by the auto routes as generated, so the creation whitelist
288
+ // would be a line about an operation that is not exposed.
289
+ if (!schema.isView) {
290
+ if (autoIncrement) {
291
+ lines.push(` excludeFromCreation: ['${pkFirst(pk)}'],`);
292
+ }
293
+ else {
294
+ lines.push(` // excludeFromCreation: [],`);
295
+ }
203
296
  }
204
297
  // Hide columns from every read (writes still accept them, e.g. a password hash)
205
298
  lines.push(` // readExclude: [],`);
299
+ lines.push(...writeExcludeLines(schema));
206
300
  lines.push(...allowedReadJoinsLines(parentRels));
207
301
  const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
208
302
  lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, [${upsertPk}])),`);
@@ -243,22 +337,26 @@ export function generateDbTablesIndex(schemas) {
243
337
  // ─── Code Generation (legacy) ───────────────────────────────
244
338
  /** One `defineTable({...})` block for the legacy single-file generator. */
245
339
  function legacyTableBlock(schema, parentRels, childRels) {
246
- const { pk, autoIncrement } = detectPrimaryKey(schema);
340
+ const { pk, autoIncrement, source } = detectPrimaryKey(schema);
247
341
  const tableVarName = 'Table' + schema.schemaName.replace(/^Schema/, '');
248
342
  const upsertPk = pkFields(pk).map((p) => `'${p}'`).join(', ');
249
343
  const exampleField = schema.fields.find((f) => !pkFields(pk).includes(f)) || schema.fields[0];
250
344
  const lines = [
251
345
  ``,
252
346
  `// ─── ${schema.tableName} ──────────────────────────────`,
347
+ ...viewNotice(schema, source),
253
348
  `// Fields: ${schema.fields.join(', ')}`,
254
349
  `const ${tableVarName} = defineTable({`,
255
350
  ` primary: ${formatPrimary(pk)},`,
256
351
  ` ...exportTableInfo(${schema.schemaName}),`,
257
- ` defaultOrder: '${pkFirst(pk)}',`,
258
- autoIncrement
259
- ? ` excludeFromCreation: ['${pkFirst(pk)}'],`
260
- : ` // excludeFromCreation: [],`,
352
+ ` defaultOrder: '${defaultOrderField(schema, pk, source)}',`,
353
+ ...viewOperationsLine(schema, source),
261
354
  ];
355
+ if (!schema.isView) {
356
+ lines.push(autoIncrement
357
+ ? ` excludeFromCreation: ['${pkFirst(pk)}'],`
358
+ : ` // excludeFromCreation: [],`);
359
+ }
262
360
  lines.push(...allowedReadJoinsLines(parentRels));
263
361
  lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, [${upsertPk}])),`, ` // schemaOverrides: {},`, ` validate: async (db, req, main, secondaries) => {`, ` const errors: ValidationError[] = [];`, ` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`, ` return errors;`, ` },`, ` // beforeInsert: async (db, req, record) => {},`, ` // beforeUpdate: async (db, req, fields) => {},`, tenantScopeLine(childRels), `});`);
264
362
  return lines;
@@ -27,12 +27,12 @@ async function finalizeUpsertedItem(params, item, inputMain, pkRow) {
27
27
  // FK auto-fill: input camelCase + generated PK
28
28
  const mainForFK = { ...inputMain, ...mainPkCamel };
29
29
  if (item.secondaries && Object.keys(item.secondaries).length > 0) {
30
- const sec = await processSecondaries({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.secondaries);
30
+ const sec = await processSecondaries({ db, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, item.secondaries);
31
31
  if (Object.keys(sec).length > 0)
32
32
  result.secondaries = sec;
33
33
  }
34
34
  if (item.deletions && Object.keys(item.deletions).length > 0) {
35
- const del = await processDeletions({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.deletions);
35
+ const del = await processDeletions({ db, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, item.deletions);
36
36
  if (Object.keys(del).length > 0)
37
37
  result.deletions = del;
38
38
  }
@@ -0,0 +1,19 @@
1
+ import type { ApiRequest } from '../../types/request.js';
2
+ import type { QueryClient } from '../db.js';
3
+ import type { ITable, ReadSource } from '../../types.js';
4
+ /**
5
+ * Run a table's `afterRead` hook over the rows a read produced.
6
+ *
7
+ * Every read path funnels through this one helper so the hook has a single meaning: it is
8
+ * handed camelCase rows of `tableConf`'s own table, in one batch, and mutates them in place.
9
+ * Batching is the contract, not an optimisation — a hook that decrypts through a remote KMS
10
+ * gets one call per result set instead of one per row.
11
+ *
12
+ * `tableConf` is resolved by the caller, which on a join is the JOINED table's config
13
+ * (`dbTables[joinSchema.tableName]`). That is what makes the hook follow a relation without
14
+ * being declared on the host, the way `readExclude` and `tenantScope` do (ADR 0010).
15
+ *
16
+ * An empty result set is skipped: there is nothing to transform, and the hook may be costly.
17
+ */
18
+ export declare function runAfterRead(db: QueryClient, request: ApiRequest | undefined, rows: Record<string, unknown>[], tableConf: ITable | undefined, source: ReadSource, alias?: string): Promise<void>;
19
+ //# sourceMappingURL=read-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-hooks.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/read-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAoB,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,UAAU,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Run a table's `afterRead` hook over the rows a read produced.
3
+ *
4
+ * Every read path funnels through this one helper so the hook has a single meaning: it is
5
+ * handed camelCase rows of `tableConf`'s own table, in one batch, and mutates them in place.
6
+ * Batching is the contract, not an optimisation — a hook that decrypts through a remote KMS
7
+ * gets one call per result set instead of one per row.
8
+ *
9
+ * `tableConf` is resolved by the caller, which on a join is the JOINED table's config
10
+ * (`dbTables[joinSchema.tableName]`). That is what makes the hook follow a relation without
11
+ * being declared on the host, the way `readExclude` and `tenantScope` do (ADR 0010).
12
+ *
13
+ * An empty result set is skipped: there is nothing to transform, and the hook may be costly.
14
+ */
15
+ export async function runAfterRead(db, request, rows, tableConf, source, alias) {
16
+ if (!tableConf?.afterRead || rows.length === 0)
17
+ return;
18
+ const ctx = alias === undefined ? { source } : { source, alias };
19
+ await tableConf.afterRead(db, request, rows, ctx);
20
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCrE"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA0CrE"}
@@ -3,9 +3,10 @@ import { camelcaseObject } from '../../naming.js';
3
3
  import { buildTenantCondition, buildTenantJoin } from '../../tenant.js';
4
4
  import { httpError } from '../../errors.js';
5
5
  import { readableSelectColumns } from '../../read-access.js';
6
+ import { runAfterRead } from '../read-hooks.js';
6
7
  import { primaryAsString, isCompositePrimary } from '../../../types.js';
7
8
  export async function getEngine(params) {
8
- const { db, tableConf, id, tenant } = params;
9
+ const { db, tableConf, id, tenant, request } = params;
9
10
  if (isCompositePrimary(tableConf.primary)) {
10
11
  throw httpError(400, `Table "${tableConf.Schema.tableName}" has a composite primary key: get-by-id would match on "${primaryAsString(tableConf.primary)}" alone. Use search with all PK fields.`);
11
12
  }
@@ -34,5 +35,7 @@ export async function getEngine(params) {
34
35
  });
35
36
  if (rows.length === 0)
36
37
  throw httpError(404, `Record not found: ${id}`);
37
- return { main: camelcaseObject(rows[0], tableConf.Schema) };
38
+ const main = camelcaseObject(rows[0], tableConf.Schema);
39
+ await runAfterRead(db, request, [main], tableConf, 'get');
40
+ return { main };
38
41
  }
@@ -32,7 +32,7 @@ export async function insertEngine(params) {
32
32
  let secondaryResults;
33
33
  if (secondaries && Object.keys(secondaries).length > 0) {
34
34
  const mainForFK = { ...inputRecord, ...mainPkCamel };
35
- secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
35
+ secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, secondaries);
36
36
  }
37
37
  // 5. afterInsert hook (camelCase — input merged with generated PK)
38
38
  if (tableConf.afterInsert) {
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2G9E"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA+G9E"}
@@ -1,4 +1,5 @@
1
1
  import { snakecaseRecord } from '../../naming.js';
2
+ import { removeWriteExcluded } from '../../write-access.js';
2
3
  import { processSecondaries, processDeletions } from '../write-helpers.js';
3
4
  import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
4
5
  import { runValidation } from '../validate.js';
@@ -46,22 +47,25 @@ export async function updateEngine(params) {
46
47
  if (tableConf.beforeUpdate) {
47
48
  await tableConf.beforeUpdate(db, request, inputRecord);
48
49
  }
49
- // 5. Convert to DB format (after all user mutations)
50
+ // 5. Drop what the database will not be told about (generated columns, writeExclude).
51
+ // After the hook on purpose: the column refuses a value whoever assigned it.
52
+ removeWriteExcluded(inputRecord, tableConf, schema);
53
+ // 6. Convert to DB format (after all user mutations)
50
54
  const snaked = snakecaseRecord(inputRecord, schema);
51
55
  const updateFields = { ...snaked };
52
56
  for (const c of pkCols)
53
57
  delete updateFields[c];
54
- // 6. Strip tenant column (user cannot change tenant of an existing record).
58
+ // 7. Strip tenant column (user cannot change tenant of an existing record).
55
59
  // For indirect scopes the tenant link is the through-FK (localField): it may be changed,
56
60
  // but only to another value the caller owns — re-validate the new FK against the tenant.
57
61
  if (tenant?.scope) {
58
62
  stripTenantColumn(updateFields, tenant.scope);
59
63
  await enforceTenantFKOnUpdate(db, tenant, updateFields);
60
64
  }
61
- // Steps 7-9 are atomic: a failure in secondaries or deletions rolls back the main
65
+ // Steps 8-10 are atomic: a failure in secondaries or deletions rolls back the main
62
66
  // update too. Degrades to non-transactional when the adapter has no connect().
63
67
  return db.withTransaction(async (tx) => {
64
- // 7. Update main
68
+ // 8. Update main
65
69
  const hasFieldsToUpdate = Object.keys(updateFields).length > 0;
66
70
  const extraCondition = buildTenantUpdateExtra(tx, tenant);
67
71
  // WHERE matching every primary key column (single or composite).
@@ -76,23 +80,23 @@ export async function updateEngine(params) {
76
80
  }
77
81
  // Build the main response (PK-only — every PK field)
78
82
  const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
79
- // 8. Secondaries (upsert/insert with FK auto-fill — camelCase)
83
+ // 9. Secondaries (upsert/insert with FK auto-fill — camelCase)
80
84
  let secondaryResults;
81
85
  if (secondaries && Object.keys(secondaries).length > 0) {
82
86
  const mainForFK = { ...inputRecord };
83
- secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
87
+ secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, secondaries);
84
88
  }
85
- // 9. Deletions (FK auto-fill from main like secondaries)
89
+ // 10. Deletions (FK auto-fill from main like secondaries)
86
90
  let deletionResults;
87
91
  if (deletions && Object.keys(deletions).length > 0) {
88
92
  const mainForFK = { ...inputRecord };
89
- deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, deletions);
93
+ deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant, request }, deletions);
90
94
  }
91
- // 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
95
+ // 11. afterUpdate hook (inside the transaction: throwing rolls everything back)
92
96
  if (tableConf.afterUpdate) {
93
97
  await tableConf.afterUpdate(tx, request, inputRecord, secondaryResults, deletionResults);
94
98
  }
95
- // 11. Return PK-only
99
+ // 12. Return PK-only
96
100
  const result = { main: mainResult };
97
101
  if (secondaryResults && Object.keys(secondaryResults).length > 0) {
98
102
  result.secondaries = secondaryResults;
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The four join families, each executed against the rows the main query returned.
3
+ *
4
+ * joinMustExist child -> main, 1:N EXISTS filter on the main query (returns no rows)
5
+ * joinMultiple child -> main, 1:N side query, rows under result.joinMultiple.<alias>
6
+ * joinGroup child -> main, 1:N aggregations under result.joinGroup.<alias>
7
+ * joinLeft parent -> main, N:1 real LEFT JOIN on demand + side query for the rows
8
+ */
9
+ import type { ApiRequest } from '../../../types/request.js';
1
10
  import type { QueryClient } from '../../db.js';
2
11
  import type { QueryParams } from '../query-params.js';
3
12
  import type { DbTables, ITable, JoinFetchRequest, JoinGroupRequest, JoinRefFilter, TenantContext } from '../../../types.js';
@@ -26,9 +35,9 @@ export declare function buildLeftJoinOnClauses(db: QueryClient, dbTables: DbTabl
26
35
  /** The WHERE fragments a `joinLeft` request's own filters and conditions contribute. */
27
36
  export declare function buildLeftJoinFilters(db: QueryClient, dbTables: DbTables, tableConf: ITable, aliasesNeedingJoin: Set<string>, joinLeft: Record<string, JoinFetchRequest> | undefined, startIdx: number): LeftJoinFilterBuild;
28
37
  /** joinMultiple: 1:N children, returned as rows under `result.joinMultiple.<alias>`. */
29
- export declare function executeJoinMultiple(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinMultiple: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
38
+ export declare function executeJoinMultiple(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinMultiple: Record<string, JoinFetchRequest>, tenant?: TenantContext, request?: ApiRequest): Promise<Record<string, Record<string, unknown>[]>>;
30
39
  /** joinLeft: N:1 parents, returned as rows under `result.joinLeft.<alias>`. */
31
- export declare function executeJoinLeft(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinLeft: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
40
+ export declare function executeJoinLeft(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinLeft: Record<string, JoinFetchRequest>, tenant?: TenantContext, request?: ApiRequest): Promise<Record<string, Record<string, unknown>[]>>;
32
41
  export declare function executeJoinGroup(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinGroup: Record<string, JoinGroupRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>>>;
33
42
  export declare function buildJoinMustExistClauses(params: QueryParams, db: QueryClient, dbTables: DbTables, tableConf: ITable, joinMustExist: Record<string, JoinRefFilter>, tenant?: TenantContext): string;
34
43
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"join-families.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/join-families.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAmBtD,OAAO,KAAK,EAEV,QAAQ,EAER,MAAM,EACN,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EAGb,aAAa,EAEd,MAAM,mBAAmB,CAAC;AAI3B,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,UAAU,mBAAmB;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAsED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,eAAe,CAgCjB;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACtD,QAAQ,EAAE,MAAM,GACf,mBAAmB,CA0CrB;AAmGD,wFAAwF;AACxF,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC9C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAKpD;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC1C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAKpD;AAsGD,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAqClD;AAyBD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC5C,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CA4BR"}
1
+ {"version":3,"file":"join-families.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/join-families.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,8 @@
1
- /**
2
- * The four join families, each executed against the rows the main query returned.
3
- *
4
- * joinMustExist child -> main, 1:N EXISTS filter on the main query (returns no rows)
5
- * joinMultiple child -> main, 1:N side query, rows under result.joinMultiple.<alias>
6
- * joinGroup child -> main, 1:N aggregations under result.joinGroup.<alias>
7
- * joinLeft parent -> main, N:1 real LEFT JOIN on demand + side query for the rows
8
- */
9
1
  import { ConditionBuilder, Expression } from 'node-condition-builder';
10
2
  import { assertReadable, assertFiltersReadable, hasOwnField, ownField } from '../../read-access.js';
11
3
  import { ALLOWED_SET } from '../../condition-methods.js';
12
4
  import { err400, validateSchemaField, evaluateComputedField, mapRowsToCamelCase, buildSelectionColumns, } from './fields.js';
5
+ import { runAfterRead } from '../read-hooks.js';
13
6
  import { appendJoinTenantScope, subqueryAlias, requireJoin, extractJoinRefs, } from './joins.js';
14
7
  import { tenantForTable, buildTenantRowGuard } from '../../tenant.js';
15
8
  import { dispatchConditionMethod, buildJoinRefCondition } from './conditions.js';
@@ -152,7 +145,7 @@ function scopeJoinSideQuery(cb, db, joinSchema, joinField, ids, joinTableConf, t
152
145
  const where = cb.build(1, db.ph);
153
146
  return { where, values: cb.getValues(), tenantJoins };
154
147
  }
155
- async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mode, tenant) {
148
+ async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mode, tenant, request) {
156
149
  const result = {};
157
150
  for (const [alias, ref] of Object.entries(requests)) {
158
151
  const joinDef = requireJoin(tableConf, alias, mode.requireUnique);
@@ -182,22 +175,27 @@ async function fetchJoinRows(db, dbTables, tableConf, mainResults, requests, mod
182
175
  joins: tenantJoins.length > 0 ? tenantJoins : undefined,
183
176
  });
184
177
  result[alias] = mapRowsToCamelCase(rows, joinSchema);
178
+ // The joined table's own hook, not the host's: a transform belongs to the table that
179
+ // owns the column, wherever its rows surface.
180
+ await runAfterRead(db, request, result[alias], joinTableConf, mode.source, alias);
185
181
  }
186
182
  return result;
187
183
  }
188
184
  /** joinMultiple: 1:N children, returned as rows under `result.joinMultiple.<alias>`. */
189
- export function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant) {
185
+ export function executeJoinMultiple(db, dbTables, tableConf, mainResults, joinMultiple, tenant, request) {
190
186
  return fetchJoinRows(db, dbTables, tableConf, mainResults, joinMultiple, {
187
+ source: 'joinMultiple',
191
188
  requireUnique: false,
192
189
  applyRefCondition: true,
193
- }, tenant);
190
+ }, tenant, request);
194
191
  }
195
192
  /** joinLeft: N:1 parents, returned as rows under `result.joinLeft.<alias>`. */
196
- export function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant) {
193
+ export function executeJoinLeft(db, dbTables, tableConf, mainResults, joinLeft, tenant, request) {
197
194
  return fetchJoinRows(db, dbTables, tableConf, mainResults, joinLeft, {
195
+ source: 'joinLeft',
198
196
  requireUnique: true,
199
197
  applyRefCondition: false,
200
- }, tenant);
198
+ }, tenant, request);
201
199
  }
202
200
  /**
203
201
  * Resolve `aggregations.by` to a SQL expression. Accepts:
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAyP3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAsFvB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/search.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAyP3B,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CA4FvB"}
@@ -5,6 +5,7 @@ import { MAX_CONDITIONS, MAX_ORDER_BY_PARTS } from '../../condition-methods.js';
5
5
  import { primaryAsString } from '../../../types.js';
6
6
  import { err400, resolveFieldRef, evaluateComputedField, refValues, renderRef, mapRowsToCamelCase, } from './fields.js';
7
7
  import { assertJoinFilterKeys, requireJoin } from './joins.js';
8
+ import { runAfterRead } from '../read-hooks.js';
8
9
  import { collectOrderByLeftAliases, validateOrderBy, convertConfiguredOrder } from './order-by.js';
9
10
  import { applyConditions, applyComputedFilters } from './conditions.js';
10
11
  import { appendAggConditions } from './aggregations.js';
@@ -187,12 +188,12 @@ function collectLeftJoinAliases(params) {
187
188
  * it returned.
188
189
  */
189
190
  async function attachJoinResults(result, dbTables, params, main) {
190
- const { db, tableConf, joinMultiple, joinLeft, joinGroup, tenant } = params;
191
+ const { db, tableConf, joinMultiple, joinLeft, joinGroup, tenant, request } = params;
191
192
  if (joinMultiple && Object.keys(joinMultiple).length > 0) {
192
- result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant);
193
+ result.joinMultiple = await executeJoinMultiple(db, dbTables, tableConf, main, joinMultiple, tenant, request);
193
194
  }
194
195
  if (joinLeft && Object.keys(joinLeft).length > 0) {
195
- result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant);
196
+ result.joinLeft = await executeJoinLeft(db, dbTables, tableConf, main, joinLeft, tenant, request);
196
197
  }
197
198
  if (joinGroup && Object.keys(joinGroup).length > 0) {
198
199
  result.joinGroup = await executeJoinGroup(db, dbTables, tableConf, main, joinGroup, tenant);
@@ -248,6 +249,11 @@ export async function searchEngine(dbTables, params) {
248
249
  }
249
250
  const result = { main };
250
251
  await attachJoinResults(result, dbTables, params, main);
252
+ // After the joins, not before: they correlate to the main rows by the values the DB
253
+ // returned (`fk IN (ids)`), and a hook that rewrites the column holding that key would
254
+ // have them look for a value no child row carries. `main` is the same array either way,
255
+ // so the response still carries the hook's mutations.
256
+ await runAfterRead(db, params.request, main, tableConf, 'search');
251
257
  if (pagination) {
252
258
  result.pagination = pagination;
253
259
  }
@@ -1,6 +1,6 @@
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 type { ITable, BulkValidatorItem } from '../../types.js';
4
- export declare function runValidation(db: QueryClient, request: FastifyRequest, tableConf: ITable, main: Record<string, unknown>, secondaries?: Record<string, Record<string, unknown>[]>): Promise<void>;
5
- export declare function runBulkValidation(db: QueryClient, request: FastifyRequest, tableConf: ITable, items: BulkValidatorItem[]): Promise<void>;
4
+ export declare function runValidation(db: QueryClient, request: ApiRequest, tableConf: ITable, main: Record<string, unknown>, secondaries?: Record<string, Record<string, unknown>[]>): Promise<void>;
5
+ export declare function runBulkValidation(db: QueryClient, request: ApiRequest, tableConf: ITable, items: BulkValidatorItem[]): Promise<void>;
6
6
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAmB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAgBjF,wBAAsB,aAAa,CACjC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACtD,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAOf"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAmB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAgBjF,wBAAsB,aAAa,CACjC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACtD,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -1,4 +1,4 @@
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 type { ITable, DbTables, TenantContext } from '../../types.js';
4
4
  export declare function findSecondaryTableConf(dbTables: DbTables, joinTableName: string): ITable | undefined;
@@ -13,11 +13,13 @@ export interface WriteJoinPass {
13
13
  /** Main record (camelCase), the source of the FK auto-filled into every child row. */
14
14
  mainRecord: Record<string, unknown>;
15
15
  tenant?: TenantContext;
16
+ /** The request that triggered the write, handed to the child table's `beforeInsert`. */
17
+ request: ApiRequest;
16
18
  }
17
19
  interface PrepareCtx {
18
20
  db: QueryClient;
19
21
  tableConf: ITable;
20
- request: FastifyRequest;
22
+ request: ApiRequest;
21
23
  }
22
24
  /**
23
25
  * Prepare a single record for INSERT/UPSERT:
@@ -26,7 +28,9 @@ interface PrepareCtx {
26
28
  * 3. drop excludeFromCreation fields (client-input whitelist; applied BEFORE the
27
29
  * hook so values it assigns — e.g. a server-generated id — reach the INSERT)
28
30
  * 4. beforeInsert hook (caller can mutate the camelCase copy)
29
- * 5. snakecaseRecord → DB format
31
+ * 5. drop non-writable fields (generated columns, writeExclude) — AFTER the hook, because
32
+ * the database refuses those values whoever assigned them
33
+ * 6. snakecaseRecord → DB format
30
34
  *
31
35
  * Returns both `camel` (input + hook mutations, used for FK auto-fill / afterInsert) and
32
36
  * `snake` (DB-ready record). The original `input` is left intact.
@@ -1 +1 @@
1
- {"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAS5C,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EAGR,aAAa,EACd,MAAM,gBAAgB,CAAC;AAqDxB,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAKpB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAmCD,UAAU,UAAU;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EACvD,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAe7E;AAkCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACrD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAkDpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACnD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAoBpD"}
1
+ {"version":3,"file":"write-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/engine/write-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAU5C,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EAGR,aAAa,EACd,MAAM,gBAAgB,CAAC;AAmCxB,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAKpB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,wFAAwF;IACxF,OAAO,EAAE,UAAU,CAAC;CACrB;AAmCD,UAAU,UAAU;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EACvD,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAgB7E;AAkCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,aAAa,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACrD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CA0DpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GACnD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAoBpD"}