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.
- package/README.md +64 -61
- package/dist/decorators/index.cjs +490 -175
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -5
- package/dist/decorators/index.d.ts +1 -5
- package/dist/decorators/index.js +490 -175
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +1044 -483
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -15
- package/dist/index.d.ts +67 -15
- package/dist/index.js +1033 -482
- package/dist/index.js.map +1 -1
- package/dist/{select-Bkv8g8u_.d.cts → select-BPCn6MOH.d.cts} +486 -32
- package/dist/{select-Bkv8g8u_.d.ts → select-BPCn6MOH.d.ts} +486 -32
- package/package.json +2 -1
- package/src/codegen/naming-strategy.ts +64 -0
- package/src/codegen/typescript.ts +48 -53
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/expression-builders.ts +22 -15
- package/src/core/ast/expression-nodes.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/ddl/schema-generator.ts +3 -2
- package/src/core/ddl/schema-introspect.ts +1 -1
- package/src/core/dialect/abstract.ts +40 -8
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/decorators/column.ts +13 -4
- package/src/index.ts +13 -5
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-context.ts +30 -0
- package/src/orm/entity-meta.ts +42 -2
- package/src/orm/entity-metadata.ts +1 -6
- package/src/orm/entity.ts +88 -88
- package/src/orm/execute.ts +42 -25
- package/src/orm/execution-context.ts +18 -0
- package/src/orm/hydration-context.ts +16 -0
- package/src/orm/identity-map.ts +4 -0
- package/src/orm/interceptor-pipeline.ts +29 -0
- package/src/orm/lazy-batch.ts +6 -6
- package/src/orm/orm-session.ts +245 -0
- package/src/orm/orm.ts +58 -0
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/relation-change-processor.ts +5 -1
- package/src/orm/relations/belongs-to.ts +45 -44
- package/src/orm/relations/has-many.ts +44 -43
- package/src/orm/relations/has-one.ts +140 -139
- package/src/orm/relations/many-to-many.ts +46 -45
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +7 -1
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select.ts +616 -18
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/types.ts +16 -6
- package/src/orm/orm-context.ts +0 -159
package/src/orm/execute.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
6
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
qb: SelectQueryBuilder<any, TTable>
|
|
28
|
-
): Promise<Entity<TTable>[]> {
|
|
29
|
-
const ast = qb.getAST();
|
|
30
|
-
const compiled =
|
|
31
|
-
const executed = await
|
|
32
|
-
const rows = flattenResults(executed);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
package/src/orm/identity-map.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/orm/lazy-batch.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|
package/src/orm/query-logger.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
+
}
|