@an5/orm 1.0.9 → 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 (56) 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 +1229 -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/generator/src/api.ts +45 -0
  41. package/generator/src/config.ts +496 -0
  42. package/generator/src/dialect.ts +415 -0
  43. package/generator/src/dotnet-generator.ts +277 -88
  44. package/generator/src/field-types.ts +473 -0
  45. package/generator/src/golang-generator.ts +89 -64
  46. package/generator/src/index.ts +71 -36
  47. package/generator/src/metadata-generator.ts +2 -2
  48. package/generator/src/parser.ts +122 -90
  49. package/generator/src/push-apply.ts +148 -0
  50. package/generator/src/push-schema.ts +250 -0
  51. package/generator/src/python-generator.ts +118 -62
  52. package/generator/src/rust-generator.ts +974 -0
  53. package/generator/src/suggest.ts +41 -0
  54. package/generator/src/type-kinds.ts +78 -0
  55. package/generator/src/types.ts +48 -0
  56. package/package.json +26 -6
@@ -1,11 +1,30 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { Model } from './types';
3
+ import { Model, Field, bracketedTableName } from './types';
4
+ import { fieldKind } from './type-kinds';
5
+ import { Provider } from './field-types';
4
6
 
5
7
  export class PythonGenerator {
6
8
  constructor(private outputPath: string) {}
7
9
 
10
+ /**
11
+ * The metadata module name, taken from the configured file name itself.
12
+ *
13
+ * The imports used to hardcode `an5_metadata` while `outputPath` was
14
+ * configurable, so setting `python.metadataFile` to another name (say
15
+ * `an5Metadata.py`) still generated `from an5_metadata import ...` and the
16
+ * client broke on import — a failure that only showed up at runtime.
17
+ */
18
+ private metadataModule(): string {
19
+ return path.basename(this.outputPath).replace(/\.py$/i, '');
20
+ }
21
+
22
+ /** The database being generated for; decides types like `TIMESTAMP`. */
23
+ private provider: Provider | undefined;
24
+
8
25
  public generate(models: Model[]) {
26
+ this.provider = models[0]?.provider;
27
+
9
28
  const outputDir = path.dirname(this.outputPath);
10
29
  if (!fs.existsSync(outputDir)) {
11
30
  fs.mkdirSync(outputDir, { recursive: true });
@@ -14,26 +33,33 @@ export class PythonGenerator {
14
33
  // 1. Generate an5_metadata.py
15
34
  this.generateMetadata(models);
16
35
 
17
- // 2. Generate an5_models.py
18
- this.generateModels(models, outputDir);
36
+ // 2. Generate per-model entity files like dotnet (<Model>.py)
37
+ for (const model of models) {
38
+ this.generateModelFile(model, outputDir);
39
+ }
19
40
 
20
- // 3. Generate an5_orm_types.py - type-safe ORM filter/args dataclasses
41
+ // 3. Generate an5_models.py as backward-compat aggregator
42
+ this.generateModelsIndex(models, outputDir);
43
+
44
+ // 4. Generate an5_orm_types.py - type-safe ORM filter/args dataclasses
21
45
  this.generateOrmTypes(models, outputDir);
22
46
 
23
- // 4. Generate an5_client.py
47
+ // 5. Generate an5_client.py
24
48
  this.generateClient(models, outputDir);
25
49
 
26
- // 5. Generate __init__.py
50
+ // 6. Generate __init__.py
27
51
  this.generateInit(models, outputDir);
28
52
  }
29
53
 
30
- private getPyFilterType(fieldType: string): string {
31
- const lower = fieldType.toLowerCase();
32
- if (['datetime', 'datetime2', 'date', 'smalldatetime', 'datetimeoffset', 'timestamp'].includes(lower)) return 'DateTimeFilter';
33
- if (['bool', 'boolean', 'bit'].includes(lower)) return 'BoolFilter';
34
- if (['int', 'integer', 'smallint', 'tinyint', 'bigint', 'number'].includes(lower)) return 'IntFilter';
35
- if (['float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) return 'NumberFilter';
36
- return 'StringFilter';
54
+ private getPyFilterType(field: Field): string {
55
+ switch (fieldKind(field, this.provider)) {
56
+ case 'date': return 'DateTimeFilter';
57
+ case 'bool': return 'BoolFilter';
58
+ case 'int':
59
+ case 'bigint': return 'IntFilter';
60
+ case 'float': return 'NumberFilter';
61
+ default: return 'StringFilter';
62
+ }
37
63
  }
38
64
 
39
65
  private generateOrmTypes(models: Model[], outputDir: string) {
@@ -67,7 +93,7 @@ export class PythonGenerator {
67
93
  content += ` OR: Optional[List['${name}WhereInput']] = None\n`;
68
94
  content += ` NOT: Optional['${name}WhereInput'] = None\n`;
69
95
  for (const f of model.fields) {
70
- const ft = this.getPyFilterType(f.type);
96
+ const ft = this.getPyFilterType(f);
71
97
  content += ` ${this.toSnakeCase(f.name)}: Optional[${ft}] = None\n`;
72
98
  }
73
99
  content += '\n';
@@ -84,10 +110,10 @@ export class PythonGenerator {
84
110
  const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
85
111
  content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
86
112
  for (const f of requiredFields) {
87
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
113
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
88
114
  }
89
115
  for (const f of optionalFields) {
90
- content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f.type)}] = None\n`;
116
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
91
117
  }
92
118
  if (requiredFields.length === 0 && optionalFields.length === 0) content += ' pass\n';
93
119
  content += '\n';
@@ -96,7 +122,7 @@ export class PythonGenerator {
96
122
  const updateFields = model.fields.filter(f => !f.isId);
97
123
  content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
98
124
  for (const f of updateFields) {
99
- content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f.type)}] = None\n`;
125
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
100
126
  }
101
127
  if (updateFields.length === 0) content += ' pass\n';
102
128
  content += '\n';
@@ -126,7 +152,7 @@ export class PythonGenerator {
126
152
  pyContent += 'MODEL_TO_TABLE = {\n';
127
153
  for (const model of models) {
128
154
  const props = this.getAllPropertyVariations(model.name);
129
- const fullTableName = `[${model.schemaName}].[${model.tableName}]`;
155
+ const fullTableName = bracketedTableName(model);
130
156
  for (const prop of props) {
131
157
  pyContent += ` "${prop}": "${fullTableName}",\n`;
132
158
  }
@@ -178,55 +204,54 @@ export class PythonGenerator {
178
204
  fs.writeFileSync(this.outputPath, pyContent);
179
205
  }
180
206
 
181
- private mapPyType(fieldType: string): string {
182
- const lower = fieldType.toLowerCase();
183
- if (['int', 'integer', 'smallint', 'tinyint', 'bigint', 'number'].includes(lower)) {
184
- return 'int';
207
+ /**
208
+ * The Python type for a field.
209
+ *
210
+ * From the declared type, so `DECIMAL` is a `float` and `BIGINT` stays an `int`:
211
+ * both arrived as `number` before and took the integer branch.
212
+ */
213
+ private mapPyType(field: Field): string {
214
+ switch (fieldKind(field, this.provider)) {
215
+ case 'int':
216
+ case 'bigint': return 'int';
217
+ case 'float': return 'float';
218
+ case 'bool': return 'bool';
219
+ case 'date': return 'datetime';
220
+ case 'bytes': return 'bytes';
221
+ case 'json': return 'Any';
222
+ case 'vector': return 'List[float]';
223
+ default: return 'str';
185
224
  }
186
- if (['float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) {
187
- return 'float';
188
- }
189
- if (['bool', 'boolean', 'bit'].includes(lower)) {
190
- return 'bool';
191
- }
192
- if (['datetime', 'datetime2', 'date', 'smalldatetime', 'datetimeoffset', 'timestamp'].includes(lower)) {
193
- return 'datetime';
194
- }
195
- if (['bytes', 'binary', 'varbinary'].includes(lower)) {
196
- return 'bytes';
197
- }
198
- return 'str';
199
225
  }
200
226
 
201
- private generateModels(models: Model[], outputDir: string) {
227
+ private generateModelFile(model: Model, outputDir: string) {
228
+ // Single-entity file like dotnet `${model.name}.cs` / typescript `${model.name}.ts`.
229
+ // Contains the @dataclass entity + TypedDict row shapes for that model only.
202
230
  let content = '# This file is auto-generated. Do not edit directly.\n';
203
231
  content += 'from dataclasses import dataclass, field\n';
204
232
  content += 'from typing import Optional, List, Any, TypedDict\n';
205
233
  content += 'from datetime import datetime\n\n';
206
234
 
207
- for (const model of models) {
208
- if (model.description) {
209
- content += `"""${model.description}"""\n`;
210
- }
211
- content += `@dataclass\nclass ${model.name}:\n`;
212
-
213
- if (model.fields.length === 0 && model.relations.length === 0) {
214
- content += ' pass\n\n';
215
- continue;
216
- }
235
+ if (model.description) {
236
+ content += `"""${model.description}"""\n`;
237
+ }
238
+ content += `@dataclass\nclass ${model.name}:\n`;
217
239
 
240
+ if (model.fields.length === 0 && model.relations.length === 0) {
241
+ content += ' pass\n\n';
242
+ } else {
218
243
  // Required fields first
219
244
  const required = model.fields.filter(f => !f.isOptional && !f.hasDefault);
220
245
  const optionals = model.fields.filter(f => f.isOptional || f.hasDefault);
221
246
 
222
247
  for (const f of required) {
223
- const pyType = this.mapPyType(f.type);
248
+ const pyType = this.mapPyType(f);
224
249
  const snakeName = this.toSnakeCase(f.name);
225
250
  content += ` ${snakeName}: ${pyType}\n`;
226
251
  }
227
252
 
228
253
  for (const f of optionals) {
229
- const pyType = this.mapPyType(f.type);
254
+ const pyType = this.mapPyType(f);
230
255
  const snakeName = this.toSnakeCase(f.name);
231
256
  content += ` ${snakeName}: Optional[${pyType}] = None\n`;
232
257
  }
@@ -256,7 +281,7 @@ export class PythonGenerator {
256
281
  content += ' pass\n';
257
282
  }
258
283
  for (const f of rowRequired) {
259
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
284
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
260
285
  }
261
286
  content += `\nclass ${model.name}Row(_${model.name}Required, total=False):\n`;
262
287
  content += ` """Row shape returned for ${model.name} queries."""\n`;
@@ -264,7 +289,7 @@ export class PythonGenerator {
264
289
  content += ' pass\n';
265
290
  }
266
291
  for (const f of rowOptional) {
267
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
292
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
268
293
  }
269
294
  for (const rel of model.relations) {
270
295
  const relName = this.toSnakeCase(rel.name);
@@ -273,6 +298,32 @@ export class PythonGenerator {
273
298
  content += '\n';
274
299
  }
275
300
 
301
+ fs.writeFileSync(path.join(outputDir, `${model.name}.py`), content);
302
+ }
303
+
304
+ private generateModelsIndex(models: Model[], outputDir: string) {
305
+ // Backward-compat aggregator: `from an5_models import User` keeps working.
306
+ // New code can also import directly: `from .User import User, UserRow`.
307
+ // Supports both package-relative (`python -m`) and top-level (`sys.path`
308
+ // pointing at the output dir) imports via try/except fallback.
309
+ let content = '# This file is auto-generated. Do not edit directly.\n';
310
+ content += '"""Backward-compat aggregator re-exporting per-model entity files."""\n';
311
+ for (const model of models) {
312
+ content += `try:\n`;
313
+ content += ` from .${model.name} import ${model.name}, ${model.name}Row, _${model.name}Required\n`;
314
+ content += `except ImportError:\n`;
315
+ content += ` from ${model.name} import ${model.name}, ${model.name}Row, _${model.name}Required\n`;
316
+ }
317
+ if (models.length > 0) {
318
+ content += '\n__all__ = [\n';
319
+ for (const model of models) {
320
+ content += ` "${model.name}", "${model.name}Row", "_${model.name}Required",\n`;
321
+ }
322
+ content += ']\n';
323
+ } else {
324
+ content += '\n__all__ = []\n';
325
+ }
326
+
276
327
  fs.writeFileSync(path.join(outputDir, 'an5_models.py'), content);
277
328
  }
278
329
 
@@ -281,23 +332,28 @@ export class PythonGenerator {
281
332
  content += 'import os\n';
282
333
  content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
283
334
 
284
- content += 'try:\n';
285
- content += ' from an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n';
286
- content += 'except ImportError:\n';
287
- content += ' from .an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n\n';
335
+ // Runtime adapter comes from the `an5-adapters` PyPI package
336
+ // (module `an5_adapter`). There is no local adapter file in the
337
+ // generated output dir, so no relative-import fallback here — a
338
+ // relative fallback would be unresolvable and trip Pylance
339
+ // `reportMissingImports` on every generated client.
340
+ content += 'from an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n\n';
288
341
 
342
+ const metadataModule = this.metadataModule();
289
343
  content += 'try:\n';
290
- content += ' from .an5_metadata import MODEL_TO_TABLE, MODEL_FIELDS\n';
344
+ content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
291
345
  content += 'except ImportError:\n';
292
- content += ' from an5_metadata import MODEL_TO_TABLE, MODEL_FIELDS\n\n';
346
+ content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n\n`;
293
347
 
294
348
  if (models.length > 0) {
295
- const rowTypes = models.map(m => `${m.name}Row`).join(', ');
296
349
  content += 'if TYPE_CHECKING:\n';
297
- content += ' try:\n';
298
- content += ` from an5_models import ${rowTypes}\n`;
299
- content += ' except ImportError:\n';
300
- content += ` from .an5_models import ${rowTypes}\n\n`;
350
+ for (const m of models) {
351
+ content += ' try:\n';
352
+ content += ` from .${m.name} import ${m.name}Row\n`;
353
+ content += ' except ImportError:\n';
354
+ content += ` from .an5_models import ${m.name}Row # fallback aggregator\n`;
355
+ }
356
+ content += '\n';
301
357
  }
302
358
 
303
359
  content += 'class An5Client:\n';
@@ -340,7 +396,7 @@ export class PythonGenerator {
340
396
 
341
397
  private generateInit(models: Model[], outputDir: string) {
342
398
  let content = '# This file is auto-generated. Do not edit directly.\n';
343
- content += 'from .an5_metadata import MODEL_TO_TABLE, MODEL_DESCRIPTIONS, MODEL_FIELDS, RELATION_MAP\n';
399
+ content += `from .${this.metadataModule()} import MODEL_TO_TABLE, MODEL_DESCRIPTIONS, MODEL_FIELDS, RELATION_MAP\n`;
344
400
  content += 'from .an5_models import *\n';
345
401
  content += 'from .an5_orm_types import (\n';
346
402
  content += ' StringFilter, IntFilter, NumberFilter, BoolFilter, DateTimeFilter,\n';