metal-orm 1.0.14 → 1.0.16

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 (129) hide show
  1. package/README.md +69 -67
  2. package/dist/decorators/index.cjs +1983 -224
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +6 -6
  5. package/dist/decorators/index.d.ts +6 -6
  6. package/dist/decorators/index.js +1982 -224
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +5284 -3751
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +524 -169
  11. package/dist/index.d.ts +524 -169
  12. package/dist/index.js +5197 -3736
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-CCp1oz9p.d.cts → select-BKZrMRCQ.d.cts} +555 -94
  15. package/dist/{select-CCp1oz9p.d.ts → select-BKZrMRCQ.d.ts} +555 -94
  16. package/package.json +1 -1
  17. package/src/codegen/naming-strategy.ts +64 -0
  18. package/src/codegen/typescript.ts +19 -21
  19. package/src/core/ast/adapters.ts +21 -0
  20. package/src/core/ast/aggregate-functions.ts +13 -13
  21. package/src/core/ast/builders.ts +56 -43
  22. package/src/core/ast/expression-builders.ts +34 -34
  23. package/src/core/ast/expression-nodes.ts +18 -16
  24. package/src/core/ast/expression-visitor.ts +122 -69
  25. package/src/core/ast/expression.ts +6 -4
  26. package/src/core/ast/join-metadata.ts +15 -0
  27. package/src/core/ast/join-node.ts +22 -20
  28. package/src/core/ast/join.ts +5 -5
  29. package/src/core/ast/query.ts +52 -88
  30. package/src/core/ast/types.ts +20 -0
  31. package/src/core/ast/window-functions.ts +55 -55
  32. package/src/core/ddl/dialects/base-schema-dialect.ts +20 -6
  33. package/src/core/ddl/dialects/mssql-schema-dialect.ts +32 -8
  34. package/src/core/ddl/dialects/mysql-schema-dialect.ts +21 -10
  35. package/src/core/ddl/dialects/postgres-schema-dialect.ts +52 -7
  36. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +23 -9
  37. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  38. package/src/core/ddl/introspect/catalogs/postgres.ts +143 -0
  39. package/src/core/ddl/introspect/context.ts +9 -0
  40. package/src/core/ddl/introspect/functions/postgres.ts +26 -0
  41. package/src/core/ddl/introspect/mssql.ts +149 -149
  42. package/src/core/ddl/introspect/mysql.ts +99 -99
  43. package/src/core/ddl/introspect/postgres.ts +245 -154
  44. package/src/core/ddl/introspect/registry.ts +26 -0
  45. package/src/core/ddl/introspect/run-select.ts +25 -0
  46. package/src/core/ddl/introspect/sqlite.ts +7 -7
  47. package/src/core/ddl/introspect/types.ts +23 -19
  48. package/src/core/ddl/introspect/utils.ts +1 -1
  49. package/src/core/ddl/naming-strategy.ts +10 -0
  50. package/src/core/ddl/schema-dialect.ts +41 -0
  51. package/src/core/ddl/schema-diff.ts +211 -179
  52. package/src/core/ddl/schema-generator.ts +17 -90
  53. package/src/core/ddl/schema-introspect.ts +25 -32
  54. package/src/core/ddl/schema-plan-executor.ts +17 -0
  55. package/src/core/ddl/schema-types.ts +46 -39
  56. package/src/core/ddl/sql-writing.ts +170 -0
  57. package/src/core/dialect/abstract.ts +172 -126
  58. package/src/core/dialect/base/cte-compiler.ts +33 -0
  59. package/src/core/dialect/base/function-table-formatter.ts +132 -0
  60. package/src/core/dialect/base/groupby-compiler.ts +21 -0
  61. package/src/core/dialect/base/join-compiler.ts +26 -0
  62. package/src/core/dialect/base/orderby-compiler.ts +21 -0
  63. package/src/core/dialect/base/pagination-strategy.ts +32 -0
  64. package/src/core/dialect/base/returning-strategy.ts +56 -0
  65. package/src/core/dialect/base/sql-dialect.ts +181 -204
  66. package/src/core/dialect/dialect-factory.ts +91 -0
  67. package/src/core/dialect/mssql/functions.ts +101 -0
  68. package/src/core/dialect/mssql/index.ts +128 -126
  69. package/src/core/dialect/mysql/functions.ts +101 -0
  70. package/src/core/dialect/mysql/index.ts +20 -18
  71. package/src/core/dialect/postgres/functions.ts +95 -0
  72. package/src/core/dialect/postgres/index.ts +30 -28
  73. package/src/core/dialect/sqlite/functions.ts +115 -0
  74. package/src/core/dialect/sqlite/index.ts +30 -28
  75. package/src/core/driver/database-driver.ts +11 -0
  76. package/src/core/driver/mssql-driver.ts +20 -0
  77. package/src/core/driver/mysql-driver.ts +20 -0
  78. package/src/core/driver/postgres-driver.ts +20 -0
  79. package/src/core/driver/sqlite-driver.ts +20 -0
  80. package/src/core/execution/db-executor.ts +63 -0
  81. package/src/core/execution/executors/mssql-executor.ts +39 -0
  82. package/src/core/execution/executors/mysql-executor.ts +47 -0
  83. package/src/core/execution/executors/postgres-executor.ts +32 -0
  84. package/src/core/execution/executors/sqlite-executor.ts +31 -0
  85. package/src/core/functions/datetime.ts +132 -0
  86. package/src/core/functions/numeric.ts +179 -0
  87. package/src/core/functions/standard-strategy.ts +47 -0
  88. package/src/core/functions/text.ts +147 -0
  89. package/src/core/functions/types.ts +18 -0
  90. package/src/core/hydration/types.ts +57 -0
  91. package/src/decorators/bootstrap.ts +10 -0
  92. package/src/decorators/column.ts +13 -4
  93. package/src/decorators/relations.ts +15 -0
  94. package/src/index.ts +37 -19
  95. package/src/orm/entity-context.ts +30 -0
  96. package/src/orm/entity-meta.ts +2 -2
  97. package/src/orm/entity-metadata.ts +8 -6
  98. package/src/orm/entity.ts +72 -41
  99. package/src/orm/execute.ts +42 -25
  100. package/src/orm/execution-context.ts +12 -0
  101. package/src/orm/hydration-context.ts +14 -0
  102. package/src/orm/hydration.ts +25 -17
  103. package/src/orm/identity-map.ts +4 -0
  104. package/src/orm/interceptor-pipeline.ts +29 -0
  105. package/src/orm/lazy-batch.ts +50 -6
  106. package/src/orm/orm-session.ts +234 -0
  107. package/src/orm/orm.ts +58 -0
  108. package/src/orm/query-logger.ts +1 -1
  109. package/src/orm/relation-change-processor.ts +48 -3
  110. package/src/orm/relations/belongs-to.ts +45 -44
  111. package/src/orm/relations/has-many.ts +44 -43
  112. package/src/orm/relations/has-one.ts +140 -0
  113. package/src/orm/relations/many-to-many.ts +46 -45
  114. package/src/orm/transaction-runner.ts +1 -1
  115. package/src/orm/unit-of-work.ts +66 -61
  116. package/src/query-builder/delete.ts +22 -5
  117. package/src/query-builder/hydration-manager.ts +2 -1
  118. package/src/query-builder/hydration-planner.ts +8 -7
  119. package/src/query-builder/insert.ts +22 -5
  120. package/src/query-builder/relation-conditions.ts +9 -8
  121. package/src/query-builder/relation-service.ts +3 -2
  122. package/src/query-builder/select.ts +575 -64
  123. package/src/query-builder/update.ts +22 -5
  124. package/src/schema/column.ts +246 -246
  125. package/src/schema/relation.ts +35 -1
  126. package/src/schema/table.ts +28 -28
  127. package/src/schema/types.ts +41 -31
  128. package/src/orm/db-executor.ts +0 -11
  129. package/src/orm/orm-context.ts +0 -159
