@an5/orm 1.0.9 → 1.1.1

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 (57) hide show
  1. package/README.md +10 -3
  2. package/dist/config.d.ts +9 -1
  3. package/dist/config.js +13 -18
  4. package/dist/generator/src/api.d.ts +35 -0
  5. package/dist/generator/src/api.js +58 -0
  6. package/dist/generator/src/config.d.ts +162 -0
  7. package/dist/generator/src/config.js +354 -0
  8. package/dist/generator/src/dialect.d.ts +88 -0
  9. package/dist/generator/src/dialect.js +318 -0
  10. package/dist/generator/src/dotnet-generator.d.ts +8 -0
  11. package/dist/generator/src/dotnet-generator.js +282 -101
  12. package/dist/generator/src/field-types.d.ts +96 -0
  13. package/dist/generator/src/field-types.js +433 -0
  14. package/dist/generator/src/golang-generator.d.ts +36 -0
  15. package/dist/generator/src/golang-generator.js +1230 -0
  16. package/dist/generator/src/index.js +69 -36
  17. package/dist/generator/src/metadata-generator.js +2 -1
  18. package/dist/generator/src/parser.d.ts +28 -2
  19. package/dist/generator/src/parser.js +92 -90
  20. package/dist/generator/src/push-apply.d.ts +37 -0
  21. package/dist/generator/src/push-apply.js +109 -0
  22. package/dist/generator/src/push-schema.d.ts +93 -0
  23. package/dist/generator/src/push-schema.js +189 -0
  24. package/dist/generator/src/python-generator.d.ts +19 -1
  25. package/dist/generator/src/python-generator.js +147 -64
  26. package/dist/generator/src/rust-generator.d.ts +38 -0
  27. package/dist/generator/src/rust-generator.js +971 -0
  28. package/dist/generator/src/suggest.d.ts +17 -0
  29. package/dist/generator/src/suggest.js +45 -0
  30. package/dist/generator/src/type-kinds.d.ts +27 -0
  31. package/dist/generator/src/type-kinds.js +72 -0
  32. package/dist/generator/src/types.d.ts +33 -0
  33. package/dist/generator/src/types.js +39 -0
  34. package/dist/migration-core.d.ts +16 -1
  35. package/dist/migration-core.js +60 -21
  36. package/dist/orm.d.ts +147 -0
  37. package/dist/orm.js +1654 -0
  38. package/dist/provider-support.d.ts +27 -0
  39. package/dist/provider-support.js +50 -0
  40. package/dist/sql-utils.js +8 -3
  41. package/generator/src/api.ts +45 -0
  42. package/generator/src/config.ts +496 -0
  43. package/generator/src/dialect.ts +415 -0
  44. package/generator/src/dotnet-generator.ts +277 -88
  45. package/generator/src/field-types.ts +473 -0
  46. package/generator/src/golang-generator.ts +94 -68
  47. package/generator/src/index.ts +71 -36
  48. package/generator/src/metadata-generator.ts +2 -2
  49. package/generator/src/parser.ts +122 -90
  50. package/generator/src/push-apply.ts +148 -0
  51. package/generator/src/push-schema.ts +250 -0
  52. package/generator/src/python-generator.ts +118 -62
  53. package/generator/src/rust-generator.ts +974 -0
  54. package/generator/src/suggest.ts +41 -0
  55. package/generator/src/type-kinds.ts +78 -0
  56. package/generator/src/types.ts +48 -0
  57. package/package.json +26 -6
@@ -1,8 +1,13 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { Model, Field, Relation } from './types';
3
+ import { Model, Field, Relation, bracketedTableName, dottedTableName } from './types';
4
+ import { fieldKind } from './type-kinds';
5
+ import { Provider } from './field-types';
4
6
 
