metal-orm 1.0.72 → 1.0.73
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/dist/index.cjs +140 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +140 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/orm/entity-relations.ts +19 -19
- package/src/orm/execute.ts +23 -19
- package/src/orm/relation-preload.ts +82 -0
- package/src/query-builder/relation-include-tree.ts +98 -0
- package/src/query-builder/select.ts +108 -64
package/package.json
CHANGED
|
@@ -125,16 +125,16 @@ const instantiateWrapper = <TTable extends TableDef>(
|
|
|
125
125
|
createEntity: RelationEntityFactory
|
|
126
126
|
): HasManyCollection<unknown> | HasOneReference<object> | BelongsToReference<object> | ManyToManyCollection<unknown> | undefined => {
|
|
127
127
|
const metaBase = meta as unknown as EntityMeta<TableDef>;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
const loadCached = <T extends Map<string, unknown>>(factory: () => Promise<T>) =>
|
|
129
|
+
relationLoaderCache(metaBase, relationName, factory);
|
|
130
|
+
const resolveOptions = () => meta.lazyRelationOptions.get(relationName);
|
|
131
131
|
switch (relation.type) {
|
|
132
132
|
case RelationKinds.HasOne: {
|
|
133
133
|
const hasOne = relation as HasOneRelation;
|
|
134
|
-
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
135
|
-
const loader = () => loadCached(() =>
|
|
136
|
-
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne,
|
|
137
|
-
);
|
|
134
|
+
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
135
|
+
const loader = () => loadCached(() =>
|
|
136
|
+
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne, resolveOptions())
|
|
137
|
+
);
|
|
138
138
|
return new DefaultHasOneReference(
|
|
139
139
|
meta.ctx,
|
|
140
140
|
metaBase,
|
|
@@ -149,10 +149,10 @@ const instantiateWrapper = <TTable extends TableDef>(
|
|
|
149
149
|
}
|
|
150
150
|
case RelationKinds.HasMany: {
|
|
151
151
|
const hasMany = relation as HasManyRelation;
|
|
152
|
-
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
153
|
-
const loader = () => loadCached(() =>
|
|
154
|
-
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany,
|
|
155
|
-
);
|
|
152
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
153
|
+
const loader = () => loadCached(() =>
|
|
154
|
+
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany, resolveOptions())
|
|
155
|
+
);
|
|
156
156
|
return new DefaultHasManyCollection(
|
|
157
157
|
meta.ctx,
|
|
158
158
|
metaBase,
|
|
@@ -167,10 +167,10 @@ const instantiateWrapper = <TTable extends TableDef>(
|
|
|
167
167
|
}
|
|
168
168
|
case RelationKinds.BelongsTo: {
|
|
169
169
|
const belongsTo = relation as BelongsToRelation;
|
|
170
|
-
const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
|
|
171
|
-
const loader = () => loadCached(() =>
|
|
172
|
-
loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo,
|
|
173
|
-
);
|
|
170
|
+
const targetKey = belongsTo.localKey || findPrimaryKey(belongsTo.target);
|
|
171
|
+
const loader = () => loadCached(() =>
|
|
172
|
+
loadBelongsToRelation(meta.ctx, meta.table, relationName, belongsTo, resolveOptions())
|
|
173
|
+
);
|
|
174
174
|
return new DefaultBelongsToReference(
|
|
175
175
|
meta.ctx,
|
|
176
176
|
metaBase,
|
|
@@ -185,10 +185,10 @@ const instantiateWrapper = <TTable extends TableDef>(
|
|
|
185
185
|
}
|
|
186
186
|
case RelationKinds.BelongsToMany: {
|
|
187
187
|
const many = relation as BelongsToManyRelation;
|
|
188
|
-
const localKey = many.localKey || findPrimaryKey(meta.table);
|
|
189
|
-
const loader = () => loadCached(() =>
|
|
190
|
-
loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many,
|
|
191
|
-
);
|
|
188
|
+
const localKey = many.localKey || findPrimaryKey(meta.table);
|
|
189
|
+
const loader = () => loadCached(() =>
|
|
190
|
+
loadBelongsToManyRelation(meta.ctx, meta.table, relationName, many, resolveOptions())
|
|
191
|
+
);
|
|
192
192
|
return new DefaultManyToManyCollection(
|
|
193
193
|
meta.ctx,
|
|
194
194
|
metaBase,
|
package/src/orm/execute.ts
CHANGED
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
import { EntityContext } from './entity-context.js';
|
|
13
13
|
import { ExecutionContext } from './execution-context.js';
|
|
14
14
|
import { HydrationContext } from './hydration-context.js';
|
|
15
|
-
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
16
|
-
import { getEntityMeta, RelationKey } from './entity-meta.js';
|
|
15
|
+
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
16
|
+
import { getEntityMeta, RelationKey } from './entity-meta.js';
|
|
17
|
+
import { preloadRelationIncludes } from './relation-preload.js';
|
|
17
18
|
import {
|
|
18
19
|
loadHasManyRelation,
|
|
19
20
|
loadHasOneRelation,
|
|
@@ -44,23 +45,26 @@ const executeWithContexts = async <TTable extends TableDef>(
|
|
|
44
45
|
qb: SelectQueryBuilder<unknown, TTable>
|
|
45
46
|
): Promise<EntityInstance<TTable>[]> => {
|
|
46
47
|
const ast = qb.getAST();
|
|
47
|
-
const compiled = execCtx.dialect.compileSelect(ast);
|
|
48
|
-
const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
49
|
-
const rows = flattenResults(executed);
|
|
50
|
-
const lazyRelations = qb.getLazyRelations() as RelationKey<TTable>[];
|
|
51
|
-
const lazyRelationOptions = qb.getLazyRelationOptions();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
const compiled = execCtx.dialect.compileSelect(ast);
|
|
49
|
+
const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
50
|
+
const rows = flattenResults(executed);
|
|
51
|
+
const lazyRelations = qb.getLazyRelations() as RelationKey<TTable>[];
|
|
52
|
+
const lazyRelationOptions = qb.getLazyRelationOptions();
|
|
53
|
+
const includeTree = qb.getIncludeTree();
|
|
54
|
+
|
|
55
|
+
if (ast.setOps && ast.setOps.length > 0) {
|
|
56
|
+
const proxies = rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
|
|
57
|
+
await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
|
|
58
|
+
await preloadRelationIncludes(proxies as Record<string, unknown>[], includeTree);
|
|
59
|
+
return proxies;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hydrated = hydrateRows(rows, qb.getHydrationPlan());
|
|
63
|
+
const entities = hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
|
|
64
|
+
await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
|
|
65
|
+
await preloadRelationIncludes(entities as Record<string, unknown>[], includeTree);
|
|
66
|
+
return entities;
|
|
67
|
+
};
|
|
64
68
|
|
|
65
69
|
const executePlainWithContexts = async <TTable extends TableDef>(
|
|
66
70
|
execCtx: ExecutionContext,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { NormalizedRelationIncludeTree } from '../query-builder/relation-include-tree.js';
|
|
2
|
+
import type { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
3
|
+
import { getEntityMeta } from './entity-meta.js';
|
|
4
|
+
|
|
5
|
+
type LoadableRelation = {
|
|
6
|
+
load?: () => Promise<unknown>;
|
|
7
|
+
getItems?: () => unknown;
|
|
8
|
+
get?: () => unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const collectEntities = (value: unknown): Record<string, unknown>[] => {
|
|
12
|
+
if (!value) return [];
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return value.filter(item => item && typeof item === 'object') as Record<string, unknown>[];
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
return [value as Record<string, unknown>];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const loadRelation = async (
|
|
23
|
+
entity: Record<string, unknown>,
|
|
24
|
+
relationName: string
|
|
25
|
+
): Promise<Record<string, unknown>[]> => {
|
|
26
|
+
const wrapper = entity[relationName] as LoadableRelation | undefined;
|
|
27
|
+
if (!wrapper) return [];
|
|
28
|
+
|
|
29
|
+
if (typeof wrapper.load === 'function') {
|
|
30
|
+
const loaded = await wrapper.load();
|
|
31
|
+
return collectEntities(loaded);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof wrapper.getItems === 'function') {
|
|
35
|
+
return collectEntities(wrapper.getItems());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof wrapper.get === 'function') {
|
|
39
|
+
return collectEntities(wrapper.get());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return collectEntities(wrapper);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const setLazyOptionsIfEmpty = (
|
|
46
|
+
entity: Record<string, unknown>,
|
|
47
|
+
relationName: string,
|
|
48
|
+
options?: RelationIncludeOptions
|
|
49
|
+
): void => {
|
|
50
|
+
if (!options) return;
|
|
51
|
+
const meta = getEntityMeta(entity);
|
|
52
|
+
if (!meta || meta.lazyRelationOptions.has(relationName)) return;
|
|
53
|
+
meta.lazyRelationOptions.set(relationName, options);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const preloadRelationIncludes = async (
|
|
57
|
+
entities: Record<string, unknown>[],
|
|
58
|
+
includeTree: NormalizedRelationIncludeTree,
|
|
59
|
+
depth = 0
|
|
60
|
+
): Promise<void> => {
|
|
61
|
+
if (!entities.length) return;
|
|
62
|
+
const entries = Object.entries(includeTree);
|
|
63
|
+
if (!entries.length) return;
|
|
64
|
+
|
|
65
|
+
for (const [relationName, node] of entries) {
|
|
66
|
+
const shouldLoad = depth > 0 || Boolean(node.include);
|
|
67
|
+
if (!shouldLoad) continue;
|
|
68
|
+
|
|
69
|
+
for (const entity of entities) {
|
|
70
|
+
setLazyOptionsIfEmpty(entity, relationName, node.options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const loaded = await Promise.all(
|
|
74
|
+
entities.map(entity => loadRelation(entity, relationName))
|
|
75
|
+
);
|
|
76
|
+
const relatedEntities = loaded.flat();
|
|
77
|
+
|
|
78
|
+
if (node.include && relatedEntities.length) {
|
|
79
|
+
await preloadRelationIncludes(relatedEntities, node.include, depth + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
import type { RelationDef } from '../schema/relation.js';
|
|
3
|
+
import type { RelationMap, RelationTargetTable } from '../schema/types.js';
|
|
4
|
+
import type { RelationIncludeOptions, TypedRelationIncludeOptions } from './relation-types.js';
|
|
5
|
+
|
|
6
|
+
export type RelationIncludeInput<TTable extends TableDef> = {
|
|
7
|
+
[K in keyof RelationMap<TTable> & string]?: true | RelationIncludeNodeInput<TTable['relations'][K]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RelationIncludeNodeInput<TRel extends RelationDef> =
|
|
11
|
+
TypedRelationIncludeOptions<TRel> & {
|
|
12
|
+
include?: RelationIncludeInput<RelationTargetTable<TRel>>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NormalizedRelationIncludeNode = {
|
|
16
|
+
options?: RelationIncludeOptions;
|
|
17
|
+
include?: NormalizedRelationIncludeTree;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type NormalizedRelationIncludeTree = Record<string, NormalizedRelationIncludeNode>;
|
|
21
|
+
|
|
22
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
23
|
+
Boolean(value && typeof value === 'object');
|
|
24
|
+
|
|
25
|
+
export const normalizeRelationIncludeNode = <TRel extends RelationDef>(
|
|
26
|
+
value?: true | RelationIncludeNodeInput<TRel>
|
|
27
|
+
): NormalizedRelationIncludeNode => {
|
|
28
|
+
if (!value || value === true) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!isObject(value)) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { include, ...rest } = value as Record<string, unknown>;
|
|
37
|
+
const options = Object.keys(rest).length ? (rest as RelationIncludeOptions) : undefined;
|
|
38
|
+
const normalizedInclude = isObject(include)
|
|
39
|
+
? normalizeRelationInclude(include as RelationIncludeInput<TableDef>)
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
if (normalizedInclude && Object.keys(normalizedInclude).length > 0) {
|
|
43
|
+
return { options, include: normalizedInclude };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { options };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const normalizeRelationInclude = (
|
|
50
|
+
input?: RelationIncludeInput<TableDef>
|
|
51
|
+
): NormalizedRelationIncludeTree => {
|
|
52
|
+
if (!input) return {};
|
|
53
|
+
|
|
54
|
+
const tree: NormalizedRelationIncludeTree = {};
|
|
55
|
+
for (const [key, value] of Object.entries(input)) {
|
|
56
|
+
tree[key] = normalizeRelationIncludeNode(value as RelationIncludeNodeInput<RelationDef> | true);
|
|
57
|
+
}
|
|
58
|
+
return tree;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const mergeRelationIncludeTrees = (
|
|
62
|
+
base: NormalizedRelationIncludeTree,
|
|
63
|
+
next: NormalizedRelationIncludeTree
|
|
64
|
+
): NormalizedRelationIncludeTree => {
|
|
65
|
+
const merged: NormalizedRelationIncludeTree = { ...base };
|
|
66
|
+
|
|
67
|
+
for (const [key, node] of Object.entries(next)) {
|
|
68
|
+
const existing = merged[key];
|
|
69
|
+
if (!existing) {
|
|
70
|
+
merged[key] = node;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const include = existing.include && node.include
|
|
75
|
+
? mergeRelationIncludeTrees(existing.include, node.include)
|
|
76
|
+
: (node.include ?? existing.include);
|
|
77
|
+
|
|
78
|
+
merged[key] = {
|
|
79
|
+
options: node.options ?? existing.options,
|
|
80
|
+
...(include ? { include } : {})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return merged;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const cloneRelationIncludeTree = (
|
|
88
|
+
tree: NormalizedRelationIncludeTree
|
|
89
|
+
): NormalizedRelationIncludeTree => {
|
|
90
|
+
const cloned: NormalizedRelationIncludeTree = {};
|
|
91
|
+
for (const [key, node] of Object.entries(tree)) {
|
|
92
|
+
cloned[key] = {
|
|
93
|
+
options: node.options,
|
|
94
|
+
...(node.include ? { include: cloneRelationIncludeTree(node.include) } : {})
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return cloned;
|
|
98
|
+
};
|
|
@@ -29,8 +29,17 @@ import {
|
|
|
29
29
|
SelectQueryBuilderEnvironment
|
|
30
30
|
} from './select-query-builder-deps.js';
|
|
31
31
|
import { ColumnSelector } from './column-selector.js';
|
|
32
|
-
import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
|
|
33
|
-
import { RelationKinds } from '../schema/relation.js';
|
|
32
|
+
import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
|
|
33
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
34
|
+
import {
|
|
35
|
+
RelationIncludeInput,
|
|
36
|
+
RelationIncludeNodeInput,
|
|
37
|
+
NormalizedRelationIncludeTree,
|
|
38
|
+
cloneRelationIncludeTree,
|
|
39
|
+
mergeRelationIncludeTrees,
|
|
40
|
+
normalizeRelationInclude,
|
|
41
|
+
normalizeRelationIncludeNode
|
|
42
|
+
} from './relation-include-tree.js';
|
|
34
43
|
import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
|
|
35
44
|
import { EntityInstance, RelationMap } from '../schema/types.js';
|
|
36
45
|
import type { ColumnToTs, InferRow } from '../schema/types.js';
|
|
@@ -104,10 +113,11 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
104
113
|
private readonly predicateFacet: SelectPredicateFacet;
|
|
105
114
|
private readonly cteFacet: SelectCTEFacet;
|
|
106
115
|
private readonly setOpFacet: SelectSetOpFacet;
|
|
107
|
-
private readonly relationFacet: SelectRelationFacet;
|
|
108
|
-
private readonly lazyRelations: Set<string>;
|
|
109
|
-
private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
|
|
110
|
-
private readonly entityConstructor?: EntityConstructor;
|
|
116
|
+
private readonly relationFacet: SelectRelationFacet;
|
|
117
|
+
private readonly lazyRelations: Set<string>;
|
|
118
|
+
private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
|
|
119
|
+
private readonly entityConstructor?: EntityConstructor;
|
|
120
|
+
private readonly includeTree: NormalizedRelationIncludeTree;
|
|
111
121
|
|
|
112
122
|
/**
|
|
113
123
|
* Creates a new SelectQueryBuilder instance
|
|
@@ -116,15 +126,16 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
116
126
|
* @param hydration - Optional hydration manager
|
|
117
127
|
* @param dependencies - Optional query builder dependencies
|
|
118
128
|
*/
|
|
119
|
-
constructor(
|
|
120
|
-
table: TTable,
|
|
121
|
-
state?: SelectQueryState,
|
|
122
|
-
hydration?: HydrationManager,
|
|
123
|
-
dependencies?: Partial<SelectQueryBuilderDependencies>,
|
|
124
|
-
lazyRelations?: Set<string>,
|
|
125
|
-
lazyRelationOptions?: Map<string, RelationIncludeOptions>,
|
|
126
|
-
entityConstructor?: EntityConstructor
|
|
127
|
-
|
|
129
|
+
constructor(
|
|
130
|
+
table: TTable,
|
|
131
|
+
state?: SelectQueryState,
|
|
132
|
+
hydration?: HydrationManager,
|
|
133
|
+
dependencies?: Partial<SelectQueryBuilderDependencies>,
|
|
134
|
+
lazyRelations?: Set<string>,
|
|
135
|
+
lazyRelationOptions?: Map<string, RelationIncludeOptions>,
|
|
136
|
+
entityConstructor?: EntityConstructor,
|
|
137
|
+
includeTree?: NormalizedRelationIncludeTree
|
|
138
|
+
) {
|
|
128
139
|
const deps = resolveSelectQueryBuilderDependencies(dependencies);
|
|
129
140
|
this.env = { table, deps };
|
|
130
141
|
const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
|
|
@@ -134,10 +145,11 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
134
145
|
state: initialState,
|
|
135
146
|
hydration: initialHydration
|
|
136
147
|
};
|
|
137
|
-
this.lazyRelations = new Set(lazyRelations ?? []);
|
|
138
|
-
this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
|
|
139
|
-
this.entityConstructor = entityConstructor;
|
|
140
|
-
this.
|
|
148
|
+
this.lazyRelations = new Set(lazyRelations ?? []);
|
|
149
|
+
this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
|
|
150
|
+
this.entityConstructor = entityConstructor;
|
|
151
|
+
this.includeTree = includeTree ?? {};
|
|
152
|
+
this.columnSelector = deps.createColumnSelector(this.env);
|
|
141
153
|
const relationManager = deps.createRelationManager(this.env);
|
|
142
154
|
this.fromFacet = new SelectFromFacet(this.env, createAstService);
|
|
143
155
|
this.joinFacet = new SelectJoinFacet(this.env, createAstService);
|
|
@@ -154,21 +166,23 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
154
166
|
* @param lazyRelations - Updated lazy relations set
|
|
155
167
|
* @returns New SelectQueryBuilder instance
|
|
156
168
|
*/
|
|
157
|
-
private clone<TNext = T>(
|
|
158
|
-
context: SelectQueryBuilderContext = this.context,
|
|
159
|
-
lazyRelations = new Set(this.lazyRelations),
|
|
160
|
-
lazyRelationOptions = new Map(this.lazyRelationOptions)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
context.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
private clone<TNext = T>(
|
|
170
|
+
context: SelectQueryBuilderContext = this.context,
|
|
171
|
+
lazyRelations = new Set(this.lazyRelations),
|
|
172
|
+
lazyRelationOptions = new Map(this.lazyRelationOptions),
|
|
173
|
+
includeTree = this.includeTree
|
|
174
|
+
): SelectQueryBuilder<TNext, TTable> {
|
|
175
|
+
return new SelectQueryBuilder(
|
|
176
|
+
this.env.table as TTable,
|
|
177
|
+
context.state,
|
|
178
|
+
context.hydration,
|
|
179
|
+
this.env.deps,
|
|
180
|
+
lazyRelations,
|
|
181
|
+
lazyRelationOptions,
|
|
182
|
+
this.entityConstructor,
|
|
183
|
+
includeTree
|
|
184
|
+
) as SelectQueryBuilder<TNext, TTable>;
|
|
185
|
+
}
|
|
172
186
|
|
|
173
187
|
/**
|
|
174
188
|
* Applies an alias to the root FROM table.
|
|
@@ -548,19 +562,42 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
548
562
|
* qb.include('posts');
|
|
549
563
|
* @example
|
|
550
564
|
* qb.include('posts', { columns: ['id', 'title', 'published'] });
|
|
551
|
-
* @example
|
|
552
|
-
* qb.include('posts', {
|
|
553
|
-
* columns: ['id', 'title'],
|
|
554
|
-
* where: eq(postTable.columns.published, true)
|
|
555
|
-
* });
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
565
|
+
* @example
|
|
566
|
+
* qb.include('posts', {
|
|
567
|
+
* columns: ['id', 'title'],
|
|
568
|
+
* where: eq(postTable.columns.published, true)
|
|
569
|
+
* });
|
|
570
|
+
* @example
|
|
571
|
+
* qb.include({ posts: { include: { author: true } } });
|
|
572
|
+
*/
|
|
573
|
+
include<K extends keyof TTable['relations'] & string>(
|
|
574
|
+
relationName: K,
|
|
575
|
+
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
576
|
+
): SelectQueryBuilder<T, TTable>;
|
|
577
|
+
include(relations: RelationIncludeInput<TTable>): SelectQueryBuilder<T, TTable>;
|
|
578
|
+
include<K extends keyof TTable['relations'] & string>(
|
|
579
|
+
relationNameOrRelations: K | RelationIncludeInput<TTable>,
|
|
580
|
+
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
581
|
+
): SelectQueryBuilder<T, TTable> {
|
|
582
|
+
if (typeof relationNameOrRelations === 'object' && relationNameOrRelations !== null) {
|
|
583
|
+
const normalized = normalizeRelationInclude(relationNameOrRelations as RelationIncludeInput<TableDef>);
|
|
584
|
+
let nextContext = this.context;
|
|
585
|
+
for (const [relationName, node] of Object.entries(normalized)) {
|
|
586
|
+
nextContext = this.relationFacet.include(nextContext, relationName, node.options);
|
|
587
|
+
}
|
|
588
|
+
const nextTree = mergeRelationIncludeTrees(this.includeTree, normalized);
|
|
589
|
+
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const relationName = relationNameOrRelations as string;
|
|
593
|
+
const normalizedNode = normalizeRelationIncludeNode(options);
|
|
594
|
+
const nextContext = this.relationFacet.include(this.context, relationName, normalizedNode.options);
|
|
595
|
+
const shouldStore = Boolean(normalizedNode.include || normalizedNode.options);
|
|
596
|
+
const nextTree = shouldStore
|
|
597
|
+
? mergeRelationIncludeTrees(this.includeTree, { [relationName]: normalizedNode })
|
|
598
|
+
: this.includeTree;
|
|
599
|
+
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
600
|
+
}
|
|
564
601
|
|
|
565
602
|
/**
|
|
566
603
|
* Includes a relation lazily in the query results
|
|
@@ -608,13 +645,13 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
608
645
|
* @example
|
|
609
646
|
* qb.includePick('posts', ['id', 'title', 'createdAt']);
|
|
610
647
|
*/
|
|
611
|
-
includePick<
|
|
612
|
-
K extends keyof TTable['relations'] & string,
|
|
613
|
-
C extends RelationTargetColumns<TTable['relations'][K]>
|
|
614
|
-
>(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
|
|
615
|
-
const options = { columns: cols as readonly C[] } as unknown as
|
|
616
|
-
return this.include(relationName, options);
|
|
617
|
-
}
|
|
648
|
+
includePick<
|
|
649
|
+
K extends keyof TTable['relations'] & string,
|
|
650
|
+
C extends RelationTargetColumns<TTable['relations'][K]>
|
|
651
|
+
>(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
|
|
652
|
+
const options = { columns: cols as readonly C[] } as unknown as RelationIncludeNodeInput<TTable['relations'][K]>;
|
|
653
|
+
return this.include(relationName, options);
|
|
654
|
+
}
|
|
618
655
|
|
|
619
656
|
/**
|
|
620
657
|
* Selects columns for the root table and relations from an array of entries
|
|
@@ -631,13 +668,13 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
631
668
|
let currBuilder: SelectQueryBuilder<T, TTable> = this;
|
|
632
669
|
|
|
633
670
|
for (const entry of config) {
|
|
634
|
-
if (entry.type === 'root') {
|
|
635
|
-
currBuilder = currBuilder.select(...entry.columns);
|
|
636
|
-
} else {
|
|
637
|
-
const options = { columns: entry.columns } as unknown as
|
|
638
|
-
currBuilder = currBuilder.include(entry.relationName, options);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
671
|
+
if (entry.type === 'root') {
|
|
672
|
+
currBuilder = currBuilder.select(...entry.columns);
|
|
673
|
+
} else {
|
|
674
|
+
const options = { columns: entry.columns } as unknown as RelationIncludeNodeInput<TTable['relations'][typeof entry.relationName]>;
|
|
675
|
+
currBuilder = currBuilder.include(entry.relationName, options);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
641
678
|
|
|
642
679
|
return currBuilder;
|
|
643
680
|
}
|
|
@@ -654,9 +691,16 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
654
691
|
* Gets lazy relation include options
|
|
655
692
|
* @returns Map of relation names to include options
|
|
656
693
|
*/
|
|
657
|
-
getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
|
|
658
|
-
return new Map(this.lazyRelationOptions);
|
|
659
|
-
}
|
|
694
|
+
getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
|
|
695
|
+
return new Map(this.lazyRelationOptions);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Gets normalized nested include information for runtime preloading.
|
|
700
|
+
*/
|
|
701
|
+
getIncludeTree(): NormalizedRelationIncludeTree {
|
|
702
|
+
return cloneRelationIncludeTree(this.includeTree);
|
|
703
|
+
}
|
|
660
704
|
|
|
661
705
|
/**
|
|
662
706
|
* Gets the table definition for this query builder
|