metal-orm 1.0.50 → 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/dist/index.cjs +151 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +151 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/render.mjs +115 -12
- package/scripts/inflection/compound.mjs +72 -0
- package/scripts/inflection/en.mjs +26 -0
- package/scripts/inflection/index.mjs +29 -0
- package/scripts/inflection/pt-br.mjs +391 -0
- package/scripts/naming-strategy.mjs +27 -63
- package/scripts/pt-pluralizer.mjs +19 -0
- package/src/core/ddl/introspect/mssql.ts +74 -2
- package/src/core/ddl/introspect/postgres.ts +69 -39
- package/src/core/ddl/introspect/sqlite.ts +69 -5
- package/src/schema/column-types.ts +14 -9
package/package.json
CHANGED
|
@@ -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('
|
|
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
|
|
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(
|
|
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
|
+
};
|