@an5/orm 1.0.8 → 1.1.0

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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -1,105 +1,428 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PythonGenerator = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- class PythonGenerator {
9
- constructor(outputPath) {
10
- this.outputPath = outputPath;
11
- }
12
- generate(models) {
13
- let pyContent = '# This file is auto-generated. Do not edit directly.\n\n';
14
- // 1. MODEL_TO_TABLE
15
- pyContent += 'MODEL_TO_TABLE = {\n';
16
- for (const model of models) {
17
- const props = this.getAllPropertyVariations(model.name);
18
- const fullTableName = `[${model.schemaName}].[${model.tableName}]`;
19
- for (const prop of props) {
20
- pyContent += ` "${prop}": "${fullTableName}",\n`;
21
- }
22
- }
23
- pyContent += '}\n\n';
24
- // 2. MODEL_DESCRIPTIONS
25
- pyContent += 'MODEL_DESCRIPTIONS = {\n';
26
- for (const model of models) {
27
- const props = this.getAllPropertyVariations(model.name);
28
- for (const prop of props) {
29
- pyContent += ` "${prop}": ${this.pyString(model.description)},\n`;
30
- }
31
- }
32
- pyContent += '}\n\n';
33
- // 3. MODEL_FIELDS
34
- pyContent += 'MODEL_FIELDS = {\n';
35
- for (const model of models) {
36
- const props = this.getAllPropertyVariations(model.name);
37
- const fieldsStr = this.formatFields(model);
38
- for (const prop of props) {
39
- pyContent += ` "${prop}": ${fieldsStr},\n`;
40
- }
41
- }
42
- pyContent += '}\n\n';
43
- // 4. RELATION_MAP
44
- pyContent += 'RELATION_MAP = {\n';
45
- for (const model of models) {
46
- const props = this.getAllPropertyVariations(model.name);
47
- let relationsContent = `{\n`;
48
- for (const rel of model.relations) {
49
- relationsContent += ` "${rel.name}": {\n`;
50
- relationsContent += ` "modelName": "${this.toCamelCase(rel.type)}",\n`;
51
- relationsContent += ` "relationType": "${rel.isArray ? 'many' : 'one'}",\n`;
52
- relationsContent += ` "foreignKey": "${rel.foreignKey || 'id'}",\n`;
53
- relationsContent += ` "localKey": "${rel.localKey || 'id'}"\n`;
54
- relationsContent += ` },\n`;
55
- }
56
- relationsContent += ` }`;
57
- for (const prop of props) {
58
- pyContent += ` "${prop}": ${relationsContent},\n`;
59
- }
60
- }
61
- pyContent += '}\n';
62
- fs_1.default.writeFileSync(this.outputPath, pyContent);
63
- }
64
- getAllPropertyVariations(modelName) {
65
- const variations = new Set();
66
- // 1. camelCase (User -> user, McpServer -> mcpServer)
67
- variations.add(this.toCamelCase(modelName));
68
- // 2. snake_case (User -> user, McpServer -> mcp_server)
69
- variations.add(this.toSnakeCase(modelName));
70
- const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
71
- for (const acronym of acronyms) {
72
- if (modelName.startsWith(acronym)) {
73
- variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
74
- variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
75
- variations.add(this.toSnakeCase(acronym.toLowerCase() + modelName.slice(acronym.length)));
76
- }
77
- }
78
- return Array.from(variations);
79
- }
80
- formatFields(model) {
81
- if (model.fields.length === 0)
82
- return '[]';
83
- const fields = model.fields.map(f => {
84
- return ` { "name": "${f.name}", "type": "${f.type}${f.isOptional ? '?' : ''}", "sql": "${f.sqlType}", "isOptional": ${f.isOptional ? 'True' : 'False'}, "hasDefault": ${f.hasDefault ? 'True' : 'False'}, "isId": ${f.isId ? 'True' : 'False'}, "description": ${this.pyString(f.description)} }`;
85
- });
86
- return `[\n${fields.join(',\n')}\n ]`;
87
- }
88
- pyString(value) {
89
- return value ? JSON.stringify(value) : 'None';
90
- }
91
- toCamelCase(str) {
92
- if (!str)
93
- return '';
94
- return str[0].toLowerCase() + str.slice(1);
95
- }
96
- toSnakeCase(str) {
97
- if (!str)
98
- return '';
99
- return str
100
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
101
- .replace(/([a-z\d])([A-Z])/g, '$1_$2')
102
- .toLowerCase();
103
- }
104
- }
105
- exports.PythonGenerator = PythonGenerator;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PythonGenerator = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const types_1 = require("./types");
10
+ const type_kinds_1 = require("./type-kinds");
11
+ class PythonGenerator {
12
+ constructor(outputPath) {
13
+ this.outputPath = outputPath;
14
+ }
15
+ /**
16
+ * The metadata module name, taken from the configured file name itself.
17
+ *
18
+ * The imports used to hardcode `an5_metadata` while `outputPath` was
19
+ * configurable, so setting `python.metadataFile` to another name (say
20
+ * `an5Metadata.py`) still generated `from an5_metadata import ...` and the
21
+ * client broke on import — a failure that only showed up at runtime.
22
+ */
23
+ metadataModule() {
24
+ return path_1.default.basename(this.outputPath).replace(/\.py$/i, '');
25
+ }
26
+ generate(models) {
27
+ this.provider = models[0]?.provider;
28
+ const outputDir = path_1.default.dirname(this.outputPath);
29
+ if (!fs_1.default.existsSync(outputDir)) {
30
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
31
+ }
32
+ // 1. Generate an5_metadata.py
33
+ this.generateMetadata(models);
34
+ // 2. Generate per-model entity files like dotnet (<Model>.py)
35
+ for (const model of models) {
36
+ this.generateModelFile(model, outputDir);
37
+ }
38
+ // 3. Generate an5_models.py as backward-compat aggregator
39
+ this.generateModelsIndex(models, outputDir);
40
+ // 4. Generate an5_orm_types.py - type-safe ORM filter/args dataclasses
41
+ this.generateOrmTypes(models, outputDir);
42
+ // 5. Generate an5_client.py
43
+ this.generateClient(models, outputDir);
44
+ // 6. Generate __init__.py
45
+ this.generateInit(models, outputDir);
46
+ }
47
+ getPyFilterType(field) {
48
+ switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
49
+ case 'date': return 'DateTimeFilter';
50
+ case 'bool': return 'BoolFilter';
51
+ case 'int':
52
+ case 'bigint': return 'IntFilter';
53
+ case 'float': return 'NumberFilter';
54
+ default: return 'StringFilter';
55
+ }
56
+ }
57
+ generateOrmTypes(models, outputDir) {
58
+ let content = '# This file is auto-generated. Do not edit directly.\n';
59
+ content += '"""\nAN5 ORM typed filter/args dataclasses for type-safe queries.\n\nUsage example::\n\n db.user.find_many(where=UserWhereInput(name=StringFilter(contains="John")),\n order_by=UserOrderByInput(created_at="desc"),\n take=10)\n"""\n';
60
+ content += 'from __future__ import annotations\n';
61
+ content += 'from dataclasses import dataclass, field\n';
62
+ content += 'from typing import List, Optional, Any\n';
63
+ content += 'from datetime import datetime\n\n';
64
+ // Base filter types
65
+ content += '# ─── Base filter types ────────────────────────────────────────────────────────\n\n';
66
+ content += `@dataclass\nclass StringFilter:\n """Type-safe filter for string fields."""\n equals: Optional[str] = None\n not_: Optional[str] = None\n contains: Optional[str] = None\n starts_with: Optional[str] = None\n ends_with: Optional[str] = None\n in_: Optional[List[str]] = None\n not_in: Optional[List[str]] = None\n gt: Optional[str] = None\n gte: Optional[str] = None\n lt: Optional[str] = None\n lte: Optional[str] = None\n\n`;
67
+ content += `@dataclass\nclass IntFilter:\n """Type-safe filter for integer fields."""\n equals: Optional[int] = None\n not_: Optional[int] = None\n in_: Optional[List[int]] = None\n not_in: Optional[List[int]] = None\n gt: Optional[int] = None\n gte: Optional[int] = None\n lt: Optional[int] = None\n lte: Optional[int] = None\n\n`;
68
+ content += `@dataclass\nclass NumberFilter:\n """Type-safe filter for float/decimal fields."""\n equals: Optional[float] = None\n not_: Optional[float] = None\n in_: Optional[List[float]] = None\n not_in: Optional[List[float]] = None\n gt: Optional[float] = None\n gte: Optional[float] = None\n lt: Optional[float] = None\n lte: Optional[float] = None\n\n`;
69
+ content += `@dataclass\nclass BoolFilter:\n """Type-safe filter for boolean fields."""\n equals: Optional[bool] = None\n\n`;
70
+ content += `@dataclass\nclass DateTimeFilter:\n """Type-safe filter for datetime fields."""\n equals: Optional[datetime] = None\n not_: Optional[datetime] = None\n in_: Optional[List[datetime]] = None\n not_in: Optional[List[datetime]] = None\n gt: Optional[datetime] = None\n gte: Optional[datetime] = None\n lt: Optional[datetime] = None\n lte: Optional[datetime] = None\n\n`;
71
+ // Per-model types
72
+ for (const model of models) {
73
+ const name = this.capitalize(model.name);
74
+ content += `# ─── ${name} ORM Types ────────────────────────────────────────────────────────\n\n`;
75
+ // WhereInput
76
+ content += `@dataclass\nclass ${name}WhereInput:\n """Type-safe WHERE filter for ${name} queries."""\n`;
77
+ content += ` AND: Optional[List['${name}WhereInput']] = None\n`;
78
+ content += ` OR: Optional[List['${name}WhereInput']] = None\n`;
79
+ content += ` NOT: Optional['${name}WhereInput'] = None\n`;
80
+ for (const f of model.fields) {
81
+ const ft = this.getPyFilterType(f);
82
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${ft}] = None\n`;
83
+ }
84
+ content += '\n';
85
+ // OrderByInput
86
+ content += `@dataclass\nclass ${name}OrderByInput:\n """Type-safe ORDER BY for ${name} queries. Value: 'asc' or 'desc'."""\n`;
87
+ for (const f of model.fields) {
88
+ content += ` ${this.toSnakeCase(f.name)}: Optional[str] = None\n`;
89
+ }
90
+ content += '\n';
91
+ // CreateInput
92
+ const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
93
+ const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
94
+ content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
95
+ for (const f of requiredFields) {
96
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
97
+ }
98
+ for (const f of optionalFields) {
99
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
100
+ }
101
+ if (requiredFields.length === 0 && optionalFields.length === 0)
102
+ content += ' pass\n';
103
+ content += '\n';
104
+ // UpdateInput
105
+ const updateFields = model.fields.filter(f => !f.isId);
106
+ content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
107
+ for (const f of updateFields) {
108
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
109
+ }
110
+ if (updateFields.length === 0)
111
+ content += ' pass\n';
112
+ content += '\n';
113
+ // FindManyArgs
114
+ content += `@dataclass\nclass ${name}FindManyArgs:\n """ORM-style args for ${name}.find_many()."""\n`;
115
+ content += ` where: Optional[${name}WhereInput] = None\n`;
116
+ content += ` order_by: Optional[${name}OrderByInput] = None\n`;
117
+ content += ` take: Optional[int] = None\n`;
118
+ content += ` skip: int = 0\n`;
119
+ content += ` select: Optional[List[str]] = None\n\n`;
120
+ // FindFirstArgs
121
+ content += `@dataclass\nclass ${name}FindFirstArgs:\n """ORM-style args for ${name}.find_first()."""\n`;
122
+ content += ` where: Optional[${name}WhereInput] = None\n`;
123
+ content += ` order_by: Optional[${name}OrderByInput] = None\n`;
124
+ content += ` select: Optional[List[str]] = None\n\n`;
125
+ }
126
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_orm_types.py'), content);
127
+ }
128
+ generateMetadata(models) {
129
+ let pyContent = '# This file is auto-generated. Do not edit directly.\n\n';
130
+ // 1. MODEL_TO_TABLE
131
+ pyContent += 'MODEL_TO_TABLE = {\n';
132
+ for (const model of models) {
133
+ const props = this.getAllPropertyVariations(model.name);
134
+ const fullTableName = (0, types_1.bracketedTableName)(model);
135
+ for (const prop of props) {
136
+ pyContent += ` "${prop}": "${fullTableName}",\n`;
137
+ }
138
+ }
139
+ pyContent += '}\n\n';
140
+ // 2. MODEL_DESCRIPTIONS
141
+ pyContent += 'MODEL_DESCRIPTIONS = {\n';
142
+ for (const model of models) {
143
+ const props = this.getAllPropertyVariations(model.name);
144
+ for (const prop of props) {
145
+ pyContent += ` "${prop}": ${this.pyString(model.description)},\n`;
146
+ }
147
+ }
148
+ pyContent += '}\n\n';
149
+ // 3. MODEL_FIELDS
150
+ pyContent += 'MODEL_FIELDS = {\n';
151
+ for (const model of models) {
152
+ const props = this.getAllPropertyVariations(model.name);
153
+ const fieldsStr = this.formatFields(model);
154
+ for (const prop of props) {
155
+ pyContent += ` "${prop}": ${fieldsStr},\n`;
156
+ }
157
+ }
158
+ pyContent += '}\n\n';
159
+ // 4. RELATION_MAP
160
+ pyContent += 'RELATION_MAP = {\n';
161
+ for (const model of models) {
162
+ const props = this.getAllPropertyVariations(model.name);
163
+ let relationsContent = `{\n`;
164
+ for (const rel of model.relations) {
165
+ relationsContent += ` "${rel.name}": {\n`;
166
+ relationsContent += ` "modelName": "${this.toCamelCase(rel.type)}",\n`;
167
+ relationsContent += ` "relationType": "${rel.isArray ? 'many' : 'one'}",\n`;
168
+ relationsContent += ` "foreignKey": "${rel.foreignKey || 'id'}",\n`;
169
+ relationsContent += ` "localKey": "${rel.localKey || 'id'}"\n`;
170
+ relationsContent += ` },\n`;
171
+ }
172
+ relationsContent += ` }`;
173
+ for (const prop of props) {
174
+ pyContent += ` "${prop}": ${relationsContent},\n`;
175
+ }
176
+ }
177
+ pyContent += '}\n';
178
+ fs_1.default.writeFileSync(this.outputPath, pyContent);
179
+ }
180
+ /**
181
+ * The Python type for a field.
182
+ *
183
+ * From the declared type, so `DECIMAL` is a `float` and `BIGINT` stays an `int`:
184
+ * both arrived as `number` before and took the integer branch.
185
+ */
186
+ mapPyType(field) {
187
+ switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
188
+ case 'int':
189
+ case 'bigint': return 'int';
190
+ case 'float': return 'float';
191
+ case 'bool': return 'bool';
192
+ case 'date': return 'datetime';
193
+ case 'bytes': return 'bytes';
194
+ case 'json': return 'Any';
195
+ case 'vector': return 'List[float]';
196
+ default: return 'str';
197
+ }
198
+ }
199
+ generateModelFile(model, outputDir) {
200
+ // Single-entity file like dotnet `${model.name}.cs` / typescript `${model.name}.ts`.
201
+ // Contains the @dataclass entity + TypedDict row shapes for that model only.
202
+ let content = '# This file is auto-generated. Do not edit directly.\n';
203
+ content += 'from dataclasses import dataclass, field\n';
204
+ content += 'from typing import Optional, List, Any, TypedDict\n';
205
+ content += 'from datetime import datetime\n\n';
206
+ if (model.description) {
207
+ content += `"""${model.description}"""\n`;
208
+ }
209
+ content += `@dataclass\nclass ${model.name}:\n`;
210
+ if (model.fields.length === 0 && model.relations.length === 0) {
211
+ content += ' pass\n\n';
212
+ }
213
+ else {
214
+ // Required fields first
215
+ const required = model.fields.filter(f => !f.isOptional && !f.hasDefault);
216
+ const optionals = model.fields.filter(f => f.isOptional || f.hasDefault);
217
+ for (const f of required) {
218
+ const pyType = this.mapPyType(f);
219
+ const snakeName = this.toSnakeCase(f.name);
220
+ content += ` ${snakeName}: ${pyType}\n`;
221
+ }
222
+ for (const f of optionals) {
223
+ const pyType = this.mapPyType(f);
224
+ const snakeName = this.toSnakeCase(f.name);
225
+ content += ` ${snakeName}: Optional[${pyType}] = None\n`;
226
+ }
227
+ // Relations
228
+ for (const rel of model.relations) {
229
+ const relName = this.toSnakeCase(rel.name);
230
+ if (rel.isArray) {
231
+ content += ` ${relName}: List[Any] = field(default_factory=list)\n`;
232
+ }
233
+ else {
234
+ content += ` ${relName}: Optional[Any] = None\n`;
235
+ }
236
+ }
237
+ content += '\n';
238
+ // TypedDict row shape: mirrors what the runtime actually returns
239
+ // (plain dicts), so `AdapterTableClient["<Model>Row"]` delegates
240
+ // type-check. Required keys stay required via a total=True base;
241
+ // everything else is optional (total=False), keeping partial
242
+ // selects valid without extra dependencies.
243
+ const rowRequired = model.fields.filter(f => !f.isOptional && !f.hasDefault);
244
+ const rowOptional = model.fields.filter(f => f.isOptional || f.hasDefault);
245
+ content += `class _${model.name}Required(TypedDict):\n`;
246
+ content += ` """Required keys of a ${model.name} row."""\n`;
247
+ if (rowRequired.length === 0) {
248
+ content += ' pass\n';
249
+ }
250
+ for (const f of rowRequired) {
251
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
252
+ }
253
+ content += `\nclass ${model.name}Row(_${model.name}Required, total=False):\n`;
254
+ content += ` """Row shape returned for ${model.name} queries."""\n`;
255
+ if (rowOptional.length === 0 && model.relations.length === 0) {
256
+ content += ' pass\n';
257
+ }
258
+ for (const f of rowOptional) {
259
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
260
+ }
261
+ for (const rel of model.relations) {
262
+ const relName = this.toSnakeCase(rel.name);
263
+ content += rel.isArray ? ` ${relName}: List[Any]\n` : ` ${relName}: Any\n`;
264
+ }
265
+ content += '\n';
266
+ }
267
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, `${model.name}.py`), content);
268
+ }
269
+ generateModelsIndex(models, outputDir) {
270
+ // Backward-compat aggregator: `from an5_models import User` keeps working.
271
+ // New code can also import directly: `from .User import User, UserRow`.
272
+ // Supports both package-relative (`python -m`) and top-level (`sys.path`
273
+ // pointing at the output dir) imports via try/except fallback.
274
+ let content = '# This file is auto-generated. Do not edit directly.\n';
275
+ content += '"""Backward-compat aggregator re-exporting per-model entity files."""\n';
276
+ for (const model of models) {
277
+ content += `try:\n`;
278
+ content += ` from .${model.name} import ${model.name}, ${model.name}Row, _${model.name}Required\n`;
279
+ content += `except ImportError:\n`;
280
+ content += ` from ${model.name} import ${model.name}, ${model.name}Row, _${model.name}Required\n`;
281
+ }
282
+ if (models.length > 0) {
283
+ content += '\n__all__ = [\n';
284
+ for (const model of models) {
285
+ content += ` "${model.name}", "${model.name}Row", "_${model.name}Required",\n`;
286
+ }
287
+ content += ']\n';
288
+ }
289
+ else {
290
+ content += '\n__all__ = []\n';
291
+ }
292
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_models.py'), content);
293
+ }
294
+ generateClient(models, outputDir) {
295
+ let content = '# This file is auto-generated. Do not edit directly.\n';
296
+ content += 'import os\n';
297
+ content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
298
+ // Runtime adapter comes from the `an5-adapters` PyPI package
299
+ // (module `an5_adapter`). There is no local adapter file in the
300
+ // generated output dir, so no relative-import fallback here — a
301
+ // relative fallback would be unresolvable and trip Pylance
302
+ // `reportMissingImports` on every generated client.
303
+ content += 'from an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n\n';
304
+ const metadataModule = this.metadataModule();
305
+ content += 'try:\n';
306
+ content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
307
+ content += 'except ImportError:\n';
308
+ content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n\n`;
309
+ if (models.length > 0) {
310
+ content += 'if TYPE_CHECKING:\n';
311
+ for (const m of models) {
312
+ content += ' try:\n';
313
+ content += ` from .${m.name} import ${m.name}Row\n`;
314
+ content += ' except ImportError:\n';
315
+ content += ` from .an5_models import ${m.name}Row # fallback aggregator\n`;
316
+ }
317
+ content += '\n';
318
+ }
319
+ content += 'class An5Client:\n';
320
+ content += ' """AN5 Python ORM Client - type-safe database access.\n\n';
321
+ content += ' Usage:\n';
322
+ content += ' db = An5Client()\n';
323
+ content += ' users = db.user.find_many(where={"name": {"contains": "John"}}, order_by={"created_at": "asc"}, take=10)\n';
324
+ content += ' user = db.user.find_first(where={"id": "abc"})\n';
325
+ content += ' new = db.user.create(data={"name": "Alice", "email": "alice@example.com"})\n';
326
+ content += ' """\n';
327
+ content += ' def __init__(self, connection_string: Optional[str] = None):\n';
328
+ content += ' conn_str = (connection_string or \n';
329
+ content += ' os.getenv("DATABASE_URL") or \n';
330
+ content += ' "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;")\n';
331
+ content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS})\n';
332
+ content += ' self.adapter: An5Adapter = create_an5_adapter(conn_str)\n\n';
333
+ for (const model of models) {
334
+ const propName = this.toSnakeCase(model.name) + 's';
335
+ const singleName = this.toSnakeCase(model.name);
336
+ content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
337
+ content += ` self.${model.name}: AdapterTableClient["${model.name}Row"] = client\n`;
338
+ content += ` self.${model.name}s: AdapterTableClient["${model.name}Row"] = client\n`;
339
+ content += ` self.${singleName}: AdapterTableClient["${model.name}Row"] = client\n`;
340
+ content += ` self.${propName}: AdapterTableClient["${model.name}Row"] = client\n`;
341
+ }
342
+ content += '\n';
343
+ content += ' def __getattr__(self, name: str) -> AdapterTableClient[Any]:\n';
344
+ content += ' return self.adapter.table(name)\n\n';
345
+ content += ' def query_raw(self, sql: str, *params) -> List[Dict]:\n';
346
+ content += ' return self.adapter.query_raw(sql, *params)\n\n';
347
+ content += ' def execute_raw(self, sql: str, *params) -> int:\n';
348
+ content += ' return self.adapter.execute_raw(sql, *params)\n\n';
349
+ content += ' def transaction(self, fn: Callable) -> Any:\n';
350
+ content += ' return self.adapter.transaction(fn)\n';
351
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_client.py'), content);
352
+ }
353
+ generateInit(models, outputDir) {
354
+ let content = '# This file is auto-generated. Do not edit directly.\n';
355
+ content += `from .${this.metadataModule()} import MODEL_TO_TABLE, MODEL_DESCRIPTIONS, MODEL_FIELDS, RELATION_MAP\n`;
356
+ content += 'from .an5_models import *\n';
357
+ content += 'from .an5_orm_types import (\n';
358
+ content += ' StringFilter, IntFilter, NumberFilter, BoolFilter, DateTimeFilter,\n';
359
+ for (const m of models) {
360
+ const name = this.capitalize(m.name);
361
+ content += ` ${name}WhereInput, ${name}OrderByInput,\n`;
362
+ content += ` ${name}CreateInput, ${name}UpdateInput,\n`;
363
+ content += ` ${name}FindManyArgs, ${name}FindFirstArgs,\n`;
364
+ }
365
+ content += ')\n';
366
+ content += 'from .an5_client import An5Client\n\n';
367
+ content += '__all__ = [\n';
368
+ content += ' "MODEL_TO_TABLE",\n';
369
+ content += ' "MODEL_DESCRIPTIONS",\n';
370
+ content += ' "MODEL_FIELDS",\n';
371
+ content += ' "RELATION_MAP",\n';
372
+ content += ' "An5Client",\n';
373
+ content += ' "StringFilter", "IntFilter", "NumberFilter", "BoolFilter", "DateTimeFilter",\n';
374
+ for (const m of models) {
375
+ const name = this.capitalize(m.name);
376
+ content += ` "${m.name}",\n`;
377
+ content += ` "${name}WhereInput", "${name}OrderByInput",\n`;
378
+ content += ` "${name}CreateInput", "${name}UpdateInput",\n`;
379
+ content += ` "${name}FindManyArgs", "${name}FindFirstArgs",\n`;
380
+ }
381
+ content += ']\n';
382
+ fs_1.default.writeFileSync(path_1.default.join(outputDir, '__init__.py'), content);
383
+ }
384
+ getAllPropertyVariations(modelName) {
385
+ const variations = new Set();
386
+ variations.add(this.toCamelCase(modelName));
387
+ variations.add(this.toSnakeCase(modelName));
388
+ const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
389
+ for (const acronym of acronyms) {
390
+ if (modelName.startsWith(acronym)) {
391
+ variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
392
+ variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
393
+ variations.add(this.toSnakeCase(acronym.toLowerCase() + modelName.slice(acronym.length)));
394
+ }
395
+ }
396
+ return Array.from(variations);
397
+ }
398
+ formatFields(model) {
399
+ if (model.fields.length === 0)
400
+ return '[]';
401
+ const fields = model.fields.map(f => {
402
+ return ` { "name": "${f.name}", "type": "${f.type}${f.isOptional ? '?' : ''}", "sql": "${f.sqlType}", "isOptional": ${f.isOptional ? 'True' : 'False'}, "hasDefault": ${f.hasDefault ? 'True' : 'False'}, "isId": ${f.isId ? 'True' : 'False'}, "description": ${this.pyString(f.description)} }`;
403
+ });
404
+ return `[\n${fields.join(',\n')}\n ]`;
405
+ }
406
+ pyString(value) {
407
+ return value ? JSON.stringify(value) : 'None';
408
+ }
409
+ toCamelCase(str) {
410
+ if (!str)
411
+ return '';
412
+ return str[0].toLowerCase() + str.slice(1);
413
+ }
414
+ toSnakeCase(str) {
415
+ if (!str)
416
+ return '';
417
+ return str
418
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
419
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
420
+ .toLowerCase();
421
+ }
422
+ capitalize(str) {
423
+ if (!str)
424
+ return '';
425
+ return str[0].toUpperCase() + str.slice(1);
426
+ }
427
+ }
428
+ exports.PythonGenerator = PythonGenerator;
@@ -0,0 +1,38 @@
1
+ import { Model } from './types';
2
+ export declare class RustGenerator {
3
+ private outputDir;
4
+ /** The database being generated for; decides types like `TIMESTAMP`. */
5
+ private provider;
6
+ constructor(outputDir: string);
7
+ generate(models: Model[]): void;
8
+ private generateCargoToml;
9
+ private generateLibRs;
10
+ private generateFiltersRs;
11
+ private generateModelsRs;
12
+ private buildModelsBody;
13
+ private generateMetadataRs;
14
+ private generateConfigRs;
15
+ private generateClientRs;
16
+ /**
17
+ * Typed, executing accessors for a model.
18
+ *
19
+ * Rust has no dynamic property access, so `db.user` cannot exist the way it
20
+ * does in TypeScript or Python. Following the Go generator (which emits a
21
+ * real `ctx.User` field), each model gets a generated method returning a
22
+ * typed handle whose queries run through the an5-adapters runtime.
23
+ */
24
+ private buildModelRuntimeHandle;
25
+ private isBoolField;
26
+ /**
27
+ * The Rust type for a field.
28
+ *
29
+ * From the declared type, so `DECIMAL` is an `f64` and `BIGINT` an `i64`: both
30
+ * arrived as `number` and were written as `i64`.
31
+ */
32
+ private mapRustType;
33
+ private getRustFilterType;
34
+ private getAllPropertyVariations;
35
+ private capitalize;
36
+ private toCamelCase;
37
+ private toSnakeCase;
38
+ }