metal-orm 1.0.50 → 1.0.52

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.50",
3
+ "version": "1.0.52",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -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 {
@@ -290,7 +391,7 @@ export const renderEntityFile = (schema, options) => {
290
391
 
291
392
  for (const table of tables) {
292
393
  const rels = relations.get(table.name) || [];
293
- const tableUsage = computeTableUsage(table, rels);
394
+ const tableUsage = computeTableUsage(table, rels, options.schema);
294
395
  aggregateUsage.needsCol ||= tableUsage.needsCol;
295
396
  aggregateUsage.needsColumnDecorator ||= tableUsage.needsColumnDecorator;
296
397
  aggregateUsage.needsPrimaryKeyDecorator ||= tableUsage.needsPrimaryKeyDecorator;
@@ -323,7 +424,8 @@ export const renderEntityFile = (schema, options) => {
323
424
  className,
324
425
  naming,
325
426
  relations: relations.get(table.name) || [],
326
- resolveClassName: metadata.resolveClassName
427
+ resolveClassName: metadata.resolveClassName,
428
+ defaultSchema: options.schema
327
429
  });
328
430
  lines.push(...classLines);
329
431
  }
@@ -350,7 +452,7 @@ export const renderSplitEntityFiles = (schema, options) => {
350
452
  for (const table of metadata.tables) {
351
453
  const className = metadata.classNames.get(table.name);
352
454
  const relations = metadata.relations.get(table.name) || [];
353
- const usage = computeTableUsage(table, relations);
455
+ const usage = computeTableUsage(table, relations, options.schema);
354
456
  const metalImportNames = getMetalOrmImportNamesFromUsage(usage);
355
457
  const metalImportStatement = buildMetalOrmImportStatement(metalImportNames);
356
458
 
@@ -390,7 +492,8 @@ export const renderSplitEntityFiles = (schema, options) => {
390
492
  className,
391
493
  naming,
392
494
  relations,
393
- resolveClassName: metadata.resolveClassName
495
+ resolveClassName: metadata.resolveClassName,
496
+ defaultSchema: options.schema
394
497
  });
395
498
  lines.push(...classLines);
396
499
 
@@ -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
+ };