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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -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 toPascalCase = value =>
114
- value
115
- .split(/[^a-zA-Z0-9]+/)
116
- .filter(Boolean)
117
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
118
- .join('') || 'Entity';
119
-
120
- const toCamelCase = value => {
121
- const pascal = toPascalCase(value);
122
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
123
- };
124
-
125
- const singularize = name => {
126
- if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
127
- if (name.endsWith('ses')) return name.slice(0, -2);
128
- if (name.endsWith('s')) return name.slice(0, -1);
129
- return name;
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 = deriveBelongsToManyName(targetB.name);
305
- const bProp = deriveBelongsToManyName(targetA.name);
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 = deriveBelongsToName(fk.name, targetTable);
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 = deriveHasManyName(table.name);
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 = deriveClassName(t.name);
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 = deriveDefaultTableNameFromClass(className);
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
+ };