ormbridge 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +408 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/cliRunner.d.ts +20 -0
- package/dist/cliRunner.js +343 -0
- package/dist/cliRunner.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +204 -0
- package/dist/config.js.map +1 -0
- package/dist/convert.d.ts +20 -0
- package/dist/convert.js +61 -0
- package/dist/convert.js.map +1 -0
- package/dist/emitters/django.d.ts +4 -0
- package/dist/emitters/django.js +501 -0
- package/dist/emitters/django.js.map +1 -0
- package/dist/emitters/doctrine.d.ts +20 -0
- package/dist/emitters/doctrine.js +1134 -0
- package/dist/emitters/doctrine.js.map +1 -0
- package/dist/emitters/graphene.d.ts +4 -0
- package/dist/emitters/graphene.js +431 -0
- package/dist/emitters/graphene.js.map +1 -0
- package/dist/emitters/laravel.d.ts +33 -0
- package/dist/emitters/laravel.js +1695 -0
- package/dist/emitters/laravel.js.map +1 -0
- package/dist/emitters/prisma.d.ts +21 -0
- package/dist/emitters/prisma.js +1058 -0
- package/dist/emitters/prisma.js.map +1 -0
- package/dist/emitters/typeorm.d.ts +9 -0
- package/dist/emitters/typeorm.js +833 -0
- package/dist/emitters/typeorm.js.map +1 -0
- package/dist/emitters/typescriptInterfaces.d.ts +15 -0
- package/dist/emitters/typescriptInterfaces.js +350 -0
- package/dist/emitters/typescriptInterfaces.js.map +1 -0
- package/dist/formats.d.ts +80 -0
- package/dist/formats.js +212 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +48 -0
- package/dist/io.js +328 -0
- package/dist/io.js.map +1 -0
- package/dist/ir.d.ts +197 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +88 -0
- package/dist/naming.js.map +1 -0
- package/dist/parsers/django.d.ts +14 -0
- package/dist/parsers/django.js +1161 -0
- package/dist/parsers/django.js.map +1 -0
- package/dist/parsers/doctrine.d.ts +12 -0
- package/dist/parsers/doctrine.js +1782 -0
- package/dist/parsers/doctrine.js.map +1 -0
- package/dist/parsers/laravel.d.ts +17 -0
- package/dist/parsers/laravel.js +2824 -0
- package/dist/parsers/laravel.js.map +1 -0
- package/dist/parsers/phpSyntax.d.ts +124 -0
- package/dist/parsers/phpSyntax.js +429 -0
- package/dist/parsers/phpSyntax.js.map +1 -0
- package/dist/parsers/prisma.d.ts +12 -0
- package/dist/parsers/prisma.js +964 -0
- package/dist/parsers/prisma.js.map +1 -0
- package/dist/parsers/pythonSyntax.d.ts +38 -0
- package/dist/parsers/pythonSyntax.js +120 -0
- package/dist/parsers/pythonSyntax.js.map +1 -0
- package/dist/parsers/typeorm.d.ts +12 -0
- package/dist/parsers/typeorm.js +1614 -0
- package/dist/parsers/typeorm.js.map +1 -0
- package/dist/parsers/typescriptSyntax.d.ts +57 -0
- package/dist/parsers/typescriptSyntax.js +200 -0
- package/dist/parsers/typescriptSyntax.js.map +1 -0
- package/dist/prismaOnlyConstructs.d.ts +7 -0
- package/dist/prismaOnlyConstructs.js +76 -0
- package/dist/prismaOnlyConstructs.js.map +1 -0
- package/dist/result.d.ts +20 -0
- package/dist/result.js +18 -0
- package/dist/result.js.map +1 -0
- package/dist/transforms.d.ts +13 -0
- package/dist/transforms.js +142 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +83 -4
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
import { toCamelCase, toPascalCase, toSnakeCase } from '../naming.js';
|
|
2
|
+
import { err, ok } from '../result.js';
|
|
3
|
+
import { decoratorsOf, evaluateNode, getTypeScriptParser, lastSegment, } from './typescriptSyntax.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Static tables
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Column type names (lower case) accepted by TypeORM, mapped to the IR scalar type. */
|
|
8
|
+
const COLUMN_TYPES = {
|
|
9
|
+
varchar: 'string',
|
|
10
|
+
'character varying': 'string',
|
|
11
|
+
varying: 'string',
|
|
12
|
+
nvarchar: 'string',
|
|
13
|
+
nvarchar2: 'string',
|
|
14
|
+
varchar2: 'string',
|
|
15
|
+
char: 'string',
|
|
16
|
+
character: 'string',
|
|
17
|
+
nchar: 'string',
|
|
18
|
+
bpchar: 'string',
|
|
19
|
+
citext: 'string',
|
|
20
|
+
string: 'string',
|
|
21
|
+
text: 'text',
|
|
22
|
+
tinytext: 'text',
|
|
23
|
+
mediumtext: 'text',
|
|
24
|
+
longtext: 'text',
|
|
25
|
+
ntext: 'text',
|
|
26
|
+
clob: 'text',
|
|
27
|
+
nclob: 'text',
|
|
28
|
+
'simple-array': 'text',
|
|
29
|
+
int: 'int',
|
|
30
|
+
int2: 'int',
|
|
31
|
+
int4: 'int',
|
|
32
|
+
int8: 'bigInt',
|
|
33
|
+
integer: 'int',
|
|
34
|
+
smallint: 'int',
|
|
35
|
+
tinyint: 'int',
|
|
36
|
+
mediumint: 'int',
|
|
37
|
+
number: 'int',
|
|
38
|
+
serial: 'int',
|
|
39
|
+
smallserial: 'int',
|
|
40
|
+
bigserial: 'bigInt',
|
|
41
|
+
bigint: 'bigInt',
|
|
42
|
+
'unsigned big int': 'bigInt',
|
|
43
|
+
float: 'float',
|
|
44
|
+
float4: 'float',
|
|
45
|
+
float8: 'float',
|
|
46
|
+
double: 'float',
|
|
47
|
+
'double precision': 'float',
|
|
48
|
+
real: 'float',
|
|
49
|
+
decimal: 'decimal',
|
|
50
|
+
dec: 'decimal',
|
|
51
|
+
numeric: 'decimal',
|
|
52
|
+
fixed: 'decimal',
|
|
53
|
+
money: 'decimal',
|
|
54
|
+
smallmoney: 'decimal',
|
|
55
|
+
boolean: 'boolean',
|
|
56
|
+
bool: 'boolean',
|
|
57
|
+
bit: 'boolean',
|
|
58
|
+
date: 'date',
|
|
59
|
+
time: 'time',
|
|
60
|
+
timetz: 'time',
|
|
61
|
+
'time with time zone': 'time',
|
|
62
|
+
'time without time zone': 'time',
|
|
63
|
+
datetime: 'dateTime',
|
|
64
|
+
datetime2: 'dateTime',
|
|
65
|
+
datetimeoffset: 'dateTime',
|
|
66
|
+
smalldatetime: 'dateTime',
|
|
67
|
+
timestamp: 'dateTime',
|
|
68
|
+
timestamptz: 'dateTime',
|
|
69
|
+
'timestamp with time zone': 'dateTime',
|
|
70
|
+
'timestamp without time zone': 'dateTime',
|
|
71
|
+
'timestamp with local time zone': 'dateTime',
|
|
72
|
+
uuid: 'uuid',
|
|
73
|
+
uniqueidentifier: 'uuid',
|
|
74
|
+
json: 'json',
|
|
75
|
+
jsonb: 'json',
|
|
76
|
+
'simple-json': 'json',
|
|
77
|
+
bytea: 'bytes',
|
|
78
|
+
blob: 'bytes',
|
|
79
|
+
tinyblob: 'bytes',
|
|
80
|
+
mediumblob: 'bytes',
|
|
81
|
+
longblob: 'bytes',
|
|
82
|
+
binary: 'bytes',
|
|
83
|
+
varbinary: 'bytes',
|
|
84
|
+
image: 'bytes',
|
|
85
|
+
raw: 'bytes',
|
|
86
|
+
};
|
|
87
|
+
/** Global constructors accepted as a column type, for example `@Column(String)`. */
|
|
88
|
+
const CONSTRUCTOR_TYPES = {
|
|
89
|
+
String: 'string',
|
|
90
|
+
Number: 'int',
|
|
91
|
+
Boolean: 'boolean',
|
|
92
|
+
Date: 'dateTime',
|
|
93
|
+
Buffer: 'bytes',
|
|
94
|
+
BigInt: 'bigInt',
|
|
95
|
+
};
|
|
96
|
+
/** Column types whose storage differs from the IR type in a way worth telling the user about. */
|
|
97
|
+
const APPROXIMATED_TYPES = {
|
|
98
|
+
'simple-array': 'stored as comma-separated text; the IR has no array type, so it was converted to text',
|
|
99
|
+
money: 'converted to a decimal column',
|
|
100
|
+
smallmoney: 'converted to a decimal column',
|
|
101
|
+
bit: 'converted to a boolean column',
|
|
102
|
+
};
|
|
103
|
+
const TS_TYPE_MAP = {
|
|
104
|
+
string: 'string',
|
|
105
|
+
number: 'int',
|
|
106
|
+
boolean: 'boolean',
|
|
107
|
+
bigint: 'bigInt',
|
|
108
|
+
Date: 'dateTime',
|
|
109
|
+
Buffer: 'bytes',
|
|
110
|
+
Uint8Array: 'bytes',
|
|
111
|
+
};
|
|
112
|
+
const JSON_LIKE_TS_TYPES = new Set([
|
|
113
|
+
'object',
|
|
114
|
+
'Object',
|
|
115
|
+
'Record',
|
|
116
|
+
'any',
|
|
117
|
+
'unknown',
|
|
118
|
+
]);
|
|
119
|
+
const ON_DELETE_MAP = {
|
|
120
|
+
CASCADE: 'cascade',
|
|
121
|
+
'SET NULL': 'setNull',
|
|
122
|
+
RESTRICT: 'restrict',
|
|
123
|
+
'NO ACTION': 'noAction',
|
|
124
|
+
'SET DEFAULT': 'setDefault',
|
|
125
|
+
};
|
|
126
|
+
const NOW_SQL = /^(now\(\)|current_timestamp(\(\d*\))?|getdate\(\)|sysdate|systimestamp|localtimestamp(\(\d*\))?|datetime\('now'\)|\(datetime\('now'\)\))$/i;
|
|
127
|
+
const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|newid\(\)|newsequentialid\(\)|sys_guid\(\))$/i;
|
|
128
|
+
const QUOTED_SQL = /^'((?:[^']|'')*)'(::[\w\s]+)?$/;
|
|
129
|
+
const RELATION_DECORATORS = new Set([
|
|
130
|
+
'OneToOne',
|
|
131
|
+
'ManyToOne',
|
|
132
|
+
'OneToMany',
|
|
133
|
+
'ManyToMany',
|
|
134
|
+
]);
|
|
135
|
+
const COLUMN_DECORATORS = new Set([
|
|
136
|
+
'Column',
|
|
137
|
+
'PrimaryColumn',
|
|
138
|
+
'PrimaryGeneratedColumn',
|
|
139
|
+
'CreateDateColumn',
|
|
140
|
+
'UpdateDateColumn',
|
|
141
|
+
'DeleteDateColumn',
|
|
142
|
+
'VersionColumn',
|
|
143
|
+
]);
|
|
144
|
+
/** Property decorators that define schema we cannot represent; each produces a warning. */
|
|
145
|
+
const UNSUPPORTED_PROPERTY_DECORATORS = {
|
|
146
|
+
ObjectIdColumn: 'MongoDB object id columns are not supported',
|
|
147
|
+
VirtualColumn: 'virtual (computed) columns are not supported',
|
|
148
|
+
RelationId: '@RelationId is a runtime-only helper and has no column',
|
|
149
|
+
TreeChildren: 'tree entities (closure/materialized path) are not supported',
|
|
150
|
+
TreeParent: 'tree entities (closure/materialized path) are not supported',
|
|
151
|
+
TreeLevelColumn: 'tree entities (closure/materialized path) are not supported',
|
|
152
|
+
ViewColumn: 'view columns are not supported',
|
|
153
|
+
};
|
|
154
|
+
/** Column options that change the database schema but have no IR equivalent. */
|
|
155
|
+
const UNSUPPORTED_COLUMN_OPTIONS = {
|
|
156
|
+
unsigned: 'unsigned integers',
|
|
157
|
+
zerofill: 'zerofill',
|
|
158
|
+
collation: 'a collation',
|
|
159
|
+
charset: 'a character set',
|
|
160
|
+
onUpdate: 'an ON UPDATE expression',
|
|
161
|
+
asExpression: 'a generated (computed) column expression',
|
|
162
|
+
generatedType: 'a generated (computed) column',
|
|
163
|
+
transformer: 'a value transformer',
|
|
164
|
+
hstoreType: 'an hstore type',
|
|
165
|
+
spatialFeatureType: 'a spatial feature type',
|
|
166
|
+
srid: 'a spatial reference id',
|
|
167
|
+
primaryKeyConstraintName: 'a primary key constraint name',
|
|
168
|
+
};
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Public entry point
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
/** Parses TypeORM entity files into the shared IR using tree-sitter (no Node project required). */
|
|
173
|
+
export async function parseTypeorm(sources, options) {
|
|
174
|
+
const parserResult = await getTypeScriptParser();
|
|
175
|
+
if (!parserResult.ok) {
|
|
176
|
+
return parserResult;
|
|
177
|
+
}
|
|
178
|
+
const parser = parserResult.value;
|
|
179
|
+
const warnings = [];
|
|
180
|
+
const classes = [];
|
|
181
|
+
const enumInfos = new Map();
|
|
182
|
+
const irEnums = new Map();
|
|
183
|
+
// Enums are collected from every file first, so a column can reference an enum from another file.
|
|
184
|
+
const trees = [];
|
|
185
|
+
for (const source of sources) {
|
|
186
|
+
const tree = parser.parse(source.text);
|
|
187
|
+
trees.push({ source, tree });
|
|
188
|
+
collectEnums(tree.rootNode, source.path, enumInfos, irEnums, warnings);
|
|
189
|
+
}
|
|
190
|
+
for (const { source, tree } of trees) {
|
|
191
|
+
classes.push(...parseClasses(tree, source, enumInfos, irEnums, warnings));
|
|
192
|
+
}
|
|
193
|
+
const schema = buildSchema(classes, [...irEnums.values()], warnings, options);
|
|
194
|
+
if (schema.models.length === 0) {
|
|
195
|
+
const checkedPaths = sources
|
|
196
|
+
.map((source) => source.path)
|
|
197
|
+
.join(', ');
|
|
198
|
+
return err('NO_MODELS_FOUND', `No TypeORM entities were found in: ${checkedPaths}. An entity is a non-abstract class decorated with @Entity() ` +
|
|
199
|
+
`(EntitySchema definitions are not supported).`);
|
|
200
|
+
}
|
|
201
|
+
return ok(schema);
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// File parsing
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
/** Reads the top-level declarations of a file, unwrapping `export` statements. */
|
|
207
|
+
function topLevelDeclarations(root) {
|
|
208
|
+
const declarations = [];
|
|
209
|
+
for (const statement of root.namedChildren) {
|
|
210
|
+
if (statement.type === 'export_statement') {
|
|
211
|
+
const decorators = decoratorsOf(statement);
|
|
212
|
+
for (const node of statement.namedChildren) {
|
|
213
|
+
declarations.push({ node, decorators });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
declarations.push({ node: statement, decorators: [] });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return declarations;
|
|
221
|
+
}
|
|
222
|
+
function collectEnums(root, filePath, enums, irEnums, warnings) {
|
|
223
|
+
for (const { node } of topLevelDeclarations(root)) {
|
|
224
|
+
const parsedEnum = node.type === 'enum_declaration'
|
|
225
|
+
? parseEnumDeclaration(node)
|
|
226
|
+
: node.type === 'lexical_declaration'
|
|
227
|
+
? parseConstObjectEnum(node)
|
|
228
|
+
: undefined;
|
|
229
|
+
if (parsedEnum !== undefined) {
|
|
230
|
+
registerEnum(parsedEnum, enums, irEnums, filePath, warnings);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function parseClasses(tree, source, enums, irEnums, sharedWarnings) {
|
|
235
|
+
if (tree.rootNode.hasError) {
|
|
236
|
+
sharedWarnings.push(`${source.path}: the file contains TypeScript syntax errors; some entities or columns may be missing from the output.`);
|
|
237
|
+
}
|
|
238
|
+
if (/\bnew\s+EntitySchema\b/.test(source.text)) {
|
|
239
|
+
sharedWarnings.push(`${source.path}: EntitySchema definitions are not supported and were skipped; use decorated entity classes.`);
|
|
240
|
+
}
|
|
241
|
+
const aliases = collectImportAliases(tree.rootNode);
|
|
242
|
+
const context = {
|
|
243
|
+
filePath: source.path,
|
|
244
|
+
enums,
|
|
245
|
+
newEnums: [],
|
|
246
|
+
warnings: sharedWarnings,
|
|
247
|
+
};
|
|
248
|
+
const classes = [];
|
|
249
|
+
for (const { node, decorators } of topLevelDeclarations(tree.rootNode)) {
|
|
250
|
+
if (node.type !== 'class_declaration' &&
|
|
251
|
+
node.type !== 'abstract_class_declaration' &&
|
|
252
|
+
node.type !== 'class') {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const rawClass = parseClass(node, [...decorators, ...decoratorsOf(node)].map((decorator) => normalizeDecorator(decorator, aliases)), aliases, context);
|
|
256
|
+
if (rawClass !== undefined) {
|
|
257
|
+
classes.push(rawClass);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const generated of context.newEnums) {
|
|
261
|
+
irEnums.set(generated.name, generated);
|
|
262
|
+
}
|
|
263
|
+
return classes;
|
|
264
|
+
}
|
|
265
|
+
function collectImportAliases(root) {
|
|
266
|
+
const aliases = new Map();
|
|
267
|
+
for (const statement of root.namedChildren) {
|
|
268
|
+
if (statement.type !== 'import_statement') {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const stack = [...statement.namedChildren];
|
|
272
|
+
while (stack.length > 0) {
|
|
273
|
+
const node = stack.pop();
|
|
274
|
+
if (node === undefined) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (node.type === 'import_specifier') {
|
|
278
|
+
const nameNode = node.childForFieldName('name');
|
|
279
|
+
const aliasNode = node.childForFieldName('alias');
|
|
280
|
+
if (nameNode !== null && aliasNode !== null) {
|
|
281
|
+
aliases.set(aliasNode.text, nameNode.text);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
stack.push(...node.namedChildren);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return aliases;
|
|
290
|
+
}
|
|
291
|
+
function normalizeDecorator(decorator, aliases) {
|
|
292
|
+
const original = aliases.get(decorator.name);
|
|
293
|
+
return original === undefined ? decorator : { ...decorator, name: original };
|
|
294
|
+
}
|
|
295
|
+
function parseEnumDeclaration(node) {
|
|
296
|
+
const nameNode = node.childForFieldName('name');
|
|
297
|
+
const bodyNode = node.childForFieldName('body');
|
|
298
|
+
if (nameNode === null || bodyNode === null) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const values = [];
|
|
302
|
+
let isNumeric = false;
|
|
303
|
+
for (const member of bodyNode.namedChildren) {
|
|
304
|
+
if (member.type === 'property_identifier') {
|
|
305
|
+
isNumeric = true;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (member.type !== 'enum_assignment') {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const memberName = member.childForFieldName('name');
|
|
312
|
+
const memberValue = member.childForFieldName('value');
|
|
313
|
+
if (memberName === null || memberValue === null) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const evaluated = evaluateNode(memberValue);
|
|
317
|
+
if (evaluated.kind === 'string') {
|
|
318
|
+
values.push({
|
|
319
|
+
name: unquoteName(memberName.text),
|
|
320
|
+
dbValue: evaluated.value,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
isNumeric = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const info = isNumeric
|
|
328
|
+
? { kind: 'numeric' }
|
|
329
|
+
: { kind: 'string', values };
|
|
330
|
+
return { name: nameNode.text, info };
|
|
331
|
+
}
|
|
332
|
+
/** `export const Status = { A: 'a', B: 'b' } as const;` is treated like a string enum. */
|
|
333
|
+
function parseConstObjectEnum(node) {
|
|
334
|
+
for (const declarator of node.namedChildren) {
|
|
335
|
+
if (declarator.type !== 'variable_declarator') {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const nameNode = declarator.childForFieldName('name');
|
|
339
|
+
const valueNode = declarator.childForFieldName('value');
|
|
340
|
+
if (nameNode === null || valueNode === null) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const evaluated = evaluateNode(valueNode);
|
|
344
|
+
if (evaluated.kind !== 'object') {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const entries = Object.entries(evaluated.properties);
|
|
348
|
+
if (entries.length === 0 ||
|
|
349
|
+
!entries.every(([, value]) => value.kind === 'string')) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const values = entries.flatMap(([key, value]) => value.kind === 'string' ? [{ name: key, dbValue: value.value }] : []);
|
|
353
|
+
return { name: nameNode.text, info: { kind: 'string', values } };
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
function unquoteName(text) {
|
|
358
|
+
return /^(["']).*\1$/.test(text) ? text.slice(1, -1) : text;
|
|
359
|
+
}
|
|
360
|
+
function registerEnum(parsedEnum, enums, irEnums, filePath, warnings) {
|
|
361
|
+
enums.set(parsedEnum.name, parsedEnum.info);
|
|
362
|
+
if (parsedEnum.info.kind === 'string') {
|
|
363
|
+
if (irEnums.has(parsedEnum.name)) {
|
|
364
|
+
warnings.push(`Duplicate enum name "${parsedEnum.name}" (${filePath}); only the first definition was used.`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Enums are registered eagerly so every file can reference them; unused ones are pruned later.
|
|
368
|
+
irEnums.set(parsedEnum.name, {
|
|
369
|
+
name: parsedEnum.name,
|
|
370
|
+
values: parsedEnum.info.values,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Value helpers
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
function objectArgument(args) {
|
|
378
|
+
for (const arg of args) {
|
|
379
|
+
if (arg.kind === 'object') {
|
|
380
|
+
return arg;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
function stringOption(options, key) {
|
|
386
|
+
const value = options?.properties[key];
|
|
387
|
+
return value !== undefined && value.kind === 'string'
|
|
388
|
+
? value.value
|
|
389
|
+
: undefined;
|
|
390
|
+
}
|
|
391
|
+
function boolOption(options, key) {
|
|
392
|
+
const value = options?.properties[key];
|
|
393
|
+
return value !== undefined && value.kind === 'bool' ? value.value : undefined;
|
|
394
|
+
}
|
|
395
|
+
function numberOption(options, key) {
|
|
396
|
+
const value = options?.properties[key];
|
|
397
|
+
if (value === undefined) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
if (value.kind === 'number') {
|
|
401
|
+
return value.value;
|
|
402
|
+
}
|
|
403
|
+
if (value.kind === 'string' && /^\d+$/.test(value.value)) {
|
|
404
|
+
return Number(value.value);
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
/** True for an option explicitly switched off (`false`, `null`). */
|
|
409
|
+
function isOff(value) {
|
|
410
|
+
return (value.kind === 'bool' && !value.value) || value.kind === 'null';
|
|
411
|
+
}
|
|
412
|
+
/** The class name an arrow function such as `() => User` or `(type) => User` returns. */
|
|
413
|
+
function arrowTargetName(value) {
|
|
414
|
+
if (value === undefined) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
if (value.kind === 'string') {
|
|
418
|
+
return lastSegment(value.value);
|
|
419
|
+
}
|
|
420
|
+
if (value.kind === 'arrow' && value.body.kind === 'name') {
|
|
421
|
+
return lastSegment(value.body.value);
|
|
422
|
+
}
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
/** The property an inverse-side function such as `(user) => user.posts` points at. */
|
|
426
|
+
function inverseSideName(value) {
|
|
427
|
+
if (value === undefined) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
if (value.kind === 'string') {
|
|
431
|
+
return value.value;
|
|
432
|
+
}
|
|
433
|
+
if (value.kind === 'arrow' && value.body.kind === 'name') {
|
|
434
|
+
const firstParam = value.params[0];
|
|
435
|
+
const text = value.body.value;
|
|
436
|
+
if (firstParam !== undefined && text.startsWith(`${firstParam}.`)) {
|
|
437
|
+
return text.slice(firstParam.length + 1);
|
|
438
|
+
}
|
|
439
|
+
return lastSegment(text);
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// TypeScript type annotations
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
function describeTypeAnnotation(propertyNode) {
|
|
447
|
+
const annotation = propertyNode.childForFieldName('type');
|
|
448
|
+
const typeNode = annotation?.namedChildren[0];
|
|
449
|
+
const info = {
|
|
450
|
+
names: [],
|
|
451
|
+
literals: [],
|
|
452
|
+
nullable: false,
|
|
453
|
+
isArray: false,
|
|
454
|
+
text: typeNode?.text ?? '',
|
|
455
|
+
};
|
|
456
|
+
if (typeNode !== undefined) {
|
|
457
|
+
collectTypeNames(typeNode, info);
|
|
458
|
+
}
|
|
459
|
+
return info;
|
|
460
|
+
}
|
|
461
|
+
function collectTypeNames(node, info) {
|
|
462
|
+
switch (node.type) {
|
|
463
|
+
case 'union_type':
|
|
464
|
+
case 'parenthesized_type':
|
|
465
|
+
for (const child of node.namedChildren) {
|
|
466
|
+
collectTypeNames(child, info);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
case 'literal_type': {
|
|
470
|
+
const inner = node.namedChildren[0];
|
|
471
|
+
if (inner === undefined) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (inner.type === 'null' || inner.type === 'undefined') {
|
|
475
|
+
info.nullable = true;
|
|
476
|
+
}
|
|
477
|
+
else if (inner.type === 'string') {
|
|
478
|
+
const value = evaluateNode(inner);
|
|
479
|
+
if (value.kind === 'string') {
|
|
480
|
+
info.literals.push(value.value);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
info.names.push(inner.type === 'number' ? 'number' : inner.text);
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
case 'predefined_type':
|
|
489
|
+
if (node.text === 'undefined' || node.text === 'null') {
|
|
490
|
+
info.nullable = true;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
info.names.push(node.text);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
case 'type_identifier':
|
|
497
|
+
case 'nested_type_identifier':
|
|
498
|
+
if (node.text === 'undefined' || node.text === 'null') {
|
|
499
|
+
info.nullable = true;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
info.names.push(lastSegment(node.text));
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
case 'array_type': {
|
|
506
|
+
info.isArray = true;
|
|
507
|
+
const element = node.namedChildren[0];
|
|
508
|
+
if (element !== undefined) {
|
|
509
|
+
collectTypeNames(element, info);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
case 'generic_type': {
|
|
514
|
+
const base = lastSegment(node.namedChildren[0]?.text ?? '');
|
|
515
|
+
const argumentNode = node.namedChildren[1]?.namedChildren[0];
|
|
516
|
+
if (base === 'Array' || base === 'ReadonlyArray') {
|
|
517
|
+
info.isArray = true;
|
|
518
|
+
}
|
|
519
|
+
if (argumentNode !== undefined &&
|
|
520
|
+
['Relation', 'Promise', 'Awaited', 'Array', 'ReadonlyArray'].includes(base)) {
|
|
521
|
+
collectTypeNames(argumentNode, info);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
info.names.push(base);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
default:
|
|
529
|
+
info.names.push(node.text);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Class parsing
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
function parseClass(node, decorators, aliases, context) {
|
|
536
|
+
const nameNode = node.childForFieldName('name');
|
|
537
|
+
const bodyNode = node.childForFieldName('body');
|
|
538
|
+
if (nameNode === null || bodyNode === null) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
const className = nameNode.text;
|
|
542
|
+
const entityDecorator = decorators.find((decorator) => decorator.name === 'Entity');
|
|
543
|
+
const rawClass = {
|
|
544
|
+
name: className,
|
|
545
|
+
filePath: context.filePath,
|
|
546
|
+
isAbstract: node.type === 'abstract_class_declaration',
|
|
547
|
+
isEntity: entityDecorator !== undefined,
|
|
548
|
+
isChildEntity: decorators.some((decorator) => decorator.name === 'ChildEntity'),
|
|
549
|
+
hasComplexBase: false,
|
|
550
|
+
members: [],
|
|
551
|
+
indexes: [],
|
|
552
|
+
};
|
|
553
|
+
const heritage = node.namedChildren.find((child) => child.type === 'class_heritage');
|
|
554
|
+
const extendsClause = heritage?.namedChildren.find((child) => child.type === 'extends_clause');
|
|
555
|
+
const baseNode = extendsClause?.namedChildren[0];
|
|
556
|
+
if (baseNode !== undefined) {
|
|
557
|
+
if (baseNode.type === 'identifier' ||
|
|
558
|
+
baseNode.type === 'member_expression') {
|
|
559
|
+
rawClass.baseName = lastSegment(baseNode.text.replace(/\s+/g, ''));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
rawClass.hasComplexBase = true;
|
|
563
|
+
context.warnings.push(`${className}: the base class expression "${baseNode.text}" is not a plain class name, so any columns it adds are missing.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
parseClassDecorators(rawClass, decorators, context);
|
|
567
|
+
for (const member of bodyNode.namedChildren) {
|
|
568
|
+
if (member.type === 'public_field_definition') {
|
|
569
|
+
parseProperty(member, rawClass, aliases, context);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return rawClass;
|
|
573
|
+
}
|
|
574
|
+
function parseClassDecorators(rawClass, decorators, context) {
|
|
575
|
+
const className = rawClass.name;
|
|
576
|
+
for (const decorator of decorators) {
|
|
577
|
+
switch (decorator.name) {
|
|
578
|
+
case 'Entity': {
|
|
579
|
+
const options = objectArgument(decorator.args);
|
|
580
|
+
const first = decorator.args[0];
|
|
581
|
+
const tableName = first !== undefined && first.kind === 'string'
|
|
582
|
+
? first.value
|
|
583
|
+
: stringOption(options, 'name');
|
|
584
|
+
if (tableName !== undefined) {
|
|
585
|
+
rawClass.tableName = tableName;
|
|
586
|
+
}
|
|
587
|
+
for (const key of ['schema', 'database']) {
|
|
588
|
+
const value = stringOption(options, key);
|
|
589
|
+
if (value !== undefined) {
|
|
590
|
+
context.warnings.push(`${className}: the ${key} "${value}" given to @Entity was ignored; only the table name is converted.`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 'Index':
|
|
596
|
+
case 'Unique': {
|
|
597
|
+
const index = parseIndexDecorator(decorator, undefined, `${className}`, context);
|
|
598
|
+
if (index !== undefined) {
|
|
599
|
+
rawClass.indexes.push(index);
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case 'ChildEntity':
|
|
604
|
+
context.warnings.push(`${className}: @ChildEntity (single-table inheritance) is not supported; the entity and its columns were skipped.`);
|
|
605
|
+
break;
|
|
606
|
+
case 'TableInheritance':
|
|
607
|
+
context.warnings.push(`${className}: @TableInheritance (single-table inheritance) is not supported; the discriminator column was not added and each class is converted as its own table.`);
|
|
608
|
+
break;
|
|
609
|
+
case 'ViewEntity':
|
|
610
|
+
context.warnings.push(`${className}: @ViewEntity views are not supported and were skipped.`);
|
|
611
|
+
rawClass.isEntity = false;
|
|
612
|
+
break;
|
|
613
|
+
case 'Tree':
|
|
614
|
+
context.warnings.push(`${className}: @Tree (tree entities) is not supported; the closure/materialized-path table was not generated.`);
|
|
615
|
+
break;
|
|
616
|
+
case 'Check':
|
|
617
|
+
case 'Exclusion':
|
|
618
|
+
context.warnings.push(`${className}: @${decorator.name} constraints are not supported and were skipped.`);
|
|
619
|
+
break;
|
|
620
|
+
default:
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Parses `@Index(...)` or `@Unique(...)`. A property decorator passes the property name. */
|
|
626
|
+
function parseIndexDecorator(decorator, propertyName, location, context) {
|
|
627
|
+
const args = decorator.args;
|
|
628
|
+
const options = objectArgument(args);
|
|
629
|
+
let name;
|
|
630
|
+
let fields;
|
|
631
|
+
const first = args[0];
|
|
632
|
+
const second = args[1];
|
|
633
|
+
const listFrom = (value) => {
|
|
634
|
+
if (value === undefined) {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
if (value.kind === 'array') {
|
|
638
|
+
const names = value.items.flatMap((item) => item.kind === 'string' ? [item.value] : []);
|
|
639
|
+
return names.length === value.items.length ? names : undefined;
|
|
640
|
+
}
|
|
641
|
+
if (value.kind === 'arrow') {
|
|
642
|
+
return listFromArrow(value);
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
};
|
|
646
|
+
if (first !== undefined && first.kind === 'string') {
|
|
647
|
+
if (second !== undefined &&
|
|
648
|
+
(second.kind === 'array' || second.kind === 'arrow')) {
|
|
649
|
+
name = first.value;
|
|
650
|
+
fields = listFrom(second);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
name = first.value;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
fields = listFrom(first);
|
|
658
|
+
}
|
|
659
|
+
if (fields === undefined && propertyName !== undefined) {
|
|
660
|
+
fields = [propertyName];
|
|
661
|
+
}
|
|
662
|
+
if (fields === undefined || fields.length === 0) {
|
|
663
|
+
context.warnings.push(`${location}: could not read the column list of @${decorator.name}; the ${decorator.name === 'Unique' ? 'unique constraint' : 'index'} was skipped.`);
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
for (const option of [
|
|
667
|
+
'where',
|
|
668
|
+
'spatial',
|
|
669
|
+
'fulltext',
|
|
670
|
+
'sparse',
|
|
671
|
+
'parser',
|
|
672
|
+
'nullFiltered',
|
|
673
|
+
'expireAfterSeconds',
|
|
674
|
+
]) {
|
|
675
|
+
const value = options?.properties[option];
|
|
676
|
+
if (value !== undefined && !isOff(value)) {
|
|
677
|
+
context.warnings.push(`${location}: the "${option}" option of @${decorator.name} has no equivalent and was ignored.`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (decorator.name === 'Unique' &&
|
|
681
|
+
options?.properties['deferrable'] !== undefined) {
|
|
682
|
+
context.warnings.push(`${location}: the "deferrable" option of @Unique has no equivalent and was ignored.`);
|
|
683
|
+
}
|
|
684
|
+
const optionName = stringOption(options, 'name');
|
|
685
|
+
const resolvedName = name ?? optionName;
|
|
686
|
+
return {
|
|
687
|
+
fields,
|
|
688
|
+
isUnique: decorator.name === 'Unique' || boolOption(options, 'unique') === true,
|
|
689
|
+
...(resolvedName === undefined ? {} : { name: resolvedName }),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/** Reads `(e) => [e.a, e.b]` into ["a", "b"]. */
|
|
693
|
+
function listFromArrow(arrow) {
|
|
694
|
+
if (arrow.body.kind !== 'array') {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
const names = [];
|
|
698
|
+
for (const item of arrow.body.items) {
|
|
699
|
+
const name = inverseSideName({
|
|
700
|
+
kind: 'arrow',
|
|
701
|
+
params: arrow.params,
|
|
702
|
+
body: item,
|
|
703
|
+
});
|
|
704
|
+
if (name === undefined) {
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
names.push(name);
|
|
708
|
+
}
|
|
709
|
+
return names;
|
|
710
|
+
}
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// Property parsing
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
function parseProperty(node, rawClass, aliases, context) {
|
|
715
|
+
const nameNode = node.childForFieldName('name');
|
|
716
|
+
if (nameNode === null) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (node.children.some((child) => child.type === 'static')) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const propertyName = unquoteName(nameNode.text);
|
|
723
|
+
const location = `${rawClass.name}.${propertyName}`;
|
|
724
|
+
const decorators = decoratorsOf(node).map((decorator) => normalizeDecorator(decorator, aliases));
|
|
725
|
+
if (decorators.length === 0) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const typeInfo = describeTypeAnnotation(node);
|
|
729
|
+
const relationDecorator = decorators.find((decorator) => RELATION_DECORATORS.has(decorator.name));
|
|
730
|
+
const columnDecorator = decorators.find((decorator) => COLUMN_DECORATORS.has(decorator.name));
|
|
731
|
+
if (relationDecorator !== undefined) {
|
|
732
|
+
parseRelation(relationDecorator, decorators, propertyName, location, rawClass, context);
|
|
733
|
+
}
|
|
734
|
+
else if (columnDecorator !== undefined) {
|
|
735
|
+
parseColumn(columnDecorator, decorators, propertyName, location, typeInfo, rawClass, context);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
for (const decorator of decorators) {
|
|
739
|
+
const reason = UNSUPPORTED_PROPERTY_DECORATORS[decorator.name];
|
|
740
|
+
if (reason !== undefined) {
|
|
741
|
+
context.warnings.push(`${location}: @${decorator.name} was skipped; ${reason}.`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for (const decorator of decorators) {
|
|
746
|
+
if (decorator.name === 'Index' || decorator.name === 'Unique') {
|
|
747
|
+
const index = parseIndexDecorator(decorator, propertyName, location, context);
|
|
748
|
+
if (index !== undefined) {
|
|
749
|
+
rawClass.indexes.push(index);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function readColumnSpec(decorator) {
|
|
755
|
+
const first = decorator.args[0];
|
|
756
|
+
const options = objectArgument(decorator.args);
|
|
757
|
+
let typeName;
|
|
758
|
+
if (first !== undefined) {
|
|
759
|
+
if (first.kind === 'string') {
|
|
760
|
+
typeName = first.value;
|
|
761
|
+
}
|
|
762
|
+
else if (first.kind === 'name') {
|
|
763
|
+
typeName = first.value;
|
|
764
|
+
}
|
|
765
|
+
else if (first.kind === 'object') {
|
|
766
|
+
const optionType = first.properties['type'];
|
|
767
|
+
if (optionType !== undefined && optionType.kind === 'string') {
|
|
768
|
+
typeName = optionType.value;
|
|
769
|
+
}
|
|
770
|
+
else if (optionType !== undefined && optionType.kind === 'name') {
|
|
771
|
+
typeName = optionType.value;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (typeName === undefined && options !== undefined) {
|
|
776
|
+
const optionType = options.properties['type'];
|
|
777
|
+
if (optionType !== undefined && optionType.kind === 'string') {
|
|
778
|
+
typeName = optionType.value;
|
|
779
|
+
}
|
|
780
|
+
else if (optionType !== undefined && optionType.kind === 'name') {
|
|
781
|
+
typeName = optionType.value;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
...(typeName === undefined ? {} : { typeName }),
|
|
786
|
+
...(options === undefined ? {} : { options }),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function parseColumn(decorator, decorators, propertyName, location, typeInfo, rawClass, context) {
|
|
790
|
+
const first = decorator.args[0];
|
|
791
|
+
if (decorator.name === 'Column' &&
|
|
792
|
+
first !== undefined &&
|
|
793
|
+
first.kind === 'arrow') {
|
|
794
|
+
const typeName = arrowTargetName(first);
|
|
795
|
+
if (typeName === undefined) {
|
|
796
|
+
context.warnings.push(`${location}: the embedded type of @Column(() => ...) could not be resolved, so the column was skipped.`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const options = objectArgument(decorator.args);
|
|
800
|
+
const prefixValue = options?.properties['prefix'];
|
|
801
|
+
let prefix;
|
|
802
|
+
if (prefixValue !== undefined && prefixValue.kind === 'string') {
|
|
803
|
+
prefix = prefixValue.value;
|
|
804
|
+
}
|
|
805
|
+
else if (prefixValue !== undefined && prefixValue.kind === 'bool') {
|
|
806
|
+
prefix = prefixValue.value ? undefined : false;
|
|
807
|
+
}
|
|
808
|
+
if (boolOption(options, 'array') === true) {
|
|
809
|
+
context.warnings.push(`${location}: an array of embedded entities is not supported and was skipped.`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
rawClass.members.push({
|
|
813
|
+
kind: 'embedded',
|
|
814
|
+
property: propertyName,
|
|
815
|
+
typeName,
|
|
816
|
+
prefix,
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const spec = readColumnSpec(decorator);
|
|
821
|
+
const options = spec.options;
|
|
822
|
+
const generatedDecorator = decorators.find((candidate) => candidate.name === 'Generated');
|
|
823
|
+
const generatedStrategy = (() => {
|
|
824
|
+
const strategy = generatedDecorator?.args[0];
|
|
825
|
+
if (generatedDecorator === undefined) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
return strategy !== undefined && strategy.kind === 'string'
|
|
829
|
+
? strategy.value
|
|
830
|
+
: 'increment';
|
|
831
|
+
})();
|
|
832
|
+
const isPrimaryKey = decorator.name === 'PrimaryColumn' ||
|
|
833
|
+
decorator.name === 'PrimaryGeneratedColumn' ||
|
|
834
|
+
boolOption(options, 'primary') === true;
|
|
835
|
+
// Resolve the type -------------------------------------------------------
|
|
836
|
+
let irType;
|
|
837
|
+
let enumName;
|
|
838
|
+
let enumInfo;
|
|
839
|
+
let strategy;
|
|
840
|
+
if (decorator.name === 'PrimaryGeneratedColumn') {
|
|
841
|
+
strategy =
|
|
842
|
+
first !== undefined && first.kind === 'string'
|
|
843
|
+
? first.value
|
|
844
|
+
: 'increment';
|
|
845
|
+
if (strategy === 'uuid') {
|
|
846
|
+
irType = 'uuid';
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const wanted = (spec.typeName ?? '').toLowerCase();
|
|
850
|
+
irType =
|
|
851
|
+
wanted === 'bigint' || wanted === 'int8' || wanted === 'bigserial'
|
|
852
|
+
? 'bigInt'
|
|
853
|
+
: 'int';
|
|
854
|
+
if (strategy === 'rowid') {
|
|
855
|
+
context.warnings.push(`${location}: the "rowid" generation strategy was converted to an auto-increment key.`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
else if (decorator.name === 'CreateDateColumn' ||
|
|
860
|
+
decorator.name === 'UpdateDateColumn' ||
|
|
861
|
+
decorator.name === 'DeleteDateColumn') {
|
|
862
|
+
irType = 'dateTime';
|
|
863
|
+
if (spec.typeName !== undefined) {
|
|
864
|
+
irType = mapColumnType(spec.typeName, location, context) ?? 'dateTime';
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else if (decorator.name === 'VersionColumn') {
|
|
868
|
+
irType = 'int';
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
const resolved = resolveColumnType(spec, typeInfo, location, rawClass, propertyName, context);
|
|
872
|
+
irType = resolved.type;
|
|
873
|
+
enumName = resolved.enumName;
|
|
874
|
+
enumInfo = resolved.enumInfo;
|
|
875
|
+
}
|
|
876
|
+
if (boolOption(options, 'array') === true ||
|
|
877
|
+
(typeInfo.isArray && irType !== 'json' && irType !== 'bytes')) {
|
|
878
|
+
context.warnings.push(`${location}: array columns have no equivalent in the shared model and were converted to json.`);
|
|
879
|
+
irType = 'json';
|
|
880
|
+
enumName = undefined;
|
|
881
|
+
enumInfo = undefined;
|
|
882
|
+
}
|
|
883
|
+
// Build the field --------------------------------------------------------
|
|
884
|
+
const columnName = stringOption(options, 'name') ?? propertyName;
|
|
885
|
+
const isNullable = !isPrimaryKey &&
|
|
886
|
+
(boolOption(options, 'nullable') === true ||
|
|
887
|
+
decorator.name === 'DeleteDateColumn');
|
|
888
|
+
const field = {
|
|
889
|
+
name: propertyName,
|
|
890
|
+
columnName,
|
|
891
|
+
type: irType,
|
|
892
|
+
isPrimaryKey,
|
|
893
|
+
isUnique: boolOption(options, 'unique') === true && !isPrimaryKey,
|
|
894
|
+
isNullable,
|
|
895
|
+
isAutoUpdated: decorator.name === 'UpdateDateColumn',
|
|
896
|
+
...(enumName === undefined ? {} : { enumName }),
|
|
897
|
+
};
|
|
898
|
+
const length = numberOption(options, 'length') ??
|
|
899
|
+
(decorator.args[1]?.kind === 'number'
|
|
900
|
+
? decorator.args[1].value
|
|
901
|
+
: undefined);
|
|
902
|
+
if (length !== undefined && (irType === 'string' || irType === 'text')) {
|
|
903
|
+
field.maxLength = length;
|
|
904
|
+
}
|
|
905
|
+
const precision = numberOption(options, 'precision');
|
|
906
|
+
const scale = numberOption(options, 'scale');
|
|
907
|
+
if (irType === 'decimal') {
|
|
908
|
+
if (precision !== undefined) {
|
|
909
|
+
field.maxDigits = precision;
|
|
910
|
+
}
|
|
911
|
+
if (scale !== undefined) {
|
|
912
|
+
field.decimalPlaces = scale;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Defaults ---------------------------------------------------------------
|
|
916
|
+
let defaultValue;
|
|
917
|
+
if (strategy === 'uuid' || generatedStrategy === 'uuid') {
|
|
918
|
+
defaultValue = { kind: 'uuid' };
|
|
919
|
+
if (irType === 'string') {
|
|
920
|
+
field.type = 'uuid';
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
else if (strategy !== undefined || generatedStrategy !== undefined) {
|
|
924
|
+
defaultValue = { kind: 'autoIncrement' };
|
|
925
|
+
}
|
|
926
|
+
else if (decorator.name === 'CreateDateColumn') {
|
|
927
|
+
defaultValue = { kind: 'now' };
|
|
928
|
+
}
|
|
929
|
+
else if (decorator.name === 'VersionColumn') {
|
|
930
|
+
defaultValue = { kind: 'literal', value: 1 };
|
|
931
|
+
}
|
|
932
|
+
const optionDefault = options?.properties['default'];
|
|
933
|
+
if (optionDefault !== undefined && defaultValue === undefined) {
|
|
934
|
+
defaultValue = convertDefault(optionDefault, field, enumName, enumInfo, location, context);
|
|
935
|
+
}
|
|
936
|
+
if (defaultValue !== undefined) {
|
|
937
|
+
field.default = defaultValue;
|
|
938
|
+
}
|
|
939
|
+
// Warnings for options with no equivalent ---------------------------------
|
|
940
|
+
for (const [option, description] of Object.entries(UNSUPPORTED_COLUMN_OPTIONS)) {
|
|
941
|
+
const optionValue = options?.properties[option];
|
|
942
|
+
if (optionValue !== undefined && !isOff(optionValue)) {
|
|
943
|
+
context.warnings.push(`${location}: ${description} (option "${option}") has no equivalent and was ignored.`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
noteDecoratorSemantics(decorator, location, context);
|
|
947
|
+
rawClass.members.push({ kind: 'field', field });
|
|
948
|
+
}
|
|
949
|
+
function noteDecoratorSemantics(decorator, location, context) {
|
|
950
|
+
if (decorator.name === 'DeleteDateColumn') {
|
|
951
|
+
context.warnings.push(`${location}: @DeleteDateColumn was converted to a nullable timestamp; TypeORM's soft-delete behaviour is not represented.`);
|
|
952
|
+
}
|
|
953
|
+
else if (decorator.name === 'VersionColumn') {
|
|
954
|
+
context.warnings.push(`${location}: @VersionColumn was converted to an integer defaulting to 1; the automatic increment on update is not represented.`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function mapColumnType(typeName, location, context) {
|
|
958
|
+
const constructorType = CONSTRUCTOR_TYPES[typeName];
|
|
959
|
+
if (constructorType !== undefined) {
|
|
960
|
+
return constructorType;
|
|
961
|
+
}
|
|
962
|
+
const key = typeName.toLowerCase().replace(/\s*\(.*\)\s*$/, '');
|
|
963
|
+
const mapped = COLUMN_TYPES[key];
|
|
964
|
+
if (mapped !== undefined) {
|
|
965
|
+
const note = APPROXIMATED_TYPES[key];
|
|
966
|
+
if (note !== undefined) {
|
|
967
|
+
context.warnings.push(`${location}: column type "${typeName}" ${note}.`);
|
|
968
|
+
}
|
|
969
|
+
return mapped;
|
|
970
|
+
}
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
function resolveColumnType(spec, typeInfo, location, rawClass, propertyName, context) {
|
|
974
|
+
const typeName = spec.typeName;
|
|
975
|
+
const options = spec.options;
|
|
976
|
+
const enumOption = options?.properties['enum'];
|
|
977
|
+
const isEnumType = typeName !== undefined &&
|
|
978
|
+
(typeName === 'enum' || typeName === 'simple-enum');
|
|
979
|
+
if (isEnumType || enumOption !== undefined) {
|
|
980
|
+
const resolved = resolveEnumColumn(enumOption, stringOption(options, 'enumName'), location, rawClass, propertyName, context);
|
|
981
|
+
if (resolved !== undefined) {
|
|
982
|
+
return resolved;
|
|
983
|
+
}
|
|
984
|
+
return { type: 'string' };
|
|
985
|
+
}
|
|
986
|
+
if (typeName !== undefined) {
|
|
987
|
+
const mapped = mapColumnType(typeName, location, context);
|
|
988
|
+
if (mapped !== undefined) {
|
|
989
|
+
return { type: mapped };
|
|
990
|
+
}
|
|
991
|
+
context.warnings.push(`${location}: column type "${typeName}" has no equivalent in the shared model and was converted to a string.`);
|
|
992
|
+
return { type: 'string' };
|
|
993
|
+
}
|
|
994
|
+
// No explicit type: infer from the TypeScript annotation.
|
|
995
|
+
const firstName = typeInfo.names[0];
|
|
996
|
+
if (firstName === undefined) {
|
|
997
|
+
if (typeInfo.literals.length > 0) {
|
|
998
|
+
return { type: 'string' };
|
|
999
|
+
}
|
|
1000
|
+
context.warnings.push(`${location}: no column type could be inferred (add a type annotation or an explicit type); it was converted to a string.`);
|
|
1001
|
+
return { type: 'string' };
|
|
1002
|
+
}
|
|
1003
|
+
const fromTs = TS_TYPE_MAP[firstName];
|
|
1004
|
+
if (fromTs !== undefined) {
|
|
1005
|
+
return { type: fromTs };
|
|
1006
|
+
}
|
|
1007
|
+
const enumInfo = context.enums.get(firstName);
|
|
1008
|
+
if (enumInfo !== undefined) {
|
|
1009
|
+
return enumInfo.kind === 'numeric' ? { type: 'int' } : { type: 'string' };
|
|
1010
|
+
}
|
|
1011
|
+
if (JSON_LIKE_TS_TYPES.has(firstName)) {
|
|
1012
|
+
context.warnings.push(`${location}: the TypeScript type "${typeInfo.text}" needs an explicit column type; it was converted to json.`);
|
|
1013
|
+
return { type: 'json' };
|
|
1014
|
+
}
|
|
1015
|
+
context.warnings.push(`${location}: the TypeScript type "${typeInfo.text}" cannot be mapped to a column type; it was converted to a string.`);
|
|
1016
|
+
return { type: 'string' };
|
|
1017
|
+
}
|
|
1018
|
+
function resolveEnumColumn(enumOption, enumNameOption, location, rawClass, propertyName, context) {
|
|
1019
|
+
if (enumOption === undefined) {
|
|
1020
|
+
context.warnings.push(`${location}: an enum column was declared without an "enum" option; it was converted to a string.`);
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
// Reference to a TypeScript enum, or Object.values(Enum).
|
|
1024
|
+
let referenced;
|
|
1025
|
+
if (enumOption.kind === 'name') {
|
|
1026
|
+
referenced = lastSegment(enumOption.value);
|
|
1027
|
+
}
|
|
1028
|
+
else if (enumOption.kind === 'call' &&
|
|
1029
|
+
enumOption.callee === 'Object.values' &&
|
|
1030
|
+
enumOption.args[0]?.kind === 'name') {
|
|
1031
|
+
referenced = lastSegment(enumOption.args[0].value);
|
|
1032
|
+
}
|
|
1033
|
+
if (referenced !== undefined) {
|
|
1034
|
+
const info = context.enums.get(referenced);
|
|
1035
|
+
if (info === undefined) {
|
|
1036
|
+
context.warnings.push(`${location}: the enum "${referenced}" was not found in the input files; the column was converted to a string. ` +
|
|
1037
|
+
`Add the file that defines it to --input.`);
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
if (info.kind === 'numeric') {
|
|
1041
|
+
context.warnings.push(`${location}: the enum "${referenced}" has numeric values, which the shared model cannot represent; the column was converted to an int.`);
|
|
1042
|
+
return { type: 'int' };
|
|
1043
|
+
}
|
|
1044
|
+
return { type: 'string', enumName: referenced, enumInfo: info };
|
|
1045
|
+
}
|
|
1046
|
+
// Inline list of string values (also the shape of a string-union type).
|
|
1047
|
+
if (enumOption.kind === 'array') {
|
|
1048
|
+
const values = [];
|
|
1049
|
+
for (const item of enumOption.items) {
|
|
1050
|
+
const value = enumItemValue(item, context);
|
|
1051
|
+
if (value === undefined) {
|
|
1052
|
+
context.warnings.push(`${location}: the enum values could not be read statically; the column was converted to a string.`);
|
|
1053
|
+
return undefined;
|
|
1054
|
+
}
|
|
1055
|
+
values.push({ name: enumMemberName(value), dbValue: value });
|
|
1056
|
+
}
|
|
1057
|
+
if (values.length === 0) {
|
|
1058
|
+
context.warnings.push(`${location}: the enum has no values; the column was converted to a string.`);
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
const name = enumNameOption === undefined
|
|
1062
|
+
? `${rawClass.name}${toPascalCase(propertyName)}`
|
|
1063
|
+
: toPascalCase(enumNameOption);
|
|
1064
|
+
const info = { kind: 'string', values };
|
|
1065
|
+
context.newEnums.push({ name, values });
|
|
1066
|
+
return { type: 'string', enumName: name, enumInfo: info };
|
|
1067
|
+
}
|
|
1068
|
+
context.warnings.push(`${location}: the enum values (${describeValue(enumOption)}) could not be read statically; the column was converted to a string.`);
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
1071
|
+
function enumItemValue(item, context) {
|
|
1072
|
+
if (item.kind === 'string') {
|
|
1073
|
+
return item.value;
|
|
1074
|
+
}
|
|
1075
|
+
if (item.kind === 'name') {
|
|
1076
|
+
const [enumName, memberName] = item.value.split('.');
|
|
1077
|
+
const info = context.enums.get(enumName ?? '');
|
|
1078
|
+
if (info !== undefined && info.kind === 'string') {
|
|
1079
|
+
return info.values.find((value) => value.name === memberName)
|
|
1080
|
+
?.dbValue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
/** Derives an enum member name from a stored value, for example "in-progress" -> "IN_PROGRESS". */
|
|
1086
|
+
function enumMemberName(value) {
|
|
1087
|
+
const snake = toSnakeCase(value).toUpperCase();
|
|
1088
|
+
if (snake === '') {
|
|
1089
|
+
return 'EMPTY';
|
|
1090
|
+
}
|
|
1091
|
+
return /^[0-9]/.test(snake) ? `V_${snake}` : snake;
|
|
1092
|
+
}
|
|
1093
|
+
function describeValue(value) {
|
|
1094
|
+
return value.kind === 'other' ? value.text : value.kind;
|
|
1095
|
+
}
|
|
1096
|
+
function convertDefault(value, field, enumName, enumInfo, location, context) {
|
|
1097
|
+
const enumValues = enumInfo !== undefined && enumInfo.kind === 'string' ? enumInfo.values : [];
|
|
1098
|
+
const asEnum = (dbValue) => {
|
|
1099
|
+
const member = enumValues.find((candidate) => candidate.dbValue === dbValue);
|
|
1100
|
+
return member === undefined
|
|
1101
|
+
? undefined
|
|
1102
|
+
: { kind: 'enumValue', value: member.name };
|
|
1103
|
+
};
|
|
1104
|
+
switch (value.kind) {
|
|
1105
|
+
case 'null':
|
|
1106
|
+
return undefined;
|
|
1107
|
+
case 'bool':
|
|
1108
|
+
case 'number':
|
|
1109
|
+
return { kind: 'literal', value: value.value };
|
|
1110
|
+
case 'string':
|
|
1111
|
+
if (enumName !== undefined) {
|
|
1112
|
+
const member = asEnum(value.value);
|
|
1113
|
+
if (member !== undefined) {
|
|
1114
|
+
return member;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return { kind: 'literal', value: value.value };
|
|
1118
|
+
case 'name': {
|
|
1119
|
+
const [enumRef, memberName] = value.value.split('.');
|
|
1120
|
+
const referenced = context.enums.get(enumRef ?? '');
|
|
1121
|
+
if (referenced !== undefined &&
|
|
1122
|
+
referenced.kind === 'string' &&
|
|
1123
|
+
memberName !== undefined) {
|
|
1124
|
+
const member = referenced.values.find((candidate) => candidate.name === memberName);
|
|
1125
|
+
if (member !== undefined) {
|
|
1126
|
+
return enumName === undefined
|
|
1127
|
+
? { kind: 'literal', value: member.dbValue }
|
|
1128
|
+
: { kind: 'enumValue', value: member.name };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
case 'arrow': {
|
|
1134
|
+
const body = value.body;
|
|
1135
|
+
if (body.kind === 'string') {
|
|
1136
|
+
return convertSqlDefault(body.value, field, enumName, asEnum, location, context);
|
|
1137
|
+
}
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
default:
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
context.warnings.push(`${location}: the default value (${describeValue(value)}) cannot be evaluated statically and was skipped.`);
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
function convertSqlDefault(sql, field, enumName, asEnum, location, context) {
|
|
1147
|
+
const trimmed = sql.trim();
|
|
1148
|
+
if (NOW_SQL.test(trimmed)) {
|
|
1149
|
+
return { kind: 'now' };
|
|
1150
|
+
}
|
|
1151
|
+
if (UUID_SQL.test(trimmed)) {
|
|
1152
|
+
return { kind: 'uuid' };
|
|
1153
|
+
}
|
|
1154
|
+
if (/^null$/i.test(trimmed)) {
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
1157
|
+
if (/^(true|false)$/i.test(trimmed)) {
|
|
1158
|
+
return { kind: 'literal', value: trimmed.toLowerCase() === 'true' };
|
|
1159
|
+
}
|
|
1160
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
1161
|
+
return field.type === 'string' || field.type === 'text'
|
|
1162
|
+
? { kind: 'literal', value: trimmed }
|
|
1163
|
+
: { kind: 'literal', value: Number(trimmed) };
|
|
1164
|
+
}
|
|
1165
|
+
const quoted = QUOTED_SQL.exec(trimmed);
|
|
1166
|
+
if (quoted !== null) {
|
|
1167
|
+
const text = (quoted[1] ?? '').replace(/''/g, "'");
|
|
1168
|
+
if (enumName !== undefined) {
|
|
1169
|
+
const member = asEnum(text);
|
|
1170
|
+
if (member !== undefined) {
|
|
1171
|
+
return member;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return { kind: 'literal', value: text };
|
|
1175
|
+
}
|
|
1176
|
+
context.warnings.push(`${location}: the SQL default "${sql}" has no equivalent and was skipped.`);
|
|
1177
|
+
return undefined;
|
|
1178
|
+
}
|
|
1179
|
+
// ---------------------------------------------------------------------------
|
|
1180
|
+
// Relations
|
|
1181
|
+
// ---------------------------------------------------------------------------
|
|
1182
|
+
function parseRelation(decorator, decorators, propertyName, location, rawClass, context) {
|
|
1183
|
+
const targetModel = arrowTargetName(decorator.args[0]);
|
|
1184
|
+
if (targetModel === undefined) {
|
|
1185
|
+
context.warnings.push(`${location}: the target of @${decorator.name} could not be resolved (use an arrow function such as () => User); the relation was skipped.`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const inverseArgument = decorator.args.find((value, index) => index > 0 && value.kind !== 'object');
|
|
1189
|
+
const inverseProperty = inverseSideName(inverseArgument);
|
|
1190
|
+
const options = objectArgument(decorator.args);
|
|
1191
|
+
const joinColumn = decorators.find((candidate) => candidate.name === 'JoinColumn');
|
|
1192
|
+
const joinTable = decorators.find((candidate) => candidate.name === 'JoinTable');
|
|
1193
|
+
if (boolOption(options, 'createForeignKeyConstraints') === false) {
|
|
1194
|
+
context.warnings.push(`${location}: createForeignKeyConstraints: false has no equivalent; the foreign key constraint is included in the output.`);
|
|
1195
|
+
}
|
|
1196
|
+
if (options?.properties['onUpdate'] !== undefined) {
|
|
1197
|
+
context.warnings.push(`${location}: the onUpdate option of @${decorator.name} has no equivalent and was ignored.`);
|
|
1198
|
+
}
|
|
1199
|
+
const isOwner = decorator.name === 'ManyToOne' ||
|
|
1200
|
+
(decorator.name === 'OneToOne' && joinColumn !== undefined) ||
|
|
1201
|
+
(decorator.name === 'ManyToMany' && joinTable !== undefined);
|
|
1202
|
+
if (!isOwner) {
|
|
1203
|
+
rawClass.members.push({
|
|
1204
|
+
kind: 'inverse',
|
|
1205
|
+
property: propertyName,
|
|
1206
|
+
targetModel,
|
|
1207
|
+
...(inverseProperty === undefined ? {} : { inverseProperty }),
|
|
1208
|
+
relationKind: decorator.name === 'OneToMany'
|
|
1209
|
+
? 'oneToMany'
|
|
1210
|
+
: decorator.name === 'ManyToMany'
|
|
1211
|
+
? 'manyToMany'
|
|
1212
|
+
: 'oneToOne',
|
|
1213
|
+
});
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const kind = decorator.name === 'ManyToMany'
|
|
1217
|
+
? 'manyToMany'
|
|
1218
|
+
: decorator.name === 'OneToOne'
|
|
1219
|
+
? 'oneToOne'
|
|
1220
|
+
: 'foreignKey';
|
|
1221
|
+
let explicitColumn;
|
|
1222
|
+
let referencedColumn;
|
|
1223
|
+
if (kind !== 'manyToMany' && joinColumn !== undefined) {
|
|
1224
|
+
const joinArgument = joinColumn.args[0];
|
|
1225
|
+
if (joinArgument !== undefined && joinArgument.kind === 'array') {
|
|
1226
|
+
if (joinArgument.items.length > 1) {
|
|
1227
|
+
context.warnings.push(`${location}: composite foreign keys (several @JoinColumn entries) are not supported; the relation was skipped.`);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const single = joinArgument.items[0];
|
|
1231
|
+
if (single !== undefined && single.kind === 'object') {
|
|
1232
|
+
explicitColumn = stringOption(single, 'name');
|
|
1233
|
+
referencedColumn = stringOption(single, 'referencedColumnName');
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
else if (joinArgument !== undefined && joinArgument.kind === 'object') {
|
|
1237
|
+
explicitColumn = stringOption(joinArgument, 'name');
|
|
1238
|
+
referencedColumn = stringOption(joinArgument, 'referencedColumnName');
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (kind === 'manyToMany' && joinTable !== undefined) {
|
|
1242
|
+
const joinOptions = objectArgument(joinTable.args);
|
|
1243
|
+
if (joinOptions !== undefined &&
|
|
1244
|
+
Object.keys(joinOptions.properties).length > 0) {
|
|
1245
|
+
context.warnings.push(`${location}: custom @JoinTable settings (${Object.keys(joinOptions.properties).join(', ')}) are not preserved; ` +
|
|
1246
|
+
`the join table name and columns are derived from the models.`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const onDeleteValue = stringOption(options, 'onDelete');
|
|
1250
|
+
let onDelete = 'noAction';
|
|
1251
|
+
if (onDeleteValue !== undefined) {
|
|
1252
|
+
const mapped = ON_DELETE_MAP[onDeleteValue.toUpperCase()];
|
|
1253
|
+
if (mapped === undefined) {
|
|
1254
|
+
context.warnings.push(`${location}: onDelete "${onDeleteValue}" has no equivalent and was converted to NoAction.`);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
onDelete = mapped;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (kind === 'manyToMany') {
|
|
1261
|
+
onDelete = 'cascade';
|
|
1262
|
+
}
|
|
1263
|
+
const nullableOption = boolOption(options, 'nullable');
|
|
1264
|
+
const isPrimaryKey = boolOption(options, 'primary') === true;
|
|
1265
|
+
const relation = {
|
|
1266
|
+
name: propertyName,
|
|
1267
|
+
kind,
|
|
1268
|
+
targetModel,
|
|
1269
|
+
columnName: explicitColumn ?? defaultJoinColumn(propertyName, referencedColumn),
|
|
1270
|
+
// TypeORM relations are nullable unless nullable: false is given.
|
|
1271
|
+
isNullable: !isPrimaryKey && nullableOption !== false,
|
|
1272
|
+
onDelete,
|
|
1273
|
+
...(inverseProperty === undefined ? {} : { relatedName: inverseProperty }),
|
|
1274
|
+
...(referencedColumn === undefined ? {} : { toField: referencedColumn }),
|
|
1275
|
+
...(isPrimaryKey ? { isPrimaryKey: true } : {}),
|
|
1276
|
+
};
|
|
1277
|
+
rawClass.members.push({
|
|
1278
|
+
kind: 'relation',
|
|
1279
|
+
relation,
|
|
1280
|
+
hasExplicitColumn: explicitColumn !== undefined,
|
|
1281
|
+
hasExplicitNullable: nullableOption !== undefined,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
/** TypeORM's default join column name: camelCase(propertyName + "_" + referencedColumn). */
|
|
1285
|
+
function defaultJoinColumn(propertyName, referencedColumn) {
|
|
1286
|
+
return toCamelCase(`${propertyName}_${referencedColumn ?? 'id'}`);
|
|
1287
|
+
}
|
|
1288
|
+
function buildSchema(classes, enums, warnings, options) {
|
|
1289
|
+
const classByName = new Map();
|
|
1290
|
+
for (const rawClass of classes) {
|
|
1291
|
+
if (classByName.has(rawClass.name)) {
|
|
1292
|
+
warnings.push(`Duplicate class name "${rawClass.name}" (${rawClass.filePath}); only the first definition was converted.`);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
classByName.set(rawClass.name, rawClass);
|
|
1296
|
+
}
|
|
1297
|
+
const models = [];
|
|
1298
|
+
const inverseSides = [];
|
|
1299
|
+
const relationMembers = new Map();
|
|
1300
|
+
for (const rawClass of classByName.values()) {
|
|
1301
|
+
if (!rawClass.isEntity || rawClass.isAbstract || rawClass.isChildEntity) {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
const collected = collectMembers(rawClass, classByName, warnings, new Set());
|
|
1305
|
+
const model = finalizeModel(rawClass, collected, warnings, options);
|
|
1306
|
+
models.push(model);
|
|
1307
|
+
relationMembers.set(model.name, collected.relations);
|
|
1308
|
+
for (const inverse of collected.inverses) {
|
|
1309
|
+
inverseSides.push({ model: model.name, inverse });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
resolveRelations(models, inverseSides, warnings);
|
|
1313
|
+
addStubModels(models, warnings, options);
|
|
1314
|
+
resolveDefaultColumns(models);
|
|
1315
|
+
return { models, enums: pruneEnums(enums, models), warnings };
|
|
1316
|
+
}
|
|
1317
|
+
function collectMembers(rawClass, classByName, warnings, visiting) {
|
|
1318
|
+
const inherited = {
|
|
1319
|
+
fields: [],
|
|
1320
|
+
relations: [],
|
|
1321
|
+
inverses: [],
|
|
1322
|
+
indexes: [],
|
|
1323
|
+
};
|
|
1324
|
+
visiting.add(rawClass.name);
|
|
1325
|
+
if (rawClass.baseName !== undefined) {
|
|
1326
|
+
const parent = classByName.get(rawClass.baseName);
|
|
1327
|
+
if (parent === undefined) {
|
|
1328
|
+
if (rawClass.baseName !== 'BaseEntity') {
|
|
1329
|
+
warnings.push(`${rawClass.name}: base class "${rawClass.baseName}" was not found in the input files, so any columns it defines ` +
|
|
1330
|
+
`are missing. Add the file that defines it to --input.`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
else if (visiting.has(parent.name)) {
|
|
1334
|
+
warnings.push(`${rawClass.name}: circular inheritance through "${parent.name}" was ignored.`);
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
const parentMembers = collectMembers(parent, classByName, warnings, new Set(visiting));
|
|
1338
|
+
inherited.fields.push(...parentMembers.fields);
|
|
1339
|
+
inherited.relations.push(...parentMembers.relations);
|
|
1340
|
+
inherited.inverses.push(...parentMembers.inverses);
|
|
1341
|
+
inherited.indexes.push(...parentMembers.indexes);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const own = {
|
|
1345
|
+
fields: [],
|
|
1346
|
+
relations: [],
|
|
1347
|
+
inverses: [],
|
|
1348
|
+
indexes: [],
|
|
1349
|
+
};
|
|
1350
|
+
for (const member of rawClass.members) {
|
|
1351
|
+
switch (member.kind) {
|
|
1352
|
+
case 'field':
|
|
1353
|
+
own.fields.push(member.field);
|
|
1354
|
+
break;
|
|
1355
|
+
case 'relation':
|
|
1356
|
+
own.relations.push(member);
|
|
1357
|
+
break;
|
|
1358
|
+
case 'inverse':
|
|
1359
|
+
own.inverses.push(member);
|
|
1360
|
+
break;
|
|
1361
|
+
case 'embedded':
|
|
1362
|
+
own.fields.push(...expandEmbedded(member, [], `${rawClass.name}.${member.property}`, classByName, warnings, new Set([rawClass.name])));
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
own.indexes.push(...rawClass.indexes);
|
|
1367
|
+
const ownFieldNames = new Set(own.fields.map((field) => field.name));
|
|
1368
|
+
const ownRelationNames = new Set(own.relations.map((member) => member.relation.name));
|
|
1369
|
+
const ownInverseNames = new Set(own.inverses.map((member) => member.property));
|
|
1370
|
+
return {
|
|
1371
|
+
fields: [
|
|
1372
|
+
...inherited.fields.filter((field) => !ownFieldNames.has(field.name)),
|
|
1373
|
+
...own.fields,
|
|
1374
|
+
],
|
|
1375
|
+
relations: [
|
|
1376
|
+
...inherited.relations.filter((member) => !ownRelationNames.has(member.relation.name)),
|
|
1377
|
+
...own.relations,
|
|
1378
|
+
],
|
|
1379
|
+
inverses: [
|
|
1380
|
+
...inherited.inverses.filter((member) => !ownInverseNames.has(member.property)),
|
|
1381
|
+
...own.inverses,
|
|
1382
|
+
],
|
|
1383
|
+
indexes: [...inherited.indexes, ...own.indexes],
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
/** TypeORM's titleCase: first letter upper case, remaining letters lower case. */
|
|
1387
|
+
function titleCase(text) {
|
|
1388
|
+
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
|
1389
|
+
}
|
|
1390
|
+
function embeddedName(prefixes, name) {
|
|
1391
|
+
return prefixes.length === 0
|
|
1392
|
+
? name
|
|
1393
|
+
: toCamelCase(prefixes.join('_')) + titleCase(name);
|
|
1394
|
+
}
|
|
1395
|
+
/** Flattens an embedded entity into prefixed columns, the way TypeORM names them. */
|
|
1396
|
+
function expandEmbedded(embedded, outerPrefixes, location, classByName, warnings, visiting) {
|
|
1397
|
+
const embeddedClass = classByName.get(embedded.typeName);
|
|
1398
|
+
if (embeddedClass === undefined) {
|
|
1399
|
+
warnings.push(`${location}: the embedded type "${embedded.typeName}" was not found in the input files, so its columns are missing. ` +
|
|
1400
|
+
`Add the file that defines it to --input.`);
|
|
1401
|
+
return [];
|
|
1402
|
+
}
|
|
1403
|
+
if (visiting.has(embeddedClass.name)) {
|
|
1404
|
+
warnings.push(`${location}: a recursive embedded type was ignored.`);
|
|
1405
|
+
return [];
|
|
1406
|
+
}
|
|
1407
|
+
const prefixes = embedded.prefix === false
|
|
1408
|
+
? outerPrefixes
|
|
1409
|
+
: [...outerPrefixes, embedded.prefix ?? embedded.property];
|
|
1410
|
+
const collected = collectMembers(embeddedClass, classByName, warnings, new Set());
|
|
1411
|
+
const nestedVisiting = new Set([
|
|
1412
|
+
...visiting,
|
|
1413
|
+
embeddedClass.name,
|
|
1414
|
+
]);
|
|
1415
|
+
const fields = collected.fields.map((field) => {
|
|
1416
|
+
return {
|
|
1417
|
+
...field,
|
|
1418
|
+
name: embeddedName(prefixes, field.name),
|
|
1419
|
+
columnName: embeddedName(prefixes, field.columnName),
|
|
1420
|
+
};
|
|
1421
|
+
});
|
|
1422
|
+
// Nested embedded entities appear as members of the embedded class itself.
|
|
1423
|
+
for (const member of embeddedClass.members) {
|
|
1424
|
+
if (member.kind === 'embedded') {
|
|
1425
|
+
fields.push(...expandEmbedded(member, prefixes, `${location}.${member.property}`, classByName, warnings, nestedVisiting));
|
|
1426
|
+
}
|
|
1427
|
+
else if (member.kind === 'relation' || member.kind === 'inverse') {
|
|
1428
|
+
const name = member.kind === 'relation' ? member.relation.name : member.property;
|
|
1429
|
+
warnings.push(`${location}.${name}: relations inside embedded entities are not supported and were skipped.`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (collected.indexes.length > 0) {
|
|
1433
|
+
warnings.push(`${location}: indexes declared inside the embedded type "${embedded.typeName}" were skipped.`);
|
|
1434
|
+
}
|
|
1435
|
+
return fields;
|
|
1436
|
+
}
|
|
1437
|
+
function finalizeModel(rawClass, collected, warnings, options) {
|
|
1438
|
+
const fields = [...collected.fields];
|
|
1439
|
+
const relations = [];
|
|
1440
|
+
/** Scalar foreign-key properties merged into relations: property name -> relation name. */
|
|
1441
|
+
const renamed = new Map();
|
|
1442
|
+
for (const member of collected.relations) {
|
|
1443
|
+
const relation = { ...member.relation };
|
|
1444
|
+
if (relation.kind !== 'manyToMany') {
|
|
1445
|
+
const backing = fields.findIndex((field) => field.columnName === relation.columnName ||
|
|
1446
|
+
(!member.hasExplicitColumn && field.name === relation.columnName));
|
|
1447
|
+
const backingField = fields[backing];
|
|
1448
|
+
if (backingField !== undefined) {
|
|
1449
|
+
// The entity also declares the foreign key column as a plain property;
|
|
1450
|
+
// the relation already represents it.
|
|
1451
|
+
fields.splice(backing, 1);
|
|
1452
|
+
renamed.set(backingField.name, relation.name);
|
|
1453
|
+
relation.columnName = backingField.columnName;
|
|
1454
|
+
if (!member.hasExplicitNullable) {
|
|
1455
|
+
relation.isNullable = backingField.isNullable;
|
|
1456
|
+
}
|
|
1457
|
+
if (backingField.isPrimaryKey) {
|
|
1458
|
+
relation.isPrimaryKey = true;
|
|
1459
|
+
relation.isNullable = false;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
relations.push(relation);
|
|
1464
|
+
}
|
|
1465
|
+
const indexes = [];
|
|
1466
|
+
for (const raw of collected.indexes) {
|
|
1467
|
+
const names = raw.fields.map((name) => renamed.get(name) ?? name);
|
|
1468
|
+
const known = new Set([
|
|
1469
|
+
...fields.map((field) => field.name),
|
|
1470
|
+
...relations.map((relation) => relation.name),
|
|
1471
|
+
]);
|
|
1472
|
+
const missing = names.filter((name) => !known.has(name));
|
|
1473
|
+
if (missing.length > 0) {
|
|
1474
|
+
warnings.push(`${rawClass.name}: the ${raw.isUnique ? 'unique constraint' : 'index'} on (${raw.fields.join(', ')}) refers to ` +
|
|
1475
|
+
`"${missing.join('", "')}", which is not a column or relation of the entity; it was skipped.`);
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
const singleField = names.length === 1
|
|
1479
|
+
? fields.find((field) => field.name === names[0])
|
|
1480
|
+
: undefined;
|
|
1481
|
+
if (raw.isUnique &&
|
|
1482
|
+
raw.name === undefined &&
|
|
1483
|
+
singleField !== undefined &&
|
|
1484
|
+
!singleField.isPrimaryKey) {
|
|
1485
|
+
singleField.isUnique = true;
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
indexes.push({
|
|
1489
|
+
fields: names,
|
|
1490
|
+
isUnique: raw.isUnique,
|
|
1491
|
+
...(raw.name === undefined ? {} : { name: raw.name }),
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
const primaryFields = fields.filter((field) => field.isPrimaryKey);
|
|
1495
|
+
const primaryRelations = relations.filter((relation) => relation.isPrimaryKey === true);
|
|
1496
|
+
const keyCount = primaryFields.length + primaryRelations.length;
|
|
1497
|
+
let compositePrimaryKey;
|
|
1498
|
+
if (keyCount === 0) {
|
|
1499
|
+
warnings.push(`${rawClass.name}: the entity has no primary key column; TypeORM requires one, so add @PrimaryGeneratedColumn() or @PrimaryColumn().`);
|
|
1500
|
+
}
|
|
1501
|
+
else if (keyCount > 1) {
|
|
1502
|
+
compositePrimaryKey = [
|
|
1503
|
+
...primaryFields.map((field) => field.name),
|
|
1504
|
+
...primaryRelations.map((relation) => relation.name),
|
|
1505
|
+
];
|
|
1506
|
+
for (const field of primaryFields) {
|
|
1507
|
+
field.isPrimaryKey = false;
|
|
1508
|
+
field.isUnique = false;
|
|
1509
|
+
}
|
|
1510
|
+
for (const relation of primaryRelations) {
|
|
1511
|
+
delete relation.isPrimaryKey;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
name: rawClass.name,
|
|
1516
|
+
tableName: rawClass.tableName ?? toSnakeCase(rawClass.name),
|
|
1517
|
+
appLabel: options.appLabel,
|
|
1518
|
+
fields,
|
|
1519
|
+
relations,
|
|
1520
|
+
indexes,
|
|
1521
|
+
...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
/** Links inverse-side decorators (@OneToMany and friends) to the owning relation. */
|
|
1525
|
+
function resolveRelations(models, inverseSides, warnings) {
|
|
1526
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
1527
|
+
for (const { model, inverse } of inverseSides) {
|
|
1528
|
+
const location = `${model}.${inverse.property}`;
|
|
1529
|
+
const target = byName.get(inverse.targetModel);
|
|
1530
|
+
if (target === undefined) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (inverse.inverseProperty === undefined) {
|
|
1534
|
+
warnings.push(`${location}: the inverse side of this @${inverse.relationKind === 'oneToMany' ? 'OneToMany' : inverse.relationKind === 'manyToMany' ? 'ManyToMany' : 'OneToOne'} ` +
|
|
1535
|
+
`has no inverse-side function, so it could not be linked to "${inverse.targetModel}".`);
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const owner = target.relations.find((relation) => relation.name === inverse.inverseProperty);
|
|
1539
|
+
if (owner === undefined) {
|
|
1540
|
+
warnings.push(`${location}: the owning side "${inverse.targetModel}.${inverse.inverseProperty}" was not found ` +
|
|
1541
|
+
`(for a one-to-one or many-to-many relation, the owning side needs @JoinColumn or @JoinTable); the reverse accessor is not included.`);
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (owner.relatedName === undefined) {
|
|
1545
|
+
owner.relatedName = inverse.property;
|
|
1546
|
+
}
|
|
1547
|
+
else if (owner.relatedName !== inverse.property) {
|
|
1548
|
+
warnings.push(`${location}: the owning side "${inverse.targetModel}.${inverse.inverseProperty}" names its reverse accessor ` +
|
|
1549
|
+
`"${owner.relatedName}"; "${inverse.property}" was ignored.`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function addStubModels(models, warnings, options) {
|
|
1554
|
+
const known = new Set(models.map((model) => model.name));
|
|
1555
|
+
const stubs = [];
|
|
1556
|
+
for (const model of models) {
|
|
1557
|
+
for (const relation of model.relations) {
|
|
1558
|
+
const target = relation.targetModel;
|
|
1559
|
+
if (known.has(target)) {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
known.add(target);
|
|
1563
|
+
stubs.push({
|
|
1564
|
+
name: target,
|
|
1565
|
+
tableName: toSnakeCase(target),
|
|
1566
|
+
appLabel: options.appLabel,
|
|
1567
|
+
fields: [
|
|
1568
|
+
{
|
|
1569
|
+
name: 'id',
|
|
1570
|
+
columnName: 'id',
|
|
1571
|
+
type: 'int',
|
|
1572
|
+
isPrimaryKey: true,
|
|
1573
|
+
isUnique: false,
|
|
1574
|
+
isNullable: false,
|
|
1575
|
+
isAutoUpdated: false,
|
|
1576
|
+
default: { kind: 'autoIncrement' },
|
|
1577
|
+
},
|
|
1578
|
+
],
|
|
1579
|
+
relations: [],
|
|
1580
|
+
indexes: [],
|
|
1581
|
+
});
|
|
1582
|
+
warnings.push(`${model.name}.${relation.name} references "${target}", which is not an entity in the input. ` +
|
|
1583
|
+
`A stub model with an auto-increment id was generated; replace it with the real definition.`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
models.push(...stubs);
|
|
1587
|
+
}
|
|
1588
|
+
/** Foreign keys without an explicit column take the referenced key's name, as TypeORM does. */
|
|
1589
|
+
function resolveDefaultColumns(models) {
|
|
1590
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
1591
|
+
for (const model of models) {
|
|
1592
|
+
for (const relation of model.relations) {
|
|
1593
|
+
if (relation.kind === 'manyToMany') {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const defaultName = defaultJoinColumn(relation.name, relation.toField);
|
|
1597
|
+
if (relation.columnName !== defaultName ||
|
|
1598
|
+
relation.toField !== undefined) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
const target = byName.get(relation.targetModel);
|
|
1602
|
+
const key = target?.fields.find((field) => field.isPrimaryKey);
|
|
1603
|
+
if (key !== undefined && key.columnName !== 'id') {
|
|
1604
|
+
relation.columnName = defaultJoinColumn(relation.name, key.columnName);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/** Keeps only enums that a converted field refers to. */
|
|
1610
|
+
function pruneEnums(enums, models) {
|
|
1611
|
+
const used = new Set(models.flatMap((model) => model.fields.flatMap((field) => field.enumName === undefined ? [] : [field.enumName])));
|
|
1612
|
+
return enums.filter((enumeration) => used.has(enumeration.name));
|
|
1613
|
+
}
|
|
1614
|
+
//# sourceMappingURL=typeorm.js.map
|