fastify-auto-sqlapi 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +50 -0
  2. package/AGENTS_BACKEND.md +264 -6
  3. package/AGENTS_FRONTEND.md +1 -1
  4. package/BREAKING_CHANGES.md +51 -0
  5. package/CHANGELOG.md +113 -0
  6. package/README.md +254 -983
  7. package/dist/bin/generate-schema.js +49 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/lib/agent/manifest.d.ts +2 -0
  12. package/dist/lib/agent/manifest.d.ts.map +1 -1
  13. package/dist/lib/agent/manifest.js +4 -0
  14. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/mysql-introspect.js +15 -5
  16. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  17. package/dist/lib/cli/pg-introspect.js +53 -14
  18. package/dist/lib/cli/schema-codegen.d.ts +2 -1
  19. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  20. package/dist/lib/cli/schema-codegen.js +17 -4
  21. package/dist/lib/cli/tables-codegen.d.ts +4 -0
  22. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  23. package/dist/lib/cli/tables-codegen.js +125 -27
  24. package/dist/lib/engine/bulk/bulk-upsert.js +2 -2
  25. package/dist/lib/engine/read-hooks.d.ts +19 -0
  26. package/dist/lib/engine/read-hooks.d.ts.map +1 -0
  27. package/dist/lib/engine/read-hooks.js +20 -0
  28. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  29. package/dist/lib/engine/rest/get.js +5 -2
  30. package/dist/lib/engine/rest/insert.js +1 -1
  31. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  32. package/dist/lib/engine/rest/update.js +14 -10
  33. package/dist/lib/engine/search/join-families.d.ts +11 -2
  34. package/dist/lib/engine/search/join-families.d.ts.map +1 -1
  35. package/dist/lib/engine/search/join-families.js +11 -13
  36. package/dist/lib/engine/search/search.d.ts.map +1 -1
  37. package/dist/lib/engine/search/search.js +9 -3
  38. package/dist/lib/engine/validate.d.ts +3 -3
  39. package/dist/lib/engine/validate.d.ts.map +1 -1
  40. package/dist/lib/engine/write-helpers.d.ts +7 -3
  41. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  42. package/dist/lib/engine/write-helpers.js +27 -28
  43. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  44. package/dist/lib/schema/bulk-upsert.js +2 -1
  45. package/dist/lib/schema/get.d.ts +14 -0
  46. package/dist/lib/schema/get.d.ts.map +1 -0
  47. package/dist/lib/schema/get.js +18 -0
  48. package/dist/lib/schema/helpers.d.ts +21 -3
  49. package/dist/lib/schema/helpers.d.ts.map +1 -1
  50. package/dist/lib/schema/helpers.js +37 -5
  51. package/dist/lib/schema/insert.d.ts.map +1 -1
  52. package/dist/lib/schema/insert.js +7 -3
  53. package/dist/lib/schema/search.d.ts.map +1 -1
  54. package/dist/lib/schema/search.js +3 -11
  55. package/dist/lib/schema/update.d.ts.map +1 -1
  56. package/dist/lib/schema/update.js +7 -3
  57. package/dist/lib/sql-api.d.ts +9 -9
  58. package/dist/lib/sql-api.d.ts.map +1 -1
  59. package/dist/lib/sql-api.js +2 -1
  60. package/dist/lib/table-helpers.d.ts.map +1 -1
  61. package/dist/lib/table-helpers.js +123 -2
  62. package/dist/lib/tenant.d.ts +2 -2
  63. package/dist/lib/tenant.d.ts.map +1 -1
  64. package/dist/lib/write-access.d.ts +36 -0
  65. package/dist/lib/write-access.d.ts.map +1 -0
  66. package/dist/lib/write-access.js +58 -0
  67. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  68. package/dist/routes/auto/get.routes.js +3 -2
  69. package/dist/routes/auto/plugin.js +2 -2
  70. package/dist/routes/auto/route-helpers.js +1 -1
  71. package/dist/routes/setup-swagger.d.ts.map +1 -0
  72. package/dist/{lib → routes}/sql-api-decorator.d.ts +1 -1
  73. package/dist/routes/sql-api-decorator.d.ts.map +1 -0
  74. package/dist/{lib → routes}/sql-api-decorator.js +4 -4
  75. package/dist/types/cli.d.ts +27 -1
  76. package/dist/types/cli.d.ts.map +1 -1
  77. package/dist/types/dml.d.ts +6 -4
  78. package/dist/types/dml.d.ts.map +1 -1
  79. package/dist/types/plugin.d.ts +3 -3
  80. package/dist/types/plugin.d.ts.map +1 -1
  81. package/dist/types/request.d.ts +24 -0
  82. package/dist/types/request.d.ts.map +1 -0
  83. package/dist/types/request.js +1 -0
  84. package/dist/types/schema.d.ts +15 -0
  85. package/dist/types/schema.d.ts.map +1 -1
  86. package/dist/types/search.d.ts +3 -0
  87. package/dist/types/search.d.ts.map +1 -1
  88. package/dist/types/table.d.ts +84 -11
  89. package/dist/types/table.d.ts.map +1 -1
  90. package/dist/types/validation.d.ts +3 -3
  91. package/dist/types/validation.d.ts.map +1 -1
  92. package/dist/types.d.ts +1 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js +1 -0
  95. package/docs/adr/0004-updates-always-open.md +2 -1
  96. package/docs/adr/0014-what-follows-a-write-join.md +84 -0
  97. package/docs/adr/0015-non-writable-columns.md +86 -0
  98. package/docs/adr/0016-a-view-is-a-table-config.md +92 -0
  99. package/docs/adr/0017-the-schema-is-the-contract.md +85 -0
  100. package/docs/adr/0018-one-seam-to-the-http-framework.md +68 -0
  101. package/docs/adr/README.md +5 -0
  102. package/package.json +1 -1
  103. package/dist/lib/setup-swagger.d.ts.map +0 -1
  104. package/dist/lib/sql-api-decorator.d.ts.map +0 -1
  105. /package/dist/{lib → routes}/setup-swagger.d.ts +0 -0
  106. /package/dist/{lib → routes}/setup-swagger.js +0 -0
