metal-orm 1.0.4 → 1.0.6
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 +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +77 -3
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +630 -592
- package/src/ast/query.ts +110 -49
- package/src/builder/delete-query-state.ts +42 -0
- package/src/builder/delete.ts +57 -0
- package/src/builder/hydration-manager.ts +3 -2
- package/src/builder/hydration-planner.ts +163 -107
- package/src/builder/insert-query-state.ts +62 -0
- package/src/builder/insert.ts +59 -0
- package/src/builder/operations/relation-manager.ts +1 -23
- package/src/builder/relation-conditions.ts +45 -1
- package/src/builder/relation-service.ts +81 -18
- package/src/builder/relation-types.ts +15 -0
- package/src/builder/relation-utils.ts +12 -0
- package/src/builder/select.ts +427 -394
- package/src/builder/update-query-state.ts +59 -0
- package/src/builder/update.ts +61 -0
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/dialect/abstract.ts +107 -47
- package/src/dialect/mssql/index.ts +31 -6
- package/src/dialect/mysql/index.ts +31 -6
- package/src/dialect/postgres/index.ts +45 -6
- package/src/dialect/sqlite/index.ts +45 -6
- package/src/index.ts +22 -11
- package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +6 -2
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +100 -38
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +105 -40
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/belongs-to-many.test.ts +57 -0
- package/tests/dml.test.ts +206 -0
- package/tests/orm-runtime.test.ts +254 -0
package/src/index.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
export * from './schema/table';
|
|
3
|
-
export * from './schema/column';
|
|
4
|
-
export * from './schema/relation';
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
2
|
+
export * from './schema/table';
|
|
3
|
+
export * from './schema/column';
|
|
4
|
+
export * from './schema/relation';
|
|
5
|
+
export * from './schema/types';
|
|
6
|
+
export * from './builder/select';
|
|
7
|
+
export * from './builder/insert';
|
|
8
|
+
export * from './builder/update';
|
|
9
|
+
export * from './builder/delete';
|
|
10
|
+
export * from './ast/expression';
|
|
11
|
+
export * from './dialect/mysql';
|
|
12
|
+
export * from './dialect/mssql';
|
|
13
|
+
export * from './dialect/sqlite';
|
|
14
|
+
export * from './runtime/als';
|
|
15
|
+
export * from './runtime/hydration';
|
|
16
|
+
export * from './codegen/typescript';
|
|
17
|
+
export * from './runtime/orm-context';
|
|
18
|
+
export * from './runtime/entity';
|
|
19
|
+
export * from './runtime/lazy-batch';
|
|
20
|
+
export * from './runtime/relations/has-many';
|
|
21
|
+
export * from './runtime/relations/belongs-to';
|
|
22
|
+
export * from './runtime/relations/many-to-many';
|
|
23
|
+
export * from './runtime/execute';
|
|
@@ -2,14 +2,26 @@ import { eq } from '../../../../../ast/expression';
|
|
|
2
2
|
import { Users } from '../schema';
|
|
3
3
|
import { createScenario } from './types';
|
|
4
4
|
|
|
5
|
-
export const HYDRATION_SCENARIOS = [
|
|
6
|
-
createScenario({
|
|
7
|
-
id: 'user_with_orders',
|
|
8
|
-
category: 'Hydration',
|
|
9
|
-
title: 'Nested User Graph',
|
|
10
|
-
description: 'Eager-load orders for a single user and hydrate a JSON graph from flat SQL rows.',
|
|
11
|
-
build: (qb) => qb
|
|
12
|
-
.include('orders', { columns: ['id', 'total', 'status', 'user_id'] })
|
|
13
|
-
.where(eq(Users.columns.id, 1))
|
|
14
|
-
})
|
|
15
|
-
|
|
5
|
+
export const HYDRATION_SCENARIOS = [
|
|
6
|
+
createScenario({
|
|
7
|
+
id: 'user_with_orders',
|
|
8
|
+
category: 'Hydration',
|
|
9
|
+
title: 'Nested User Graph',
|
|
10
|
+
description: 'Eager-load orders for a single user and hydrate a JSON graph from flat SQL rows.',
|
|
11
|
+
build: (qb) => qb
|
|
12
|
+
.include('orders', { columns: ['id', 'total', 'status', 'user_id'] })
|
|
13
|
+
.where(eq(Users.columns.id, 1))
|
|
14
|
+
}),
|
|
15
|
+
createScenario({
|
|
16
|
+
id: 'user_projects_with_pivot',
|
|
17
|
+
category: 'Hydration',
|
|
18
|
+
title: 'Projects with Pivot Data',
|
|
19
|
+
description: 'Include projects for a user along with pivot metadata stored in `_pivot`.',
|
|
20
|
+
build: (qb) => qb
|
|
21
|
+
.include('projects', {
|
|
22
|
+
columns: ['id', 'name', 'client'],
|
|
23
|
+
pivot: { columns: ['assigned_at', 'role_id'] }
|
|
24
|
+
})
|
|
25
|
+
.where(eq(Users.columns.id, 1))
|
|
26
|
+
})
|
|
27
|
+
];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { SelectQueryBuilder } from '../../../../../builder/select';
|
|
1
|
+
import { SelectQueryBuilder } from '../../../../../builder/select';
|
|
2
|
+
import { TableDef } from '../../../../../schema/table';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Extracts the TypeScript code from a build function
|
|
5
6
|
*/
|
|
6
|
-
function extractTypeScriptCode
|
|
7
|
+
function extractTypeScriptCode<TTable extends TableDef>(
|
|
8
|
+
buildFn: (builder: SelectQueryBuilder<any, TTable>) => SelectQueryBuilder<any, TTable>
|
|
9
|
+
): string {
|
|
7
10
|
const fnString = buildFn.toString();
|
|
8
11
|
|
|
9
12
|
// Remove the function wrapper and return statement
|
|
@@ -34,12 +37,12 @@ function extractTypeScriptCode(buildFn: (builder: SelectQueryBuilder<any>) => Se
|
|
|
34
37
|
return fnString;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export interface Scenario {
|
|
38
|
-
id: string;
|
|
39
|
-
title: string;
|
|
40
|
-
description: string;
|
|
41
|
-
category: string;
|
|
42
|
-
build: (builder: SelectQueryBuilder<any>) => SelectQueryBuilder<any>;
|
|
40
|
+
export interface Scenario {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
description: string;
|
|
44
|
+
category: string;
|
|
45
|
+
build: <TTable extends TableDef>(builder: SelectQueryBuilder<any, TTable>) => SelectQueryBuilder<any, TTable>;
|
|
43
46
|
|
|
44
47
|
code?: string;
|
|
45
48
|
typescriptCode?: string;
|
|
@@ -48,13 +51,13 @@ export interface Scenario {
|
|
|
48
51
|
/**
|
|
49
52
|
* Creates a scenario with auto-extracted TypeScript code
|
|
50
53
|
*/
|
|
51
|
-
export function createScenario(config: {
|
|
52
|
-
id: string;
|
|
53
|
-
title: string;
|
|
54
|
-
description: string;
|
|
55
|
-
category: string;
|
|
56
|
-
build: (builder: SelectQueryBuilder<any>) => SelectQueryBuilder<any>;
|
|
57
|
-
}): Scenario {
|
|
54
|
+
export function createScenario(config: {
|
|
55
|
+
id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
description: string;
|
|
58
|
+
category: string;
|
|
59
|
+
build: <TTable extends TableDef>(builder: SelectQueryBuilder<any, TTable>) => SelectQueryBuilder<any, TTable>;
|
|
60
|
+
}): Scenario {
|
|
58
61
|
return {
|
|
59
62
|
...config,
|
|
60
63
|
get code() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTable } from '../../../../schema/table';
|
|
2
2
|
import { col } from '../../../../schema/column';
|
|
3
|
-
import { hasMany, belongsTo } from '../../../../schema/relation';
|
|
3
|
+
import { hasMany, belongsTo, belongsToMany } from '../../../../schema/relation';
|
|
4
4
|
|
|
5
5
|
export const Users = defineTable('users', {
|
|
6
6
|
id: col.primaryKey(col.int()),
|
|
@@ -55,7 +55,11 @@ export const ProjectAssignments = defineTable('project_assignments', {
|
|
|
55
55
|
Users.relations = {
|
|
56
56
|
orders: hasMany(Orders, 'user_id'),
|
|
57
57
|
profiles: hasMany(Profiles, 'user_id'),
|
|
58
|
-
userRoles: hasMany(UserRoles, 'user_id')
|
|
58
|
+
userRoles: hasMany(UserRoles, 'user_id'),
|
|
59
|
+
projects: belongsToMany(Projects, ProjectAssignments, {
|
|
60
|
+
pivotForeignKeyToRoot: 'user_id',
|
|
61
|
+
pivotForeignKeyToTarget: 'project_id'
|
|
62
|
+
})
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
Orders.relations = {
|
|
@@ -6,11 +6,12 @@ import { hydrateRows } from '../../../../runtime/hydration';
|
|
|
6
6
|
import type { IDatabaseClient } from '../common/IDatabaseClient';
|
|
7
7
|
import type { QueryExecutionResult } from '../api/types';
|
|
8
8
|
import type { Scenario } from '../data/scenarios';
|
|
9
|
+
import type { TableDef } from '../../../../schema/table';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Extracts the TypeScript code from a build function
|
|
12
13
|
*/
|
|
13
|
-
function extractTypeScriptCode(buildFn: (builder: SelectQueryBuilder<any>) => SelectQueryBuilder<any>): string {
|
|
14
|
+
function extractTypeScriptCode<TTable extends TableDef>(buildFn: (builder: SelectQueryBuilder<any, TTable>) => SelectQueryBuilder<any, TTable>): string {
|
|
14
15
|
const fnString = buildFn.toString();
|
|
15
16
|
|
|
16
17
|
// Remove the function wrapper and return statement
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { OrmContext } from './orm-context';
|
|
3
|
+
import { RelationMap } from '../schema/types';
|
|
4
|
+
|
|
5
|
+
export const ENTITY_META = Symbol('EntityMeta');
|
|
6
|
+
|
|
7
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
8
|
+
|
|
9
|
+
export interface EntityMeta<TTable extends TableDef> {
|
|
10
|
+
ctx: OrmContext;
|
|
11
|
+
table: TTable;
|
|
12
|
+
lazyRelations: (keyof RelationMap<TTable>)[];
|
|
13
|
+
relationCache: Map<string, Promise<any>>;
|
|
14
|
+
relationHydration: Map<string, Map<string, any>>;
|
|
15
|
+
relationWrappers: Map<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getHydrationRows = <TTable extends TableDef>(
|
|
19
|
+
meta: EntityMeta<TTable>,
|
|
20
|
+
relationName: string,
|
|
21
|
+
key: unknown
|
|
22
|
+
): Record<string, any>[] | undefined => {
|
|
23
|
+
const map = meta.relationHydration.get(relationName);
|
|
24
|
+
if (!map) return undefined;
|
|
25
|
+
const rows = map.get(toKey(key));
|
|
26
|
+
if (!rows) return undefined;
|
|
27
|
+
return Array.isArray(rows) ? rows : undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getHydrationRecord = <TTable extends TableDef>(
|
|
31
|
+
meta: EntityMeta<TTable>,
|
|
32
|
+
relationName: string,
|
|
33
|
+
key: unknown
|
|
34
|
+
): Record<string, any> | undefined => {
|
|
35
|
+
const map = meta.relationHydration.get(relationName);
|
|
36
|
+
if (!map) return undefined;
|
|
37
|
+
const value = map.get(toKey(key));
|
|
38
|
+
if (!value) return undefined;
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value[0];
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getEntityMeta = <TTable extends TableDef>(entity: any): EntityMeta<TTable> | undefined => {
|
|
46
|
+
if (!entity || typeof entity !== 'object') return undefined;
|
|
47
|
+
return (entity as any)[ENTITY_META];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const hasEntityMeta = (entity: any): entity is { [ENTITY_META]: EntityMeta<TableDef> } => {
|
|
51
|
+
return Boolean(getEntityMeta(entity));
|
|
52
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { Entity, RelationMap, HasManyCollection, BelongsToReference, ManyToManyCollection } from '../schema/types';
|
|
3
|
+
import { OrmContext } from './orm-context';
|
|
4
|
+
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta';
|
|
5
|
+
import { DefaultHasManyCollection } from './relations/has-many';
|
|
6
|
+
import { DefaultBelongsToReference } from './relations/belongs-to';
|
|
7
|
+
import { DefaultManyToManyCollection } from './relations/many-to-many';
|
|
8
|
+
import { HasManyRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation';
|
|
9
|
+
import { loadHasManyRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch';
|
|
10
|
+
import { findPrimaryKey } from '../builder/hydration-planner';
|
|
11
|
+
|
|
12
|
+
type Rows = Record<string, any>[];
|
|
13
|
+
|
|
14
|
+
const relationLoaderCache = <T extends Map<string, any>>(
|
|
15
|
+
meta: EntityMeta<any>,
|
|
16
|
+
relationName: string,
|
|
17
|
+
factory: () => Promise<T>
|
|
18
|
+
): Promise<T> => {
|
|
19
|
+
if (meta.relationCache.has(relationName)) {
|
|
20
|
+
return meta.relationCache.get(relationName)! as Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const promise = factory().then(value => {
|
|
24
|
+
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
25
|
+
const otherMeta = getEntityMeta(tracked.entity);
|
|
26
|
+
if (!otherMeta) continue;
|
|
27
|
+
otherMeta.relationHydration.set(relationName, value);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
meta.relationCache.set(relationName, promise);
|
|
33
|
+
|
|
34
|
+
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
35
|
+
const otherMeta = getEntityMeta(tracked.entity);
|
|
36
|
+
if (!otherMeta) continue;
|
|
37
|
+
otherMeta.relationCache.set(relationName, promise);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return promise;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const createEntityProxy = <
|
|
44
|
+
TTable extends TableDef,
|
|
45
|
+
TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
|
|
46
|
+
>(
|
|
47
|
+
ctx: OrmContext,
|
|
48
|
+
table: TTable,
|
|
49
|
+
row: Record<string, any>,
|
|
50
|
+
lazyRelations: TLazy[] = [] as TLazy[]
|
|
51
|
+
): Entity<TTable> => {
|
|
52
|
+
const target: Record<string, any> = { ...row };
|
|
53
|
+
const meta: EntityMeta<TTable> = {
|
|
54
|
+
ctx,
|
|
55
|
+
table,
|
|
56
|
+
lazyRelations: [...lazyRelations],
|
|
57
|
+
relationCache: new Map(),
|
|
58
|
+
relationHydration: new Map(),
|
|
59
|
+
relationWrappers: new Map()
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
Object.defineProperty(target, ENTITY_META, {
|
|
63
|
+
value: meta,
|
|
64
|
+
enumerable: false,
|
|
65
|
+
writable: false
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let proxy: Entity<TTable>;
|
|
69
|
+
const handler: ProxyHandler<any> = {
|
|
70
|
+
get(targetObj, prop, receiver) {
|
|
71
|
+
if (prop === ENTITY_META) {
|
|
72
|
+
return meta;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (prop === '$load') {
|
|
76
|
+
return async (relationName: keyof RelationMap<TTable>) => {
|
|
77
|
+
const wrapper = getRelationWrapper(meta, relationName as string, proxy);
|
|
78
|
+
if (wrapper && typeof wrapper.load === 'function') {
|
|
79
|
+
return wrapper.load();
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof prop === 'string' && table.relations[prop]) {
|
|
86
|
+
return getRelationWrapper(meta, prop, proxy);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Reflect.get(targetObj, prop, receiver);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
set(targetObj, prop, value, receiver) {
|
|
93
|
+
const result = Reflect.set(targetObj, prop, value, receiver);
|
|
94
|
+
if (typeof prop === 'string' && table.columns[prop]) {
|
|
95
|
+
ctx.markDirty(proxy);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
proxy = new Proxy(target, handler) as Entity<TTable>;
|
|
102
|
+
populateHydrationCache(proxy, row, meta);
|
|
103
|
+
return proxy;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const createEntityFromRow = <TTable extends TableDef>(
|
|
107
|
+
ctx: OrmContext,
|
|
108
|
+
table: TTable,
|
|
109
|
+
row: Record<string, any>,
|
|
110
|
+
lazyRelations: (keyof RelationMap<TTable>)[] = []
|
|
111
|
+
): Entity<TTable> => {
|
|
112
|
+
const pkName = findPrimaryKey(table);
|
|
113
|
+
const pkValue = row[pkName];
|
|
114
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
115
|
+
const tracked = ctx.getEntity(table, pkValue);
|
|
116
|
+
if (tracked) return tracked;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const entity = createEntityProxy(ctx, table, row, lazyRelations);
|
|
120
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
121
|
+
ctx.trackManaged(table, pkValue, entity);
|
|
122
|
+
} else {
|
|
123
|
+
ctx.trackNew(table, entity);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return entity;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
130
|
+
|
|
131
|
+
const populateHydrationCache = <TTable extends TableDef>(
|
|
132
|
+
entity: any,
|
|
133
|
+
row: Record<string, any>,
|
|
134
|
+
meta: EntityMeta<TTable>
|
|
135
|
+
): void => {
|
|
136
|
+
for (const relationName of Object.keys(meta.table.relations)) {
|
|
137
|
+
const relation = meta.table.relations[relationName];
|
|
138
|
+
const data = row[relationName];
|
|
139
|
+
if (!Array.isArray(data)) continue;
|
|
140
|
+
|
|
141
|
+
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
142
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
143
|
+
const rootValue = entity[localKey];
|
|
144
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
145
|
+
const cache = new Map<string, Rows>();
|
|
146
|
+
cache.set(toKey(rootValue), data as Rows);
|
|
147
|
+
meta.relationHydration.set(relationName, cache);
|
|
148
|
+
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
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getRelationWrapper = (
|
|
169
|
+
meta: EntityMeta<any>,
|
|
170
|
+
relationName: string,
|
|
171
|
+
owner: any
|
|
172
|
+
): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
173
|
+
if (meta.relationWrappers.has(relationName)) {
|
|
174
|
+
return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const relation = meta.table.relations[relationName];
|
|
178
|
+
if (!relation) return undefined;
|
|
179
|
+
|
|
180
|
+
const wrapper = instantiateWrapper(meta, relationName, relation as any, owner);
|
|
181
|
+
if (wrapper) {
|
|
182
|
+
meta.relationWrappers.set(relationName, wrapper);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return wrapper;
|
|
186
|
+
};
|
|
187
|
+
|
|
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.HasMany: {
|
|
196
|
+
const hasMany = relation as HasManyRelation;
|
|
197
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
198
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
199
|
+
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
|
|
200
|
+
);
|
|
201
|
+
return new DefaultHasManyCollection(
|
|
202
|
+
meta.ctx,
|
|
203
|
+
meta,
|
|
204
|
+
owner,
|
|
205
|
+
relationName,
|
|
206
|
+
hasMany,
|
|
207
|
+
meta.table,
|
|
208
|
+
loader,
|
|
209
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
210
|
+
localKey
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
case RelationKinds.BelongsTo: {
|
|
214
|
+
const belongsTo = relation as BelongsToRelation;
|
|
215
|
+
const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
|
|
216
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
217
|
+
loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo)
|
|
218
|
+
);
|
|
219
|
+
return new DefaultBelongsToReference(
|
|
220
|
+
meta.ctx,
|
|
221
|
+
meta,
|
|
222
|
+
owner,
|
|
223
|
+
relationName,
|
|
224
|
+
belongsTo,
|
|
225
|
+
meta.table,
|
|
226
|
+
loader,
|
|
227
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
228
|
+
targetKey
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
case RelationKinds.BelongsToMany: {
|
|
232
|
+
const many = relation as BelongsToManyRelation;
|
|
233
|
+
const localKey = many.localKey || findPrimaryKey(meta.table);
|
|
234
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
235
|
+
loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many)
|
|
236
|
+
);
|
|
237
|
+
return new DefaultManyToManyCollection(
|
|
238
|
+
meta.ctx,
|
|
239
|
+
meta,
|
|
240
|
+
owner,
|
|
241
|
+
relationName,
|
|
242
|
+
many,
|
|
243
|
+
meta.table,
|
|
244
|
+
loader,
|
|
245
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
246
|
+
localKey
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
default:
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { Entity } from '../schema/types';
|
|
3
|
+
import { hydrateRows } from './hydration';
|
|
4
|
+
import { OrmContext } from './orm-context';
|
|
5
|
+
import { SelectQueryBuilder } from '../builder/select';
|
|
6
|
+
import { createEntityFromRow } from './entity';
|
|
7
|
+
|
|
8
|
+
type Row = Record<string, any>;
|
|
9
|
+
|
|
10
|
+
const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
|
|
11
|
+
const rows: Row[] = [];
|
|
12
|
+
for (const result of results) {
|
|
13
|
+
const { columns, values } = result;
|
|
14
|
+
for (const valueRow of values) {
|
|
15
|
+
const row: Row = {};
|
|
16
|
+
columns.forEach((column, idx) => {
|
|
17
|
+
row[column] = valueRow[idx];
|
|
18
|
+
});
|
|
19
|
+
rows.push(row);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return rows;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export async function executeHydrated<TTable extends TableDef>(
|
|
26
|
+
ctx: OrmContext,
|
|
27
|
+
qb: SelectQueryBuilder<any, TTable>
|
|
28
|
+
): Promise<Entity<TTable>[]> {
|
|
29
|
+
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
30
|
+
const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
31
|
+
const rows = flattenResults(executed);
|
|
32
|
+
const hydrated = hydrateRows(rows, qb.getHydrationPlan());
|
|
33
|
+
return hydrated.map(row =>
|
|
34
|
+
createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/runtime/hydration.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HydrationPlan } from '../ast/query';
|
|
1
|
+
import { HydrationPlan, HydrationRelationPlan } from '../ast/query';
|
|
2
2
|
import { isRelationAlias, makeRelationAlias } from '../utils/relation-alias';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,42 +7,104 @@ import { isRelationAlias, makeRelationAlias } from '../utils/relation-alias';
|
|
|
7
7
|
* @param plan - Hydration plan
|
|
8
8
|
* @returns Hydrated result objects with nested relations
|
|
9
9
|
*/
|
|
10
|
-
export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
|
|
11
|
-
if (!plan || !rows.length) return rows;
|
|
12
|
-
|
|
13
|
-
const rootMap = new Map<any, any
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
|
|
11
|
+
if (!plan || !rows.length) return rows;
|
|
12
|
+
|
|
13
|
+
const rootMap = new Map<any, Record<string, any>>();
|
|
14
|
+
const relationIndex = new Map<any, Record<string, Set<any>>>();
|
|
15
|
+
|
|
16
|
+
const getOrCreateParent = (row: Record<string, any>) => {
|
|
17
|
+
const rootId = row[plan.rootPrimaryKey];
|
|
18
|
+
if (rootId === undefined) return undefined;
|
|
19
|
+
|
|
20
|
+
if (!rootMap.has(rootId)) {
|
|
21
|
+
rootMap.set(rootId, createBaseRow(row, plan));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return rootMap.get(rootId);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getRelationSeenSet = (rootId: any, relationName: string): Set<any> => {
|
|
28
|
+
let byRelation = relationIndex.get(rootId);
|
|
29
|
+
if (!byRelation) {
|
|
30
|
+
byRelation = {};
|
|
31
|
+
relationIndex.set(rootId, byRelation);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let seen = byRelation[relationName];
|
|
35
|
+
if (!seen) {
|
|
36
|
+
seen = new Set<any>();
|
|
37
|
+
byRelation[relationName] = seen;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return seen;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
const rootId = row[plan.rootPrimaryKey];
|
|
45
|
+
if (rootId === undefined) continue;
|
|
46
|
+
|
|
47
|
+
const parent = getOrCreateParent(row);
|
|
48
|
+
if (!parent) continue;
|
|
49
|
+
|
|
50
|
+
for (const rel of plan.relations) {
|
|
30
51
|
const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
|
|
31
52
|
const childPk = row[childPkKey];
|
|
32
|
-
if (childPk === null || childPk === undefined)
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rel.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
const bucket = parent[rel.name] as any[];
|
|
60
|
+
bucket.push(buildChild(row, rel));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Array.from(rootMap.values());
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<string, any> => {
|
|
68
|
+
const base: Record<string, any> = {};
|
|
69
|
+
const baseKeys = plan.rootColumns.length
|
|
70
|
+
? plan.rootColumns
|
|
71
|
+
: Object.keys(row).filter(k => !isRelationAlias(k));
|
|
72
|
+
|
|
73
|
+
for (const key of baseKeys) {
|
|
74
|
+
base[key] = row[key];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const rel of plan.relations) {
|
|
78
|
+
base[rel.name] = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return base;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const buildChild = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> => {
|
|
85
|
+
const child: Record<string, any> = {};
|
|
86
|
+
for (const col of rel.columns) {
|
|
87
|
+
const key = makeRelationAlias(rel.aliasPrefix, col);
|
|
88
|
+
child[col] = row[key];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pivot = buildPivot(row, rel);
|
|
92
|
+
if (pivot) {
|
|
93
|
+
(child as any)._pivot = pivot;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return child;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const buildPivot = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> | undefined => {
|
|
100
|
+
if (!rel.pivot) return undefined;
|
|
101
|
+
|
|
102
|
+
const pivot: Record<string, any> = {};
|
|
103
|
+
for (const col of rel.pivot.columns) {
|
|
104
|
+
const key = makeRelationAlias(rel.pivot.aliasPrefix, col);
|
|
105
|
+
pivot[col] = row[key];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasValue = Object.values(pivot).some(v => v !== null && v !== undefined);
|
|
109
|
+
return hasValue ? pivot : undefined;
|
|
110
|
+
};
|