metal-orm 1.0.49 → 1.0.51

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.49",
3
+ "version": "1.0.51",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -1,8 +1,107 @@
1
+ import fs from 'node:fs';
1
2
  import process from 'node:process';
2
3
  import path from 'node:path';
4
+ import { createRequire } from 'node:module';
3
5
  import { parseArgs as parseCliArgs } from 'node:util';
4
6
 
5
7
  const DIALECTS = new Set(['postgres', 'mysql', 'sqlite', 'mssql']);
8
+ const NODE_NEXT_MODULE_RESOLUTIONS = new Set(['node16', 'nodenext']);
9
+
10
+ const TS_CONFIG_BASE_NAMES = ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json'];
11
+ const nodeRequire = createRequire(import.meta.url);
12
+
13
+ const normalizeCompilerOption = value => (typeof value === 'string' ? value.trim().toLowerCase() : '');
14
+
15
+ const hasNodeNextModuleResolution = compilerOptions => {
16
+ if (!compilerOptions || typeof compilerOptions !== 'object') return false;
17
+ const moduleResolution = normalizeCompilerOption(compilerOptions.moduleResolution);
18
+ const moduleOption = normalizeCompilerOption(compilerOptions.module);
19
+ return (
20
+ NODE_NEXT_MODULE_RESOLUTIONS.has(moduleResolution) || NODE_NEXT_MODULE_RESOLUTIONS.has(moduleOption)
21
+ );
22
+ };
23
+
24
+ const resolveExtendsPath = (extendsValue, baseDir) => {
25
+ if (!extendsValue || typeof extendsValue !== 'string') return undefined;
26
+ const candidatePaths = [];
27
+ const normalizedValue = extendsValue.trim();
28
+ candidatePaths.push(path.resolve(baseDir, normalizedValue));
29
+ if (!path.extname(normalizedValue)) {
30
+ candidatePaths.push(`${path.resolve(baseDir, normalizedValue)}.json`);
31
+ }
32
+ for (const candidate of candidatePaths) {
33
+ if (fs.existsSync(candidate)) return candidate;
34
+ }
35
+ try {
36
+ return nodeRequire.resolve(normalizedValue, { paths: [baseDir] });
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ };
41
+
42
+ const inspectTsConfig = (configPath, visited) => {
43
+ if (!configPath) return false;
44
+ const normalized = path.resolve(configPath);
45
+ if (visited.has(normalized)) return false;
46
+ if (!fs.existsSync(normalized)) return false;
47
+ visited.add(normalized);
48
+ let raw;
49
+ try {
50
+ raw = fs.readFileSync(normalized, 'utf8');
51
+ } catch {
52
+ return false;
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(raw);
57
+ } catch {
58
+ return false;
59
+ }
60
+ if (hasNodeNextModuleResolution(parsed.compilerOptions)) {
61
+ return true;
62
+ }
63
+ if (parsed.extends) {
64
+ const extended = resolveExtendsPath(parsed.extends, path.dirname(normalized));
65
+ if (extended && inspectTsConfig(extended, visited)) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ };
71
+
72
+ const discoverTsConfigPaths = cwd => {
73
+ const candidates = new Set();
74
+ for (const name of TS_CONFIG_BASE_NAMES) {
75
+ const fullPath = path.join(cwd, name);
76
+ if (fs.existsSync(fullPath)) {
77
+ candidates.add(fullPath);
78
+ }
79
+ }
80
+ try {
81
+ const entries = fs.readdirSync(cwd);
82
+ for (const entry of entries) {
83
+ const lower = entry.toLowerCase();
84
+ if (lower.startsWith('tsconfig') && lower.endsWith('.json')) {
85
+ candidates.add(path.join(cwd, entry));
86
+ }
87
+ }
88
+ } catch {
89
+ // ignore readdir errors
90
+ }
91
+ return Array.from(candidates);
92
+ };
93
+
94
+ const shouldUseJsImportExtensions = cwd => {
95
+ const paths = discoverTsConfigPaths(cwd);
96
+ if (!paths.length) return false;
97
+ const visited = new Set();
98
+ for (const configPath of paths) {
99
+ if (inspectTsConfig(configPath, visited)) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ };
6
105
 
7
106
  export const parseOptions = (argv = process.argv.slice(2), env = process.env, cwd = process.cwd()) => {
8
107
  const parser = {
@@ -52,6 +151,8 @@ export const parseOptions = (argv = process.argv.slice(2), env = process.env, cw
52
151
  dryRun: Boolean(values['dry-run'])
53
152
  };
54
153
 
154
+ opts.useJsImportExtensions = shouldUseJsImportExtensions(cwd);
155
+
55
156
  if (!DIALECTS.has(opts.dialect)) {
56
157
  throw new Error(`Unsupported dialect "${opts.dialect}". Supported: ${Array.from(DIALECTS).join(', ')}`);
57
158
  }
@@ -4,11 +4,47 @@ import { buildSchemaMetadata } from './schema.mjs';
4
4
 
5
5
  const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
6
6
 
7
+ const formatJsDoc = comment => {
8
+ if (!comment) return null;
9
+ const normalized = comment.replace(/\r\n?/g, '\n').trim();
10
+ if (!normalized) return null;
11
+ const lines = normalized.split('\n').map(line => line.replace(/\*\//g, '*\\/'));
12
+ return ['/**', ...lines.map(line => ` * ${line}`), ' */'].join('\n');
13
+ };
14
+
15
+ const appendJsDoc = (lines, comment, indent = '') => {
16
+ const doc = formatJsDoc(comment);
17
+ if (!doc) return;
18
+ doc.split('\n').forEach(line => lines.push(`${indent}${line}`));
19
+ };
20
+
21
+ const normalizeReferenceTable = (refTable, sourceSchema, defaultSchema) => {
22
+ if (!refTable || typeof refTable !== 'string') return refTable;
23
+ const parts = refTable.split('.');
24
+ if (parts.length === 1) return refTable;
25
+
26
+ if (parts.length === 2) {
27
+ const [schema, table] = parts;
28
+ return schema === sourceSchema || (defaultSchema && schema === defaultSchema) ? table : refTable;
29
+ }
30
+
31
+ if (parts.length === 3) {
32
+ const [db, schema, table] = parts;
33
+ if (schema === sourceSchema || (defaultSchema && schema === defaultSchema)) {
34
+ return `${db}.${table}`;
35
+ }
36
+ return refTable;
37
+ }
38
+
39
+ return refTable;
40
+ };
41
+
7
42
  const parseColumnType = colTypeRaw => {
8
43
  const type = (colTypeRaw || '').toLowerCase();
9
44
  const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
10
45
  const length = lengthMatch ? Number(lengthMatch[1]) : undefined;
11
46
  const scale = lengthMatch && lengthMatch[2] ? Number(lengthMatch[2]) : undefined;
47
+ const isMaxLength = /\(max\)/.test(type);
12
48
 
13
49
  const base = type.replace(/\(.*\)/, '');
14
50
 
@@ -18,7 +54,8 @@ const parseColumnType = colTypeRaw => {
18
54
  if (base.includes('uuid') || base.includes('uniqueidentifier')) return { factory: 'col.uuid()', ts: 'string' };
19
55
  if (base === 'date') return { factory: 'col.date<Date>()', ts: 'Date' };
20
56
  if (base.includes('datetime') || base === 'time') return { factory: 'col.datetime<Date>()', ts: 'Date' };
21
- if (base.includes('char') || base.includes('text')) {
57
+ if (base.includes('text') || (isMaxLength && base.includes('char'))) return { factory: 'col.text()', ts: 'string' };
58
+ if (base.includes('char')) {
22
59
  const lenArg = length ? `${length}` : '255';
23
60
  return { factory: `col.varchar(${lenArg})`, ts: 'string' };
24
61
  }
@@ -64,7 +101,65 @@ const normalizeDefault = value => {
64
101
  return { kind: 'value', code: JSON.stringify(value) };
65
102
  };
66
103
 
67
- const renderColumnExpression = (column, tablePk) => {
104
+ const formatDefaultValueForDoc = value => {
105
+ const normalized = normalizeDefault(value);
106
+ if (!normalized) return null;
107
+ const code = normalized.code;
108
+ if (typeof code !== 'string') {
109
+ return `${code}`;
110
+ }
111
+ if (normalized.kind === 'raw') {
112
+ const trimmed = code.trim();
113
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
114
+ return trimmed.slice(1, -1);
115
+ }
116
+ return trimmed;
117
+ }
118
+ return code;
119
+ };
120
+
121
+ const buildColumnRemarks = column => {
122
+ const notes = [];
123
+ if (column.autoIncrement) {
124
+ notes.push('Auto-increment identity column');
125
+ }
126
+ if (column.generated) {
127
+ const verb = column.generated === 'always' ? 'Generated always' : 'Generated by default';
128
+ notes.push(`${verb} column`);
129
+ }
130
+ if (column.check) {
131
+ notes.push(`Check constraint: ${column.check}`);
132
+ }
133
+ if (column.references) {
134
+ const ref = column.references;
135
+ const actions = [];
136
+ if (ref.onDelete) actions.push(`on delete ${ref.onDelete}`);
137
+ if (ref.onUpdate) actions.push(`on update ${ref.onUpdate}`);
138
+ const actionSuffix = actions.length ? ` (${actions.join(', ')})` : '';
139
+ notes.push(`References ${ref.table}.${ref.column}${actionSuffix}`);
140
+ }
141
+ if (!notes.length) return null;
142
+ return notes.join('; ');
143
+ };
144
+
145
+ const buildColumnDoc = column => {
146
+ const entries = [];
147
+ if (column.comment) {
148
+ entries.push(column.comment);
149
+ }
150
+ const defaultValue = formatDefaultValueForDoc(column.default);
151
+ if (defaultValue) {
152
+ entries.push(`@defaultValue ${defaultValue}`);
153
+ }
154
+ const remarks = buildColumnRemarks(column);
155
+ if (remarks) {
156
+ entries.push(`@remarks ${remarks}`);
157
+ }
158
+ if (!entries.length) return null;
159
+ return entries.join('\n');
160
+ };
161
+
162
+ const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) => {
68
163
  const base = parseColumnType(column.type);
69
164
  let expr = base.factory;
70
165
 
@@ -88,8 +183,9 @@ const renderColumnExpression = (column, tablePk) => {
88
183
  }
89
184
  }
90
185
  if (column.references) {
186
+ const refTable = normalizeReferenceTable(column.references.table, tableSchema, defaultSchema);
91
187
  const refParts = [
92
- `table: '${escapeJsString(column.references.table)}'`,
188
+ `table: '${escapeJsString(refTable)}'`,
93
189
  `column: '${escapeJsString(column.references.column)}'`
94
190
  ];
95
191
  if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
@@ -106,7 +202,8 @@ const renderColumnExpression = (column, tablePk) => {
106
202
  decorator,
107
203
  expr,
108
204
  tsType,
109
- optional
205
+ optional,
206
+ comment: buildColumnDoc(column)
110
207
  };
111
208
  };
112
209
 
@@ -126,16 +223,20 @@ const METAL_ORM_IMPORT_ORDER = [
126
223
  'getTableDefFromEntity'
127
224
  ];
128
225
 
129
- const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName }) => {
226
+ const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName, defaultSchema }) => {
130
227
  const lines = [];
131
228
  const derivedDefault = naming.defaultTableNameFromClass(className);
132
229
  const needsTableNameOption = table.name !== derivedDefault;
133
230
  const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
231
+ if (table.comment) {
232
+ appendJsDoc(lines, table.comment);
233
+ }
134
234
  lines.push(`@Entity(${entityOpts})`);
135
235
  lines.push(`export class ${className} {`);
136
236
 
137
237
  for (const col of table.columns) {
138
- const rendered = renderColumnExpression(col, table.primaryKey);
238
+ const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
239
+ appendJsDoc(lines, rendered.comment, ' ');
139
240
  lines.push(` @${rendered.decorator}(${rendered.expr})`);
140
241
  lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
141
242
  lines.push('');
@@ -188,7 +289,7 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
188
289
  return lines;
189
290
  };
190
291
 
191
- const computeTableUsage = (table, relations) => {
292
+ const computeTableUsage = (table, relations, defaultSchema) => {
192
293
  const usage = {
193
294
  needsCol: false,
194
295
  needsEntity: true,
@@ -205,7 +306,7 @@ const computeTableUsage = (table, relations) => {
205
306
 
206
307
  for (const col of table.columns) {
207
308
  usage.needsCol = true;
208
- const rendered = renderColumnExpression(col, table.primaryKey);
309
+ const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
209
310
  if (rendered.decorator === 'PrimaryKey') {
210
311
  usage.needsPrimaryKeyDecorator = true;
211
312
  } else {
@@ -257,11 +358,16 @@ const buildMetalOrmImportStatement = names => {
257
358
  return `import { ${ordered.join(', ')} } from 'metal-orm';`;
258
359
  };
259
360
 
260
- const getRelativeModuleSpecifier = (from, to) => {
361
+ const appendJsExtension = (specifier, useExtension) => (useExtension ? `${specifier}.js` : specifier);
362
+
363
+ const getRelativeModuleSpecifier = (from, to, extension = '') => {
261
364
  const rel = path.relative(path.dirname(from), to).replace(/\\/g, '/');
262
- if (!rel) return './';
365
+ if (!rel) {
366
+ return './';
367
+ }
263
368
  const withoutExt = rel.replace(/\.ts$/i, '');
264
- return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
369
+ const normalized = withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
370
+ return `${normalized}${extension}`;
265
371
  };
266
372
 
267
373
  export const renderEntityFile = (schema, options) => {
@@ -285,7 +391,7 @@ export const renderEntityFile = (schema, options) => {
285
391
 
286
392
  for (const table of tables) {
287
393
  const rels = relations.get(table.name) || [];
288
- const tableUsage = computeTableUsage(table, rels);
394
+ const tableUsage = computeTableUsage(table, rels, options.schema);
289
395
  aggregateUsage.needsCol ||= tableUsage.needsCol;
290
396
  aggregateUsage.needsColumnDecorator ||= tableUsage.needsColumnDecorator;
291
397
  aggregateUsage.needsPrimaryKeyDecorator ||= tableUsage.needsPrimaryKeyDecorator;
@@ -318,7 +424,8 @@ export const renderEntityFile = (schema, options) => {
318
424
  className,
319
425
  naming,
320
426
  relations: relations.get(table.name) || [],
321
- resolveClassName: metadata.resolveClassName
427
+ resolveClassName: metadata.resolveClassName,
428
+ defaultSchema: options.schema
322
429
  });
323
430
  lines.push(...classLines);
324
431
  }
@@ -345,7 +452,7 @@ export const renderSplitEntityFiles = (schema, options) => {
345
452
  for (const table of metadata.tables) {
346
453
  const className = metadata.classNames.get(table.name);
347
454
  const relations = metadata.relations.get(table.name) || [];
348
- const usage = computeTableUsage(table, relations);
455
+ const usage = computeTableUsage(table, relations, options.schema);
349
456
  const metalImportNames = getMetalOrmImportNamesFromUsage(usage);
350
457
  const metalImportStatement = buildMetalOrmImportStatement(metalImportNames);
351
458
 
@@ -368,7 +475,8 @@ export const renderSplitEntityFiles = (schema, options) => {
368
475
  importLines.push(metalImportStatement);
369
476
  }
370
477
  for (const targetClass of Array.from(relationImports).sort()) {
371
- importLines.push(`import { ${targetClass} } from './${targetClass}';`);
478
+ const specifier = appendJsExtension(`./${targetClass}`, options.useJsImportExtensions);
479
+ importLines.push(`import { ${targetClass} } from '${specifier}';`);
372
480
  }
373
481
 
374
482
  const lines = [
@@ -384,7 +492,8 @@ export const renderSplitEntityFiles = (schema, options) => {
384
492
  className,
385
493
  naming,
386
494
  relations,
387
- resolveClassName: metadata.resolveClassName
495
+ resolveClassName: metadata.resolveClassName,
496
+ defaultSchema: options.schema
388
497
  });
389
498
  lines.push(...classLines);
390
499
 
@@ -404,7 +513,11 @@ export const renderSplitIndexFile = (metadata, options) => {
404
513
  for (const table of metadata.tables) {
405
514
  const className = metadata.classNames.get(table.name);
406
515
  const filePath = path.join(options.outDir, `${className}.ts`);
407
- const moduleSpecifier = getRelativeModuleSpecifier(options.out, filePath);
516
+ const moduleSpecifier = getRelativeModuleSpecifier(
517
+ options.out,
518
+ filePath,
519
+ options.useJsImportExtensions ? '.js' : ''
520
+ );
408
521
  importLines.push(`import { ${className} } from '${moduleSpecifier}';`);
409
522
  exportedClasses.push(className);
410
523
  }
@@ -0,0 +1,72 @@
1
+ export const stripDiacritics = value =>
2
+ String(value || '')
3
+ .normalize('NFD')
4
+ .replace(/[\u0300-\u036f]/g, '');
5
+
6
+ export const normalizeLookup = value => stripDiacritics(value).toLowerCase().trim();
7
+
8
+ export const detectTextFormat = text => {
9
+ if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(text)) return 'SCREAMING_SNAKE';
10
+ if (/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(text)) return 'snake_case';
11
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(text) && /[a-z]/.test(text)) return 'PascalCase';
12
+ if (/^[a-z][a-zA-Z0-9]*$/.test(text) && /[A-Z]/.test(text)) return 'camelCase';
13
+ return 'normal';
14
+ };
15
+
16
+ export const splitIntoWords = (text, format) => {
17
+ switch (format) {
18
+ case 'camelCase':
19
+ case 'PascalCase':
20
+ return text
21
+ .replace(/([a-z])([A-Z])/g, '$1\0$2')
22
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1\0$2')
23
+ .split('\0')
24
+ .map(p => p.toLowerCase());
25
+ case 'snake_case':
26
+ return text.toLowerCase().split('_');
27
+ case 'SCREAMING_SNAKE':
28
+ return text.toLowerCase().split('_');
29
+ default:
30
+ return text.toLowerCase().split(/\s+/);
31
+ }
32
+ };
33
+
34
+ const capitalize = value => (value ? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() : '');
35
+
36
+ export const rebuildFromWords = (words, format, originalText) => {
37
+ switch (format) {
38
+ case 'camelCase':
39
+ return words.map((w, i) => (i === 0 ? w : capitalize(w))).join('');
40
+ case 'PascalCase':
41
+ return words.map(capitalize).join('');
42
+ case 'snake_case':
43
+ return words.join('_');
44
+ case 'SCREAMING_SNAKE':
45
+ return words.map(w => w.toUpperCase()).join('_');
46
+ default: {
47
+ const result = words.join(' ');
48
+ return originalText && originalText[0] === originalText[0].toUpperCase() ? capitalize(result) : result;
49
+ }
50
+ }
51
+ };
52
+
53
+ export const applyToCompoundHead = (term, { connectors, transformWord } = {}) => {
54
+ if (!term || !String(term).trim()) return '';
55
+ const original = String(term).trim();
56
+ const format = detectTextFormat(original);
57
+ const words = splitIntoWords(original, format);
58
+
59
+ let transformed = false;
60
+ const out = words.map(word => {
61
+ const normalized = normalizeLookup(word);
62
+ if (connectors?.has?.(normalized)) return word;
63
+ if (!transformed) {
64
+ transformed = true;
65
+ return transformWord ? transformWord(word) : word;
66
+ }
67
+ return word;
68
+ });
69
+
70
+ return rebuildFromWords(out, format, original);
71
+ };
72
+
@@ -0,0 +1,26 @@
1
+ export const EN_DEFAULT_IRREGULARS = {};
2
+
3
+ export const pluralizeWordEn = word => {
4
+ const lower = String(word || '').toLowerCase();
5
+ if (!lower) return '';
6
+ if (lower.endsWith('y')) return `${lower.slice(0, -1)}ies`;
7
+ if (lower.endsWith('s')) return `${lower}es`;
8
+ return `${lower}s`;
9
+ };
10
+
11
+ export const singularizeWordEn = word => {
12
+ const lower = String(word || '').toLowerCase();
13
+ if (!lower) return '';
14
+ if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
15
+ if (lower.endsWith('ses')) return lower.slice(0, -2);
16
+ if (lower.endsWith('s')) return lower.slice(0, -1);
17
+ return lower;
18
+ };
19
+
20
+ export const createEnglishInflector = () => ({
21
+ locale: 'en',
22
+ defaultIrregulars: EN_DEFAULT_IRREGULARS,
23
+ pluralizeWord: pluralizeWordEn,
24
+ singularizeWord: singularizeWordEn
25
+ });
26
+
@@ -0,0 +1,29 @@
1
+ import { createEnglishInflector } from './en.mjs';
2
+ import { createPtBrInflector } from './pt-br.mjs';
3
+
4
+ const INFLECTOR_FACTORIES = new Map();
5
+
6
+ export const registerInflector = (localePrefix, factory) => {
7
+ const key = String(localePrefix || '').toLowerCase();
8
+ if (!key) throw new Error('localePrefix is required');
9
+ if (typeof factory !== 'function') throw new Error('factory must be a function that returns an inflector');
10
+ INFLECTOR_FACTORIES.set(key, factory);
11
+ };
12
+
13
+ registerInflector('en', createEnglishInflector);
14
+ registerInflector('pt', createPtBrInflector);
15
+
16
+ export const resolveInflector = locale => {
17
+ const normalized = String(locale || 'en').toLowerCase();
18
+ let bestFactory = undefined;
19
+ let bestPrefixLength = 0;
20
+ for (const [prefix, factory] of INFLECTOR_FACTORIES) {
21
+ if (!normalized.startsWith(prefix)) continue;
22
+ if (prefix.length > bestPrefixLength) {
23
+ bestPrefixLength = prefix.length;
24
+ bestFactory = factory;
25
+ }
26
+ }
27
+ if (bestFactory) return bestFactory();
28
+ return createEnglishInflector();
29
+ };