metal-orm 1.1.9 → 1.1.11
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/README.md +769 -764
- package/dist/index.cjs +2255 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +559 -39
- package/dist/index.d.ts +559 -39
- package/dist/index.js +2227 -284
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/scripts/generate-entities/render.mjs +21 -12
- package/scripts/generate-entities/schema.mjs +87 -73
- package/scripts/generate-entities/tree-detection.mjs +67 -61
- package/src/bulk/bulk-context.ts +83 -0
- package/src/bulk/bulk-delete-executor.ts +87 -0
- package/src/bulk/bulk-executor.base.ts +73 -0
- package/src/bulk/bulk-insert-executor.ts +74 -0
- package/src/bulk/bulk-types.ts +70 -0
- package/src/bulk/bulk-update-executor.ts +192 -0
- package/src/bulk/bulk-upsert-executor.ts +93 -0
- package/src/bulk/bulk-utils.ts +91 -0
- package/src/bulk/index.ts +18 -0
- package/src/codegen/typescript.ts +30 -21
- package/src/core/ast/expression-builders.ts +107 -10
- package/src/core/ast/expression-nodes.ts +52 -22
- package/src/core/ast/expression-visitor.ts +23 -13
- package/src/core/ddl/introspect/mysql.ts +113 -36
- package/src/core/dialect/abstract.ts +30 -17
- package/src/core/dialect/mysql/index.ts +20 -5
- package/src/core/execution/db-executor.ts +96 -64
- package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
- package/src/core/execution/executors/mssql-executor.ts +66 -34
- package/src/core/execution/executors/mysql-executor.ts +98 -66
- package/src/core/execution/executors/postgres-executor.ts +33 -11
- package/src/core/execution/executors/sqlite-executor.ts +86 -30
- package/src/decorators/bootstrap.ts +482 -398
- package/src/decorators/column-decorator.ts +87 -96
- package/src/decorators/decorator-metadata.ts +100 -24
- package/src/decorators/entity.ts +27 -24
- package/src/decorators/relations.ts +231 -149
- package/src/decorators/transformers/transformer-decorators.ts +26 -29
- package/src/decorators/validators/country-validators-decorators.ts +9 -15
- package/src/dto/apply-filter.ts +568 -551
- package/src/index.ts +16 -9
- package/src/orm/entity-hydration.ts +116 -72
- package/src/orm/entity-metadata.ts +347 -301
- package/src/orm/entity-relations.ts +264 -207
- package/src/orm/entity.ts +199 -199
- package/src/orm/execute.ts +13 -13
- package/src/orm/lazy-batch/morph-many.ts +70 -0
- package/src/orm/lazy-batch/morph-one.ts +69 -0
- package/src/orm/lazy-batch/morph-to.ts +59 -0
- package/src/orm/lazy-batch.ts +4 -1
- package/src/orm/orm-session.ts +170 -104
- package/src/orm/pooled-executor-factory.ts +99 -58
- package/src/orm/query-logger.ts +49 -40
- package/src/orm/relation-change-processor.ts +198 -96
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -0
- package/src/orm/relations/morph-one.ts +151 -0
- package/src/orm/relations/morph-to.ts +162 -0
- package/src/orm/save-graph.ts +116 -1
- package/src/query-builder/expression-table-mapper.ts +5 -0
- package/src/query-builder/hydration-manager.ts +345 -345
- package/src/query-builder/hydration-planner.ts +178 -148
- package/src/query-builder/relation-conditions.ts +171 -151
- package/src/query-builder/relation-cte-builder.ts +5 -1
- package/src/query-builder/relation-filter-utils.ts +9 -6
- package/src/query-builder/relation-include-strategies.ts +44 -2
- package/src/query-builder/relation-join-strategies.ts +8 -1
- package/src/query-builder/relation-service.ts +250 -241
- package/src/query-builder/select/select-operations.ts +110 -105
- package/src/query-builder/update-include.ts +4 -0
- package/src/schema/relation.ts +296 -188
- package/src/schema/types.ts +138 -123
- package/src/tree/tree-decorator.ts +127 -137
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"lint:fix": "node scripts/run-eslint.mjs --fix"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
+
"better-sqlite3": "^11.0.0",
|
|
43
44
|
"ioredis": "^5.0.0",
|
|
44
45
|
"keyv": "^5.6.0",
|
|
45
46
|
"mysql2": "^3.9.0",
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"tedious": "^19.0.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
52
|
+
"better-sqlite3": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
51
55
|
"mysql2": {
|
|
52
56
|
"optional": true
|
|
53
57
|
},
|
|
@@ -68,24 +72,25 @@
|
|
|
68
72
|
}
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
71
|
-
"@electric-sql/pglite": "^0.
|
|
72
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
73
|
-
"@typescript-eslint/parser": "^8.
|
|
74
|
-
"@vitest/ui": "^4.
|
|
75
|
-
"
|
|
75
|
+
"@electric-sql/pglite": "^0.4.2",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
77
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
78
|
+
"@vitest/ui": "^4.1.2",
|
|
79
|
+
"better-sqlite3": "^12.8.0",
|
|
80
|
+
"eslint": "^10.1.0",
|
|
76
81
|
"express": "^5.2.1",
|
|
77
|
-
"ioredis": "^5.
|
|
78
|
-
"ioredis-mock": "^8.
|
|
82
|
+
"ioredis": "^5.10.1",
|
|
83
|
+
"ioredis-mock": "^8.13.1",
|
|
79
84
|
"keyv": "^5.6.0",
|
|
80
85
|
"mysql-memory-server": "^1.14.1",
|
|
81
|
-
"mysql2": "^3.
|
|
82
|
-
"pg": "^8.
|
|
83
|
-
"sqlite3": "^
|
|
86
|
+
"mysql2": "^3.20.0",
|
|
87
|
+
"pg": "^8.20.0",
|
|
88
|
+
"sqlite3": "^6.0.1",
|
|
84
89
|
"supertest": "^7.2.2",
|
|
85
90
|
"tedious": "^19.2.1",
|
|
86
91
|
"tsup": "^8.5.1",
|
|
87
92
|
"tsx": "^4.21.0",
|
|
88
93
|
"typescript": "^5.9.3",
|
|
89
|
-
"vitest": "^4.
|
|
94
|
+
"vitest": "^4.1.2"
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -5,15 +5,20 @@ import { buildSchemaMetadata } from './schema.mjs';
|
|
|
5
5
|
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
6
6
|
|
|
7
7
|
const sanitizePropertyName = columnName => {
|
|
8
|
-
if (!columnName) return '';
|
|
8
|
+
if (typeof columnName !== 'string' || !columnName) return '';
|
|
9
9
|
return columnName
|
|
10
10
|
.replace(/\s+/g, '_')
|
|
11
11
|
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
12
12
|
.replace(/^[0-9]/, '_$&');
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const normalizeColumns = columns =>
|
|
16
|
+
(Array.isArray(columns) ? columns : []).filter(
|
|
17
|
+
column => column && typeof column.name === 'string' && column.name.trim().length > 0
|
|
18
|
+
);
|
|
19
|
+
|
|
15
20
|
const formatJsDoc = comment => {
|
|
16
|
-
if (
|
|
21
|
+
if (typeof comment !== 'string') return null;
|
|
17
22
|
const normalized = comment.replace(/\r\n?/g, '\n').trim();
|
|
18
23
|
if (!normalized) return null;
|
|
19
24
|
const lines = normalized.split('\n').map(line => line.replace(/\*\//g, '*\\/'));
|
|
@@ -152,7 +157,7 @@ const buildColumnRemarks = column => {
|
|
|
152
157
|
|
|
153
158
|
const buildColumnDoc = column => {
|
|
154
159
|
const entries = [];
|
|
155
|
-
if (column.comment) {
|
|
160
|
+
if (typeof column.comment === 'string' && column.comment) {
|
|
156
161
|
entries.push(column.comment);
|
|
157
162
|
}
|
|
158
163
|
const defaultValue = formatDefaultValueForDoc(column.default);
|
|
@@ -190,12 +195,13 @@ const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema, pro
|
|
|
190
195
|
: `col.default(${expr}, ${def.code})`;
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
|
-
if (
|
|
198
|
+
if (
|
|
199
|
+
column.references &&
|
|
200
|
+
typeof column.references.table === 'string' &&
|
|
201
|
+
typeof column.references.column === 'string'
|
|
202
|
+
) {
|
|
194
203
|
const refTable = normalizeReferenceTable(column.references.table, tableSchema, defaultSchema);
|
|
195
|
-
const refParts = [
|
|
196
|
-
`table: '${escapeJsString(refTable)}'`,
|
|
197
|
-
`column: '${escapeJsString(column.references.column)}'`
|
|
198
|
-
];
|
|
204
|
+
const refParts = [`table: '${escapeJsString(refTable)}'`, `column: '${escapeJsString(column.references.column)}'`];
|
|
199
205
|
if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
|
|
200
206
|
if (column.references.onUpdate) refParts.push(`onUpdate: '${escapeJsString(column.references.onUpdate)}'`);
|
|
201
207
|
expr = `col.references(${expr}, { ${refParts.join(', ')} })`;
|
|
@@ -277,9 +283,10 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
|
|
|
277
283
|
lines.push(`@Entity(${entityOpts})`);
|
|
278
284
|
lines.push(`export class ${className} {`);
|
|
279
285
|
|
|
280
|
-
const
|
|
286
|
+
const tableColumns = normalizeColumns(table.columns);
|
|
287
|
+
const columnPropertyNames = new Set(tableColumns.map(col => sanitizePropertyName(col.name)));
|
|
281
288
|
|
|
282
|
-
for (const col of
|
|
289
|
+
for (const col of tableColumns) {
|
|
283
290
|
const propertyName = sanitizePropertyName(col.name);
|
|
284
291
|
const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema, propertyName);
|
|
285
292
|
appendJsDoc(lines, rendered.comment, ' ');
|
|
@@ -414,7 +421,9 @@ const computeTableUsage = (table, relations, defaultSchema, treeConfig) => {
|
|
|
414
421
|
needsTreeChildrenDecorator: false
|
|
415
422
|
};
|
|
416
423
|
|
|
417
|
-
|
|
424
|
+
const tableColumns = normalizeColumns(table.columns);
|
|
425
|
+
|
|
426
|
+
for (const col of tableColumns) {
|
|
418
427
|
usage.needsCol = true;
|
|
419
428
|
const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema);
|
|
420
429
|
if (rendered.decorator === 'PrimaryKey') {
|
|
@@ -552,7 +561,7 @@ export const renderEntityFile = (schema, options) => {
|
|
|
552
561
|
|
|
553
562
|
// Views only need col and Column decorator
|
|
554
563
|
for (const view of views) {
|
|
555
|
-
if (view.columns.length > 0) {
|
|
564
|
+
if (normalizeColumns(view.columns).length > 0) {
|
|
556
565
|
aggregateUsage.needsCol = true;
|
|
557
566
|
aggregateUsage.needsColumnDecorator = true;
|
|
558
567
|
}
|
|
@@ -1,33 +1,42 @@
|
|
|
1
|
-
import { detectTreeTable, mapTreeTables } from './tree-detection.mjs';
|
|
2
|
-
|
|
3
|
-
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { detectTreeTable, mapTreeTables } from './tree-detection.mjs';
|
|
2
|
+
|
|
3
|
+
const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
|
|
4
|
+
const normalizeTableColumns = columns =>
|
|
5
|
+
(Array.isArray(columns) ? columns : []).filter(
|
|
6
|
+
col => col && typeof col.name === 'string' && col.name.trim().length > 0
|
|
7
|
+
);
|
|
8
|
+
const normalizeTables = tables => (Array.isArray(tables) ? tables : []);
|
|
9
|
+
|
|
10
|
+
export const mapRelations = (tables, naming) => {
|
|
11
|
+
const relationMap = new Map();
|
|
12
|
+
const relationKeys = new Map();
|
|
13
|
+
const fkIndex = new Map();
|
|
14
|
+
const uniqueSingleColumns = new Map();
|
|
15
|
+
const normalizedTables = normalizeTables(tables).filter(
|
|
16
|
+
table => table && typeof table.name === 'string' && table.name.trim().length > 0
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
for (const table of normalizedTables) {
|
|
20
|
+
const tableColumns = normalizeTableColumns(table.columns);
|
|
21
|
+
relationMap.set(table.name, []);
|
|
22
|
+
relationKeys.set(table.name, new Set());
|
|
23
|
+
for (const col of tableColumns) {
|
|
24
|
+
if (col.references) {
|
|
25
|
+
const list = fkIndex.get(table.name) || [];
|
|
26
|
+
list.push(col);
|
|
27
|
+
fkIndex.set(table.name, list);
|
|
28
|
+
}
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
const uniqueCols = new Set();
|
|
23
|
-
if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
|
|
24
|
-
uniqueCols.add(table.primaryKey[0]);
|
|
25
|
-
}
|
|
26
|
-
for (const col of
|
|
27
|
-
if (col.unique) uniqueCols.add(col.name);
|
|
28
|
-
}
|
|
29
|
-
for (const idx of table.indexes || []) {
|
|
30
|
-
if (!idx?.unique) continue;
|
|
32
|
+
if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
|
|
33
|
+
uniqueCols.add(table.primaryKey[0]);
|
|
34
|
+
}
|
|
35
|
+
for (const col of tableColumns) {
|
|
36
|
+
if (col.unique) uniqueCols.add(col.name);
|
|
37
|
+
}
|
|
38
|
+
for (const idx of table.indexes || []) {
|
|
39
|
+
if (!idx?.unique) continue;
|
|
31
40
|
if (!Array.isArray(idx.columns) || idx.columns.length !== 1 || !idx.columns[0]?.column) continue;
|
|
32
41
|
const columnName = idx.columns[0].column;
|
|
33
42
|
if (idx.where) {
|
|
@@ -38,15 +47,15 @@ export const mapRelations = (tables, naming) => {
|
|
|
38
47
|
uniqueCols.add(columnName);
|
|
39
48
|
}
|
|
40
49
|
uniqueSingleColumns.set(table.name, uniqueCols);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const findTable = name => {
|
|
44
|
-
const norm = normalizeName(name);
|
|
45
|
-
return
|
|
46
|
-
};
|
|
47
|
-
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const findTable = name => {
|
|
53
|
+
const norm = normalizeName(name);
|
|
54
|
+
return normalizedTables.find(t => t.name === name || t.name === norm);
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
const pivotTables = new Set();
|
|
49
|
-
for (const table of
|
|
58
|
+
for (const table of normalizedTables) {
|
|
50
59
|
const fkCols = fkIndex.get(table.name) || [];
|
|
51
60
|
const hasSelfReference = fkCols.some(c => normalizeName(c.references.table) === table.name);
|
|
52
61
|
const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
|
|
@@ -84,11 +93,11 @@ export const mapRelations = (tables, naming) => {
|
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const table of
|
|
90
|
-
const fkCols = fkIndex.get(table.name) || [];
|
|
91
|
-
for (const fk of fkCols) {
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const table of normalizedTables) {
|
|
99
|
+
const fkCols = fkIndex.get(table.name) || [];
|
|
100
|
+
for (const fk of fkCols) {
|
|
92
101
|
const targetTable = fk.references.table;
|
|
93
102
|
const targetKey = normalizeName(targetTable);
|
|
94
103
|
const belongsKey = relationKeys.get(table.name);
|
|
@@ -130,39 +139,44 @@ export const mapRelations = (tables, naming) => {
|
|
|
130
139
|
return relationMap;
|
|
131
140
|
};
|
|
132
141
|
|
|
133
|
-
export const buildSchemaMetadata = (schema, naming) => {
|
|
134
|
-
const tables = schema
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
142
|
+
export const buildSchemaMetadata = (schema, naming) => {
|
|
143
|
+
const tables = normalizeTables(schema?.tables)
|
|
144
|
+
.filter(t => t && typeof t.name === 'string' && t.name.trim().length > 0)
|
|
145
|
+
.map(t => {
|
|
146
|
+
const columns = normalizeTableColumns(t.columns).map(col => ({ ...col }));
|
|
147
|
+
const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
|
|
148
|
+
const uniqueSingleColumns = new Set(
|
|
149
|
+
indexes
|
|
150
|
+
.filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
|
|
151
|
+
.map(idx => idx.columns[0]?.column)
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: t.name,
|
|
157
|
+
schema: t.schema,
|
|
158
|
+
columns: columns.map(col => {
|
|
159
|
+
const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
|
|
160
|
+
return { ...col, unique };
|
|
161
|
+
}),
|
|
162
|
+
primaryKey: t.primaryKey || [],
|
|
163
|
+
indexes,
|
|
164
|
+
isView: false
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const views = normalizeTables(schema?.views)
|
|
169
|
+
.filter(v => v && typeof v.name === 'string' && v.name.trim().length > 0)
|
|
170
|
+
.map(v => ({
|
|
171
|
+
name: v.name,
|
|
172
|
+
schema: v.schema,
|
|
173
|
+
columns: normalizeTableColumns(v.columns).map(col => ({ ...col })),
|
|
174
|
+
primaryKey: [],
|
|
175
|
+
indexes: [],
|
|
176
|
+
isView: true,
|
|
177
|
+
definition: v.definition,
|
|
178
|
+
comment: v.comment
|
|
179
|
+
}));
|
|
166
180
|
|
|
167
181
|
const allEntities = [...tables, ...views];
|
|
168
182
|
|
|
@@ -16,38 +16,40 @@
|
|
|
16
16
|
* @param {Object} naming - Naming strategy instance
|
|
17
17
|
* @returns {Object|null} Tree configuration if detected, null otherwise
|
|
18
18
|
*/
|
|
19
|
-
export const detectTreeTable = (table, naming) => {
|
|
20
|
-
const columns = table.columns
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
19
|
+
export const detectTreeTable = (table, naming) => {
|
|
20
|
+
const columns = Array.isArray(table?.columns) ? table.columns : [];
|
|
21
|
+
const getColumnName = column =>
|
|
22
|
+
column && typeof column.name === 'string' && column.name.trim().length > 0 ? column.name : undefined;
|
|
23
|
+
|
|
24
|
+
// Common patterns for tree columns
|
|
25
|
+
const parentPatterns = ['parentId', 'parent_id', 'parent'];
|
|
26
|
+
const leftPatterns = ['lft', 'left', 'leftValue', 'left_value'];
|
|
27
|
+
const rightPatterns = ['rght', 'right', 'rightValue', 'right_value'];
|
|
28
|
+
const depthPatterns = ['depth', 'level', 'treeLevel', 'tree_level', 'cod_nivel'];
|
|
29
|
+
|
|
30
|
+
const matchesTreePattern = (column, patterns, term) => {
|
|
31
|
+
const name = getColumnName(column);
|
|
32
|
+
if (!name) return false;
|
|
33
|
+
return patterns.includes(name) || name.toLowerCase().includes(term);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Find matching columns
|
|
37
|
+
const parentCol = columns.find(c => matchesTreePattern(c, parentPatterns, 'parent'));
|
|
38
|
+
|
|
39
|
+
const leftCol = columns.find(c => matchesTreePattern(c, leftPatterns, 'left'));
|
|
40
|
+
|
|
41
|
+
const rightCol = columns.find(c => matchesTreePattern(c, rightPatterns, 'right'));
|
|
42
|
+
|
|
43
|
+
const depthCol = columns.find(c => {
|
|
44
|
+
const name = getColumnName(c);
|
|
45
|
+
if (!name) return false;
|
|
46
|
+
return (
|
|
47
|
+
depthPatterns.includes(name) ||
|
|
48
|
+
name.toLowerCase().includes('depth') ||
|
|
49
|
+
name.toLowerCase().includes('level') ||
|
|
50
|
+
name.toLowerCase().includes('nivel')
|
|
51
|
+
);
|
|
52
|
+
});
|
|
51
53
|
|
|
52
54
|
// Validate: must have parent, left, and right columns
|
|
53
55
|
if (!parentCol || !leftCol || !rightCol) {
|
|
@@ -86,25 +88,29 @@ export const detectTreeTable = (table, naming) => {
|
|
|
86
88
|
config.depthKey = depthCol.name;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
// Detect scope columns (columns that might be used for multi-tree scoping)
|
|
90
|
-
const scopeColumns = columns.filter(c => {
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
// Skip if it
|
|
101
|
-
if (
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
// Detect scope columns (columns that might be used for multi-tree scoping)
|
|
92
|
+
const scopeColumns = columns.filter(c => {
|
|
93
|
+
const name = getColumnName(c);
|
|
94
|
+
if (!name) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// Skip tree columns and primary key
|
|
98
|
+
if (name === config.parentKey || name === config.leftKey ||
|
|
99
|
+
name === config.rightKey || name === config.depthKey) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Skip if it's the primary key
|
|
103
|
+
if (table.primaryKey?.includes(name)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Skip if it has a foreign key to another table
|
|
107
|
+
if (c.references && c.references.table !== table.name) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Look for columns that might be scope columns (e.g., tenantId, organizationId)
|
|
111
|
+
const scopePatterns = ['tenant', 'organization', 'company', 'site', 'workspace'];
|
|
112
|
+
return scopePatterns.some(pattern => name.toLowerCase().includes(pattern));
|
|
113
|
+
}).map(c => getColumnName(c)).filter(Boolean);
|
|
108
114
|
|
|
109
115
|
if (scopeColumns.length > 0) {
|
|
110
116
|
config.scope = scopeColumns;
|
|
@@ -119,15 +125,15 @@ export const detectTreeTable = (table, naming) => {
|
|
|
119
125
|
* @param {Object} naming - Naming strategy instance
|
|
120
126
|
* @returns {Map} Map of table name to tree configuration
|
|
121
127
|
*/
|
|
122
|
-
export const mapTreeTables = (tables, naming) => {
|
|
123
|
-
const treeMap = new Map();
|
|
124
|
-
|
|
125
|
-
for (const table of tables) {
|
|
126
|
-
const treeConfig = detectTreeTable(table, naming);
|
|
127
|
-
if (treeConfig) {
|
|
128
|
-
treeMap.set(table.name, treeConfig);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
128
|
+
export const mapTreeTables = (tables, naming) => {
|
|
129
|
+
const treeMap = new Map();
|
|
130
|
+
|
|
131
|
+
for (const table of Array.isArray(tables) ? tables : []) {
|
|
132
|
+
const treeConfig = detectTreeTable(table, naming);
|
|
133
|
+
if (treeConfig && table && typeof table.name === 'string' && table.name.trim().length > 0) {
|
|
134
|
+
treeMap.set(table.name, treeConfig);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
131
137
|
|
|
132
138
|
return treeMap;
|
|
133
139
|
};
|
|
@@ -150,4 +156,4 @@ export const getTreeConfig = (tableName, treeMap) => {
|
|
|
150
156
|
*/
|
|
151
157
|
export const isTreeTable = (tableName, treeMap) => {
|
|
152
158
|
return treeMap.has(tableName);
|
|
153
|
-
};
|
|
159
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
2
|
+
import type { ExecutionContext } from '../orm/execution-context.js';
|
|
3
|
+
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
4
|
+
import type { ColumnNode } from '../core/ast/expression.js';
|
|
5
|
+
import type { TableDef } from '../schema/table.js';
|
|
6
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
7
|
+
import type { QueryResult } from '../core/execution/db-executor.js';
|
|
8
|
+
|
|
9
|
+
export interface BulkExecutionContext {
|
|
10
|
+
readonly session: OrmSession;
|
|
11
|
+
readonly executionContext: ExecutionContext;
|
|
12
|
+
readonly dialect: Dialect;
|
|
13
|
+
readonly supportsReturning: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createBulkExecutionContext(session: OrmSession): BulkExecutionContext {
|
|
17
|
+
const executionContext = session.getExecutionContext();
|
|
18
|
+
return {
|
|
19
|
+
session,
|
|
20
|
+
executionContext,
|
|
21
|
+
dialect: executionContext.dialect,
|
|
22
|
+
supportsReturning: executionContext.dialect.supportsDmlReturningClause(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function executeCompiled(
|
|
27
|
+
ctx: BulkExecutionContext,
|
|
28
|
+
compiled: CompiledQuery
|
|
29
|
+
): Promise<QueryResult[]> {
|
|
30
|
+
const payload = await ctx.executionContext.interceptors.run(
|
|
31
|
+
{ sql: compiled.sql, params: compiled.params },
|
|
32
|
+
ctx.executionContext.executor
|
|
33
|
+
);
|
|
34
|
+
return extractResultSets(payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractResultSets(payload: unknown): QueryResult[] {
|
|
38
|
+
const result = payload as { resultSets?: QueryResult[] };
|
|
39
|
+
if (result.resultSets) {
|
|
40
|
+
return result.resultSets;
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function flattenQueryResults(resultSets: QueryResult[]): Record<string, unknown>[] {
|
|
46
|
+
const rows: Record<string, unknown>[] = [];
|
|
47
|
+
for (const rs of resultSets) {
|
|
48
|
+
for (const valueRow of rs.values) {
|
|
49
|
+
const obj: Record<string, unknown> = {};
|
|
50
|
+
rs.columns.forEach((col, idx) => {
|
|
51
|
+
const bare = col.split('.').pop()!.replace(/^["`[\]]+|["`[\]]+$/g, '');
|
|
52
|
+
obj[bare] = valueRow[idx];
|
|
53
|
+
});
|
|
54
|
+
rows.push(obj);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveReturningColumns(
|
|
61
|
+
ctx: BulkExecutionContext,
|
|
62
|
+
table: TableDef,
|
|
63
|
+
returning: boolean | ColumnDef[] | undefined
|
|
64
|
+
): ColumnNode[] | undefined {
|
|
65
|
+
if (!returning) return undefined;
|
|
66
|
+
if (!ctx.supportsReturning) return undefined;
|
|
67
|
+
|
|
68
|
+
if (returning === true) {
|
|
69
|
+
return Object.values(table.columns).map(col => ({
|
|
70
|
+
type: 'Column' as const,
|
|
71
|
+
table: table.name,
|
|
72
|
+
name: col.name,
|
|
73
|
+
alias: col.name,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return returning.map(col => ({
|
|
78
|
+
type: 'Column' as const,
|
|
79
|
+
table: table.name,
|
|
80
|
+
name: col.name,
|
|
81
|
+
alias: col.name,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
2
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
3
|
+
import { and, inList } from '../core/ast/expression-builders.js';
|
|
4
|
+
import type { TableDef } from '../schema/table.js';
|
|
5
|
+
import type { OrmSession } from '../orm/orm-session.js';
|
|
6
|
+
import type { BulkDeleteOptions, ChunkOutcome } from './bulk-types.js';
|
|
7
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
8
|
+
import type { ExpressionNode } from '../core/ast/expression-nodes.js';
|
|
9
|
+
import { BulkBaseExecutor, type BulkExecutorOptions } from './bulk-executor.base.js';
|
|
10
|
+
import { createBulkExecutionContext, executeCompiled } from './bulk-context.js';
|
|
11
|
+
import { maybeTransaction } from './bulk-utils.js';
|
|
12
|
+
|
|
13
|
+
interface DeleteExecutorOptions extends BulkExecutorOptions {
|
|
14
|
+
by?: string;
|
|
15
|
+
where?: ExpressionNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class BulkDeleteExecutor extends BulkBaseExecutor<DeleteExecutorOptions> {
|
|
19
|
+
private readonly byColumnName: string;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
session: OrmSession,
|
|
23
|
+
table: TableDef,
|
|
24
|
+
ids: ValueOperandInput[],
|
|
25
|
+
options: BulkDeleteOptions = {}
|
|
26
|
+
) {
|
|
27
|
+
super(session, table, ids, options);
|
|
28
|
+
this.byColumnName = options.by ?? findPrimaryKey(table);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected async executeChunk(chunk: ValueOperandInput[], _chunkIndex: number): Promise<ChunkOutcome> {
|
|
32
|
+
const byColumn = this.table.columns[this.byColumnName];
|
|
33
|
+
if (!byColumn) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`bulkDelete: column "${this.byColumnName}" not found in table "${this.table.name}"`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const extraWhere = this.options.where;
|
|
40
|
+
const inExpr = inList(byColumn, chunk as unknown as Parameters<typeof inList>[1]);
|
|
41
|
+
const finalWhere = extraWhere ? and(inExpr, extraWhere) : inExpr;
|
|
42
|
+
|
|
43
|
+
const builder = new DeleteQueryBuilder(this.table).where(finalWhere as ExpressionNode);
|
|
44
|
+
const compiled = builder.compile(this.ctx.dialect);
|
|
45
|
+
await executeCompiled(this.ctx, compiled);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
processedRows: chunk.length,
|
|
49
|
+
returning: [],
|
|
50
|
+
elapsedMs: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function bulkDelete<TTable extends TableDef>(
|
|
56
|
+
session: OrmSession,
|
|
57
|
+
table: TTable,
|
|
58
|
+
ids: ValueOperandInput[],
|
|
59
|
+
options: BulkDeleteOptions = {}
|
|
60
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
61
|
+
if (!ids.length) {
|
|
62
|
+
return { processedRows: 0, chunksExecuted: 0, returning: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const executor = new BulkDeleteExecutor(session, table, ids, options);
|
|
66
|
+
return executor.execute();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function bulkDeleteWhere<TTable extends TableDef>(
|
|
70
|
+
session: OrmSession,
|
|
71
|
+
table: TTable,
|
|
72
|
+
where: ExpressionNode,
|
|
73
|
+
options: Pick<BulkDeleteOptions, 'transactional'> = {}
|
|
74
|
+
): Promise<import('./bulk-types.js').BulkResult> {
|
|
75
|
+
const { transactional = false } = options;
|
|
76
|
+
|
|
77
|
+
const ctx = createBulkExecutionContext(session);
|
|
78
|
+
const builder = new DeleteQueryBuilder(table).where(where);
|
|
79
|
+
const compiled = builder.compile(ctx.dialect);
|
|
80
|
+
|
|
81
|
+
const execute = async (): Promise<import('./bulk-types.js').BulkResult> => {
|
|
82
|
+
await executeCompiled(ctx, compiled);
|
|
83
|
+
return { processedRows: 0, chunksExecuted: 1, returning: [] };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return maybeTransaction(session, transactional, execute);
|
|
87
|
+
}
|