@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.
- package/README.md +10 -3
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/config.d.ts +162 -0
- package/dist/generator/src/config.js +354 -0
- package/dist/generator/src/dialect.d.ts +88 -0
- package/dist/generator/src/dialect.js +318 -0
- package/dist/generator/src/dotnet-generator.d.ts +8 -0
- package/dist/generator/src/dotnet-generator.js +282 -101
- package/dist/generator/src/field-types.d.ts +96 -0
- package/dist/generator/src/field-types.js +433 -0
- package/dist/generator/src/golang-generator.d.ts +36 -0
- package/dist/generator/src/golang-generator.js +1229 -0
- package/dist/generator/src/index.js +69 -36
- package/dist/generator/src/metadata-generator.js +2 -1
- package/dist/generator/src/parser.d.ts +28 -2
- package/dist/generator/src/parser.js +92 -90
- package/dist/generator/src/push-apply.d.ts +37 -0
- package/dist/generator/src/push-apply.js +109 -0
- package/dist/generator/src/push-schema.d.ts +93 -0
- package/dist/generator/src/push-schema.js +189 -0
- package/dist/generator/src/python-generator.d.ts +19 -1
- package/dist/generator/src/python-generator.js +147 -64
- package/dist/generator/src/rust-generator.d.ts +38 -0
- package/dist/generator/src/rust-generator.js +971 -0
- package/dist/generator/src/suggest.d.ts +17 -0
- package/dist/generator/src/suggest.js +45 -0
- package/dist/generator/src/type-kinds.d.ts +27 -0
- package/dist/generator/src/type-kinds.js +72 -0
- package/dist/generator/src/types.d.ts +33 -0
- package/dist/generator/src/types.js +39 -0
- package/dist/migration-core.d.ts +16 -1
- package/dist/migration-core.js +60 -21
- package/dist/orm.d.ts +147 -0
- package/dist/orm.js +1654 -0
- package/dist/provider-support.d.ts +27 -0
- package/dist/provider-support.js +50 -0
- package/generator/src/api.ts +45 -0
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +277 -88
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +89 -64
- package/generator/src/index.ts +71 -36
- package/generator/src/metadata-generator.ts +2 -2
- package/generator/src/parser.ts +122 -90
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +118 -62
- package/generator/src/rust-generator.ts +974 -0
- package/generator/src/suggest.ts +41 -0
- package/generator/src/type-kinds.ts +78 -0
- package/generator/src/types.ts +48 -0
- 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
|
|
18
|
-
|
|
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
|
|
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
|
-
//
|
|
47
|
+
// 5. Generate an5_client.py
|
|
24
48
|
this.generateClient(models, outputDir);
|
|
25
49
|
|
|
26
|
-
//
|
|
50
|
+
// 6. Generate __init__.py
|
|
27
51
|
this.generateInit(models, outputDir);
|
|
28
52
|
}
|
|
29
53
|
|
|
30
|
-
private getPyFilterType(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 +=
|
|
344
|
+
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
|
|
291
345
|
content += 'except ImportError:\n';
|
|
292
|
-
content +=
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 +=
|
|
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';
|