metal-orm 1.0.60 → 1.0.63

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.
@@ -31,22 +31,20 @@ export class HydrationPlanner {
31
31
  * @param columns - Columns to capture
32
32
  * @returns Updated HydrationPlanner with captured columns
33
33
  */
34
- captureRootColumns(columns: ProjectionNode[]): HydrationPlanner {
35
- const currentPlan = this.getPlanOrDefault();
36
- const rootCols = new Set(currentPlan.rootColumns);
37
- let changed = false;
38
-
39
- columns.forEach(node => {
40
- if (node.type !== 'Column') return;
41
- if (node.table !== this.table.name) return;
42
-
43
- const alias = node.alias || node.name;
44
- if (isRelationAlias(alias)) return;
45
- if (!rootCols.has(alias)) {
46
- rootCols.add(alias);
47
- changed = true;
48
- }
49
- });
34
+ captureRootColumns(columns: ProjectionNode[]): HydrationPlanner {
35
+ const currentPlan = this.getPlanOrDefault();
36
+ const rootCols = new Set(currentPlan.rootColumns);
37
+ let changed = false;
38
+
39
+ columns.forEach(node => {
40
+ const alias = node.type === 'Column' ? (node.alias || node.name) : node.alias;
41
+ if (!alias || isRelationAlias(alias)) return;
42
+ if (node.type === 'Column' && node.table !== this.table.name) return;
43
+ if (!rootCols.has(alias)) {
44
+ rootCols.add(alias);
45
+ changed = true;
46
+ }
47
+ });
50
48
 
51
49
  if (!changed) return this;
52
50
  return new HydrationPlanner(this.table, {
@@ -9,7 +9,6 @@ import { SelectPredicateFacet } from './predicate-facet.js';
9
9
  import { SelectRelationFacet } from './relation-facet.js';
10
10
  import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
11
11
  import { OrmSession } from '../../orm/orm-session.js';
12
- import { EntityInstance } from '../../schema/types.js';
13
12
  import type { SelectQueryBuilder } from '../select.js';
14
13
 
15
14
  export type WhereHasOptions = {
@@ -85,7 +84,7 @@ export async function executePagedQuery<T, TTable extends TableDef>(
85
84
  session: OrmSession,
86
85
  options: { page: number; pageSize: number },
87
86
  countCallback: (session: OrmSession) => Promise<number>
88
- ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
87
+ ): Promise<{ items: T[]; totalItems: number }> {
89
88
  const { page, pageSize } = options;
90
89
 
91
90
  if (!Number.isInteger(page) || page < 1) {
@@ -2,18 +2,19 @@ import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column-types.js';
3
3
  import { OrderingTerm, SelectQueryNode, SetOperationKind } from '../core/ast/query.js';
4
4
  import { HydrationPlan } from '../core/hydration/types.js';
5
- import {
6
- ColumnNode,
7
- ExpressionNode,
8
- FunctionNode,
9
- BinaryExpressionNode,
10
- CaseExpressionNode,
11
- WindowFunctionNode,
12
- and,
13
- exists,
14
- notExists,
15
- OperandNode
16
- } from '../core/ast/expression.js';
5
+ import {
6
+ ColumnNode,
7
+ ExpressionNode,
8
+ FunctionNode,
9
+ BinaryExpressionNode,
10
+ CaseExpressionNode,
11
+ WindowFunctionNode,
12
+ and,
13
+ exists,
14
+ notExists,
15
+ OperandNode
16
+ } from '../core/ast/expression.js';
17
+ import type { TypedExpression } from '../core/ast/expression.js';
17
18
  import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
18
19
  import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
19
20
 
@@ -31,11 +32,14 @@ import { ColumnSelector } from './column-selector.js';
31
32
  import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
32
33
  import { RelationKinds } from '../schema/relation.js';
33
34
  import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
34
- import { EntityInstance, RelationMap } from '../schema/types.js';
35
+ import { EntityInstance, RelationMap } from '../schema/types.js';
36
+ import type { ColumnToTs, InferRow } from '../schema/types.js';
35
37
  import { OrmSession } from '../orm/orm-session.ts';
36
38
  import { ExecutionContext } from '../orm/execution-context.js';
37
39
  import { HydrationContext } from '../orm/hydration-context.js';
38
- import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
40
+ import { executeHydrated, executeHydratedPlain, executeHydratedWithContexts } from '../orm/execute.js';
41
+ import { EntityConstructor } from '../orm/entity-metadata.js';
42
+ import { materializeAs } from '../orm/entity-materializer.js';
39
43
  import { resolveSelectQuery } from './query-resolution.js';
40
44
  import {
41
45
  applyOrderBy,
@@ -53,8 +57,27 @@ import { SelectCTEFacet } from './select/cte-facet.js';
53
57
  import { SelectSetOpFacet } from './select/setop-facet.js';
54
58
  import { SelectRelationFacet } from './select/relation-facet.js';
55
59
 
56
- type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
57
-
60
+ type ColumnSelectionValue =
61
+ | ColumnDef
62
+ | FunctionNode
63
+ | CaseExpressionNode
64
+ | WindowFunctionNode
65
+ | TypedExpression<unknown>;
66
+
67
+ type SelectionValueType<TValue> =
68
+ TValue extends TypedExpression<infer TRuntime> ? TRuntime :
69
+ TValue extends ColumnDef ? ColumnToTs<TValue> :
70
+ unknown;
71
+
72
+ type SelectionResult<TSelection extends Record<string, ColumnSelectionValue>> = {
73
+ [K in keyof TSelection]: SelectionValueType<TSelection[K]>;
74
+ };
75
+
76
+ type SelectionFromKeys<
77
+ TTable extends TableDef,
78
+ K extends keyof TTable['columns'] & string
79
+ > = Pick<InferRow<TTable>, K>;
80
+
58
81
  type DeepSelectEntry<TTable extends TableDef> = {
59
82
  type: 'root';
60
83
  columns: (keyof TTable['columns'] & string)[];
@@ -71,7 +94,7 @@ type DeepSelectConfig<TTable extends TableDef> = DeepSelectEntry<TTable>[];
71
94
  * @typeParam T - Result type for projections (unused)
72
95
  * @typeParam TTable - Table definition being queried
73
96
  */
74
- export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef> {
97
+ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends TableDef = TableDef> {
75
98
  private readonly env: SelectQueryBuilderEnvironment;
76
99
  private readonly context: SelectQueryBuilderContext;
77
100
  private readonly columnSelector: ColumnSelector;
@@ -84,6 +107,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
84
107
  private readonly relationFacet: SelectRelationFacet;
85
108
  private readonly lazyRelations: Set<string>;
86
109
  private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
110
+ private readonly entityConstructor?: EntityConstructor;
87
111
 
88
112
  /**
89
113
  * Creates a new SelectQueryBuilder instance
@@ -98,7 +122,8 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
98
122
  hydration?: HydrationManager,
99
123
  dependencies?: Partial<SelectQueryBuilderDependencies>,
100
124
  lazyRelations?: Set<string>,
101
- lazyRelationOptions?: Map<string, RelationIncludeOptions>
125
+ lazyRelationOptions?: Map<string, RelationIncludeOptions>,
126
+ entityConstructor?: EntityConstructor
102
127
  ) {
103
128
  const deps = resolveSelectQueryBuilderDependencies(dependencies);
104
129
  this.env = { table, deps };
@@ -111,6 +136,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
111
136
  };
112
137
  this.lazyRelations = new Set(lazyRelations ?? []);
113
138
  this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
139
+ this.entityConstructor = entityConstructor;
114
140
  this.columnSelector = deps.createColumnSelector(this.env);
115
141
  const relationManager = deps.createRelationManager(this.env);
116
142
  this.fromFacet = new SelectFromFacet(this.env, createAstService);
@@ -128,20 +154,21 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
128
154
  * @param lazyRelations - Updated lazy relations set
129
155
  * @returns New SelectQueryBuilder instance
130
156
  */
131
- private clone(
132
- context: SelectQueryBuilderContext = this.context,
133
- lazyRelations = new Set(this.lazyRelations),
134
- lazyRelationOptions = new Map(this.lazyRelationOptions)
135
- ): SelectQueryBuilder<T, TTable> {
136
- return new SelectQueryBuilder(
137
- this.env.table as TTable,
138
- context.state,
139
- context.hydration,
140
- this.env.deps,
141
- lazyRelations,
142
- lazyRelationOptions
143
- );
144
- }
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
+ }
145
172
 
146
173
  /**
147
174
  * Applies an alias to the root FROM table.
@@ -207,32 +234,41 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
207
234
  * fullName: concat(userTable.columns.firstName, ' ', userTable.columns.lastName)
208
235
  * });
209
236
  */
210
- select<K extends keyof TTable['columns'] & string>(
211
- ...args: K[]
212
- ): SelectQueryBuilder<T, TTable>;
213
- select(columns: Record<string, ColumnSelectionValue>): SelectQueryBuilder<T, TTable>;
214
- select<K extends keyof TTable['columns'] & string>(
215
- ...args: K[] | [Record<string, ColumnSelectionValue>]
216
- ): SelectQueryBuilder<T, TTable> {
237
+ select<K extends keyof TTable['columns'] & string>(
238
+ ...args: K[]
239
+ ): SelectQueryBuilder<T & SelectionFromKeys<TTable, K>, TTable>;
240
+ select<TSelection extends Record<string, ColumnSelectionValue>>(
241
+ columns: TSelection
242
+ ): SelectQueryBuilder<T & SelectionResult<TSelection>, TTable>;
243
+ select<
244
+ K extends keyof TTable['columns'] & string,
245
+ TSelection extends Record<string, ColumnSelectionValue>
246
+ >(
247
+ ...args: K[] | [TSelection]
248
+ ): SelectQueryBuilder<T, TTable> {
217
249
  // If first arg is an object (not a string), treat as projection map
218
250
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
219
- const columns = args[0] as Record<string, ColumnSelectionValue>;
220
- return this.clone(this.projectionFacet.select(this.context, columns));
221
- }
251
+ const columns = args[0] as TSelection;
252
+ return this.clone<T & SelectionResult<TSelection>>(
253
+ this.projectionFacet.select(this.context, columns)
254
+ );
255
+ }
222
256
 
223
257
  // Otherwise, treat as column names
224
258
  const cols = args as K[];
225
259
  const selection: Record<string, ColumnDef> = {};
226
- for (const key of cols) {
227
- const col = this.env.table.columns[key];
228
- if (!col) {
229
- throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
230
- }
231
- selection[key] = col;
232
- }
233
-
234
- return this.clone(this.projectionFacet.select(this.context, selection));
235
- }
260
+ for (const key of cols) {
261
+ const col = this.env.table.columns[key];
262
+ if (!col) {
263
+ throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
264
+ }
265
+ selection[key] = col;
266
+ }
267
+
268
+ return this.clone<T & SelectionFromKeys<TTable, K>>(
269
+ this.projectionFacet.select(this.context, selection)
270
+ );
271
+ }
236
272
 
237
273
  /**
238
274
  * Selects raw column expressions
@@ -348,10 +384,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
348
384
  * qb.select('id', 'name')
349
385
  * .selectSubquery('postCount', postCount);
350
386
  */
351
- selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
352
- const query = resolveSelectQuery(sub);
353
- return this.clone(this.projectionFacet.selectSubquery(this.context, alias, query));
354
- }
387
+ selectSubquery<TValue = unknown, K extends string = string, TSub extends TableDef = TableDef>(
388
+ alias: K,
389
+ sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
390
+ ): SelectQueryBuilder<T & Record<K, TValue>, TTable> {
391
+ const query = resolveSelectQuery(sub);
392
+ return this.clone<T & Record<K, TValue>>(
393
+ this.projectionFacet.selectSubquery(this.context, alias, query)
394
+ );
395
+ }
355
396
 
356
397
  /**
357
398
  * Adds a JOIN against a derived table (subquery with alias)
@@ -625,16 +666,75 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
625
666
  }
626
667
 
627
668
  /**
628
- * Executes the query and returns hydrated results
669
+ * Ensures that if no columns are selected, all columns from the table are selected by default.
670
+ */
671
+ private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
672
+ const columns = this.context.state.ast.columns;
673
+ if (!columns || columns.length === 0) {
674
+ const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
675
+ return this.select(...columnKeys);
676
+ }
677
+ return this;
678
+ }
679
+
680
+ /**
681
+ * Executes the query and returns hydrated results.
682
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
683
+ * this will automatically return fully materialized entity instances.
684
+ *
685
+ * @param ctx - ORM session context
686
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
687
+ * @example
688
+ * const users = await selectFromEntity(User).execute(session);
689
+ * // users is User[]
690
+ * users[0] instanceof User; // true
691
+ */
692
+ async execute(ctx: OrmSession): Promise<T[]> {
693
+ if (this.entityConstructor) {
694
+ return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
695
+ }
696
+ const builder = this.ensureDefaultSelection();
697
+ return executeHydrated(ctx, builder) as unknown as T[];
698
+ }
699
+
700
+ /**
701
+ * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
702
+ * Use this if you want raw data even when using selectFromEntity.
703
+ *
704
+ * @param ctx - ORM session context
705
+ * @returns Promise of plain entity instances
706
+ * @example
707
+ * const rows = await selectFromEntity(User).executePlain(session);
708
+ * // rows is EntityInstance<UserTable>[] (plain objects)
709
+ * rows[0] instanceof User; // false
710
+ */
711
+ async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
712
+ const builder = this.ensureDefaultSelection();
713
+ const rows = await executeHydratedPlain(ctx, builder);
714
+ return rows as EntityInstance<TTable>[];
715
+ }
716
+
717
+ /**
718
+ * Executes the query and returns results as real class instances.
719
+ * Unlike execute(), this returns actual instances of the decorated entity class
720
+ * with working methods and proper instanceof checks.
721
+ * @param entityClass - The entity class constructor
629
722
  * @param ctx - ORM session context
630
- * @returns Promise of entity instances
723
+ * @returns Promise of entity class instances
631
724
  * @example
632
- * const users = await qb.select('id', 'name')
633
- * .where(eq(userTable.columns.active, true))
634
- * .execute(session);
725
+ * const users = await selectFromEntity(User)
726
+ * .include('posts')
727
+ * .executeAs(User, session);
728
+ * users[0] instanceof User; // true!
729
+ * users[0].getFullName(); // works!
635
730
  */
636
- async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
637
- return executeHydrated(ctx, this);
731
+ async executeAs<TEntity extends object>(
732
+ entityClass: EntityConstructor<TEntity>,
733
+ ctx: OrmSession
734
+ ): Promise<TEntity[]> {
735
+ const builder = this.ensureDefaultSelection();
736
+ const results = await executeHydrated(ctx, builder);
737
+ return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
638
738
  }
639
739
 
640
740
  /**
@@ -656,8 +756,9 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
656
756
  async executePaged(
657
757
  session: OrmSession,
658
758
  options: { page: number; pageSize: number }
659
- ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
660
- return executePagedQuery(this, session, options, sess => this.count(sess));
759
+ ): Promise<{ items: T[]; totalItems: number }> {
760
+ const builder = this.ensureDefaultSelection();
761
+ return executePagedQuery(builder, session, options, sess => this.count(sess));
661
762
  }
662
763
 
663
764
  /**
@@ -670,8 +771,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
670
771
  * const hydCtx = new HydrationContext();
671
772
  * const users = await qb.executeWithContexts(execCtx, hydCtx);
672
773
  */
673
- async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<EntityInstance<TTable>[]> {
674
- return executeHydratedWithContexts(execCtx, hydCtx, this);
774
+ async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
775
+ const builder = this.ensureDefaultSelection();
776
+ const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
777
+ if (this.entityConstructor) {
778
+ return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
779
+ }
780
+ return results as unknown as T[];
675
781
  }
676
782
 
677
783
  /**