metal-orm 1.0.14 → 1.0.16
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 +69 -67
- package/dist/decorators/index.cjs +1983 -224
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +6 -6
- package/dist/decorators/index.d.ts +6 -6
- package/dist/decorators/index.js +1982 -224
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +5284 -3751
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -169
- package/dist/index.d.ts +524 -169
- package/dist/index.js +5197 -3736
- package/dist/index.js.map +1 -1
- package/dist/{select-CCp1oz9p.d.cts → select-BKZrMRCQ.d.cts} +555 -94
- package/dist/{select-CCp1oz9p.d.ts → select-BKZrMRCQ.d.ts} +555 -94
- package/package.json +1 -1
- package/src/codegen/naming-strategy.ts +64 -0
- package/src/codegen/typescript.ts +19 -21
- 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 +17 -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 +172 -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/column.ts +13 -4
- package/src/decorators/relations.ts +15 -0
- package/src/index.ts +37 -19
- package/src/orm/entity-context.ts +30 -0
- package/src/orm/entity-meta.ts +2 -2
- package/src/orm/entity-metadata.ts +8 -6
- package/src/orm/entity.ts +72 -41
- package/src/orm/execute.ts +42 -25
- package/src/orm/execution-context.ts +12 -0
- package/src/orm/hydration-context.ts +14 -0
- package/src/orm/hydration.ts +25 -17
- package/src/orm/identity-map.ts +4 -0
- package/src/orm/interceptor-pipeline.ts +29 -0
- package/src/orm/lazy-batch.ts +50 -6
- package/src/orm/orm-session.ts +234 -0
- package/src/orm/orm.ts +58 -0
- package/src/orm/query-logger.ts +1 -1
- package/src/orm/relation-change-processor.ts +48 -3
- 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 -0
- package/src/orm/relations/many-to-many.ts +46 -45
- package/src/orm/transaction-runner.ts +1 -1
- package/src/orm/unit-of-work.ts +66 -61
- 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 +575 -64
- 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/orm-context.ts +0 -159
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 {
|
|
2
|
+
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
+
import { EntityContext } from './entity-context.js';
|
|
4
4
|
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
5
|
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
+
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
6
7
|
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
7
8
|
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
8
|
-
import { HasManyRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
9
|
-
import { loadHasManyRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.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>[];
|
|
@@ -44,7 +45,7 @@ export const createEntityProxy = <
|
|
|
44
45
|
TTable extends TableDef,
|
|
45
46
|
TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
|
|
46
47
|
>(
|
|
47
|
-
ctx:
|
|
48
|
+
ctx: EntityContext,
|
|
48
49
|
table: TTable,
|
|
49
50
|
row: Record<string, any>,
|
|
50
51
|
lazyRelations: TLazy[] = [] as TLazy[]
|
|
@@ -104,7 +105,7 @@ export const createEntityProxy = <
|
|
|
104
105
|
};
|
|
105
106
|
|
|
106
107
|
export const createEntityFromRow = <TTable extends TableDef>(
|
|
107
|
-
ctx:
|
|
108
|
+
ctx: EntityContext,
|
|
108
109
|
table: TTable,
|
|
109
110
|
row: Record<string, any>,
|
|
110
111
|
lazyRelations: (keyof RelationMap<TTable>)[] = []
|
|
@@ -128,48 +129,60 @@ export const createEntityFromRow = <TTable extends TableDef>(
|
|
|
128
129
|
|
|
129
130
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
const populateHydrationCache = <TTable extends TableDef>(
|
|
133
|
+
entity: any,
|
|
134
|
+
row: Record<string, any>,
|
|
135
|
+
meta: EntityMeta<TTable>
|
|
136
|
+
): void => {
|
|
137
|
+
for (const relationName of Object.keys(meta.table.relations)) {
|
|
138
|
+
const relation = meta.table.relations[relationName];
|
|
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) {
|
|
155
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
156
|
+
const rootValue = entity[localKey];
|
|
157
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
158
|
+
const cache = new Map<string, Rows>();
|
|
159
|
+
cache.set(toKey(rootValue), data as Rows);
|
|
160
|
+
meta.relationHydration.set(relationName, cache);
|
|
161
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (relation.type === RelationKinds.BelongsTo) {
|
|
166
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
167
|
+
const cache = new Map<string, Record<string, any>>();
|
|
168
|
+
for (const item of data) {
|
|
169
|
+
const pkValue = item[targetKey];
|
|
170
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
171
|
+
cache.set(toKey(pkValue), item);
|
|
172
|
+
}
|
|
173
|
+
if (cache.size) {
|
|
147
174
|
meta.relationHydration.set(relationName, cache);
|
|
148
175
|
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (relation.type === RelationKinds.BelongsTo) {
|
|
153
|
-
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
154
|
-
const cache = new Map<string, Record<string, any>>();
|
|
155
|
-
for (const item of data) {
|
|
156
|
-
const pkValue = item[targetKey];
|
|
157
|
-
if (pkValue === undefined || pkValue === null) continue;
|
|
158
|
-
cache.set(toKey(pkValue), item);
|
|
159
|
-
}
|
|
160
|
-
if (cache.size) {
|
|
161
|
-
meta.relationHydration.set(relationName, cache);
|
|
162
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
163
|
-
}
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
|
-
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
167
180
|
|
|
168
181
|
const getRelationWrapper = (
|
|
169
182
|
meta: EntityMeta<any>,
|
|
170
183
|
relationName: string,
|
|
171
184
|
owner: any
|
|
172
|
-
): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
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
|
}
|
|
@@ -188,10 +201,28 @@ const getRelationWrapper = (
|
|
|
188
201
|
const instantiateWrapper = (
|
|
189
202
|
meta: EntityMeta<any>,
|
|
190
203
|
relationName: string,
|
|
191
|
-
relation: HasManyRelation | BelongsToRelation | BelongsToManyRelation,
|
|
204
|
+
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
192
205
|
owner: any
|
|
193
|
-
): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
206
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
194
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
|
+
}
|
|
195
226
|
case RelationKinds.HasMany: {
|
|
196
227
|
const hasMany = relation as HasManyRelation;
|
|
197
228
|
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
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,12 @@
|
|
|
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
|
+
export interface ExecutionContext {
|
|
6
|
+
dialect: Dialect;
|
|
7
|
+
executor: DbExecutor;
|
|
8
|
+
interceptors: InterceptorPipeline;
|
|
9
|
+
// plus anything *purely about executing SQL*:
|
|
10
|
+
// - logging
|
|
11
|
+
// - query timeout config
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IdentityMap } from './identity-map.js';
|
|
2
|
+
import { UnitOfWork } from './unit-of-work.js';
|
|
3
|
+
import { DomainEventBus } from './domain-event-bus.js';
|
|
4
|
+
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
5
|
+
import { EntityContext } from './entity-context.js';
|
|
6
|
+
|
|
7
|
+
export interface HydrationContext {
|
|
8
|
+
identityMap: IdentityMap;
|
|
9
|
+
unitOfWork: UnitOfWork;
|
|
10
|
+
domainEvents: DomainEventBus<any>;
|
|
11
|
+
relationChanges: RelationChangeProcessor;
|
|
12
|
+
entityContext: EntityContext;
|
|
13
|
+
// maybe mapping registry, converters, etc.
|
|
14
|
+
}
|
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/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
|
@@ -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 {
|
|
5
|
+
import { EntityContext } from './entity-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
|
|
|
@@ -29,7 +30,7 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
|
|
|
29
30
|
return rows;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
const executeQuery = async (ctx:
|
|
33
|
+
const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
|
|
33
34
|
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
34
35
|
const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
35
36
|
return rowsFromResults(results);
|
|
@@ -38,7 +39,7 @@ const executeQuery = async (ctx: OrmContext, qb: SelectQueryBuilder<any, TableDe
|
|
|
38
39
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
39
40
|
|
|
40
41
|
export const loadHasManyRelation = async (
|
|
41
|
-
ctx:
|
|
42
|
+
ctx: EntityContext,
|
|
42
43
|
rootTable: TableDef,
|
|
43
44
|
_relationName: string,
|
|
44
45
|
relation: HasManyRelation
|
|
@@ -80,8 +81,51 @@ export const loadHasManyRelation = async (
|
|
|
80
81
|
return grouped;
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
export const loadHasOneRelation = async (
|
|
85
|
+
ctx: EntityContext,
|
|
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
|
-
ctx:
|
|
128
|
+
ctx: EntityContext,
|
|
85
129
|
rootTable: TableDef,
|
|
86
130
|
_relationName: string,
|
|
87
131
|
relation: BelongsToRelation
|
|
@@ -120,7 +164,7 @@ export const loadBelongsToRelation = async (
|
|
|
120
164
|
};
|
|
121
165
|
|
|
122
166
|
export const loadBelongsToManyRelation = async (
|
|
123
|
-
ctx:
|
|
167
|
+
ctx: EntityContext,
|
|
124
168
|
rootTable: TableDef,
|
|
125
169
|
_relationName: string,
|
|
126
170
|
relation: BelongsToManyRelation
|