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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.72",
3
+ "version": "1.0.73",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -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 lazyOptions = meta.lazyRelationOptions.get(relationName);
129
- const loadCached = <T extends Map<string, unknown>>(factory: () => Promise<T>) =>
130
- relationLoaderCache(metaBase, relationName, factory);
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, lazyOptions)
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, lazyOptions)
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, lazyOptions)
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, lazyOptions)
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,
@@ -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
- if (ast.setOps && ast.setOps.length > 0) {
54
- const proxies = rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
55
- await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
56
- return proxies;
57
- }
58
-
59
- const hydrated = hydrateRows(rows, qb.getHydrationPlan());
60
- const entities = hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, lazyRelations, lazyRelationOptions));
61
- await loadLazyRelationsForTable(entityCtx, qb.getTable(), lazyRelations, lazyRelationOptions);
62
- return entities;
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.columnSelector = deps.createColumnSelector(this.env);
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
- ): SelectQueryBuilder<TNext, TTable> {
162
- return new SelectQueryBuilder(
163
- this.env.table as TTable,
164
- context.state,
165
- context.hydration,
166
- this.env.deps,
167
- lazyRelations,
168
- lazyRelationOptions,
169
- this.entityConstructor
170
- ) as SelectQueryBuilder<TNext, TTable>;
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
- include<K extends keyof TTable['relations'] & string>(
558
- relationName: K,
559
- options?: TypedRelationIncludeOptions<TTable['relations'][K]>
560
- ): SelectQueryBuilder<T, TTable> {
561
- const nextContext = this.relationFacet.include(this.context, relationName, options);
562
- return this.clone(nextContext);
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 TypedRelationIncludeOptions<TTable['relations'][K]>;
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 TypedRelationIncludeOptions<TTable['relations'][typeof entry.relationName]>;
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