metal-orm 1.0.7 → 1.0.9

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 (153) hide show
  1. package/README.md +133 -121
  2. package/dist/decorators/index.cjs +2564 -0
  3. package/dist/decorators/index.cjs.map +1 -0
  4. package/dist/decorators/index.d.cts +53 -0
  5. package/dist/decorators/index.d.ts +53 -0
  6. package/dist/decorators/index.js +2530 -0
  7. package/dist/decorators/index.js.map +1 -0
  8. package/dist/index.cjs +4227 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +701 -0
  11. package/dist/index.d.ts +701 -0
  12. package/dist/index.js +4131 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/select-654m4qy8.d.cts +1522 -0
  15. package/dist/select-654m4qy8.d.ts +1522 -0
  16. package/package.json +27 -20
  17. package/src/codegen/typescript.ts +405 -393
  18. package/src/core/ast/aggregate-functions.ts +30 -0
  19. package/src/core/ast/builders.ts +43 -0
  20. package/src/core/ast/expression-builders.ts +310 -0
  21. package/src/core/ast/expression-nodes.ts +211 -0
  22. package/src/core/ast/expression-visitor.ts +99 -0
  23. package/src/core/ast/expression.ts +5 -0
  24. package/src/{utils → core/ast}/join-node.ts +20 -20
  25. package/src/{ast → core/ast}/join.ts +18 -18
  26. package/src/{ast → core/ast}/query.ts +113 -113
  27. package/src/core/ast/window-functions.ts +140 -0
  28. package/src/{dialect → core/dialect}/abstract.ts +94 -94
  29. package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
  30. package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
  31. package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
  32. package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
  33. package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
  34. package/src/decorators/bootstrap.ts +126 -0
  35. package/src/decorators/column.ts +78 -0
  36. package/src/decorators/entity.ts +36 -0
  37. package/src/decorators/index.ts +4 -0
  38. package/src/decorators/relations.ts +107 -0
  39. package/src/global.d.ts +1 -0
  40. package/src/index.ts +22 -22
  41. package/src/orm/db-executor.ts +11 -0
  42. package/src/orm/domain-event-bus.ts +52 -0
  43. package/src/{runtime → orm}/entity-meta.ts +52 -52
  44. package/src/orm/entity-metadata.ts +140 -0
  45. package/src/{runtime → orm}/entity.ts +252 -252
  46. package/src/{runtime → orm}/execute.ts +36 -36
  47. package/src/{runtime → orm}/hydration.ts +103 -103
  48. package/src/orm/identity-map.ts +37 -0
  49. package/src/{runtime → orm}/lazy-batch.ts +205 -205
  50. package/src/orm/orm-context.ts +154 -0
  51. package/src/orm/relation-change-processor.ts +140 -0
  52. package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
  53. package/src/{runtime → orm}/relations/has-many.ts +111 -111
  54. package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
  55. package/src/orm/runtime-types.ts +39 -0
  56. package/src/orm/transaction-runner.ts +17 -0
  57. package/src/orm/unit-of-work.ts +232 -0
  58. package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
  59. package/src/{builder → query-builder}/delete-query-state.ts +38 -42
  60. package/src/{builder → query-builder}/delete.ts +46 -57
  61. package/src/{builder → query-builder}/hydration-manager.ts +87 -87
  62. package/src/{builder → query-builder}/hydration-planner.ts +182 -182
  63. package/src/{builder → query-builder}/insert-query-state.ts +51 -62
  64. package/src/{builder → query-builder}/insert.ts +48 -59
  65. package/src/{builder → query-builder}/query-ast-service.ts +208 -226
  66. package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
  67. package/src/{builder → query-builder}/relation-conditions.ts +112 -112
  68. package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
  69. package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
  70. package/src/{builder → query-builder}/relation-service.ts +284 -284
  71. package/src/{builder → query-builder}/relation-types.ts +21 -21
  72. package/src/{builder → query-builder}/relation-utils.ts +12 -12
  73. package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
  74. package/src/{builder → query-builder}/select-query-state.ts +179 -179
  75. package/src/{builder → query-builder}/select.ts +78 -69
  76. package/src/{builder → query-builder}/update-query-state.ts +55 -59
  77. package/src/{builder → query-builder}/update.ts +50 -61
  78. package/src/schema/column.ts +25 -25
  79. package/src/schema/relation.ts +116 -116
  80. package/src/schema/table.ts +34 -34
  81. package/src/schema/types.ts +76 -76
  82. package/.github/workflows/publish-metal-orm.yml +0 -38
  83. package/ROADMAP.md +0 -125
  84. package/docs/CHANGES.md +0 -104
  85. package/docs/advanced-features.md +0 -176
  86. package/docs/api-reference.md +0 -31
  87. package/docs/dml-operations.md +0 -156
  88. package/docs/getting-started.md +0 -171
  89. package/docs/hydration.md +0 -115
  90. package/docs/index.md +0 -36
  91. package/docs/multi-dialect-support.md +0 -59
  92. package/docs/query-builder.md +0 -135
  93. package/docs/runtime.md +0 -105
  94. package/docs/schema-definition.md +0 -112
  95. package/metadata.json +0 -5
  96. package/playground/api/playground-api.ts +0 -94
  97. package/playground/index.html +0 -15
  98. package/playground/src/App.css +0 -1
  99. package/playground/src/App.tsx +0 -114
  100. package/playground/src/components/CodeDisplay.tsx +0 -43
  101. package/playground/src/components/QueryExecutor.tsx +0 -189
  102. package/playground/src/components/ResultsTable.tsx +0 -67
  103. package/playground/src/components/ResultsTabs.tsx +0 -105
  104. package/playground/src/components/ScenarioList.tsx +0 -56
  105. package/playground/src/components/logo.svg +0 -45
  106. package/playground/src/data/scenarios.ts +0 -2
  107. package/playground/src/main.tsx +0 -9
  108. package/playground/src/services/PlaygroundApiService.ts +0 -60
  109. package/postcss.config.cjs +0 -5
  110. package/sql_sql-ansi-cheatsheet-2025.md +0 -264
  111. package/src/ast/expression.ts +0 -658
  112. package/src/builder/operations/cte-manager.ts +0 -34
  113. package/src/builder/operations/filter-manager.ts +0 -68
  114. package/src/builder/operations/join-manager.ts +0 -36
  115. package/src/builder/operations/pagination-manager.ts +0 -36
  116. package/src/playground/features/playground/api/types.ts +0 -16
  117. package/src/playground/features/playground/clients/MockClient.ts +0 -17
  118. package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
  119. package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
  120. package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
  121. package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
  122. package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
  123. package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
  124. package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
  125. package/src/playground/features/playground/data/scenarios/index.ts +0 -29
  126. package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
  127. package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
  128. package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
  129. package/src/playground/features/playground/data/scenarios/types.ts +0 -70
  130. package/src/playground/features/playground/data/schema.ts +0 -91
  131. package/src/playground/features/playground/data/seed.ts +0 -104
  132. package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
  133. package/src/runtime/orm-context.ts +0 -539
  134. package/tests/belongs-to-many.test.ts +0 -57
  135. package/tests/between.test.ts +0 -43
  136. package/tests/case-expression.test.ts +0 -58
  137. package/tests/complex-exists.test.ts +0 -230
  138. package/tests/cte.test.ts +0 -118
  139. package/tests/dml.test.ts +0 -206
  140. package/tests/exists.test.ts +0 -127
  141. package/tests/like.test.ts +0 -33
  142. package/tests/orm-runtime.test.ts +0 -254
  143. package/tests/postgres.test.ts +0 -30
  144. package/tests/right-join.test.ts +0 -89
  145. package/tests/subquery-having.test.ts +0 -193
  146. package/tests/window-function.test.ts +0 -151
  147. package/tsconfig.json +0 -30
  148. package/tsup.config.ts +0 -10
  149. package/vite.config.ts +0 -22
  150. package/vitest.config.ts +0 -14
  151. /package/src/{constants → core/sql}/sql.ts +0 -0
  152. /package/src/{runtime → orm}/als.ts +0 -0
  153. /package/src/{utils → query-builder}/relation-alias.ts +0 -0
