metal-orm 1.0.48 → 1.0.50
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 +198 -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 +452 -0
- package/scripts/generate-entities/schema.mjs +178 -0
- package/scripts/generate-entities.mjs +19 -720
- package/scripts/naming-strategy.mjs +6 -2
|
@@ -10,739 +10,38 @@
|
|
|
10
10
|
* Dialects supported: postgres, mysql, sqlite, mssql.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import fs from 'node:fs/promises';
|
|
14
13
|
import { readFileSync } from 'node:fs';
|
|
15
14
|
import path from 'node:path';
|
|
16
15
|
import process from 'node:process';
|
|
17
|
-
import {
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
18
17
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
createPostgresExecutor,
|
|
22
|
-
createMysqlExecutor,
|
|
23
|
-
createSqliteExecutor,
|
|
24
|
-
createMssqlExecutor
|
|
25
|
-
} from '../dist/index.js';
|
|
26
|
-
import { createNamingStrategy } from './naming-strategy.mjs';
|
|
18
|
+
import { parseOptions, printUsage } from './generate-entities/cli.mjs';
|
|
19
|
+
import { generateEntities } from './generate-entities/generate.mjs';
|
|
27
20
|
|
|
28
21
|
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
29
22
|
|
|
30
|
-
const
|
|
23
|
+
const isEntrypoint =
|
|
24
|
+
typeof process.argv?.[1] === 'string' && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
31
25
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
positionals
|
|
36
|
-
} = parseCliArgs({
|
|
37
|
-
options: {
|
|
38
|
-
dialect: { type: 'string' },
|
|
39
|
-
url: { type: 'string' },
|
|
40
|
-
db: { type: 'string' },
|
|
41
|
-
schema: { type: 'string' },
|
|
42
|
-
include: { type: 'string' },
|
|
43
|
-
exclude: { type: 'string' },
|
|
44
|
-
out: { type: 'string' },
|
|
45
|
-
locale: { type: 'string' },
|
|
46
|
-
'naming-overrides': { type: 'string' },
|
|
47
|
-
'dry-run': { type: 'boolean' },
|
|
48
|
-
help: { type: 'boolean', short: 'h' },
|
|
49
|
-
version: { type: 'boolean' }
|
|
50
|
-
},
|
|
51
|
-
strict: true
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (values.help) {
|
|
26
|
+
const main = async () => {
|
|
27
|
+
const result = parseOptions(process.argv.slice(2), process.env, process.cwd());
|
|
28
|
+
if (result.kind === 'help') {
|
|
55
29
|
printUsage();
|
|
56
|
-
|
|
30
|
+
return;
|
|
57
31
|
}
|
|
58
|
-
|
|
59
|
-
if (values.version) {
|
|
32
|
+
if (result.kind === 'version') {
|
|
60
33
|
console.log(`metal-orm ${pkgVersion}`);
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (positionals.length) {
|
|
65
|
-
throw new Error(`Unexpected positional args: ${positionals.join(' ')}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const opts = {
|
|
69
|
-
dialect: (values.dialect || 'postgres').toLowerCase(),
|
|
70
|
-
url: values.url || process.env.DATABASE_URL,
|
|
71
|
-
dbPath: values.db,
|
|
72
|
-
schema: values.schema,
|
|
73
|
-
include: values.include ? values.include.split(',').map(v => v.trim()).filter(Boolean) : undefined,
|
|
74
|
-
exclude: values.exclude ? values.exclude.split(',').map(v => v.trim()).filter(Boolean) : undefined,
|
|
75
|
-
out: values.out ? path.resolve(process.cwd(), values.out) : path.join(process.cwd(), 'generated-entities.ts'),
|
|
76
|
-
locale: (values.locale || 'en').toLowerCase(),
|
|
77
|
-
namingOverrides: values['naming-overrides']
|
|
78
|
-
? path.resolve(process.cwd(), values['naming-overrides'])
|
|
79
|
-
: undefined,
|
|
80
|
-
dryRun: Boolean(values['dry-run'])
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (!DIALECTS.has(opts.dialect)) {
|
|
84
|
-
throw new Error(`Unsupported dialect "${opts.dialect}". Supported: ${Array.from(DIALECTS).join(', ')}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (opts.dialect === 'sqlite' && !opts.dbPath) {
|
|
88
|
-
opts.dbPath = ':memory:';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (opts.dialect !== 'sqlite' && !opts.url) {
|
|
92
|
-
throw new Error('Missing connection string. Provide --url or set DATABASE_URL.');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return opts;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const printUsage = () => {
|
|
99
|
-
console.log(
|
|
100
|
-
`
|
|
101
|
-
MetalORM decorator generator
|
|
102
|
-
---------------------------
|
|
103
|
-
Usage:
|
|
104
|
-
node scripts/generate-entities.mjs --dialect=postgres --url=<connection> --schema=public --include=users,orders [--out=src/entities.ts]
|
|
105
|
-
node scripts/generate-entities.mjs --dialect=mysql --url=<connection> --schema=mydb --exclude=archived [--out=src/entities.ts]
|
|
106
|
-
node scripts/generate-entities.mjs --dialect=sqlite --db=./my.db [--out=src/entities.ts]
|
|
107
|
-
node scripts/generate-entities.mjs --dialect=mssql --url=mssql://user:pass@host/db [--out=src/entities.ts]
|
|
108
|
-
|
|
109
|
-
Flags:
|
|
110
|
-
--include=tbl1,tbl2 Only include these tables
|
|
111
|
-
--exclude=tbl3,tbl4 Exclude these tables
|
|
112
|
-
--locale=pt-BR Naming locale for class/relation names (default: en)
|
|
113
|
-
--naming-overrides Path to JSON map of irregular plurals { "singular": "plural" }
|
|
114
|
-
--dry-run Print to stdout instead of writing a file
|
|
115
|
-
--help Show this help
|
|
116
|
-
`
|
|
117
|
-
);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
121
|
-
|
|
122
|
-
const loadIrregulars = async filePath => {
|
|
123
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
124
|
-
let parsed;
|
|
125
|
-
try {
|
|
126
|
-
parsed = JSON.parse(raw);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
throw new Error(`Failed to parse naming overrides at ${filePath}: ${err.message || err}`);
|
|
129
|
-
}
|
|
130
|
-
const irregulars =
|
|
131
|
-
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
132
|
-
? parsed.irregulars && typeof parsed.irregulars === 'object' && !Array.isArray(parsed.irregulars)
|
|
133
|
-
? parsed.irregulars
|
|
134
|
-
: parsed
|
|
135
|
-
: undefined;
|
|
136
|
-
if (!irregulars) {
|
|
137
|
-
throw new Error(`Naming overrides at ${filePath} must be an object or { "irregulars": { ... } }`);
|
|
138
|
-
}
|
|
139
|
-
return irregulars;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const parseColumnType = colTypeRaw => {
|
|
143
|
-
const type = (colTypeRaw || '').toLowerCase();
|
|
144
|
-
const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
|
|
145
|
-
const length = lengthMatch ? Number(lengthMatch[1]) : undefined;
|
|
146
|
-
const scale = lengthMatch && lengthMatch[2] ? Number(lengthMatch[2]) : undefined;
|
|
147
|
-
|
|
148
|
-
const base = type.replace(/\(.*\)/, '');
|
|
149
|
-
|
|
150
|
-
if (base === 'bit') return { factory: 'col.boolean()', ts: 'boolean' };
|
|
151
|
-
if (base.includes('bigint')) return { factory: 'col.bigint()', ts: 'number' };
|
|
152
|
-
if (base.includes('int')) return { factory: 'col.int()', ts: 'number' };
|
|
153
|
-
if (base.includes('uuid') || base.includes('uniqueidentifier')) return { factory: 'col.uuid()', ts: 'string' };
|
|
154
|
-
if (base === 'date') return { factory: 'col.date<Date>()', ts: 'Date' };
|
|
155
|
-
if (base.includes('datetime') || base === 'time') return { factory: 'col.datetime<Date>()', ts: 'Date' };
|
|
156
|
-
if (base.includes('char') || base.includes('text')) {
|
|
157
|
-
const lenArg = length ? `${length}` : '255';
|
|
158
|
-
return { factory: `col.varchar(${lenArg})`, ts: 'string' };
|
|
159
|
-
}
|
|
160
|
-
if (base.includes('json')) return { factory: 'col.json()', ts: 'any' };
|
|
161
|
-
if (base.includes('bool') || (base.includes('tinyint') && length === 1)) {
|
|
162
|
-
return { factory: 'col.boolean()', ts: 'boolean' };
|
|
163
|
-
}
|
|
164
|
-
if (base.includes('date') || base.includes('time')) return { factory: 'col.datetime<Date>()', ts: 'Date' };
|
|
165
|
-
if (base.includes('decimal') || base.includes('numeric')) {
|
|
166
|
-
const precision = length ?? 10;
|
|
167
|
-
const scaleVal = scale ?? 0;
|
|
168
|
-
return { factory: `col.decimal(${precision}, ${scaleVal})`, ts: 'number' };
|
|
169
|
-
}
|
|
170
|
-
if (base.includes('double')) return { factory: 'col.float()', ts: 'number' };
|
|
171
|
-
if (base.includes('float') || base.includes('real')) return { factory: 'col.float()', ts: 'number' };
|
|
172
|
-
if (base.includes('blob') || base.includes('binary') || base.includes('bytea')) {
|
|
173
|
-
return { factory: 'col.blob()', ts: 'Buffer' };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { factory: `col.varchar(255) /* TODO: review type ${colTypeRaw} */`, ts: 'any' };
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const normalizeDefault = value => {
|
|
180
|
-
if (value === undefined) return undefined;
|
|
181
|
-
if (value === null) return { kind: 'value', code: 'null' };
|
|
182
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
183
|
-
return { kind: 'value', code: JSON.stringify(value) };
|
|
184
|
-
}
|
|
185
|
-
if (typeof value === 'string') {
|
|
186
|
-
const trimmed = value.trim();
|
|
187
|
-
if (/^[-]?\d+(\.\d+)?$/.test(trimmed)) return { kind: 'value', code: trimmed };
|
|
188
|
-
if (/^(true|false)$/i.test(trimmed)) return { kind: 'value', code: trimmed.toLowerCase() };
|
|
189
|
-
if (/^null$/i.test(trimmed)) return { kind: 'value', code: 'null' };
|
|
190
|
-
if (/current_|now\(\)|uuid_generate_v4|uuid\(\)/i.test(trimmed) || trimmed.includes('(')) {
|
|
191
|
-
return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
|
|
192
|
-
}
|
|
193
|
-
if (/^'.*'$/.test(trimmed) || /^".*"$/.test(trimmed)) {
|
|
194
|
-
const unquoted = trimmed.slice(1, -1);
|
|
195
|
-
return { kind: 'value', code: `'${escapeJsString(unquoted)}'` };
|
|
196
|
-
}
|
|
197
|
-
return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
|
|
198
|
-
}
|
|
199
|
-
return { kind: 'value', code: JSON.stringify(value) };
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const renderColumnExpression = (column, tablePk) => {
|
|
203
|
-
const base = parseColumnType(column.type);
|
|
204
|
-
let expr = base.factory;
|
|
205
|
-
|
|
206
|
-
if (column.autoIncrement) {
|
|
207
|
-
expr = `col.autoIncrement(${expr})`;
|
|
208
|
-
}
|
|
209
|
-
if (column.notNull) {
|
|
210
|
-
expr = `col.notNull(${expr})`;
|
|
211
|
-
}
|
|
212
|
-
if (column.unique) {
|
|
213
|
-
const name = typeof column.unique === 'string' ? `, '${escapeJsString(column.unique)}'` : '';
|
|
214
|
-
expr = `col.unique(${expr}${name})`;
|
|
215
|
-
}
|
|
216
|
-
if (column.default !== undefined) {
|
|
217
|
-
const def = normalizeDefault(column.default);
|
|
218
|
-
if (def) {
|
|
219
|
-
expr =
|
|
220
|
-
def.kind === 'raw'
|
|
221
|
-
? `col.defaultRaw(${expr}, ${def.code})`
|
|
222
|
-
: `col.default(${expr}, ${def.code})`;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (column.references) {
|
|
226
|
-
const refParts = [
|
|
227
|
-
`table: '${escapeJsString(column.references.table)}'`,
|
|
228
|
-
`column: '${escapeJsString(column.references.column)}'`
|
|
229
|
-
];
|
|
230
|
-
if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
|
|
231
|
-
if (column.references.onUpdate) refParts.push(`onUpdate: '${escapeJsString(column.references.onUpdate)}'`);
|
|
232
|
-
expr = `col.references(${expr}, { ${refParts.join(', ')} })`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const isPrimary = Array.isArray(tablePk) && tablePk.includes(column.name);
|
|
236
|
-
const decorator = isPrimary ? 'PrimaryKey' : 'Column';
|
|
237
|
-
const tsType = base.ts || 'any';
|
|
238
|
-
const optional = !column.notNull;
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
decorator,
|
|
242
|
-
expr,
|
|
243
|
-
tsType,
|
|
244
|
-
optional
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const mapRelations = (tables, naming) => {
|
|
249
|
-
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
250
|
-
const relationMap = new Map();
|
|
251
|
-
const relationKeys = new Map();
|
|
252
|
-
const fkIndex = new Map();
|
|
253
|
-
|
|
254
|
-
for (const table of tables) {
|
|
255
|
-
relationMap.set(table.name, []);
|
|
256
|
-
relationKeys.set(table.name, new Set());
|
|
257
|
-
for (const col of table.columns) {
|
|
258
|
-
if (col.references) {
|
|
259
|
-
const list = fkIndex.get(table.name) || [];
|
|
260
|
-
list.push(col);
|
|
261
|
-
fkIndex.set(table.name, list);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const findTable = name => {
|
|
267
|
-
const norm = normalizeName(name);
|
|
268
|
-
return tables.find(t => t.name === name || t.name === norm);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const pivotTables = new Set();
|
|
272
|
-
for (const table of tables) {
|
|
273
|
-
const fkCols = fkIndex.get(table.name) || [];
|
|
274
|
-
const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
|
|
275
|
-
if (fkCols.length === 2 && distinctTargets.length === 2) {
|
|
276
|
-
const [a, b] = fkCols;
|
|
277
|
-
pivotTables.add(table.name);
|
|
278
|
-
const targetA = findTable(a.references.table);
|
|
279
|
-
const targetB = findTable(b.references.table);
|
|
280
|
-
if (targetA && targetB) {
|
|
281
|
-
const aKey = relationKeys.get(targetA.name);
|
|
282
|
-
const bKey = relationKeys.get(targetB.name);
|
|
283
|
-
const aProp = naming.belongsToManyProperty(targetB.name);
|
|
284
|
-
const bProp = naming.belongsToManyProperty(targetA.name);
|
|
285
|
-
if (!aKey.has(aProp)) {
|
|
286
|
-
aKey.add(aProp);
|
|
287
|
-
relationMap.get(targetA.name)?.push({
|
|
288
|
-
kind: 'belongsToMany',
|
|
289
|
-
property: aProp,
|
|
290
|
-
target: targetB.name,
|
|
291
|
-
pivotTable: table.name,
|
|
292
|
-
pivotForeignKeyToRoot: a.name,
|
|
293
|
-
pivotForeignKeyToTarget: b.name
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
if (!bKey.has(bProp)) {
|
|
297
|
-
bKey.add(bProp);
|
|
298
|
-
relationMap.get(targetB.name)?.push({
|
|
299
|
-
kind: 'belongsToMany',
|
|
300
|
-
property: bProp,
|
|
301
|
-
target: targetA.name,
|
|
302
|
-
pivotTable: table.name,
|
|
303
|
-
pivotForeignKeyToRoot: b.name,
|
|
304
|
-
pivotForeignKeyToTarget: a.name
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
for (const table of tables) {
|
|
312
|
-
const fkCols = fkIndex.get(table.name) || [];
|
|
313
|
-
for (const fk of fkCols) {
|
|
314
|
-
const targetTable = fk.references.table;
|
|
315
|
-
const targetKey = normalizeName(targetTable);
|
|
316
|
-
const belongsKey = relationKeys.get(table.name);
|
|
317
|
-
const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
|
|
318
|
-
|
|
319
|
-
if (!belongsKey || !hasManyKey) continue;
|
|
320
|
-
|
|
321
|
-
const belongsProp = naming.belongsToProperty(fk.name, targetTable);
|
|
322
|
-
if (!belongsKey.has(belongsProp)) {
|
|
323
|
-
belongsKey.add(belongsProp);
|
|
324
|
-
relationMap.get(table.name)?.push({
|
|
325
|
-
kind: 'belongsTo',
|
|
326
|
-
property: belongsProp,
|
|
327
|
-
target: targetTable,
|
|
328
|
-
foreignKey: fk.name
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const hasManyProp = naming.hasManyProperty(table.name);
|
|
333
|
-
if (!hasManyKey.has(hasManyProp)) {
|
|
334
|
-
hasManyKey.add(hasManyProp);
|
|
335
|
-
relationMap.get(targetKey)?.push({
|
|
336
|
-
kind: 'hasMany',
|
|
337
|
-
property: hasManyProp,
|
|
338
|
-
target: table.name,
|
|
339
|
-
foreignKey: fk.name
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
34
|
+
return;
|
|
343
35
|
}
|
|
344
|
-
|
|
345
|
-
return relationMap;
|
|
36
|
+
await generateEntities(result.options);
|
|
346
37
|
};
|
|
347
38
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
schema: t.schema,
|
|
353
|
-
columns: t.columns,
|
|
354
|
-
primaryKey: t.primaryKey || []
|
|
355
|
-
}));
|
|
356
|
-
|
|
357
|
-
const classNames = new Map();
|
|
358
|
-
tables.forEach(t => {
|
|
359
|
-
const className = naming.classNameFromTable(t.name);
|
|
360
|
-
classNames.set(t.name, className);
|
|
361
|
-
if (t.schema) {
|
|
362
|
-
const qualified = `${t.schema}.${t.name}`;
|
|
363
|
-
if (!classNames.has(qualified)) {
|
|
364
|
-
classNames.set(qualified, className);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
39
|
+
if (isEntrypoint) {
|
|
40
|
+
main().catch(err => {
|
|
41
|
+
console.error(err);
|
|
42
|
+
process.exit(1);
|
|
367
43
|
});
|
|
44
|
+
}
|
|
368
45
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (classNames.has(target)) return classNames.get(target);
|
|
372
|
-
const fallback = target.split('.').pop();
|
|
373
|
-
if (fallback && classNames.has(fallback)) {
|
|
374
|
-
return classNames.get(fallback);
|
|
375
|
-
}
|
|
376
|
-
return undefined;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const relations = mapRelations(tables, naming);
|
|
380
|
-
|
|
381
|
-
const usage = {
|
|
382
|
-
needsCol: false,
|
|
383
|
-
needsEntity: tables.length > 0,
|
|
384
|
-
needsColumnDecorator: false,
|
|
385
|
-
needsPrimaryKeyDecorator: false,
|
|
386
|
-
needsHasManyDecorator: false,
|
|
387
|
-
needsBelongsToDecorator: false,
|
|
388
|
-
needsBelongsToManyDecorator: false,
|
|
389
|
-
needsHasManyCollection: false,
|
|
390
|
-
needsManyToManyCollection: false
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const lines = [];
|
|
394
|
-
lines.push('// AUTO-GENERATED by scripts/generate-entities.mjs');
|
|
395
|
-
lines.push('// Regenerate after schema changes.');
|
|
396
|
-
const imports = [];
|
|
397
|
-
|
|
398
|
-
for (const table of tables) {
|
|
399
|
-
for (const col of table.columns) {
|
|
400
|
-
usage.needsCol = true;
|
|
401
|
-
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
402
|
-
if (rendered.decorator === 'PrimaryKey') {
|
|
403
|
-
usage.needsPrimaryKeyDecorator = true;
|
|
404
|
-
} else {
|
|
405
|
-
usage.needsColumnDecorator = true;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const rels = relations.get(table.name) || [];
|
|
410
|
-
for (const rel of rels) {
|
|
411
|
-
if (rel.kind === 'hasMany') {
|
|
412
|
-
usage.needsHasManyDecorator = true;
|
|
413
|
-
usage.needsHasManyCollection = true;
|
|
414
|
-
}
|
|
415
|
-
if (rel.kind === 'belongsTo') {
|
|
416
|
-
usage.needsBelongsToDecorator = true;
|
|
417
|
-
}
|
|
418
|
-
if (rel.kind === 'belongsToMany') {
|
|
419
|
-
usage.needsBelongsToManyDecorator = true;
|
|
420
|
-
usage.needsManyToManyCollection = true;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (usage.needsCol) {
|
|
426
|
-
imports.push("import { col } from 'metal-orm';");
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const decoratorSet = new Set(['bootstrapEntities', 'getTableDefFromEntity']);
|
|
430
|
-
if (usage.needsEntity) decoratorSet.add('Entity');
|
|
431
|
-
if (usage.needsColumnDecorator) decoratorSet.add('Column');
|
|
432
|
-
if (usage.needsPrimaryKeyDecorator) decoratorSet.add('PrimaryKey');
|
|
433
|
-
if (usage.needsHasManyDecorator) decoratorSet.add('HasMany');
|
|
434
|
-
if (usage.needsBelongsToDecorator) decoratorSet.add('BelongsTo');
|
|
435
|
-
if (usage.needsBelongsToManyDecorator) decoratorSet.add('BelongsToMany');
|
|
436
|
-
const decoratorOrder = [
|
|
437
|
-
'Entity',
|
|
438
|
-
'Column',
|
|
439
|
-
'PrimaryKey',
|
|
440
|
-
'HasMany',
|
|
441
|
-
'BelongsTo',
|
|
442
|
-
'BelongsToMany',
|
|
443
|
-
'bootstrapEntities',
|
|
444
|
-
'getTableDefFromEntity'
|
|
445
|
-
];
|
|
446
|
-
const decoratorImports = decoratorOrder.filter(name => decoratorSet.has(name));
|
|
447
|
-
if (decoratorImports.length) {
|
|
448
|
-
imports.push(`import { ${decoratorImports.join(', ')} } from 'metal-orm';`);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const ormTypes = [];
|
|
452
|
-
if (usage.needsHasManyCollection) ormTypes.push('HasManyCollection');
|
|
453
|
-
if (usage.needsManyToManyCollection) ormTypes.push('ManyToManyCollection');
|
|
454
|
-
if (ormTypes.length) {
|
|
455
|
-
imports.push(`import { ${ormTypes.join(', ')} } from 'metal-orm';`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (imports.length) {
|
|
459
|
-
lines.push(...imports, '');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
for (const table of tables) {
|
|
463
|
-
const className = classNames.get(table.name);
|
|
464
|
-
const derivedDefault = naming.defaultTableNameFromClass(className);
|
|
465
|
-
const needsTableNameOption = table.name !== derivedDefault;
|
|
466
|
-
const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
|
|
467
|
-
lines.push(`@Entity(${entityOpts})`);
|
|
468
|
-
lines.push(`export class ${className} {`);
|
|
469
|
-
|
|
470
|
-
for (const col of table.columns) {
|
|
471
|
-
const rendered = renderColumnExpression(col, table.primaryKey);
|
|
472
|
-
lines.push(` @${rendered.decorator}(${rendered.expr})`);
|
|
473
|
-
lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
|
|
474
|
-
lines.push('');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const rels = relations.get(table.name) || [];
|
|
478
|
-
for (const rel of rels) {
|
|
479
|
-
const targetClass = resolveClassName(rel.target);
|
|
480
|
-
if (!targetClass) continue;
|
|
481
|
-
switch (rel.kind) {
|
|
482
|
-
case 'belongsTo':
|
|
483
|
-
lines.push(
|
|
484
|
-
` @BelongsTo({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
|
|
485
|
-
);
|
|
486
|
-
lines.push(` ${rel.property}?: ${targetClass};`);
|
|
487
|
-
lines.push('');
|
|
488
|
-
break;
|
|
489
|
-
case 'hasMany':
|
|
490
|
-
lines.push(
|
|
491
|
-
` @HasMany({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
|
|
492
|
-
);
|
|
493
|
-
lines.push(` ${rel.property}!: HasManyCollection<${targetClass}>;`);
|
|
494
|
-
lines.push('');
|
|
495
|
-
break;
|
|
496
|
-
case 'belongsToMany':
|
|
497
|
-
const pivotClass = resolveClassName(rel.pivotTable);
|
|
498
|
-
if (!pivotClass) break;
|
|
499
|
-
lines.push(
|
|
500
|
-
` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${pivotClass}, pivotForeignKeyToRoot: '${escapeJsString(rel.pivotForeignKeyToRoot)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
|
|
501
|
-
);
|
|
502
|
-
lines.push(` ${rel.property}!: ManyToManyCollection<${targetClass}>;`);
|
|
503
|
-
lines.push('');
|
|
504
|
-
break;
|
|
505
|
-
default:
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
lines.push('}');
|
|
511
|
-
lines.push('');
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
lines.push(
|
|
515
|
-
'export const bootstrapEntityTables = () => {',
|
|
516
|
-
' const tables = bootstrapEntities();',
|
|
517
|
-
' return {',
|
|
518
|
-
...tables.map(t => ` ${classNames.get(t.name)}: getTableDefFromEntity(${classNames.get(t.name)})!,`),
|
|
519
|
-
' };',
|
|
520
|
-
'};'
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
lines.push('');
|
|
524
|
-
lines.push(
|
|
525
|
-
'export const allTables = () => bootstrapEntities();'
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
return lines.join('\n');
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const parseSqlServerConnectionConfig = connectionString => {
|
|
532
|
-
if (!connectionString) {
|
|
533
|
-
throw new Error('Missing connection string for SQL Server');
|
|
534
|
-
}
|
|
535
|
-
const url = new URL(connectionString);
|
|
536
|
-
const config = {
|
|
537
|
-
server: url.hostname,
|
|
538
|
-
authentication: {
|
|
539
|
-
type: 'default',
|
|
540
|
-
options: {
|
|
541
|
-
userName: decodeURIComponent(url.username || ''),
|
|
542
|
-
password: decodeURIComponent(url.password || '')
|
|
543
|
-
}
|
|
544
|
-
},
|
|
545
|
-
options: {}
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
const database = url.pathname ? url.pathname.replace(/^\//, '') : '';
|
|
549
|
-
if (database) {
|
|
550
|
-
config.options.database = database;
|
|
551
|
-
}
|
|
552
|
-
if (url.port) {
|
|
553
|
-
config.options.port = Number(url.port);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
for (const [key, value] of url.searchParams) {
|
|
557
|
-
config.options[key] = parseSqlServerOptionValue(value);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return config;
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
const parseSqlServerOptionValue = value => {
|
|
564
|
-
if (!value) return value;
|
|
565
|
-
if (/^-?\d+$/.test(value)) return Number(value);
|
|
566
|
-
if (/^(true|false)$/i.test(value)) return value.toLowerCase() === 'true';
|
|
567
|
-
return value;
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
const getTediousParameterType = (value, TYPES) => {
|
|
571
|
-
if (value === null || value === undefined) {
|
|
572
|
-
return TYPES.NVarChar;
|
|
573
|
-
}
|
|
574
|
-
if (typeof value === 'number') {
|
|
575
|
-
return Number.isInteger(value) ? TYPES.Int : TYPES.Float;
|
|
576
|
-
}
|
|
577
|
-
if (typeof value === 'bigint') {
|
|
578
|
-
return TYPES.BigInt;
|
|
579
|
-
}
|
|
580
|
-
if (typeof value === 'boolean') {
|
|
581
|
-
return TYPES.Bit;
|
|
582
|
-
}
|
|
583
|
-
if (value instanceof Date) {
|
|
584
|
-
return TYPES.DateTime;
|
|
585
|
-
}
|
|
586
|
-
if (Buffer.isBuffer(value)) {
|
|
587
|
-
return TYPES.VarBinary;
|
|
588
|
-
}
|
|
589
|
-
return TYPES.NVarChar;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
const loadDriver = async (dialect, url, dbPath) => {
|
|
593
|
-
switch (dialect) {
|
|
594
|
-
case 'postgres': {
|
|
595
|
-
const mod = await import('pg');
|
|
596
|
-
const { Client } = mod;
|
|
597
|
-
const client = new Client({ connectionString: url });
|
|
598
|
-
await client.connect();
|
|
599
|
-
const executor = createPostgresExecutor(client);
|
|
600
|
-
const cleanup = async () => client.end();
|
|
601
|
-
return { executor, cleanup };
|
|
602
|
-
}
|
|
603
|
-
case 'mysql': {
|
|
604
|
-
const mod = await import('mysql2/promise');
|
|
605
|
-
const conn = await mod.createConnection(url);
|
|
606
|
-
const executor = createMysqlExecutor({
|
|
607
|
-
query: (...args) => conn.execute(...args),
|
|
608
|
-
beginTransaction: () => conn.beginTransaction(),
|
|
609
|
-
commit: () => conn.commit(),
|
|
610
|
-
rollback: () => conn.rollback()
|
|
611
|
-
});
|
|
612
|
-
const cleanup = async () => conn.end();
|
|
613
|
-
return { executor, cleanup };
|
|
614
|
-
}
|
|
615
|
-
case 'sqlite': {
|
|
616
|
-
const mod = await import('sqlite3');
|
|
617
|
-
const sqlite3 = mod.default || mod;
|
|
618
|
-
const db = new sqlite3.Database(dbPath);
|
|
619
|
-
const execAll = (sql, params) =>
|
|
620
|
-
new Promise((resolve, reject) => {
|
|
621
|
-
db.all(sql, params || [], (err, rows) => {
|
|
622
|
-
if (err) return reject(err);
|
|
623
|
-
resolve(rows);
|
|
624
|
-
});
|
|
625
|
-
});
|
|
626
|
-
const executor = createSqliteExecutor({
|
|
627
|
-
all: execAll,
|
|
628
|
-
beginTransaction: () => execAll('BEGIN'),
|
|
629
|
-
commitTransaction: () => execAll('COMMIT'),
|
|
630
|
-
rollbackTransaction: () => execAll('ROLLBACK')
|
|
631
|
-
});
|
|
632
|
-
const cleanup = async () =>
|
|
633
|
-
new Promise((resolve, reject) => db.close(err => (err ? reject(err) : resolve())));
|
|
634
|
-
return { executor, cleanup };
|
|
635
|
-
}
|
|
636
|
-
case 'mssql': {
|
|
637
|
-
const mod = await import('tedious');
|
|
638
|
-
const { Connection, Request, TYPES } = mod;
|
|
639
|
-
const config = parseSqlServerConnectionConfig(url);
|
|
640
|
-
const connection = new Connection(config);
|
|
641
|
-
|
|
642
|
-
await new Promise((resolve, reject) => {
|
|
643
|
-
const onConnect = err => {
|
|
644
|
-
connection.removeListener('error', onError);
|
|
645
|
-
if (err) return reject(err);
|
|
646
|
-
resolve();
|
|
647
|
-
};
|
|
648
|
-
const onError = err => {
|
|
649
|
-
connection.removeListener('connect', onConnect);
|
|
650
|
-
reject(err);
|
|
651
|
-
};
|
|
652
|
-
connection.once('connect', onConnect);
|
|
653
|
-
connection.once('error', onError);
|
|
654
|
-
// Tedious requires an explicit connect() call to start the handshake.
|
|
655
|
-
connection.connect();
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
const execQuery = (sql, params) =>
|
|
659
|
-
new Promise((resolve, reject) => {
|
|
660
|
-
const rows = [];
|
|
661
|
-
const request = new Request(sql, err => {
|
|
662
|
-
if (err) return reject(err);
|
|
663
|
-
resolve({ recordset: rows });
|
|
664
|
-
});
|
|
665
|
-
request.on('row', columns => {
|
|
666
|
-
const row = {};
|
|
667
|
-
for (const column of columns) {
|
|
668
|
-
row[column.metadata.colName] = column.value;
|
|
669
|
-
}
|
|
670
|
-
rows.push(row);
|
|
671
|
-
});
|
|
672
|
-
params?.forEach((value, index) => {
|
|
673
|
-
request.addParameter(`p${index + 1}`, getTediousParameterType(value, TYPES), value);
|
|
674
|
-
});
|
|
675
|
-
connection.execSql(request);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
const executor = createMssqlExecutor({
|
|
679
|
-
query: execQuery,
|
|
680
|
-
beginTransaction: () =>
|
|
681
|
-
new Promise((resolve, reject) => {
|
|
682
|
-
connection.beginTransaction(err => (err ? reject(err) : resolve()));
|
|
683
|
-
}),
|
|
684
|
-
commit: () =>
|
|
685
|
-
new Promise((resolve, reject) => {
|
|
686
|
-
connection.commitTransaction(err => (err ? reject(err) : resolve()));
|
|
687
|
-
}),
|
|
688
|
-
rollback: () =>
|
|
689
|
-
new Promise((resolve, reject) => {
|
|
690
|
-
connection.rollbackTransaction(err => (err ? reject(err) : resolve()));
|
|
691
|
-
})
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
const cleanup = async () =>
|
|
695
|
-
new Promise((resolve, reject) => {
|
|
696
|
-
const onEnd = () => {
|
|
697
|
-
connection.removeListener('error', onError);
|
|
698
|
-
resolve();
|
|
699
|
-
};
|
|
700
|
-
const onError = err => {
|
|
701
|
-
connection.removeListener('end', onEnd);
|
|
702
|
-
reject(err);
|
|
703
|
-
};
|
|
704
|
-
connection.once('end', onEnd);
|
|
705
|
-
connection.once('error', onError);
|
|
706
|
-
connection.close();
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
return { executor, cleanup };
|
|
710
|
-
}
|
|
711
|
-
default:
|
|
712
|
-
throw new Error(`Unsupported dialect ${dialect}`);
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
const main = async () => {
|
|
717
|
-
const opts = parseArgs();
|
|
718
|
-
const irregulars = opts.namingOverrides ? await loadIrregulars(opts.namingOverrides) : undefined;
|
|
719
|
-
const naming = createNamingStrategy(opts.locale, irregulars);
|
|
720
|
-
|
|
721
|
-
const { executor, cleanup } = await loadDriver(opts.dialect, opts.url, opts.dbPath);
|
|
722
|
-
let schema;
|
|
723
|
-
try {
|
|
724
|
-
schema = await introspectSchema(executor, opts.dialect, {
|
|
725
|
-
schema: opts.schema,
|
|
726
|
-
includeTables: opts.include,
|
|
727
|
-
excludeTables: opts.exclude
|
|
728
|
-
});
|
|
729
|
-
} finally {
|
|
730
|
-
await cleanup?.();
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const code = renderEntityFile(schema, { ...opts, naming });
|
|
734
|
-
|
|
735
|
-
if (opts.dryRun) {
|
|
736
|
-
console.log(code);
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
await fs.mkdir(path.dirname(opts.out), { recursive: true });
|
|
741
|
-
await fs.writeFile(opts.out, code, 'utf8');
|
|
742
|
-
console.log(`Wrote ${opts.out} (${schema.tables.length} tables)`);
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
main().catch(err => {
|
|
746
|
-
console.error(err);
|
|
747
|
-
process.exit(1);
|
|
748
|
-
});
|
|
46
|
+
export { mapRelations, buildSchemaMetadata } from './generate-entities/schema.mjs';
|
|
47
|
+
export { renderEntityFile } from './generate-entities/render.mjs';
|