metal-orm 1.0.43 → 1.0.44

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 (84) hide show
  1. package/README.md +173 -30
  2. package/dist/index.cjs +896 -476
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1146 -275
  5. package/dist/index.d.ts +1146 -275
  6. package/dist/index.js +896 -474
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ast/adapters.ts +8 -2
  10. package/src/core/ast/builders.ts +105 -81
  11. package/src/core/ast/expression-builders.ts +430 -390
  12. package/src/core/ast/expression-visitor.ts +47 -8
  13. package/src/core/ast/helpers.ts +23 -0
  14. package/src/core/ast/join-node.ts +17 -1
  15. package/src/core/ddl/dialects/base-schema-dialect.ts +7 -1
  16. package/src/core/ddl/dialects/index.ts +1 -0
  17. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -0
  18. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -0
  19. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -0
  20. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -0
  21. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  22. package/src/core/ddl/introspect/catalogs/postgres.ts +2 -0
  23. package/src/core/ddl/introspect/context.ts +6 -0
  24. package/src/core/ddl/introspect/functions/postgres.ts +13 -0
  25. package/src/core/ddl/introspect/mssql.ts +11 -0
  26. package/src/core/ddl/introspect/mysql.ts +2 -0
  27. package/src/core/ddl/introspect/postgres.ts +14 -0
  28. package/src/core/ddl/introspect/registry.ts +14 -0
  29. package/src/core/ddl/introspect/run-select.ts +13 -0
  30. package/src/core/ddl/introspect/sqlite.ts +22 -0
  31. package/src/core/ddl/introspect/utils.ts +18 -0
  32. package/src/core/ddl/naming-strategy.ts +6 -0
  33. package/src/core/ddl/schema-dialect.ts +19 -6
  34. package/src/core/ddl/schema-diff.ts +22 -0
  35. package/src/core/ddl/schema-generator.ts +22 -0
  36. package/src/core/ddl/schema-plan-executor.ts +6 -0
  37. package/src/core/ddl/schema-types.ts +6 -0
  38. package/src/core/dialect/abstract.ts +2 -2
  39. package/src/core/execution/pooling/pool.ts +12 -7
  40. package/src/core/functions/datetime.ts +57 -33
  41. package/src/core/functions/numeric.ts +95 -30
  42. package/src/core/functions/standard-strategy.ts +35 -0
  43. package/src/core/functions/text.ts +83 -22
  44. package/src/core/functions/types.ts +23 -8
  45. package/src/decorators/bootstrap.ts +16 -4
  46. package/src/decorators/column.ts +17 -0
  47. package/src/decorators/decorator-metadata.ts +27 -0
  48. package/src/decorators/entity.ts +8 -0
  49. package/src/decorators/index.ts +3 -0
  50. package/src/decorators/relations.ts +32 -0
  51. package/src/orm/als.ts +34 -9
  52. package/src/orm/entity-context.ts +54 -0
  53. package/src/orm/entity-metadata.ts +122 -9
  54. package/src/orm/execute.ts +15 -0
  55. package/src/orm/lazy-batch.ts +68 -98
  56. package/src/orm/relations/has-many.ts +44 -0
  57. package/src/query/index.ts +74 -0
  58. package/src/query/target.ts +46 -0
  59. package/src/query-builder/delete-query-state.ts +30 -0
  60. package/src/query-builder/delete.ts +64 -19
  61. package/src/query-builder/hydration-manager.ts +46 -0
  62. package/src/query-builder/insert-query-state.ts +30 -0
  63. package/src/query-builder/insert.ts +46 -2
  64. package/src/query-builder/query-ast-service.ts +5 -0
  65. package/src/query-builder/query-resolution.ts +78 -0
  66. package/src/query-builder/raw-column-parser.ts +5 -0
  67. package/src/query-builder/relation-alias.ts +7 -0
  68. package/src/query-builder/relation-conditions.ts +61 -48
  69. package/src/query-builder/relation-service.ts +68 -63
  70. package/src/query-builder/relation-utils.ts +3 -0
  71. package/src/query-builder/select/cte-facet.ts +40 -0
  72. package/src/query-builder/select/from-facet.ts +80 -0
  73. package/src/query-builder/select/join-facet.ts +62 -0
  74. package/src/query-builder/select/predicate-facet.ts +103 -0
  75. package/src/query-builder/select/projection-facet.ts +69 -0
  76. package/src/query-builder/select/relation-facet.ts +81 -0
  77. package/src/query-builder/select/setop-facet.ts +36 -0
  78. package/src/query-builder/select-helpers.ts +13 -0
  79. package/src/query-builder/select-query-builder-deps.ts +19 -1
  80. package/src/query-builder/select-query-state.ts +2 -1
  81. package/src/query-builder/select.ts +795 -1163
  82. package/src/query-builder/update-query-state.ts +52 -0
  83. package/src/query-builder/update.ts +69 -19
  84. package/src/schema/table-guards.ts +31 -0
