@mikro-orm/entity-generator 7.0.4-dev.8 → 7.0.4
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/CoreImportsHelper.d.ts +10 -1
- package/CoreImportsHelper.js +1 -10
- package/DefineEntitySourceFile.d.ts +2 -2
- package/DefineEntitySourceFile.js +140 -139
- package/EntityGenerator.d.ts +12 -12
- package/EntityGenerator.js +377 -343
- package/EntitySchemaSourceFile.d.ts +5 -5
- package/EntitySchemaSourceFile.js +139 -143
- package/NativeEnumSourceFile.d.ts +14 -8
- package/NativeEnumSourceFile.js +43 -41
- package/README.md +1 -1
- package/SourceFile.d.ts +59 -41
- package/SourceFile.js +987 -919
- package/package.json +4 -4
package/EntityGenerator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntitySchema, ReferenceKind, types, Utils
|
|
2
|
-
import { DatabaseSchema
|
|
1
|
+
import { EntitySchema, ReferenceKind, types, Utils } from '@mikro-orm/core';
|
|
2
|
+
import { DatabaseSchema } from '@mikro-orm/sql';
|
|
3
3
|
import { fs } from '@mikro-orm/core/fs-utils';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { writeFile } from 'node:fs/promises';
|
|
@@ -9,371 +9,405 @@ import { NativeEnumSourceFile } from './NativeEnumSourceFile.js';
|
|
|
9
9
|
import { SourceFile } from './SourceFile.js';
|
|
10
10
|
/** Generates entity source files by introspecting an existing database schema. */
|
|
11
11
|
export class EntityGenerator {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
#config;
|
|
13
|
+
#driver;
|
|
14
|
+
#platform;
|
|
15
|
+
#helper;
|
|
16
|
+
#connection;
|
|
17
|
+
#namingStrategy;
|
|
18
|
+
#sources = [];
|
|
19
|
+
#referencedEntities = new WeakSet();
|
|
20
|
+
#em;
|
|
21
|
+
constructor(em) {
|
|
22
|
+
this.#em = em;
|
|
23
|
+
this.#config = this.#em.config;
|
|
24
|
+
this.#driver = this.#em.getDriver();
|
|
25
|
+
this.#platform = this.#driver.getPlatform();
|
|
26
|
+
this.#helper = this.#platform.getSchemaHelper();
|
|
27
|
+
this.#connection = this.#driver.getConnection();
|
|
28
|
+
this.#namingStrategy = this.#config.getNamingStrategy();
|
|
29
|
+
}
|
|
30
|
+
static register(orm) {
|
|
31
|
+
orm.config.registerExtension('@mikro-orm/entity-generator', () => new EntityGenerator(orm.em));
|
|
32
|
+
}
|
|
33
|
+
async generate(options = {}) {
|
|
34
|
+
options = Utils.mergeConfig({}, this.#config.get('entityGenerator'), options);
|
|
35
|
+
const schema = await DatabaseSchema.create(
|
|
36
|
+
this.#connection,
|
|
37
|
+
this.#platform,
|
|
38
|
+
this.#config,
|
|
39
|
+
undefined,
|
|
40
|
+
undefined,
|
|
41
|
+
options.takeTables,
|
|
42
|
+
options.skipTables,
|
|
43
|
+
);
|
|
44
|
+
const metadata = await this.getEntityMetadata(schema, options);
|
|
45
|
+
const defaultPath = `${this.#config.get('baseDir')}/generated-entities`;
|
|
46
|
+
const baseDir = fs.normalizePath(options.path ?? defaultPath);
|
|
47
|
+
this.#sources.length = 0;
|
|
48
|
+
const map = {
|
|
49
|
+
defineEntity: DefineEntitySourceFile,
|
|
50
|
+
entitySchema: EntitySchemaSourceFile,
|
|
51
|
+
decorators: SourceFile,
|
|
52
|
+
};
|
|
53
|
+
if (options.entityDefinition !== 'decorators') {
|
|
54
|
+
options.scalarTypeInDecorator = true;
|
|
29
55
|
}
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
for (const meta of metadata) {
|
|
57
|
+
if (meta.pivotTable && !options.outputPurePivotTables && !this.#referencedEntities.has(meta)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
this.#sources.push(new map[options.entityDefinition](meta, this.#namingStrategy, this.#platform, options));
|
|
32
61
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
for (const nativeEnum of Object.values(schema.getNativeEnums())) {
|
|
63
|
+
this.#sources.push(new NativeEnumSourceFile({}, this.#namingStrategy, this.#platform, options, nativeEnum));
|
|
64
|
+
}
|
|
65
|
+
const files = this.#sources.map(file => [file.getBaseName(), file.generate()]);
|
|
66
|
+
if (options.save) {
|
|
67
|
+
fs.ensureDir(baseDir);
|
|
68
|
+
const promises = [];
|
|
69
|
+
for (const [fileName, data] of files) {
|
|
70
|
+
promises.push(
|
|
71
|
+
(async () => {
|
|
72
|
+
const fileDir = dirname(fileName);
|
|
73
|
+
if (fileDir !== '.') {
|
|
74
|
+
fs.ensureDir(join(baseDir, fileDir));
|
|
75
|
+
}
|
|
76
|
+
await writeFile(join(baseDir, fileName), data, { flush: true });
|
|
77
|
+
})(),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
await Promise.all(promises);
|
|
81
|
+
}
|
|
82
|
+
return files.map(([, data]) => data);
|
|
83
|
+
}
|
|
84
|
+
async getEntityMetadata(schema, options) {
|
|
85
|
+
const metadata = schema
|
|
86
|
+
.getTables()
|
|
87
|
+
.filter(table => !options.schema || table.schema === options.schema)
|
|
88
|
+
.sort((a, b) => `${a.schema}.${a.name}`.localeCompare(`${b.schema}.${b.name}`))
|
|
89
|
+
.map(table => {
|
|
90
|
+
const skipColumns = options.skipColumns?.[table.getShortestName(false)];
|
|
91
|
+
if (skipColumns) {
|
|
92
|
+
for (const col of table.getColumns()) {
|
|
93
|
+
if (skipColumns.some(matchColumnName => this.matchName(col.name, matchColumnName))) {
|
|
94
|
+
table.removeColumn(col.name);
|
|
51
95
|
}
|
|
52
|
-
|
|
96
|
+
}
|
|
53
97
|
}
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
return table.getEntityDeclaration(this.#namingStrategy, this.#helper, options.scalarPropertiesForRelations);
|
|
99
|
+
});
|
|
100
|
+
for (const meta of metadata) {
|
|
101
|
+
for (const prop of meta.relations) {
|
|
102
|
+
if (
|
|
103
|
+
!metadata.some(
|
|
104
|
+
otherMeta =>
|
|
105
|
+
prop.referencedTableName === otherMeta.collection ||
|
|
106
|
+
prop.referencedTableName === `${otherMeta.schema ?? schema.name}.${otherMeta.collection}`,
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
prop.kind = ReferenceKind.SCALAR;
|
|
110
|
+
const mappedTypes = prop.columnTypes.map((t, i) => this.#platform.getMappedType(t));
|
|
111
|
+
const runtimeTypes = mappedTypes.map(t => t.runtimeType);
|
|
112
|
+
prop.runtimeType = runtimeTypes.length === 1 ? runtimeTypes[0] : `[${runtimeTypes.join(', ')}]`;
|
|
113
|
+
prop.type =
|
|
114
|
+
mappedTypes.length === 1
|
|
115
|
+
? (Utils.entries(types).find(([k, v]) => Object.getPrototypeOf(mappedTypes[0]) === v.prototype)?.[0] ??
|
|
116
|
+
mappedTypes[0].name)
|
|
117
|
+
: 'unknown';
|
|
56
118
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
const meta2 = metadata.find(meta2 => meta2.className === prop.type);
|
|
120
|
+
const targetPrimaryColumns = meta2?.getPrimaryProps().flatMap(p => p.fieldNames);
|
|
121
|
+
if (targetPrimaryColumns && targetPrimaryColumns.length !== prop.referencedColumnNames.length) {
|
|
122
|
+
prop.ownColumns = prop.joinColumns.filter(col => {
|
|
123
|
+
return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await options.onInitialMetadata?.(metadata, this.#platform);
|
|
129
|
+
// enforce schema usage in class names only on duplicates
|
|
130
|
+
const duplicates = Utils.findDuplicates(metadata.map(meta => meta.className));
|
|
131
|
+
for (const duplicate of duplicates) {
|
|
132
|
+
for (const meta of metadata.filter(meta => meta.className === duplicate)) {
|
|
133
|
+
meta.className = this.#namingStrategy.getEntityName(`${meta.schema ?? schema.name}_${meta.className}`);
|
|
134
|
+
for (const relMeta of metadata) {
|
|
135
|
+
for (const prop of relMeta.relations) {
|
|
136
|
+
if (
|
|
137
|
+
prop.type === duplicate &&
|
|
138
|
+
(prop.referencedTableName === meta.collection ||
|
|
139
|
+
prop.referencedTableName === `${meta.schema ?? schema.name}.${meta.collection}`)
|
|
140
|
+
) {
|
|
141
|
+
prop.type = meta.className;
|
|
69
142
|
}
|
|
70
|
-
|
|
143
|
+
}
|
|
71
144
|
}
|
|
72
|
-
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.detectManyToManyRelations(
|
|
148
|
+
metadata,
|
|
149
|
+
options.onlyPurePivotTables,
|
|
150
|
+
options.readOnlyPivotTables,
|
|
151
|
+
options.outputPurePivotTables,
|
|
152
|
+
);
|
|
153
|
+
this.cleanUpReferentialIntegrityRules(metadata);
|
|
154
|
+
if (options.bidirectionalRelations) {
|
|
155
|
+
this.generateBidirectionalRelations(metadata, options.outputPurePivotTables);
|
|
156
|
+
}
|
|
157
|
+
if (options.identifiedReferences) {
|
|
158
|
+
this.generateIdentifiedReferences(metadata);
|
|
159
|
+
}
|
|
160
|
+
if (options.customBaseEntityName) {
|
|
161
|
+
this.generateAndAttachCustomBaseEntity(metadata, options.customBaseEntityName);
|
|
73
162
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
163
|
+
if (options.undefinedDefaults) {
|
|
164
|
+
this.castNullDefaultsToUndefined(metadata);
|
|
165
|
+
}
|
|
166
|
+
await options.onProcessedMetadata?.(metadata, this.#platform);
|
|
167
|
+
return metadata;
|
|
168
|
+
}
|
|
169
|
+
cleanUpReferentialIntegrityRules(metadata) {
|
|
170
|
+
// Clear FK rules that match defaults for:
|
|
171
|
+
// 1. FK-as-PK entities (all PKs are FKs) - cascade for both update and delete
|
|
172
|
+
// 2. Fixed-order pivot tables (autoincrement id + 2 FK relations only) - cascade for both
|
|
173
|
+
// 3. Relations to composite PK targets - cascade for update
|
|
174
|
+
for (const meta of metadata) {
|
|
175
|
+
const pks = meta.getPrimaryProps();
|
|
176
|
+
const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
|
|
177
|
+
// Case 1: All PKs are FKs - default is cascade for both update and delete
|
|
178
|
+
if (fkPks.length > 0 && fkPks.length === pks.length) {
|
|
179
|
+
for (const pk of fkPks) {
|
|
180
|
+
if (pk.deleteRule === 'cascade') {
|
|
181
|
+
delete pk.deleteRule;
|
|
182
|
+
}
|
|
183
|
+
if (pk.updateRule === 'cascade') {
|
|
184
|
+
delete pk.updateRule;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Case 2: Fixed-order pivot table (single autoincrement id PK + exactly 2 M:1 relations)
|
|
189
|
+
const hasAutoIncrementPk = pks.length === 1 && pks[0].autoincrement;
|
|
190
|
+
const m2oRelations = meta.relations.filter(r => r.kind === ReferenceKind.MANY_TO_ONE);
|
|
191
|
+
if (hasAutoIncrementPk && m2oRelations.length === 2) {
|
|
192
|
+
// Check if all columns are either the PK or FK columns
|
|
193
|
+
const fkColumns = new Set(m2oRelations.flatMap(r => r.fieldNames));
|
|
194
|
+
const pkColumns = new Set(pks.flatMap(p => p.fieldNames));
|
|
195
|
+
const allColumns = new Set(meta.props.filter(p => p.persist !== false).flatMap(p => p.fieldNames));
|
|
196
|
+
const isPivotLike = [...allColumns].every(col => fkColumns.has(col) || pkColumns.has(col));
|
|
197
|
+
if (isPivotLike) {
|
|
198
|
+
for (const rel of m2oRelations) {
|
|
199
|
+
if (rel.updateRule === 'cascade') {
|
|
200
|
+
delete rel.updateRule;
|
|
87
201
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const meta of metadata) {
|
|
91
|
-
for (const prop of meta.relations) {
|
|
92
|
-
if (!metadata.some(otherMeta => prop.referencedTableName === otherMeta.collection ||
|
|
93
|
-
prop.referencedTableName === `${otherMeta.schema ?? schema.name}.${otherMeta.collection}`)) {
|
|
94
|
-
prop.kind = ReferenceKind.SCALAR;
|
|
95
|
-
const mappedTypes = prop.columnTypes.map((t, i) => this.#platform.getMappedType(t));
|
|
96
|
-
const runtimeTypes = mappedTypes.map(t => t.runtimeType);
|
|
97
|
-
prop.runtimeType = (runtimeTypes.length === 1 ? runtimeTypes[0] : `[${runtimeTypes.join(', ')}]`);
|
|
98
|
-
prop.type =
|
|
99
|
-
mappedTypes.length === 1
|
|
100
|
-
? (Utils.entries(types).find(([k, v]) => Object.getPrototypeOf(mappedTypes[0]) === v.prototype)?.[0] ??
|
|
101
|
-
mappedTypes[0].name)
|
|
102
|
-
: 'unknown';
|
|
103
|
-
}
|
|
104
|
-
const meta2 = metadata.find(meta2 => meta2.className === prop.type);
|
|
105
|
-
const targetPrimaryColumns = meta2?.getPrimaryProps().flatMap(p => p.fieldNames);
|
|
106
|
-
if (targetPrimaryColumns && targetPrimaryColumns.length !== prop.referencedColumnNames.length) {
|
|
107
|
-
prop.ownColumns = prop.joinColumns.filter(col => {
|
|
108
|
-
return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
|
|
109
|
-
});
|
|
110
|
-
}
|
|
202
|
+
if (rel.deleteRule === 'cascade') {
|
|
203
|
+
delete rel.deleteRule;
|
|
111
204
|
}
|
|
205
|
+
}
|
|
112
206
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
207
|
+
}
|
|
208
|
+
// Case 3: Relations to composite PK targets - default is cascade for update
|
|
209
|
+
// Case 4: Nullable relations - default is set null for delete
|
|
210
|
+
for (const rel of meta.relations) {
|
|
211
|
+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(rel.kind)) {
|
|
212
|
+
const targetMeta = metadata.find(m => m.className === rel.type);
|
|
213
|
+
if (targetMeta?.compositePK && rel.updateRule === 'cascade') {
|
|
214
|
+
delete rel.updateRule;
|
|
215
|
+
}
|
|
216
|
+
if (rel.nullable && rel.deleteRule === 'set null') {
|
|
217
|
+
delete rel.deleteRule;
|
|
218
|
+
}
|
|
129
219
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
matchName(name, nameToMatch) {
|
|
224
|
+
return typeof nameToMatch === 'string'
|
|
225
|
+
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
|
|
226
|
+
: nameToMatch.test(name);
|
|
227
|
+
}
|
|
228
|
+
detectManyToManyRelations(metadata, onlyPurePivotTables, readOnlyPivotTables, outputPurePivotTables) {
|
|
229
|
+
for (const meta of metadata) {
|
|
230
|
+
const isReferenced = metadata.some(m => {
|
|
231
|
+
return (
|
|
232
|
+
m.tableName !== meta.tableName &&
|
|
233
|
+
m.relations.some(r => {
|
|
234
|
+
return (
|
|
235
|
+
r.referencedTableName === meta.tableName &&
|
|
236
|
+
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(r.kind)
|
|
237
|
+
);
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
if (isReferenced) {
|
|
242
|
+
this.#referencedEntities.add(meta);
|
|
243
|
+
}
|
|
244
|
+
// Entities with non-composite PKs are never pivot tables. Skip.
|
|
245
|
+
if (!meta.compositePK) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Entities where there are not exactly 2 PK relations that are both ManyToOne are never pivot tables. Skip.
|
|
249
|
+
const pkRelations = meta.relations.filter(rel => rel.primary);
|
|
250
|
+
if (pkRelations.length !== 2 || pkRelations.some(rel => rel.kind !== ReferenceKind.MANY_TO_ONE)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const pkRelationFields = new Set(pkRelations.flatMap(rel => rel.fieldNames));
|
|
254
|
+
const nonPkFields = Array.from(new Set(meta.props.flatMap(prop => prop.fieldNames))).filter(
|
|
255
|
+
fieldName => !pkRelationFields.has(fieldName),
|
|
256
|
+
);
|
|
257
|
+
let fixedOrderColumn;
|
|
258
|
+
let isReadOnly = false;
|
|
259
|
+
// If there are any fields other than the ones in the two PK relations, table may or may not be a pivot one.
|
|
260
|
+
// Check further and skip on disqualification.
|
|
261
|
+
if (nonPkFields.length > 0) {
|
|
262
|
+
// Additional columns have been disabled with the setting.
|
|
263
|
+
// Skip table even it otherwise would have qualified as a pivot table.
|
|
264
|
+
if (onlyPurePivotTables) {
|
|
265
|
+
continue;
|
|
134
266
|
}
|
|
135
|
-
|
|
136
|
-
|
|
267
|
+
const pkRelationNames = pkRelations.map(rel => rel.name);
|
|
268
|
+
let otherProps = meta.props.filter(
|
|
269
|
+
prop =>
|
|
270
|
+
!pkRelationNames.includes(prop.name) &&
|
|
271
|
+
prop.persist !== false && // Skip checking non-persist props
|
|
272
|
+
prop.fieldNames.some(fieldName => nonPkFields.includes(fieldName)),
|
|
273
|
+
);
|
|
274
|
+
// Deal with the auto increment column first. That is the column used for fixed ordering, if present.
|
|
275
|
+
const autoIncrementProp = meta.props.find(prop => prop.autoincrement && prop.fieldNames.length === 1);
|
|
276
|
+
if (autoIncrementProp) {
|
|
277
|
+
otherProps = otherProps.filter(prop => prop !== autoIncrementProp);
|
|
278
|
+
fixedOrderColumn = autoIncrementProp.fieldNames[0];
|
|
137
279
|
}
|
|
138
|
-
|
|
139
|
-
|
|
280
|
+
isReadOnly = otherProps.some(prop => {
|
|
281
|
+
// If the prop is non-nullable and unique, it will trivially end up causing issues.
|
|
282
|
+
// Mark as read only.
|
|
283
|
+
if (!prop.nullable && prop.unique) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
// Any other props need to also be optional.
|
|
287
|
+
// Whether they have a default or are generated,
|
|
288
|
+
// we've already checked that not explicitly setting the property means the default is either NULL,
|
|
289
|
+
// or a non-unique non-null value, making it safe to write to pivot entity.
|
|
290
|
+
return !prop.optional;
|
|
291
|
+
});
|
|
292
|
+
if (isReadOnly && !readOnlyPivotTables) {
|
|
293
|
+
continue;
|
|
140
294
|
}
|
|
141
|
-
|
|
142
|
-
|
|
295
|
+
// If this now proven pivot entity has persistent props other than the fixed order column,
|
|
296
|
+
// output it, by considering it as a referenced one.
|
|
297
|
+
if (otherProps.length > 0) {
|
|
298
|
+
this.#referencedEntities.add(meta);
|
|
143
299
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// 3. Relations to composite PK targets - cascade for update
|
|
152
|
-
for (const meta of metadata) {
|
|
153
|
-
const pks = meta.getPrimaryProps();
|
|
154
|
-
const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
|
|
155
|
-
// Case 1: All PKs are FKs - default is cascade for both update and delete
|
|
156
|
-
if (fkPks.length > 0 && fkPks.length === pks.length) {
|
|
157
|
-
for (const pk of fkPks) {
|
|
158
|
-
if (pk.deleteRule === 'cascade') {
|
|
159
|
-
delete pk.deleteRule;
|
|
160
|
-
}
|
|
161
|
-
if (pk.updateRule === 'cascade') {
|
|
162
|
-
delete pk.updateRule;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Case 2: Fixed-order pivot table (single autoincrement id PK + exactly 2 M:1 relations)
|
|
167
|
-
const hasAutoIncrementPk = pks.length === 1 && pks[0].autoincrement;
|
|
168
|
-
const m2oRelations = meta.relations.filter(r => r.kind === ReferenceKind.MANY_TO_ONE);
|
|
169
|
-
if (hasAutoIncrementPk && m2oRelations.length === 2) {
|
|
170
|
-
// Check if all columns are either the PK or FK columns
|
|
171
|
-
const fkColumns = new Set(m2oRelations.flatMap(r => r.fieldNames));
|
|
172
|
-
const pkColumns = new Set(pks.flatMap(p => p.fieldNames));
|
|
173
|
-
const allColumns = new Set(meta.props.filter(p => p.persist !== false).flatMap(p => p.fieldNames));
|
|
174
|
-
const isPivotLike = [...allColumns].every(col => fkColumns.has(col) || pkColumns.has(col));
|
|
175
|
-
if (isPivotLike) {
|
|
176
|
-
for (const rel of m2oRelations) {
|
|
177
|
-
if (rel.updateRule === 'cascade') {
|
|
178
|
-
delete rel.updateRule;
|
|
179
|
-
}
|
|
180
|
-
if (rel.deleteRule === 'cascade') {
|
|
181
|
-
delete rel.deleteRule;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Case 3: Relations to composite PK targets - default is cascade for update
|
|
187
|
-
// Case 4: Nullable relations - default is set null for delete
|
|
188
|
-
for (const rel of meta.relations) {
|
|
189
|
-
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(rel.kind)) {
|
|
190
|
-
const targetMeta = metadata.find(m => m.className === rel.type);
|
|
191
|
-
if (targetMeta?.compositePK && rel.updateRule === 'cascade') {
|
|
192
|
-
delete rel.updateRule;
|
|
193
|
-
}
|
|
194
|
-
if (rel.nullable && rel.deleteRule === 'set null') {
|
|
195
|
-
delete rel.deleteRule;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
300
|
+
}
|
|
301
|
+
meta.pivotTable = true;
|
|
302
|
+
// Clear FK rules that match the default for pivot tables (cascade)
|
|
303
|
+
// so they don't get output explicitly in generated code
|
|
304
|
+
for (const rel of meta.relations) {
|
|
305
|
+
if (rel.updateRule === 'cascade') {
|
|
306
|
+
delete rel.updateRule;
|
|
199
307
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return typeof nameToMatch === 'string'
|
|
203
|
-
? name.toLocaleLowerCase() === nameToMatch.toLocaleLowerCase()
|
|
204
|
-
: nameToMatch.test(name);
|
|
205
|
-
}
|
|
206
|
-
detectManyToManyRelations(metadata, onlyPurePivotTables, readOnlyPivotTables, outputPurePivotTables) {
|
|
207
|
-
for (const meta of metadata) {
|
|
208
|
-
const isReferenced = metadata.some(m => {
|
|
209
|
-
return (m.tableName !== meta.tableName &&
|
|
210
|
-
m.relations.some(r => {
|
|
211
|
-
return (r.referencedTableName === meta.tableName &&
|
|
212
|
-
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(r.kind));
|
|
213
|
-
}));
|
|
214
|
-
});
|
|
215
|
-
if (isReferenced) {
|
|
216
|
-
this.#referencedEntities.add(meta);
|
|
217
|
-
}
|
|
218
|
-
// Entities with non-composite PKs are never pivot tables. Skip.
|
|
219
|
-
if (!meta.compositePK) {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
// Entities where there are not exactly 2 PK relations that are both ManyToOne are never pivot tables. Skip.
|
|
223
|
-
const pkRelations = meta.relations.filter(rel => rel.primary);
|
|
224
|
-
if (pkRelations.length !== 2 || pkRelations.some(rel => rel.kind !== ReferenceKind.MANY_TO_ONE)) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const pkRelationFields = new Set(pkRelations.flatMap(rel => rel.fieldNames));
|
|
228
|
-
const nonPkFields = Array.from(new Set(meta.props.flatMap(prop => prop.fieldNames))).filter(fieldName => !pkRelationFields.has(fieldName));
|
|
229
|
-
let fixedOrderColumn;
|
|
230
|
-
let isReadOnly = false;
|
|
231
|
-
// If there are any fields other than the ones in the two PK relations, table may or may not be a pivot one.
|
|
232
|
-
// Check further and skip on disqualification.
|
|
233
|
-
if (nonPkFields.length > 0) {
|
|
234
|
-
// Additional columns have been disabled with the setting.
|
|
235
|
-
// Skip table even it otherwise would have qualified as a pivot table.
|
|
236
|
-
if (onlyPurePivotTables) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const pkRelationNames = pkRelations.map(rel => rel.name);
|
|
240
|
-
let otherProps = meta.props.filter(prop => !pkRelationNames.includes(prop.name) &&
|
|
241
|
-
prop.persist !== false && // Skip checking non-persist props
|
|
242
|
-
prop.fieldNames.some(fieldName => nonPkFields.includes(fieldName)));
|
|
243
|
-
// Deal with the auto increment column first. That is the column used for fixed ordering, if present.
|
|
244
|
-
const autoIncrementProp = meta.props.find(prop => prop.autoincrement && prop.fieldNames.length === 1);
|
|
245
|
-
if (autoIncrementProp) {
|
|
246
|
-
otherProps = otherProps.filter(prop => prop !== autoIncrementProp);
|
|
247
|
-
fixedOrderColumn = autoIncrementProp.fieldNames[0];
|
|
248
|
-
}
|
|
249
|
-
isReadOnly = otherProps.some(prop => {
|
|
250
|
-
// If the prop is non-nullable and unique, it will trivially end up causing issues.
|
|
251
|
-
// Mark as read only.
|
|
252
|
-
if (!prop.nullable && prop.unique) {
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
// Any other props need to also be optional.
|
|
256
|
-
// Whether they have a default or are generated,
|
|
257
|
-
// we've already checked that not explicitly setting the property means the default is either NULL,
|
|
258
|
-
// or a non-unique non-null value, making it safe to write to pivot entity.
|
|
259
|
-
return !prop.optional;
|
|
260
|
-
});
|
|
261
|
-
if (isReadOnly && !readOnlyPivotTables) {
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
// If this now proven pivot entity has persistent props other than the fixed order column,
|
|
265
|
-
// output it, by considering it as a referenced one.
|
|
266
|
-
if (otherProps.length > 0) {
|
|
267
|
-
this.#referencedEntities.add(meta);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
meta.pivotTable = true;
|
|
271
|
-
// Clear FK rules that match the default for pivot tables (cascade)
|
|
272
|
-
// so they don't get output explicitly in generated code
|
|
273
|
-
for (const rel of meta.relations) {
|
|
274
|
-
if (rel.updateRule === 'cascade') {
|
|
275
|
-
delete rel.updateRule;
|
|
276
|
-
}
|
|
277
|
-
if (rel.deleteRule === 'cascade') {
|
|
278
|
-
delete rel.deleteRule;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
const owner = metadata.find(m => m.className === meta.relations[0].type);
|
|
282
|
-
const target = metadata.find(m => m.className === meta.relations[1].type);
|
|
283
|
-
const name = this.#namingStrategy.manyToManyPropertyName(owner.className, target.className, meta.tableName, owner.tableName, meta.schema);
|
|
284
|
-
const ownerProp = {
|
|
285
|
-
name,
|
|
286
|
-
kind: ReferenceKind.MANY_TO_MANY,
|
|
287
|
-
pivotTable: meta.tableName,
|
|
288
|
-
type: meta.relations[1].type,
|
|
289
|
-
joinColumns: meta.relations[0].fieldNames,
|
|
290
|
-
inverseJoinColumns: meta.relations[1].fieldNames,
|
|
291
|
-
};
|
|
292
|
-
if (outputPurePivotTables || this.#referencedEntities.has(meta)) {
|
|
293
|
-
ownerProp.pivotEntity = meta.class;
|
|
294
|
-
}
|
|
295
|
-
if (fixedOrderColumn) {
|
|
296
|
-
ownerProp.fixedOrder = true;
|
|
297
|
-
ownerProp.fixedOrderColumn = fixedOrderColumn;
|
|
298
|
-
}
|
|
299
|
-
if (isReadOnly) {
|
|
300
|
-
ownerProp.persist = false;
|
|
301
|
-
}
|
|
302
|
-
owner.addProperty(ownerProp);
|
|
308
|
+
if (rel.deleteRule === 'cascade') {
|
|
309
|
+
delete rel.deleteRule;
|
|
303
310
|
}
|
|
311
|
+
}
|
|
312
|
+
const owner = metadata.find(m => m.className === meta.relations[0].type);
|
|
313
|
+
const target = metadata.find(m => m.className === meta.relations[1].type);
|
|
314
|
+
const name = this.#namingStrategy.manyToManyPropertyName(
|
|
315
|
+
owner.className,
|
|
316
|
+
target.className,
|
|
317
|
+
meta.tableName,
|
|
318
|
+
owner.tableName,
|
|
319
|
+
meta.schema,
|
|
320
|
+
);
|
|
321
|
+
const ownerProp = {
|
|
322
|
+
name,
|
|
323
|
+
kind: ReferenceKind.MANY_TO_MANY,
|
|
324
|
+
pivotTable: meta.tableName,
|
|
325
|
+
type: meta.relations[1].type,
|
|
326
|
+
joinColumns: meta.relations[0].fieldNames,
|
|
327
|
+
inverseJoinColumns: meta.relations[1].fieldNames,
|
|
328
|
+
};
|
|
329
|
+
if (outputPurePivotTables || this.#referencedEntities.has(meta)) {
|
|
330
|
+
ownerProp.pivotEntity = meta.class;
|
|
331
|
+
}
|
|
332
|
+
if (fixedOrderColumn) {
|
|
333
|
+
ownerProp.fixedOrder = true;
|
|
334
|
+
ownerProp.fixedOrderColumn = fixedOrderColumn;
|
|
335
|
+
}
|
|
336
|
+
if (isReadOnly) {
|
|
337
|
+
ownerProp.persist = false;
|
|
338
|
+
}
|
|
339
|
+
owner.addProperty(ownerProp);
|
|
304
340
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const newProp = {
|
|
316
|
-
type: meta.className,
|
|
317
|
-
joinColumns: prop.fieldNames,
|
|
318
|
-
referencedTableName: meta.tableName,
|
|
319
|
-
referencedColumnNames: Utils.flatten(targetMeta.getPrimaryProps().map(pk => pk.fieldNames)),
|
|
320
|
-
mappedBy: prop.name,
|
|
321
|
-
persist: prop.persist,
|
|
322
|
-
};
|
|
323
|
-
if (prop.kind === ReferenceKind.MANY_TO_ONE) {
|
|
324
|
-
newProp.kind = ReferenceKind.ONE_TO_MANY;
|
|
325
|
-
}
|
|
326
|
-
else if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.mappedBy) {
|
|
327
|
-
newProp.kind = ReferenceKind.ONE_TO_ONE;
|
|
328
|
-
newProp.nullable = true;
|
|
329
|
-
newProp.default = null;
|
|
330
|
-
newProp.defaultRaw = 'null';
|
|
331
|
-
}
|
|
332
|
-
else if (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.mappedBy) {
|
|
333
|
-
newProp.kind = ReferenceKind.MANY_TO_MANY;
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
let i = 1;
|
|
339
|
-
const name = (newProp.name = this.#namingStrategy.inverseSideName(meta.className, prop.name, newProp.kind));
|
|
340
|
-
while (targetMeta.properties[newProp.name]) {
|
|
341
|
-
newProp.name = name + i++;
|
|
342
|
-
}
|
|
343
|
-
targetMeta.addProperty(newProp);
|
|
344
|
-
}
|
|
341
|
+
}
|
|
342
|
+
generateBidirectionalRelations(metadata, includeUnreferencedPurePivotTables) {
|
|
343
|
+
const filteredMetadata = includeUnreferencedPurePivotTables
|
|
344
|
+
? metadata
|
|
345
|
+
: metadata.filter(m => !m.pivotTable || this.#referencedEntities.has(m));
|
|
346
|
+
for (const meta of filteredMetadata) {
|
|
347
|
+
for (const prop of meta.relations) {
|
|
348
|
+
const targetMeta = metadata.find(m => m.className === prop.type);
|
|
349
|
+
if (!targetMeta) {
|
|
350
|
+
continue;
|
|
345
351
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
const newProp = {
|
|
353
|
+
type: meta.className,
|
|
354
|
+
joinColumns: prop.fieldNames,
|
|
355
|
+
referencedTableName: meta.tableName,
|
|
356
|
+
referencedColumnNames: Utils.flatten(targetMeta.getPrimaryProps().map(pk => pk.fieldNames)),
|
|
357
|
+
mappedBy: prop.name,
|
|
358
|
+
persist: prop.persist,
|
|
359
|
+
};
|
|
360
|
+
if (prop.kind === ReferenceKind.MANY_TO_ONE) {
|
|
361
|
+
newProp.kind = ReferenceKind.ONE_TO_MANY;
|
|
362
|
+
} else if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.mappedBy) {
|
|
363
|
+
newProp.kind = ReferenceKind.ONE_TO_ONE;
|
|
364
|
+
newProp.nullable = true;
|
|
365
|
+
newProp.default = null;
|
|
366
|
+
newProp.defaultRaw = 'null';
|
|
367
|
+
} else if (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.mappedBy) {
|
|
368
|
+
newProp.kind = ReferenceKind.MANY_TO_MANY;
|
|
369
|
+
} else {
|
|
370
|
+
continue;
|
|
354
371
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const schema = new EntitySchema({ className: customBaseEntityName, abstract: true });
|
|
360
|
-
base = schema.init().meta;
|
|
361
|
-
metadata.push(base);
|
|
372
|
+
let i = 1;
|
|
373
|
+
const name = (newProp.name = this.#namingStrategy.inverseSideName(meta.className, prop.name, newProp.kind));
|
|
374
|
+
while (targetMeta.properties[newProp.name]) {
|
|
375
|
+
newProp.name = name + i++;
|
|
362
376
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
377
|
+
targetMeta.addProperty(newProp);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
generateIdentifiedReferences(metadata) {
|
|
382
|
+
for (const meta of metadata.filter(m => !m.pivotTable || this.#referencedEntities.has(m))) {
|
|
383
|
+
for (const prop of Object.values(meta.properties)) {
|
|
384
|
+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || prop.lazy) {
|
|
385
|
+
prop.ref = true;
|
|
367
386
|
}
|
|
387
|
+
}
|
|
368
388
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
389
|
+
}
|
|
390
|
+
generateAndAttachCustomBaseEntity(metadata, customBaseEntityName) {
|
|
391
|
+
let base = metadata.find(meta => meta.className === customBaseEntityName);
|
|
392
|
+
if (!base) {
|
|
393
|
+
const schema = new EntitySchema({ className: customBaseEntityName, abstract: true });
|
|
394
|
+
base = schema.init().meta;
|
|
395
|
+
metadata.push(base);
|
|
396
|
+
}
|
|
397
|
+
for (const meta of metadata) {
|
|
398
|
+
if (meta.className !== customBaseEntityName) {
|
|
399
|
+
meta.extends ??= base.class;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
castNullDefaultsToUndefined(metadata) {
|
|
404
|
+
for (const meta of metadata) {
|
|
405
|
+
for (const prop of Object.values(meta.properties)) {
|
|
406
|
+
if (prop.nullable && !prop.optional && prop.default === null && typeof prop.defaultRaw === 'undefined') {
|
|
407
|
+
prop.default = undefined;
|
|
408
|
+
prop.optional = true;
|
|
377
409
|
}
|
|
410
|
+
}
|
|
378
411
|
}
|
|
412
|
+
}
|
|
379
413
|
}
|