metal-orm 1.0.15 → 1.0.17

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 (63) hide show
  1. package/README.md +64 -61
  2. package/dist/decorators/index.cjs +490 -175
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -5
  5. package/dist/decorators/index.d.ts +1 -5
  6. package/dist/decorators/index.js +490 -175
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1044 -483
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +67 -15
  11. package/dist/index.d.ts +67 -15
  12. package/dist/index.js +1033 -482
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-Bkv8g8u_.d.cts → select-BPCn6MOH.d.cts} +486 -32
  15. package/dist/{select-Bkv8g8u_.d.ts → select-BPCn6MOH.d.ts} +486 -32
  16. package/package.json +2 -1
  17. package/src/codegen/naming-strategy.ts +64 -0
  18. package/src/codegen/typescript.ts +48 -53
  19. package/src/core/ast/aggregate-functions.ts +50 -4
  20. package/src/core/ast/expression-builders.ts +22 -15
  21. package/src/core/ast/expression-nodes.ts +6 -0
  22. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  23. package/src/core/ddl/schema-generator.ts +3 -2
  24. package/src/core/ddl/schema-introspect.ts +1 -1
  25. package/src/core/dialect/abstract.ts +40 -8
  26. package/src/core/dialect/mssql/functions.ts +24 -15
  27. package/src/core/dialect/postgres/functions.ts +33 -24
  28. package/src/core/dialect/sqlite/functions.ts +19 -12
  29. package/src/core/functions/datetime.ts +2 -1
  30. package/src/core/functions/numeric.ts +2 -1
  31. package/src/core/functions/standard-strategy.ts +52 -12
  32. package/src/core/functions/text.ts +2 -1
  33. package/src/core/functions/types.ts +8 -8
  34. package/src/decorators/column.ts +13 -4
  35. package/src/index.ts +13 -5
  36. package/src/orm/domain-event-bus.ts +43 -25
  37. package/src/orm/entity-context.ts +30 -0
  38. package/src/orm/entity-meta.ts +42 -2
  39. package/src/orm/entity-metadata.ts +1 -6
  40. package/src/orm/entity.ts +88 -88
  41. package/src/orm/execute.ts +42 -25
  42. package/src/orm/execution-context.ts +18 -0
  43. package/src/orm/hydration-context.ts +16 -0
  44. package/src/orm/identity-map.ts +4 -0
  45. package/src/orm/interceptor-pipeline.ts +29 -0
  46. package/src/orm/lazy-batch.ts +6 -6
  47. package/src/orm/orm-session.ts +245 -0
  48. package/src/orm/orm.ts +58 -0
  49. package/src/orm/query-logger.ts +15 -0
  50. package/src/orm/relation-change-processor.ts +5 -1
  51. package/src/orm/relations/belongs-to.ts +45 -44
  52. package/src/orm/relations/has-many.ts +44 -43
  53. package/src/orm/relations/has-one.ts +140 -139
  54. package/src/orm/relations/many-to-many.ts +46 -45
  55. package/src/orm/runtime-types.ts +60 -2
  56. package/src/orm/transaction-runner.ts +7 -0
  57. package/src/orm/unit-of-work.ts +7 -1
  58. package/src/query-builder/insert-query-state.ts +13 -3
  59. package/src/query-builder/select-helpers.ts +50 -0
  60. package/src/query-builder/select.ts +616 -18
  61. package/src/query-builder/update-query-state.ts +31 -9
  62. package/src/schema/types.ts +16 -6
  63. package/src/orm/orm-context.ts +0 -159