5
7
  export class GolangGenerator {
8
+ /** The database being generated for; decides types like `TIMESTAMP`. */
9
+ private provider: Provider | undefined;
10
+
6
11
  constructor(private outputDir: string) {
7
12
  if (!fs.existsSync(this.outputDir)) {
8
13
  fs.mkdirSync(this.outputDir, { recursive: true });
@@ -10,8 +15,16 @@ export class GolangGenerator {
10
15
  }
11
16
 
12
17
  public generate(models: Model[]) {
13
- // 1. Generate models.go - entity structs
14
- this.generateModelsGo(models);
18
+ // Remembered for the type mapping: `TIMESTAMP` is a rowversion on SQL Server and
19
+ // a point in time on every other provider.
20
+ this.provider = models[0]?.provider;
21
+
22
+ // 1. Generate per-model files like dotnet (<Model>.go) -
23
+ // entity struct + that model's WhereInput/OrderByInput/Args types.
24
+ // Same Go package, so no re-export shim is needed.
25
+ for (const model of models) {
26
+ this.generateModelFile(model);
27
+ }
15
28
 
16
29
  // 2. Generate client.go - ORM DbContext + TableClient with full type-safe API
17
30
  this.generateClientGo(models);
@@ -25,22 +38,28 @@ export class GolangGenerator {
25
38
 
26
39
  // ─── Type mapping ──────────────────────────────────────────────────────────
27
40
 
28
- private mapGoType(fieldType: string, isOptional: boolean): string {
29
- let goType = 'string';
30
- const lower = fieldType.toLowerCase();
31
-
32
- if (['int', 'integer', 'smallint', 'tinyint'].includes(lower)) {
33
- goType = 'int';
34
- } else if (['bigint', 'long'].includes(lower)) {
35
- goType = 'int64';
36
- } else if (['float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) {
37
- goType = 'float64';
38
- } else if (['bool', 'boolean', 'bit'].includes(lower)) {
39
- goType = 'bool';
40
- } else if (['datetime', 'datetime2', 'smalldatetime', 'date', 'datetimeoffset', 'timestamp'].includes(lower)) {
41
- goType = 'time.Time';
42
- } else if (['bytes', 'binary', 'varbinary'].includes(lower)) {
43
- goType = '[]byte';
41
+ /**
42
+ * The Go type for a field.
43
+ *
44
+ * Driven by the declared type, not the generated TypeScript one: that collapses
45
+ * `INT`, `FLOAT` and `DECIMAL` into `number`, which matched none of the branches
46
+ * below and made every numeric column a `string`.
47
+ */
48
+ private mapGoType(field: Field, isOptional: boolean): string {
49
+ const kind = fieldKind(field, this.provider);
50
+ let goType: string;
51
+ switch (kind) {
52
+ case 'int': goType = 'int'; break;
53
+ case 'bigint': goType = 'int64'; break;
54
+ case 'float': goType = 'float64'; break;
55
+ case 'bool': goType = 'bool'; break;
56
+ case 'date': goType = 'time.Time'; break;
57
+ case 'bytes': goType = '[]byte'; break;
58
+ case 'vector': goType = '[]float32'; break;
59
+ // A JSON column holds whatever the row put in it; string keeps any value
60
+ // representable, and json.RawMessage would need the import everywhere.
61
+ case 'json': goType = 'string'; break;
62
+ default: goType = 'string'; break;
44
63
  }
45
64
 
46
65
  if (isOptional && goType !== '[]byte') {
@@ -49,42 +68,56 @@ export class GolangGenerator {
49
68
  return goType;
50
69
  }
51
70
 
52
- // ─── models.go ─────────────────────────────────────────────────────────────
71
+ // ─── <Model>.go (per-model file, like dotnet `<Model>.cs`) ────────────────
53
72
 
54
- private generateModelsGo(models: Model[]) {
73
+ private generateModelFile(model: Model) {
74
+ // Single-model file: entity struct + its WhereInput/OrderByInput/Args
75
+ // types. All files share `package an5client`, so callers keep using
76
+ // `an5client.User` etc. regardless of file layout.
77
+ // NOTE: Go rejects unused imports, so `time` is only imported when a
78
+ // field actually maps to `time.Time`.
79
+ const needsTime = model.fields.some(f => this.mapGoType(f, false).includes('time.Time'));
55
80
  let content = `// Code generated by an5ORM. DO NOT EDIT.
56
81
  package an5client
57
82
 
58
- import (
83
+ `;
84
+ if (needsTime) {
85
+ content += `import (
59
86
  \t"time"
60
87
  )
61
88
 
62
89
  `;
63
- for (const model of models) {
64
- if (model.description) {
65
- content += `// ${model.name} - ${model.description}\n`;
66
- }
67
- content += `type ${model.name} struct {\n`;
90
+ }
91
+ content += this.generateModelStruct(model);
92
+ content += this.generateModelOrmTypes(model);
68
93
 
69
- for (const field of model.fields) {
70
- const goType = this.mapGoType(field.type, field.isOptional);
71
- const jsonTag = field.isOptional ? `${this.toCamelCase(field.name)},omitempty` : this.toCamelCase(field.name);
72
- const dbTag = this.toSnakeCase(field.name);
73
- content += `\t${this.capitalize(field.name)} ${goType} \`json:"${jsonTag}" db:"${dbTag}"\`\n`;
74
- }
94
+ fs.writeFileSync(path.join(this.outputDir, `${model.name}.go`), content);
95
+ }
75
96
 
76
- if (model.relations.length > 0) {
77
- content += `\n\t// Relations\n`;
78
- for (const rel of model.relations) {
79
- const relType = rel.isArray ? `[]${rel.type}` : `*${rel.type}`;
80
- content += `\t${this.capitalize(rel.name)} ${relType} \`json:"${this.toCamelCase(rel.name)},omitempty"\`\n`;
81
- }
82
- }
97
+ private generateModelStruct(model: Model): string {
98
+ let content = '';
99
+ if (model.description) {
100
+ content += `// ${model.name} - ${model.description}\n`;
101
+ }
102
+ content += `type ${model.name} struct {\n`;
83
103
 
84
- content += `}\n\n`;
104
+ for (const field of model.fields) {
105
+ const goType = this.mapGoType(field, field.isOptional);
106
+ const jsonTag = field.isOptional ? `${this.toCamelCase(field.name)},omitempty` : this.toCamelCase(field.name);
107
+ const dbTag = this.toSnakeCase(field.name);
108
+ content += `\t${this.capitalize(field.name)} ${goType} \`json:"${jsonTag}" db:"${dbTag}"\`\n`;
109
+ }
110
+
111
+ if (model.relations.length > 0) {
112
+ content += `\n\t// Relations\n`;
113
+ for (const rel of model.relations) {
114
+ const relType = rel.isArray ? `[]${rel.type}` : `*${rel.type}`;
115
+ content += `\t${this.capitalize(rel.name)} ${relType} \`json:"${this.toCamelCase(rel.name)},omitempty"\`\n`;
116
+ }
85
117
  }
86
118
 
87
- fs.writeFileSync(path.join(this.outputDir, 'models.go'), content);
119
+ content += `}\n\n`;
120
+ return content;
88
121
  }
89
122
 
90
123
  // ─── metadata.go ───────────────────────────────────────────────────────────
@@ -97,7 +130,7 @@ package an5client
97
130
  var ModelToTable = map[string]string{
98
131
  `;
99
132
  for (const model of models) {
100
- const fullTable = `[${model.schemaName}].[${model.tableName}]`;
133
+ const fullTable = bracketedTableName(model);
101
134
  for (const prop of this.getAllPropertyVariations(model.name)) {
102
135
  content += `\t"${prop}": "${fullTable}",\n`;
103
136
  }
@@ -158,11 +191,7 @@ func GetDefaultConnectionString() string {
158
191
  content += this.generateVectorSearch();
159
192
  content += this.generateHelpers();
160
193
 
161
- // Per-model ORM types (WhereInput, OrderByInput, Args structs)
162
- for (const model of models) {
163
- content += this.generateModelOrmTypes(model);
164
- }
165
-
194
+ // NOTE: per-model ORM types live in each `<Model>.go` file.
166
195
  fs.writeFileSync(path.join(this.outputDir, 'client.go'), content);
167
196
  }
168
197
 
@@ -267,7 +296,7 @@ func detectDialect(connStr string) Dialect {
267
296
  \tif strings.HasPrefix(l, "postgres://") || strings.HasPrefix(l, "postgresql://") || strings.Contains(l, "port=5432") {
268
297
  \t\treturn DialectPostgres
269
298
  \t}
270
- \tif strings.HasPrefix(l, "sqlite://") || strings.HasPrefix(l, "sqlite:") || strings.HasPrefix(l, "file:") || strings.HasSuffix(l, ".db") || strings.HasSuffix(l, ".sqlite") || strings.HasSuffix(l, ".sqlite3") || l == ":memory:" {
299
+ \tif l == "sqlite" || strings.HasPrefix(l, "sqlite://") || strings.HasPrefix(l, "sqlite:") || strings.HasPrefix(l, "file:") || strings.HasSuffix(l, ".db") || strings.HasSuffix(l, ".sqlite") || strings.HasSuffix(l, ".sqlite3") || l == ":memory:" {
271
300
  \t\treturn DialectSqlite
272
301
  \t}
273
302
  \treturn DialectMssql
@@ -309,7 +338,7 @@ func NewAn5DbContextWithConnStr(db *sql.DB, connStr string) *An5DbContext {
309
338
  `;
310
339
  for (const model of models) {
311
340
  const name = this.capitalize(model.name);
312
- content += `\tclient${name} := NewTableClient[${model.name}](db, "${model.schemaName}.${model.tableName}", d)\n`;
341
+ content += `\tclient${name} := NewTableClient[${model.name}](db, "${dottedTableName(model)}", d)\n`;
313
342
  content += `\tctx.${name}s = client${name}\n`;
314
343
  content += `\tctx.${name} = client${name}\n`;
315
344
  }
@@ -348,6 +377,7 @@ func (c *TableClient[T]) quoteName(name string) string {
348
377
  // quoteTable quotes a schema.table name for the dialect.
349
378
  func (c *TableClient[T]) quoteTable() string {
350
379
  \tparts := strings.Split(c.TableName, ".")
380
+ \tif c.Dialect == DialectSqlite && len(parts) == 2 && strings.EqualFold(parts[0], "dbo") { parts = parts[1:] }
351
381
  \tquoted := make([]string, len(parts))
352
382
  \tfor i, p := range parts {
353
383
  \t\tquoted[i] = c.quoteName(p)
@@ -889,14 +919,14 @@ func (c *TableClient[T]) buildWhereFromStruct(v reflect.Value, argOffset int) (s
889
919
  \t\t\tvar subParts []string
890
920
  \t\t\tfor j := 0; j < fv.Len(); j++ {
891
921
  \t\t\t\tsub, subArgs := c.buildWhereFromStruct(fv.Index(j), argOffset+len(args))
892
- \t\t\t\tif sub != "" { subParts = append(subParts, sub); args = append(args, subArgs...) }
922
+ \t\t\t\tif sub == "" { sub = "1=1" }; subParts = append(subParts, sub); args = append(args, subArgs...)
893
923
  \t\t\t}
894
- \t\t\tif len(subParts) > 0 { parts = append(parts, "("+strings.Join(subParts, " OR ")+")") }
924
+ \t\t\tif len(subParts) > 0 { parts = append(parts, "("+strings.Join(subParts, " OR ")+")") } else { parts = append(parts, "1=0") }
895
925
  \t\t\tcontinue
896
926
  \t\t}
897
927
  \t\tif name == "NOT" && fv.Kind() == reflect.Ptr && !fv.IsNil() {
898
928
  \t\t\tsub, subArgs := c.buildWhereFromStruct(fv.Elem(), argOffset+len(args))
899
- \t\t\tif sub != "" { parts = append(parts, "NOT ("+sub+")"); args = append(args, subArgs...) }
929
+ \t\t\tif sub == "" { sub = "1=1" }; parts = append(parts, "NOT ("+sub+")"); args = append(args, subArgs...)
900
930
  \t\t\tcontinue
901
931
  \t\t}
902
932
 
@@ -1136,7 +1166,7 @@ var _ = time.Time{}
1136
1166
  content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1137
1167
  for (const field of model.fields) {
1138
1168
  if (!field.isId) {
1139
- const goType = this.mapGoType(field.type, field.isOptional || field.hasDefault);
1169
+ const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1140
1170
  content += `\t${this.capitalize(field.name)} ${goType}\n`;
1141
1171
  }
1142
1172
  }
@@ -1146,7 +1176,7 @@ var _ = time.Time{}
1146
1176
  content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1147
1177
  for (const field of model.fields) {
1148
1178
  if (!field.isId) {
1149
- const goType = this.mapGoType(field.type, true); // always pointer so we can detect zero
1179
+ const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1150
1180
  content += `\t${this.capitalize(field.name)} ${goType}\n`;
1151
1181
  }
1152
1182
  }
@@ -1187,20 +1217,16 @@ var _ = time.Time{}
1187
1217
  }
1188
1218
 
1189
1219
  private getGoFilterType(field: Field): string {
1190
- const lower = field.type.toLowerCase();
1191
- if (['datetime', 'datetime2', 'smalldatetime', 'date', 'datetimeoffset', 'timestamp'].includes(lower)) {
1192
- return 'DateTimeFilter';
1193
- }
1194
- if (['bool', 'boolean', 'bit'].includes(lower)) {
1195
- return 'BoolFilter';
1196
- }
1197
- if (['int', 'integer', 'smallint', 'tinyint'].includes(lower)) {
1198
- return 'IntFilter';
1199
- }
1200
- if (['bigint', 'long', 'float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) {
1201
- return 'NumberFilter';
1220
+ // From the declared type, so a DECIMAL column gets the number filter and an INT
1221
+ // column the integer one; both used to arrive as `number` and pick the first.
1222
+ switch (fieldKind(field, this.provider)) {
1223
+ case 'date': return 'DateTimeFilter';
1224
+ case 'bool': return 'BoolFilter';
1225
+ case 'int':
1226
+ case 'bigint': return 'IntFilter';
1227
+ case 'float': return 'NumberFilter';
1228
+ default: return 'StringFilter';
1202
1229
  }
1203
- return 'StringFilter';
1204
1230
  }
1205
1231
 
1206
1232
  // ─── Utilities ─────────────────────────────────────────────────────────────
@@ -1,10 +1,13 @@
1
1
  import path from 'path';
2
+ import { loadConfig, providerFromConfig, ConfigError, formatIssues } from './config';
3
+ import { FieldTypeError, PROVIDER_LABELS } from './field-types';
2
4
  import { SchemaParser } from './parser';
3
5
  import { CodeGenerator } from './code-generator';
4
6
  import { MetadataGenerator } from './metadata-generator';
5
7
  import { PythonGenerator } from './python-generator';
6
8
  import { DotnetGenerator } from './dotnet-generator';
7
9
  import { GolangGenerator } from './golang-generator';
10
+ import { RustGenerator } from './rust-generator';
8
11
 
9
12
  import fs from 'fs';
10
13
 
@@ -24,52 +27,69 @@ function clearGeneratedFiles(outputDir: string, extension: string) {
24
27
  }
25
28
  }
26
29
 
30
+ function clearGeneratedPythonFiles(outputDir: string) {
31
+ if (!fs.existsSync(outputDir)) {
32
+ fs.mkdirSync(outputDir, { recursive: true });
33
+ return;
34
+ }
35
+
36
+ // Delete generated .py files but preserve hand-maintained adapter.
37
+ const preserve = new Set(['an5_adapter.py']);
38
+ for (const entry of fs.readdirSync(outputDir)) {
39
+ const fullPath = path.join(outputDir, entry);
40
+ const stat = fs.statSync(fullPath);
41
+ if (stat.isDirectory()) continue;
42
+ if (entry.endsWith('.py') && !preserve.has(entry)) {
43
+ fs.unlinkSync(fullPath);
44
+ }
45
+ }
46
+ }
47
+
27
48
  async function main() {
28
- let rootDir = process.cwd();
29
- let config: any = {};
30
-
49
+ // Loaded through the validating loader, so a mistyped key or a wrong type
50
+ // stops here instead of quietly generating somewhere else.
51
+ let config: ReturnType<typeof loadConfig>;
31
52
  try {
32
- let configPath = path.join(rootDir, 'an5Orm.config.js');
33
- if (!fs.existsSync(configPath)) {
34
- configPath = path.join(rootDir, 'an5Orm.config.cjs');
35
- }
36
- if (!fs.existsSync(configPath)) {
37
- configPath = path.join(rootDir, '..', 'an5Orm.config.js');
38
- }
39
- if (!fs.existsSync(configPath)) {
40
- configPath = path.join(rootDir, '..', 'an5Orm.config.cjs');
41
- }
42
- if (fs.existsSync(configPath)) {
43
- config = require(configPath);
44
- rootDir = path.dirname(configPath);
45
- } else if (fs.existsSync(path.join(rootDir, '..', 'an5Schema'))) {
46
- rootDir = path.resolve(rootDir, '..');
47
- }
53
+ config = loadConfig();
48
54
  } catch (err) {
55
+ if (err instanceof ConfigError) {
56
+ console.error(`❌ ${err.message}:`);
57
+ console.error(formatIssues(err.issues));
58
+ process.exit(1);
59
+ }
49
60
  console.warn('⚠️ Could not load an5Orm.config.js/.cjs, using defaults.', err);
61
+ config = loadConfig(path.join(process.cwd(), 'no-such-dir'));
50
62
  }
51
63
 
52
- const schemaDir = path.resolve(rootDir, config.schemaDir || 'an5Schema');
53
- const outputTypesDir = path.resolve(rootDir, config.outputs?.typescript?.outputDir || 'an5Client/typescript');
54
- const outputMetadataPath = path.resolve(rootDir, config.outputs?.typescript?.metadataFile || 'an5Client/typescript/an5Metadata.ts');
55
- const outputPythonMetadataPath = path.resolve(rootDir, config.outputs?.python?.metadataFile || 'an5Client/python/an5_metadata.py');
56
- const outputDotnetDir = path.resolve(rootDir, config.outputs?.dotnet?.outputDir || 'an5Client/dotnet');
57
- const outputGolangDir = path.resolve(rootDir, config.outputs?.golang?.outputDir || 'an5Client/golang');
64
+ const { rootDir } = config;
65
+ const schemaDir = config.outputs.schemaDir;
66
+ const outputTypesDir = config.outputs.typescriptDir;
67
+ const outputMetadataPath = config.outputs.typescriptMetadataFile;
68
+ const outputPythonMetadataPath = config.outputs.pythonMetadataFile;
69
+ const outputDotnetDir = config.outputs.dotnetDir;
70
+ const outputGolangDir = config.outputs.golangDir;
71
+ const outputRustDir = config.outputs.rustDir;
72
+ const generateMetadata = config.config.generation.generateMetadata;
58
73
 
59
74
  console.log('🚀 Starting ORM generation...');
60
75
 
76
+ // The target provider comes from the connection string: both the allowed
77
+ // field types and the SQL follow it, instead of one shared list.
78
+ const provider = providerFromConfig(config.config, process.env);
79
+ console.log(`🗄️ Provider: ${PROVIDER_LABELS[provider]} (${provider})`);
80
+
61
81
  try {
62
82
  clearGeneratedFiles(outputTypesDir, '.ts');
63
83
  clearGeneratedFiles(outputDotnetDir, '.cs');
64
84
  clearGeneratedFiles(outputGolangDir, '.go');
65
- if (fs.existsSync(outputMetadataPath)) {
85
+ clearGeneratedFiles(path.join(outputRustDir, 'src'), '.rs');
86
+ const pythonDirEarly = path.dirname(outputPythonMetadataPath);
87
+ clearGeneratedPythonFiles(pythonDirEarly);
88
+ if (config.config.generation.generateMetadata && fs.existsSync(outputMetadataPath)) {
66
89
  fs.unlinkSync(outputMetadataPath);
67
90
  }
68
- if (fs.existsSync(outputPythonMetadataPath)) {
69
- fs.unlinkSync(outputPythonMetadataPath);
70
- }
71
91
 
72
- const parser = new SchemaParser(schemaDir);
92
+ const parser = new SchemaParser(schemaDir, provider);
73
93
  const models = await parser.parse();
74
94
  console.log(`📦 Parsed ${models.length} models from schema.`);
75
95
 
@@ -77,13 +97,17 @@ async function main() {
77
97
  codeGen.generate(models);
78
98
  console.log(`✨ Generated modular types in ${outputTypesDir}`);
79
99
 
80
- const metadataDir = path.dirname(outputMetadataPath);
81
- if (!fs.existsSync(metadataDir)) {
82
- fs.mkdirSync(metadataDir, { recursive: true });
100
+ if (generateMetadata) {
101
+ const metadataDir = path.dirname(outputMetadataPath);
102
+ if (!fs.existsSync(metadataDir)) {
103
+ fs.mkdirSync(metadataDir, { recursive: true });
104
+ }
105
+ const metadataGen = new MetadataGenerator(outputMetadataPath);
106
+ metadataGen.generate(models);
107
+ console.log(`✨ Generated metadata in ${outputMetadataPath}`);
108
+ } else {
109
+ console.log('⏭ Skipping metadata (generation.generateMetadata is false)');
83
110
  }
84
- const metadataGen = new MetadataGenerator(outputMetadataPath);
85
- metadataGen.generate(models);
86
- console.log(`✨ Generated metadata in ${outputMetadataPath}`);
87
111
 
88
112
  const pythonDir = path.dirname(outputPythonMetadataPath);
89
113
  if (!fs.existsSync(pythonDir)) {
@@ -101,8 +125,19 @@ async function main() {
101
125
  golangGen.generate(models);
102
126
  console.log(`✨ Generated Golang models in ${outputGolangDir}`);
103
127
 
128
+ const rustGen = new RustGenerator(outputRustDir);
129
+ rustGen.generate(models);
130
+ console.log(`✨ Generated Rust client in ${outputRustDir}`);
131
+
104
132
  console.log('✅ ORM generation completed successfully.');
105
133
  } catch (error) {
134
+ // Print every bad field type with its provider, instead of the parser's
135
+ // nested error object.
136
+ if (error instanceof FieldTypeError) {
137
+ console.error(`❌ ${error.message}:`);
138
+ console.error(formatIssues(error.issues));
139
+ process.exit(1);
140
+ }
106
141
  console.error('❌ ORM generation failed:', error);
107
142
  process.exit(1);
108
143
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { Model } from './types';
3
+ import { Model, bracketedTableName } from './types';
4
4
 
5
5
  export class MetadataGenerator {
6
6
  constructor(
@@ -15,7 +15,7 @@ export class MetadataGenerator {
15
15
  metaContent += 'export const modelToTable: Record<string, string> = {\n';
16
16
  for (const model of models) {
17
17
  const props = this.getAllPropertyVariations(model.name);
18
- const fullTableName = `[${model.schemaName}].[${model.tableName}]`;
18
+ const fullTableName = bracketedTableName(model);
19
19
  for (const prop of props) {
20
20
  metaContent += ` ${prop}: "${fullTableName}",\n`;
21
21
  }