metal-orm 1.0.16 → 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 (45) hide show
  1. package/README.md +33 -37
  2. package/dist/decorators/index.cjs +152 -23
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +152 -23
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +322 -115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +53 -4
  11. package/dist/index.d.ts +53 -4
  12. package/dist/index.js +316 -115
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-BKZrMRCQ.d.cts → select-BPCn6MOH.d.cts} +183 -64
  15. package/dist/{select-BKZrMRCQ.d.ts → select-BPCn6MOH.d.ts} +183 -64
  16. package/package.json +2 -1
  17. package/src/core/ast/aggregate-functions.ts +50 -4
  18. package/src/core/ast/expression-builders.ts +22 -15
  19. package/src/core/ast/expression-nodes.ts +6 -0
  20. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  21. package/src/core/dialect/abstract.ts +12 -8
  22. package/src/core/dialect/mssql/functions.ts +24 -15
  23. package/src/core/dialect/postgres/functions.ts +33 -24
  24. package/src/core/dialect/sqlite/functions.ts +19 -12
  25. package/src/core/functions/datetime.ts +2 -1
  26. package/src/core/functions/numeric.ts +2 -1
  27. package/src/core/functions/standard-strategy.ts +52 -12
  28. package/src/core/functions/text.ts +2 -1
  29. package/src/core/functions/types.ts +8 -8
  30. package/src/index.ts +5 -4
  31. package/src/orm/domain-event-bus.ts +43 -25
  32. package/src/orm/entity-meta.ts +40 -0
  33. package/src/orm/execution-context.ts +6 -0
  34. package/src/orm/hydration-context.ts +6 -4
  35. package/src/orm/orm-session.ts +35 -24
  36. package/src/orm/orm.ts +10 -10
  37. package/src/orm/query-logger.ts +15 -0
  38. package/src/orm/runtime-types.ts +60 -2
  39. package/src/orm/transaction-runner.ts +7 -0
  40. package/src/orm/unit-of-work.ts +1 -0
  41. package/src/query-builder/insert-query-state.ts +13 -3
  42. package/src/query-builder/select-helpers.ts +50 -0
  43. package/src/query-builder/select.ts +122 -30
  44. package/src/query-builder/update-query-state.ts +31 -9
  45. package/src/schema/types.ts +16 -6
@@ -69,27 +69,36 @@ export class PostgresFunctionStrategy extends StandardFunctionStrategy {
69
69
  return `TO_CHAR(${date}, ${format})`;
70
70
  });
71
71
 
72
- this.add('END_OF_MONTH', ({ compiledArgs }) => {
73
- if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
74
- return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
75
- });
76
-
77
- this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
78
- if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
79
- return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
80
- });
81
-
82
- this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
83
- if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
84
- return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
85
- });
86
-
87
- this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
88
- if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
89
- const [, date] = compiledArgs;
90
- const partArg = node.args[0] as LiteralNode;
91
- const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
92
- return `DATE_TRUNC('${partClean}', ${date})`;
93
- });
94
- }
95
- }
72
+ this.add('END_OF_MONTH', ({ compiledArgs }) => {
73
+ if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
74
+ return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
75
+ });
76
+
77
+ this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
78
+ if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
79
+ return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
80
+ });
81
+
82
+ this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
83
+ if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
84
+ return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
85
+ });
86
+
87
+ this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
88
+ if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
89
+ const [, date] = compiledArgs;
90
+ const partArg = node.args[0] as LiteralNode;
91
+ const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
92
+ return `DATE_TRUNC('${partClean}', ${date})`;
93
+ });
94
+
95
+ this.add('GROUP_CONCAT', ctx => {
96
+ const arg = ctx.compiledArgs[0];
97
+ const orderClause = this.buildOrderByExpression(ctx);
98
+ const orderSegment = orderClause ? ` ${orderClause}` : '';
99
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
100
+ const separator = ctx.compileOperand(separatorOperand);
101
+ return `STRING_AGG(${arg}, ${separator}${orderSegment})`;
102
+ });
103
+ }
104
+ }
@@ -1,12 +1,12 @@
1
- import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
2
- import { FunctionRenderContext } from '../../functions/types.js';
3
- import { LiteralNode } from '../../ast/expression.js';
4
-
5
- export class SqliteFunctionStrategy extends StandardFunctionStrategy {
6
- constructor() {
7
- super();
8
- this.registerOverrides();
9
- }
1
+ import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
2
+ import { FunctionRenderContext } from '../../functions/types.js';
3
+ import { LiteralNode } from '../../ast/expression.js';
4
+
5
+ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
6
+ constructor() {
7
+ super();
8
+ this.registerOverrides();
9
+ }
10
10
 
11
11
  private registerOverrides() {
12
12
  // Override Standard/Abstract definitions with SQLite specifics
@@ -110,6 +110,13 @@ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
110
110
  return `date(${date})`;
111
111
  }
112
112
  return `date(${date}, 'start of ${partClean}')`;
113
- });
114
- }
115
- }
113
+ });
114
+
115
+ this.add('GROUP_CONCAT', ctx => {
116
+ const arg = ctx.compiledArgs[0];
117
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
118
+ const separator = ctx.compileOperand(separatorOperand);
119
+ return `GROUP_CONCAT(${arg}, ${separator})`;
120
+ });
121
+ }
122
+ }
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
11
11
  const toOperand = (input: OperandInput): OperandNode => {
12
12
  if (isOperandNode(input)) return input;
13
13
  if (isColumnDef(input)) return columnOperand(input);
14
- return valueToOperand(input as any);
14
+
15
+ return valueToOperand(input);
15
16
  };