@@ -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,18 @@
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
+ /**
6
+ * Context for SQL query execution
7
+ */
8
+ export interface ExecutionContext {
9
+ /** Database dialect to use for SQL generation */
10
+ dialect: Dialect;
11
+ /** Database executor for running SQL queries */
12
+ executor: DbExecutor;
13
+ /** Interceptor pipeline for query processing */
14
+ interceptors: InterceptorPipeline;
15
+ // plus anything *purely about executing SQL*:
16
+ // - logging
17
+ // - query timeout config
18
+ }
@@ -0,0 +1,16 @@
1
+ import { IdentityMap } from './identity-map.js';
2
+ import { UnitOfWork } from './unit-of-work.js';
3
+ import type { DomainEventBus } from './domain-event-bus.js';
4
+ import { RelationChangeProcessor } from './relation-change-processor.js';
5
+ import type { EntityContext } from './entity-context.js';
6
+ import type { AnyDomainEvent, DomainEvent } from './runtime-types.js';
7
+ import type { OrmSession } from './orm-session.js';
8
+
9
+ export interface HydrationContext<E extends DomainEvent = AnyDomainEvent> {
10
+ identityMap: IdentityMap;
11
+ unitOfWork: UnitOfWork;
12
+ domainEvents: DomainEventBus<E, OrmSession<E>>;
13
+ relationChanges: RelationChangeProcessor;
14
+ entityContext: EntityContext;
15
+ // maybe mapping registry, converters, etc.
16
+ }
@@ -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
+ }
@@ -2,7 +2,7 @@ import { TableDef } from '../schema/table.js';
2
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 } from './orm-context.js';
5
+ import { EntityContext } from './entity-context.js';
6
6
  import type { QueryResult } from '../core/execution/db-executor.js';
7
7
  import { ColumnDef } from '../schema/column.js';
8
8
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
@@ -30,7 +30,7 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
30
30
  return rows;
31
31
  };
32
32
 
