metal-orm 1.0.5 → 1.0.7
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 +78 -13
- 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 +38 -18
- package/src/builder/hydration-planner.ts +74 -74
- package/src/builder/select.ts +427 -395
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/index.ts +16 -8
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +10 -10
- 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 +99 -49
- 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 +82 -58
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/orm-runtime.test.ts +254 -0
|
@@ -19,8 +19,11 @@ export interface SqlOperatorConfig {
|
|
|
19
19
|
*/
|
|
20
20
|
export const SQL_OPERATOR_REGISTRY: Record<SqlOperator, SqlOperatorConfig> = {
|
|
21
21
|
[SQL_OPERATORS.EQUALS]: { sql: SQL_OPERATORS.EQUALS, tsName: 'eq' },
|
|
22
|
+
[SQL_OPERATORS.NOT_EQUALS]: { sql: SQL_OPERATORS.NOT_EQUALS, tsName: 'neq' },
|
|
22
23
|
[SQL_OPERATORS.GREATER_THAN]: { sql: SQL_OPERATORS.GREATER_THAN, tsName: 'gt' },
|
|
24
|
+
[SQL_OPERATORS.GREATER_OR_EQUAL]: { sql: SQL_OPERATORS.GREATER_OR_EQUAL, tsName: 'gte' },
|
|
23
25
|
[SQL_OPERATORS.LESS_THAN]: { sql: SQL_OPERATORS.LESS_THAN, tsName: 'lt' },
|
|
26
|
+
[SQL_OPERATORS.LESS_OR_EQUAL]: { sql: SQL_OPERATORS.LESS_OR_EQUAL, tsName: 'lte' },
|
|
24
27
|
[SQL_OPERATORS.LIKE]: { sql: SQL_OPERATORS.LIKE, tsName: 'like' },
|
|
25
28
|
[SQL_OPERATORS.NOT_LIKE]: { sql: SQL_OPERATORS.NOT_LIKE, tsName: 'notLike' },
|
|
26
29
|
[SQL_OPERATORS.IN]: { sql: SQL_OPERATORS.IN, tsName: 'inList' },
|
package/src/constants/sql.ts
CHANGED
|
@@ -33,38 +33,44 @@ export const SQL_KEYWORDS = {
|
|
|
33
33
|
/**
|
|
34
34
|
* SQL operators used in query conditions
|
|
35
35
|
*/
|
|
36
|
-
export const SQL_OPERATORS = {
|
|
37
|
-
/** Equality operator */
|
|
38
|
-
EQUALS: '=',
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
36
|
+
export const SQL_OPERATORS = {
|
|
37
|
+
/** Equality operator */
|
|
38
|
+
EQUALS: '=',
|
|
39
|
+
/** Not equals operator */
|
|
40
|
+
NOT_EQUALS: '!=',
|
|
41
|
+
/** Greater than operator */
|
|
42
|
+
GREATER_THAN: '>',
|
|
43
|
+
/** Greater than or equal operator */
|
|
44
|
+
GREATER_OR_EQUAL: '>=',
|
|
45
|
+
/** Less than operator */
|
|
46
|
+
LESS_THAN: '<',
|
|
47
|
+
/** Less than or equal operator */
|
|
48
|
+
LESS_OR_EQUAL: '<=',
|
|
49
|
+
/** LIKE pattern matching operator */
|
|
50
|
+
LIKE: 'LIKE',
|
|
51
|
+
/** NOT LIKE pattern matching operator */
|
|
52
|
+
NOT_LIKE: 'NOT LIKE',
|
|
53
|
+
/** IN membership operator */
|
|
54
|
+
IN: 'IN',
|
|
55
|
+
/** NOT IN membership operator */
|
|
56
|
+
NOT_IN: 'NOT IN',
|
|
57
|
+
/** BETWEEN range operator */
|
|
58
|
+
BETWEEN: 'BETWEEN',
|
|
59
|
+
/** NOT BETWEEN range operator */
|
|
60
|
+
NOT_BETWEEN: 'NOT BETWEEN',
|
|
61
|
+
/** IS NULL null check operator */
|
|
62
|
+
IS_NULL: 'IS NULL',
|
|
63
|
+
/** IS NOT NULL null check operator */
|
|
64
|
+
IS_NOT_NULL: 'IS NOT NULL',
|
|
65
|
+
/** Logical AND operator */
|
|
66
|
+
AND: 'AND',
|
|
67
|
+
/** Logical OR operator */
|
|
68
|
+
OR: 'OR',
|
|
69
|
+
/** EXISTS operator */
|
|
70
|
+
EXISTS: 'EXISTS',
|
|
71
|
+
/** NOT EXISTS operator */
|
|
72
|
+
NOT_EXISTS: 'NOT EXISTS'
|
|
73
|
+
} as const;
|
|
68
74
|
|
|
69
75
|
/**
|
|
70
76
|
* Type representing any supported SQL operator
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
export * from './schema/table';
|
|
3
|
-
export * from './schema/column';
|
|
2
|
+
export * from './schema/table';
|
|
3
|
+
export * from './schema/column';
|
|
4
4
|
export * from './schema/relation';
|
|
5
|
+
export * from './schema/types';
|
|
5
6
|
export * from './builder/select';
|
|
6
7
|
export * from './builder/insert';
|
|
7
8
|
export * from './builder/update';
|
|
8
9
|
export * from './builder/delete';
|
|
9
10
|
export * from './ast/expression';
|
|
10
|
-
export * from './dialect/mysql';
|
|
11
|
-
export * from './dialect/mssql';
|
|
12
|
-
export * from './dialect/sqlite';
|
|
13
|
-
export * from './runtime/als';
|
|
14
|
-
export * from './runtime/hydration';
|
|
15
|
-
export * from './codegen/typescript';
|
|
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';
|
|
@@ -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, belongsToMany } 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()),
|
|
@@ -52,15 +52,15 @@ export const ProjectAssignments = defineTable('project_assignments', {
|
|
|
52
52
|
assigned_at: col.varchar(50)
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
Users.relations = {
|
|
56
|
-
orders: hasMany(Orders, 'user_id'),
|
|
57
|
-
profiles: hasMany(Profiles, 'user_id'),
|
|
58
|
-
userRoles: hasMany(UserRoles, 'user_id'),
|
|
59
|
-
projects: belongsToMany(Projects, ProjectAssignments, {
|
|
60
|
-
pivotForeignKeyToRoot: 'user_id',
|
|
61
|
-
pivotForeignKeyToTarget: 'project_id'
|
|
62
|
-
})
|
|
63
|
-
};
|
|
55
|
+
Users.relations = {
|
|
56
|
+
orders: hasMany(Orders, 'user_id'),
|
|
57
|
+
profiles: hasMany(Profiles, 'user_id'),
|
|
58
|
+
userRoles: hasMany(UserRoles, 'user_id'),
|
|
59
|
+
projects: belongsToMany(Projects, ProjectAssignments, {
|
|
60
|
+
pivotForeignKeyToRoot: 'user_id',
|
|
61
|
+
pivotForeignKeyToTarget: 'project_id'
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
64
|
|
|
65
65
|
Orders.relations = {
|
|
66
66
|
user: belongsTo(Users, 'user_id')
|
|
@@ -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
|
+
}
|