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
@@ -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';
@@ -34,5 +34,5 @@ export { BulkDeleteTableBody, BulkDeleteTableResponse } from './lib/schema/bulk-
34
34
  export { default as bulkDeleteRoutes } from './routes/auto/bulk-delete.routes.js';
35
35
  export { Type, type Static } from '@sinclair/typebox';
36
36
  export { ConditionBuilder, Expression } from 'node-condition-builder';
37
- export type { SqlApiConfig, ColumnInfo, TableMap, Queryable, TransactionQueryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, TableOperation, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, ComputedFieldFn, ComputedFieldContext, ComputedFieldExpr, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
37
+ export type { ApiRequest, SqlApiConfig, ColumnInfo, TableMap, Queryable, TransactionQueryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, TableOperation, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, ComputedFieldFn, ComputedFieldContext, ComputedFieldExpr, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
38
38
  //# sourceMappingURL=index.d.ts.map
@@ -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":"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"}
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;
@@ -47,7 +49,8 @@ export declare function tableOperations(tableConf: ITable): TableOperation[];
47
49
  export declare function buildAgentManifest(dbTables: DbTables): AgentManifest;
48
50
  /**
49
51
  * Compact markdown rendering of the manifest, designed for an LLM system prompt.
50
- * Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
52
+ * Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads,
53
+ * `(readOnly)` returned by reads and refused by every write.
51
54
  */
52
55
  export declare function renderAgentManifestMd(manifest: AgentManifest): string;
53
56
  export {};
@@ -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;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAmBrE"}
@@ -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;
@@ -115,13 +119,15 @@ export function buildAgentManifest(dbTables) {
115
119
  }
116
120
  /**
117
121
  * Compact markdown rendering of the manifest, designed for an LLM system prompt.
118
- * Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
122
+ * Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads,
123
+ * `(readOnly)` returned by reads and refused by every write.
119
124
  */
120
125
  export function renderAgentManifestMd(manifest) {
121
126
  const lines = [
122
127
  '# Tables',
123
128
  '',
124
- 'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads.',
129
+ 'Notation: `!`=required on insert, `?`=nullable, `(writeOnly)`=writable, never in reads, ' +
130
+ '`(readOnly)`=returned by reads, refused by every write (omit it from write payloads).',
125
131
  'serverGenerated fields: omit on insert (values from client are ignored).',
126
132
  'readJoins aliases: `1:N` → joinMustExist/joinMultiple/joinGroup; `N:1` → joinLeft.',
127
133
  'writeJoins aliases: keys for `secondaries`/`deletions` in writes.',
@@ -138,7 +144,11 @@ export function renderAgentManifestMd(manifest) {
138
144
  function namedTypes(map) {
139
145
  return Object.entries(map).map(([n, ty]) => `${n}:${ty}`).join(', ');
140
146
  }
141
- /** A field's type with its notation suffixes: `!` required, `?` nullable, `(writeOnly)`. */
147
+ /**
148
+ * A field's type with its notation suffixes: `!` required, `?` nullable, `(writeOnly)`,
149
+ * `(readOnly)`. The two exclusion suffixes are mutually exclusive — `defineTable` rejects a
150
+ * field that is both — so at most one of them is ever appended.
151
+ */
142
152
  function fieldNotation(f) {
143
153
  let s = f.type;
144
154
  if (f.required)
@@ -147,6 +157,8 @@ function fieldNotation(f) {
147
157
  s += '?';
148
158
  if (f.writeOnly)
149
159
  s += '(writeOnly)';
160
+ if (f.readOnly)
161
+ s += '(readOnly)';
150
162
  return s;
151
163
  }
152
164
  function renderTableMd(t) {
@@ -6,6 +6,34 @@ export interface MysqlConnectionConfig {
6
6
  password: string;
7
7
  database: string;
8
8
  }
9
+ /** information_schema.columns row. MySQL and MariaDB differ on column-name case. */
10
+ interface MysqlColumnRow {
11
+ TABLE_NAME?: string;
12
+ table_name?: string;
13
+ COLUMN_NAME?: string;
14
+ column_name?: string;
15
+ DATA_TYPE?: string;
16
+ data_type?: string;
17
+ COLUMN_DEFAULT?: string | null;
18
+ column_default?: string | null;
19
+ IS_NULLABLE?: string;
20
+ is_nullable?: string;
21
+ COLUMN_KEY?: string;
22
+ column_key?: string;
23
+ EXTRA?: string;
24
+ extra?: string;
25
+ GENERATION_EXPRESSION?: string | null;
26
+ generation_expression?: string | null;
27
+ TABLE_TYPE?: string;
28
+ table_type?: string;
29
+ }
9
30
  export declare function buildMysqlConnectionConfig(): MysqlConnectionConfig;
31
+ /**
32
+ * One `information_schema.columns` row as the generator sees it.
33
+ *
34
+ * @testonly Exported only so unit tests can exercise it directly.
35
+ */
36
+ export declare function mapMysqlColumnRow(row: MysqlColumnRow): ColumnInfo;
10
37
  export declare function introspectMysqlTables(connectionConfig: MysqlConnectionConfig, schema: string): Promise<ColumnInfo[]>;
38
+ export {};
11
39
  //# sourceMappingURL=mysql-introspect.d.ts.map
@@ -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;AAkBD,oFAAoF;AACpF,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,0BAA0B,IAAI,qBAAqB,CAmBlE;AAyED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,CAejE;AAED,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE,qBAAqB,EACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB"}
@@ -65,26 +65,59 @@ 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
+ }
72
+ /** The two `EXTRA` markers that mean a real `GENERATED ALWAYS AS (...)` column. */
73
+ const GENERATED_EXTRA = /\b(?:virtual|stored|persistent) generated\b/;
74
+ /**
75
+ * True only for a column the database computes from an expression — never writable.
76
+ *
77
+ * `EXTRA` cannot be matched on the word 'generated': MySQL writes `DEFAULT_GENERATED` there
78
+ * for any column with a default *expression*, `DEFAULT CURRENT_TIMESTAMP` included, so a
79
+ * substring match takes every such timestamp column out of the write bodies. Only
80
+ * `GENERATION_EXPRESSION` separates the two, and it is non-empty for generated columns alone.
81
+ * `EXTRA` stays as a fallback, matched on the exact markers, for a driver or an older server
82
+ * that does not return the expression.
83
+ */
84
+ function isGenerated(row) {
85
+ const expression = String(row.GENERATION_EXPRESSION ?? row.generation_expression ?? '').trim();
86
+ return expression !== '' || GENERATED_EXTRA.test(extra(row));
87
+ }
88
+ /**
89
+ * One `information_schema.columns` row as the generator sees it.
90
+ *
91
+ * @testonly Exported only so unit tests can exercise it directly.
92
+ */
93
+ export function mapMysqlColumnRow(row) {
94
+ // `||` is kept as-is from the untyped version: this change types the driver, it does
95
+ // not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
96
+ // hold `undefined`.
97
+ return {
98
+ table_name: row.TABLE_NAME || row.table_name || '',
99
+ column_name: row.COLUMN_NAME || row.column_name || '',
100
+ udt_name: mapMysqlType(row.DATA_TYPE || row.data_type || ''),
101
+ column_default: row.COLUMN_DEFAULT || row.column_default || null,
102
+ is_nullable: row.IS_NULLABLE || row.is_nullable || '',
103
+ is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
104
+ is_auto_increment: extra(row).includes('auto_increment'),
105
+ is_generated: isGenerated(row),
106
+ is_view: (row.TABLE_TYPE || row.table_type) === 'VIEW',
107
+ };
108
+ }
68
109
  export async function introspectMysqlTables(connectionConfig, schema) {
69
110
  const mysql2 = loadOptionalDependency('mysql2/promise', 'npm install mysql2');
70
111
  const connection = await mysql2.createConnection(connectionConfig);
71
112
  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
- // `||` is kept as-is from the untyped version: this change types the driver, it does
77
- // not alter behaviour. The trailing fallbacks only satisfy ColumnInfo, which cannot
78
- // hold `undefined`.
79
- return rows.map((row) => ({
80
- table_name: row.TABLE_NAME || row.table_name || '',
81
- column_name: row.COLUMN_NAME || row.column_name || '',
82
- udt_name: mapMysqlType(row.DATA_TYPE || row.data_type || ''),
83
- column_default: row.COLUMN_DEFAULT || row.column_default || null,
84
- is_nullable: row.IS_NULLABLE || row.is_nullable || '',
85
- is_primary: (row.COLUMN_KEY || row.column_key) === 'PRI',
86
- is_auto_increment: String(row.EXTRA || row.extra || '').includes('auto_increment'),
87
- }));
113
+ const [rows] = await connection.query(`SELECT c.table_name, c.column_name, c.data_type, c.column_default, c.is_nullable,
114
+ c.column_key, c.extra, c.generation_expression, t.table_type
115
+ FROM information_schema.columns c
116
+ JOIN information_schema.tables t
117
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
118
+ WHERE c.table_schema = ?
119
+ ORDER BY c.table_name, c.ordinal_position`, [schema]);
120
+ return rows.map(mapMysqlColumnRow);
88
121
  }
89
122
  finally {
90
123
  await connection.end();
@@ -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"}