metal-orm 1.0.62 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -2,6 +2,8 @@
2
2
  * Expression AST nodes and builders.
3
3
  * Re-exports components for building and visiting SQL expression trees.
4
4
  */
5
+ import type { CaseExpressionNode, FunctionNode, WindowFunctionNode } from './expression-nodes.js';
6
+
5
7
  export * from './expression-nodes.js';
6
8
  export * from './expression-builders.js';
7
9
  export * from './window-functions.js';
@@ -9,3 +11,10 @@ export * from './aggregate-functions.js';
9
11
  export * from './expression-visitor.js';
10
12
  export type { ColumnRef, TableRef as AstTableRef } from './types.js';
11
13
  export * from './adapters.js';
14
+
15
+ export type TypedExpression<T> =
16
+ (FunctionNode | CaseExpressionNode | WindowFunctionNode) & { __tsType: T };
17
+
18
+ export const asType = <T>(
19
+ expr: FunctionNode | CaseExpressionNode | WindowFunctionNode
20
+ ): TypedExpression<T> => expr as TypedExpression<T>;
@@ -25,13 +25,14 @@ import {
25
25
 
26
26
  import { tableRef, type TableRef } from '../schema/table.js';
27
27
  import {
28
- SelectableKeys,
29
- ColumnDef,
30
- HasManyCollection,
31
- HasOneReference,
32
- BelongsToReference,
33
- ManyToManyCollection
34
- } from '../schema/types.js';
28
+ SelectableKeys,
29
+ ColumnDef,
30
+ HasManyCollection,
31
+ HasOneReference,
32
+ BelongsToReference,
33
+ ManyToManyCollection,
34
+ EntityInstance
35
+ } from '../schema/types.js';
35
36
 
36
37
  const unwrapTarget = (target: EntityOrTableTargetResolver): EntityOrTableTarget => {
37
38
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -196,9 +197,9 @@ type NonFunctionKeys<T> = {
196
197
  type RelationKeys<TEntity extends object> =
197
198
  Exclude<NonFunctionKeys<TEntity>, SelectableKeys<TEntity>> & string;
198
199
 
199
- type EntityTable<TEntity extends object> =
200
- Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
201
- relations: {
200
+ type EntityTable<TEntity extends object> =
201
+ Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
202
+ relations: {
202
203
  [K in RelationKeys<TEntity>]:
203
204
  NonNullable<TEntity[K]> extends HasManyCollection<infer TChild>
204
205
  ? HasManyRelation<EntityTable<NonNullable<TChild> & object>>
@@ -214,15 +215,18 @@ type EntityTable<TEntity extends object> =
214
215
  : NonNullable<TEntity[K]> extends object
215
216
  ? BelongsToRelation<EntityTable<NonNullable<TEntity[K]> & object>>
216
217
  : never;
217
- };
218
- };
219
-
220
- export const selectFromEntity = <TEntity extends object>(
221
- ctor: EntityConstructor<TEntity>
222
- ): SelectQueryBuilder<TEntity, EntityTable<TEntity>> => {
223
- const table = getTableDefFromEntity(ctor);
224
- if (!table) {
225
- throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
218
+ };
219
+ };
220
+
221
+ export type DecoratedEntityInstance<TEntity extends object> =
222
+ TEntity & EntityInstance<EntityTable<TEntity>>;
223
+
224
+ export const selectFromEntity = <TEntity extends object>(
225
+ ctor: EntityConstructor<TEntity>
226
+ ): SelectQueryBuilder<DecoratedEntityInstance<TEntity>, EntityTable<TEntity>> => {
227
+ const table = getTableDefFromEntity(ctor);
228
+ if (!table) {
229
+ throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
226
230
  }
227
231
  return new SelectQueryBuilder(
228
232
  table as unknown as EntityTable<TEntity>,
@@ -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, {
@@ -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,7 +32,8 @@ 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';
@@ -55,8 +57,27 @@ import { SelectCTEFacet } from './select/cte-facet.js';
55
57
  import { SelectSetOpFacet } from './select/setop-facet.js';
56
58
  import { SelectRelationFacet } from './select/relation-facet.js';
57
59
 
58
- type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
59
-
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
+
60
81
  type DeepSelectEntry<TTable extends TableDef> = {
61
82
  type: 'root';
62
83
  columns: (keyof TTable['columns'] & string)[];
@@ -133,21 +154,21 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
133
154
  * @param lazyRelations - Updated lazy relations set
134
155
  * @returns New SelectQueryBuilder instance
135
156
  */
136
- private clone(
137
- context: SelectQueryBuilderContext = this.context,
138
- lazyRelations = new Set(this.lazyRelations),
139
- lazyRelationOptions = new Map(this.lazyRelationOptions)
140
- ): SelectQueryBuilder<T, TTable> {
141
- return new SelectQueryBuilder(
142
- this.env.table as TTable,
143
- context.state,
144
- context.hydration,
145
- this.env.deps,
146
- lazyRelations,
147
- lazyRelationOptions,
148
- this.entityConstructor
149
- );
150
- }
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
+ }
151
172
 
152
173
  /**
153
174
  * Applies an alias to the root FROM table.
@@ -213,32 +234,41 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
213
234
  * fullName: concat(userTable.columns.firstName, ' ', userTable.columns.lastName)
214
235
  * });
215
236
  */
216
- select<K extends keyof TTable['columns'] & string>(
217
- ...args: K[]
218
- ): SelectQueryBuilder<T, TTable>;
219
- select(columns: Record<string, ColumnSelectionValue>): SelectQueryBuilder<T, TTable>;
220
- select<K extends keyof TTable['columns'] & string>(
221
- ...args: K[] | [Record<string, ColumnSelectionValue>]
222
- ): 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> {
223
249
  // If first arg is an object (not a string), treat as projection map
224
250
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
225
- const columns = args[0] as Record<string, ColumnSelectionValue>;
226
- return this.clone(this.projectionFacet.select(this.context, columns));
227
- }
251
+ const columns = args[0] as TSelection;
252
+ return this.clone<T & SelectionResult<TSelection>>(
253
+ this.projectionFacet.select(this.context, columns)
254
+ );
255
+ }
228
256
 
229
257
  // Otherwise, treat as column names
230
258
  const cols = args as K[];
231
259
  const selection: Record<string, ColumnDef> = {};
232
- for (const key of cols) {
233
- const col = this.env.table.columns[key];
234
- if (!col) {
235
- throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
236
- }
237
- selection[key] = col;
238
- }
239
-
240
- return this.clone(this.projectionFacet.select(this.context, selection));
241
- }
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
+ }
242
272
 
243
273
  /**
244
274
  * Selects raw column expressions
@@ -354,10 +384,15 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
354
384
  * qb.select('id', 'name')
355
385
  * .selectSubquery('postCount', postCount);
356
386
  */
357
- selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
358
- const query = resolveSelectQuery(sub);
359
- return this.clone(this.projectionFacet.selectSubquery(this.context, alias, query));
360
- }
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
+ }
361
396
 
362
397
  /**
363
398
  * Adds a JOIN against a derived table (subquery with alias)
@@ -675,7 +710,8 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
675
710
  */
676
711
  async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
677
712
  const builder = this.ensureDefaultSelection();
678
- return executeHydratedPlain(ctx, builder) as EntityInstance<TTable>[];
713
+ const rows = await executeHydratedPlain(ctx, builder);
714
+ return rows as EntityInstance<TTable>[];
679
715
  }
680
716
 
681
717
  /**