metal-orm 1.0.47 → 1.0.49
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/package.json +1 -1
- package/scripts/generate-entities/cli.mjs +97 -0
- package/scripts/generate-entities/drivers.mjs +183 -0
- package/scripts/generate-entities/emit.mjs +24 -0
- package/scripts/generate-entities/generate.mjs +68 -0
- package/scripts/generate-entities/render.mjs +442 -0
- package/scripts/generate-entities/schema.mjs +178 -0
- package/scripts/generate-entities.mjs +19 -738
- package/scripts/naming-strategy.mjs +152 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createNamingStrategy } from '../naming-strategy.mjs';
|
|
3
|
+
import { buildSchemaMetadata } from './schema.mjs';
|
|
4
|
+
|
|
5
|
+
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
6
|
+
|
|
7
|
+
const parseColumnType = colTypeRaw => {
|
|
8
|
+
const type = (colTypeRaw || '').toLowerCase();
|
|
9
|
+
const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
|
|
10
|
+
const length = lengthMatch ? Number(lengthMatch[1]) : undefined;
|
|
11
|
+
const scale = lengthMatch && lengthMatch[2] ? Number(lengthMatch[2]) : undefined;
|
|
12
|
+
|
|
13
|
+
const base = type.replace(/\(.*\)/, '');
|
|
14
|
+
|
|
15
|
+
if (base === 'bit') return { factory: 'col.boolean()', ts: 'boolean' };
|
|
16
|
+
if (base.includes('bigint')) return { factory: 'col.bigint()', ts: 'number' };
|
|
17
|
+
if (base.includes('int')) return { factory: 'col.int()', ts: 'number' };
|
|
18
|
+
if (base.includes('uuid') || base.includes('uniqueidentifier')) return { factory: 'col.uuid()', ts: 'string' };
|
|
19
|
+
if (base === 'date') return { factory: 'col.date<Date>()', ts: 'Date' };
|
|
20
|
+
if (base.includes('datetime') || base === 'time') return { factory: 'col.datetime<Date>()', ts: 'Date' };
|
|
21
|
+
if (base.includes('char') || base.includes('text')) {
|
|
22
|
+
const lenArg = length ? `${length}` : '255';
|
|
23
|
+
return { factory: `col.varchar(${lenArg})`, ts: 'string' };
|
|
24
|
+
}
|
|
25
|
+
if (base.includes('json')) return { factory: 'col.json()', ts: 'any' };
|
|
26
|
+
if (base.includes('bool') || (base.includes('tinyint') && length === 1)) {
|
|
27
|
+
return { factory: 'col.boolean()', ts: 'boolean' };
|
|
28
|
+
}
|
|
29
|
+
if (base.includes('date') || base.includes('time')) return { factory: 'col.datetime<Date>()', ts: 'Date' };
|
|
30
|
+
if (base.includes('decimal') || base.includes('numeric')) {
|
|
31
|
+
const precision = length ?? 10;
|
|
32
|
+
const scaleVal = scale ?? 0;
|
|
33
|
+
return { factory: `col.decimal(${precision}, ${scaleVal})`, ts: 'number' };
|
|
34
|
+
}
|
|
35
|
+
if (base.includes('double')) return { factory: 'col.float()', ts: 'number' };
|
|
36
|
+
if (base.includes('float') || base.includes('real')) return { factory: 'col.float()', ts: 'number' };
|
|
37
|
+
if (base.includes('blob') || base.includes('binary') || base.includes('bytea')) {
|
|
38
|
+
return { factory: 'col.blob()', ts: 'Buffer' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { factory: `col.varchar(255) /* TODO: review type ${colTypeRaw} */`, ts: 'any' };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeDefault = value => {
|
|
45
|
+
if (value === undefined) return undefined;
|
|
46
|
+
if (value === null) return { kind: 'value', code: 'null' };
|
|
47
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
48
|
+
return { kind: 'value', code: JSON.stringify(value) };
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (/^[-]?\d+(\.\d+)?$/.test(trimmed)) return { kind: 'value', code: trimmed };
|
|
53
|
+
if (/^(true|false)$/i.test(trimmed)) return { kind: 'value', code: trimmed.toLowerCase() };
|
|
54
|
+
if (/^null$/i.test(trimmed)) return { kind: 'value', code: 'null' };
|
|
55
|
+
if (/current_|now\(\)|uuid_generate_v4|uuid\(\)/i.test(trimmed) || trimmed.includes('(')) {
|
|
56
|
+
return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
|
|
57
|
+
}
|
|
58
|
+
if (/^'.*'$/.test(trimmed) || /^".*"$/.test(trimmed)) {
|
|
59
|
+
const unquoted = trimmed.slice(1, -1);
|
|
60
|
+
return { kind: 'value', code: `'${escapeJsString(unquoted)}'` };
|
|
61
|
+
}
|
|
62
|
+
return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'value', code: JSON.stringify(value) };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const renderColumnExpression = (column, tablePk) => {
|
|
68
|
+
const base = parseColumnType(column.type);
|
|
69
|
+
let expr = base.factory;
|
|
70
|
+
|
|
71
|
+
if (column.autoIncrement) {
|
|
72
|
+
expr = `col.autoIncrement(${expr})`;
|
|
73
|
+
}
|
|
74
|
+
if (column.notNull) {
|
|
75
|
+
expr = `col.notNull(${expr})`;
|
|
76
|
+
}
|
|
77
|
+
if (column.unique) {
|
|
78
|
+
const name = typeof column.unique === 'string' ? `, '${escapeJsString(column.unique)}'` : '';
|
|
79
|
+
expr = `col.unique(${expr}${name})`;
|
|
80
|
+
}
|
|
81
|
+
if (column.default !== undefined) {
|
|
82
|
+
const def = normalizeDefault(column.default);
|
|
83
|
+
if (def) {
|
|
84
|
+
expr =
|
|
85
|
+
def.kind === 'raw'
|
|
86
|
+
? `col.defaultRaw(${expr}, ${def.code})`
|
|
87
|
+
: `col.default(${expr}, ${def.code})`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (column.references) {
|
|
91
|
+
const refParts = [
|
|
92
|
+
`table: '${escapeJsString(column.references.table)}'`,
|
|
93
|
+
`column: '${escapeJsString(column.references.column)}'`
|
|
94
|
+
];
|
|
95
|
+
if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
|
|
96
|
+
if (column.references.onUpdate) refParts.push(`onUpdate: '${escapeJsString(column.references.onUpdate)}'`);
|
|
97
|
+
expr = `col.references(${expr}, { ${refParts.join(', ')} })`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isPrimary = Array.isArray(tablePk) && tablePk.includes(column.name);
|
|
101
|
+
const decorator = isPrimary ? 'PrimaryKey' : 'Column';
|
|
102
|
+
const tsType = base.ts || 'any';
|
|
103
|
+
const optional = !column.notNull;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
decorator,
|
|
107
|
+
expr,
|
|
108
|
+
tsType,
|
|
109
|
+
optional
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const METAL_ORM_IMPORT_ORDER = [
|
|
114
|
+
'col',
|
|
115
|
+
'Entity',
|
|
116
|
+
'Column',
|
|
117
|
+
'PrimaryKey',
|
|
118
|
+
'HasMany',
|
|
119
|
+
'HasOne',
|
|
120
|
+
'BelongsTo',
|
|
121
|
+
'BelongsToMany',
|
|
122
|
+
'HasManyCollection',
|
|
123
|
+
'HasOneReference',
|
|
124
|
+
'ManyToManyCollection',
|
|
125
|
+
'bootstrapEntities',
|
|
126
|
+
'getTableDefFromEntity'
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName }) => {
|
|
130
|
+
const lines = [];
|
|
131
|
+
const derivedDefault = naming.defaultTableNameFromClass(className);
|
|
132
|
+
const needsTableNameOption = table.name !== derivedDefault;
|
|
133
|
+
const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
|
|
134
|
+
lines.push(`@Entity(${entityOpts})`);
|
|
135
|
+
lines.push(`export class ${className} {`);
|
|
136
|
+
|
|
137
|
+
for (const col of table.columns) {
|
|
138
|
+
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
139
|
+
lines.push(` @${rendered.decorator}(${rendered.expr})`);
|
|
140
|
+
lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const rel of relations) {
|
|
145
|
+
const targetClass = resolveClassName(rel.target);
|
|
146
|
+
if (!targetClass) continue;
|
|
147
|
+
switch (rel.kind) {
|
|
148
|
+
case 'belongsTo':
|
|
149
|
+
lines.push(
|
|
150
|
+
` @BelongsTo({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
|
|
151
|
+
);
|
|
152
|
+
lines.push(` ${rel.property}?: ${targetClass};`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
break;
|
|
155
|
+
case 'hasMany':
|
|
156
|
+
lines.push(
|
|
157
|
+
` @HasMany({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
|
|
158
|
+
);
|
|
159
|
+
lines.push(` ${rel.property}!: HasManyCollection<${targetClass}>;`);
|
|
160
|
+
lines.push('');
|
|
161
|
+
break;
|
|
162
|
+
case 'hasOne':
|
|
163
|
+
lines.push(
|
|
164
|
+
` @HasOne({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
|
|
165
|
+
);
|
|
166
|
+
lines.push(` ${rel.property}!: HasOneReference<${targetClass}>;`);
|
|
167
|
+
lines.push('');
|
|
168
|
+
break;
|
|
169
|
+
case 'belongsToMany': {
|
|
170
|
+
const pivotClass = resolveClassName(rel.pivotTable);
|
|
171
|
+
if (!pivotClass) break;
|
|
172
|
+
lines.push(
|
|
173
|
+
` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${pivotClass}, pivotForeignKeyToRoot: '${escapeJsString(
|
|
174
|
+
rel.pivotForeignKeyToRoot
|
|
175
|
+
)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
|
|
176
|
+
);
|
|
177
|
+
lines.push(` ${rel.property}!: ManyToManyCollection<${targetClass}>;`);
|
|
178
|
+
lines.push('');
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push('}');
|
|
187
|
+
lines.push('');
|
|
188
|
+
return lines;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const computeTableUsage = (table, relations) => {
|
|
192
|
+
const usage = {
|
|
193
|
+
needsCol: false,
|
|
194
|
+
needsEntity: true,
|
|
195
|
+
needsColumnDecorator: false,
|
|
196
|
+
needsPrimaryKeyDecorator: false,
|
|
197
|
+
needsHasManyDecorator: false,
|
|
198
|
+
needsHasOneDecorator: false,
|
|
199
|
+
needsBelongsToDecorator: false,
|
|
200
|
+
needsBelongsToManyDecorator: false,
|
|
201
|
+
needsHasManyCollection: false,
|
|
202
|
+
needsHasOneReference: false,
|
|
203
|
+
needsManyToManyCollection: false
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
for (const col of table.columns) {
|
|
207
|
+
usage.needsCol = true;
|
|
208
|
+
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
209
|
+
if (rendered.decorator === 'PrimaryKey') {
|
|
210
|
+
usage.needsPrimaryKeyDecorator = true;
|
|
211
|
+
} else {
|
|
212
|
+
usage.needsColumnDecorator = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const rel of relations) {
|
|
217
|
+
if (rel.kind === 'hasMany') {
|
|
218
|
+
usage.needsHasManyDecorator = true;
|
|
219
|
+
usage.needsHasManyCollection = true;
|
|
220
|
+
}
|
|
221
|
+
if (rel.kind === 'hasOne') {
|
|
222
|
+
usage.needsHasOneDecorator = true;
|
|
223
|
+
usage.needsHasOneReference = true;
|
|
224
|
+
}
|
|
225
|
+
if (rel.kind === 'belongsTo') {
|
|
226
|
+
usage.needsBelongsToDecorator = true;
|
|
227
|
+
}
|
|
228
|
+
if (rel.kind === 'belongsToMany') {
|
|
229
|
+
usage.needsBelongsToManyDecorator = true;
|
|
230
|
+
usage.needsManyToManyCollection = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return usage;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const getMetalOrmImportNamesFromUsage = usage => {
|
|
238
|
+
const names = new Set();
|
|
239
|
+
if (usage.needsCol) names.add('col');
|
|
240
|
+
if (usage.needsEntity) names.add('Entity');
|
|
241
|
+
if (usage.needsColumnDecorator) names.add('Column');
|
|
242
|
+
if (usage.needsPrimaryKeyDecorator) names.add('PrimaryKey');
|
|
243
|
+
if (usage.needsHasManyDecorator) names.add('HasMany');
|
|
244
|
+
if (usage.needsHasOneDecorator) names.add('HasOne');
|
|
245
|
+
if (usage.needsBelongsToDecorator) names.add('BelongsTo');
|
|
246
|
+
if (usage.needsBelongsToManyDecorator) names.add('BelongsToMany');
|
|
247
|
+
if (usage.needsHasManyCollection) names.add('HasManyCollection');
|
|
248
|
+
if (usage.needsHasOneReference) names.add('HasOneReference');
|
|
249
|
+
if (usage.needsManyToManyCollection) names.add('ManyToManyCollection');
|
|
250
|
+
return names;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const buildMetalOrmImportStatement = names => {
|
|
254
|
+
if (!names || names.size === 0) return '';
|
|
255
|
+
const ordered = METAL_ORM_IMPORT_ORDER.filter(name => names.has(name));
|
|
256
|
+
if (!ordered.length) return '';
|
|
257
|
+
return `import { ${ordered.join(', ')} } from 'metal-orm';`;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const getRelativeModuleSpecifier = (from, to) => {
|
|
261
|
+
const rel = path.relative(path.dirname(from), to).replace(/\\/g, '/');
|
|
262
|
+
if (!rel) return './';
|
|
263
|
+
const withoutExt = rel.replace(/\.ts$/i, '');
|
|
264
|
+
return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const renderEntityFile = (schema, options) => {
|
|
268
|
+
const naming = options.naming || createNamingStrategy('en');
|
|
269
|
+
const metadata = buildSchemaMetadata(schema, naming);
|
|
270
|
+
const { tables, relations } = metadata;
|
|
271
|
+
|
|
272
|
+
const aggregateUsage = {
|
|
273
|
+
needsCol: false,
|
|
274
|
+
needsEntity: tables.length > 0,
|
|
275
|
+
needsColumnDecorator: false,
|
|
276
|
+
needsPrimaryKeyDecorator: false,
|
|
277
|
+
needsHasManyDecorator: false,
|
|
278
|
+
needsHasOneDecorator: false,
|
|
279
|
+
needsBelongsToDecorator: false,
|
|
280
|
+
needsBelongsToManyDecorator: false,
|
|
281
|
+
needsHasManyCollection: false,
|
|
282
|
+
needsHasOneReference: false,
|
|
283
|
+
needsManyToManyCollection: false
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (const table of tables) {
|
|
287
|
+
const rels = relations.get(table.name) || [];
|
|
288
|
+
const tableUsage = computeTableUsage(table, rels);
|
|
289
|
+
aggregateUsage.needsCol ||= tableUsage.needsCol;
|
|
290
|
+
aggregateUsage.needsColumnDecorator ||= tableUsage.needsColumnDecorator;
|
|
291
|
+
aggregateUsage.needsPrimaryKeyDecorator ||= tableUsage.needsPrimaryKeyDecorator;
|
|
292
|
+
aggregateUsage.needsHasManyDecorator ||= tableUsage.needsHasManyDecorator;
|
|
293
|
+
aggregateUsage.needsHasOneDecorator ||= tableUsage.needsHasOneDecorator;
|
|
294
|
+
aggregateUsage.needsBelongsToDecorator ||= tableUsage.needsBelongsToDecorator;
|
|
295
|
+
aggregateUsage.needsBelongsToManyDecorator ||= tableUsage.needsBelongsToManyDecorator;
|
|
296
|
+
aggregateUsage.needsHasManyCollection ||= tableUsage.needsHasManyCollection;
|
|
297
|
+
aggregateUsage.needsHasOneReference ||= tableUsage.needsHasOneReference;
|
|
298
|
+
aggregateUsage.needsManyToManyCollection ||= tableUsage.needsManyToManyCollection;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const importNames = getMetalOrmImportNamesFromUsage(aggregateUsage);
|
|
302
|
+
importNames.add('bootstrapEntities');
|
|
303
|
+
importNames.add('getTableDefFromEntity');
|
|
304
|
+
const importStatement = buildMetalOrmImportStatement(importNames);
|
|
305
|
+
|
|
306
|
+
const lines = [
|
|
307
|
+
'// AUTO-GENERATED by scripts/generate-entities.mjs',
|
|
308
|
+
'// Regenerate after schema changes.'
|
|
309
|
+
];
|
|
310
|
+
if (importStatement) {
|
|
311
|
+
lines.push(importStatement, '');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const table of tables) {
|
|
315
|
+
const className = metadata.classNames.get(table.name);
|
|
316
|
+
const classLines = renderEntityClassLines({
|
|
317
|
+
table,
|
|
318
|
+
className,
|
|
319
|
+
naming,
|
|
320
|
+
relations: relations.get(table.name) || [],
|
|
321
|
+
resolveClassName: metadata.resolveClassName
|
|
322
|
+
});
|
|
323
|
+
lines.push(...classLines);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lines.push(
|
|
327
|
+
'export const bootstrapEntityTables = () => {',
|
|
328
|
+
' const tables = bootstrapEntities();',
|
|
329
|
+
' return {',
|
|
330
|
+
...tables.map(t => ` ${metadata.classNames.get(t.name)}: getTableDefFromEntity(${metadata.classNames.get(t.name)})!,`),
|
|
331
|
+
' };',
|
|
332
|
+
'};',
|
|
333
|
+
'',
|
|
334
|
+
'export const allTables = () => bootstrapEntities();'
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return lines.join('\n');
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const renderSplitEntityFiles = (schema, options) => {
|
|
341
|
+
const naming = options.naming || createNamingStrategy('en');
|
|
342
|
+
const metadata = buildSchemaMetadata(schema, naming);
|
|
343
|
+
const tableFiles = [];
|
|
344
|
+
|
|
345
|
+
for (const table of metadata.tables) {
|
|
346
|
+
const className = metadata.classNames.get(table.name);
|
|
347
|
+
const relations = metadata.relations.get(table.name) || [];
|
|
348
|
+
const usage = computeTableUsage(table, relations);
|
|
349
|
+
const metalImportNames = getMetalOrmImportNamesFromUsage(usage);
|
|
350
|
+
const metalImportStatement = buildMetalOrmImportStatement(metalImportNames);
|
|
351
|
+
|
|
352
|
+
const relationImports = new Set();
|
|
353
|
+
for (const rel of relations) {
|
|
354
|
+
const targetClass = metadata.resolveClassName(rel.target);
|
|
355
|
+
if (targetClass && targetClass !== className) {
|
|
356
|
+
relationImports.add(targetClass);
|
|
357
|
+
}
|
|
358
|
+
if (rel.kind === 'belongsToMany') {
|
|
359
|
+
const pivotClass = metadata.resolveClassName(rel.pivotTable);
|
|
360
|
+
if (pivotClass && pivotClass !== className) {
|
|
361
|
+
relationImports.add(pivotClass);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const importLines = [];
|
|
367
|
+
if (metalImportStatement) {
|
|
368
|
+
importLines.push(metalImportStatement);
|
|
369
|
+
}
|
|
370
|
+
for (const targetClass of Array.from(relationImports).sort()) {
|
|
371
|
+
importLines.push(`import { ${targetClass} } from './${targetClass}';`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const lines = [
|
|
375
|
+
'// AUTO-GENERATED by scripts/generate-entities.mjs',
|
|
376
|
+
'// Regenerate after schema changes.'
|
|
377
|
+
];
|
|
378
|
+
if (importLines.length) {
|
|
379
|
+
lines.push(...importLines, '');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const classLines = renderEntityClassLines({
|
|
383
|
+
table,
|
|
384
|
+
className,
|
|
385
|
+
naming,
|
|
386
|
+
relations,
|
|
387
|
+
resolveClassName: metadata.resolveClassName
|
|
388
|
+
});
|
|
389
|
+
lines.push(...classLines);
|
|
390
|
+
|
|
391
|
+
tableFiles.push({
|
|
392
|
+
path: path.join(options.outDir, `${className}.ts`),
|
|
393
|
+
code: lines.join('\n')
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { tableFiles, metadata };
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const renderSplitIndexFile = (metadata, options) => {
|
|
401
|
+
const importLines = ["import { bootstrapEntities, getTableDefFromEntity } from 'metal-orm';"];
|
|
402
|
+
|
|
403
|
+
const exportedClasses = [];
|
|
404
|
+
for (const table of metadata.tables) {
|
|
405
|
+
const className = metadata.classNames.get(table.name);
|
|
406
|
+
const filePath = path.join(options.outDir, `${className}.ts`);
|
|
407
|
+
const moduleSpecifier = getRelativeModuleSpecifier(options.out, filePath);
|
|
408
|
+
importLines.push(`import { ${className} } from '${moduleSpecifier}';`);
|
|
409
|
+
exportedClasses.push(className);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const lines = [
|
|
413
|
+
'// AUTO-GENERATED by scripts/generate-entities.mjs',
|
|
414
|
+
'// Regenerate after schema changes.',
|
|
415
|
+
...importLines,
|
|
416
|
+
''
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
if (exportedClasses.length) {
|
|
420
|
+
lines.push('export {');
|
|
421
|
+
for (const className of exportedClasses) {
|
|
422
|
+
lines.push(` ${className},`);
|
|
423
|
+
}
|
|
424
|
+
lines.push('};', '');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
lines.push(
|
|
428
|
+
'export const bootstrapEntityTables = () => {',
|
|
429
|
+
' const tables = bootstrapEntities();',
|
|
430
|
+
' return {',
|
|
431
|
+
...metadata.tables.map(
|
|
432
|
+
t =>
|
|
433
|
+
` ${metadata.classNames.get(t.name)}: getTableDefFromEntity(${metadata.classNames.get(t.name)})!,`
|
|
434
|
+
),
|
|
435
|
+
' };',
|
|
436
|
+
'};',
|
|
437
|
+
'',
|
|
438
|
+
'export const allTables = () => bootstrapEntities();'
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
return lines.join('\n');
|
|
442
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
2
|
+
|
|
3
|
+
export const mapRelations = (tables, naming) => {
|
|
4
|
+
const relationMap = new Map();
|
|
5
|
+
const relationKeys = new Map();
|
|
6
|
+
const fkIndex = new Map();
|
|
7
|
+
const uniqueSingleColumns = new Map();
|
|
8
|
+
|
|
9
|
+
for (const table of tables) {
|
|
10
|
+
relationMap.set(table.name, []);
|
|
11
|
+
relationKeys.set(table.name, new Set());
|
|
12
|
+
for (const col of table.columns) {
|
|
13
|
+
if (col.references) {
|
|
14
|
+
const list = fkIndex.get(table.name) || [];
|
|
15
|
+
list.push(col);
|
|
16
|
+
fkIndex.set(table.name, list);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const uniqueCols = new Set();
|
|
21
|
+
if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
|
|
22
|
+
uniqueCols.add(table.primaryKey[0]);
|
|
23
|
+
}
|
|
24
|
+
for (const col of table.columns) {
|
|
25
|
+
if (col.unique) uniqueCols.add(col.name);
|
|
26
|
+
}
|
|
27
|
+
for (const idx of table.indexes || []) {
|
|
28
|
+
if (!idx?.unique) continue;
|
|
29
|
+
if (!Array.isArray(idx.columns) || idx.columns.length !== 1 || !idx.columns[0]?.column) continue;
|
|
30
|
+
const columnName = idx.columns[0].column;
|
|
31
|
+
if (idx.where) {
|
|
32
|
+
const predicate = String(idx.where);
|
|
33
|
+
const isNotNullOnly = new RegExp(`\\b${columnName}\\b\\s+is\\s+not\\s+null\\b`, 'i').test(predicate);
|
|
34
|
+
if (!isNotNullOnly) continue;
|
|
35
|
+
}
|
|
36
|
+
uniqueCols.add(columnName);
|
|
37
|
+
}
|
|
38
|
+
uniqueSingleColumns.set(table.name, uniqueCols);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const findTable = name => {
|
|
42
|
+
const norm = normalizeName(name);
|
|
43
|
+
return tables.find(t => t.name === name || t.name === norm);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const pivotTables = new Set();
|
|
47
|
+
for (const table of tables) {
|
|
48
|
+
const fkCols = fkIndex.get(table.name) || [];
|
|
49
|
+
const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
|
|
50
|
+
if (fkCols.length === 2 && distinctTargets.length === 2) {
|
|
51
|
+
const [a, b] = fkCols;
|
|
52
|
+
pivotTables.add(table.name);
|
|
53
|
+
const targetA = findTable(a.references.table);
|
|
54
|
+
const targetB = findTable(b.references.table);
|
|
55
|
+
if (targetA && targetB) {
|
|
56
|
+
const aKey = relationKeys.get(targetA.name);
|
|
57
|
+
const bKey = relationKeys.get(targetB.name);
|
|
58
|
+
const aProp = naming.belongsToManyProperty(targetB.name);
|
|
59
|
+
const bProp = naming.belongsToManyProperty(targetA.name);
|
|
60
|
+
if (!aKey.has(aProp)) {
|
|
61
|
+
aKey.add(aProp);
|
|
62
|
+
relationMap.get(targetA.name)?.push({
|
|
63
|
+
kind: 'belongsToMany',
|
|
64
|
+
property: aProp,
|
|
65
|
+
target: targetB.name,
|
|
66
|
+
pivotTable: table.name,
|
|
67
|
+
pivotForeignKeyToRoot: a.name,
|
|
68
|
+
pivotForeignKeyToTarget: b.name
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!bKey.has(bProp)) {
|
|
72
|
+
bKey.add(bProp);
|
|
73
|
+
relationMap.get(targetB.name)?.push({
|
|
74
|
+
kind: 'belongsToMany',
|
|
75
|
+
property: bProp,
|
|
76
|
+
target: targetA.name,
|
|
77
|
+
pivotTable: table.name,
|
|
78
|
+
pivotForeignKeyToRoot: b.name,
|
|
79
|
+
pivotForeignKeyToTarget: a.name
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const table of tables) {
|
|
87
|
+
const fkCols = fkIndex.get(table.name) || [];
|
|
88
|
+
for (const fk of fkCols) {
|
|
89
|
+
const targetTable = fk.references.table;
|
|
90
|
+
const targetKey = normalizeName(targetTable);
|
|
91
|
+
const belongsKey = relationKeys.get(table.name);
|
|
92
|
+
const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
|
|
93
|
+
|
|
94
|
+
if (!belongsKey || !hasManyKey) continue;
|
|
95
|
+
|
|
96
|
+
const belongsProp = naming.belongsToProperty(fk.name, targetTable);
|
|
97
|
+
if (!belongsKey.has(belongsProp)) {
|
|
98
|
+
belongsKey.add(belongsProp);
|
|
99
|
+
relationMap.get(table.name)?.push({
|
|
100
|
+
kind: 'belongsTo',
|
|
101
|
+
property: belongsProp,
|
|
102
|
+
target: targetTable,
|
|
103
|
+
foreignKey: fk.name
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const uniqueCols = uniqueSingleColumns.get(table.name);
|
|
108
|
+
const isHasOne = Boolean(uniqueCols?.has(fk.name));
|
|
109
|
+
const relationKind = isHasOne ? 'hasOne' : 'hasMany';
|
|
110
|
+
const inverseProp = isHasOne ? naming.hasOneProperty(table.name) : naming.hasManyProperty(table.name);
|
|
111
|
+
if (!hasManyKey.has(inverseProp)) {
|
|
112
|
+
hasManyKey.add(inverseProp);
|
|
113
|
+
relationMap.get(targetKey)?.push({
|
|
114
|
+
kind: relationKind,
|
|
115
|
+
property: inverseProp,
|
|
116
|
+
target: table.name,
|
|
117
|
+
foreignKey: fk.name
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return relationMap;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const buildSchemaMetadata = (schema, naming) => {
|
|
127
|
+
const tables = schema.tables.map(t => {
|
|
128
|
+
const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
|
|
129
|
+
const uniqueSingleColumns = new Set(
|
|
130
|
+
indexes
|
|
131
|
+
.filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
|
|
132
|
+
.map(idx => idx.columns[0]?.column)
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
name: t.name,
|
|
138
|
+
schema: t.schema,
|
|
139
|
+
columns: (t.columns || []).map(col => {
|
|
140
|
+
const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
|
|
141
|
+
return { ...col, unique };
|
|
142
|
+
}),
|
|
143
|
+
primaryKey: t.primaryKey || [],
|
|
144
|
+
indexes
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const classNames = new Map();
|
|
149
|
+
tables.forEach(t => {
|
|
150
|
+
const className = naming.classNameFromTable(t.name);
|
|
151
|
+
classNames.set(t.name, className);
|
|
152
|
+
if (t.schema) {
|
|
153
|
+
const qualified = `${t.schema}.${t.name}`;
|
|
154
|
+
if (!classNames.has(qualified)) {
|
|
155
|
+
classNames.set(qualified, className);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const resolveClassName = target => {
|
|
161
|
+
if (!target) return undefined;
|
|
162
|
+
if (classNames.has(target)) return classNames.get(target);
|
|
163
|
+
const fallback = target.split('.').pop();
|
|
164
|
+
if (fallback && classNames.has(fallback)) {
|
|
165
|
+
return classNames.get(fallback);
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const relations = mapRelations(tables, naming);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
tables,
|
|
174
|
+
classNames,
|
|
175
|
+
relations,
|
|
176
|
+
resolveClassName
|
|
177
|
+
};
|
|
178
|
+
};
|