metal-orm 1.0.47 → 1.0.48
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.mjs +39 -57
- package/scripts/naming-strategy.mjs +148 -0
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
createSqliteExecutor,
|
|
24
24
|
createMssqlExecutor
|
|
25
25
|
} from '../dist/index.js';
|
|
26
|
+
import { createNamingStrategy } from './naming-strategy.mjs';
|
|
26
27
|
|
|
27
28
|
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
28
29
|
|
|
@@ -41,6 +42,8 @@ const parseArgs = () => {
|
|
|
41
42
|
include: { type: 'string' },
|
|
42
43
|
exclude: { type: 'string' },
|
|
43
44
|
out: { type: 'string' },
|
|
45
|
+
locale: { type: 'string' },
|
|
46
|
+
'naming-overrides': { type: 'string' },
|
|
44
47
|
'dry-run': { type: 'boolean' },
|
|
45
48
|
help: { type: 'boolean', short: 'h' },
|
|
46
49
|
version: { type: 'boolean' }
|
|
@@ -70,6 +73,10 @@ const parseArgs = () => {
|
|
|
70
73
|
include: values.include ? values.include.split(',').map(v => v.trim()).filter(Boolean) : undefined,
|
|
71
74
|
exclude: values.exclude ? values.exclude.split(',').map(v => v.trim()).filter(Boolean) : undefined,
|
|
72
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,
|
|
73
80
|
dryRun: Boolean(values['dry-run'])
|
|
74
81
|
};
|
|
75
82
|
|
|
@@ -102,6 +109,8 @@ Usage:
|
|
|
102
109
|
Flags:
|
|
103
110
|
--include=tbl1,tbl2 Only include these tables
|
|
104
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" }
|
|
105
114
|
--dry-run Print to stdout instead of writing a file
|
|
106
115
|
--help Show this help
|
|
107
116
|
`
|
|
@@ -110,56 +119,26 @@ Flags:
|
|
|
110
119
|
|
|
111
120
|
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
112
121
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const pluralize = name => {
|
|
133
|
-
if (name.endsWith('y')) return `${name.slice(0, -1)}ies`;
|
|
134
|
-
if (name.endsWith('s')) return `${name}es`;
|
|
135
|
-
return `${name}s`;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const deriveClassName = tableName => toPascalCase(singularize(tableName));
|
|
139
|
-
|
|
140
|
-
const toSnakeCase = value =>
|
|
141
|
-
value
|
|
142
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
143
|
-
.replace(/[^a-z0-9_]+/gi, '_')
|
|
144
|
-
.replace(/__+/g, '_')
|
|
145
|
-
.replace(/^_|_$/g, '')
|
|
146
|
-
.toLowerCase();
|
|
147
|
-
|
|
148
|
-
const deriveDefaultTableNameFromClass = className => {
|
|
149
|
-
const normalized = toSnakeCase(className);
|
|
150
|
-
if (!normalized) return 'unknown';
|
|
151
|
-
return normalized.endsWith('s') ? normalized : `${normalized}s`;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const deriveBelongsToName = (fkName, targetTable) => {
|
|
155
|
-
const trimmed = fkName.replace(/_?id$/i, '');
|
|
156
|
-
const base = trimmed && trimmed !== fkName ? trimmed : singularize(targetTable);
|
|
157
|
-
return toCamelCase(base);
|
|
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;
|
|
158
140
|
};
|
|
159
141
|
|
|
160
|
-
const deriveHasManyName = targetTable => toCamelCase(pluralize(targetTable));
|
|
161
|
-
const deriveBelongsToManyName = targetTable => toCamelCase(pluralize(targetTable));
|
|
162
|
-
|
|
163
142
|
const parseColumnType = colTypeRaw => {
|
|
164
143
|
const type = (colTypeRaw || '').toLowerCase();
|
|
165
144
|
const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
|
|
@@ -266,7 +245,7 @@ const renderColumnExpression = (column, tablePk) => {
|
|
|
266
245
|
};
|
|
267
246
|
};
|
|
268
247
|
|
|
269
|
-
const mapRelations = tables => {
|
|
248
|
+
const mapRelations = (tables, naming) => {
|
|
270
249
|
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
271
250
|
const relationMap = new Map();
|
|
272
251
|
const relationKeys = new Map();
|
|
@@ -301,8 +280,8 @@ const mapRelations = tables => {
|
|
|
301
280
|
if (targetA && targetB) {
|
|
302
281
|
const aKey = relationKeys.get(targetA.name);
|
|
303
282
|
const bKey = relationKeys.get(targetB.name);
|
|
304
|
-
const aProp =
|
|
305
|
-
const bProp =
|
|
283
|
+
const aProp = naming.belongsToManyProperty(targetB.name);
|
|
284
|
+
const bProp = naming.belongsToManyProperty(targetA.name);
|
|
306
285
|
if (!aKey.has(aProp)) {
|
|
307
286
|
aKey.add(aProp);
|
|
308
287
|
relationMap.get(targetA.name)?.push({
|
|
@@ -339,7 +318,7 @@ const mapRelations = tables => {
|
|
|
339
318
|
|
|
340
319
|
if (!belongsKey || !hasManyKey) continue;
|
|
341
320
|
|
|
342
|
-
const belongsProp =
|
|
321
|
+
const belongsProp = naming.belongsToProperty(fk.name, targetTable);
|
|
343
322
|
if (!belongsKey.has(belongsProp)) {
|
|
344
323
|
belongsKey.add(belongsProp);
|
|
345
324
|
relationMap.get(table.name)?.push({
|
|
@@ -350,7 +329,7 @@ const mapRelations = tables => {
|
|
|
350
329
|
});
|
|
351
330
|
}
|
|
352
331
|
|
|
353
|
-
const hasManyProp =
|
|
332
|
+
const hasManyProp = naming.hasManyProperty(table.name);
|
|
354
333
|
if (!hasManyKey.has(hasManyProp)) {
|
|
355
334
|
hasManyKey.add(hasManyProp);
|
|
356
335
|
relationMap.get(targetKey)?.push({
|
|
@@ -367,6 +346,7 @@ const mapRelations = tables => {
|
|
|
367
346
|
};
|
|
368
347
|
|
|
369
348
|
const renderEntityFile = (schema, options) => {
|
|
349
|
+
const naming = options.naming || createNamingStrategy('en');
|
|
370
350
|
const tables = schema.tables.map(t => ({
|
|
371
351
|
name: t.name,
|
|
372
352
|
schema: t.schema,
|
|
@@ -376,7 +356,7 @@ const renderEntityFile = (schema, options) => {
|
|
|
376
356
|
|
|
377
357
|
const classNames = new Map();
|
|
378
358
|
tables.forEach(t => {
|
|
379
|
-
const className =
|
|
359
|
+
const className = naming.classNameFromTable(t.name);
|
|
380
360
|
classNames.set(t.name, className);
|
|
381
361
|
if (t.schema) {
|
|
382
362
|
const qualified = `${t.schema}.${t.name}`;
|
|
@@ -396,7 +376,7 @@ const renderEntityFile = (schema, options) => {
|
|
|
396
376
|
return undefined;
|
|
397
377
|
};
|
|
398
378
|
|
|
399
|
-
const relations = mapRelations(tables);
|
|
379
|
+
const relations = mapRelations(tables, naming);
|
|
400
380
|
|
|
401
381
|
const usage = {
|
|
402
382
|
needsCol: false,
|
|
@@ -481,7 +461,7 @@ const renderEntityFile = (schema, options) => {
|
|
|
481
461
|
|
|
482
462
|
for (const table of tables) {
|
|
483
463
|
const className = classNames.get(table.name);
|
|
484
|
-
const derivedDefault =
|
|
464
|
+
const derivedDefault = naming.defaultTableNameFromClass(className);
|
|
485
465
|
const needsTableNameOption = table.name !== derivedDefault;
|
|
486
466
|
const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
|
|
487
467
|
lines.push(`@Entity(${entityOpts})`);
|
|
@@ -735,6 +715,8 @@ const loadDriver = async (dialect, url, dbPath) => {
|
|
|
735
715
|
|
|
736
716
|
const main = async () => {
|
|
737
717
|
const opts = parseArgs();
|
|
718
|
+
const irregulars = opts.namingOverrides ? await loadIrregulars(opts.namingOverrides) : undefined;
|
|
719
|
+
const naming = createNamingStrategy(opts.locale, irregulars);
|
|
738
720
|
|
|
739
721
|
const { executor, cleanup } = await loadDriver(opts.dialect, opts.url, opts.dbPath);
|
|
740
722
|
let schema;
|
|
@@ -748,7 +730,7 @@ const main = async () => {
|
|
|
748
730
|
await cleanup?.();
|
|
749
731
|
}
|
|
750
732
|
|
|
751
|
-
const code = renderEntityFile(schema, opts);
|
|
733
|
+
const code = renderEntityFile(schema, { ...opts, naming });
|
|
752
734
|
|
|
753
735
|
if (opts.dryRun) {
|
|
754
736
|
console.log(code);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export class BaseNamingStrategy {
|
|
2
|
+
constructor(irregulars = {}) {
|
|
3
|
+
this.irregulars = new Map();
|
|
4
|
+
this.inverseIrregulars = new Map();
|
|
5
|
+
for (const [singular, plural] of Object.entries(irregulars)) {
|
|
6
|
+
if (!singular || !plural) continue;
|
|
7
|
+
const singularKey = singular.toLowerCase();
|
|
8
|
+
const pluralValue = plural.toLowerCase();
|
|
9
|
+
this.irregulars.set(singularKey, pluralValue);
|
|
10
|
+
this.inverseIrregulars.set(pluralValue, singularKey);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
applyIrregular(word, direction) {
|
|
15
|
+
const lower = word.toLowerCase();
|
|
16
|
+
if (direction === 'plural' && this.irregulars.has(lower)) {
|
|
17
|
+
return this.irregulars.get(lower);
|
|
18
|
+
}
|
|
19
|
+
if (direction === 'singular' && this.inverseIrregulars.has(lower)) {
|
|
20
|
+
return this.inverseIrregulars.get(lower);
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toPascalCase(value) {
|
|
26
|
+
return (
|
|
27
|
+
value
|
|
28
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
31
|
+
.join('') || 'Entity'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toCamelCase(value) {
|
|
36
|
+
const pascal = this.toPascalCase(value);
|
|
37
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toSnakeCase(value) {
|
|
41
|
+
return value
|
|
42
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
43
|
+
.replace(/[^a-z0-9_]+/gi, '_')
|
|
44
|
+
.replace(/__+/g, '_')
|
|
45
|
+
.replace(/^_|_$/g, '')
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pluralize(word) {
|
|
50
|
+
const irregular = this.applyIrregular(word, 'plural');
|
|
51
|
+
if (irregular) return irregular;
|
|
52
|
+
const lower = word.toLowerCase();
|
|
53
|
+
if (lower.endsWith('y')) return `${lower.slice(0, -1)}ies`;
|
|
54
|
+
if (lower.endsWith('s')) return `${lower}es`;
|
|
55
|
+
return `${lower}s`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
singularize(word) {
|
|
59
|
+
const irregular = this.applyIrregular(word, 'singular');
|
|
60
|
+
if (irregular) return irregular;
|
|
61
|
+
const lower = word.toLowerCase();
|
|
62
|
+
if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
|
|
63
|
+
if (lower.endsWith('ses')) return lower.slice(0, -2);
|
|
64
|
+
if (lower.endsWith('s')) return lower.slice(0, -1);
|
|
65
|
+
return lower;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
classNameFromTable(tableName) {
|
|
69
|
+
return this.toPascalCase(this.singularize(tableName));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
belongsToProperty(foreignKeyName, targetTable) {
|
|
73
|
+
const trimmed = foreignKeyName.replace(/_?id$/i, '');
|
|
74
|
+
const base = trimmed && trimmed !== foreignKeyName ? trimmed : this.singularize(targetTable);
|
|
75
|
+
return this.toCamelCase(base);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hasManyProperty(targetTable) {
|
|
79
|
+
return this.toCamelCase(this.pluralize(targetTable));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
belongsToManyProperty(targetTable) {
|
|
83
|
+
return this.toCamelCase(this.pluralize(targetTable));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
defaultTableNameFromClass(className) {
|
|
87
|
+
const normalized = this.toSnakeCase(className);
|
|
88
|
+
if (!normalized) return 'unknown';
|
|
89
|
+
return this.pluralize(normalized);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class EnglishNamingStrategy extends BaseNamingStrategy {}
|
|
94
|
+
|
|
95
|
+
const DEFAULT_PT_IRREGULARS = {
|
|
96
|
+
mao: 'maos',
|
|
97
|
+
pao: 'paes',
|
|
98
|
+
cao: 'caes',
|
|
99
|
+
mal: 'males',
|
|
100
|
+
consul: 'consules'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export class PortugueseNamingStrategy extends BaseNamingStrategy {
|
|
104
|
+
constructor(irregulars = {}) {
|
|
105
|
+
super({ ...DEFAULT_PT_IRREGULARS, ...irregulars });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pluralize(word) {
|
|
109
|
+
const irregular = this.applyIrregular(word, 'plural');
|
|
110
|
+
if (irregular) return irregular;
|
|
111
|
+
const lower = word.toLowerCase();
|
|
112
|
+
if (lower.endsWith('cao')) return `${lower.slice(0, -3)}coes`;
|
|
113
|
+
if (lower.endsWith('ao')) return `${lower.slice(0, -2)}oes`;
|
|
114
|
+
if (lower.endsWith('m')) return `${lower.slice(0, -1)}ns`;
|
|
115
|
+
if (lower.endsWith('al')) return `${lower.slice(0, -2)}ais`;
|
|
116
|
+
if (lower.endsWith('el')) return `${lower.slice(0, -2)}eis`;
|
|
117
|
+
if (lower.endsWith('ol')) return `${lower.slice(0, -2)}ois`;
|
|
118
|
+
if (lower.endsWith('ul')) return `${lower.slice(0, -2)}uis`;
|
|
119
|
+
if (lower.endsWith('il')) return `${lower.slice(0, -2)}is`;
|
|
120
|
+
if (/[rznsx]$/.test(lower)) return `${lower}es`;
|
|
121
|
+
if (lower.endsWith('s')) return lower;
|
|
122
|
+
return `${lower}s`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
singularize(word) {
|
|
126
|
+
const irregular = this.applyIrregular(word, 'singular');
|
|
127
|
+
if (irregular) return irregular;
|
|
128
|
+
const lower = word.toLowerCase();
|
|
129
|
+
if (lower.endsWith('coes')) return `${lower.slice(0, -4)}cao`;
|
|
130
|
+
if (lower.endsWith('oes')) return `${lower.slice(0, -3)}ao`;
|
|
131
|
+
if (lower.endsWith('ns')) return `${lower.slice(0, -2)}m`;
|
|
132
|
+
if (lower.endsWith('ais')) return `${lower.slice(0, -3)}al`;
|
|
133
|
+
if (lower.endsWith('eis')) return `${lower.slice(0, -3)}el`;
|
|
134
|
+
if (lower.endsWith('ois')) return `${lower.slice(0, -3)}ol`;
|
|
135
|
+
if (lower.endsWith('uis')) return `${lower.slice(0, -3)}ul`;
|
|
136
|
+
if (lower.endsWith('is')) return `${lower.slice(0, -2)}il`;
|
|
137
|
+
if (/[rznsx]es$/.test(lower)) return lower.replace(/es$/, '');
|
|
138
|
+
if (lower.endsWith('s')) return lower.slice(0, -1);
|
|
139
|
+
return lower;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const createNamingStrategy = (locale = 'en', irregulars) => {
|
|
144
|
+
const normalized = (locale || 'en').toLowerCase();
|
|
145
|
+
if (normalized.startsWith('pt')) return new PortugueseNamingStrategy(irregulars);
|
|
146
|
+
if (normalized.startsWith('en')) return new EnglishNamingStrategy(irregulars);
|
|
147
|
+
return new EnglishNamingStrategy(irregulars);
|
|
148
|
+
};
|