@@ -37,10 +37,10 @@ function fileStatus(filename, content) {
37
37
  function writeSchemaFiles(tableMap, schemasDir) {
38
38
  const report = { created: false, touched: false, untouched: 0, files: new Set() };
39
39
  for (const schemaName of Object.keys(tableMap)) {
40
- const { name: tableName, fields, colMap, primary } = tableMap[schemaName];
40
+ const { name: tableName, fields, colMap, primary, generated, isView } = tableMap[schemaName];
41
41
  const filename = path.join(schemasDir, `${schemaName}.ts`);
42
42
  report.files.add(`${schemaName}.ts`);
43
- const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary);
43
+ const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView);
44
44
  const status = fileStatus(filename, content);
45
45
  if (status === 'created')
46
46
  report.created = true;
@@ -71,6 +71,51 @@ function removeOrphanSchemas(schemasDir, generated) {
71
71
  }
72
72
  return removed;
73
73
  }
74
+ /**
75
+ * Name the columns the database computes, table by table.
76
+ *
77
+ * These are the columns an INSERT or an UPDATE may not mention — both engines reject the
78
+ * statement — so the generator leaves them out of every write body. Saying so here matters
79
+ * more than the comment it also writes into the file: the generated source is not always
80
+ * read, whereas this output is what whoever ran the command actually sees.
81
+ */
82
+ function reportGeneratedColumns(tableMap) {
83
+ const withGenerated = Object.keys(tableMap)
84
+ .map((schemaName) => tableMap[schemaName])
85
+ .filter((t) => t.generated.length > 0);
86
+ if (withGenerated.length === 0)
87
+ return;
88
+ console.log('');
89
+ display('Database-computed columns found (GENERATED ALWAYS AS ...):', CONSOLE_COLORS.yellow);
90
+ for (const table of withGenerated) {
91
+ display(` ${table.name}: ${table.generated.join(', ')}`, CONSOLE_COLORS.yellow);
92
+ }
93
+ display(' They are readable but rejected by every write, so they are excluded from the insert, ' +
94
+ 'update and bulk bodies. Do not send them: they are computed by the database.', CONSOLE_COLORS.gray);
95
+ }
96
+ /**
97
+ * Name the relations that are views.
98
+ *
99
+ * The generated `Table*.ts` says the same thing in a comment, but a generated source file is
100
+ * not always read — this output is what whoever ran the command actually sees, and the two
101
+ * assumptions worth contesting (which column is the key, whether writes make sense) are both
102
+ * assumptions the generator had to make without the database's help.
103
+ */
104
+ function reportViews(tableMap) {
105
+ const views = Object.keys(tableMap)
106
+ .map((schemaName) => tableMap[schemaName])
107
+ .filter((t) => t.isView);
108
+ if (views.length === 0)
109
+ return;
110
+ console.log('');
111
+ display(`Views found: ${views.map((v) => v.name).join(', ')}`, CONSOLE_COLORS.yellow);
112
+ display(' A view is a table config like any other, but the database reports no PRIMARY KEY for ' +
113
+ 'one. The table generator will NOT invent one: it uses "id" when the view has it and says ' +
114
+ 'so, and otherwise writes a TODO placeholder. Check the key is really unique in the view ' +
115
+ 'before exposing get/update/delete — those address rows by it.', CONSOLE_COLORS.gray);
116
+ display(' Writes are left off by default: only a view simple enough for the engine to make ' +
117
+ 'updatable accepts one, and an aggregating view rejects every INSERT and UPDATE.', CONSOLE_COLORS.gray);
118
+ }
74
119
  function reportOutcome(report) {
75
120
  console.log('');
76
121
  if (report.untouched > 0) {
@@ -117,6 +162,8 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
117
162
  display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
118
163
  return;
119
164
  }
165
+ reportViews(tableMap);
166
+ reportGeneratedColumns(tableMap);
120
167
  const report = writeSchemaFiles(tableMap, schemasDir);
121
168
  if (!tableNames.length && removeOrphanSchemas(schemasDir, report.files)) {
122
169
  report.touched = true;
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { resolveTenant } from './lib/tenant.js';
12
12
  export { SqlApi, createSqlApi } from './lib/sql-api.js';
13
13
  export type { SqlApiOptions, SqlApiSearchParams, SqlApiInsertParams, SqlApiUpdateParams } from './lib/sql-api.js';
14
14
  export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
15
- export { setupSwagger } from './lib/setup-swagger.js';
15
+ export { setupSwagger } from './routes/setup-swagger.js';
16
16
  export { searchEngine } from './lib/engine/search/search.js';
17
17
  export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
18
18
  export { default as searchRoutes } from './routes/auto/search.routes.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1E,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACxH,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAGxF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAIlH,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC5H,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGlF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGlF,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGtE,YAAY,EACV,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,SAAS,EACT,oBAAoB,EACpB,SAAS,EACT,QAAQ,EACR,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,MAAM,EACN,cAAc,EACd,QAAQ,EACR,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { SqlApi, createSqlApi } from './lib/sql-api.js';
21
21
  // Plugin
22
22
  export { default as fastifyAutoSqlApi } from './routes/auto/plugin.js';
23
23
  // Swagger
24
- export { setupSwagger } from './lib/setup-swagger.js';
24
+ export { setupSwagger } from './routes/setup-swagger.js';
25
25
  // Search
26
26
  export { searchEngine } from './lib/engine/search/search.js';
27
27
  export { SearchTableBodyPost, SearchTableQuery, SearchTableQueryString, SearchTableResponse } from './lib/schema/search.js';
@@ -7,6 +7,8 @@ interface ManifestField {
7
7
  nullable?: boolean;
8
8
  /** Present (true) when the field is writable but never returned by reads (readExclude). */
9
9
  writeOnly?: boolean;
10
+ /** Present (true) when the field is returned by reads but refused by every write. */
11
+ readOnly?: boolean;
10
12
  }
11
13
  interface ManifestJoin {
12
14
  alias: string;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOvF,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACtC,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACvC;AAgCD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAInE;AA0DD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAMpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAkBrE"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAQvF,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACtC,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACvC;AAgCD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAInE;AA4DD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAMpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAkBrE"}
@@ -1,4 +1,5 @@
1
1
  import { isCompositePrimary } from '../../types.js';
2
+ import { writeExcludedFields } from '../write-access.js';
2
3
  const ALL_OPERATIONS = [
3
4
  'search', 'get', 'insert', 'update', 'delete', 'bulkUpsert', 'bulkDelete',
4
5
  ];
@@ -45,6 +46,7 @@ function manifestFields(tableConf) {
45
46
  const schema = tableConf.Schema;
46
47
  const required = new Set((schema.validation.required) ?? []);
47
48
  const readExcluded = new Set(tableConf.readExclude ?? []);
49
+ const writeExcluded = writeExcludedFields(tableConf, schema);
48
50
  const fields = {};
49
51
  for (const [name, fieldSchema] of Object.entries(schema.fields)) {
50
52
  const f = { type: shortType(fieldSchema) };
@@ -54,6 +56,8 @@ function manifestFields(tableConf) {
54
56
  f.nullable = true;
55
57
  if (readExcluded.has(name))
56
58
  f.writeOnly = true;
59
+ if (writeExcluded.has(name))
60
+ f.readOnly = true;
57
61
  fields[name] = f;
58
62
  }
59
63
  return fields;
@@ -1 +1 @@
1
- {"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoCD,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAkDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CA4BvB"}
1
+ {"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsCD,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAuDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAkCvB"}
@@ -65,14 +65,21 @@ function mapMysqlType(dataType) {
65
65
  return 'varchar';
66
66
  }
67
67
  }
68
+ /** `EXTRA`, lowercased — MySQL and MariaDB disagree on the case of both key and value. */
69
+ function extra(row) {
70
+ return String(row.EXTRA || row.extra || '').toLowerCase();
71
+ }
68
72
  export async function introspectMysqlTables(connectionConfig, schema) {
69
73
  const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
70
74
  const connection = await mysql2.createConnection(connectionConfig);
71
75
  try {
72
- const [rows] = await connection.query(`SELECT table_name, column_name, data_type, column_default, is_nullable, column_key, extra
73
- FROM information_schema.columns
74
- WHERE table_schema = ?
75
- ORDER BY table_name, ordinal_position`, [schema]);
76
+ const [rows] = await connection.query(`SELECT c.table_name, c.column_name, c.data_type, c.column_default, c.is_nullable,
77
+ c.column_key, c.extra, t.table_type
78
+ FROM information_schema.columns c
79
+ JOIN information_schema.tables t
80
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
81
+ WHERE c.table_schema = ?
82
+ ORDER BY c.table_name, c.ordinal_position`, [schema]);
76
83
  // `||` is kept as-is from the untyped version: this change types the driver, it does
77
84
  // not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
78
85
  // hold `undefined`.
@@ -83,7 +90,10 @@ export async function introspectMysqlTables(connectionConfig, schema) {
83
90
  column_default: row.COLUMN_DEFAULT || row.column_default || null,
84
91
  is_nullable: row.IS_NULLABLE || row.is_nullable || '',
85
92
  is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
86
- is_auto_increment: String(row.EXTRA || row.extra || '').includes('auto_increment'),
93
+ is_auto_increment: extra(row).includes('auto_increment'),
94
+ // 'VIRTUAL GENERATED' / 'STORED GENERATED': computed from an expression, never writable.
95
+ is_generated: extra(row).includes('generated'),
96
+ is_view: (row.TABLE_TYPE || row.table_type) === 'VIEW',
87
97
  }));
88
98
  }
89
99
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CA6BvB"}
1
+ {"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAmBjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAcD,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAmEvB"}
@@ -15,20 +15,59 @@ export async function introspectTables(connectionString, schema) {
15
15
  const client = new pg.Client({ connectionString });
16
16
  try {
17
17
  await client.connect();
18
- const result = await client.query(`SELECT c.table_name, c.column_name, c.udt_name, c.column_default, c.is_nullable,
19
- (pk.column_name IS NOT NULL) AS is_primary
20
- FROM information_schema.columns c
21
- LEFT JOIN (
22
- SELECT kcu.table_name, kcu.column_name
23
- FROM information_schema.table_constraints tc
24
- JOIN information_schema.key_column_usage kcu
25
- ON tc.constraint_name = kcu.constraint_name
26
- AND tc.table_schema = kcu.table_schema
27
- WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
28
- ) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
29
- WHERE c.table_schema = $1
30
- ORDER BY c.table_name, c.ordinal_position`, [schema]);
31
- return result.rows;
18
+ const result = await client.query(
19
+ // The UNION picks up materialized views, which `information_schema` does not describe at
20
+ // all: they live only in `pg_catalog`, and without this half they were silently absent
21
+ // from every generation run. Their columns are cast to the same shapes the standard view
22
+ // returns, so the two halves stay union-compatible.
23
+ `SELECT * FROM (
24
+ SELECT c.table_name::text, c.column_name::text, c.udt_name::text,
25
+ c.column_default::text, c.is_nullable::text,
26
+ c.is_identity::text, c.is_generated::text,
27
+ (pk.column_name IS NOT NULL) AS is_primary,
28
+ (t.table_type = 'VIEW') AS is_view,
29
+ c.ordinal_position::int AS ordinal_position
30
+ FROM information_schema.columns c
31
+ JOIN information_schema.tables t
32
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
33
+ LEFT JOIN (
34
+ SELECT kcu.table_name, kcu.column_name
35
+ FROM information_schema.table_constraints tc
36
+ JOIN information_schema.key_column_usage kcu
37
+ ON tc.constraint_name = kcu.constraint_name
38
+ AND tc.table_schema = kcu.table_schema
39
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1
40
+ ) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
41
+ WHERE c.table_schema = $1
42
+ UNION ALL
43
+ SELECT mc.relname::text, ma.attname::text, mt.typname::text,
44
+ NULL::text, CASE WHEN ma.attnotnull THEN 'NO' ELSE 'YES' END,
45
+ 'NO'::text, 'NEVER'::text,
46
+ false, true, ma.attnum::int
47
+ FROM pg_class mc
48
+ JOIN pg_namespace mn ON mn.oid = mc.relnamespace
49
+ JOIN pg_attribute ma
50
+ ON ma.attrelid = mc.oid AND ma.attnum > 0 AND NOT ma.attisdropped
51
+ JOIN pg_type mt ON mt.oid = ma.atttypid
52
+ WHERE mc.relkind = 'm' AND mn.nspname = $1
53
+ ) cols
54
+ ORDER BY table_name, ordinal_position`, [schema]);
55
+ // The query's own columns are spelled out rather than spread: `is_identity` and
56
+ // `is_generated` arrive as the standard's string enums and `ordinal_position` only exists
57
+ // to order the union, so none of the three belongs in the `ColumnInfo` handed back.
58
+ return result.rows.map((row) => ({
59
+ table_name: row.table_name,
60
+ column_name: row.column_name,
61
+ udt_name: row.udt_name,
62
+ column_default: row.column_default,
63
+ is_nullable: row.is_nullable,
64
+ is_primary: row.is_primary,
65
+ is_view: row.is_view,
66
+ // An identity column has no `column_default`, so without this flag it is emitted as a
67
+ // mandatory field and the table cannot be inserted into at all.
68
+ ...(row.is_identity === 'YES' ? { is_auto_increment: true } : {}),
69
+ ...(row.is_generated === 'ALWAYS' ? { is_generated: true } : {}),
70
+ }));
32
71
  }
33
72
  finally {
34
73
  await client.end();
@@ -6,6 +6,7 @@ export declare function convertColType(udtName: string, columnInfo: {
6
6
  column_default: string | null;
7
7
  is_nullable: string;
8
8
  is_auto_increment?: boolean;
9
+ is_generated?: boolean;
9
10
  }): string;
10
11
  export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
11
12
  /**
@@ -15,5 +16,5 @@ export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
15
16
  * Returns the names of the excluded tables, in map order.
16
17
  */
17
18
  export declare function applyExcludeTables(tableMap: TableMap, excludeTables?: string[]): string[];
18
- export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[]): string;
19
+ export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>, primary?: string[], generated?: string[], isView?: boolean): string;
19
20
  //# sourceMappingURL=schema-codegen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,MAAM,CA0DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqC1D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAoBzF;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,GACjB,MAAM,CA8CR"}
1
+ {"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,MAAM,CA6DR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CA0C1D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAoBzF;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,EAClB,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,GACf,MAAM,CAoDR"}
@@ -3,8 +3,10 @@ import { toCamelCase, toSchemaName } from '../naming.js';
3
3
  * @testonly Exported only so unit tests can exercise it directly.
4
4
  */
5
5
  export function convertColType(udtName, columnInfo) {
6
- // AUTO_INCREMENT (mysql) behaves like a default: the DB generates the value.
7
- const hasDefault = columnInfo.column_default || columnInfo.is_auto_increment;
6
+ // AUTO_INCREMENT (mysql) and IDENTITY (postgres) behave like a default: the DB generates
7
+ // the value. A generated column goes further — it refuses one — so it can never be a
8
+ // mandatory field either, whatever its nullability says.
9
+ const hasDefault = columnInfo.column_default || columnInfo.is_auto_increment || columnInfo.is_generated;
8
10
  const isNullable = columnInfo.is_nullable === 'YES';
9
11
  const wrap = (t) => {
10
12
  // Nullable columns need the type-array form (`Nullable(T)` → type: [..., 'null']):
@@ -69,6 +71,8 @@ export function buildTableMap(rows) {
69
71
  fields: {},
70
72
  colMap: {},
71
73
  primary: [],
74
+ generated: [],
75
+ isView: row.is_view === true,
72
76
  };
73
77
  }
74
78
  const fieldName = toCamelCase(row.column_name);
@@ -85,6 +89,9 @@ export function buildTableMap(rows) {
85
89
  if (row.is_primary) {
86
90
  acc[schemaName].primary.push(fieldName);
87
91
  }
92
+ if (row.is_generated) {
93
+ acc[schemaName].generated.push(fieldName);
94
+ }
88
95
  return acc;
89
96
  }, {});
90
97
  }
@@ -113,10 +120,16 @@ export function applyExcludeTables(tableMap, excludeTables) {
113
120
  }
114
121
  return excluded;
115
122
  }
