metal-orm 1.0.12 → 1.0.13
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 +17 -15
- package/dist/decorators/index.cjs +302 -32
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +302 -32
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +583 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +583 -130
- package/dist/index.js.map +1 -1
- package/dist/{select-BKlr2ivY.d.cts → select-CCp1oz9p.d.cts} +114 -1
- package/dist/{select-BKlr2ivY.d.ts → select-CCp1oz9p.d.ts} +114 -1
- package/package.json +3 -2
- package/src/core/ast/query.ts +40 -22
- package/src/core/dialect/abstract.ts +122 -37
- package/src/core/dialect/base/sql-dialect.ts +51 -8
- package/src/core/dialect/mssql/index.ts +125 -80
- package/src/core/dialect/postgres/index.ts +5 -6
- package/src/core/dialect/sqlite/index.ts +5 -6
- package/src/orm/execute.ts +25 -16
- package/src/orm/orm-context.ts +60 -55
- package/src/orm/query-logger.ts +38 -0
- package/src/orm/relations/belongs-to.ts +42 -26
- package/src/orm/relations/has-many.ts +41 -25
- package/src/orm/relations/many-to-many.ts +43 -27
- package/src/orm/unit-of-work.ts +60 -23
- package/src/query-builder/hydration-manager.ts +229 -25
- package/src/query-builder/query-ast-service.ts +27 -12
- package/src/query-builder/select-query-state.ts +24 -12
- package/src/query-builder/select.ts +58 -14
package/src/orm/orm-context.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { Dialect } from '../core/dialect/abstract.js';
|
|
2
2
|
import type { RelationDef } from '../schema/relation.js';
|
|
3
3
|
import type { TableDef } from '../schema/table.js';
|
|
4
|
-
import type { DbExecutor, QueryResult } from './db-executor.js';
|
|
5
|
-
import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
|
|
6
|
-
import { IdentityMap } from './identity-map.js';
|
|
7
|
-
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
8
|
-
import { runInTransaction } from './transaction-runner.js';
|
|
9
|
-
import { UnitOfWork } from './unit-of-work.js';
|
|
10
|
-
import {
|
|
11
|
-
EntityStatus,
|
|
12
|
-
HasDomainEvents,
|
|
13
|
-
RelationChange,
|
|
14
|
-
RelationChangeEntry,
|
|
15
|
-
RelationKey,
|
|
16
|
-
TrackedEntity
|
|
17
|
-
} from './runtime-types.js';
|
|
4
|
+
import type { DbExecutor, QueryResult } from './db-executor.js';
|
|
5
|
+
import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
|
|
6
|
+
import { IdentityMap } from './identity-map.js';
|
|
7
|
+
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
8
|
+
import { runInTransaction } from './transaction-runner.js';
|
|
9
|
+
import { UnitOfWork } from './unit-of-work.js';
|
|
10
|
+
import {
|
|
11
|
+
EntityStatus,
|
|
12
|
+
HasDomainEvents,
|
|
13
|
+
RelationChange,
|
|
14
|
+
RelationChangeEntry,
|
|
15
|
+
RelationKey,
|
|
16
|
+
TrackedEntity
|
|
17
|
+
} from './runtime-types.js';
|
|
18
|
+
import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
|
|
18
19
|
|
|
19
20
|
export interface OrmInterceptor {
|
|
20
21
|
beforeFlush?(ctx: OrmContext): Promise<void> | void;
|
|
@@ -23,43 +24,46 @@ export interface OrmInterceptor {
|
|
|
23
24
|
|
|
24
25
|
export type DomainEventHandler = DomainEventHandlerFn<OrmContext>;
|
|
25
26
|
|
|
26
|
-
export interface OrmContextOptions {
|
|
27
|
-
dialect: Dialect;
|
|
28
|
-
executor: DbExecutor;
|
|
29
|
-
interceptors?: OrmInterceptor[];
|
|
30
|
-
domainEventHandlers?: Record<string, DomainEventHandler[]>;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
private readonly
|
|
36
|
-
private readonly
|
|
37
|
-
private readonly
|
|
38
|
-
private readonly
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
export interface OrmContextOptions {
|
|
28
|
+
dialect: Dialect;
|
|
29
|
+
executor: DbExecutor;
|
|
30
|
+
interceptors?: OrmInterceptor[];
|
|
31
|
+
domainEventHandlers?: Record<string, DomainEventHandler[]>;
|
|
32
|
+
queryLogger?: QueryLogger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class OrmContext {
|
|
36
|
+
private readonly identityMap = new IdentityMap();
|
|
37
|
+
private readonly executorWithLogging: DbExecutor;
|
|
38
|
+
private readonly unitOfWork: UnitOfWork;
|
|
39
|
+
private readonly relationChanges: RelationChangeProcessor;
|
|
40
|
+
private readonly interceptors: OrmInterceptor[];
|
|
41
|
+
private readonly domainEvents: DomainEventBus<OrmContext>;
|
|
42
|
+
|
|
43
|
+
constructor(private readonly options: OrmContextOptions) {
|
|
44
|
+
this.interceptors = [...(options.interceptors ?? [])];
|
|
45
|
+
this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
|
|
46
|
+
this.unitOfWork = new UnitOfWork(
|
|
47
|
+
options.dialect,
|
|
48
|
+
this.executorWithLogging,
|
|
49
|
+
this.identityMap,
|
|
50
|
+
() => this
|
|
51
|
+
);
|
|
52
|
+
this.relationChanges = new RelationChangeProcessor(
|
|
53
|
+
this.unitOfWork,
|
|
54
|
+
options.dialect,
|
|
55
|
+
this.executorWithLogging
|
|
56
|
+
);
|
|
57
|
+
this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
|
|
58
|
+
}
|
|
55
59
|
|
|
56
60
|
get dialect(): Dialect {
|
|
57
61
|
return this.options.dialect;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
get executor(): DbExecutor {
|
|
61
|
-
return this.
|
|
62
|
-
}
|
|
64
|
+
get executor(): DbExecutor {
|
|
65
|
+
return this.executorWithLogging;
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
65
69
|
return this.unitOfWork.identityBuckets;
|
|
@@ -143,12 +147,13 @@ export class OrmContext {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
export { addDomainEvent };
|
|
147
|
-
export { EntityStatus };
|
|
148
|
-
export type {
|
|
149
|
-
QueryResult,
|
|
150
|
-
DbExecutor,
|
|
151
|
-
RelationKey,
|
|
152
|
-
RelationChange,
|
|
153
|
-
HasDomainEvents
|
|
154
|
-
};
|
|
150
|
+
export { addDomainEvent };
|
|
151
|
+
export { EntityStatus };
|
|
152
|
+
export type {
|
|
153
|
+
QueryResult,
|
|
154
|
+
DbExecutor,
|
|
155
|
+
RelationKey,
|
|
156
|
+
RelationChange,
|
|
157
|
+
HasDomainEvents
|
|
158
|
+
};
|
|
159
|
+
export type { QueryLogEntry, QueryLogger } from './query-logger.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DbExecutor } from './db-executor.js';
|
|
2
|
+
|
|
3
|
+
export interface QueryLogEntry {
|
|
4
|
+
sql: string;
|
|
5
|
+
params?: unknown[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type QueryLogger = (entry: QueryLogEntry) => void;
|
|
9
|
+
|
|
10
|
+
export const createQueryLoggingExecutor = (
|
|
11
|
+
executor: DbExecutor,
|
|
12
|
+
logger?: QueryLogger
|
|
13
|
+
): DbExecutor => {
|
|
14
|
+
if (!logger) {
|
|
15
|
+
return executor;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const wrapped: DbExecutor = {
|
|
19
|
+
async executeSql(sql, params) {
|
|
20
|
+
logger({ sql, params });
|
|
21
|
+
return executor.executeSql(sql, params);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (executor.beginTransaction) {
|
|
26
|
+
wrapped.beginTransaction = executor.beginTransaction.bind(executor);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (executor.commitTransaction) {
|
|
30
|
+
wrapped.commitTransaction = executor.commitTransaction.bind(executor);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (executor.rollbackTransaction) {
|
|
34
|
+
wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return wrapped;
|
|
38
|
+
};
|
|
@@ -4,15 +4,26 @@ import { BelongsToRelation } from '../../schema/relation.js';
|
|
|
4
4
|
import { TableDef } from '../../schema/table.js';
|
|
5
5
|
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
|
|
6
6
|
|
|
7
|
-
type Rows = Record<string, any>;
|
|
8
|
-
|
|
9
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
16
27
|
private readonly ctx: OrmContext,
|
|
17
28
|
private readonly meta: EntityMeta<any>,
|
|
18
29
|
private readonly root: any,
|
|
@@ -20,14 +31,15 @@ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TP
|
|
|
20
31
|
private readonly relation: BelongsToRelation,
|
|
21
32
|
private readonly rootTable: TableDef,
|
|
22
33
|
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
23
|
-
private readonly createEntity: (row: Record<string, any>) => TParent,
|
|
24
|
-
private readonly targetKey: string
|
|
25
|
-
) {
|
|
26
|
-
this
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
43
|
const map = await this.loader();
|
|
32
44
|
const fkValue = this.root[this.relation.foreignKey];
|
|
33
45
|
if (fkValue === null || fkValue === undefined) {
|
|
@@ -81,12 +93,16 @@ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TP
|
|
|
81
93
|
return `${this.rootTable.name}.${this.relationName}`;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
|
-
private populateFromHydrationCache(): void {
|
|
85
|
-
const fkValue = this.root[this.relation.foreignKey];
|
|
86
|
-
if (fkValue === undefined || fkValue === null) return;
|
|
87
|
-
const row = getHydrationRecord(this.meta, this.relationName, fkValue);
|
|
88
|
-
if (!row) return;
|
|
89
|
-
this.current = this.createEntity(row);
|
|
90
|
-
this.loaded = true;
|
|
91
|
-
}
|
|
92
|
-
|
|
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
|
+
}
|
|
@@ -4,15 +4,26 @@ import { HasManyRelation } from '../../schema/relation.js';
|
|
|
4
4
|
import { TableDef } from '../../schema/table.js';
|
|
5
5
|
import { EntityMeta, getHydrationRows } from '../entity-meta.js';
|
|
6
6
|
|
|
7
|
-
type Rows = Record<string, any>[];
|
|
8
|
-
|
|
9
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
|
|
23
|
+
private loaded = false;
|
|
24
|
+
private items: TChild[] = [];
|
|
25
|
+
private readonly added = new Set<TChild>();
|
|
26
|
+
private readonly removed = new Set<TChild>();
|
|
16
27
|
|
|
17
28
|
constructor(
|
|
18
29
|
private readonly ctx: OrmContext,
|
|
@@ -23,13 +34,14 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
23
34
|
private readonly rootTable: TableDef,
|
|
24
35
|
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
25
36
|
private readonly createEntity: (row: Record<string, any>) => TChild,
|
|
26
|
-
private readonly localKey: string
|
|
27
|
-
) {
|
|
28
|
-
this
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
private readonly localKey: string
|
|
38
|
+
) {
|
|
39
|
+
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
|
|
40
|
+
this.hydrateFromCache();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async load(): Promise<TChild[]> {
|
|
44
|
+
if (this.loaded) return this.items;
|
|
33
45
|
const map = await this.loader();
|
|
34
46
|
const key = toKey(this.root[this.localKey]);
|
|
35
47
|
const rows = map.get(key) ?? [];
|
|
@@ -100,12 +112,16 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
100
112
|
return `${this.rootTable.name}.${this.relationName}`;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
private hydrateFromCache(): void {
|
|
104
|
-
const keyValue = this.root[this.localKey];
|
|
105
|
-
if (keyValue === undefined || keyValue === null) return;
|
|
106
|
-
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
107
|
-
if (!rows?.length) return;
|
|
108
|
-
this.items = rows.map(row => this.createEntity(row));
|
|
109
|
-
this.loaded = true;
|
|
110
|
-
}
|
|
111
|
-
|
|
115
|
+
private hydrateFromCache(): void {
|
|
116
|
+
const keyValue = this.root[this.localKey];
|
|
117
|
+
if (keyValue === undefined || keyValue === null) return;
|
|
118
|
+
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
119
|
+
if (!rows?.length) return;
|
|
120
|
+
this.items = rows.map(row => this.createEntity(row));
|
|
121
|
+
this.loaded = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toJSON(): TChild[] {
|
|
125
|
+
return this.items;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -5,15 +5,26 @@ import { TableDef } from '../../schema/table.js';
|
|
|
5
5
|
import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
|
|
6
6
|
import { EntityMeta, getHydrationRows } from '../entity-meta.js';
|
|
7
7
|
|
|
8
|
-
type Rows = Record<string, any>[];
|
|
9
|
-
|
|
10
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
|
|
24
|
+
private loaded = false;
|
|
25
|
+
private items: TTarget[] = [];
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
17
28
|
private readonly ctx: OrmContext,
|
|
18
29
|
private readonly meta: EntityMeta<any>,
|
|
19
30
|
private readonly root: any,
|
|
@@ -21,14 +32,15 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
21
32
|
private readonly relation: BelongsToManyRelation,
|
|
22
33
|
private readonly rootTable: TableDef,
|
|
23
34
|
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
24
|
-
private readonly createEntity: (row: Record<string, any>) => TTarget,
|
|
25
|
-
private readonly localKey: string
|
|
26
|
-
) {
|
|
27
|
-
this
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
private readonly createEntity: (row: Record<string, any>) => TTarget,
|
|
36
|
+
private readonly localKey: string
|
|
37
|
+
) {
|
|
38
|
+
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
|
|
39
|
+
this.hydrateFromCache();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async load(): Promise<TTarget[]> {
|
|
43
|
+
if (this.loaded) return this.items;
|
|
32
44
|
const map = await this.loader();
|
|
33
45
|
const key = toKey(this.root[this.localKey]);
|
|
34
46
|
const rows = map.get(key) ?? [];
|
|
@@ -132,18 +144,22 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
132
144
|
return this.relation.targetKey || findPrimaryKey(this.relation.target);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
private hydrateFromCache(): void {
|
|
136
|
-
const keyValue = this.root[this.localKey];
|
|
137
|
-
if (keyValue === undefined || keyValue === null) return;
|
|
138
|
-
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
139
|
-
if (!rows?.length) return;
|
|
147
|
+
private hydrateFromCache(): void {
|
|
148
|
+
const keyValue = this.root[this.localKey];
|
|
149
|
+
if (keyValue === undefined || keyValue === null) return;
|
|
150
|
+
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
151
|
+
if (!rows?.length) return;
|
|
140
152
|
this.items = rows.map(row => {
|
|
141
153
|
const entity = this.createEntity(row);
|
|
142
154
|
if ((row as any)._pivot) {
|
|
143
155
|
(entity as any)._pivot = (row as any)._pivot;
|
|
144
156
|
}
|
|
145
|
-
return entity;
|
|
146
|
-
});
|
|
147
|
-
this.loaded = true;
|
|
148
|
-
}
|
|
149
|
-
|
|
157
|
+
return entity;
|
|
158
|
+
});
|
|
159
|
+
this.loaded = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
toJSON(): TTarget[] {
|
|
163
|
+
return this.items;
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { eq } from '../core/ast/expression.js';
|
|
2
|
-
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
1
|
+
import { ColumnNode, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
|
|
3
3
|
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
4
4
|
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
5
5
|
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
6
6
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
7
7
|
import type { TableDef, TableHooks } from '../schema/table.js';
|
|
8
|
-
import type { DbExecutor } from './db-executor.js';
|
|
8
|
+
import type { DbExecutor, QueryResult } from './db-executor.js';
|
|
9
9
|
import { IdentityMap } from './identity-map.js';
|
|
10
10
|
import { EntityStatus } from './runtime-types.js';
|
|
11
11
|
import type { TrackedEntity } from './runtime-types.js';
|
|
@@ -120,10 +120,14 @@ export class UnitOfWork {
|
|
|
120
120
|
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
121
121
|
await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
|
|
122
122
|
|
|
123
|
-
const payload = this.extractColumns(tracked.table, tracked.entity);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
const payload = this.extractColumns(tracked.table, tracked.entity);
|
|
124
|
+
let builder = new InsertQueryBuilder(tracked.table).values(payload);
|
|
125
|
+
if (this.dialect.supportsReturning()) {
|
|
126
|
+
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
127
|
+
}
|
|
128
|
+
const compiled = builder.compile(this.dialect);
|
|
129
|
+
const results = await this.executeCompiled(compiled);
|
|
130
|
+
this.applyReturningResults(tracked, results);
|
|
127
131
|
|
|
128
132
|
tracked.status = EntityStatus.Managed;
|
|
129
133
|
tracked.original = this.createSnapshot(tracked.table, tracked.entity);
|
|
@@ -146,12 +150,17 @@ export class UnitOfWork {
|
|
|
146
150
|
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
147
151
|
if (!pkColumn) return;
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
.set(changes)
|
|
151
|
-
.where(eq(pkColumn, tracked.pk));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
let builder = new UpdateQueryBuilder(tracked.table)
|
|
154
|
+
.set(changes)
|
|
155
|
+
.where(eq(pkColumn, tracked.pk));
|
|
156
|
+
|
|
157
|
+
if (this.dialect.supportsReturning()) {
|
|
158
|
+
builder = builder.returning(...this.getReturningColumns(tracked.table));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const compiled = builder.compile(this.dialect);
|
|
162
|
+
const results = await this.executeCompiled(compiled);
|
|
163
|
+
this.applyReturningResults(tracked, results);
|
|
155
164
|
|
|
156
165
|
tracked.status = EntityStatus.Managed;
|
|
157
166
|
tracked.original = this.createSnapshot(tracked.table, tracked.entity);
|
|
@@ -206,16 +215,44 @@ export class UnitOfWork {
|
|
|
206
215
|
return payload;
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
private async executeCompiled(compiled: CompiledQuery): Promise<
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
|
|
219
|
+
return this.executor.executeSql(compiled.sql, compiled.params);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private getReturningColumns(table: TableDef): ColumnNode[] {
|
|
223
|
+
return Object.values(table.columns).map(column => ({
|
|
224
|
+
type: 'Column',
|
|
225
|
+
table: table.name,
|
|
226
|
+
name: column.name,
|
|
227
|
+
alias: column.name
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
|
|
232
|
+
if (!this.dialect.supportsReturning()) return;
|
|
233
|
+
const first = results[0];
|
|
234
|
+
if (!first || first.values.length === 0) return;
|
|
235
|
+
|
|
236
|
+
const row = first.values[0];
|
|
237
|
+
for (let i = 0; i < first.columns.length; i++) {
|
|
238
|
+
const columnName = this.normalizeColumnName(first.columns[i]);
|
|
239
|
+
if (!(columnName in tracked.table.columns)) continue;
|
|
240
|
+
tracked.entity[columnName] = row[i];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private normalizeColumnName(column: string): string {
|
|
245
|
+
const parts = column.split('.');
|
|
246
|
+
const candidate = parts[parts.length - 1];
|
|
247
|
+
return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private registerIdentity(tracked: TrackedEntity): void {
|
|
251
|
+
if (tracked.pk == null) return;
|
|
252
|
+
this.identityMap.register(tracked);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private createSnapshot(table: TableDef, entity: any): Record<string, any> {
|
|
219
256
|
const snapshot: Record<string, any> = {};
|
|
220
257
|
for (const column of Object.keys(table.columns)) {
|
|
221
258
|
snapshot[column] = entity[column];
|