@@ -4,21 +4,75 @@ import { TableDef } from '../schema/table.js';
4
4
  import { RelationDef } from '../schema/relation.js';
5
5
  import { RelationChange, RelationKey, TrackedEntity } from './runtime-types.js';
6
6
 
7
+ /**
8
+ * Interface for entity context providing entity tracking and management.
9
+ */
7
10
  export interface EntityContext {
11
+ /** The database dialect */
8
12
  dialect: Dialect;
13
+ /** The database executor */
9
14
  executor: DbExecutor;
10
15
 
16
+ /**
17
+ * Gets an entity by table and primary key.
18
+ * @param table - The table definition
19
+ * @param pk - The primary key value
20
+ * @returns The entity or undefined
21
+ */
11
22
  getEntity(table: TableDef, pk: unknown): unknown;
23
+
24
+ /**
25
+ * Sets an entity in the context.
26
+ * @param table - The table definition
27
+ * @param pk - The primary key value
28
+ * @param entity - The entity to set
29
+ */
12
30
  setEntity(table: TableDef, pk: unknown, entity: unknown): void;
13
31
 
32
+ /**
33
+ * Tracks a new entity.
34
+ * @param table - The table definition
35
+ * @param entity - The new entity
36
+ * @param pk - Optional primary key
37
+ */
14
38
  trackNew(table: TableDef, entity: unknown, pk?: unknown): void;
39
+
40
+ /**
41
+ * Tracks a managed entity.
42
+ * @param table - The table definition
43
+ * @param pk - The primary key
44
+ * @param entity - The managed entity
45
+ */
15
46
  trackManaged(table: TableDef, pk: unknown, entity: unknown): void;
16
47
 
48
+ /**
49
+ * Marks an entity as dirty.
50
+ * @param entity - The entity to mark
51
+ */
17
52
  markDirty(entity: unknown): void;
53
+
54
+ /**
55
+ * Marks an entity as removed.
56
+ * @param entity - The entity to mark
57
+ */
18
58
  markRemoved(entity: unknown): void;
19
59
 
60
+ /**
61
+ * Gets all tracked entities for a table.
62
+ * @param table - The table definition
63
+ * @returns Array of tracked entities
64
+ */
20
65
  getEntitiesForTable(table: TableDef): TrackedEntity[];
21
66
 
67
+ /**
68
+ * Registers a relation change.
69
+ * @param root - The root entity
70
+ * @param relationKey - The relation key
71
+ * @param rootTable - The root table definition
72
+ * @param relationName - The relation name
73
+ * @param relation - The relation definition
74
+ * @param change - The relation change
75
+ */
22
76
  registerRelationChange(
23
77
  root: unknown,
24
78
  relationKey: RelationKey,
@@ -2,13 +2,33 @@ import { ColumnDef } from '../schema/column.js';
2
2
  import { defineTable, TableDef, TableHooks } from '../schema/table.js';
3
3
  import { CascadeMode, RelationKinds } from '../schema/relation.js';
4
4
 
5
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
- export type EntityConstructor<T = object> = new (...args: any[]) => T;
5
+ /**
6
+ * Constructor type for entities.
7
+ * Supports any constructor signature for maximum flexibility with decorator-based entities.
8
+ * @template T - The entity type
9
+ */
10
+ export type EntityConstructor<T = object> = new (...args: never[]) => T;
11
+
12
+ /**
13
+ * Target that can be an entity constructor or table definition.
14
+ */
7
15
  export type EntityOrTableTarget = EntityConstructor | TableDef;
16
+
17
+ /**
18
+ * Resolver for entity or table target, can be direct or function.
19
+ */
8
20
  export type EntityOrTableTargetResolver = EntityOrTableTarget | (() => EntityOrTableTarget);
9
21
 
22
+ /**
23
+ * Simplified column definition structure used during metadata registration.
24
+ * @template T - Concrete column definition type being extended
25
+ */
10
26
  export type ColumnDefLike<T extends ColumnDef = ColumnDef> = Omit<T, 'name' | 'table'>;
11
27
 
28
+ /**
29
+ * Transforms simplified column metadata into full ColumnDef objects during table building.
30
+ * @template TColumns - Mapping of column names to simplified definitions
31
+ */
12
32
  type MaterializeColumns<TColumns extends Record<string, ColumnDefLike>> = {
13
33
  [K in keyof TColumns]: ColumnDef<TColumns[K]['type'], TColumns[K]['tsType']> & Omit<
14
34
  TColumns[K],
@@ -16,62 +36,119 @@ type MaterializeColumns<TColumns extends Record<string, ColumnDefLike>> = {
16
36
  > & { name: string; table: string };
17
37
  };
18
38
 
39
+ /**
40
+ * Common properties shared by all relation metadata types.
41
+ */
19
42
  interface BaseRelationMetadata {
43
+ /** The property key for the relation */
20
44
  propertyKey: string;
45
+ /** The target entity or table */
21
46
  target: EntityOrTableTargetResolver;
47
+ /** Optional cascade mode */
22
48
  cascade?: CascadeMode;
23
49
  }
24
50
 
51
+ /**
52
+ * Metadata for has many relations.
53
+ */
25
54
  export interface HasManyRelationMetadata extends BaseRelationMetadata {
55
+ /** The relation kind */
26
56
  kind: typeof RelationKinds.HasMany;
57
+ /** The foreign key */
27
58
  foreignKey: string;
59
+ /** Optional local key */
28
60
  localKey?: string;
29
61
  }
30
62
 
63
+ /**
64
+ * Metadata for has one relations.
65
+ */
31
66
  export interface HasOneRelationMetadata extends BaseRelationMetadata {
67
+ /** The relation kind */
32
68
  kind: typeof RelationKinds.HasOne;
69
+ /** The foreign key */
33
70
  foreignKey: string;
71
+ /** Optional local key */
34
72
  localKey?: string;
35
73
  }
36
74
 
75
+ /**
76
+ * Metadata for belongs to relations.
77
+ */
37
78
  export interface BelongsToRelationMetadata extends BaseRelationMetadata {
79
+ /** The relation kind */
38
80
  kind: typeof RelationKinds.BelongsTo;
81
+ /** The foreign key */
39
82
  foreignKey: string;
83
+ /** Optional local key */
40
84
  localKey?: string;
41
85
  }
42
86
 
87
+ /**
88
+ * Metadata for belongs to many relations.
89
+ */
43
90
  export interface BelongsToManyRelationMetadata extends BaseRelationMetadata {
91
+ /** The relation kind */
44
92
  kind: typeof RelationKinds.BelongsToMany;
93
+ /** The pivot table */
45
94
  pivotTable: EntityOrTableTargetResolver;
95
+ /** The pivot foreign key to root */
46
96
  pivotForeignKeyToRoot: string;
97
+ /** The pivot foreign key to target */
47
98
  pivotForeignKeyToTarget: string;
99
+ /** Optional local key */
48
100
  localKey?: string;
101
+ /** Optional target key */
49
102
  targetKey?: string;
103
+ /** Optional pivot primary key */
50
104
  pivotPrimaryKey?: string;
105
+ /** Optional default pivot columns */
51
106
  defaultPivotColumns?: string[];
52
107
  }
53
108
 
109
+ /**
110
+ * Union type for all relation metadata.
111
+ */
54
112
  export type RelationMetadata =
55
113
  | HasManyRelationMetadata
56
114
  | HasOneRelationMetadata
57
115
  | BelongsToRelationMetadata
58
116
  | BelongsToManyRelationMetadata;
59
117
 
118
+ /**
119
+ * Metadata for entities.
120
+ * @template TColumns - The columns type
121
+ */
60
122
  export interface EntityMetadata<TColumns extends Record<string, ColumnDefLike> = Record<string, ColumnDefLike>> {
123
+ /** The entity constructor */
61
124
  target: EntityConstructor;
125
+ /** The table name */
62
126
  tableName: string;
127
+ /** The columns */
63
128
  columns: TColumns;
129
+ /** The relations */
64
130
  relations: Record<string, RelationMetadata>;
131
+ /** Optional hooks */
65
132
  hooks?: TableHooks;
133
+ /** Optional table definition */
66
134
  table?: TableDef<MaterializeColumns<TColumns>>;
67
135
  }
68
136
 
69
137
  const metadataMap = new Map<EntityConstructor, EntityMetadata>();
70
138
 
139
+ /**
140
+ * Registers entity metadata.
141
+ * @param meta - The entity metadata to register
142
+ */
71
143
  export const registerEntityMetadata = (meta: EntityMetadata): void => {
72
144
  metadataMap.set(meta.target, meta);
73
145
  };
74
146
 
147
+ /**
148
+ * Ensures entity metadata exists for the target, creating it if necessary.
149
+ * @param target - The entity constructor
150
+ * @returns The entity metadata
151
+ */
75
152
  export const ensureEntityMetadata = (target: EntityConstructor): EntityMetadata => {
76
153
  let meta = metadataMap.get(target);
77
154
  if (!meta) {
@@ -86,18 +163,36 @@ export const ensureEntityMetadata = (target: EntityConstructor): EntityMetadata
86
163
  return meta;
87
164
  };
88
165
 
166
+ /**
167
+ * Gets entity metadata for the target.
168
+ * @param target - The entity constructor
169
+ * @returns The entity metadata or undefined if not found
170
+ */
89
171
  export const getEntityMetadata = (target: EntityConstructor): EntityMetadata | undefined => {
90
172
  return metadataMap.get(target);
91
173
  };
92
174
 
175
+ /**
176
+ * Gets all entity metadata.
177
+ * @returns Array of all entity metadata
178
+ */
93
179
  export const getAllEntityMetadata = (): EntityMetadata[] => {
94
180
  return Array.from(metadataMap.values());
95
181
  };
96
182
 
183
+ /**
184
+ * Clears all entity metadata.
185
+ */
97
186
  export const clearEntityMetadata = (): void => {
98
187
  metadataMap.clear();
99
188
  };
100
189
 
190
+ /**
191
+ * Adds column metadata to an entity.
192
+ * @param target - The entity constructor
193
+ * @param propertyKey - The property key
194
+ * @param column - The column definition
195
+ */
101
196
  export const addColumnMetadata = (
102
197
  target: EntityConstructor,
103
198
  propertyKey: string,
@@ -107,6 +202,12 @@ export const addColumnMetadata = (
107
202
  (meta.columns as Record<string, ColumnDefLike>)[propertyKey] = { ...column };
108
203
  };
109
204
 
205
+ /**
206
+ * Adds relation metadata to an entity.
207
+ * @param target - The entity constructor
208
+ * @param propertyKey - The property key
209
+ * @param relation - The relation metadata
210
+ */
110
211
  export const addRelationMetadata = (
111
212
  target: EntityConstructor,
112
213
  propertyKey: string,
@@ -116,6 +217,12 @@ export const addRelationMetadata = (
116
217
  meta.relations[propertyKey] = relation;
117
218
  };
118
219
 
220
+ /**
221
+ * Sets the table name and hooks for an entity.
222
+ * @param target - The entity constructor
223
+ * @param tableName - The table name
224
+ * @param hooks - Optional table hooks
225
+ */
119
226
  export const setEntityTableName = (
120
227
  target: EntityConstructor,
121
228
  tableName: string,
@@ -130,22 +237,28 @@ export const setEntityTableName = (
130
237
  }
131
238
  };
132
239
 
240
+ /**
241
+ * Builds a table definition from entity metadata.
242
+ * @template TColumns - The columns type
243
+ * @param meta - The entity metadata
244
+ * @returns The table definition
245
+ */
133
246
  export const buildTableDef = <TColumns extends Record<string, ColumnDefLike>>(meta: EntityMetadata<TColumns>): TableDef<MaterializeColumns<TColumns>> => {
134
247
  if (meta.table) {
135
248
  return meta.table;
136
249
  }
137
250
 
138
- const columns = Object.entries(meta.columns).reduce<MaterializeColumns<TColumns>>((acc, [key, def]) => {
139
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
- (acc as any)[key] = {
251
+ // Build columns using a simpler approach that avoids type assertion
252
+ const columns: Record<string, ColumnDef> = {};
253
+ for (const [key, def] of Object.entries(meta.columns)) {
254
+ columns[key] = {
141
255
  ...def,
142
256
  name: key,
143
257
  table: meta.tableName
144
- };
145
- return acc;
146
- }, {} as MaterializeColumns<TColumns>);
258
+ } as ColumnDef;
259
+ }
147
260
 
148
- const table = defineTable(meta.tableName, columns, {}, meta.hooks);
261
+ const table = defineTable(meta.tableName, columns as MaterializeColumns<TColumns>, {}, meta.hooks);
149
262
  meta.table = table;
150
263
  return table;
151
264
  };
@@ -42,6 +42,13 @@ const executeWithEntityContext = async <TTable extends TableDef>(
42
42
  return hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
43
43
  };
44
44
 
45
+ /**
46
+ * Executes a hydrated query using the ORM session.
47
+ * @template TTable - The table type
48
+ * @param session - The ORM session
49
+ * @param qb - The select query builder
50
+ * @returns Promise resolving to array of entity instances
51
+ */
45
52
  export async function executeHydrated<TTable extends TableDef>(
46
53
  session: OrmSession,
47
54
  qb: SelectQueryBuilder<unknown, TTable>
@@ -49,6 +56,14 @@ export async function executeHydrated<TTable extends TableDef>(
49
56
  return executeWithEntityContext(session, qb);
50
57
  }
51
58
 
59
+ /**
60
+ * Executes a hydrated query using execution and hydration contexts.
61
+ * @template TTable - The table type
62
+ * @param _execCtx - The execution context (unused)
63
+ * @param hydCtx - The hydration context
64
+ * @param qb - The select query builder
65
+ * @returns Promise resolving to array of entity instances
66
+ */
52
67
  export async function executeHydratedWithContexts<TTable extends TableDef>(
53
68
  _execCtx: ExecutionContext,
54
69
  hydCtx: HydrationContext,
@@ -9,6 +9,8 @@ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
9
9
 
10
10
  type Rows = Record<string, unknown>[];
11
11
 
12
+ type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
13
+
12
14
  const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
13
15
  Object.entries(table.columns).reduce((acc, [name, def]) => {
14
16
  acc[name] = def;
@@ -38,6 +40,57 @@ const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown,
38
40
 
39
41
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
40
42
 
43
+ const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
44
+ const collected = new Set<unknown>();
45
+ for (const tracked of roots) {
46
+ const value = tracked.entity[key];
47
+ if (value !== null && value !== undefined) {
48
+ collected.add(value);
49
+ }
50
+ }
51
+ return collected;
52
+ };
53
+
54
+ const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
55
+ Array.from(keys) as (string | number | LiteralNode)[];
56
+
57
+ const fetchRowsForKeys = async (
58
+ ctx: EntityContext,
59
+ table: TableDef,
60
+ column: ColumnDef,
61
+ keys: Set<unknown>
62
+ ): Promise<Rows> => {
63
+ const qb = new SelectQueryBuilder(table).select(selectAllColumns(table));
64
+ qb.where(inList(column, buildInListValues(keys)));
65
+ return executeQuery(ctx, qb);
66
+ };
67
+
68
+ const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
69
+ const grouped = new Map<string, Rows>();
70
+ for (const row of rows) {
71
+ const value = row[keyColumn];
72
+ if (value === null || value === undefined) continue;
73
+ const key = toKey(value);
74
+ const bucket = grouped.get(key) ?? [];
75
+ bucket.push(row);
76
+ grouped.set(key, bucket);
77
+ }
78
+ return grouped;
79
+ };
80
+
81
+ const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
82
+ const lookup = new Map<string, Record<string, unknown>>();
83
+ for (const row of rows) {
84
+ const value = row[keyColumn];
85
+ if (value === null || value === undefined) continue;
86
+ const key = toKey(value);
87
+ if (!lookup.has(key)) {
88
+ lookup.set(key, row);
89
+ }
90
+ }
91
+ return lookup;
92
+ };
93
+
41
94
  export const loadHasManyRelation = async (
42
95
  ctx: EntityContext,
43
96
  rootTable: TableDef,
@@ -46,39 +99,17 @@ export const loadHasManyRelation = async (
46
99
  ): Promise<Map<string, Rows>> => {
47
100
  const localKey = relation.localKey || findPrimaryKey(rootTable);
48
101
  const roots = ctx.getEntitiesForTable(rootTable);
49
- const keys = new Set<unknown>();
50
-
51
- for (const tracked of roots) {
52
- const value = tracked.entity[localKey];
53
- if (value !== null && value !== undefined) {
54
- keys.add(value);
55
- }
56
- }
102
+ const keys = collectKeysFromRoots(roots, localKey);
57
103
 
58
104
  if (!keys.size) {
59
105
  return new Map();
60
106
  }
61
107
 
62
- const selectMap = selectAllColumns(relation.target);
63
- const fb = new SelectQueryBuilder(relation.target).select(selectMap);
64
108
  const fkColumn = relation.target.columns[relation.foreignKey];
65
109
  if (!fkColumn) return new Map();
66
110
 
67
- fb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
68
-
69
- const rows = await executeQuery(ctx, fb);
70
- const grouped = new Map<string, Rows>();
71
-
72
- for (const row of rows) {
73
- const fkValue = row[relation.foreignKey];
74
- if (fkValue === null || fkValue === undefined) continue;
75
- const key = toKey(fkValue);
76
- const bucket = grouped.get(key) ?? [];
77
- bucket.push(row);
78
- grouped.set(key, bucket);
79
- }
80
-
81
- return grouped;
111
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
112
+ return groupRowsByMany(rows, relation.foreignKey);
82
113
  };
83
114
 
84
115
  export const loadHasOneRelation = async (
@@ -89,39 +120,17 @@ export const loadHasOneRelation = async (
89
120
  ): Promise<Map<string, Record<string, unknown>>> => {
90
121
  const localKey = relation.localKey || findPrimaryKey(rootTable);
91
122
  const roots = ctx.getEntitiesForTable(rootTable);
92
- const keys = new Set<unknown>();
93
-
94
- for (const tracked of roots) {
95
- const value = tracked.entity[localKey];
96
- if (value !== null && value !== undefined) {
97
- keys.add(value);
98
- }
99
- }
123
+ const keys = collectKeysFromRoots(roots, localKey);
100
124
 
101
125
  if (!keys.size) {
102
126
  return new Map();
103
127
  }
104
128
 
105
- const selectMap = selectAllColumns(relation.target);
106
- const qb = new SelectQueryBuilder(relation.target).select(selectMap);
107
129
  const fkColumn = relation.target.columns[relation.foreignKey];
108
130
  if (!fkColumn) return new Map();
109
131
 
110
- qb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
111
-
112
- const rows = await executeQuery(ctx, qb);
113
- const lookup = new Map<string, Record<string, unknown>>();
114
-
115
- for (const row of rows) {
116
- const fkValue = row[relation.foreignKey];
117
- if (fkValue === null || fkValue === undefined) continue;
118
- const key = toKey(fkValue);
119
- if (!lookup.has(key)) {
120
- lookup.set(key, row);
121
- }
122
- }
123
-
124
- return lookup;
132
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
133
+ return groupRowsByUnique(rows, relation.foreignKey);
125
134
  };
126
135
 
127
136
  export const loadBelongsToRelation = async (
@@ -131,36 +140,18 @@ export const loadBelongsToRelation = async (
131
140
  relation: BelongsToRelation
132
141
  ): Promise<Map<string, Record<string, unknown>>> => {
133
142
  const roots = ctx.getEntitiesForTable(rootTable);
134
- const foreignKeys = new Set<unknown>();
135
-
136
- for (const tracked of roots) {
137
- const value = tracked.entity[relation.foreignKey];
138
- if (value !== null && value !== undefined) {
139
- foreignKeys.add(value);
140
- }
141
- }
143
+ const foreignKeys = collectKeysFromRoots(roots, relation.foreignKey);
142
144
 
143
145
  if (!foreignKeys.size) {
144
146
  return new Map();
145
147
  }
146
148
 
147
- const selectMap = selectAllColumns(relation.target);
148
- const qb = new SelectQueryBuilder(relation.target).select(selectMap);
149
149
  const targetKey = relation.localKey || findPrimaryKey(relation.target);
150
150
  const pkColumn = relation.target.columns[targetKey];
151
151
  if (!pkColumn) return new Map();
152
152
 
153
- qb.where(inList(pkColumn, Array.from(foreignKeys) as (string | number | LiteralNode)[]));
154
- const rows = await executeQuery(ctx, qb);
155
- const map = new Map<string, Record<string, unknown>>();
156
-
157
- for (const row of rows) {
158
- const keyValue = row[targetKey];
159
- if (keyValue === null || keyValue === undefined) continue;
160
- map.set(toKey(keyValue), row);
161
- }
162
-
163
- return map;
153
+ const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys);
154
+ return groupRowsByUnique(rows, targetKey);
164
155
  };
165
156
 
166
157
  export const loadBelongsToManyRelation = async (
@@ -171,27 +162,16 @@ export const loadBelongsToManyRelation = async (
171
162
  ): Promise<Map<string, Rows>> => {
172
163
  const rootKey = relation.localKey || findPrimaryKey(rootTable);
173
164
  const roots = ctx.getEntitiesForTable(rootTable);
174
- const rootIds = new Set<unknown>();
175
-
176
- for (const tracked of roots) {
177
- const value = tracked.entity[rootKey];
178
- if (value !== null && value !== undefined) {
179
- rootIds.add(value);
180
- }
181
- }
165
+ const rootIds = collectKeysFromRoots(roots, rootKey);
182
166
 
183
167
  if (!rootIds.size) {
184
168
  return new Map();
185
169
  }
186
170
 
187
- const pivotSelect = selectAllColumns(relation.pivotTable);
188
- const pivotQb = new SelectQueryBuilder(relation.pivotTable).select(pivotSelect);
189
- const pivotFkCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
190
- if (!pivotFkCol) return new Map();
191
-
192
- pivotQb.where(inList(pivotFkCol, Array.from(rootIds) as (string | number | LiteralNode)[]));
193
- const pivotRows = await executeQuery(ctx, pivotQb);
171
+ const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
172
+ if (!pivotColumn) return new Map();
194
173
 
174
+ const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds);
195
175
  const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
196
176
  const targetIds = new Set<unknown>();
197
177
 
@@ -214,22 +194,12 @@ export const loadBelongsToManyRelation = async (
214
194
  return new Map();
215
195
  }
216
196
 
217
- const targetSelect = selectAllColumns(relation.target);
218
197
  const targetKey = relation.targetKey || findPrimaryKey(relation.target);
219
198
  const targetPkColumn = relation.target.columns[targetKey];
220
199
  if (!targetPkColumn) return new Map();
221
200
 
222
- const targetQb = new SelectQueryBuilder(relation.target).select(targetSelect);
223
- targetQb.where(inList(targetPkColumn, Array.from(targetIds) as (string | number | LiteralNode)[]));
224
- const targetRows = await executeQuery(ctx, targetQb);
225
- const targetMap = new Map<string, Record<string, unknown>>();
226
-
227
- for (const row of targetRows) {
228
- const pkValue = row[targetKey];
229
- if (pkValue === null || pkValue === undefined) continue;
230
- targetMap.set(toKey(pkValue), row);
231
- }
232
-
201
+ const targetRows = await fetchRowsForKeys(ctx, relation.target, targetPkColumn, targetIds);
202
+ const targetMap = groupRowsByUnique(targetRows, targetKey);
233
203
  const result = new Map<string, Rows>();
234
204
 
235
205
  for (const [rootId, entries] of rootLookup.entries()) {