metal-orm 1.0.46 → 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/dist/index.cjs +1037 -336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -6
- package/dist/index.d.ts +28 -6
- package/dist/index.js +1035 -336
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities.mjs +74 -65
- package/scripts/naming-strategy.mjs +148 -0
- package/src/codegen/typescript.ts +22 -10
- package/src/core/ast/expression-builders.ts +13 -0
- package/src/core/ast/expression-nodes.ts +25 -5
- package/src/core/ast/expression-visitor.ts +5 -0
- package/src/core/ast/query.ts +9 -1
- package/src/core/ddl/introspect/catalogs/index.ts +4 -1
- package/src/core/ddl/introspect/catalogs/mssql.ts +126 -0
- package/src/core/ddl/introspect/catalogs/mysql.ts +89 -0
- package/src/core/ddl/introspect/catalogs/sqlite.ts +47 -0
- package/src/core/ddl/introspect/functions/mssql.ts +84 -0
- package/src/core/ddl/introspect/mssql.ts +471 -210
- package/src/core/ddl/introspect/mysql.ts +336 -125
- package/src/core/ddl/introspect/postgres.ts +44 -5
- package/src/core/ddl/introspect/run-select.ts +3 -8
- package/src/core/ddl/introspect/sqlite.ts +113 -60
- package/src/core/ddl/schema-types.ts +2 -1
- package/src/core/dialect/abstract.ts +12 -1
- package/src/core/dialect/mssql/index.ts +4 -10
- package/src/query-builder/query-ast-service.ts +3 -3
- package/src/query-builder/select-query-state.ts +2 -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,8 @@ const renderColumnExpression = (column, tablePk) => {
|
|
|
266
245
|
};
|
|
267
246
|
};
|
|
268
247
|
|
|
269
|
-
const mapRelations = tables => {
|
|
248
|
+
const mapRelations = (tables, naming) => {
|
|
249
|
+
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
270
250
|
const relationMap = new Map();
|
|
271
251
|
const relationKeys = new Map();
|
|
272
252
|
const fkIndex = new Map();
|
|
@@ -283,12 +263,15 @@ const mapRelations = tables => {
|
|
|
283
263
|
}
|
|
284
264
|
}
|
|
285
265
|
|
|
286
|
-
const findTable = name =>
|
|
266
|
+
const findTable = name => {
|
|
267
|
+
const norm = normalizeName(name);
|
|
268
|
+
return tables.find(t => t.name === name || t.name === norm);
|
|
269
|
+
};
|
|
287
270
|
|
|
288
271
|
const pivotTables = new Set();
|
|
289
272
|
for (const table of tables) {
|
|
290
273
|
const fkCols = fkIndex.get(table.name) || [];
|
|
291
|
-
const distinctTargets = Array.from(new Set(fkCols.map(c => c.references.table)));
|
|
274
|
+
const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
|
|
292
275
|
if (fkCols.length === 2 && distinctTargets.length === 2) {
|
|
293
276
|
const [a, b] = fkCols;
|
|
294
277
|
pivotTables.add(table.name);
|
|
@@ -297,8 +280,8 @@ const mapRelations = tables => {
|
|
|
297
280
|
if (targetA && targetB) {
|
|
298
281
|
const aKey = relationKeys.get(targetA.name);
|
|
299
282
|
const bKey = relationKeys.get(targetB.name);
|
|
300
|
-
const aProp =
|
|
301
|
-
const bProp =
|
|
283
|
+
const aProp = naming.belongsToManyProperty(targetB.name);
|
|
284
|
+
const bProp = naming.belongsToManyProperty(targetA.name);
|
|
302
285
|
if (!aKey.has(aProp)) {
|
|
303
286
|
aKey.add(aProp);
|
|
304
287
|
relationMap.get(targetA.name)?.push({
|
|
@@ -329,10 +312,13 @@ const mapRelations = tables => {
|
|
|
329
312
|
const fkCols = fkIndex.get(table.name) || [];
|
|
330
313
|
for (const fk of fkCols) {
|
|
331
314
|
const targetTable = fk.references.table;
|
|
315
|
+
const targetKey = normalizeName(targetTable);
|
|
332
316
|
const belongsKey = relationKeys.get(table.name);
|
|
333
|
-
const hasManyKey = relationKeys.get(
|
|
317
|
+
const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
|
|
334
318
|
|
|
335
|
-
|
|
319
|
+
if (!belongsKey || !hasManyKey) continue;
|
|
320
|
+
|
|
321
|
+
const belongsProp = naming.belongsToProperty(fk.name, targetTable);
|
|
336
322
|
if (!belongsKey.has(belongsProp)) {
|
|
337
323
|
belongsKey.add(belongsProp);
|
|
338
324
|
relationMap.get(table.name)?.push({
|
|
@@ -343,10 +329,10 @@ const mapRelations = tables => {
|
|
|
343
329
|
});
|
|
344
330
|
}
|
|
345
331
|
|
|
346
|
-
const hasManyProp =
|
|
332
|
+
const hasManyProp = naming.hasManyProperty(table.name);
|
|
347
333
|
if (!hasManyKey.has(hasManyProp)) {
|
|
348
334
|
hasManyKey.add(hasManyProp);
|
|
349
|
-
relationMap.get(
|
|
335
|
+
relationMap.get(targetKey)?.push({
|
|
350
336
|
kind: 'hasMany',
|
|
351
337
|
property: hasManyProp,
|
|
352
338
|
target: table.name,
|
|
@@ -360,16 +346,37 @@ const mapRelations = tables => {
|
|
|
360
346
|
};
|
|
361
347
|
|
|
362
348
|
const renderEntityFile = (schema, options) => {
|
|
349
|
+
const naming = options.naming || createNamingStrategy('en');
|
|
363
350
|
const tables = schema.tables.map(t => ({
|
|
364
351
|
name: t.name,
|
|
352
|
+
schema: t.schema,
|
|
365
353
|
columns: t.columns,
|
|
366
354
|
primaryKey: t.primaryKey || []
|
|
367
355
|
}));
|
|
368
356
|
|
|
369
357
|
const classNames = new Map();
|
|
370
|
-
tables.forEach(t =>
|
|
358
|
+
tables.forEach(t => {
|
|
359
|
+
const className = naming.classNameFromTable(t.name);
|
|
360
|
+
classNames.set(t.name, className);
|
|
361
|
+
if (t.schema) {
|
|
362
|
+
const qualified = `${t.schema}.${t.name}`;
|
|
363
|
+
if (!classNames.has(qualified)) {
|
|
364
|
+
classNames.set(qualified, className);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const resolveClassName = target => {
|
|
370
|
+
if (!target) return undefined;
|
|
371
|
+
if (classNames.has(target)) return classNames.get(target);
|
|
372
|
+
const fallback = target.split('.').pop();
|
|
373
|
+
if (fallback && classNames.has(fallback)) {
|
|
374
|
+
return classNames.get(fallback);
|
|
375
|
+
}
|
|
376
|
+
return undefined;
|
|
377
|
+
};
|
|
371
378
|
|
|
372
|
-
const relations = mapRelations(tables);
|
|
379
|
+
const relations = mapRelations(tables, naming);
|
|
373
380
|
|
|
374
381
|
const usage = {
|
|
375
382
|
needsCol: false,
|
|
@@ -454,7 +461,7 @@ const renderEntityFile = (schema, options) => {
|
|
|
454
461
|
|
|
455
462
|
for (const table of tables) {
|
|
456
463
|
const className = classNames.get(table.name);
|
|
457
|
-
const derivedDefault =
|
|
464
|
+
const derivedDefault = naming.defaultTableNameFromClass(className);
|
|
458
465
|
const needsTableNameOption = table.name !== derivedDefault;
|
|
459
466
|
const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
|
|
460
467
|
lines.push(`@Entity(${entityOpts})`);
|
|
@@ -469,7 +476,7 @@ const renderEntityFile = (schema, options) => {
|
|
|
469
476
|
|
|
470
477
|
const rels = relations.get(table.name) || [];
|
|
471
478
|
for (const rel of rels) {
|
|
472
|
-
const targetClass =
|
|
479
|
+
const targetClass = resolveClassName(rel.target);
|
|
473
480
|
if (!targetClass) continue;
|
|
474
481
|
switch (rel.kind) {
|
|
475
482
|
case 'belongsTo':
|
|
@@ -487,10 +494,10 @@ const renderEntityFile = (schema, options) => {
|
|
|
487
494
|
lines.push('');
|
|
488
495
|
break;
|
|
489
496
|
case 'belongsToMany':
|
|
497
|
+
const pivotClass = resolveClassName(rel.pivotTable);
|
|
498
|
+
if (!pivotClass) break;
|
|
490
499
|
lines.push(
|
|
491
|
-
` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${
|
|
492
|
-
rel.pivotTable
|
|
493
|
-
)}, pivotForeignKeyToRoot: '${escapeJsString(rel.pivotForeignKeyToRoot)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
|
|
500
|
+
` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${pivotClass}, pivotForeignKeyToRoot: '${escapeJsString(rel.pivotForeignKeyToRoot)}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
|
|
494
501
|
);
|
|
495
502
|
lines.push(` ${rel.property}!: ManyToManyCollection<${targetClass}>;`);
|
|
496
503
|
lines.push('');
|
|
@@ -708,6 +715,8 @@ const loadDriver = async (dialect, url, dbPath) => {
|
|
|
708
715
|
|
|
709
716
|
const main = async () => {
|
|
710
717
|
const opts = parseArgs();
|
|
718
|
+
const irregulars = opts.namingOverrides ? await loadIrregulars(opts.namingOverrides) : undefined;
|
|
719
|
+
const naming = createNamingStrategy(opts.locale, irregulars);
|
|
711
720
|
|
|
712
721
|
const { executor, cleanup } = await loadDriver(opts.dialect, opts.url, opts.dbPath);
|
|
713
722
|
let schema;
|
|
@@ -721,7 +730,7 @@ const main = async () => {
|
|
|
721
730
|
await cleanup?.();
|
|
722
731
|
}
|
|
723
732
|
|
|
724
|
-
const code = renderEntityFile(schema, opts);
|
|
733
|
+
const code = renderEntityFile(schema, { ...opts, naming });
|
|
725
734
|
|
|
726
735
|
if (opts.dryRun) {
|
|
727
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
|
+
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
LiteralNode,
|
|
18
18
|
FunctionNode,
|
|
19
19
|
AliasRefNode,
|
|
20
|
+
CastExpressionNode,
|
|
20
21
|
ExpressionVisitor,
|
|
21
22
|
OperandVisitor,
|
|
22
23
|
visitExpression,
|
|
@@ -41,7 +42,8 @@ type SelectionColumn =
|
|
|
41
42
|
| FunctionNode
|
|
42
43
|
| ScalarSubqueryNode
|
|
43
44
|
| CaseExpressionNode
|
|
44
|
-
| WindowFunctionNode
|
|
45
|
+
| WindowFunctionNode
|
|
46
|
+
| CastExpressionNode;
|
|
45
47
|
|
|
46
48
|
export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
|
|
47
49
|
constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) { }
|
|
@@ -181,15 +183,16 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
181
183
|
return `${this.namingStrategy.tableToSymbol(term.table)}.${term.name}`;
|
|
182
184
|
case 'AliasRef':
|
|
183
185
|
return this.visitAliasRef(term);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
case 'Literal':
|
|
187
|
+
case 'Function':
|
|
188
|
+
case 'JsonPath':
|
|
189
|
+
case 'ScalarSubquery':
|
|
190
|
+
case 'CaseExpression':
|
|
191
|
+
case 'WindowFunction':
|
|
192
|
+
case 'Cast':
|
|
193
|
+
return this.printOperand(term);
|
|
194
|
+
default:
|
|
195
|
+
return this.printExpression(term);
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -263,6 +266,10 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
263
266
|
return this.printWindowFunctionOperand(node);
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
public visitCast(node: CastExpressionNode): string {
|
|
270
|
+
return this.printCastOperand(node);
|
|
271
|
+
}
|
|
272
|
+
|
|
266
273
|
public visitAliasRef(node: AliasRefNode): string {
|
|
267
274
|
return `aliasRef('${node.name}')`;
|
|
268
275
|
}
|
|
@@ -454,6 +461,11 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
454
461
|
return result;
|
|
455
462
|
}
|
|
456
463
|
|
|
464
|
+
private printCastOperand(node: CastExpressionNode): string {
|
|
465
|
+
const typeLiteral = node.castType.replace(/'/g, "\\'");
|
|
466
|
+
return `cast(${this.printOperand(node.expression)}, '${typeLiteral}')`;
|
|
467
|
+
}
|
|
468
|
+
|
|
457
469
|
/**
|
|
458
470
|
* Converts method chain lines to inline format
|
|
459
471
|
* @param lines - Method chain lines
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
JsonPathNode,
|
|
8
8
|
OperandNode,
|
|
9
9
|
CaseExpressionNode,
|
|
10
|
+
CastExpressionNode,
|
|
10
11
|
BinaryExpressionNode,
|
|
11
12
|
ExpressionNode,
|
|
12
13
|
LogicalExpressionNode,
|
|
@@ -407,6 +408,18 @@ export const caseWhen = (
|
|
|
407
408
|
else: elseValue !== undefined ? toOperand(elseValue) : undefined
|
|
408
409
|
});
|
|
409
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Builds a CAST expression node for casting values to SQL types.
|
|
413
|
+
*/
|
|
414
|
+
export const cast = (
|
|
415
|
+
expression: OperandNode | ColumnRef | string | number | boolean | null,
|
|
416
|
+
castType: string
|
|
417
|
+
): CastExpressionNode => ({
|
|
418
|
+
type: 'Cast',
|
|
419
|
+
expression: toOperand(expression),
|
|
420
|
+
castType
|
|
421
|
+
});
|
|
422
|
+
|
|
410
423
|
/**
|
|
411
424
|
* Creates an EXISTS expression
|
|
412
425
|
* @param subquery - Subquery to check for existence
|
|
@@ -95,6 +95,19 @@ export interface CaseExpressionNode {
|
|
|
95
95
|
alias?: string;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* AST node representing a CAST expression (CAST(value AS type)).
|
|
100
|
+
*/
|
|
101
|
+
export interface CastExpressionNode {
|
|
102
|
+
type: 'Cast';
|
|
103
|
+
/** Expression being cast */
|
|
104
|
+
expression: OperandNode;
|
|
105
|
+
/** SQL type literal, e.g. "varchar(255)" */
|
|
106
|
+
castType: string;
|
|
107
|
+
/** Optional alias for the result */
|
|
108
|
+
alias?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
/**
|
|
99
112
|
* AST node representing a window function
|
|
100
113
|
*/
|
|
@@ -133,7 +146,9 @@ export type OperandNode =
|
|
|
133
146
|
| JsonPathNode
|
|
134
147
|
| ScalarSubqueryNode
|
|
135
148
|
| CaseExpressionNode
|
|
136
|
-
|
|
|
149
|
+
| CastExpressionNode
|
|
150
|
+
| WindowFunctionNode
|
|
151
|
+
| ArithmeticExpressionNode;
|
|
137
152
|
|
|
138
153
|
const operandTypes = new Set<OperandNode['type']>([
|
|
139
154
|
'AliasRef',
|
|
@@ -143,7 +158,9 @@ const operandTypes = new Set<OperandNode['type']>([
|
|
|
143
158
|
'JsonPath',
|
|
144
159
|
'ScalarSubquery',
|
|
145
160
|
'CaseExpression',
|
|
146
|
-
'
|
|
161
|
+
'Cast',
|
|
162
|
+
'WindowFunction',
|
|
163
|
+
'ArithmeticExpression'
|
|
147
164
|
]);
|
|
148
165
|
|
|
149
166
|
const hasTypeProperty = (value: unknown): value is { type?: string } =>
|
|
@@ -158,12 +175,15 @@ export const isFunctionNode = (node: unknown): node is FunctionNode =>
|
|
|
158
175
|
isOperandNode(node) && node.type === 'Function';
|
|
159
176
|
export const isCaseExpressionNode = (node: unknown): node is CaseExpressionNode =>
|
|
160
177
|
isOperandNode(node) && node.type === 'CaseExpression';
|
|
178
|
+
|
|
179
|
+
export const isCastExpressionNode = (node: unknown): node is CastExpressionNode =>
|
|
180
|
+
isOperandNode(node) && node.type === 'Cast';
|
|
161
181
|
export const isWindowFunctionNode = (node: unknown): node is WindowFunctionNode =>
|
|
162
182
|
isOperandNode(node) && node.type === 'WindowFunction';
|
|
163
183
|
export const isExpressionSelectionNode = (
|
|
164
|
-
node: ColumnRef | FunctionNode | CaseExpressionNode | WindowFunctionNode
|
|
165
|
-
): node is FunctionNode | CaseExpressionNode | WindowFunctionNode =>
|
|
166
|
-
isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
|
|
184
|
+
node: ColumnRef | FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode
|
|
185
|
+
): node is FunctionNode | CaseExpressionNode | CastExpressionNode | WindowFunctionNode =>
|
|
186
|
+
isFunctionNode(node) || isCaseExpressionNode(node) || isCastExpressionNode(node) || isWindowFunctionNode(node);
|
|
167
187
|
|
|
168
188
|
/**
|
|
169
189
|
* AST node representing a binary expression (e.g., column = value)
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
JsonPathNode,
|
|
15
15
|
ScalarSubqueryNode,
|
|
16
16
|
CaseExpressionNode,
|
|
17
|
+
CastExpressionNode,
|
|
17
18
|
WindowFunctionNode,
|
|
18
19
|
AliasRefNode
|
|
19
20
|
} from './expression-nodes.js';
|
|
@@ -42,6 +43,7 @@ export interface OperandVisitor<R> {
|
|
|
42
43
|
visitJsonPath?(node: JsonPathNode): R;
|
|
43
44
|
visitScalarSubquery?(node: ScalarSubqueryNode): R;
|
|
44
45
|
visitCaseExpression?(node: CaseExpressionNode): R;
|
|
46
|
+
visitCast?(node: CastExpressionNode): R;
|
|
45
47
|
visitWindowFunction?(node: WindowFunctionNode): R;
|
|
46
48
|
visitAliasRef?(node: AliasRefNode): R;
|
|
47
49
|
otherwise?(node: OperandNode): R;
|
|
@@ -196,6 +198,9 @@ export const visitOperand = <R>(node: OperandNode, visitor: OperandVisitor<R>):
|
|
|
196
198
|
case 'AliasRef':
|
|
197
199
|
if (visitor.visitAliasRef) return visitor.visitAliasRef(node);
|
|
198
200
|
break;
|
|
201
|
+
case 'Cast':
|
|
202
|
+
if (visitor.visitCast) return visitor.visitCast(node);
|
|
203
|
+
break;
|
|
199
204
|
default:
|
|
200
205
|
break;
|
|
201
206
|
}
|
package/src/core/ast/query.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AliasRefNode,
|
|
3
3
|
CaseExpressionNode,
|
|
4
|
+
CastExpressionNode,
|
|
4
5
|
ColumnNode,
|
|
5
6
|
ExpressionNode,
|
|
6
7
|
FunctionNode,
|
|
@@ -121,7 +122,14 @@ export interface SelectQueryNode {
|
|
|
121
122
|
/** FROM clause table (either a Table or a FunctionTable) */
|
|
122
123
|
from: TableSourceNode;
|
|
123
124
|
/** SELECT clause columns */
|
|
124
|
-
columns: (
|
|
125
|
+
columns: (
|
|
126
|
+
ColumnNode |
|
|
127
|
+
FunctionNode |
|
|
128
|
+
ScalarSubqueryNode |
|
|
129
|
+
CaseExpressionNode |
|
|
130
|
+
CastExpressionNode |
|
|
131
|
+
WindowFunctionNode
|
|
132
|
+
)[];
|
|
125
133
|
/** JOIN clauses */
|
|
126
134
|
joins: JoinNode[];
|
|
127
135
|
/** Optional WHERE clause */
|