116
- export function generateSchemaFile(schemaName, tableName, fields, colMap, primary) {
123
+ export function generateSchemaFile(schemaName, tableName, fields, colMap, primary, generated, isView) {
117
124
  const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
118
125
  const colMapStr = JSON.stringify(colMap, null, 2);
119
126
  const primaryKeyLine = primary?.length ? `\n primaryKey: ${JSON.stringify(primary)},` : '';
127
+ // Published so the table generator — and the runtime — know which columns the database
128
+ // computes: those are readable but reject every write.
129
+ const generatedLine = generated?.length ? `\n generatedFields: ${JSON.stringify(generated)},` : '';
130
+ // A view has no PRIMARY KEY constraint to introspect, so the table generator has to be told
131
+ // it may not read the absence of one as "no key exists on this data".
132
+ const isViewLine = isView ? `\n isView: true,` : '';
120
133
  const usesNullable = Object.values(fields).some((t) => t.includes('Nullable('));
121
134
  const valueImports = usesNullable ? 'Type, Nullable' : 'Type';
122
135
  return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
@@ -144,7 +157,7 @@ export const Schema = {
144
157
  fields: _Schema,
145
158
  validation,
146
159
  tableName: "${tableName}",
147
- partialValidation,${primaryKeyLine}
160
+ partialValidation,${primaryKeyLine}${generatedLine}${isViewLine}
148
161
  };
149
162
  export const ${schemaName} = Schema;
150
163
  export type TypeSchema = Static<typeof Schema.validation>;
@@ -5,6 +5,10 @@ export interface ParsedSchema {
5
5
  fieldTypes: Record<string, string>;
6
6
  /** PRIMARY KEY fields declared in the schema file (`primaryKey: [...]`), when present. */
7
7
  primary?: string[];
8
+ /** Database-computed fields declared in the schema file (`generatedFields: [...]`). */
9
+ generated?: string[];
10
+ /** True when the schema file declares `isView: true` — the relation is a view. */
11
+ isView?: boolean;
8
12
  }
9
13
  interface DetectedRelation {
10
14
  parentSchemaName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,UAAU,gBAAgB;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuCpE;AAyCD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAyED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAgFhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AA8CD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAoClE"}
1
+ {"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoCpE;AA4GD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAgID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAwFhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAqDD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAoClE"}
@@ -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
  }