@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading `.an5` files for `db:push`.
|
|
3
|
+
*
|
|
4
|
+
* `SchemaParser` and `parseSchemaText` cover the generators and `db:migrate`;
|
|
5
|
+
* this is the third reader, and it exists because push needs a shape of its own:
|
|
6
|
+
* raw `@default` expressions rather than rendered SQL, and the schema a table
|
|
7
|
+
* lands in. It is kept here rather than inline in `push.ts` so the mapping from
|
|
8
|
+
* schema text to a column can be tested without a database.
|
|
9
|
+
*/
|
|
10
|
+
import { FieldTypeIssue, Provider } from './field-types';
|
|
11
|
+
/** A column, as push needs it: the type as written plus what the dialect renders. */
|
|
12
|
+
export interface PushField {
|
|
13
|
+
name: string;
|
|
14
|
+
sqlType: string;
|
|
15
|
+
isOptional: boolean;
|
|
16
|
+
isId: boolean;
|
|
17
|
+
isUnique: boolean;
|
|
18
|
+
/** The `@default(...)` expression, not SQL: only the dialect knows the spelling. */
|
|
19
|
+
defaultExpr?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface PushModel {
|
|
22
|
+
name: string;
|
|
23
|
+
tableName: string;
|
|
24
|
+
/** The schema to create the table in; `''` when the provider has none. */
|
|
25
|
+
schema: string;
|
|
26
|
+
/** Set only when the schema file named one, so a wrong one can be reported. */
|
|
27
|
+
declaredSchema?: string | undefined;
|
|
28
|
+
fields: PushField[];
|
|
29
|
+
/** `@@unique([a, b])`, as field lists. */
|
|
30
|
+
compoundUniques: PushIndex[];
|
|
31
|
+
/** `@@index([a, b])`, as field lists. */
|
|
32
|
+
indexes: PushIndex[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The schema a model lands in when the schema file does not say: none, for every
|
|
36
|
+
* provider, so the name goes wherever the connection resolves.
|
|
37
|
+
*
|
|
38
|
+
* The parser used to default this to `dbo` and push followed it, which failed two
|
|
39
|
+
* ways: SQLite said "unknown database [dbo]", and a SQL Server login whose default
|
|
40
|
+
* schema was something else had push look for `[dbo].[widgets]`, miss the table it
|
|
41
|
+
* had created, and then fail with "there is already an object named ...".
|
|
42
|
+
*/
|
|
43
|
+
export declare function defaultSchemaFor(_provider: Provider): string;
|
|
44
|
+
/**
|
|
45
|
+
* One `@@unique` or `@@index`, with the options `db:migrate` already understands.
|
|
46
|
+
*
|
|
47
|
+
* `name` matters as much as the fields: `map:` is how a schema asks for a specific
|
|
48
|
+
* artifact name, and if push invented its own the two commands would create two
|
|
49
|
+
* different constraints for the same directive — and the next migration would keep
|
|
50
|
+
* trying to add the one push never made.
|
|
51
|
+
*/
|
|
52
|
+
export interface PushIndex {
|
|
53
|
+
fields: string[];
|
|
54
|
+
name?: string | undefined;
|
|
55
|
+
includeFields?: string[] | undefined;
|
|
56
|
+
filter?: string | undefined;
|
|
57
|
+
options?: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
export interface PushSchema {
|
|
60
|
+
models: PushModel[];
|
|
61
|
+
/** Field types the provider does not have; empty when the schema is valid. */
|
|
62
|
+
issues: FieldTypeIssue[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The name a `@@unique` or `@@index` artifact gets.
|
|
66
|
+
*
|
|
67
|
+
* `map:` wins; otherwise the name is derived from the table and fields, which is what
|
|
68
|
+
* both commands have always done for an unnamed directive.
|
|
69
|
+
*/
|
|
70
|
+
export declare function directiveName(directive: PushIndex, table: string, kind: 'compound' | 'index', position: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* Reads schema text for push, validating every field type against the provider.
|
|
73
|
+
*
|
|
74
|
+
* All bad types come back in `issues` rather than as an exception, so the caller
|
|
75
|
+
* can print them as a group. A field whose type matches no table is only a
|
|
76
|
+
* mistake once the whole schema has been read: it may be a relation to a model
|
|
77
|
+
* declared further down.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parsePushSchema(text: string, provider: Provider): PushSchema;
|
|
80
|
+
/**
|
|
81
|
+
* The table name to create, `schema.table` unless the model cleared the schema.
|
|
82
|
+
*
|
|
83
|
+
* An empty schema has to stay empty: `[].[widgets]` is not valid SQL, which is the
|
|
84
|
+
* whole reason `@@schema("")` exists.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* An identifier for a generated artifact name: letters, digits and underscores only.
|
|
88
|
+
*
|
|
89
|
+
* The name goes into DDL as an identifier, so anything else is replaced rather than
|
|
90
|
+
* quoted — `@@map("catalog entries")` must not produce a name the provider rejects.
|
|
91
|
+
*/
|
|
92
|
+
export declare function safeIdentifierName(raw: string): string;
|
|
93
|
+
export declare function qualifiedTableName(model: PushModel): string;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultSchemaFor = defaultSchemaFor;
|
|
4
|
+
exports.directiveName = directiveName;
|
|
5
|
+
exports.parsePushSchema = parsePushSchema;
|
|
6
|
+
exports.safeIdentifierName = safeIdentifierName;
|
|
7
|
+
exports.qualifiedTableName = qualifiedTableName;
|
|
8
|
+
/**
|
|
9
|
+
* Reading `.an5` files for `db:push`.
|
|
10
|
+
*
|
|
11
|
+
* `SchemaParser` and `parseSchemaText` cover the generators and `db:migrate`;
|
|
12
|
+
* this is the third reader, and it exists because push needs a shape of its own:
|
|
13
|
+
* raw `@default` expressions rather than rendered SQL, and the schema a table
|
|
14
|
+
* lands in. It is kept here rather than inline in `push.ts` so the mapping from
|
|
15
|
+
* schema text to a column can be tested without a database.
|
|
16
|
+
*/
|
|
17
|
+
const field_types_1 = require("./field-types");
|
|
18
|
+
/**
|
|
19
|
+
* The schema a model lands in when the schema file does not say: none, for every
|
|
20
|
+
* provider, so the name goes wherever the connection resolves.
|
|
21
|
+
*
|
|
22
|
+
* The parser used to default this to `dbo` and push followed it, which failed two
|
|
23
|
+
* ways: SQLite said "unknown database [dbo]", and a SQL Server login whose default
|
|
24
|
+
* schema was something else had push look for `[dbo].[widgets]`, miss the table it
|
|
25
|
+
* had created, and then fail with "there is already an object named ...".
|
|
26
|
+
*/
|
|
27
|
+
function defaultSchemaFor(_provider) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
/** The field list of `@@unique([a, b], …)`, plus whatever options follow it. */
|
|
31
|
+
const DIRECTIVE = /^@@(?:unique|index)\(\s*\[([\w\s,]+)\]([^)]*)\)/;
|
|
32
|
+
function parseDirective(line, fields) {
|
|
33
|
+
const options = line.match(DIRECTIVE)?.[2] ?? '';
|
|
34
|
+
const name = options.match(/\bmap\s*:\s*"([^"]+)"/)?.[1];
|
|
35
|
+
const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
|
|
36
|
+
const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
|
|
37
|
+
const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
|
|
38
|
+
return {
|
|
39
|
+
fields: fields.split(',').map((field) => field.trim()).filter(Boolean),
|
|
40
|
+
...(name ? { name } : {}),
|
|
41
|
+
...(include ? { includeFields: include.split(',').map((field) => field.trim()).filter(Boolean) } : {}),
|
|
42
|
+
...(filter ? { filter } : {}),
|
|
43
|
+
...(opts ? { options: opts } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The name a `@@unique` or `@@index` artifact gets.
|
|
48
|
+
*
|
|
49
|
+
* `map:` wins; otherwise the name is derived from the table and fields, which is what
|
|
50
|
+
* both commands have always done for an unnamed directive.
|
|
51
|
+
*/
|
|
52
|
+
function directiveName(directive, table, kind, position) {
|
|
53
|
+
if (directive.name)
|
|
54
|
+
return safeIdentifierName(directive.name);
|
|
55
|
+
const base = safeIdentifierName(table);
|
|
56
|
+
return kind === 'compound'
|
|
57
|
+
? `UQ_${base}_compound_${position}`
|
|
58
|
+
: `IX_${base}_${directive.fields.join('_')}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads schema text for push, validating every field type against the provider.
|
|
62
|
+
*
|
|
63
|
+
* All bad types come back in `issues` rather than as an exception, so the caller
|
|
64
|
+
* can print them as a group. A field whose type matches no table is only a
|
|
65
|
+
* mistake once the whole schema has been read: it may be a relation to a model
|
|
66
|
+
* declared further down.
|
|
67
|
+
*/
|
|
68
|
+
function parsePushSchema(text, provider) {
|
|
69
|
+
const models = [];
|
|
70
|
+
const issues = [];
|
|
71
|
+
const modelRefs = [];
|
|
72
|
+
let current = null;
|
|
73
|
+
for (const raw of text.split('\n')) {
|
|
74
|
+
const line = raw.trim();
|
|
75
|
+
if (!line || line.startsWith('//'))
|
|
76
|
+
continue;
|
|
77
|
+
const header = line.match(/^model\s+(\w+)\s*\{/);
|
|
78
|
+
if (header) {
|
|
79
|
+
current = {
|
|
80
|
+
name: header[1],
|
|
81
|
+
tableName: header[1].toLowerCase() + 's',
|
|
82
|
+
schema: defaultSchemaFor(provider),
|
|
83
|
+
fields: [],
|
|
84
|
+
compoundUniques: [],
|
|
85
|
+
indexes: [],
|
|
86
|
+
};
|
|
87
|
+
models.push(current);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (line === '}') {
|
|
91
|
+
current = null;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!current)
|
|
95
|
+
continue;
|
|
96
|
+
if (line.startsWith('@@map')) {
|
|
97
|
+
const match = line.match(/@@map\("(.+)"\)/);
|
|
98
|
+
if (match)
|
|
99
|
+
current.tableName = match[1];
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith('@@schema')) {
|
|
103
|
+
// `(.*)` rather than `(.+)`, because `@@schema("")` is how a model says "no
|
|
104
|
+
// schema" and has to survive as an empty string rather than being dropped as
|
|
105
|
+
// no match — which is how SQLite and MySQL schemas are written.
|
|
106
|
+
const match = line.match(/@@schema\("(.*)"\)/);
|
|
107
|
+
if (match) {
|
|
108
|
+
current.schema = match[1].trim();
|
|
109
|
+
current.declaredSchema = current.schema;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (line.startsWith('@@unique') || line.startsWith('@@index')) {
|
|
114
|
+
const match = line.match(DIRECTIVE);
|
|
115
|
+
if (match) {
|
|
116
|
+
const definition = parseDirective(line, match[1] ?? '');
|
|
117
|
+
if (line.startsWith('@@unique'))
|
|
118
|
+
current.compoundUniques.push(definition);
|
|
119
|
+
else
|
|
120
|
+
current.indexes.push(definition);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith('@@'))
|
|
125
|
+
continue;
|
|
126
|
+
const head = (0, field_types_1.readFieldLineHead)(line);
|
|
127
|
+
if (!head)
|
|
128
|
+
continue;
|
|
129
|
+
if (!(0, field_types_1.resolveFieldType)(head.type, provider)) {
|
|
130
|
+
modelRefs.push({ model: current.name, field: head.name, type: head.type });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// The type is kept as written (upper-cased) because that is what the database
|
|
134
|
+
// is told; which types exist is what `resolveFieldType` just checked.
|
|
135
|
+
current.fields.push({
|
|
136
|
+
name: head.name,
|
|
137
|
+
sqlType: head.type.toUpperCase(),
|
|
138
|
+
isOptional: head.isOptional,
|
|
139
|
+
isId: line.includes('@id'),
|
|
140
|
+
isUnique: line.includes('@unique'),
|
|
141
|
+
// The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
|
|
142
|
+
// differently per provider, and only the dialect knows how. `@updatedAt` is
|
|
143
|
+
// `now()` under another name.
|
|
144
|
+
defaultExpr: line.match(/@default\((.*)\)/)?.[1]
|
|
145
|
+
?? (line.includes('@updatedAt') ? 'now()' : undefined),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// A declared `dbo` on a provider without schemas would push to a database that
|
|
149
|
+
// does not exist there; SQLite answers "unknown database [dbo]", which says
|
|
150
|
+
// nothing about the schema file. Only an explicit declaration is worth reporting:
|
|
151
|
+
// an absent one now means "whatever the connection resolves", on every provider.
|
|
152
|
+
for (const model of models) {
|
|
153
|
+
if (model.declaredSchema === 'dbo' && provider !== 'mssql') {
|
|
154
|
+
issues.push({
|
|
155
|
+
path: model.name,
|
|
156
|
+
message: `schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
|
|
157
|
+
'to the schema this database uses',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const modelNames = models.map((model) => model.name);
|
|
162
|
+
for (const ref of modelRefs) {
|
|
163
|
+
if (!modelNames.includes(ref.type)) {
|
|
164
|
+
issues.push({
|
|
165
|
+
path: `${ref.model}.${ref.field}`,
|
|
166
|
+
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, modelNames),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { models, issues };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The table name to create, `schema.table` unless the model cleared the schema.
|
|
174
|
+
*
|
|
175
|
+
* An empty schema has to stay empty: `[].[widgets]` is not valid SQL, which is the
|
|
176
|
+
* whole reason `@@schema("")` exists.
|
|
177
|
+
*/
|
|
178
|
+
/**
|
|
179
|
+
* An identifier for a generated artifact name: letters, digits and underscores only.
|
|
180
|
+
*
|
|
181
|
+
* The name goes into DDL as an identifier, so anything else is replaced rather than
|
|
182
|
+
* quoted — `@@map("catalog entries")` must not produce a name the provider rejects.
|
|
183
|
+
*/
|
|
184
|
+
function safeIdentifierName(raw) {
|
|
185
|
+
return raw.replace(/[^A-Za-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
186
|
+
}
|
|
187
|
+
function qualifiedTableName(model) {
|
|
188
|
+
return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
|
|
189
|
+
}
|
|
@@ -2,12 +2,30 @@ import { Model } from './types';
|
|
|
2
2
|
export declare class PythonGenerator {
|
|
3
3
|
private outputPath;
|
|
4
4
|
constructor(outputPath: string);
|
|
5
|
+
/**
|
|
6
|
+
* The metadata module name, taken from the configured file name itself.
|
|
7
|
+
*
|
|
8
|
+
* The imports used to hardcode `an5_metadata` while `outputPath` was
|
|
9
|
+
* configurable, so setting `python.metadataFile` to another name (say
|
|
10
|
+
* `an5Metadata.py`) still generated `from an5_metadata import ...` and the
|
|
11
|
+
* client broke on import — a failure that only showed up at runtime.
|
|
12
|
+
*/
|
|
13
|
+
private metadataModule;
|
|
14
|
+
/** The database being generated for; decides types like `TIMESTAMP`. */
|
|
15
|
+
private provider;
|
|
5
16
|
generate(models: Model[]): void;
|
|
6
17
|
private getPyFilterType;
|
|
7
18
|
private generateOrmTypes;
|
|
8
19
|
private generateMetadata;
|
|
20
|
+
/**
|
|
21
|
+
* The Python type for a field.
|
|
22
|
+
*
|
|
23
|
+
* From the declared type, so `DECIMAL` is a `float` and `BIGINT` stays an `int`:
|
|
24
|
+
* both arrived as `number` before and took the integer branch.
|
|
25
|
+
*/
|
|
9
26
|
private mapPyType;
|
|
10
|
-
private
|
|
27
|
+
private generateModelFile;
|
|
28
|
+
private generateModelsIndex;
|
|
11
29
|
private generateClient;
|
|
12
30
|
private generateInit;
|
|
13
31
|
private getAllPropertyVariations;
|
|
@@ -6,37 +6,53 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.PythonGenerator = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const type_kinds_1 = require("./type-kinds");
|
|
9
11
|
class PythonGenerator {
|
|
10
12
|
constructor(outputPath) {
|
|
11
13
|
this.outputPath = outputPath;
|
|
12
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
|
+
}
|
|
13
26
|
generate(models) {
|
|
27
|
+
this.provider = models[0]?.provider;
|
|
14
28
|
const outputDir = path_1.default.dirname(this.outputPath);
|
|
15
29
|
if (!fs_1.default.existsSync(outputDir)) {
|
|
16
30
|
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
17
31
|
}
|
|
18
32
|
// 1. Generate an5_metadata.py
|
|
19
33
|
this.generateMetadata(models);
|
|
20
|
-
// 2. Generate
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
23
41
|
this.generateOrmTypes(models, outputDir);
|
|
24
|
-
//
|
|
42
|
+
// 5. Generate an5_client.py
|
|
25
43
|
this.generateClient(models, outputDir);
|
|
26
|
-
//
|
|
44
|
+
// 6. Generate __init__.py
|
|
27
45
|
this.generateInit(models, outputDir);
|
|
28
46
|
}
|
|
29
|
-
getPyFilterType(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return '
|
|
33
|
-
|
|
34
|
-
return '
|
|
35
|
-
|
|
36
|
-
return '
|
|
37
|
-
|
|
38
|
-
return 'NumberFilter';
|
|
39
|
-
return 'StringFilter';
|
|
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
|
+
}
|
|
40
56
|
}
|
|
41
57
|
generateOrmTypes(models, outputDir) {
|
|
42
58
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
@@ -62,7 +78,7 @@ class PythonGenerator {
|
|
|
62
78
|
content += ` OR: Optional[List['${name}WhereInput']] = None\n`;
|
|
63
79
|
content += ` NOT: Optional['${name}WhereInput'] = None\n`;
|
|
64
80
|
for (const f of model.fields) {
|
|
65
|
-
const ft = this.getPyFilterType(f
|
|
81
|
+
const ft = this.getPyFilterType(f);
|
|
66
82
|
content += ` ${this.toSnakeCase(f.name)}: Optional[${ft}] = None\n`;
|
|
67
83
|
}
|
|
68
84
|
content += '\n';
|
|
@@ -77,10 +93,10 @@ class PythonGenerator {
|
|
|
77
93
|
const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
|
|
78
94
|
content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
|
|
79
95
|
for (const f of requiredFields) {
|
|
80
|
-
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f
|
|
96
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
|
|
81
97
|
}
|
|
82
98
|
for (const f of optionalFields) {
|
|
83
|
-
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f
|
|
99
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
84
100
|
}
|
|
85
101
|
if (requiredFields.length === 0 && optionalFields.length === 0)
|
|
86
102
|
content += ' pass\n';
|
|
@@ -89,7 +105,7 @@ class PythonGenerator {
|
|
|
89
105
|
const updateFields = model.fields.filter(f => !f.isId);
|
|
90
106
|
content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
|
|
91
107
|
for (const f of updateFields) {
|
|
92
|
-
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f
|
|
108
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
93
109
|
}
|
|
94
110
|
if (updateFields.length === 0)
|
|
95
111
|
content += ' pass\n';
|
|
@@ -115,7 +131,7 @@ class PythonGenerator {
|
|
|
115
131
|
pyContent += 'MODEL_TO_TABLE = {\n';
|
|
116
132
|
for (const model of models) {
|
|
117
133
|
const props = this.getAllPropertyVariations(model.name);
|
|
118
|
-
const fullTableName =
|
|
134
|
+
const fullTableName = (0, types_1.bracketedTableName)(model);
|
|
119
135
|
for (const prop of props) {
|
|
120
136
|
pyContent += ` "${prop}": "${fullTableName}",\n`;
|
|
121
137
|
}
|
|
@@ -161,49 +177,50 @@ class PythonGenerator {
|
|
|
161
177
|
pyContent += '}\n';
|
|
162
178
|
fs_1.default.writeFileSync(this.outputPath, pyContent);
|
|
163
179
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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';
|
|
171
197
|
}
|
|
172
|
-
if (['bool', 'boolean', 'bit'].includes(lower)) {
|
|
173
|
-
return 'bool';
|
|
174
|
-
}
|
|
175
|
-
if (['datetime', 'datetime2', 'date', 'smalldatetime', 'datetimeoffset', 'timestamp'].includes(lower)) {
|
|
176
|
-
return 'datetime';
|
|
177
|
-
}
|
|
178
|
-
if (['bytes', 'binary', 'varbinary'].includes(lower)) {
|
|
179
|
-
return 'bytes';
|
|
180
|
-
}
|
|
181
|
-
return 'str';
|
|
182
198
|
}
|
|
183
|
-
|
|
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.
|
|
184
202
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
185
203
|
content += 'from dataclasses import dataclass, field\n';
|
|
186
|
-
content += 'from typing import Optional, List, Any\n';
|
|
204
|
+
content += 'from typing import Optional, List, Any, TypedDict\n';
|
|
187
205
|
content += 'from datetime import datetime\n\n';
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
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 {
|
|
197
214
|
// Required fields first
|
|
198
215
|
const required = model.fields.filter(f => !f.isOptional && !f.hasDefault);
|
|
199
216
|
const optionals = model.fields.filter(f => f.isOptional || f.hasDefault);
|
|
200
217
|
for (const f of required) {
|
|
201
|
-
const pyType = this.mapPyType(f
|
|
218
|
+
const pyType = this.mapPyType(f);
|
|
202
219
|
const snakeName = this.toSnakeCase(f.name);
|
|
203
220
|
content += ` ${snakeName}: ${pyType}\n`;
|
|
204
221
|
}
|
|
205
222
|
for (const f of optionals) {
|
|
206
|
-
const pyType = this.mapPyType(f
|
|
223
|
+
const pyType = this.mapPyType(f);
|
|
207
224
|
const snakeName = this.toSnakeCase(f.name);
|
|
208
225
|
content += ` ${snakeName}: Optional[${pyType}] = None\n`;
|
|
209
226
|
}
|
|
@@ -218,21 +235,87 @@ class PythonGenerator {
|
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
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';
|
|
221
291
|
}
|
|
222
292
|
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_models.py'), content);
|
|
223
293
|
}
|
|
224
294
|
generateClient(models, outputDir) {
|
|
225
295
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
226
296
|
content += 'import os\n';
|
|
227
|
-
content += 'from typing import Dict, List, Optional, Any, Callable\n\n';
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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();
|
|
232
305
|
content += 'try:\n';
|
|
233
|
-
content +=
|
|
306
|
+
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
|
|
234
307
|
content += 'except ImportError:\n';
|
|
235
|
-
content +=
|
|
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
|
+
}
|
|
236
319
|
content += 'class An5Client:\n';
|
|
237
320
|
content += ' """AN5 Python ORM Client - type-safe database access.\n\n';
|
|
238
321
|
content += ' Usage:\n';
|
|
@@ -251,13 +334,13 @@ class PythonGenerator {
|
|
|
251
334
|
const propName = this.toSnakeCase(model.name) + 's';
|
|
252
335
|
const singleName = this.toSnakeCase(model.name);
|
|
253
336
|
content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
|
|
254
|
-
content += ` self.${model.name}: AdapterTableClient = client\n`;
|
|
255
|
-
content += ` self.${model.name}s: AdapterTableClient = client\n`;
|
|
256
|
-
content += ` self.${singleName}: AdapterTableClient = client\n`;
|
|
257
|
-
content += ` self.${propName}: AdapterTableClient = client\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`;
|
|
258
341
|
}
|
|
259
342
|
content += '\n';
|
|
260
|
-
content += ' def __getattr__(self, name: str) -> AdapterTableClient:\n';
|
|
343
|
+
content += ' def __getattr__(self, name: str) -> AdapterTableClient[Any]:\n';
|
|
261
344
|
content += ' return self.adapter.table(name)\n\n';
|
|
262
345
|
content += ' def query_raw(self, sql: str, *params) -> List[Dict]:\n';
|
|
263
346
|
content += ' return self.adapter.query_raw(sql, *params)\n\n';
|
|
@@ -269,7 +352,7 @@ class PythonGenerator {
|
|
|
269
352
|
}
|
|
270
353
|
generateInit(models, outputDir) {
|
|
271
354
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
272
|
-
content +=
|
|
355
|
+
content += `from .${this.metadataModule()} import MODEL_TO_TABLE, MODEL_DESCRIPTIONS, MODEL_FIELDS, RELATION_MAP\n`;
|
|
273
356
|
content += 'from .an5_models import *\n';
|
|
274
357
|
content += 'from .an5_orm_types import (\n';
|
|
275
358
|
content += ' StringFilter, IntFilter, NumberFilter, BoolFilter, DateTimeFilter,\n';
|
|
@@ -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
|
+
}
|