33
- 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> => {
34
34
  const compiled = ctx.dialect.compileSelect(qb.getAST());
35
35
  const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
36
36
  return rowsFromResults(results);
@@ -39,7 +39,7 @@ const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDe
39
39
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
40
40
 
41
41
  export const loadHasManyRelation = async (
42
- ctx: OrmContext,
42
+ ctx: EntityContext,
43
43
  rootTable: TableDef,
44
44
  _relationName: string,
45
45
  relation: HasManyRelation
@@ -82,7 +82,7 @@ export const loadHasManyRelation = async (
82
82
  };
83
83
 
84
84
  export const loadHasOneRelation = async (
85
- ctx: OrmContext,
85
+ ctx: EntityContext,
86
86
  rootTable: TableDef,
87
87
  _relationName: string,
88
88
  relation: HasOneRelation
@@ -125,7 +125,7 @@ export const loadHasOneRelation = async (
125
125
  };
126
126
 
127
127
  export const loadBelongsToRelation = async (
128
- ctx: OrmContext,
128
+ ctx: EntityContext,
129
129
  rootTable: TableDef,
130
130
  _relationName: string,
131
131
  relation: BelongsToRelation
@@ -164,7 +164,7 @@ export const loadBelongsToRelation = async (
164
164
  };
165
165
 
166
166
  export const loadBelongsToManyRelation = async (
167
- ctx: OrmContext,
167
+ ctx: EntityContext,
168
168
  rootTable: TableDef,
169
169
  _relationName: string,
170
170
  relation: BelongsToManyRelation
@@ -0,0 +1,245 @@
1
+ import { Dialect } from '../core/dialect/abstract.js';
2
+ import { eq } from '../core/ast/expression.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
4
+ import { SelectQueryBuilder } from '../query-builder/select.js';
5
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
+ import type { ColumnDef } from '../schema/column.js';
7
+ import type { TableDef } from '../schema/table.js';
8
+ import { Entity } from '../schema/types.js';
9
+ import { RelationDef } from '../schema/relation.js';
10
+
11
+ import { selectFromEntity, getTableDefFromEntity } from '../decorators/bootstrap.js';
12
+ import type { EntityConstructor } from './entity-metadata.js';
13
+ import { Orm } from './orm.js';
14
+ import { IdentityMap } from './identity-map.js';
15
+ import { UnitOfWork } from './unit-of-work.js';
16
+ import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
17
+ import { RelationChangeProcessor } from './relation-change-processor.js';
18
+ import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
+ import { ExecutionContext } from './execution-context.js';
20
+ import type { HydrationContext } from './hydration-context.js';
21
+ import type { EntityContext } from './entity-context.js';
22
+ import {
23
+ DomainEvent,
24
+ OrmDomainEvent,
25
+ RelationChange,
26
+ RelationChangeEntry,
27
+ RelationKey,
28
+ TrackedEntity
29
+ } from './runtime-types.js';
30
+ import { executeHydrated } from './execute.js';
31
+ import { runInTransaction } from './transaction-runner.js';
32
+
33
+ export interface OrmInterceptor {
34
+ beforeFlush?(ctx: EntityContext): Promise<void> | void;
35
+ afterFlush?(ctx: EntityContext): Promise<void> | void;
36
+ }
37
+
38
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
39
+ orm: Orm<E>;
40
+ executor: DbExecutor;
41
+ queryLogger?: QueryLogger;
42
+ interceptors?: OrmInterceptor[];
43
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
44
+ }
45
+
46
+ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
47
+ readonly orm: Orm<E>;
48
+ readonly executor: DbExecutor;
49
+ readonly identityMap: IdentityMap;
50
+ readonly unitOfWork: UnitOfWork;
51
+ readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
52
+ readonly relationChanges: RelationChangeProcessor;
53
+
54
+ private readonly interceptors: OrmInterceptor[];
55
+
56
+ constructor(opts: OrmSessionOptions<E>) {
57
+ this.orm = opts.orm;
58
+ this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
59
+ this.interceptors = [...(opts.interceptors ?? [])];
60
+
61
+ this.identityMap = new IdentityMap();
62
+ this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
63
+ this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
64
+ this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
65
+ }
66
+
67
+ get dialect(): Dialect {
68
+ return this.orm.dialect;
69
+ }
70
+
71
+ get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
72
+ return this.unitOfWork.identityBuckets;
73
+ }
74
+
75
+ get tracked(): TrackedEntity[] {
76
+ return this.unitOfWork.getTracked();
77
+ }
78
+
79
+ getEntity(table: TableDef, pk: any): any | undefined {
80
+ return this.unitOfWork.getEntity(table, pk);
81
+ }
82
+
83
+ setEntity(table: TableDef, pk: any, entity: any): void {
84
+ this.unitOfWork.setEntity(table, pk, entity);
85
+ }
86
+
87
+ trackNew(table: TableDef, entity: any, pk?: any): void {
88
+ this.unitOfWork.trackNew(table, entity, pk);
89
+ }
90
+
91
+ trackManaged(table: TableDef, pk: any, entity: any): void {
92
+ this.unitOfWork.trackManaged(table, pk, entity);
93
+ }
94
+
95
+ markDirty(entity: any): void {
96
+ this.unitOfWork.markDirty(entity);
97
+ }
98
+
99
+ markRemoved(entity: any): void {
100
+ this.unitOfWork.markRemoved(entity);
101
+ }
102
+
103
+ registerRelationChange = (
104
+ root: any,
105
+ relationKey: RelationKey,
106
+ rootTable: TableDef,
107
+ relationName: string,
108
+ relation: RelationDef,
109
+ change: RelationChange<any>
110
+ ): void => {
111
+ this.relationChanges.registerChange(
112
+ buildRelationChangeEntry(root, relationKey, rootTable, relationName, relation, change)
113
+ );
114
+ };
115
+
116
+ getEntitiesForTable(table: TableDef): TrackedEntity[] {
117
+ return this.unitOfWork.getEntitiesForTable(table);
118
+ }
119
+
120
+ registerInterceptor(interceptor: OrmInterceptor): void {
121
+ this.interceptors.push(interceptor);
122
+ }
123
+
124
+ registerDomainEventHandler<TType extends E['type']>(
125
+ type: TType,
126
+ handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
127
+ ): void {
128
+ this.domainEvents.on(type, handler);
129
+ }
130
+
131
+ async find<TTable extends TableDef>(entityClass: EntityConstructor, id: any): Promise<Entity<TTable> | null> {
132
+ const table = getTableDefFromEntity(entityClass);
133
+ if (!table) {
134
+ throw new Error('Entity metadata has not been bootstrapped');
135
+ }
136
+ const primaryKey = findPrimaryKey(table);
137
+ const column = table.columns[primaryKey];
138
+ if (!column) {
139
+ throw new Error('Entity table does not expose a primary key');
140
+ }
141
+ const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
142
+ acc[col.name] = col;
143
+ return acc;
144
+ }, {});
145
+ const qb = selectFromEntity<TTable>(entityClass)
146
+ .select(columnSelections)
147
+ .where(eq(column, id))
148
+ .limit(1);
149
+ const rows = await executeHydrated(this, qb);
150
+ return rows[0] ?? null;
151
+ }
152
+
153
+ async findOne<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<Entity<TTable> | null> {
154
+ const limited = qb.limit(1);
155
+ const rows = await executeHydrated(this, limited);
156
+ return rows[0] ?? null;
157
+ }
158
+
159
+ async findMany<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<Entity<TTable>[]> {
160
+ return executeHydrated(this, qb);
161
+ }
162
+
163
+ async persist(entity: object): Promise<void> {
164
+ if (this.unitOfWork.findTracked(entity)) {
165
+ return;
166
+ }
167
+ const table = getTableDefFromEntity((entity as any).constructor as EntityConstructor);
168
+ if (!table) {
169
+ throw new Error('Entity metadata has not been bootstrapped');
170
+ }
171
+ const primaryKey = findPrimaryKey(table);
172
+ const pkValue = (entity as Record<string, any>)[primaryKey];
173
+ if (pkValue !== undefined && pkValue !== null) {
174
+ this.trackManaged(table, pkValue, entity);
175
+ } else {
176
+ this.trackNew(table, entity);
177
+ }
178
+ }
179
+
180
+ async remove(entity: object): Promise<void> {
181
+ this.markRemoved(entity);
182
+ }
183
+
184
+ async flush(): Promise<void> {
185
+ await this.unitOfWork.flush();
186
+ }
187
+
188
+ async commit(): Promise<void> {
189
+ await runInTransaction(this.executor, async () => {
190
+ for (const interceptor of this.interceptors) {
191
+ await interceptor.beforeFlush?.(this);
192
+ }
193
+
194
+ await this.unitOfWork.flush();
195
+ await this.relationChanges.process();
196
+ await this.unitOfWork.flush();
197
+
198
+ for (const interceptor of this.interceptors) {
199
+ await interceptor.afterFlush?.(this);
200
+ }
201
+ });
202
+
203
+ await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
204
+ }
205
+
206
+ async rollback(): Promise<void> {
207
+ await this.executor.rollbackTransaction?.();
208
+ this.unitOfWork.reset();
209
+ this.relationChanges.reset();
210
+ }
211
+
212
+ getExecutionContext(): ExecutionContext {
213
+ return {
214
+ dialect: this.orm.dialect,
215
+ executor: this.executor,
216
+ interceptors: this.orm.interceptors
217
+ };
218
+ }
219
+
220
+ getHydrationContext(): HydrationContext<E> {
221
+ return {
222
+ identityMap: this.identityMap,
223
+ unitOfWork: this.unitOfWork,
224
+ domainEvents: this.domainEvents,
225
+ relationChanges: this.relationChanges,
226
+ entityContext: this
227
+ };
228
+ }
229
+ }
230
+
231
+ const buildRelationChangeEntry = (
232
+ root: any,
233
+ relationKey: RelationKey,
234
+ rootTable: TableDef,
235
+ relationName: string,
236
+ relation: RelationDef,
237
+ change: RelationChange<any>
238
+ ): RelationChangeEntry => ({
239
+ root,
240
+ relationKey,
241
+ rootTable,
242
+ relationName,
243
+ relation,
244
+ change
245
+ });
package/src/orm/orm.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { DomainEvent, OrmDomainEvent } from './runtime-types.js';
2
+ import type { Dialect } from '../core/dialect/abstract.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
4
+ import type { NamingStrategy } from '../codegen/naming-strategy.js';
5
+ import { InterceptorPipeline } from './interceptor-pipeline.js';
6
+ import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
7
+ import { OrmSession } from './orm-session.js';
8
+
9
+ export interface OrmOptions<E extends DomainEvent = OrmDomainEvent> {
10
+ dialect: Dialect;
11
+ executorFactory: DbExecutorFactory;
12
+ interceptors?: InterceptorPipeline;
13
+ namingStrategy?: NamingStrategy;
14
+ // model registrations etc.
15
+ }
16
+
17
+ export interface DbExecutorFactory {
18
+ createExecutor(options?: { tx?: ExternalTransaction }): DbExecutor;
19
+ createTransactionalExecutor(): DbExecutor;
20
+ }
21
+
22
+ export interface ExternalTransaction {
23
+ // Transaction-specific properties
24
+ }
25
+
26
+ export class Orm<E extends DomainEvent = OrmDomainEvent> {
27
+ readonly dialect: Dialect;
28
+ readonly interceptors: InterceptorPipeline;
29
+ readonly namingStrategy: NamingStrategy;
30
+ private readonly executorFactory: DbExecutorFactory;
31
+
32
+ constructor(opts: OrmOptions<E>) {
33
+ this.dialect = opts.dialect;
34
+ this.interceptors = opts.interceptors ?? new InterceptorPipeline();
35
+ this.namingStrategy = opts.namingStrategy ?? new DefaultNamingStrategy();
36
+ this.executorFactory = opts.executorFactory;
37
+ }
38
+
39
+ createSession(options?: { tx?: ExternalTransaction }): OrmSession<E> {
40
+ const executor = this.executorFactory.createExecutor(options?.tx);
41
+ return new OrmSession<E>({ orm: this, executor });
42
+ }
43
+
44
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
45
+ const executor = this.executorFactory.createTransactionalExecutor();
46
+ const session = new OrmSession<E>({ orm: this, executor });
47
+ try {
48
+ const result = await fn(session);
49
+ await session.commit();
50
+ return result;
51
+ } catch (err) {
52
+ await session.rollback();
53
+ throw err;
54
+ } finally {
55
+ // executor cleanup if needed
56
+ }
57
+ }
58
+ }
@@ -1,12 +1,27 @@
1
1
  import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
