metal-orm 1.0.13 → 1.0.15
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 +75 -82
- package/dist/decorators/index.cjs +1600 -27
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +6 -2
- package/dist/decorators/index.d.ts +6 -2
- package/dist/decorators/index.js +1599 -27
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +4608 -3429
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +511 -159
- package/dist/index.d.ts +511 -159
- package/dist/index.js +4526 -3415
- package/dist/index.js.map +1 -1
- package/dist/{select-CCp1oz9p.d.cts → select-Bkv8g8u_.d.cts} +193 -67
- package/dist/{select-CCp1oz9p.d.ts → select-Bkv8g8u_.d.ts} +193 -67
- package/package.json +1 -1
- package/src/codegen/typescript.ts +38 -35
- package/src/core/ast/adapters.ts +21 -0
- package/src/core/ast/aggregate-functions.ts +13 -13
- package/src/core/ast/builders.ts +56 -43
- package/src/core/ast/expression-builders.ts +34 -34
- package/src/core/ast/expression-nodes.ts +18 -16
- package/src/core/ast/expression-visitor.ts +122 -69
- package/src/core/ast/expression.ts +6 -4
- package/src/core/ast/join-metadata.ts +15 -0
- package/src/core/ast/join-node.ts +22 -20
- package/src/core/ast/join.ts +5 -5
- package/src/core/ast/query.ts +52 -88
- package/src/core/ast/types.ts +20 -0
- package/src/core/ast/window-functions.ts +55 -55
- package/src/core/ddl/dialects/base-schema-dialect.ts +20 -6
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +32 -8
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +21 -10
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +52 -7
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +23 -9
- package/src/core/ddl/introspect/catalogs/index.ts +1 -0
- package/src/core/ddl/introspect/catalogs/postgres.ts +143 -0
- package/src/core/ddl/introspect/context.ts +9 -0
- package/src/core/ddl/introspect/functions/postgres.ts +26 -0
- package/src/core/ddl/introspect/mssql.ts +149 -149
- package/src/core/ddl/introspect/mysql.ts +99 -99
- package/src/core/ddl/introspect/postgres.ts +245 -154
- package/src/core/ddl/introspect/registry.ts +26 -0
- package/src/core/ddl/introspect/run-select.ts +25 -0
- package/src/core/ddl/introspect/sqlite.ts +7 -7
- package/src/core/ddl/introspect/types.ts +23 -19
- package/src/core/ddl/introspect/utils.ts +1 -1
- package/src/core/ddl/naming-strategy.ts +10 -0
- package/src/core/ddl/schema-dialect.ts +41 -0
- package/src/core/ddl/schema-diff.ts +211 -179
- package/src/core/ddl/schema-generator.ts +16 -90
- package/src/core/ddl/schema-introspect.ts +25 -32
- package/src/core/ddl/schema-plan-executor.ts +17 -0
- package/src/core/ddl/schema-types.ts +46 -39
- package/src/core/ddl/sql-writing.ts +170 -0
- package/src/core/dialect/abstract.ts +144 -126
- package/src/core/dialect/base/cte-compiler.ts +33 -0
- package/src/core/dialect/base/function-table-formatter.ts +132 -0
- package/src/core/dialect/base/groupby-compiler.ts +21 -0
- package/src/core/dialect/base/join-compiler.ts +26 -0
- package/src/core/dialect/base/orderby-compiler.ts +21 -0
- package/src/core/dialect/base/pagination-strategy.ts +32 -0
- package/src/core/dialect/base/returning-strategy.ts +56 -0
- package/src/core/dialect/base/sql-dialect.ts +181 -204
- package/src/core/dialect/dialect-factory.ts +91 -0
- package/src/core/dialect/mssql/functions.ts +101 -0
- package/src/core/dialect/mssql/index.ts +128 -126
- package/src/core/dialect/mysql/functions.ts +101 -0
- package/src/core/dialect/mysql/index.ts +20 -18
- package/src/core/dialect/postgres/functions.ts +95 -0
- package/src/core/dialect/postgres/index.ts +30 -28
- package/src/core/dialect/sqlite/functions.ts +115 -0
- package/src/core/dialect/sqlite/index.ts +30 -28
- package/src/core/driver/database-driver.ts +11 -0
- package/src/core/driver/mssql-driver.ts +20 -0
- package/src/core/driver/mysql-driver.ts +20 -0
- package/src/core/driver/postgres-driver.ts +20 -0
- package/src/core/driver/sqlite-driver.ts +20 -0
- package/src/core/execution/db-executor.ts +63 -0
- package/src/core/execution/executors/mssql-executor.ts +39 -0
- package/src/core/execution/executors/mysql-executor.ts +47 -0
- package/src/core/execution/executors/postgres-executor.ts +32 -0
- package/src/core/execution/executors/sqlite-executor.ts +31 -0
- package/src/core/functions/datetime.ts +132 -0
- package/src/core/functions/numeric.ts +179 -0
- package/src/core/functions/standard-strategy.ts +47 -0
- package/src/core/functions/text.ts +147 -0
- package/src/core/functions/types.ts +18 -0
- package/src/core/hydration/types.ts +57 -0
- package/src/decorators/bootstrap.ts +10 -0
- package/src/decorators/relations.ts +15 -0
- package/src/index.ts +30 -19
- package/src/orm/entity-metadata.ts +7 -0
- package/src/orm/entity.ts +58 -27
- package/src/orm/hydration.ts +25 -17
- package/src/orm/lazy-batch.ts +46 -2
- package/src/orm/orm-context.ts +60 -60
- package/src/orm/query-logger.ts +1 -1
- package/src/orm/relation-change-processor.ts +43 -2
- package/src/orm/relations/has-one.ts +139 -0
- package/src/orm/transaction-runner.ts +1 -1
- package/src/orm/unit-of-work.ts +60 -60
- package/src/query-builder/delete.ts +22 -5
- package/src/query-builder/hydration-manager.ts +2 -1
- package/src/query-builder/hydration-planner.ts +8 -7
- package/src/query-builder/insert.ts +22 -5
- package/src/query-builder/relation-conditions.ts +9 -8
- package/src/query-builder/relation-service.ts +3 -2
- package/src/query-builder/select.ts +66 -61
- package/src/query-builder/update.ts +22 -5
- package/src/schema/column.ts +246 -246
- package/src/schema/relation.ts +35 -1
- package/src/schema/table.ts +28 -28
- package/src/schema/types.ts +41 -31
- package/src/orm/db-executor.ts +0 -11
package/src/orm/entity.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { Entity, RelationMap, HasManyCollection, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
-
import { OrmContext } from './orm-context.js';
|
|
4
|
-
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
-
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
+
import { OrmContext } from './orm-context.js';
|
|
4
|
+
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
+
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
+
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
7
|
+
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
8
|
+
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
9
|
+
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
10
|
+
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
10
11
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
11
12
|
|
|
12
13
|
type Rows = Record<string, any>[];
|
|
@@ -135,10 +136,22 @@ const toKey = (value: unknown): string => (value === null || value === undefined
|
|
|
135
136
|
): void => {
|
|
136
137
|
for (const relationName of Object.keys(meta.table.relations)) {
|
|
137
138
|
const relation = meta.table.relations[relationName];
|
|
138
|
-
const data = row[relationName];
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const data = row[relationName];
|
|
140
|
+
if (relation.type === RelationKinds.HasOne) {
|
|
141
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
142
|
+
const rootValue = entity[localKey];
|
|
143
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
144
|
+
if (!data || typeof data !== 'object') continue;
|
|
145
|
+
const cache = new Map<string, Record<string, any>>();
|
|
146
|
+
cache.set(toKey(rootValue), data as Record<string, any>);
|
|
147
|
+
meta.relationHydration.set(relationName, cache);
|
|
148
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(data)) continue;
|
|
153
|
+
|
|
154
|
+
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
142
155
|
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
143
156
|
const rootValue = entity[localKey];
|
|
144
157
|
if (rootValue === undefined || rootValue === null) continue;
|
|
@@ -165,11 +178,11 @@ const toKey = (value: unknown): string => (value === null || value === undefined
|
|
|
165
178
|
}
|
|
166
179
|
};
|
|
167
180
|
|
|
168
|
-
const getRelationWrapper = (
|
|
169
|
-
meta: EntityMeta<any>,
|
|
170
|
-
relationName: string,
|
|
171
|
-
owner: any
|
|
172
|
-
): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
181
|
+
const getRelationWrapper = (
|
|
182
|
+
meta: EntityMeta<any>,
|
|
183
|
+
relationName: string,
|
|
184
|
+
owner: any
|
|
185
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
173
186
|
if (meta.relationWrappers.has(relationName)) {
|
|
174
187
|
return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
|
|
175
188
|
}
|
|
@@ -185,16 +198,34 @@ const getRelationWrapper = (
|
|
|
185
198
|
return wrapper;
|
|
186
199
|
};
|
|
187
200
|
|
|
188
|
-
const instantiateWrapper = (
|
|
189
|
-
meta: EntityMeta<any>,
|
|
190
|
-
relationName: string,
|
|
191
|
-
relation: HasManyRelation | BelongsToRelation | BelongsToManyRelation,
|
|
192
|
-
owner: any
|
|
193
|
-
): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
194
|
-
switch (relation.type) {
|
|
195
|
-
case RelationKinds.
|
|
196
|
-
const
|
|
197
|
-
const localKey =
|
|
201
|
+
const instantiateWrapper = (
|
|
202
|
+
meta: EntityMeta<any>,
|
|
203
|
+
relationName: string,
|
|
204
|
+
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
205
|
+
owner: any
|
|
206
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
207
|
+
switch (relation.type) {
|
|
208
|
+
case RelationKinds.HasOne: {
|
|
209
|
+
const hasOne = relation as HasOneRelation;
|
|
210
|
+
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
211
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
212
|
+
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
213
|
+
);
|
|
214
|
+
return new DefaultHasOneReference(
|
|
215
|
+
meta.ctx,
|
|
216
|
+
meta,
|
|
217
|
+
owner,
|
|
218
|
+
relationName,
|
|
219
|
+
hasOne,
|
|
220
|
+
meta.table,
|
|
221
|
+
loader,
|
|
222
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
223
|
+
localKey
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
case RelationKinds.HasMany: {
|
|
227
|
+
const hasMany = relation as HasManyRelation;
|
|
228
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
198
229
|
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
199
230
|
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
|
|
200
231
|
);
|
package/src/orm/hydration.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { HydrationPlan, HydrationRelationPlan } from '../core/
|
|
2
|
-
import {
|
|
1
|
+
import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
|
|
2
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
3
|
+
import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Hydrates query results according to a hydration plan
|
|
@@ -47,18 +48,25 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
|
|
|
47
48
|
const parent = getOrCreateParent(row);
|
|
48
49
|
if (!parent) continue;
|
|
49
50
|
|
|
50
|
-
for (const rel of plan.relations) {
|
|
51
|
-
const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
|
|
52
|
-
const childPk = row[childPkKey];
|
|
53
|
-
if (childPk === null || childPk === undefined) continue;
|
|
54
|
-
|
|
55
|
-
const seen = getRelationSeenSet(rootId, rel.name);
|
|
56
|
-
if (seen.has(childPk)) continue;
|
|
57
|
-
seen.add(childPk);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
for (const rel of plan.relations) {
|
|
52
|
+
const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
|
|
53
|
+
const childPk = row[childPkKey];
|
|
54
|
+
if (childPk === null || childPk === undefined) continue;
|
|
55
|
+
|
|
56
|
+
const seen = getRelationSeenSet(rootId, rel.name);
|
|
57
|
+
if (seen.has(childPk)) continue;
|
|
58
|
+
seen.add(childPk);
|
|
59
|
+
|
|
60
|
+
if (rel.type === RelationKinds.HasOne) {
|
|
61
|
+
if (!parent[rel.name]) {
|
|
62
|
+
parent[rel.name] = buildChild(row, rel);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const bucket = parent[rel.name] as any[];
|
|
68
|
+
bucket.push(buildChild(row, rel));
|
|
69
|
+
}
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
return Array.from(rootMap.values());
|
|
@@ -74,9 +82,9 @@ const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<st
|
|
|
74
82
|
base[key] = row[key];
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
for (const rel of plan.relations) {
|
|
78
|
-
base[rel.name] = [];
|
|
79
|
-
}
|
|
85
|
+
for (const rel of plan.relations) {
|
|
86
|
+
base[rel.name] = rel.type === RelationKinds.HasOne ? null : [];
|
|
87
|
+
}
|
|
80
88
|
|
|
81
89
|
return base;
|
|
82
90
|
};
|
package/src/orm/lazy-batch.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { BelongsToManyRelation, HasManyRelation, BelongsToRelation } from '../schema/relation.js';
|
|
2
|
+
import { BelongsToManyRelation, HasManyRelation, HasOneRelation, BelongsToRelation } from '../schema/relation.js';
|
|
3
3
|
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
4
4
|
import { inList, LiteralNode } from '../core/ast/expression.js';
|
|
5
|
-
import { OrmContext
|
|
5
|
+
import { OrmContext } from './orm-context.js';
|
|
6
|
+
import type { QueryResult } from '../core/execution/db-executor.js';
|
|
6
7
|
import { ColumnDef } from '../schema/column.js';
|
|
7
8
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
9
|
|
|
@@ -80,6 +81,49 @@ export const loadHasManyRelation = async (
|
|
|
80
81
|
return grouped;
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
export const loadHasOneRelation = async (
|
|
85
|
+
ctx: OrmContext,
|
|
86
|
+
rootTable: TableDef,
|
|
87
|
+
_relationName: string,
|
|
88
|
+
relation: HasOneRelation
|
|
89
|
+
): Promise<Map<string, Record<string, any>>> => {
|
|
90
|
+
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
91
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
92
|
+
const keys = new Set<unknown>();
|
|
93
|
+
|
|
94
|
+
for (const tracked of roots) {
|
|
95
|
+
const value = tracked.entity[localKey];
|
|
96
|
+
if (value !== null && value !== undefined) {
|
|
97
|
+
keys.add(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!keys.size) {
|
|
102
|
+
return new Map();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const selectMap = selectAllColumns(relation.target);
|
|
106
|
+
const qb = new SelectQueryBuilder(relation.target).select(selectMap);
|
|
107
|
+
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
108
|
+
if (!fkColumn) return new Map();
|
|
109
|
+
|
|
110
|
+
qb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
|
|
111
|
+
|
|
112
|
+
const rows = await executeQuery(ctx, qb);
|
|
113
|
+
const lookup = new Map<string, Record<string, any>>();
|
|
114
|
+
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const fkValue = row[relation.foreignKey];
|
|
117
|
+
if (fkValue === null || fkValue === undefined) continue;
|
|
118
|
+
const key = toKey(fkValue);
|
|
119
|
+
if (!lookup.has(key)) {
|
|
120
|
+
lookup.set(key, row);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lookup;
|
|
125
|
+
};
|
|
126
|
+
|
|
83
127
|
export const loadBelongsToRelation = async (
|
|
84
128
|
ctx: OrmContext,
|
|
85
129
|
rootTable: TableDef,
|
package/src/orm/orm-context.ts
CHANGED
|
@@ -1,21 +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 '
|
|
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';
|
|
4
|
+
import type { DbExecutor, QueryResult } from '../core/execution/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';
|
|
19
19
|
|
|
20
20
|
export interface OrmInterceptor {
|
|
21
21
|
beforeFlush?(ctx: OrmContext): Promise<void> | void;
|
|
@@ -24,46 +24,46 @@ export interface OrmInterceptor {
|
|
|
24
24
|
|
|
25
25
|
export type DomainEventHandler = DomainEventHandlerFn<OrmContext>;
|
|
26
26
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
59
59
|
|
|
60
60
|
get dialect(): Dialect {
|
|
61
61
|
return this.options.dialect;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
get executor(): DbExecutor {
|
|
65
|
-
return this.executorWithLogging;
|
|
66
|
-
}
|
|
64
|
+
get executor(): DbExecutor {
|
|
65
|
+
return this.executorWithLogging;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
68
|
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
69
69
|
return this.unitOfWork.identityBuckets;
|
|
@@ -147,13 +147,13 @@ export class OrmContext {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
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';
|
|
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';
|
package/src/orm/query-logger.ts
CHANGED
|
@@ -3,10 +3,10 @@ import type { Dialect } from '../core/dialect/abstract.js';
|
|
|
3
3
|
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
4
4
|
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
5
5
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
-
import type { BelongsToManyRelation, HasManyRelation } from '../schema/relation.js';
|
|
6
|
+
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation } from '../schema/relation.js';
|
|
7
7
|
import { RelationKinds } from '../schema/relation.js';
|
|
8
8
|
import type { TableDef } from '../schema/table.js';
|
|
9
|
-
import type { DbExecutor } from '
|
|
9
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
10
10
|
import type { RelationChangeEntry } from './runtime-types.js';
|
|
11
11
|
import { UnitOfWork } from './unit-of-work.js';
|
|
12
12
|
|
|
@@ -33,6 +33,9 @@ export class RelationChangeProcessor {
|
|
|
33
33
|
case RelationKinds.HasMany:
|
|
34
34
|
await this.handleHasManyChange(entry);
|
|
35
35
|
break;
|
|
36
|
+
case RelationKinds.HasOne:
|
|
37
|
+
await this.handleHasOneChange(entry);
|
|
38
|
+
break;
|
|
36
39
|
case RelationKinds.BelongsToMany:
|
|
37
40
|
await this.handleBelongsToManyChange(entry);
|
|
38
41
|
break;
|
|
@@ -66,6 +69,29 @@ export class RelationChangeProcessor {
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
73
|
+
const relation = entry.relation as HasOneRelation;
|
|
74
|
+
const target = entry.change.entity;
|
|
75
|
+
if (!target) return;
|
|
76
|
+
|
|
77
|
+
const tracked = this.unitOfWork.findTracked(target);
|
|
78
|
+
if (!tracked) return;
|
|
79
|
+
|
|
80
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
81
|
+
const rootValue = entry.root[localKey];
|
|
82
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
83
|
+
|
|
84
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
85
|
+
this.assignHasOneForeignKey(tracked.entity, relation, rootValue);
|
|
86
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (entry.change.kind === 'remove') {
|
|
91
|
+
this.detachHasOneChild(tracked.entity, relation);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
70
96
|
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
71
97
|
}
|
|
@@ -108,6 +134,21 @@ export class RelationChangeProcessor {
|
|
|
108
134
|
this.unitOfWork.markDirty(child);
|
|
109
135
|
}
|
|
110
136
|
|
|
137
|
+
private assignHasOneForeignKey(child: any, relation: HasOneRelation, rootValue: unknown): void {
|
|
138
|
+
const current = child[relation.foreignKey];
|
|
139
|
+
if (current === rootValue) return;
|
|
140
|
+
child[relation.foreignKey] = rootValue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private detachHasOneChild(child: any, relation: HasOneRelation): void {
|
|
144
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
145
|
+
this.unitOfWork.markRemoved(child);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
child[relation.foreignKey] = null;
|
|
149
|
+
this.unitOfWork.markDirty(child);
|
|
150
|
+
}
|
|
151
|
+
|
|
111
152
|
private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
112
153
|
const payload = {
|
|
113
154
|
[relation.pivotForeignKeyToRoot]: rootId,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HasOneReference } from '../../schema/types.js';
|
|
2
|
+
import { OrmContext, RelationKey } from '../orm-context.js';
|
|
3
|
+
import { HasOneRelation } from '../../schema/relation.js';
|
|
4
|
+
import { TableDef } from '../../schema/table.js';
|
|
5
|
+
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
|
|
6
|
+
|
|
7
|
+
type Row = 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 DefaultHasOneReference<TChild> implements HasOneReference<TChild> {
|
|
23
|
+
private loaded = false;
|
|
24
|
+
private current: TChild | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly ctx: OrmContext,
|
|
28
|
+
private readonly meta: EntityMeta<any>,
|
|
29
|
+
private readonly root: any,
|
|
30
|
+
private readonly relationName: string,
|
|
31
|
+
private readonly relation: HasOneRelation,
|
|
32
|
+
private readonly rootTable: TableDef,
|
|
33
|
+
private readonly loader: () => Promise<Map<string, Row>>,
|
|
34
|
+
private readonly createEntity: (row: Row) => TChild,
|
|
35
|
+
private readonly localKey: string
|
|
36
|
+
) {
|
|
37
|
+
hideInternal(this, [
|
|
38
|
+
'ctx',
|
|
39
|
+
'meta',
|
|
40
|
+
'root',
|
|
41
|
+
'relationName',
|
|
42
|
+
'relation',
|
|
43
|
+
'rootTable',
|
|
44
|
+
'loader',
|
|
45
|
+
'createEntity',
|
|
46
|
+
'localKey'
|
|
47
|
+
]);
|
|
48
|
+
this.populateFromHydrationCache();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async load(): Promise<TChild | null> {
|
|
52
|
+
if (this.loaded) return this.current;
|
|
53
|
+
const map = await this.loader();
|
|
54
|
+
const keyValue = this.root[this.localKey];
|
|
55
|
+
if (keyValue === undefined || keyValue === null) {
|
|
56
|
+
this.loaded = true;
|
|
57
|
+
return this.current;
|
|
58
|
+
}
|
|
59
|
+
const row = map.get(toKey(keyValue));
|
|
60
|
+
this.current = row ? this.createEntity(row) : null;
|
|
61
|
+
this.loaded = true;
|
|
62
|
+
return this.current;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get(): TChild | null {
|
|
66
|
+
return this.current;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set(data: Partial<TChild> | TChild | null): TChild | null {
|
|
70
|
+
if (data === null) {
|
|
71
|
+
return this.detachCurrent();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entity = hasEntityMeta(data) ? (data as TChild) : this.createEntity(data as Row);
|
|
75
|
+
if (this.current && this.current !== entity) {
|
|
76
|
+
this.ctx.registerRelationChange(
|
|
77
|
+
this.root,
|
|
78
|
+
this.relationKey,
|
|
79
|
+
this.rootTable,
|
|
80
|
+
this.relationName,
|
|
81
|
+
this.relation,
|
|
82
|
+
{ kind: 'remove', entity: this.current }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.assignForeignKey(entity);
|
|
87
|
+
this.current = entity;
|
|
88
|
+
this.loaded = true;
|
|
89
|
+
|
|
90
|
+
this.ctx.registerRelationChange(
|
|
91
|
+
this.root,
|
|
92
|
+
this.relationKey,
|
|
93
|
+
this.rootTable,
|
|
94
|
+
this.relationName,
|
|
95
|
+
this.relation,
|
|
96
|
+
{ kind: 'attach', entity }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return entity;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toJSON(): TChild | null {
|
|
103
|
+
return this.current;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private detachCurrent(): TChild | null {
|
|
107
|
+
const previous = this.current;
|
|
108
|
+
if (!previous) return null;
|
|
109
|
+
this.current = null;
|
|
110
|
+
this.loaded = true;
|
|
111
|
+
this.ctx.registerRelationChange(
|
|
112
|
+
this.root,
|
|
113
|
+
this.relationKey,
|
|
114
|
+
this.rootTable,
|
|
115
|
+
this.relationName,
|
|
116
|
+
this.relation,
|
|
117
|
+
{ kind: 'remove', entity: previous }
|
|
118
|
+
);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private assignForeignKey(entity: TChild): void {
|
|
123
|
+
const keyValue = this.root[this.localKey];
|
|
124
|
+
(entity as Row)[this.relation.foreignKey] = keyValue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get relationKey(): RelationKey {
|
|
128
|
+
return `${this.rootTable.name}.${this.relationName}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private populateFromHydrationCache(): void {
|
|
132
|
+
const keyValue = this.root[this.localKey];
|
|
133
|
+
if (keyValue === undefined || keyValue === null) return;
|
|
134
|
+
const row = getHydrationRecord(this.meta, this.relationName, keyValue);
|
|
135
|
+
if (!row) return;
|
|
136
|
+
this.current = this.createEntity(row);
|
|
137
|
+
this.loaded = true;
|
|
138
|
+
}
|
|
139
|
+
}
|