@@ -1,5 +1,5 @@
1
- import { HydrationPlan, HydrationRelationPlan } from '../ast/query';
2
- import { isRelationAlias, makeRelationAlias } from '../utils/relation-alias';
1
+ import { HydrationPlan, HydrationRelationPlan } from '../core/ast/query.js';
2
+ import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
3
3
 
4
4
  /**
5
5
  * Hydrates query results according to a hydration plan
@@ -7,104 +7,104 @@ import { isRelationAlias, makeRelationAlias } from '../utils/relation-alias';
7
7
  * @param plan - Hydration plan
8
8
  * @returns Hydrated result objects with nested relations
9
9
  */
10
- export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
11
- if (!plan || !rows.length) return rows;
12
-
13
- const rootMap = new Map<any, Record<string, any>>();
14
- const relationIndex = new Map<any, Record<string, Set<any>>>();
15
-
16
- const getOrCreateParent = (row: Record<string, any>) => {
17
- const rootId = row[plan.rootPrimaryKey];
18
- if (rootId === undefined) return undefined;
19
-
20
- if (!rootMap.has(rootId)) {
21
- rootMap.set(rootId, createBaseRow(row, plan));
22
- }
23
-
24
- return rootMap.get(rootId);
25
- };
26
-
27
- const getRelationSeenSet = (rootId: any, relationName: string): Set<any> => {
28
- let byRelation = relationIndex.get(rootId);
29
- if (!byRelation) {
30
- byRelation = {};
31
- relationIndex.set(rootId, byRelation);
32
- }
33
-
34
- let seen = byRelation[relationName];
35
- if (!seen) {
36
- seen = new Set<any>();
37
- byRelation[relationName] = seen;
38
- }
39
-
40
- return seen;
41
- };
42
-
43
- for (const row of rows) {
44
- const rootId = row[plan.rootPrimaryKey];
45
- if (rootId === undefined) continue;
46
-
47
- const parent = getOrCreateParent(row);
48
- if (!parent) continue;
49
-
50
- for (const rel of plan.relations) {
51
- const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
52
- const childPk = row[childPkKey];
53
- if (childPk === null || childPk === undefined) continue;
54
-
55
- const seen = getRelationSeenSet(rootId, rel.name);
56
- if (seen.has(childPk)) continue;
57
- seen.add(childPk);
58
-
59
- const bucket = parent[rel.name] as any[];
60
- bucket.push(buildChild(row, rel));
61
- }
62
- }
63
-
64
- return Array.from(rootMap.values());
65
- };
66
-
67
- const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<string, any> => {
68
- const base: Record<string, any> = {};
69
- const baseKeys = plan.rootColumns.length
70
- ? plan.rootColumns
71
- : Object.keys(row).filter(k => !isRelationAlias(k));
72
-
73
- for (const key of baseKeys) {
74
- base[key] = row[key];
75
- }
76
-
77
- for (const rel of plan.relations) {
78
- base[rel.name] = [];
79
- }
80
-
81
- return base;
82
- };
83
-
84
- const buildChild = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> => {
85
- const child: Record<string, any> = {};
86
- for (const col of rel.columns) {
87
- const key = makeRelationAlias(rel.aliasPrefix, col);
88
- child[col] = row[key];
89
- }
90
-
91
- const pivot = buildPivot(row, rel);
92
- if (pivot) {
93
- (child as any)._pivot = pivot;
94
- }
95
-
96
- return child;
97
- };
98
-
99
- const buildPivot = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> | undefined => {
100
- if (!rel.pivot) return undefined;
101
-
102
- const pivot: Record<string, any> = {};
103
- for (const col of rel.pivot.columns) {
104
- const key = makeRelationAlias(rel.pivot.aliasPrefix, col);
105
- pivot[col] = row[key];
106
- }
107
-
108
- const hasValue = Object.values(pivot).some(v => v !== null && v !== undefined);
109
- return hasValue ? pivot : undefined;
110
- };
10
+ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
11
+ if (!plan || !rows.length) return rows;
12
+
13
+ const rootMap = new Map<any, Record<string, any>>();
14
+ const relationIndex = new Map<any, Record<string, Set<any>>>();
15
+
16
+ const getOrCreateParent = (row: Record<string, any>) => {
17
+ const rootId = row[plan.rootPrimaryKey];
18
+ if (rootId === undefined) return undefined;
19
+
20
+ if (!rootMap.has(rootId)) {
21
+ rootMap.set(rootId, createBaseRow(row, plan));
22
+ }
23
+
24
+ return rootMap.get(rootId);
25
+ };
26
+
27
+ const getRelationSeenSet = (rootId: any, relationName: string): Set<any> => {
28
+ let byRelation = relationIndex.get(rootId);
29
+ if (!byRelation) {
30
+ byRelation = {};
31
+ relationIndex.set(rootId, byRelation);
32
+ }
33
+
34
+ let seen = byRelation[relationName];
35
+ if (!seen) {
36
+ seen = new Set<any>();
37
+ byRelation[relationName] = seen;
38
+ }
39
+
40
+ return seen;
41
+ };
42
+
43
+ for (const row of rows) {
44
+ const rootId = row[plan.rootPrimaryKey];
45
+ if (rootId === undefined) continue;
46
+
47
+ const parent = getOrCreateParent(row);
48
+ if (!parent) continue;
49
+
50
+ for (const rel of plan.relations) {
51
+ const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
52
+ const childPk = row[childPkKey];
53
+ if (childPk === null || childPk === undefined) continue;
54
+
55
+ const seen = getRelationSeenSet(rootId, rel.name);
56
+ if (seen.has(childPk)) continue;
57
+ seen.add(childPk);
58
+
59
+ const bucket = parent[rel.name] as any[];
60
+ bucket.push(buildChild(row, rel));
61
+ }
62
+ }
63
+
64
+ return Array.from(rootMap.values());
65
+ };
66
+
67
+ const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<string, any> => {
68
+ const base: Record<string, any> = {};
69
+ const baseKeys = plan.rootColumns.length
70
+ ? plan.rootColumns
71
+ : Object.keys(row).filter(k => !isRelationAlias(k));
72
+
73
+ for (const key of baseKeys) {
74
+ base[key] = row[key];
75
+ }
76
+
77
+ for (const rel of plan.relations) {
78
+ base[rel.name] = [];
79
+ }
80
+
81
+ return base;
82
+ };
83
+
84
+ const buildChild = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> => {
85
+ const child: Record<string, any> = {};
86
+ for (const col of rel.columns) {
87
+ const key = makeRelationAlias(rel.aliasPrefix, col);
88
+ child[col] = row[key];
89
+ }
90
+
91
+ const pivot = buildPivot(row, rel);
92
+ if (pivot) {
93
+ (child as any)._pivot = pivot;
94
+ }
95
+
96
+ return child;
97
+ };
98
+
99
+ const buildPivot = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> | undefined => {
100
+ if (!rel.pivot) return undefined;
101
+
102
+ const pivot: Record<string, any> = {};
103
+ for (const col of rel.pivot.columns) {
104
+ const key = makeRelationAlias(rel.pivot.aliasPrefix, col);
105
+ pivot[col] = row[key];
106
+ }
107
+
108
+ const hasValue = Object.values(pivot).some(v => v !== null && v !== undefined);
109
+ return hasValue ? pivot : undefined;
110
+ };
@@ -0,0 +1,37 @@
1
+ import type { TableDef } from '../schema/table.js';
2
+ import type { TrackedEntity } from './runtime-types.js';
3
+
4
+ export class IdentityMap {
5
+ private readonly buckets = new Map<string, Map<string, TrackedEntity>>();
6
+
7
+ get bucketsMap(): Map<string, Map<string, TrackedEntity>> {
8
+ return this.buckets;
9
+ }
10
+
11
+ getEntity(table: TableDef, pk: string | number): any | undefined {
12
+ const bucket = this.buckets.get(table.name);
13
+ return bucket?.get(this.toIdentityKey(pk))?.entity;
14
+ }
15
+
16
+ register(tracked: TrackedEntity): void {
17
+ if (tracked.pk == null) return;
18
+ const bucket = this.buckets.get(tracked.table.name) ?? new Map<string, TrackedEntity>();
19
+ bucket.set(this.toIdentityKey(tracked.pk), tracked);
20
+ this.buckets.set(tracked.table.name, bucket);
21
+ }
22
+
23
+ remove(tracked: TrackedEntity): void {
24
+ if (tracked.pk == null) return;
25
+ const bucket = this.buckets.get(tracked.table.name);
26
+ bucket?.delete(this.toIdentityKey(tracked.pk));
27
+ }
28
+
29
+ getEntitiesForTable(table: TableDef): TrackedEntity[] {
30
+ const bucket = this.buckets.get(table.name);
31
+ return bucket ? Array.from(bucket.values()) : [];
32
+ }
33
+
34
+ private toIdentityKey(pk: string | number): string {
35
+ return String(pk);
36
+ }
37
+ }
@@ -1,205 +1,205 @@
1
- import { TableDef } from '../schema/table';
2
- import { BelongsToManyRelation, HasManyRelation, BelongsToRelation } from '../schema/relation';
3
- import { SelectQueryBuilder } from '../builder/select';
4
- import { inList, LiteralNode } from '../ast/expression';
5
- import { OrmContext, QueryResult } from './orm-context';
6
- import { ColumnDef } from '../schema/column';
7
- import { findPrimaryKey } from '../builder/hydration-planner';
8
-
9
- type Rows = Record<string, any>[];
10
-
11
- const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
12
- Object.entries(table.columns).reduce((acc, [name, def]) => {
13
- acc[name] = def;
14
- return acc;
15
- }, {} as Record<string, ColumnDef>);
16
-
17
- const rowsFromResults = (results: QueryResult[]): Rows => {
18
- const rows: Rows = [];
19
- for (const result of results) {
20
- const { columns, values } = result;
21
- for (const valueRow of values) {
22
- const row: Record<string, any> = {};
23
- columns.forEach((column, idx) => {
24
- row[column] = valueRow[idx];
25
- });
26
- rows.push(row);
27
- }
28
- }
29
- return rows;
30
- };
31
-
32
- const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
33
- const compiled = ctx.dialect.compileSelect(qb.getAST());
34
- const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
35
- return rowsFromResults(results);
36
- };
37
-
38
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
39
-
40
- export const loadHasManyRelation = async (
41
- ctx: OrmContext,
42
- rootTable: TableDef,
43
- _relationName: string,
44
- relation: HasManyRelation
45
- ): Promise<Map<string, Rows>> => {
46
- const localKey = relation.localKey || findPrimaryKey(rootTable);
47
- const roots = ctx.getEntitiesForTable(rootTable);
48
- const keys = new Set<unknown>();
49
-
50
- for (const tracked of roots) {
51
- const value = tracked.entity[localKey];
52
- if (value !== null && value !== undefined) {
53
- keys.add(value);
54
- }
55
- }
56
-
57
- if (!keys.size) {
58
- return new Map();
59
- }
60
-
61
- const selectMap = selectAllColumns(relation.target);
62
- const fb = new SelectQueryBuilder(relation.target).select(selectMap);
63
- const fkColumn = relation.target.columns[relation.foreignKey];
64
- if (!fkColumn) return new Map();
65
-
66
- fb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
67
-
68
- const rows = await executeQuery(ctx, fb);
69
- const grouped = new Map<string, Rows>();
70
-
71
- for (const row of rows) {
72
- const fkValue = row[relation.foreignKey];
73
- if (fkValue === null || fkValue === undefined) continue;
74
- const key = toKey(fkValue);
75
- const bucket = grouped.get(key) ?? [];
76
- bucket.push(row);
77
- grouped.set(key, bucket);
78
- }
79
-
80
- return grouped;
81
- };
82
-
83
- export const loadBelongsToRelation = async (
84
- ctx: OrmContext,
85
- rootTable: TableDef,
86
- _relationName: string,
87
- relation: BelongsToRelation
88
- ): Promise<Map<string, Record<string, any>>> => {
89
- const roots = ctx.getEntitiesForTable(rootTable);
90
- const foreignKeys = new Set<unknown>();
91
-
92
- for (const tracked of roots) {
93
- const value = tracked.entity[relation.foreignKey];
94
- if (value !== null && value !== undefined) {
95
- foreignKeys.add(value);
96
- }
97
- }
98
-
99
- if (!foreignKeys.size) {
100
- return new Map();
101
- }
102
-
103
- const selectMap = selectAllColumns(relation.target);
104
- const qb = new SelectQueryBuilder(relation.target).select(selectMap);
105
- const targetKey = relation.localKey || findPrimaryKey(relation.target);
106
- const pkColumn = relation.target.columns[targetKey];
107
- if (!pkColumn) return new Map();
108
-
109
- qb.where(inList(pkColumn, Array.from(foreignKeys) as (string | number | LiteralNode)[]));
110
- const rows = await executeQuery(ctx, qb);
111
- const map = new Map<string, Record<string, any>>();
112
-
113
- for (const row of rows) {
114
- const keyValue = row[targetKey];
115
- if (keyValue === null || keyValue === undefined) continue;
116
- map.set(toKey(keyValue), row);
117
- }
118
-
119
- return map;
120
- };
121
-
122
- export const loadBelongsToManyRelation = async (
123
- ctx: OrmContext,
124
- rootTable: TableDef,
125
- _relationName: string,
126
- relation: BelongsToManyRelation
127
- ): Promise<Map<string, Rows>> => {
128
- const rootKey = relation.localKey || findPrimaryKey(rootTable);
129
- const roots = ctx.getEntitiesForTable(rootTable);
130
- const rootIds = new Set<unknown>();
131
-
132
- for (const tracked of roots) {
133
- const value = tracked.entity[rootKey];
134
- if (value !== null && value !== undefined) {
135
- rootIds.add(value);
136
- }
137
- }
138
-
139
- if (!rootIds.size) {
140
- return new Map();
141
- }
142
-
143
- const pivotSelect = selectAllColumns(relation.pivotTable);
144
- const pivotQb = new SelectQueryBuilder(relation.pivotTable).select(pivotSelect);
145
- const pivotFkCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
146
- if (!pivotFkCol) return new Map();
147
-
148
- pivotQb.where(inList(pivotFkCol, Array.from(rootIds) as (string | number | LiteralNode)[]));
149
- const pivotRows = await executeQuery(ctx, pivotQb);
150
-
151
- const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, any> }[]>();
152
- const targetIds = new Set<unknown>();
153
-
154
- for (const pivot of pivotRows) {
155
- const rootValue = pivot[relation.pivotForeignKeyToRoot];
156
- const targetValue = pivot[relation.pivotForeignKeyToTarget];
157
- if (rootValue === null || rootValue === undefined || targetValue === null || targetValue === undefined) {
158
- continue;
159
- }
160
- const bucket = rootLookup.get(toKey(rootValue)) ?? [];
161
- bucket.push({
162
- targetId: targetValue,
163
- pivot: { ...pivot }
164
- });
165
- rootLookup.set(toKey(rootValue), bucket);
166
- targetIds.add(targetValue);
167
- }
168
-
169
- if (!targetIds.size) {
170
- return new Map();
171
- }
172
-
173
- const targetSelect = selectAllColumns(relation.target);
174
- const targetKey = relation.targetKey || findPrimaryKey(relation.target);
175
- const targetPkColumn = relation.target.columns[targetKey];
176
- if (!targetPkColumn) return new Map();
177
-
178
- const targetQb = new SelectQueryBuilder(relation.target).select(targetSelect);
179
- targetQb.where(inList(targetPkColumn, Array.from(targetIds) as (string | number | LiteralNode)[]));
180
- const targetRows = await executeQuery(ctx, targetQb);
181
- const targetMap = new Map<string, Record<string, any>>();
182
-
183
- for (const row of targetRows) {
184
- const pkValue = row[targetKey];
185
- if (pkValue === null || pkValue === undefined) continue;
186
- targetMap.set(toKey(pkValue), row);
187
- }
188
-
189
- const result = new Map<string, Rows>();
190
-
191
- for (const [rootId, entries] of rootLookup.entries()) {
192
- const bucket: Rows = [];
193
- for (const entry of entries) {
194
- const targetRow = targetMap.get(toKey(entry.targetId));
195
- if (!targetRow) continue;
196
- bucket.push({
197
- ...targetRow,
198
- _pivot: entry.pivot
199
- });
200
- }
201
- result.set(rootId, bucket);
202
- }
203
-
204
- return result;
205
- };
1
+ import { TableDef } from '../schema/table.js';
2
+ import { BelongsToManyRelation, HasManyRelation, BelongsToRelation } from '../schema/relation.js';
3
+ import { SelectQueryBuilder } from '../query-builder/select.js';
4
+ import { inList, LiteralNode } from '../core/ast/expression.js';
5
+ import { OrmContext, QueryResult } from './orm-context.js';
6
+ import { ColumnDef } from '../schema/column.js';
7
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
+
9
+ type Rows = Record<string, any>[];
10
+
11
+ const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
12
+ Object.entries(table.columns).reduce((acc, [name, def]) => {
13
+ acc[name] = def;
14
+ return acc;
15
+ }, {} as Record<string, ColumnDef>);
16
+
17
+ const rowsFromResults = (results: QueryResult[]): Rows => {
18
+ const rows: Rows = [];
19
+ for (const result of results) {
20
+ const { columns, values } = result;
21
+ for (const valueRow of values) {
22
+ const row: Record<string, any> = {};
23
+ columns.forEach((column, idx) => {
24
+ row[column] = valueRow[idx];
25
+ });
26
+ rows.push(row);
27
+ }
28
+ }
29
+ return rows;
30
+ };
31
+
32
+ const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
33
+ const compiled = ctx.dialect.compileSelect(qb.getAST());
34
+ const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
35
+ return rowsFromResults(results);
36
+ };
37
+
38
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
39
+
40
+ export const loadHasManyRelation = async (
41
+ ctx: OrmContext,
42
+ rootTable: TableDef,
43
+ _relationName: string,
44
+ relation: HasManyRelation
45
+ ): Promise<Map<string, Rows>> => {
46
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
47
+ const roots = ctx.getEntitiesForTable(rootTable);
48
+ const keys = new Set<unknown>();
49
+
50
+ for (const tracked of roots) {
51
+ const value = tracked.entity[localKey];
52
+ if (value !== null && value !== undefined) {
53
+ keys.add(value);
54
+ }
55
+ }
56
+
57
+ if (!keys.size) {
58
+ return new Map();
59
+ }
60
+
61
+ const selectMap = selectAllColumns(relation.target);
62
+ const fb = new SelectQueryBuilder(relation.target).select(selectMap);
63
+ const fkColumn = relation.target.columns[relation.foreignKey];
64
+ if (!fkColumn) return new Map();
65
+
66
+ fb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
67
+
68
+ const rows = await executeQuery(ctx, fb);
69
+ const grouped = new Map<string, Rows>();
70
+
71
+ for (const row of rows) {
72
+ const fkValue = row[relation.foreignKey];
73
+ if (fkValue === null || fkValue === undefined) continue;
74
+ const key = toKey(fkValue);
75
+ const bucket = grouped.get(key) ?? [];
76
+ bucket.push(row);
77
+ grouped.set(key, bucket);
78
+ }
79
+
80
+ return grouped;
81
+ };
82
+
83
+ export const loadBelongsToRelation = async (
84
+ ctx: OrmContext,
85
+ rootTable: TableDef,
86
+ _relationName: string,
87
+ relation: BelongsToRelation
88
+ ): Promise<Map<string, Record<string, any>>> => {
89
+ const roots = ctx.getEntitiesForTable(rootTable);
90
+ const foreignKeys = new Set<unknown>();
91
+
92
+ for (const tracked of roots) {
93
+ const value = tracked.entity[relation.foreignKey];
94
+ if (value !== null && value !== undefined) {
95
+ foreignKeys.add(value);
96
+ }
97
+ }
98
+
99
+ if (!foreignKeys.size) {
100
+ return new Map();
101
+ }
102
+
103
+ const selectMap = selectAllColumns(relation.target);
104
+ const qb = new SelectQueryBuilder(relation.target).select(selectMap);
105
+ const targetKey = relation.localKey || findPrimaryKey(relation.target);
106
+ const pkColumn = relation.target.columns[targetKey];
107
+ if (!pkColumn) return new Map();
108
+
109
+ qb.where(inList(pkColumn, Array.from(foreignKeys) as (string | number | LiteralNode)[]));
110
+ const rows = await executeQuery(ctx, qb);
111
+ const map = new Map<string, Record<string, any>>();
112
+
113
+ for (const row of rows) {
114
+ const keyValue = row[targetKey];
115
+ if (keyValue === null || keyValue === undefined) continue;
116
+ map.set(toKey(keyValue), row);
117
+ }
118
+
119
+ return map;
120
+ };
121
+
122
+ export const loadBelongsToManyRelation = async (
123
+ ctx: OrmContext,
124
+ rootTable: TableDef,
125
+ _relationName: string,
126
+ relation: BelongsToManyRelation
127
+ ): Promise<Map<string, Rows>> => {
128
+ const rootKey = relation.localKey || findPrimaryKey(rootTable);
129
+ const roots = ctx.getEntitiesForTable(rootTable);
130
+ const rootIds = new Set<unknown>();
131
+
132
+ for (const tracked of roots) {
133
+ const value = tracked.entity[rootKey];
134
+ if (value !== null && value !== undefined) {
135
+ rootIds.add(value);
136
+ }
137
+ }
138
+
139
+ if (!rootIds.size) {
140
+ return new Map();
141
+ }
142
+
143
+ const pivotSelect = selectAllColumns(relation.pivotTable);
144
+ const pivotQb = new SelectQueryBuilder(relation.pivotTable).select(pivotSelect);
145
+ const pivotFkCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
146
+ if (!pivotFkCol) return new Map();
147
+
148
+ pivotQb.where(inList(pivotFkCol, Array.from(rootIds) as (string | number | LiteralNode)[]));
149
+ const pivotRows = await executeQuery(ctx, pivotQb);
150
+
151
+ const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, any> }[]>();
152
+ const targetIds = new Set<unknown>();
153
+
154
+ for (const pivot of pivotRows) {
155
+ const rootValue = pivot[relation.pivotForeignKeyToRoot];
156
+ const targetValue = pivot[relation.pivotForeignKeyToTarget];
157
+ if (rootValue === null || rootValue === undefined || targetValue === null || targetValue === undefined) {
158
+ continue;
159
+ }
160
+ const bucket = rootLookup.get(toKey(rootValue)) ?? [];
161
+ bucket.push({
162
+ targetId: targetValue,
163
+ pivot: { ...pivot }
164
+ });
165
+ rootLookup.set(toKey(rootValue), bucket);
166
+ targetIds.add(targetValue);
167
+ }
168
+
169
+ if (!targetIds.size) {
170
+ return new Map();
171
+ }
172
+
173
+ const targetSelect = selectAllColumns(relation.target);
174
+ const targetKey = relation.targetKey || findPrimaryKey(relation.target);
175
+ const targetPkColumn = relation.target.columns[targetKey];
176
+ if (!targetPkColumn) return new Map();
177
+
178
+ const targetQb = new SelectQueryBuilder(relation.target).select(targetSelect);
179
+ targetQb.where(inList(targetPkColumn, Array.from(targetIds) as (string | number | LiteralNode)[]));
180
+ const targetRows = await executeQuery(ctx, targetQb);
181
+ const targetMap = new Map<string, Record<string, any>>();
182
+
183
+ for (const row of targetRows) {
184
+ const pkValue = row[targetKey];
185
+ if (pkValue === null || pkValue === undefined) continue;
186
+ targetMap.set(toKey(pkValue), row);
187
+ }
188
+
189
+ const result = new Map<string, Rows>();
190
+
191
+ for (const [rootId, entries] of rootLookup.entries()) {
192
+ const bucket: Rows = [];
193
+ for (const entry of entries) {
194
+ const targetRow = targetMap.get(toKey(entry.targetId));
195
+ if (!targetRow) continue;
196
+ bucket.push({
197
+ ...targetRow,
198
+ _pivot: entry.pivot
199
+ });
200
+ }
201
+ result.set(rootId, bucket);
202
+ }
203
+
204
+ return result;
205
+ };