16
17
 
17
18
  const fn = (key: string, args: OperandInput[]): FunctionNode => ({
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
11
11
  const toOperand = (input: OperandInput): OperandNode => {
12
12
  if (isOperandNode(input)) return input;
13
13
  if (isColumnDef(input)) return columnOperand(input);
14
- return valueToOperand(input as any);
14
+
15
+ return valueToOperand(input);
15
16
  };
16
17
 
17
18
  const fn = (key: string, args: OperandInput[]): FunctionNode => ({
@@ -1,4 +1,5 @@
1
- import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
1
+ import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
2
+ import { LiteralNode, OperandNode } from '../ast/expression.js';
2
3
 
3
4
  export class StandardFunctionStrategy implements FunctionStrategy {
4
5
  protected renderers: Map<string, FunctionRenderer> = new Map();
@@ -7,11 +8,16 @@ export class StandardFunctionStrategy implements FunctionStrategy {
7
8
  this.registerStandard();
8
9
  }
9
10
 
10
- protected registerStandard() {
11
- // Register ANSI standard implementations
12
- this.add('ABS', ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
13
- this.add('UPPER', ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
14
- this.add('LOWER', ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
11
+ protected registerStandard() {
12
+ // Register ANSI standard implementations
13
+ this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
14
+ this.add('SUM', ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
15
+ this.add('AVG', ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
16
+ this.add('MIN', ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
17
+ this.add('MAX', ({ compiledArgs }) => `MAX(${compiledArgs[0]})`);
18
+ this.add('ABS', ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
19
+ this.add('UPPER', ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
20
+ this.add('LOWER', ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
15
21
  this.add('LENGTH', ({ compiledArgs }) => `LENGTH(${compiledArgs[0]})`);
16
22
  this.add('TRIM', ({ compiledArgs }) => `TRIM(${compiledArgs[0]})`);
17
23
  this.add('LTRIM', ({ compiledArgs }) => `LTRIM(${compiledArgs[0]})`);
@@ -33,15 +39,49 @@ export class StandardFunctionStrategy implements FunctionStrategy {
33
39
  this.add('FROM_UNIXTIME', ({ compiledArgs }) => `FROM_UNIXTIME(${compiledArgs[0]})`);
34
40
  this.add('END_OF_MONTH', ({ compiledArgs }) => `LAST_DAY(${compiledArgs[0]})`);
35
41
  this.add('DAY_OF_WEEK', ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
36
- this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
37
- this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
42
+ this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
43
+ this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
44
+ this.add('GROUP_CONCAT', ctx => this.renderGroupConcat(ctx));
38
45
  }
39
46
 
40
47
  protected add(name: string, renderer: FunctionRenderer) {
41
48
  this.renderers.set(name, renderer);
42
49
  }
43
50
 
44
- getRenderer(name: string): FunctionRenderer | undefined {
45
- return this.renderers.get(name);
46
- }
47
- }
51
+ getRenderer(name: string): FunctionRenderer | undefined {
52
+ return this.renderers.get(name);
53
+ }
54
+
55
+ private renderGroupConcat(ctx: FunctionRenderContext): string {
56
+ const arg = ctx.compiledArgs[0];
57
+ const orderClause = this.buildOrderByExpression(ctx);
58
+ const orderSegment = orderClause ? ` ${orderClause}` : '';
59
+ const separatorClause = this.formatGroupConcatSeparator(ctx);
60
+ return `GROUP_CONCAT(${arg}${orderSegment}${separatorClause})`;
61
+ }
62
+
63
+ protected buildOrderByExpression(ctx: FunctionRenderContext): string {
64
+ const orderBy = ctx.node.orderBy;
65
+ if (!orderBy || orderBy.length === 0) {
66
+ return '';
67
+ }
68
+ const parts = orderBy.map(order => `${ctx.compileOperand(order.column)} ${order.direction}`);
69
+ return `ORDER BY ${parts.join(', ')}`;
70
+ }
71
+
72
+ protected formatGroupConcatSeparator(ctx: FunctionRenderContext): string {
73
+ if (!ctx.node.separator) {
74
+ return '';
75
+ }
76
+ return ` SEPARATOR ${ctx.compileOperand(ctx.node.separator)}`;
77
+ }
78
+
79
+ protected getGroupConcatSeparatorOperand(ctx: FunctionRenderContext): OperandNode {
80
+ return ctx.node.separator ?? StandardFunctionStrategy.DEFAULT_GROUP_CONCAT_SEPARATOR;
81
+ }
82
+
83
+ protected static readonly DEFAULT_GROUP_CONCAT_SEPARATOR: LiteralNode = {
84
+ type: 'Literal',
85
+ value: ','
86
+ };
87
+ }
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
11
11
  const toOperand = (input: OperandInput): OperandNode => {
12
12
  if (isOperandNode(input)) return input;
13
13
  if (isColumnDef(input)) return columnOperand(input);
14
- return valueToOperand(input as any);
14
+
15
+ return valueToOperand(input);
15
16
  };
16
17
 
17
18
  const fn = (key: string, args: OperandInput[]): FunctionNode => ({
@@ -1,11 +1,11 @@
1
- import { FunctionNode } from '../ast/expression.js';
2
-
3
- export interface FunctionRenderContext {
4
- node: FunctionNode;
5
- compiledArgs: string[];
6
- // Helper to allow dialects to call back into the compiler if needed
7
- // compileOperand: (node: OperandNode) => string;
8
- }
1
+ import { FunctionNode, OperandNode } from '../ast/expression.js';
2
+
3
+ export interface FunctionRenderContext {
4
+ node: FunctionNode;
5
+ compiledArgs: string[];
6
+ /** Helper to compile additional operands (e.g., separators or ORDER BY columns) */
7
+ compileOperand: (operand: OperandNode) => string;
8
+ }
9
9
 
10
10
  export type FunctionRenderer = (ctx: FunctionRenderContext) => string;
11
11
 
package/src/index.ts CHANGED
@@ -2,10 +2,11 @@ export * from './schema/table.js';
2
2
  export * from './schema/column.js';
3
3
  export * from './schema/relation.js';
4
4
  export * from './schema/types.js';
5
- export * from './query-builder/select.js';
6
- export * from './query-builder/insert.js';
7
- export * from './query-builder/update.js';
8
- export * from './query-builder/delete.js';
5
+ export * from './query-builder/select.js';
6
+ export * from './query-builder/select-helpers.js';
7
+ export * from './query-builder/insert.js';
8
+ export * from './query-builder/update.js';
9
+ export * from './query-builder/delete.js';
9
10
  export * from './core/ast/expression.js';
10
11
  export * from './core/hydration/types.js';
11
12
  export * from './core/dialect/mysql/index.js';
@@ -1,32 +1,53 @@
1
- import type { HasDomainEvents, TrackedEntity } from './runtime-types.js';
1
+ import type { DomainEvent, HasDomainEvents, TrackedEntity } from './runtime-types.js';
2
2
 
3
- export type DomainEventHandler<Context> = (event: any, ctx: Context) => Promise<void> | void;
3
+ type EventOfType<E extends DomainEvent, TType extends E['type']> =
4
+ Extract<E, { type: TType }>;
4
5
 
5
- export class DomainEventBus<Context> {
6
- private readonly handlers = new Map<string, DomainEventHandler<Context>[]>();
6
+ export type DomainEventHandler<E extends DomainEvent, Context> =
7
+ (event: E, ctx: Context) => Promise<void> | void;
7
8
 
8
- constructor(initialHandlers?: Record<string, DomainEventHandler<Context>[]>) {
9
- const handlers = initialHandlers ?? {};
10
- Object.entries(handlers).forEach(([name, list]) => {
11
- this.handlers.set(name, [...list]);
12
- });
9
+ export type InitialHandlers<E extends DomainEvent, Context> = {
10
+ [K in E['type']]?: DomainEventHandler<EventOfType<E, K>, Context>[];
11
+ };
12
+
13
+ export class DomainEventBus<E extends DomainEvent, Context> {
14
+ private readonly handlers = new Map<E['type'], DomainEventHandler<E, Context>[]>();
15
+
16
+ constructor(initialHandlers?: InitialHandlers<E, Context>) {
17
+ if (initialHandlers) {
18
+ for (const key in initialHandlers) {
19
+ const type = key as E['type'];
20
+ const list = initialHandlers[type] ?? [];
21
+ this.handlers.set(type, [...(list as DomainEventHandler<E, Context>[])]);
22
+ }
23
+ }
13
24
  }
14
25
 
15
- register(name: string, handler: DomainEventHandler<Context>): void {
16
- const existing = this.handlers.get(name) ?? [];
17
- existing.push(handler);
18
- this.handlers.set(name, existing);
26
+ on<TType extends E['type']>(
27
+ type: TType,
28
+ handler: DomainEventHandler<EventOfType<E, TType>, Context>
29
+ ): void {
30
+ const key = type as E['type'];
31
+ const existing = this.handlers.get(key) ?? [];
32
+ existing.push(handler as unknown as DomainEventHandler<E, Context>);
33
+ this.handlers.set(key, existing);
34
+ }
35
+
36
+ register<TType extends E['type']>(
37
+ type: TType,
38
+ handler: DomainEventHandler<EventOfType<E, TType>, Context>
39
+ ): void {
40
+ this.on(type, handler);
19
41
  }
20
42
 
21
43
  async dispatch(trackedEntities: Iterable<TrackedEntity>, ctx: Context): Promise<void> {
22
44
  for (const tracked of trackedEntities) {
23
- const entity = tracked.entity as HasDomainEvents;
24
- if (!entity.domainEvents || !entity.domainEvents.length) continue;
45
+ const entity = tracked.entity as HasDomainEvents<E>;
46
+ if (!entity.domainEvents?.length) continue;
25
47
 
26
48
  for (const event of entity.domainEvents) {
27
- const eventName = this.getEventName(event);
28
- const handlers = this.handlers.get(eventName);
29
- if (!handlers) continue;
49
+ const handlers = this.handlers.get(event.type as E['type']);
50
+ if (!handlers?.length) continue;
30
51
 
31
52
  for (const handler of handlers) {
32
53
  await handler(event, ctx);
@@ -36,15 +57,12 @@ export class DomainEventBus<Context> {
36
57
  entity.domainEvents = [];
37
58
  }
38
59
  }
39
-
40
- private getEventName(event: any): string {
41
- if (!event) return 'Unknown';
42
- if (typeof event === 'string') return event;
43
- return event.constructor?.name ?? 'Unknown';
44
- }
45
60
  }
46
61
 
47
- export const addDomainEvent = (entity: HasDomainEvents, event: any): void => {
62
+ export const addDomainEvent = <E extends DomainEvent>(
63
+ entity: HasDomainEvents<E>,
64
+ event: E
65
+ ): void => {
48
66
  if (!entity.domainEvents) {
49
67
  entity.domainEvents = [];
50
68
  }
@@ -2,19 +2,40 @@ import { TableDef } from '../schema/table.js';
2
2
  import { EntityContext } from './entity-context.js';
3
3
  import { RelationMap } from '../schema/types.js';
4
4
 
5
+ /**
6
+ * Symbol used to store entity metadata on entity instances
7
+ */
5
8
  export const ENTITY_META = Symbol('EntityMeta');
6
9
 
7
10
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
8
11
 
12
+ /**
13
+ * Metadata stored on entity instances for ORM internal use
14
+ * @typeParam TTable - Table definition type
15
+ */
9
16
  export interface EntityMeta<TTable extends TableDef> {
17
+ /** Entity context */
10
18
  ctx: EntityContext;
19
+ /** Table definition */
11
20
  table: TTable;
21
+ /** Relations that should be loaded lazily */
12
22
  lazyRelations: (keyof RelationMap<TTable>)[];
23
+ /** Cache for relation promises */
13
24
  relationCache: Map<string, Promise<any>>;
25
+ /** Hydration data for relations */
14
26
  relationHydration: Map<string, Map<string, any>>;
27
+ /** Relation wrapper instances */
15
28
  relationWrappers: Map<string, unknown>;
16
29
  }
17
30
 
31
+ /**
32
+ * Gets hydration rows for a specific relation and key
33
+ * @param meta - Entity metadata
34
+ * @param relationName - Name of the relation
35
+ * @param key - Key to look up in the hydration map
36
+ * @returns Array of hydration rows or undefined if not found
37
+ * @typeParam TTable - Table definition type
38
+ */
18
39
  export const getHydrationRows = <TTable extends TableDef>(
19
40
  meta: EntityMeta<TTable>,
20
41
  relationName: string,
@@ -27,6 +48,14 @@ export const getHydrationRows = <TTable extends TableDef>(
27
48
  return Array.isArray(rows) ? rows : undefined;
28
49
  };
29
50
 
51
+ /**
52
+ * Gets a single hydration record for a specific relation and key
53
+ * @param meta - Entity metadata
54
+ * @param relationName - Name of the relation
55
+ * @param key - Key to look up in the hydration map
56
+ * @returns Single hydration record or undefined if not found
57
+ * @typeParam TTable - Table definition type
58
+ */
30
59
  export const getHydrationRecord = <TTable extends TableDef>(
31
60
  meta: EntityMeta<TTable>,
32
61
  relationName: string,
@@ -42,11 +71,22 @@ export const getHydrationRecord = <TTable extends TableDef>(
42
71
  return value;
43
72
  };
44
73
 
74
+ /**
75
+ * Gets entity metadata from an entity instance
76
+ * @param entity - Entity instance to get metadata from
77
+ * @returns Entity metadata or undefined if not found
78
+ * @typeParam TTable - Table definition type
79
+ */
45
80
  export const getEntityMeta = <TTable extends TableDef>(entity: any): EntityMeta<TTable> | undefined => {
46
81
  if (!entity || typeof entity !== 'object') return undefined;
47
82
  return (entity as any)[ENTITY_META];
48
83
  };
49
84
 
85
+ /**
86
+ * Checks if an entity has metadata attached
87
+ * @param entity - Entity instance to check
88
+ * @returns True if the entity has metadata, false otherwise
89
+ */
50
90
  export const hasEntityMeta = (entity: any): entity is { [ENTITY_META]: EntityMeta<TableDef> } => {
51
91
  return Boolean(getEntityMeta(entity));
52
92
  };
@@ -2,9 +2,15 @@ import type { Dialect } from '../core/dialect/abstract.js';
2
2
  import type { DbExecutor } from '../core/execution/db-executor.js';
3
3
  import { InterceptorPipeline } from './interceptor-pipeline.js';
4
4
 
5
+ /**
6
+ * Context for SQL query execution
7
+ */
5
8
  export interface ExecutionContext {
9
+ /** Database dialect to use for SQL generation */
6
10
  dialect: Dialect;
11
+ /** Database executor for running SQL queries */
7
12
  executor: DbExecutor;
13
+ /** Interceptor pipeline for query processing */
8
14
  interceptors: InterceptorPipeline;
9
15
  // plus anything *purely about executing SQL*:
10
16
  // - logging
@@ -1,13 +1,15 @@
1
1
  import { IdentityMap } from './identity-map.js';
2
2
  import { UnitOfWork } from './unit-of-work.js';
3
- import { DomainEventBus } from './domain-event-bus.js';
3
+ import type { DomainEventBus } from './domain-event-bus.js';
4
4
  import { RelationChangeProcessor } from './relation-change-processor.js';
5
- import { EntityContext } from './entity-context.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';
6
8
 
7
- export interface HydrationContext {
9
+ export interface HydrationContext<E extends DomainEvent = AnyDomainEvent> {
8
10
  identityMap: IdentityMap;
9
11
  unitOfWork: UnitOfWork;
10
- domainEvents: DomainEventBus<any>;
12
+ domainEvents: DomainEventBus<E, OrmSession<E>>;
11
13
  relationChanges: RelationChangeProcessor;
12
14
  entityContext: EntityContext;
13
15
  // maybe mapping registry, converters, etc.
@@ -1,9 +1,10 @@
1
1
  import { Dialect } from '../core/dialect/abstract.js';
2
2
  import { eq } from '../core/ast/expression.js';
3
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 { TableDef } from '../schema/table.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';
7
8
  import { Entity } from '../schema/types.js';
8
9
  import { RelationDef } from '../schema/relation.js';
9
10
 
@@ -12,13 +13,15 @@ import type { EntityConstructor } from './entity-metadata.js';
12
13
  import { Orm } from './orm.js';
13
14
  import { IdentityMap } from './identity-map.js';
14
15
  import { UnitOfWork } from './unit-of-work.js';
15
- import { DomainEventBus, DomainEventHandler } from './domain-event-bus.js';
16
+ import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
16
17
  import { RelationChangeProcessor } from './relation-change-processor.js';
17
18
  import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
18
19
  import { ExecutionContext } from './execution-context.js';
19
- import { HydrationContext } from './hydration-context.js';
20
- import { EntityContext } from './entity-context.js';
20
+ import type { HydrationContext } from './hydration-context.js';
21
+ import type { EntityContext } from './entity-context.js';
21
22
  import {
23
+ DomainEvent,
24
+ OrmDomainEvent,
22
25
  RelationChange,
23
26
  RelationChangeEntry,
24
27
  RelationKey,
@@ -32,25 +35,25 @@ export interface OrmInterceptor {
32
35
  afterFlush?(ctx: EntityContext): Promise<void> | void;
33
36
  }
34
37
 
35
- export interface OrmSessionOptions {
36
- orm: Orm;
38
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
39
+ orm: Orm<E>;
37
40
  executor: DbExecutor;
38
41
  queryLogger?: QueryLogger;
39
42
  interceptors?: OrmInterceptor[];
40
- domainEventHandlers?: Record<string, DomainEventHandler<OrmSession>[]>;
43
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
41
44
  }
42
45
 
43
- export class OrmSession implements EntityContext {
44
- readonly orm: Orm;
46
+ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
47
+ readonly orm: Orm<E>;
45
48
  readonly executor: DbExecutor;
46
49
  readonly identityMap: IdentityMap;
47
50
  readonly unitOfWork: UnitOfWork;
48
- readonly domainEvents: DomainEventBus<OrmSession>;
51
+ readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
49
52
  readonly relationChanges: RelationChangeProcessor;
50
53
 
51
54
  private readonly interceptors: OrmInterceptor[];
52
55
 
53
- constructor(opts: OrmSessionOptions) {
56
+ constructor(opts: OrmSessionOptions<E>) {
54
57
  this.orm = opts.orm;
55
58
  this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
56
59
  this.interceptors = [...(opts.interceptors ?? [])];
@@ -58,7 +61,7 @@ export class OrmSession implements EntityContext {
58
61
  this.identityMap = new IdentityMap();
59
62
  this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
60
63
  this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
61
- this.domainEvents = new DomainEventBus<OrmSession>(opts.domainEventHandlers);
64
+ this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
62
65
  }
63
66
 
64
67
  get dialect(): Dialect {
@@ -118,8 +121,11 @@ export class OrmSession implements EntityContext {
118
121
  this.interceptors.push(interceptor);
119
122
  }
120
123
 
121
- registerDomainEventHandler(name: string, handler: DomainEventHandler<OrmSession>): void {
122
- this.domainEvents.register(name, handler);
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);
123
129
  }
124
130
 
125
131
  async find<TTable extends TableDef>(entityClass: EntityConstructor, id: any): Promise<Entity<TTable> | null> {
@@ -128,13 +134,18 @@ export class OrmSession implements EntityContext {
128
134
  throw new Error('Entity metadata has not been bootstrapped');
129
135
  }
130
136
  const primaryKey = findPrimaryKey(table);
131
- const column = table.columns[primaryKey];
132
- if (!column) {
133
- throw new Error('Entity table does not expose a primary key');
134
- }
135
- const qb = selectFromEntity<TTable>(entityClass)
136
- .where(eq(column, id))
137
- .limit(1);
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);
138
149
  const rows = await executeHydrated(this, qb);
139
150
  return rows[0] ?? null;
140
151
  }
@@ -206,7 +217,7 @@ export class OrmSession implements EntityContext {
206
217
  };
207
218
  }
208
219
 
209
- getHydrationContext(): HydrationContext {
220
+ getHydrationContext(): HydrationContext<E> {
210
221
  return {
211
222
  identityMap: this.identityMap,
212
223
  unitOfWork: this.unitOfWork,
package/src/orm/orm.ts CHANGED
@@ -1,11 +1,12 @@
1
+ import type { DomainEvent, OrmDomainEvent } from './runtime-types.js';
1
2
  import type { Dialect } from '../core/dialect/abstract.js';
2
- import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
3
4
  import type { NamingStrategy } from '../codegen/naming-strategy.js';
4
5
  import { InterceptorPipeline } from './interceptor-pipeline.js';
5
6
  import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
6
- import { OrmSession } from './orm-session.ts';
7
+ import { OrmSession } from './orm-session.js';
7
8
 
8
- export interface OrmOptions {
9
+ export interface OrmOptions<E extends DomainEvent = OrmDomainEvent> {
9
10
  dialect: Dialect;
10
11
  executorFactory: DbExecutorFactory;
11
12
  interceptors?: InterceptorPipeline;
@@ -22,28 +23,27 @@ export interface ExternalTransaction {
22
23
  // Transaction-specific properties
23
24
  }
24
25
 
25
- export class Orm {
26
+ export class Orm<E extends DomainEvent = OrmDomainEvent> {
26
27
  readonly dialect: Dialect;
27
28
  readonly interceptors: InterceptorPipeline;
28
29
  readonly namingStrategy: NamingStrategy;
29
30
  private readonly executorFactory: DbExecutorFactory;
30
31
 
31
- constructor(opts: OrmOptions) {
32
+ constructor(opts: OrmOptions<E>) {
32
33
  this.dialect = opts.dialect;
33
34
  this.interceptors = opts.interceptors ?? new InterceptorPipeline();
34
35
  this.namingStrategy = opts.namingStrategy ?? new DefaultNamingStrategy();
35
36
  this.executorFactory = opts.executorFactory;
36
37
  }
37
38
 
38
- createSession(options?: { tx?: ExternalTransaction }): OrmSession {
39
+ createSession(options?: { tx?: ExternalTransaction }): OrmSession<E> {
39
40
  const executor = this.executorFactory.createExecutor(options?.tx);
40
- return new OrmSession({ orm: this, executor });
41
+ return new OrmSession<E>({ orm: this, executor });
41
42
  }
42
43
 
43
- // Nice convenience:
44
- async transaction<T>(fn: (session: OrmSession) => Promise<T>): Promise<T> {
44
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
45
45
  const executor = this.executorFactory.createTransactionalExecutor();
46
- const session = new OrmSession({ orm: this, executor });
46
+ const session = new OrmSession<E>({ orm: this, executor });
47
47
  try {
48
48
  const result = await fn(session);
49
49
  await session.commit();