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.
Files changed (77) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2255 -284
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +559 -39
  5. package/dist/index.d.ts +559 -39
  6. package/dist/index.js +2227 -284
  7. package/dist/index.js.map +1 -1
  8. package/package.json +17 -12
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +87 -73
  11. package/scripts/generate-entities/tree-detection.mjs +67 -61
  12. package/src/bulk/bulk-context.ts +83 -0
  13. package/src/bulk/bulk-delete-executor.ts +87 -0
  14. package/src/bulk/bulk-executor.base.ts +73 -0
  15. package/src/bulk/bulk-insert-executor.ts +74 -0
  16. package/src/bulk/bulk-types.ts +70 -0
  17. package/src/bulk/bulk-update-executor.ts +192 -0
  18. package/src/bulk/bulk-upsert-executor.ts +93 -0
  19. package/src/bulk/bulk-utils.ts +91 -0
  20. package/src/bulk/index.ts +18 -0
  21. package/src/codegen/typescript.ts +30 -21
  22. package/src/core/ast/expression-builders.ts +107 -10
  23. package/src/core/ast/expression-nodes.ts +52 -22
  24. package/src/core/ast/expression-visitor.ts +23 -13
  25. package/src/core/ddl/introspect/mysql.ts +113 -36
  26. package/src/core/dialect/abstract.ts +30 -17
  27. package/src/core/dialect/mysql/index.ts +20 -5
  28. package/src/core/execution/db-executor.ts +96 -64
  29. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  30. package/src/core/execution/executors/mssql-executor.ts +66 -34
  31. package/src/core/execution/executors/mysql-executor.ts +98 -66
  32. package/src/core/execution/executors/postgres-executor.ts +33 -11
  33. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  34. package/src/decorators/bootstrap.ts +482 -398
  35. package/src/decorators/column-decorator.ts +87 -96
  36. package/src/decorators/decorator-metadata.ts +100 -24
  37. package/src/decorators/entity.ts +27 -24
  38. package/src/decorators/relations.ts +231 -149
  39. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  40. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  41. package/src/dto/apply-filter.ts +568 -551
  42. package/src/index.ts +16 -9
  43. package/src/orm/entity-hydration.ts +116 -72
  44. package/src/orm/entity-metadata.ts +347 -301
  45. package/src/orm/entity-relations.ts +264 -207
  46. package/src/orm/entity.ts +199 -199
  47. package/src/orm/execute.ts +13 -13
  48. package/src/orm/lazy-batch/morph-many.ts +70 -0
  49. package/src/orm/lazy-batch/morph-one.ts +69 -0
  50. package/src/orm/lazy-batch/morph-to.ts +59 -0
  51. package/src/orm/lazy-batch.ts +4 -1
  52. package/src/orm/orm-session.ts +170 -104
  53. package/src/orm/pooled-executor-factory.ts +99 -58
  54. package/src/orm/query-logger.ts +49 -40
  55. package/src/orm/relation-change-processor.ts +198 -96
  56. package/src/orm/relations/belongs-to.ts +143 -143
  57. package/src/orm/relations/has-many.ts +204 -204
  58. package/src/orm/relations/has-one.ts +174 -174
  59. package/src/orm/relations/many-to-many.ts +288 -288
  60. package/src/orm/relations/morph-many.ts +156 -0
  61. package/src/orm/relations/morph-one.ts +151 -0
  62. package/src/orm/relations/morph-to.ts +162 -0
  63. package/src/orm/save-graph.ts +116 -1
  64. package/src/query-builder/expression-table-mapper.ts +5 -0
  65. package/src/query-builder/hydration-manager.ts +345 -345
  66. package/src/query-builder/hydration-planner.ts +178 -148
  67. package/src/query-builder/relation-conditions.ts +171 -151
  68. package/src/query-builder/relation-cte-builder.ts +5 -1
  69. package/src/query-builder/relation-filter-utils.ts +9 -6
  70. package/src/query-builder/relation-include-strategies.ts +44 -2
  71. package/src/query-builder/relation-join-strategies.ts +8 -1
  72. package/src/query-builder/relation-service.ts +250 -241
  73. package/src/query-builder/select/select-operations.ts +110 -105
  74. package/src/query-builder/update-include.ts +4 -0
  75. package/src/schema/relation.ts +296 -188
  76. package/src/schema/types.ts +138 -123
  77. 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.9",
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.3.15",
72
- "@typescript-eslint/eslint-plugin": "^8.54.0",
73
- "@typescript-eslint/parser": "^8.54.0",
74
- "@vitest/ui": "^4.0.18",
75
- "eslint": "^9.39.2",
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.6.1",
78
- "ioredis-mock": "^8.9.0",
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.18.2",
82
- "pg": "^8.19.0",
83
- "sqlite3": "^5.1.7",
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.0.18"
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 (!comment) return null;
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 (column.references) {
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 columnPropertyNames = new Set(table.columns.map(col => sanitizePropertyName(col.name)));
286
+ const tableColumns = normalizeColumns(table.columns);
287
+ const columnPropertyNames = new Set(tableColumns.map(col => sanitizePropertyName(col.name)));
281
288
 
282
- for (const col of table.columns) {
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
- for (const col of table.columns) {
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
- export const mapRelations = (tables, naming) => {
6
- const relationMap = new Map();
7
- const relationKeys = new Map();
8
- const fkIndex = new Map();
9
- const uniqueSingleColumns = new Map();
10
-
11
- for (const table of tables) {
12
- relationMap.set(table.name, []);
13
- relationKeys.set(table.name, new Set());
14
- for (const col of table.columns) {
15
- if (col.references) {
16
- const list = fkIndex.get(table.name) || [];
17
- list.push(col);
18
- fkIndex.set(table.name, list);
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 table.columns) {
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 tables.find(t => t.name === name || t.name === norm);
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 tables) {
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 tables) {
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.tables.map(t => {
135
- const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
136
- const uniqueSingleColumns = new Set(
137
- indexes
138
- .filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
139
- .map(idx => idx.columns[0]?.column)
140
- .filter(Boolean)
141
- );
142
-
143
- return {
144
- name: t.name,
145
- schema: t.schema,
146
- columns: (t.columns || []).map(col => {
147
- const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
148
- return { ...col, unique };
149
- }),
150
- primaryKey: t.primaryKey || [],
151
- indexes,
152
- isView: false
153
- };
154
- });
155
-
156
- const views = (schema.views || []).map(v => ({
157
- name: v.name,
158
- schema: v.schema,
159
- columns: (v.columns || []).map(col => ({ ...col })),
160
- primaryKey: [],
161
- indexes: [],
162
- isView: true,
163
- definition: v.definition,
164
- comment: v.comment
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 columnNames = new Set(columns.map(c => c.name));
22
-
23
- // Common patterns for tree columns
24
- const parentPatterns = ['parentId', 'parent_id', 'parent'];
25
- const leftPatterns = ['lft', 'left', 'leftValue', 'left_value'];
26
- const rightPatterns = ['rght', 'right', 'rightValue', 'right_value'];
27
- const depthPatterns = ['depth', 'level', 'treeLevel', 'tree_level', 'cod_nivel'];
28
-
29
- // Find matching columns
30
- const parentCol = columns.find(c =>
31
- parentPatterns.includes(c.name) ||
32
- c.name.toLowerCase().includes('parent')
33
- );
34
-
35
- const leftCol = columns.find(c =>
36
- leftPatterns.includes(c.name) ||
37
- c.name.toLowerCase().includes('left')
38
- );
39
-
40
- const rightCol = columns.find(c =>
41
- rightPatterns.includes(c.name) ||
42
- c.name.toLowerCase().includes('right')
43
- );
44
-
45
- const depthCol = columns.find(c =>
46
- depthPatterns.includes(c.name) ||
47
- c.name.toLowerCase().includes('depth') ||
48
- c.name.toLowerCase().includes('level') ||
49
- c.name.toLowerCase().includes('nivel')
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
- // Skip tree columns and primary key
92
- if (c.name === config.parentKey || c.name === config.leftKey ||
93
- c.name === config.rightKey || c.name === config.depthKey) {
94
- return false;
95
- }
96
- // Skip if it's the primary key
97
- if (table.primaryKey?.includes(c.name)) {
98
- return false;
99
- }
100
- // Skip if it has a foreign key to another table
101
- if (c.references && c.references.table !== table.name) {
102
- return false;
103
- }
104
- // Look for columns that might be scope columns (e.g., tenantId, organizationId)
105
- const scopePatterns = ['tenant', 'organization', 'company', 'site', 'workspace'];
106
- return scopePatterns.some(pattern => c.name.toLowerCase().includes(pattern));
107
- }).map(c => c.name);
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
+ }