+ /**
4
+ * Represents a single SQL query log entry
5
+ */
3
6
  export interface QueryLogEntry {
7
+ /** The SQL query that was executed */
4
8
  sql: string;
9
+ /** Parameters used in the query */
5
10
  params?: unknown[];
6
11
  }
7
12
 
13
+ /**
14
+ * Function type for query logging callbacks
15
+ * @param entry - The query log entry to process
16
+ */
8
17
  export type QueryLogger = (entry: QueryLogEntry) => void;
9
18
 
19
+ /**
20
+ * Creates a wrapped database executor that logs all SQL queries
21
+ * @param executor - Original database executor to wrap
22
+ * @param logger - Optional logger function to receive query log entries
23
+ * @returns Wrapped executor that logs queries before execution
24
+ */
10
25
  export const createQueryLoggingExecutor = (
11
26
  executor: DbExecutor,
12
27
  logger?: QueryLogger
@@ -17,12 +17,16 @@ export class RelationChangeProcessor {
17
17
  private readonly unitOfWork: UnitOfWork,
18
18
  private readonly dialect: Dialect,
19
19
  private readonly executor: DbExecutor
20
- ) {}
20
+ ) { }
21
21
 
22
22
  registerChange(entry: RelationChangeEntry): void {
23
23
  this.relationChanges.push(entry);
24
24
  }
