metal-orm 1.0.8 → 1.0.9
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 +12 -1
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /package/src/{utils → query-builder}/relation-alias.ts +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { Dialect } from '../core/dialect/abstract.js';
|
|
2
|
+
import type { RelationDef } from '../schema/relation.js';
|
|
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';
|
|
18
|
+
|
|
19
|
+
export interface OrmInterceptor {
|
|
20
|
+
beforeFlush?(ctx: OrmContext): Promise<void> | void;
|
|
21
|
+
afterFlush?(ctx: OrmContext): Promise<void> | void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DomainEventHandler = DomainEventHandlerFn<OrmContext>;
|
|
25
|
+
|
|
26
|
+
export interface OrmContextOptions {
|
|
27
|
+
dialect: Dialect;
|
|
28
|
+
executor: DbExecutor;
|
|
29
|
+
interceptors?: OrmInterceptor[];
|
|
30
|
+
domainEventHandlers?: Record<string, DomainEventHandler[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class OrmContext {
|
|
34
|
+
private readonly identityMap = new IdentityMap();
|
|
35
|
+
private readonly unitOfWork: UnitOfWork;
|
|
36
|
+
private readonly relationChanges: RelationChangeProcessor;
|
|
37
|
+
private readonly interceptors: OrmInterceptor[];
|
|
38
|
+
private readonly domainEvents: DomainEventBus<OrmContext>;
|
|
39
|
+
|
|
40
|
+
constructor(private readonly options: OrmContextOptions) {
|
|
41
|
+
this.interceptors = [...(options.interceptors ?? [])];
|
|
42
|
+
this.unitOfWork = new UnitOfWork(
|
|
43
|
+
options.dialect,
|
|
44
|
+
options.executor,
|
|
45
|
+
this.identityMap,
|
|
46
|
+
() => this
|
|
47
|
+
);
|
|
48
|
+
this.relationChanges = new RelationChangeProcessor(
|
|
49
|
+
this.unitOfWork,
|
|
50
|
+
options.dialect,
|
|
51
|
+
options.executor
|
|
52
|
+
);
|
|
53
|
+
this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get dialect(): Dialect {
|
|
57
|
+
return this.options.dialect;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get executor(): DbExecutor {
|
|
61
|
+
return this.options.executor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
65
|
+
return this.unitOfWork.identityBuckets;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get tracked(): TrackedEntity[] {
|
|
69
|
+
return this.unitOfWork.getTracked();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getEntity(table: TableDef, pk: string | number): any | undefined {
|
|
73
|
+
return this.unitOfWork.getEntity(table, pk);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setEntity(table: TableDef, pk: string | number, entity: any): void {
|
|
77
|
+
this.unitOfWork.setEntity(table, pk, entity);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
trackNew(table: TableDef, entity: any, pk?: string | number): void {
|
|
81
|
+
this.unitOfWork.trackNew(table, entity, pk);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
trackManaged(table: TableDef, pk: string | number, entity: any): void {
|
|
85
|
+
this.unitOfWork.trackManaged(table, pk, entity);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
markDirty(entity: any): void {
|
|
89
|
+
this.unitOfWork.markDirty(entity);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
markRemoved(entity: any): void {
|
|
93
|
+
this.unitOfWork.markRemoved(entity);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
registerRelationChange(
|
|
97
|
+
root: any,
|
|
98
|
+
relationKey: RelationKey,
|
|
99
|
+
rootTable: TableDef,
|
|
100
|
+
relationName: string,
|
|
101
|
+
relation: RelationDef,
|
|
102
|
+
change: RelationChange<any>
|
|
103
|
+
): void {
|
|
104
|
+
const entry: RelationChangeEntry = {
|
|
105
|
+
root,
|
|
106
|
+
relationKey,
|
|
107
|
+
rootTable,
|
|
108
|
+
relationName,
|
|
109
|
+
relation,
|
|
110
|
+
change
|
|
111
|
+
};
|
|
112
|
+
this.relationChanges.registerChange(entry);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
registerInterceptor(interceptor: OrmInterceptor): void {
|
|
116
|
+
this.interceptors.push(interceptor);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
registerDomainEventHandler(name: string, handler: DomainEventHandler): void {
|
|
120
|
+
this.domainEvents.register(name, handler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async saveChanges(): Promise<void> {
|
|
124
|
+
await runInTransaction(this.executor, async () => {
|
|
125
|
+
for (const interceptor of this.interceptors) {
|
|
126
|
+
await interceptor.beforeFlush?.(this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await this.unitOfWork.flush();
|
|
130
|
+
await this.relationChanges.process();
|
|
131
|
+
await this.unitOfWork.flush();
|
|
132
|
+
|
|
133
|
+
for (const interceptor of this.interceptors) {
|
|
134
|
+
await interceptor.afterFlush?.(this);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getEntitiesForTable(table: TableDef): TrackedEntity[] {
|
|
142
|
+
return this.unitOfWork.getEntitiesForTable(table);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { addDomainEvent };
|
|
147
|
+
export { EntityStatus };
|
|
148
|
+
export type {
|
|
149
|
+
QueryResult,
|
|
150
|
+
DbExecutor,
|
|
151
|
+
RelationKey,
|
|
152
|
+
RelationChange,
|
|
153
|
+
HasDomainEvents
|
|
154
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { and, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { Dialect } from '../core/dialect/abstract.js';
|
|
3
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
4
|
+
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
5
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
+
import type { BelongsToManyRelation, HasManyRelation } from '../schema/relation.js';
|
|
7
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
8
|
+
import type { TableDef } from '../schema/table.js';
|
|
9
|
+
import type { DbExecutor } from './db-executor.js';
|
|
10
|
+
import type { RelationChangeEntry } from './runtime-types.js';
|
|
11
|
+
import { UnitOfWork } from './unit-of-work.js';
|
|
12
|
+
|
|
13
|
+
export class RelationChangeProcessor {
|
|
14
|
+
private readonly relationChanges: RelationChangeEntry[] = [];
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly unitOfWork: UnitOfWork,
|
|
18
|
+
private readonly dialect: Dialect,
|
|
19
|
+
private readonly executor: DbExecutor
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
registerChange(entry: RelationChangeEntry): void {
|
|
23
|
+
this.relationChanges.push(entry);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async process(): Promise<void> {
|
|
27
|
+
if (!this.relationChanges.length) return;
|
|
28
|
+
const entries = [...this.relationChanges];
|
|
29
|
+
this.relationChanges.length = 0;
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
switch (entry.relation.type) {
|
|
33
|
+
case RelationKinds.HasMany:
|
|
34
|
+
await this.handleHasManyChange(entry);
|
|
35
|
+
break;
|
|
36
|
+
case RelationKinds.BelongsToMany:
|
|
37
|
+
await this.handleBelongsToManyChange(entry);
|
|
38
|
+
break;
|
|
39
|
+
case RelationKinds.BelongsTo:
|
|
40
|
+
await this.handleBelongsToChange(entry);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
47
|
+
const relation = entry.relation as HasManyRelation;
|
|
48
|
+
const target = entry.change.entity;
|
|
49
|
+
if (!target) return;
|
|
50
|
+
|
|
51
|
+
const tracked = this.unitOfWork.findTracked(target);
|
|
52
|
+
if (!tracked) return;
|
|
53
|
+
|
|
54
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
55
|
+
const rootValue = entry.root[localKey];
|
|
56
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
57
|
+
|
|
58
|
+
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
59
|
+
this.assignHasManyForeignKey(tracked.entity, relation, rootValue);
|
|
60
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (entry.change.kind === 'remove') {
|
|
65
|
+
this.detachHasManyChild(tracked.entity, relation);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
70
|
+
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
74
|
+
const relation = entry.relation as BelongsToManyRelation;
|
|
75
|
+
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
76
|
+
const rootId = entry.root[rootKey];
|
|
77
|
+
if (rootId === undefined || rootId === null) return;
|
|
78
|
+
|
|
79
|
+
const targetId = this.resolvePrimaryKeyValue(entry.change.entity, relation.target);
|
|
80
|
+
if (targetId === null) return;
|
|
81
|
+
|
|
82
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
83
|
+
await this.insertPivotRow(relation, rootId, targetId);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (entry.change.kind === 'detach' || entry.change.kind === 'remove') {
|
|
88
|
+
await this.deletePivotRow(relation, rootId, targetId);
|
|
89
|
+
|
|
90
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
91
|
+
this.unitOfWork.markRemoved(entry.change.entity);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private assignHasManyForeignKey(child: any, relation: HasManyRelation, rootValue: unknown): void {
|
|
97
|
+
const current = child[relation.foreignKey];
|
|
98
|
+
if (current === rootValue) return;
|
|
99
|
+
child[relation.foreignKey] = rootValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private detachHasManyChild(child: any, relation: HasManyRelation): void {
|
|
103
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
104
|
+
this.unitOfWork.markRemoved(child);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
child[relation.foreignKey] = null;
|
|
108
|
+
this.unitOfWork.markDirty(child);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
112
|
+
const payload = {
|
|
113
|
+
[relation.pivotForeignKeyToRoot]: rootId,
|
|
114
|
+
[relation.pivotForeignKeyToTarget]: targetId
|
|
115
|
+
};
|
|
116
|
+
const builder = new InsertQueryBuilder(relation.pivotTable).values(payload);
|
|
117
|
+
const compiled = builder.compile(this.dialect);
|
|
118
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
122
|
+
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
123
|
+
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
124
|
+
if (!rootCol || !targetCol) return;
|
|
125
|
+
|
|
126
|
+
const builder = new DeleteQueryBuilder(relation.pivotTable).where(
|
|
127
|
+
and(eq(rootCol, rootId), eq(targetCol, targetId))
|
|
128
|
+
);
|
|
129
|
+
const compiled = builder.compile(this.dialect);
|
|
130
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private resolvePrimaryKeyValue(entity: any, table: TableDef): string | number | null {
|
|
134
|
+
if (!entity) return null;
|
|
135
|
+
const key = findPrimaryKey(table);
|
|
136
|
+
const value = entity[key];
|
|
137
|
+
if (value === undefined || value === null) return null;
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { BelongsToReference } from '../../schema/types';
|
|
2
|
-
import { OrmContext, RelationKey } from '../orm-context';
|
|
3
|
-
import { BelongsToRelation } from '../../schema/relation';
|
|
4
|
-
import { TableDef } from '../../schema/table';
|
|
5
|
-
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta';
|
|
6
|
-
|
|
7
|
-
type Rows = Record<string, any>;
|
|
8
|
-
|
|
9
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
10
|
-
|
|
11
|
-
export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
|
|
12
|
-
private loaded = false;
|
|
13
|
-
private current: TParent | null = null;
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly ctx: OrmContext,
|
|
17
|
-
private readonly meta: EntityMeta<any>,
|
|
18
|
-
private readonly root: any,
|
|
19
|
-
private readonly relationName: string,
|
|
20
|
-
private readonly relation: BelongsToRelation,
|
|
21
|
-
private readonly rootTable: TableDef,
|
|
22
|
-
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
23
|
-
private readonly createEntity: (row: Record<string, any>) => TParent,
|
|
24
|
-
private readonly targetKey: string
|
|
25
|
-
) {
|
|
26
|
-
this.populateFromHydrationCache();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async load(): Promise<TParent | null> {
|
|
30
|
-
if (this.loaded) return this.current;
|
|
31
|
-
const map = await this.loader();
|
|
32
|
-
const fkValue = this.root[this.relation.foreignKey];
|
|
33
|
-
if (fkValue === null || fkValue === undefined) {
|
|
34
|
-
this.current = null;
|
|
35
|
-
} else {
|
|
36
|
-
const row = map.get(toKey(fkValue));
|
|
37
|
-
this.current = row ? this.createEntity(row) : null;
|
|
38
|
-
}
|
|
39
|
-
this.loaded = true;
|
|
40
|
-
return this.current;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get(): TParent | null {
|
|
44
|
-
return this.current;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
set(data: Partial<TParent> | TParent | null): TParent | null {
|
|
48
|
-
if (data === null) {
|
|
49
|
-
const previous = this.current;
|
|
50
|
-
this.root[this.relation.foreignKey] = null;
|
|
51
|
-
this.current = null;
|
|
52
|
-
this.ctx.registerRelationChange(
|
|
53
|
-
this.root,
|
|
54
|
-
this.relationKey,
|
|
55
|
-
this.rootTable,
|
|
56
|
-
this.relationName,
|
|
57
|
-
this.relation,
|
|
58
|
-
{ kind: 'remove', entity: previous }
|
|
59
|
-
);
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, any>);
|
|
64
|
-
const pkValue = (entity as any)[this.targetKey];
|
|
65
|
-
if (pkValue !== undefined) {
|
|
66
|
-
this.root[this.relation.foreignKey] = pkValue;
|
|
67
|
-
}
|
|
68
|
-
this.current = entity;
|
|
69
|
-
this.ctx.registerRelationChange(
|
|
70
|
-
this.root,
|
|
71
|
-
this.relationKey,
|
|
72
|
-
this.rootTable,
|
|
73
|
-
this.relationName,
|
|
74
|
-
this.relation,
|
|
75
|
-
{ kind: 'attach', entity }
|
|
76
|
-
);
|
|
77
|
-
return entity;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private get relationKey(): RelationKey {
|
|
81
|
-
return `${this.rootTable.name}.${this.relationName}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
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
|
-
}
|
|
1
|
+
import { BelongsToReference } from '../../schema/types.js';
|
|
2
|
+
import { OrmContext, RelationKey } from '../orm-context.js';
|
|
3
|
+
import { BelongsToRelation } from '../../schema/relation.js';
|
|
4
|
+
import { TableDef } from '../../schema/table.js';
|
|
5
|
+
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
|
|
6
|
+
|
|
7
|
+
type Rows = Record<string, any>;
|
|
8
|
+
|
|
9
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
10
|
+
|
|
11
|
+
export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
|
|
12
|
+
private loaded = false;
|
|
13
|
+
private current: TParent | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly ctx: OrmContext,
|
|
17
|
+
private readonly meta: EntityMeta<any>,
|
|
18
|
+
private readonly root: any,
|
|
19
|
+
private readonly relationName: string,
|
|
20
|
+
private readonly relation: BelongsToRelation,
|
|
21
|
+
private readonly rootTable: TableDef,
|
|
22
|
+
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
23
|
+
private readonly createEntity: (row: Record<string, any>) => TParent,
|
|
24
|
+
private readonly targetKey: string
|
|
25
|
+
) {
|
|
26
|
+
this.populateFromHydrationCache();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async load(): Promise<TParent | null> {
|
|
30
|
+
if (this.loaded) return this.current;
|
|
31
|
+
const map = await this.loader();
|
|
32
|
+
const fkValue = this.root[this.relation.foreignKey];
|
|
33
|
+
if (fkValue === null || fkValue === undefined) {
|
|
34
|
+
this.current = null;
|
|
35
|
+
} else {
|
|
36
|
+
const row = map.get(toKey(fkValue));
|
|
37
|
+
this.current = row ? this.createEntity(row) : null;
|
|
38
|
+
}
|
|
39
|
+
this.loaded = true;
|
|
40
|
+
return this.current;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get(): TParent | null {
|
|
44
|
+
return this.current;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
set(data: Partial<TParent> | TParent | null): TParent | null {
|
|
48
|
+
if (data === null) {
|
|
49
|
+
const previous = this.current;
|
|
50
|
+
this.root[this.relation.foreignKey] = null;
|
|
51
|
+
this.current = null;
|
|
52
|
+
this.ctx.registerRelationChange(
|
|
53
|
+
this.root,
|
|
54
|
+
this.relationKey,
|
|
55
|
+
this.rootTable,
|
|
56
|
+
this.relationName,
|
|
57
|
+
this.relation,
|
|
58
|
+
{ kind: 'remove', entity: previous }
|
|
59
|
+
);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, any>);
|
|
64
|
+
const pkValue = (entity as any)[this.targetKey];
|
|
65
|
+
if (pkValue !== undefined) {
|
|
66
|
+
this.root[this.relation.foreignKey] = pkValue;
|
|
67
|
+
}
|
|
68
|
+
this.current = entity;
|
|
69
|
+
this.ctx.registerRelationChange(
|
|
70
|
+
this.root,
|
|
71
|
+
this.relationKey,
|
|
72
|
+
this.rootTable,
|
|
73
|
+
this.relationName,
|
|
74
|
+
this.relation,
|
|
75
|
+
{ kind: 'attach', entity }
|
|
76
|
+
);
|
|
77
|
+
return entity;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private get relationKey(): RelationKey {
|
|
81
|
+
return `${this.rootTable.name}.${this.relationName}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
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
|
+
}
|