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,833 @@
|
|
|
1
|
+
import { prismaOnlyWarnings } from '../prismaOnlyConstructs.js';
|
|
2
|
+
import { findModel } from '../ir.js';
|
|
3
|
+
import { toCamelCase, toSnakeCase } from '../naming.js';
|
|
4
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
const CREATED_FIELD_PATTERN = /^(created|created_?at|date_?created|createdon)$/i;
|
|
6
|
+
const MAX_LINE_WIDTH = 80;
|
|
7
|
+
const ON_DELETE_NAMES = {
|
|
8
|
+
cascade: 'CASCADE',
|
|
9
|
+
setNull: 'SET NULL',
|
|
10
|
+
restrict: 'RESTRICT',
|
|
11
|
+
noAction: 'NO ACTION',
|
|
12
|
+
setDefault: 'SET DEFAULT',
|
|
13
|
+
};
|
|
14
|
+
export function emitTypeorm(schema, options) {
|
|
15
|
+
const context = {
|
|
16
|
+
schema,
|
|
17
|
+
options,
|
|
18
|
+
dialect: dialectOf(options.provider),
|
|
19
|
+
warnings: prismaOnlyWarnings(schema),
|
|
20
|
+
imports: new Set(),
|
|
21
|
+
names: new Map(),
|
|
22
|
+
inverseNames: new Map(),
|
|
23
|
+
};
|
|
24
|
+
if (options.provider === 'mongodb') {
|
|
25
|
+
context.warnings.push('Provider "mongodb": TypeORM entities for MongoDB need different decorators; SQL column types (PostgreSQL style) were used.');
|
|
26
|
+
}
|
|
27
|
+
allocateNames(context);
|
|
28
|
+
const blocks = [];
|
|
29
|
+
for (const enumDefinition of schema.enums) {
|
|
30
|
+
blocks.push(emitEnum(context, enumDefinition));
|
|
31
|
+
}
|
|
32
|
+
const inverseMembers = new Map();
|
|
33
|
+
const classMembers = new Map();
|
|
34
|
+
for (const model of schema.models) {
|
|
35
|
+
classMembers.set(model.name, buildForwardMembers(context, model, inverseMembers));
|
|
36
|
+
}
|
|
37
|
+
for (const model of schema.models) {
|
|
38
|
+
const members = [
|
|
39
|
+
...(classMembers.get(model.name) ?? []),
|
|
40
|
+
...(inverseMembers.get(model.name) ?? []),
|
|
41
|
+
];
|
|
42
|
+
blocks.push(emitClass(context, model, members));
|
|
43
|
+
}
|
|
44
|
+
if (blocks.length === 0) {
|
|
45
|
+
return { text: '', warnings: context.warnings };
|
|
46
|
+
}
|
|
47
|
+
const importLine = buildImportLine(context.imports);
|
|
48
|
+
return {
|
|
49
|
+
text: `${[importLine, ...blocks].join('\n\n')}\n`,
|
|
50
|
+
warnings: context.warnings,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Imports and small formatting helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function buildImportLine(imports) {
|
|
57
|
+
const names = [...imports].sort((first, second) => first.localeCompare(second));
|
|
58
|
+
const specifiers = names.map((name) => name === 'Relation' ? 'type Relation' : name);
|
|
59
|
+
return `import { ${specifiers.join(', ')} } from 'typeorm';`;
|
|
60
|
+
}
|
|
61
|
+
function use(context, name) {
|
|
62
|
+
context.imports.add(name);
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
function quote(value) {
|
|
66
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
|
|
67
|
+
if (value.includes("'") && !value.includes('"')) {
|
|
68
|
+
return `"${escaped}"`;
|
|
69
|
+
}
|
|
70
|
+
return `'${escaped.replace(/'/g, "\\'")}'`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Formats a decorator. Leading arguments are written as-is; options become one
|
|
74
|
+
* object literal that wraps onto several lines when the decorator is too wide.
|
|
75
|
+
*/
|
|
76
|
+
function decorator(name, args, entries, indent = ' ') {
|
|
77
|
+
const parts = [...args];
|
|
78
|
+
if (entries.length > 0) {
|
|
79
|
+
parts.push(`{ ${entries.join(', ')} }`);
|
|
80
|
+
}
|
|
81
|
+
const inline = `${indent}@${name}(${parts.join(', ')})`;
|
|
82
|
+
if (entries.length === 0 || inline.length <= MAX_LINE_WIDTH) {
|
|
83
|
+
return [inline];
|
|
84
|
+
}
|
|
85
|
+
const lead = args.length > 0 ? `${args.join(', ')}, ` : '';
|
|
86
|
+
return [
|
|
87
|
+
`${indent}@${name}(${lead}{`,
|
|
88
|
+
...entries.map((entry) => `${indent} ${entry},`),
|
|
89
|
+
`${indent}})`,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
function identifier(context, owner, raw) {
|
|
93
|
+
if (IDENTIFIER_PATTERN.test(raw)) {
|
|
94
|
+
return raw;
|
|
95
|
+
}
|
|
96
|
+
const replaced = raw.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
97
|
+
const safe = /^[0-9]/.test(replaced) ? `_${replaced}` : replaced;
|
|
98
|
+
const result = safe === '' ? '_' : safe;
|
|
99
|
+
context.warnings.push(`${owner}: "${raw}" is not a valid TypeScript identifier; it was written as "${result}".`);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
function uniqueName(baseName, used) {
|
|
103
|
+
let candidate = baseName;
|
|
104
|
+
let suffix = 2;
|
|
105
|
+
while (used.has(candidate)) {
|
|
106
|
+
candidate = `${baseName}${suffix}`;
|
|
107
|
+
suffix += 1;
|
|
108
|
+
}
|
|
109
|
+
used.add(candidate);
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
function dialectOf(provider) {
|
|
113
|
+
switch (provider) {
|
|
114
|
+
case 'mysql':
|
|
115
|
+
return 'mysql';
|
|
116
|
+
case 'sqlite':
|
|
117
|
+
return 'sqlite';
|
|
118
|
+
case 'sqlserver':
|
|
119
|
+
return 'sqlserver';
|
|
120
|
+
default:
|
|
121
|
+
return 'postgres';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Names
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
function propertyName(context, rawName) {
|
|
128
|
+
return context.options.camelFields ? toCamelCase(rawName) : rawName;
|
|
129
|
+
}
|
|
130
|
+
function isKeyedRelation(model, relation) {
|
|
131
|
+
return (relation.isPrimaryKey === true ||
|
|
132
|
+
(model.compositePrimaryKey?.includes(relation.name) ?? false));
|
|
133
|
+
}
|
|
134
|
+
function allocateNames(context) {
|
|
135
|
+
for (const model of context.schema.models) {
|
|
136
|
+
const names = {
|
|
137
|
+
fields: new Map(),
|
|
138
|
+
relations: new Map(),
|
|
139
|
+
scalars: new Map(),
|
|
140
|
+
used: new Set(),
|
|
141
|
+
};
|
|
142
|
+
for (const field of model.fields) {
|
|
143
|
+
const safe = identifier(context, `${model.name}.${field.name}`, propertyName(context, field.name));
|
|
144
|
+
names.fields.set(field.name, uniqueName(safe, names.used));
|
|
145
|
+
}
|
|
146
|
+
for (const relation of model.relations) {
|
|
147
|
+
const safe = identifier(context, `${model.name}.${relation.name}`, propertyName(context, relation.name));
|
|
148
|
+
const relationProp = uniqueName(safe, names.used);
|
|
149
|
+
names.relations.set(relation.name, relationProp);
|
|
150
|
+
if (relation.kind !== 'manyToMany' && isKeyedRelation(model, relation)) {
|
|
151
|
+
const columnProp = propertyName(context, relation.columnName);
|
|
152
|
+
const base = columnProp !== relationProp ? columnProp : `${relationProp}Id`;
|
|
153
|
+
names.scalars.set(relation.name, uniqueName(identifier(context, `${model.name}.${relation.name}`, base), names.used));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
context.names.set(model.name, names);
|
|
157
|
+
}
|
|
158
|
+
for (const model of context.schema.models) {
|
|
159
|
+
for (const relation of model.relations) {
|
|
160
|
+
const target = findModel(context.schema, relation.targetModel);
|
|
161
|
+
const targetNames = context.names.get(relation.targetModel);
|
|
162
|
+
if (target === undefined || targetNames === undefined) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const defaultName = relation.kind === 'oneToOne'
|
|
166
|
+
? toSnakeCase(model.name)
|
|
167
|
+
: `${toSnakeCase(model.name)}_set`;
|
|
168
|
+
const safe = identifier(context, `${model.name}.${relation.name}`, propertyName(context, relation.relatedName ?? defaultName));
|
|
169
|
+
context.inverseNames.set(`${model.name}.${relation.name}`, uniqueName(safe, targetNames.used));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function namesOf(context, model) {
|
|
174
|
+
return (context.names.get(model.name) ?? {
|
|
175
|
+
fields: new Map(),
|
|
176
|
+
relations: new Map(),
|
|
177
|
+
scalars: new Map(),
|
|
178
|
+
used: new Set(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Enums
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
function emitEnum(context, enumDefinition) {
|
|
185
|
+
const lines = [`export enum ${enumDefinition.name} {`];
|
|
186
|
+
for (const value of enumDefinition.values) {
|
|
187
|
+
lines.push(` ${enumMemberName(context, enumDefinition, value)} = ${quote(value.dbValue)},`);
|
|
188
|
+
}
|
|
189
|
+
lines.push('}');
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
function enumMemberName(context, enumDefinition, value) {
|
|
193
|
+
return identifier(context, `enum ${enumDefinition.name}`, value.name);
|
|
194
|
+
}
|
|
195
|
+
/** Finds the enum member a default refers to, by member name or stored value. */
|
|
196
|
+
function enumReference(context, enumDefinition, raw) {
|
|
197
|
+
const value = enumDefinition.values.find((candidate) => candidate.name === raw) ??
|
|
198
|
+
enumDefinition.values.find((candidate) => candidate.dbValue === raw);
|
|
199
|
+
if (value === undefined) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const memberName = identifier({ ...context, warnings: [] }, '', value.name);
|
|
203
|
+
return `${enumDefinition.name}.${memberName}`;
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Column types
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
const POSTGRES_RANGE_TYPES = {
|
|
209
|
+
int: 'int4range',
|
|
210
|
+
bigInt: 'int8range',
|
|
211
|
+
decimal: 'numrange',
|
|
212
|
+
date: 'daterange',
|
|
213
|
+
dateTime: 'tstzrange',
|
|
214
|
+
};
|
|
215
|
+
/** Column type of a field, including array columns built on top of the element type. */
|
|
216
|
+
function columnSpecOf(context, label, field) {
|
|
217
|
+
const depth = field.arrayDepth ?? 0;
|
|
218
|
+
if (depth === 0) {
|
|
219
|
+
return scalarColumnSpec(context, label, field);
|
|
220
|
+
}
|
|
221
|
+
const element = scalarColumnSpec(context, label, {
|
|
222
|
+
...field,
|
|
223
|
+
arrayDepth: undefined,
|
|
224
|
+
});
|
|
225
|
+
const tsType = `${element.tsType.includes(' ') ? `(${element.tsType})` : element.tsType}${'[]'.repeat(depth)}`;
|
|
226
|
+
if (context.dialect === 'postgres') {
|
|
227
|
+
return {
|
|
228
|
+
typeEntries: [...element.typeEntries, 'array: true'],
|
|
229
|
+
tsType,
|
|
230
|
+
dbType: element.dbType,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const jsonType = context.dialect === 'mysql' ? 'json' : 'simple-json';
|
|
234
|
+
context.warnings.push(`${label}: ${context.dialect} has no array column type; the array was written as ${jsonType}.`);
|
|
235
|
+
return {
|
|
236
|
+
typeEntries: [`type: '${jsonType}'`],
|
|
237
|
+
tsType,
|
|
238
|
+
dbType: jsonType,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function scalarColumnSpec(context, label, field) {
|
|
242
|
+
const dialect = context.dialect;
|
|
243
|
+
if (field.enumName !== undefined) {
|
|
244
|
+
const enumDefinition = context.schema.enums.find((candidate) => candidate.name === field.enumName);
|
|
245
|
+
if (enumDefinition === undefined) {
|
|
246
|
+
context.warnings.push(`${label}: enum "${field.enumName}" does not exist in the schema; the column is written as a plain string.`);
|
|
247
|
+
}
|
|
248
|
+
else if (dialect === 'sqlserver') {
|
|
249
|
+
context.warnings.push(`${label}: SQL Server has no enum column type; it was written as varchar and the TypeScript enum "${enumDefinition.name}" is only used for typing.`);
|
|
250
|
+
return {
|
|
251
|
+
typeEntries: [`type: 'varchar'`],
|
|
252
|
+
tsType: enumDefinition.name,
|
|
253
|
+
dbType: 'varchar',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const enumType = dialect === 'sqlite' ? 'simple-enum' : 'enum';
|
|
258
|
+
return {
|
|
259
|
+
typeEntries: [
|
|
260
|
+
`type: ${quote(enumType)}`,
|
|
261
|
+
`enum: ${enumDefinition.name}`,
|
|
262
|
+
],
|
|
263
|
+
tsType: enumDefinition.name,
|
|
264
|
+
dbType: enumType,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
switch (field.type) {
|
|
269
|
+
case 'string': {
|
|
270
|
+
const entries = [`type: 'varchar'`];
|
|
271
|
+
if (field.maxLength !== undefined) {
|
|
272
|
+
entries.push(`length: ${field.maxLength}`);
|
|
273
|
+
}
|
|
274
|
+
return { typeEntries: entries, tsType: 'string', dbType: 'varchar' };
|
|
275
|
+
}
|
|
276
|
+
case 'text':
|
|
277
|
+
return {
|
|
278
|
+
typeEntries: [`type: 'text'`],
|
|
279
|
+
tsType: 'string',
|
|
280
|
+
dbType: 'text',
|
|
281
|
+
};
|
|
282
|
+
case 'uuid':
|
|
283
|
+
if (dialect === 'mysql' || dialect === 'sqlite') {
|
|
284
|
+
return {
|
|
285
|
+
typeEntries: [`type: 'varchar'`, 'length: 36'],
|
|
286
|
+
tsType: 'string',
|
|
287
|
+
dbType: 'varchar',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return dialect === 'sqlserver'
|
|
291
|
+
? {
|
|
292
|
+
typeEntries: [`type: 'uniqueidentifier'`],
|
|
293
|
+
tsType: 'string',
|
|
294
|
+
dbType: 'uniqueidentifier',
|
|
295
|
+
}
|
|
296
|
+
: { typeEntries: [`type: 'uuid'`], tsType: 'string', dbType: 'uuid' };
|
|
297
|
+
case 'int':
|
|
298
|
+
return { typeEntries: [`type: 'int'`], tsType: 'number', dbType: 'int' };
|
|
299
|
+
case 'bigInt':
|
|
300
|
+
return {
|
|
301
|
+
typeEntries: [`type: 'bigint'`],
|
|
302
|
+
tsType: 'string',
|
|
303
|
+
dbType: 'bigint',
|
|
304
|
+
};
|
|
305
|
+
case 'float':
|
|
306
|
+
return {
|
|
307
|
+
typeEntries: [`type: 'float'`],
|
|
308
|
+
tsType: 'number',
|
|
309
|
+
dbType: 'float',
|
|
310
|
+
};
|
|
311
|
+
case 'decimal': {
|
|
312
|
+
const entries = [`type: 'decimal'`];
|
|
313
|
+
if (field.maxDigits !== undefined && field.decimalPlaces !== undefined) {
|
|
314
|
+
entries.push(`precision: ${field.maxDigits}`, `scale: ${field.decimalPlaces}`);
|
|
315
|
+
}
|
|
316
|
+
return { typeEntries: entries, tsType: 'string', dbType: 'decimal' };
|
|
317
|
+
}
|
|
318
|
+
case 'boolean': {
|
|
319
|
+
const booleanType = dialect === 'sqlserver' ? 'bit' : 'boolean';
|
|
320
|
+
return {
|
|
321
|
+
typeEntries: [`type: '${booleanType}'`],
|
|
322
|
+
tsType: 'boolean',
|
|
323
|
+
dbType: booleanType,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
case 'dateTime': {
|
|
327
|
+
const dateTimeType = dialect === 'postgres'
|
|
328
|
+
? 'timestamptz'
|
|
329
|
+
: dialect === 'sqlserver'
|
|
330
|
+
? 'datetimeoffset'
|
|
331
|
+
: 'datetime';
|
|
332
|
+
return {
|
|
333
|
+
typeEntries: [`type: '${dateTimeType}'`],
|
|
334
|
+
tsType: 'Date',
|
|
335
|
+
dbType: dateTimeType,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
case 'date':
|
|
339
|
+
return {
|
|
340
|
+
typeEntries: [`type: 'date'`],
|
|
341
|
+
tsType: 'string',
|
|
342
|
+
dbType: 'date',
|
|
343
|
+
};
|
|
344
|
+
case 'time':
|
|
345
|
+
return {
|
|
346
|
+
typeEntries: [`type: 'time'`],
|
|
347
|
+
tsType: 'string',
|
|
348
|
+
dbType: 'time',
|
|
349
|
+
};
|
|
350
|
+
case 'json': {
|
|
351
|
+
const jsonType = dialect === 'postgres'
|
|
352
|
+
? 'jsonb'
|
|
353
|
+
: dialect === 'mysql'
|
|
354
|
+
? 'json'
|
|
355
|
+
: 'simple-json';
|
|
356
|
+
return {
|
|
357
|
+
typeEntries: [`type: '${jsonType}'`],
|
|
358
|
+
tsType: 'unknown',
|
|
359
|
+
dbType: jsonType,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
case 'bytes': {
|
|
363
|
+
const bytesType = dialect === 'postgres'
|
|
364
|
+
? 'bytea'
|
|
365
|
+
: dialect === 'sqlserver'
|
|
366
|
+
? 'varbinary'
|
|
367
|
+
: 'blob';
|
|
368
|
+
return {
|
|
369
|
+
typeEntries: [`type: '${bytesType}'`],
|
|
370
|
+
tsType: 'Buffer',
|
|
371
|
+
dbType: bytesType,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
case 'duration':
|
|
375
|
+
if (dialect === 'postgres') {
|
|
376
|
+
return {
|
|
377
|
+
typeEntries: [`type: 'interval'`],
|
|
378
|
+
tsType: 'string',
|
|
379
|
+
dbType: 'interval',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
context.warnings.push(`${label}: ${dialect} has no interval type; the duration was written as bigint (microseconds).`);
|
|
383
|
+
return {
|
|
384
|
+
typeEntries: [`type: 'bigint'`],
|
|
385
|
+
tsType: 'string',
|
|
386
|
+
dbType: 'bigint',
|
|
387
|
+
};
|
|
388
|
+
case 'ipAddress':
|
|
389
|
+
if (dialect === 'postgres') {
|
|
390
|
+
return {
|
|
391
|
+
typeEntries: [`type: 'inet'`],
|
|
392
|
+
tsType: 'string',
|
|
393
|
+
dbType: 'inet',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
typeEntries: [`type: 'varchar'`, 'length: 45'],
|
|
398
|
+
tsType: 'string',
|
|
399
|
+
dbType: 'varchar',
|
|
400
|
+
};
|
|
401
|
+
case 'hstore':
|
|
402
|
+
if (dialect === 'postgres') {
|
|
403
|
+
return {
|
|
404
|
+
typeEntries: [`type: 'hstore'`],
|
|
405
|
+
tsType: 'Record<string, string | null>',
|
|
406
|
+
dbType: 'hstore',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
context.warnings.push(`${label}: hstore exists only on PostgreSQL; the field was written as ${dialect === 'mysql' ? 'json' : 'simple-json'}.`);
|
|
410
|
+
return {
|
|
411
|
+
typeEntries: [
|
|
412
|
+
`type: '${dialect === 'mysql' ? 'json' : 'simple-json'}'`,
|
|
413
|
+
],
|
|
414
|
+
tsType: 'Record<string, string | null>',
|
|
415
|
+
dbType: dialect === 'mysql' ? 'json' : 'simple-json',
|
|
416
|
+
};
|
|
417
|
+
case 'range':
|
|
418
|
+
if (dialect === 'postgres') {
|
|
419
|
+
const rangeType = POSTGRES_RANGE_TYPES[field.rangeOf ?? 'int'] ?? 'int4range';
|
|
420
|
+
return {
|
|
421
|
+
typeEntries: [`type: '${rangeType}'`],
|
|
422
|
+
tsType: 'string',
|
|
423
|
+
dbType: rangeType,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
context.warnings.push(`${label}: range columns exist only on PostgreSQL; the field was written as varchar.`);
|
|
427
|
+
return {
|
|
428
|
+
typeEntries: [`type: 'varchar'`],
|
|
429
|
+
tsType: 'string',
|
|
430
|
+
dbType: 'varchar',
|
|
431
|
+
};
|
|
432
|
+
default:
|
|
433
|
+
context.warnings.push(`${label}: unknown field type "${String(field.type)}"; it was written as varchar.`);
|
|
434
|
+
return {
|
|
435
|
+
typeEntries: [`type: 'varchar'`],
|
|
436
|
+
tsType: 'string',
|
|
437
|
+
dbType: 'varchar',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Returns the `default:` option text for a field, or undefined when there is none. */
|
|
442
|
+
function defaultEntryOf(context, label, field) {
|
|
443
|
+
const defaultValue = field.default;
|
|
444
|
+
if (defaultValue === undefined) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const enumDefinition = field.enumName === undefined
|
|
448
|
+
? undefined
|
|
449
|
+
: context.schema.enums.find((candidate) => candidate.name === field.enumName);
|
|
450
|
+
switch (defaultValue.kind) {
|
|
451
|
+
case 'autoIncrement':
|
|
452
|
+
context.warnings.push(`${label}: an auto-increment default is only supported on primary keys; it was dropped.`);
|
|
453
|
+
return undefined;
|
|
454
|
+
case 'now':
|
|
455
|
+
return `default: () => 'CURRENT_TIMESTAMP'`;
|
|
456
|
+
case 'uuid': {
|
|
457
|
+
const expression = context.dialect === 'postgres'
|
|
458
|
+
? 'gen_random_uuid()'
|
|
459
|
+
: context.dialect === 'mysql'
|
|
460
|
+
? '(UUID())'
|
|
461
|
+
: context.dialect === 'sqlserver'
|
|
462
|
+
? 'NEWID()'
|
|
463
|
+
: undefined;
|
|
464
|
+
if (expression === undefined) {
|
|
465
|
+
context.warnings.push(`${label}: SQLite has no database-side UUID default; the default was dropped.`);
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
return `default: () => ${quote(expression)}`;
|
|
469
|
+
}
|
|
470
|
+
case 'enumValue': {
|
|
471
|
+
const reference = enumDefinition === undefined
|
|
472
|
+
? undefined
|
|
473
|
+
: enumReference(context, enumDefinition, defaultValue.value);
|
|
474
|
+
if (reference === undefined) {
|
|
475
|
+
context.warnings.push(`${label}: the enum default "${defaultValue.value}" does not match a member of the enum; it was written as a string.`);
|
|
476
|
+
return `default: ${quote(defaultValue.value)}`;
|
|
477
|
+
}
|
|
478
|
+
return `default: ${reference}`;
|
|
479
|
+
}
|
|
480
|
+
case 'literal': {
|
|
481
|
+
if (typeof defaultValue.value === 'string') {
|
|
482
|
+
if (enumDefinition !== undefined) {
|
|
483
|
+
const reference = enumReference(context, enumDefinition, defaultValue.value);
|
|
484
|
+
if (reference !== undefined) {
|
|
485
|
+
return `default: ${reference}`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if ((field.arrayDepth ?? 0) > 0) {
|
|
489
|
+
if (defaultValue.value !== '[]') {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
return context.dialect === 'postgres'
|
|
493
|
+
? `default: () => ${quote(quote('{}'))}`
|
|
494
|
+
: `default: () => ${quote(quote('[]'))}`;
|
|
495
|
+
}
|
|
496
|
+
if (field.type === 'hstore' && defaultValue.value === '{}') {
|
|
497
|
+
return context.dialect === 'postgres'
|
|
498
|
+
? `default: () => ${quote(quote(''))}`
|
|
499
|
+
: `default: () => ${quote(quote('{}'))}`;
|
|
500
|
+
}
|
|
501
|
+
if (field.type === 'json') {
|
|
502
|
+
return `default: () => ${quote(quote(defaultValue.value))}`;
|
|
503
|
+
}
|
|
504
|
+
if (field.type === 'bytes') {
|
|
505
|
+
context.warnings.push(`${label}: a literal default on a binary column cannot be represented; it was dropped.`);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
return `default: ${quote(defaultValue.value)}`;
|
|
509
|
+
}
|
|
510
|
+
return `default: ${String(defaultValue.value)}`;
|
|
511
|
+
}
|
|
512
|
+
default:
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Members
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
function isCreatedTimestamp(field) {
|
|
520
|
+
return (field.type === 'dateTime' &&
|
|
521
|
+
!field.isNullable &&
|
|
522
|
+
!field.isAutoUpdated &&
|
|
523
|
+
field.default !== undefined &&
|
|
524
|
+
field.default.kind === 'now' &&
|
|
525
|
+
CREATED_FIELD_PATTERN.test(field.name));
|
|
526
|
+
}
|
|
527
|
+
function isUpdatedTimestamp(field) {
|
|
528
|
+
return field.isAutoUpdated && field.type === 'dateTime' && !field.isNullable;
|
|
529
|
+
}
|
|
530
|
+
function nameEntry(columnName, propName) {
|
|
531
|
+
return columnName === propName ? undefined : `name: ${quote(columnName)}`;
|
|
532
|
+
}
|
|
533
|
+
function compact(entries) {
|
|
534
|
+
return entries.filter((entry) => entry !== undefined);
|
|
535
|
+
}
|
|
536
|
+
function buildFieldMember(context, model, field, propName) {
|
|
537
|
+
const label = `${model.name}.${field.name}`;
|
|
538
|
+
const spec = columnSpecOf(context, label, field);
|
|
539
|
+
if (field.generated !== undefined) {
|
|
540
|
+
context.warnings.push(`${label}: the generated column expression ${field.generated.expression} is Python, not SQL, and has no TypeORM equivalent; ` +
|
|
541
|
+
`the property was written as a regular column.`);
|
|
542
|
+
}
|
|
543
|
+
const nameOption = nameEntry(field.columnName, propName);
|
|
544
|
+
const inCompositeKey = model.compositePrimaryKey?.includes(field.name) ?? false;
|
|
545
|
+
const nullableSuffix = field.isNullable ? ' | null' : '';
|
|
546
|
+
const property = ` ${propName}!: ${spec.tsType}${nullableSuffix};`;
|
|
547
|
+
if (field.isAutoUpdated && !isUpdatedTimestamp(field)) {
|
|
548
|
+
context.warnings.push(`${label}: an auto-updated timestamp must be a non-null date-time column to become @UpdateDateColumn; it was written as a plain column.`);
|
|
549
|
+
}
|
|
550
|
+
if (field.isPrimaryKey || inCompositeKey) {
|
|
551
|
+
const generated = field.default;
|
|
552
|
+
if (field.isPrimaryKey &&
|
|
553
|
+
!inCompositeKey &&
|
|
554
|
+
generated !== undefined &&
|
|
555
|
+
generated.kind === 'autoIncrement' &&
|
|
556
|
+
(field.type === 'int' || field.type === 'bigInt')) {
|
|
557
|
+
const options = compact([
|
|
558
|
+
nameOption,
|
|
559
|
+
field.type === 'bigInt' ? `type: 'bigint'` : undefined,
|
|
560
|
+
]);
|
|
561
|
+
return {
|
|
562
|
+
lines: [
|
|
563
|
+
...decorator(use(context, 'PrimaryGeneratedColumn'), [`'increment'`], options),
|
|
564
|
+
property,
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (field.isPrimaryKey &&
|
|
569
|
+
!inCompositeKey &&
|
|
570
|
+
generated !== undefined &&
|
|
571
|
+
generated.kind === 'uuid' &&
|
|
572
|
+
field.type === 'uuid' &&
|
|
573
|
+
context.dialect !== 'sqlite') {
|
|
574
|
+
return {
|
|
575
|
+
lines: [
|
|
576
|
+
...decorator(use(context, 'PrimaryGeneratedColumn'), [`'uuid'`], compact([nameOption])),
|
|
577
|
+
property,
|
|
578
|
+
],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const entries = compact([
|
|
582
|
+
nameOption,
|
|
583
|
+
...spec.typeEntries,
|
|
584
|
+
defaultEntryOf(context, label, field),
|
|
585
|
+
]);
|
|
586
|
+
return {
|
|
587
|
+
lines: [
|
|
588
|
+
...decorator(use(context, 'PrimaryColumn'), [], entries),
|
|
589
|
+
property,
|
|
590
|
+
],
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (isUpdatedTimestamp(field)) {
|
|
594
|
+
return {
|
|
595
|
+
lines: [
|
|
596
|
+
...decorator(use(context, 'UpdateDateColumn'), [], compact([nameOption, ...spec.typeEntries])),
|
|
597
|
+
property,
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (isCreatedTimestamp(field)) {
|
|
602
|
+
return {
|
|
603
|
+
lines: [
|
|
604
|
+
...decorator(use(context, 'CreateDateColumn'), [], compact([nameOption, ...spec.typeEntries])),
|
|
605
|
+
property,
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const entries = compact([
|
|
610
|
+
nameOption,
|
|
611
|
+
...spec.typeEntries,
|
|
612
|
+
field.isNullable ? 'nullable: true' : undefined,
|
|
613
|
+
field.isUnique ? 'unique: true' : undefined,
|
|
614
|
+
defaultEntryOf(context, label, field),
|
|
615
|
+
]);
|
|
616
|
+
return {
|
|
617
|
+
lines: [...decorator(use(context, 'Column'), [], entries), property],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/** Resolves the target column of a relation: an explicit to_field, otherwise the primary key. */
|
|
621
|
+
function referencedKey(context, label, target, toField, depth = 0) {
|
|
622
|
+
if (depth > 5) {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
const targetNames = namesOf(context, target);
|
|
626
|
+
const explicit = toField === undefined
|
|
627
|
+
? undefined
|
|
628
|
+
: target.fields.find((field) => field.name === toField);
|
|
629
|
+
const column = explicit ?? target.fields.find((field) => field.isPrimaryKey);
|
|
630
|
+
if (column !== undefined) {
|
|
631
|
+
return {
|
|
632
|
+
property: targetNames.fields.get(column.name) ?? column.name,
|
|
633
|
+
spec: columnSpecOf(context, label, column),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const primaryRelation = target.relations.find((relation) => relation.isPrimaryKey === true);
|
|
637
|
+
if (primaryRelation !== undefined) {
|
|
638
|
+
const chained = findModel(context.schema, primaryRelation.targetModel);
|
|
639
|
+
const chainedKey = chained === undefined
|
|
640
|
+
? undefined
|
|
641
|
+
: referencedKey(context, label, chained, primaryRelation.toField, depth + 1);
|
|
642
|
+
if (chainedKey !== undefined) {
|
|
643
|
+
return {
|
|
644
|
+
property: targetNames.scalars.get(primaryRelation.name) ?? chainedKey.property,
|
|
645
|
+
spec: chainedKey.spec,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
/** Column name of the first primary key property of a model, for join tables. */
|
|
652
|
+
function primaryKeyProperty(context, model) {
|
|
653
|
+
const primary = model.fields.find((field) => field.isPrimaryKey);
|
|
654
|
+
return primary === undefined
|
|
655
|
+
? undefined
|
|
656
|
+
: namesOf(context, model).fields.get(primary.name);
|
|
657
|
+
}
|
|
658
|
+
function buildForwardMembers(context, model, inverseMembers) {
|
|
659
|
+
const members = [];
|
|
660
|
+
const names = namesOf(context, model);
|
|
661
|
+
for (const field of model.fields) {
|
|
662
|
+
members.push(buildFieldMember(context, model, field, names.fields.get(field.name) ?? field.name));
|
|
663
|
+
}
|
|
664
|
+
for (const relation of model.relations) {
|
|
665
|
+
const target = findModel(context.schema, relation.targetModel);
|
|
666
|
+
if (target === undefined) {
|
|
667
|
+
context.warnings.push(`${model.name}.${relation.name}: target model "${relation.targetModel}" does not exist in the schema; the relation was skipped.`);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (relation.kind === 'manyToMany') {
|
|
671
|
+
addManyToMany(context, model, relation, target, members, inverseMembers);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
addSingleRelation(context, model, relation, target, members, inverseMembers);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return members;
|
|
678
|
+
}
|
|
679
|
+
function addInverse(inverseMembers, targetName, member) {
|
|
680
|
+
const existing = inverseMembers.get(targetName) ?? [];
|
|
681
|
+
existing.push(member);
|
|
682
|
+
inverseMembers.set(targetName, existing);
|
|
683
|
+
}
|
|
684
|
+
function addSingleRelation(context, model, relation, target, members, inverseMembers) {
|
|
685
|
+
const label = `${model.name}.${relation.name}`;
|
|
686
|
+
const names = namesOf(context, model);
|
|
687
|
+
const relationProp = names.relations.get(relation.name) ?? relation.name;
|
|
688
|
+
const inverseProp = context.inverseNames.get(label) ?? toSnakeCase(model.name);
|
|
689
|
+
const keyed = isKeyedRelation(model, relation);
|
|
690
|
+
const needsKey = keyed || relation.toField !== undefined;
|
|
691
|
+
const key = needsKey
|
|
692
|
+
? referencedKey(context, label, target, relation.toField)
|
|
693
|
+
: undefined;
|
|
694
|
+
if (needsKey && key === undefined) {
|
|
695
|
+
context.warnings.push(`${label}: target model "${target.name}" has no single-column primary key to reference; the relation was skipped.`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (relation.onDelete === 'setNull' && !relation.isNullable) {
|
|
699
|
+
context.warnings.push(`${label}: onDelete SET NULL on a required relation will fail at the database level; review the relation.`);
|
|
700
|
+
}
|
|
701
|
+
const scalarProp = names.scalars.get(relation.name);
|
|
702
|
+
if (keyed && key !== undefined && scalarProp !== undefined) {
|
|
703
|
+
members.push({
|
|
704
|
+
lines: [
|
|
705
|
+
...decorator(use(context, 'PrimaryColumn'), [], compact([
|
|
706
|
+
`name: ${quote(relation.columnName)}`,
|
|
707
|
+
...key.spec.typeEntries,
|
|
708
|
+
])),
|
|
709
|
+
` ${scalarProp}!: ${key.spec.tsType};`,
|
|
710
|
+
],
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
const decoratorName = relation.kind === 'oneToOne' ? 'OneToOne' : 'ManyToOne';
|
|
714
|
+
const options = [
|
|
715
|
+
`onDelete: ${quote(ON_DELETE_NAMES[relation.onDelete])}`,
|
|
716
|
+
`nullable: ${relation.isNullable ? 'true' : 'false'}`,
|
|
717
|
+
];
|
|
718
|
+
const joinEntries = compact([
|
|
719
|
+
`name: ${quote(relation.columnName)}`,
|
|
720
|
+
key !== undefined && relation.toField !== undefined
|
|
721
|
+
? `referencedColumnName: ${quote(key.property)}`
|
|
722
|
+
: undefined,
|
|
723
|
+
]);
|
|
724
|
+
const relationType = `${use(context, 'Relation')}<${target.name}>${relation.isNullable ? ' | null' : ''}`;
|
|
725
|
+
members.push({
|
|
726
|
+
lines: [
|
|
727
|
+
...decorator(use(context, decoratorName), [`() => ${target.name}`, `(entity) => entity.${inverseProp}`], options),
|
|
728
|
+
...decorator(use(context, 'JoinColumn'), [], joinEntries),
|
|
729
|
+
` ${relationProp}!: ${relationType};`,
|
|
730
|
+
],
|
|
731
|
+
});
|
|
732
|
+
if (relation.kind === 'oneToOne') {
|
|
733
|
+
addInverse(inverseMembers, target.name, {
|
|
734
|
+
lines: [
|
|
735
|
+
...decorator(use(context, 'OneToOne'), [`() => ${model.name}`, `(entity) => entity.${relationProp}`], []),
|
|
736
|
+
` ${inverseProp}!: ${use(context, 'Relation')}<${model.name}> | null;`,
|
|
737
|
+
],
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
addInverse(inverseMembers, target.name, {
|
|
742
|
+
lines: [
|
|
743
|
+
...decorator(use(context, 'OneToMany'), [`() => ${model.name}`, `(entity) => entity.${relationProp}`], []),
|
|
744
|
+
` ${inverseProp}!: ${model.name}[];`,
|
|
745
|
+
],
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function addManyToMany(context, model, relation, target, members, inverseMembers) {
|
|
750
|
+
const label = `${model.name}.${relation.name}`;
|
|
751
|
+
const names = namesOf(context, model);
|
|
752
|
+
const relationProp = names.relations.get(relation.name) ?? relation.name;
|
|
753
|
+
const inverseProp = context.inverseNames.get(label) ?? `${toSnakeCase(model.name)}_set`;
|
|
754
|
+
const ownerSnake = toSnakeCase(model.name);
|
|
755
|
+
const targetSnake = toSnakeCase(target.name);
|
|
756
|
+
const isSelfReference = model.name === target.name;
|
|
757
|
+
const ownerColumn = `${isSelfReference ? 'from_' : ''}${ownerSnake}_id`;
|
|
758
|
+
const targetColumn = `${isSelfReference ? 'to_' : ''}${targetSnake}_id`;
|
|
759
|
+
const ownerKey = primaryKeyProperty(context, model);
|
|
760
|
+
const targetKey = primaryKeyProperty(context, target);
|
|
761
|
+
const joinColumn = compact([
|
|
762
|
+
`name: ${quote(ownerColumn)}`,
|
|
763
|
+
ownerKey === undefined
|
|
764
|
+
? undefined
|
|
765
|
+
: `referencedColumnName: ${quote(ownerKey)}`,
|
|
766
|
+
]);
|
|
767
|
+
const inverseJoinColumn = compact([
|
|
768
|
+
`name: ${quote(targetColumn)}`,
|
|
769
|
+
targetKey === undefined
|
|
770
|
+
? undefined
|
|
771
|
+
: `referencedColumnName: ${quote(targetKey)}`,
|
|
772
|
+
]);
|
|
773
|
+
members.push({
|
|
774
|
+
lines: [
|
|
775
|
+
...decorator(use(context, 'ManyToMany'), [`() => ${target.name}`, `(entity) => entity.${inverseProp}`], []),
|
|
776
|
+
...decorator(use(context, 'JoinTable'), [], [
|
|
777
|
+
`name: ${quote(`${model.tableName}_${relation.name}`)}`,
|
|
778
|
+
`joinColumn: { ${joinColumn.join(', ')} }`,
|
|
779
|
+
`inverseJoinColumn: { ${inverseJoinColumn.join(', ')} }`,
|
|
780
|
+
]),
|
|
781
|
+
` ${relationProp}!: ${target.name}[];`,
|
|
782
|
+
],
|
|
783
|
+
});
|
|
784
|
+
addInverse(inverseMembers, target.name, {
|
|
785
|
+
lines: [
|
|
786
|
+
...decorator(use(context, 'ManyToMany'), [`() => ${model.name}`, `(entity) => entity.${relationProp}`], []),
|
|
787
|
+
` ${inverseProp}!: ${model.name}[];`,
|
|
788
|
+
],
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
// Classes
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
function resolveIndexProperty(context, model, fieldName) {
|
|
795
|
+
const names = namesOf(context, model);
|
|
796
|
+
const relation = model.relations.find((candidate) => candidate.name === fieldName) ??
|
|
797
|
+
model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.columnName === fieldName);
|
|
798
|
+
if (relation !== undefined) {
|
|
799
|
+
return names.relations.get(relation.name) ?? fieldName;
|
|
800
|
+
}
|
|
801
|
+
const field = model.fields.find((candidate) => candidate.name === fieldName) ??
|
|
802
|
+
model.fields.find((candidate) => candidate.columnName === fieldName);
|
|
803
|
+
if (field !== undefined) {
|
|
804
|
+
return names.fields.get(field.name) ?? fieldName;
|
|
805
|
+
}
|
|
806
|
+
context.warnings.push(`${model.name}: an index references "${fieldName}", which is not a field of the model; it was written as-is.`);
|
|
807
|
+
return propertyName(context, fieldName);
|
|
808
|
+
}
|
|
809
|
+
function indexDecorator(context, model, index) {
|
|
810
|
+
const properties = `[${index.fields
|
|
811
|
+
.map((fieldName) => quote(resolveIndexProperty(context, model, fieldName)))
|
|
812
|
+
.join(', ')}]`;
|
|
813
|
+
const args = index.name === undefined ? [properties] : [quote(index.name), properties];
|
|
814
|
+
return decorator(use(context, index.isUnique ? 'Unique' : 'Index'), args, [], '');
|
|
815
|
+
}
|
|
816
|
+
function emitClass(context, model, members) {
|
|
817
|
+
const hasPrimaryKey = model.fields.some((field) => field.isPrimaryKey) ||
|
|
818
|
+
model.relations.some((relation) => relation.isPrimaryKey === true) ||
|
|
819
|
+
(model.compositePrimaryKey !== undefined &&
|
|
820
|
+
model.compositePrimaryKey.length > 0);
|
|
821
|
+
if (!hasPrimaryKey) {
|
|
822
|
+
context.warnings.push(`${model.name}: the model has no primary key; TypeORM requires at least one primary column.`);
|
|
823
|
+
}
|
|
824
|
+
const header = [
|
|
825
|
+
...decorator(use(context, 'Entity'), [quote(model.tableName)], [], ''),
|
|
826
|
+
];
|
|
827
|
+
for (const index of model.indexes) {
|
|
828
|
+
header.push(...indexDecorator(context, model, index));
|
|
829
|
+
}
|
|
830
|
+
const body = members.flatMap((member, position) => position === 0 ? member.lines : ['', ...member.lines]);
|
|
831
|
+
return [...header, `export class ${model.name} {`, ...body, '}'].join('\n');
|
|
832
|
+
}
|
|
833
|
+
//# sourceMappingURL=typeorm.js.map
|