25
25
 
26
+ reset(): void {
27
+ this.relationChanges.length = 0;
28
+ }
29
+
26
30
  async process(): Promise<void> {
27
31
  if (!this.relationChanges.length) return;
28
32
  const entries = [...this.relationChanges];
@@ -1,45 +1,46 @@
1
1
  import { BelongsToReference } from '../../schema/types.js';
2
- import { OrmContext, RelationKey } from '../orm-context.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
3
4
  import { BelongsToRelation } from '../../schema/relation.js';
4
5
  import { TableDef } from '../../schema/table.js';
5
6
  import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
6
7
 
7
- type Rows = Record<string, any>;
8
-
9
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
-
11
- const hideInternal = (obj: any, keys: string[]): void => {
12
- for (const key of keys) {
13
- Object.defineProperty(obj, key, {
14
- value: obj[key],
15
- writable: false,
16
- configurable: false,
17
- enumerable: false
18
- });
19
- }
20
- };
21
-
22
- export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
23
- private loaded = false;
24
- private current: TParent | null = null;
25
-
26
- constructor(
27
- private readonly ctx: OrmContext,
8
+ type Rows = Record<string, any>;
9
+
10
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
+
12
+ const hideInternal = (obj: any, keys: string[]): void => {
13
+ for (const key of keys) {
14
+ Object.defineProperty(obj, key, {
15
+ value: obj[key],
16
+ writable: false,
17
+ configurable: false,
18
+ enumerable: false
19
+ });
20
+ }
21
+ };
22
+
23
+ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
24
+ private loaded = false;
25
+ private current: TParent | null = null;
26
+
27
+ constructor(
28
+ private readonly ctx: EntityContext,
28
29
  private readonly meta: EntityMeta<any>,
29
30
  private readonly root: any,
30
31
  private readonly relationName: string,
31
32
  private readonly relation: BelongsToRelation,
32
33
  private readonly rootTable: TableDef,
33
34
  private readonly loader: () => Promise<Map<string, Rows>>,
34
- private readonly createEntity: (row: Record<string, any>) => TParent,
35
- private readonly targetKey: string
36
- ) {
37
- hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
38
- this.populateFromHydrationCache();
39
- }
40
-
41
- async load(): Promise<TParent | null> {
42
- if (this.loaded) return this.current;
35
+ private readonly createEntity: (row: Record<string, any>) => TParent,
36
+ private readonly targetKey: string
37
+ ) {
38
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
39
+ this.populateFromHydrationCache();
40
+ }
41
+
42
+ async load(): Promise<TParent | null> {
43
+ if (this.loaded) return this.current;
43
44
  const map = await this.loader();
44
45
  const fkValue = this.root[this.relation.foreignKey];
45
46
  if (fkValue === null || fkValue === undefined) {
@@ -93,16 +94,16 @@ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TP
93
94
  return `${this.rootTable.name}.${this.relationName}`;
94
95
  }
95
96
 
96
- private populateFromHydrationCache(): void {
97
- const fkValue = this.root[this.relation.foreignKey];
98
- if (fkValue === undefined || fkValue === null) return;
99
- const row = getHydrationRecord(this.meta, this.relationName, fkValue);
100
- if (!row) return;
101
- this.current = this.createEntity(row);
102
- this.loaded = true;
103
- }
104
-
105
- toJSON(): TParent | null {
106
- return this.current;
107
- }
108
- }
97
+ private populateFromHydrationCache(): void {
98
+ const fkValue = this.root[this.relation.foreignKey];
99
+ if (fkValue === undefined || fkValue === null) return;
100
+ const row = getHydrationRecord(this.meta, this.relationName, fkValue);
101
+ if (!row) return;
102
+ this.current = this.createEntity(row);
103
+ this.loaded = true;
104
+ }
105
+
106
+ toJSON(): TParent | null {
107
+ return this.current;
108
+ }
109
+ }