package/src/orm/entity.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { Entity, RelationMap, HasManyCollection, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
3
- import { OrmContext } from './orm-context.js';
2
+ import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
3
+ import { EntityContext } from './entity-context.js';
4
4
  import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
5
5
  import { DefaultHasManyCollection } from './relations/has-many.js';
6
+ import { DefaultHasOneReference } from './relations/has-one.js';
6
7
  import { DefaultBelongsToReference } from './relations/belongs-to.js';
7
8
  import { DefaultManyToManyCollection } from './relations/many-to-many.js';
8
- import { HasManyRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
9
- import { loadHasManyRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
9
+ import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
10
+ import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
10
11
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
11
12
 
12
13
  type Rows = Record<string, any>[];
@@ -44,7 +45,7 @@ export const createEntityProxy = <
44
45
  TTable extends TableDef,
45
46
  TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
46
47
  >(
47
- ctx: OrmContext,
48
+ ctx: EntityContext,
48
49
  table: TTable,
49
50
  row: Record<string, any>,
50
51
  lazyRelations: TLazy[] = [] as TLazy[]
@@ -104,7 +105,7 @@ export const createEntityProxy = <
104
105
  };
105
106
 
106
107
  export const createEntityFromRow = <TTable extends TableDef>(
107
- ctx: OrmContext,
108
+ ctx: EntityContext,
108
109
  table: TTable,
109
110
  row: Record<string, any>,
110
111
  lazyRelations: (keyof RelationMap<TTable>)[] = []
@@ -128,48 +129,60 @@ export const createEntityFromRow = <TTable extends TableDef>(
128
129
 
129
130
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
130
131
 
131
- const populateHydrationCache = <TTable extends TableDef>(
132
- entity: any,
133
- row: Record<string, any>,
134
- meta: EntityMeta<TTable>
135
- ): void => {
136
- for (const relationName of Object.keys(meta.table.relations)) {
137
- const relation = meta.table.relations[relationName];
138
- const data = row[relationName];
139
- if (!Array.isArray(data)) continue;
140
-
141
- if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
142
- const localKey = relation.localKey || findPrimaryKey(meta.table);
143
- const rootValue = entity[localKey];
144
- if (rootValue === undefined || rootValue === null) continue;
145
- const cache = new Map<string, Rows>();
146
- cache.set(toKey(rootValue), data as Rows);
132
+ const populateHydrationCache = <TTable extends TableDef>(
133
+ entity: any,
134
+ row: Record<string, any>,
135
+ meta: EntityMeta<TTable>
136
+ ): void => {
137
+ for (const relationName of Object.keys(meta.table.relations)) {
138
+ const relation = meta.table.relations[relationName];
139
+ const data = row[relationName];
140
+ if (relation.type === RelationKinds.HasOne) {
141
+ const localKey = relation.localKey || findPrimaryKey(meta.table);
142
+ const rootValue = entity[localKey];
143
+ if (rootValue === undefined || rootValue === null) continue;
144
+ if (!data || typeof data !== 'object') continue;
145
+ const cache = new Map<string, Record<string, any>>();
146
+ cache.set(toKey(rootValue), data as Record<string, any>);
147
+ meta.relationHydration.set(relationName, cache);
148
+ meta.relationCache.set(relationName, Promise.resolve(cache));
149
+ continue;
150
+ }
151
+
152
+ if (!Array.isArray(data)) continue;
153
+
154
+ if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
155
+ const localKey = relation.localKey || findPrimaryKey(meta.table);
156
+ const rootValue = entity[localKey];
157
+ if (rootValue === undefined || rootValue === null) continue;
158
+ const cache = new Map<string, Rows>();
159
+ cache.set(toKey(rootValue), data as Rows);
160
+ meta.relationHydration.set(relationName, cache);
161
+ meta.relationCache.set(relationName, Promise.resolve(cache));
162
+ continue;
163
+ }
164
+
165
+ if (relation.type === RelationKinds.BelongsTo) {
166
+ const targetKey = relation.localKey || findPrimaryKey(relation.target);
167
+ const cache = new Map<string, Record<string, any>>();
168
+ for (const item of data) {
169
+ const pkValue = item[targetKey];
170
+ if (pkValue === undefined || pkValue === null) continue;
171
+ cache.set(toKey(pkValue), item);
172
+ }
173
+ if (cache.size) {
147
174
  meta.relationHydration.set(relationName, cache);
148
175
  meta.relationCache.set(relationName, Promise.resolve(cache));
149
- continue;
150
- }
151
-
152
- if (relation.type === RelationKinds.BelongsTo) {
153
- const targetKey = relation.localKey || findPrimaryKey(relation.target);
154
- const cache = new Map<string, Record<string, any>>();
155
- for (const item of data) {
156
- const pkValue = item[targetKey];
157
- if (pkValue === undefined || pkValue === null) continue;
158
- cache.set(toKey(pkValue), item);
159
- }
160
- if (cache.size) {
161
- meta.relationHydration.set(relationName, cache);
162
- meta.relationCache.set(relationName, Promise.resolve(cache));
163
- }
164
176
  }
165
177
  }
166
- };
178
+ }
179
+ };
167
180
 
168
181
  const getRelationWrapper = (
169
182
  meta: EntityMeta<any>,
170
183
  relationName: string,
171
184
  owner: any
172
- ): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
185
+ ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
173
186
  if (meta.relationWrappers.has(relationName)) {
174
187
  return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
175
188
  }
@@ -188,10 +201,28 @@ const getRelationWrapper = (
188
201
  const instantiateWrapper = (
189
202
  meta: EntityMeta<any>,
190
203
  relationName: string,
191
- relation: HasManyRelation | BelongsToRelation | BelongsToManyRelation,
204
+ relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
192
205
  owner: any
193
- ): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
206
+ ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
194
207
  switch (relation.type) {
208
+ case RelationKinds.HasOne: {
209
+ const hasOne = relation as HasOneRelation;
210
+ const localKey = hasOne.localKey || findPrimaryKey(meta.table);
211
+ const loader = () => relationLoaderCache(meta, relationName, () =>
212
+ loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
213
+ );
214
+ return new DefaultHasOneReference(
215
+ meta.ctx,
216
+ meta,
217
+ owner,
218
+ relationName,
219
+ hasOne,
220
+ meta.table,
221
+ loader,
222
+ (row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
223
+ localKey
224
+ );
225
+ }
195
226
  case RelationKinds.HasMany: {
196
227
  const hasMany = relation as HasManyRelation;
197
228
  const localKey = hasMany.localKey || findPrimaryKey(meta.table);
@@ -1,9 +1,12 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { Entity } from '../schema/types.js';
3
- import { hydrateRows } from './hydration.js';
4
- import { OrmContext } from './orm-context.js';
5
- import { SelectQueryBuilder } from '../query-builder/select.js';
6
- import { createEntityFromRow, createEntityProxy } from './entity.js';
3
+ import { hydrateRows } from './hydration.js';
4
+ import { OrmSession } from './orm-session.ts';
5
+ import { SelectQueryBuilder } from '../query-builder/select.js';
6
+ import { createEntityProxy, createEntityFromRow } from './entity.js';
7
+ import { EntityContext } from './entity-context.js';
8
+ import { ExecutionContext } from './execution-context.js';
9
+ import { HydrationContext } from './hydration-context.js';
7
10
 
8
11
  type Row = Record<string, any>;
9
12
 
@@ -22,24 +25,38 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
22
25
  return rows;
23
26
  };
24
27
 
25
- export async function executeHydrated<TTable extends TableDef>(
26
- ctx: OrmContext,
27
- qb: SelectQueryBuilder<any, TTable>
28
- ): Promise<Entity<TTable>[]> {
29
- const ast = qb.getAST();
30
- const compiled = ctx.dialect.compileSelect(ast);
31
- const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
32
- const rows = flattenResults(executed);
33
-
34
- // Set-operation queries cannot be reliably hydrated and should not collapse duplicates.
35
- if (ast.setOps && ast.setOps.length > 0) {
36
- return rows.map(row =>
37
- createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
38
- );
39
- }
40
-
41
- const hydrated = hydrateRows(rows, qb.getHydrationPlan());
42
- return hydrated.map(row =>
43
- createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
44
- );
45
- }
28
+ const executeWithEntityContext = async <TTable extends TableDef>(
29
+ entityCtx: EntityContext,
30
+ qb: SelectQueryBuilder<any, TTable>
31
+ ): Promise<Entity<TTable>[]> => {
32
+ const ast = qb.getAST();
33
+ const compiled = entityCtx.dialect.compileSelect(ast);
34
+ const executed = await entityCtx.executor.executeSql(compiled.sql, compiled.params);
35
+ const rows = flattenResults(executed);
36
+
37
+ if (ast.setOps && ast.setOps.length > 0) {
38
+ return rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
39
+ }
40
+
41
+ const hydrated = hydrateRows(rows, qb.getHydrationPlan());
42
+ return hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
43
+ };
44
+
45
+ export async function executeHydrated<TTable extends TableDef>(
46
+ session: OrmSession,
47
+ qb: SelectQueryBuilder<any, TTable>
48
+ ): Promise<Entity<TTable>[]> {
49
+ return executeWithEntityContext(session, qb);
50
+ }
51
+
52
+ export async function executeHydratedWithContexts<TTable extends TableDef>(
53
+ _execCtx: ExecutionContext,
54
+ hydCtx: HydrationContext,
55
+ qb: SelectQueryBuilder<any, TTable>
56
+ ): Promise<Entity<TTable>[]> {
57
+ const entityCtx = hydCtx.entityContext;
58
+ if (!entityCtx) {
59
+ throw new Error('Hydration context is missing an EntityContext');
60
+ }
61
+ return executeWithEntityContext(entityCtx, qb);
62
+ }
@@ -0,0 +1,12 @@
1
+ import type { Dialect } from '../core/dialect/abstract.js';
2
+ import type { DbExecutor } from '../core/execution/db-executor.js';
3
+ import { InterceptorPipeline } from './interceptor-pipeline.js';
4
+
5
+ export interface ExecutionContext {
6
+ dialect: Dialect;
7
+ executor: DbExecutor;
8
+ interceptors: InterceptorPipeline;
9
+ // plus anything *purely about executing SQL*:
10
+ // - logging
11
+ // - query timeout config
12
+ }
@@ -0,0 +1,14 @@
1
+ import { IdentityMap } from './identity-map.js';
2
+ import { UnitOfWork } from './unit-of-work.js';
3
+ import { DomainEventBus } from './domain-event-bus.js';
4
+ import { RelationChangeProcessor } from './relation-change-processor.js';
5
+ import { EntityContext } from './entity-context.js';
6
+
7
+ export interface HydrationContext {
8
+ identityMap: IdentityMap;
9
+ unitOfWork: UnitOfWork;
10
+ domainEvents: DomainEventBus<any>;
11
+ relationChanges: RelationChangeProcessor;
12
+ entityContext: EntityContext;
13
+ // maybe mapping registry, converters, etc.
14
+ }
@@ -1,5 +1,6 @@
1
- import { HydrationPlan, HydrationRelationPlan } from '../core/ast/query.js';
2
- import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
1
+ import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
2
+ import { RelationKinds } from '../schema/relation.js';
3
+ import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
3
4
 
4
5
  /**
5
6
  * Hydrates query results according to a hydration plan
@@ -47,18 +48,25 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
47
48
  const parent = getOrCreateParent(row);
48
49
  if (!parent) continue;
49
50
 
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
- }
51
+ for (const rel of plan.relations) {
52
+ const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
53
+ const childPk = row[childPkKey];
54
+ if (childPk === null || childPk === undefined) continue;
55
+
56
+ const seen = getRelationSeenSet(rootId, rel.name);
57
+ if (seen.has(childPk)) continue;
58
+ seen.add(childPk);
59
+
60
+ if (rel.type === RelationKinds.HasOne) {
61
+ if (!parent[rel.name]) {
62
+ parent[rel.name] = buildChild(row, rel);
63
+ }
64
+ continue;
65
+ }
66
+
67
+ const bucket = parent[rel.name] as any[];
68
+ bucket.push(buildChild(row, rel));
69
+ }
62
70
  }
63
71
 
64
72
  return Array.from(rootMap.values());
@@ -74,9 +82,9 @@ const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<st
74
82
  base[key] = row[key];
75
83
  }
76
84
 
77
- for (const rel of plan.relations) {
78
- base[rel.name] = [];
79
- }
85
+ for (const rel of plan.relations) {
86
+ base[rel.name] = rel.type === RelationKinds.HasOne ? null : [];
87
+ }
80
88
 
81
89
  return base;
82
90
  };
@@ -31,6 +31,10 @@ export class IdentityMap {
31
31
  return bucket ? Array.from(bucket.values()) : [];
32
32
  }
33
33
 
34
+ clear(): void {
35
+ this.buckets.clear();
36
+ }
37
+
34
38
  private toIdentityKey(pk: string | number): string {
35
39
  return String(pk);
36
40
  }
@@ -0,0 +1,29 @@
1
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
2
+
3
+ export interface QueryContext {
4
+ sql: string;
5
+ params: unknown[];
6
+ // maybe metadata like entity type, operation type, etc.
7
+ }
8
+
9
+ export type QueryInterceptor = (ctx: QueryContext, next: () => Promise<QueryResult[]>) => Promise<QueryResult[]>;
10
+
11
+ export class InterceptorPipeline {
12
+ private interceptors: QueryInterceptor[] = [];
13
+
14
+ use(interceptor: QueryInterceptor) {
15
+ this.interceptors.push(interceptor);
16
+ }
17
+
18
+ async run(ctx: QueryContext, executor: DbExecutor): Promise<QueryResult[]> {
19
+ let i = 0;
20
+ const dispatch = async (): Promise<QueryResult[]> => {
21
+ const interceptor = this.interceptors[i++];
22
+ if (!interceptor) {
23
+ return executor.executeSql(ctx.sql, ctx.params);
24
+ }
25
+ return interceptor(ctx, dispatch);
26
+ };
27
+ return dispatch();
28
+ }
29
+ }
@@ -1,8 +1,9 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { BelongsToManyRelation, HasManyRelation, BelongsToRelation } from '../schema/relation.js';
2
+ import { BelongsToManyRelation, HasManyRelation, HasOneRelation, BelongsToRelation } from '../schema/relation.js';
3
3
  import { SelectQueryBuilder } from '../query-builder/select.js';
4
4
  import { inList, LiteralNode } from '../core/ast/expression.js';
5
- import { OrmContext, QueryResult } from './orm-context.js';
5
+ import { EntityContext } from './entity-context.js';
6
+ import type { QueryResult } from '../core/execution/db-executor.js';
6
7
  import { ColumnDef } from '../schema/column.js';
7
8
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
9
 
@@ -29,7 +30,7 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
29
30
  return rows;
30
31
  };
31
32
 
32
- const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
33
+ const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
33
34
  const compiled = ctx.dialect.compileSelect(qb.getAST());
34
35
  const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
35
36
  return rowsFromResults(results);
@@ -38,7 +39,7 @@ const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDe
38
39
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
39
40
 
40
41
  export const loadHasManyRelation = async (
41
- ctx: OrmContext,
42
+ ctx: EntityContext,
42
43
  rootTable: TableDef,
43
44
  _relationName: string,
44
45
  relation: HasManyRelation
@@ -80,8 +81,51 @@ export const loadHasManyRelation = async (
80
81
  return grouped;
81
82
  };
82
83
 
84
+ export const loadHasOneRelation = async (
85
+ ctx: EntityContext,
86
+ rootTable: TableDef,
87
+ _relationName: string,
88
+ relation: HasOneRelation
89
+ ): Promise<Map<string, Record<string, any>>> => {
90
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
91
+ 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
+ }
100
+
101
+ if (!keys.size) {
102
+ return new Map();
103
+ }
104
+
105
+ const selectMap = selectAllColumns(relation.target);
106
+ const qb = new SelectQueryBuilder(relation.target).select(selectMap);
107
+ const fkColumn = relation.target.columns[relation.foreignKey];
108
+ if (!fkColumn) return new Map();
109
+
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, any>>();
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;
125
+ };
126
+
83
127
  export const loadBelongsToRelation = async (
84
- ctx: OrmContext,
128
+ ctx: EntityContext,
85
129
  rootTable: TableDef,
86
130
  _relationName: string,
87
131
  relation: BelongsToRelation
@@ -120,7 +164,7 @@ export const loadBelongsToRelation = async (
120
164
  };
121
165
 
122
166
  export const loadBelongsToManyRelation = async (
123
- ctx: OrmContext,
167
+ ctx: EntityContext,
124
168
  rootTable: TableDef,
125
169
  _relationName: string,
126
170
  relation: BelongsToManyRelation