@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.
- package/README.md +174 -175
- package/dist/an5Metadata.js +64 -0
- package/dist/an5Orm.d.ts +92 -3
- package/dist/an5Orm.js +697 -206
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/code-generator.d.ts +14 -13
- package/dist/generator/src/code-generator.js +249 -225
- 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 +21 -11
- package/dist/generator/src/dotnet-generator.js +1053 -546
- 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.d.ts +1 -1
- package/dist/generator/src/index.js +137 -88
- package/dist/generator/src/metadata-generator.d.ts +11 -8
- package/dist/generator/src/metadata-generator.js +105 -90
- package/dist/generator/src/parser.d.ts +37 -11
- package/dist/generator/src/parser.js +208 -206
- 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 +37 -9
- package/dist/generator/src/python-generator.js +428 -105
- 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 +60 -27
- package/dist/generator/src/types.js +41 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +19 -8
- package/dist/logger.js +1 -1
- package/dist/migration-core.d.ts +102 -0
- package/dist/migration-core.js +731 -0
- 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/dist/scripts/release-cli.d.ts +2 -2
- package/dist/scripts/release-cli.js +115 -115
- package/dist/sql-utils.d.ts +3 -1
- package/dist/sql-utils.js +94 -24
- package/generator/src/api.ts +45 -0
- package/generator/src/code-generator.ts +259 -230
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +1071 -544
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +1267 -0
- package/generator/src/index.ts +146 -116
- package/generator/src/metadata-generator.ts +116 -101
- package/generator/src/parser.ts +234 -202
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +480 -114
- 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 +77 -29
- package/package.json +32 -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
|
+
}
|
|
@@ -1,9 +1,37 @@
|
|
|
1
|
-
import { Model } from './types';
|
|
2
|
-
export declare class PythonGenerator {
|
|
3
|
-
private outputPath;
|
|
4
|
-
constructor(outputPath: string);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { Model } from './types';
|
|
2
|
+
export declare class PythonGenerator {
|
|
3
|
+
private outputPath;
|
|
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;
|
|
16
|
+
generate(models: Model[]): void;
|
|
17
|
+
private getPyFilterType;
|
|
18
|
+
private generateOrmTypes;
|
|
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
|
+
*/
|
|
26
|
+
private mapPyType;
|
|
27
|
+
private generateModelFile;
|
|
28
|
+
private generateModelsIndex;
|
|
29
|
+
private generateClient;
|
|
30
|
+
private generateInit;
|
|
31
|
+
private getAllPropertyVariations;
|
|
32
|
+
private formatFields;
|
|
33
|
+
private pyString;
|
|
34
|
+
private toCamelCase;
|
|
35
|
+
private toSnakeCase;
|
|
36
|
+
private capitalize;
|
|
37
|
+
}
|