@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,250 @@
|
|
|
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 {
|
|
11
|
+
FieldTypeIssue,
|
|
12
|
+
Provider,
|
|
13
|
+
readFieldLineHead,
|
|
14
|
+
resolveFieldType,
|
|
15
|
+
unknownFieldTypeMessage,
|
|
16
|
+
} from './field-types';
|
|
17
|
+
|
|
18
|
+
/** A column, as push needs it: the type as written plus what the dialect renders. */
|
|
19
|
+
export interface PushField {
|
|
20
|
+
name: string;
|
|
21
|
+
sqlType: string;
|
|
22
|
+
isOptional: boolean;
|
|
23
|
+
isId: boolean;
|
|
24
|
+
isUnique: boolean;
|
|
25
|
+
/** The `@default(...)` expression, not SQL: only the dialect knows the spelling. */
|
|
26
|
+
defaultExpr?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PushModel {
|
|
30
|
+
name: string;
|
|
31
|
+
tableName: string;
|
|
32
|
+
/** The schema to create the table in; `''` when the provider has none. */
|
|
33
|
+
schema: string;
|
|
34
|
+
/** Set only when the schema file named one, so a wrong one can be reported. */
|
|
35
|
+
declaredSchema?: string | undefined;
|
|
36
|
+
fields: PushField[];
|
|
37
|
+
/** `@@unique([a, b])`, as field lists. */
|
|
38
|
+
compoundUniques: PushIndex[];
|
|
39
|
+
/** `@@index([a, b])`, as field lists. */
|
|
40
|
+
indexes: PushIndex[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The schema a model lands in when the schema file does not say: none, for every
|
|
45
|
+
* provider, so the name goes wherever the connection resolves.
|
|
46
|
+
*
|
|
47
|
+
* The parser used to default this to `dbo` and push followed it, which failed two
|
|
48
|
+
* ways: SQLite said "unknown database [dbo]", and a SQL Server login whose default
|
|
49
|
+
* schema was something else had push look for `[dbo].[widgets]`, miss the table it
|
|
50
|
+
* had created, and then fail with "there is already an object named ...".
|
|
51
|
+
*/
|
|
52
|
+
export function defaultSchemaFor(_provider: Provider): string {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One `@@unique` or `@@index`, with the options `db:migrate` already understands.
|
|
58
|
+
*
|
|
59
|
+
* `name` matters as much as the fields: `map:` is how a schema asks for a specific
|
|
60
|
+
* artifact name, and if push invented its own the two commands would create two
|
|
61
|
+
* different constraints for the same directive — and the next migration would keep
|
|
62
|
+
* trying to add the one push never made.
|
|
63
|
+
*/
|
|
64
|
+
export interface PushIndex {
|
|
65
|
+
fields: string[];
|
|
66
|
+
name?: string | undefined;
|
|
67
|
+
includeFields?: string[] | undefined;
|
|
68
|
+
filter?: string | undefined;
|
|
69
|
+
options?: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PushSchema {
|
|
73
|
+
models: PushModel[];
|
|
74
|
+
/** Field types the provider does not have; empty when the schema is valid. */
|
|
75
|
+
issues: FieldTypeIssue[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The field list of `@@unique([a, b], …)`, plus whatever options follow it. */
|
|
79
|
+
const DIRECTIVE = /^@@(?:unique|index)\(\s*\[([\w\s,]+)\]([^)]*)\)/;
|
|
80
|
+
|
|
81
|
+
function parseDirective(line: string, fields: string): PushIndex {
|
|
82
|
+
const options = line.match(DIRECTIVE)?.[2] ?? '';
|
|
83
|
+
const name = options.match(/\bmap\s*:\s*"([^"]+)"/)?.[1];
|
|
84
|
+
const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
|
|
85
|
+
const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
|
|
86
|
+
const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
|
|
87
|
+
return {
|
|
88
|
+
fields: fields.split(',').map((field) => field.trim()).filter(Boolean),
|
|
89
|
+
...(name ? { name } : {}),
|
|
90
|
+
...(include ? { includeFields: include.split(',').map((field) => field.trim()).filter(Boolean) } : {}),
|
|
91
|
+
...(filter ? { filter } : {}),
|
|
92
|
+
...(opts ? { options: opts } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The name a `@@unique` or `@@index` artifact gets.
|
|
98
|
+
*
|
|
99
|
+
* `map:` wins; otherwise the name is derived from the table and fields, which is what
|
|
100
|
+
* both commands have always done for an unnamed directive.
|
|
101
|
+
*/
|
|
102
|
+
export function directiveName(
|
|
103
|
+
directive: PushIndex,
|
|
104
|
+
table: string,
|
|
105
|
+
kind: 'compound' | 'index',
|
|
106
|
+
position: number,
|
|
107
|
+
): string {
|
|
108
|
+
if (directive.name) return safeIdentifierName(directive.name);
|
|
109
|
+
const base = safeIdentifierName(table);
|
|
110
|
+
return kind === 'compound'
|
|
111
|
+
? `UQ_${base}_compound_${position}`
|
|
112
|
+
: `IX_${base}_${directive.fields.join('_')}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reads schema text for push, validating every field type against the provider.
|
|
117
|
+
*
|
|
118
|
+
* All bad types come back in `issues` rather than as an exception, so the caller
|
|
119
|
+
* can print them as a group. A field whose type matches no table is only a
|
|
120
|
+
* mistake once the whole schema has been read: it may be a relation to a model
|
|
121
|
+
* declared further down.
|
|
122
|
+
*/
|
|
123
|
+
export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
124
|
+
const models: PushModel[] = [];
|
|
125
|
+
const issues: FieldTypeIssue[] = [];
|
|
126
|
+
const modelRefs: Array<{ model: string; field: string; type: string }> = [];
|
|
127
|
+
let current: PushModel | null = null;
|
|
128
|
+
|
|
129
|
+
for (const raw of text.split('\n')) {
|
|
130
|
+
const line = raw.trim();
|
|
131
|
+
if (!line || line.startsWith('//')) continue;
|
|
132
|
+
|
|
133
|
+
const header = line.match(/^model\s+(\w+)\s*\{/);
|
|
134
|
+
if (header) {
|
|
135
|
+
current = {
|
|
136
|
+
name: header[1],
|
|
137
|
+
tableName: header[1].toLowerCase() + 's',
|
|
138
|
+
schema: defaultSchemaFor(provider),
|
|
139
|
+
fields: [],
|
|
140
|
+
compoundUniques: [],
|
|
141
|
+
indexes: [],
|
|
142
|
+
};
|
|
143
|
+
models.push(current);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (line === '}') {
|
|
148
|
+
current = null;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!current) continue;
|
|
152
|
+
|
|
153
|
+
if (line.startsWith('@@map')) {
|
|
154
|
+
const match = line.match(/@@map\("(.+)"\)/);
|
|
155
|
+
if (match) current.tableName = match[1];
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith('@@schema')) {
|
|
159
|
+
// `(.*)` rather than `(.+)`, because `@@schema("")` is how a model says "no
|
|
160
|
+
// schema" and has to survive as an empty string rather than being dropped as
|
|
161
|
+
// no match — which is how SQLite and MySQL schemas are written.
|
|
162
|
+
const match = line.match(/@@schema\("(.*)"\)/);
|
|
163
|
+
if (match) {
|
|
164
|
+
current.schema = match[1].trim();
|
|
165
|
+
current.declaredSchema = current.schema;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (line.startsWith('@@unique') || line.startsWith('@@index')) {
|
|
170
|
+
const match = line.match(DIRECTIVE);
|
|
171
|
+
if (match) {
|
|
172
|
+
const definition = parseDirective(line, match[1] ?? '');
|
|
173
|
+
if (line.startsWith('@@unique')) current.compoundUniques.push(definition);
|
|
174
|
+
else current.indexes.push(definition);
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.startsWith('@@')) continue;
|
|
179
|
+
|
|
180
|
+
const head = readFieldLineHead(line);
|
|
181
|
+
if (!head) continue;
|
|
182
|
+
|
|
183
|
+
if (!resolveFieldType(head.type, provider)) {
|
|
184
|
+
modelRefs.push({ model: current.name, field: head.name, type: head.type });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The type is kept as written (upper-cased) because that is what the database
|
|
189
|
+
// is told; which types exist is what `resolveFieldType` just checked.
|
|
190
|
+
current.fields.push({
|
|
191
|
+
name: head.name,
|
|
192
|
+
sqlType: head.type.toUpperCase(),
|
|
193
|
+
isOptional: head.isOptional,
|
|
194
|
+
isId: line.includes('@id'),
|
|
195
|
+
isUnique: line.includes('@unique'),
|
|
196
|
+
// The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
|
|
197
|
+
// differently per provider, and only the dialect knows how. `@updatedAt` is
|
|
198
|
+
// `now()` under another name.
|
|
199
|
+
defaultExpr: line.match(/@default\((.*)\)/)?.[1]
|
|
200
|
+
?? (line.includes('@updatedAt') ? 'now()' : undefined),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A declared `dbo` on a provider without schemas would push to a database that
|
|
205
|
+
// does not exist there; SQLite answers "unknown database [dbo]", which says
|
|
206
|
+
// nothing about the schema file. Only an explicit declaration is worth reporting:
|
|
207
|
+
// an absent one now means "whatever the connection resolves", on every provider.
|
|
208
|
+
for (const model of models) {
|
|
209
|
+
if (model.declaredSchema === 'dbo' && provider !== 'mssql') {
|
|
210
|
+
issues.push({
|
|
211
|
+
path: model.name,
|
|
212
|
+
message:
|
|
213
|
+
`schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
|
|
214
|
+
'to the schema this database uses',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const modelNames = models.map((model) => model.name);
|
|
220
|
+
for (const ref of modelRefs) {
|
|
221
|
+
if (!modelNames.includes(ref.type)) {
|
|
222
|
+
issues.push({
|
|
223
|
+
path: `${ref.model}.${ref.field}`,
|
|
224
|
+
message: unknownFieldTypeMessage(ref.type, provider, modelNames),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { models, issues };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The table name to create, `schema.table` unless the model cleared the schema.
|
|
234
|
+
*
|
|
235
|
+
* An empty schema has to stay empty: `[].[widgets]` is not valid SQL, which is the
|
|
236
|
+
* whole reason `@@schema("")` exists.
|
|
237
|
+
*/
|
|
238
|
+
/**
|
|
239
|
+
* An identifier for a generated artifact name: letters, digits and underscores only.
|
|
240
|
+
*
|
|
241
|
+
* The name goes into DDL as an identifier, so anything else is replaced rather than
|
|
242
|
+
* quoted — `@@map("catalog entries")` must not produce a name the provider rejects.
|
|
243
|
+
*/
|
|
244
|
+
export function safeIdentifierName(raw: string): string {
|
|
245
|
+
return raw.replace(/[^A-Za-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function qualifiedTableName(model: PushModel): string {
|
|
249
|
+
return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
|
|
250
|
+
}
|