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.
- package/README.md +15 -11
- package/dist/index.cjs +266 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -16
- package/dist/index.d.ts +163 -16
- package/dist/index.js +259 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/expression.ts +9 -0
- package/src/decorators/bootstrap.ts +149 -137
- package/src/decorators/index.ts +4 -0
- package/src/orm/entity-materializer.ts +159 -0
- package/src/orm/entity-registry.ts +39 -0
- package/src/orm/execute.ts +62 -18
- package/src/query-builder/hydration-planner.ts +14 -16
- package/src/query-builder/select/select-operations.ts +1 -2
- package/src/query-builder/select.ts +173 -67
|
@@ -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
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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<
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
220
|
-
return this.clone(
|
|
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
|
|
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<
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
*
|
|
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
|
|
633
|
-
* .
|
|
634
|
-
* .
|
|
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
|
|
637
|
-
|
|
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:
|
|
660
|
-
|
|
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<
|
|
674
|
-
|
|
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
|
/**
|