metal-orm 1.0.82 → 1.0.85
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 +300 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +300 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/ast-validation.ts +14 -0
- package/src/core/ast/expression-builders.ts +43 -24
- package/src/core/ast/expression-nodes.ts +35 -25
- package/src/core/ast/expression-visitor.ts +34 -25
- package/src/core/ast/param-proxy.ts +1 -4
- package/src/core/ast/query-visitor.ts +227 -0
- package/src/core/dialect/abstract.ts +70 -46
- package/src/query-builder/query-ast-service.ts +13 -0
- package/src/query-builder/select.ts +128 -109
|
@@ -37,15 +37,17 @@ import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
|
|
|
37
37
|
import type { TableFunctionStrategy } from '../functions/table-types.js';
|
|
38
38
|
import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Context for SQL compilation with parameter management
|
|
42
|
-
*/
|
|
43
|
-
export interface CompilerContext {
|
|
44
|
-
/** Array of parameters */
|
|
45
|
-
params: unknown[];
|
|
46
|
-
/** Function to add a parameter and get its placeholder */
|
|
47
|
-
addParameter(value: unknown): string;
|
|
48
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Context for SQL compilation with parameter management
|
|
42
|
+
*/
|
|
43
|
+
export interface CompilerContext {
|
|
44
|
+
/** Array of parameters */
|
|
45
|
+
params: unknown[];
|
|
46
|
+
/** Function to add a parameter and get its placeholder */
|
|
47
|
+
addParameter(value: unknown): string;
|
|
48
|
+
/** Whether Param operands are allowed (for schema generation) */
|
|
49
|
+
allowParams?: boolean;
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Result of SQL compilation
|
|
@@ -86,18 +88,29 @@ export abstract class Dialect
|
|
|
86
88
|
* @param ast - Query AST to compile
|
|
87
89
|
* @returns Compiled query with SQL and parameters
|
|
88
90
|
*/
|
|
89
|
-
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
90
|
-
const ctx = this.createCompilerContext();
|
|
91
|
-
const normalized = this.normalizeSelectAst(ast);
|
|
92
|
-
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
93
|
-
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
94
|
-
return {
|
|
95
|
-
sql,
|
|
96
|
-
params: [...ctx.params]
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
92
|
+
const ctx = this.createCompilerContext();
|
|
93
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
94
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
95
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
96
|
+
return {
|
|
97
|
+
sql,
|
|
98
|
+
params: [...ctx.params]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
compileSelectWithOptions(ast: SelectQueryNode, options: { allowParams?: boolean } = {}): CompiledQuery {
|
|
103
|
+
const ctx = this.createCompilerContext(options);
|
|
104
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
105
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
106
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
107
|
+
return {
|
|
108
|
+
sql,
|
|
109
|
+
params: [...ctx.params]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
101
114
|
const ctx = this.createCompilerContext();
|
|
102
115
|
const rawSql = this.compileInsertAst(ast, ctx).trim();
|
|
103
116
|
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
@@ -198,22 +211,24 @@ export abstract class Dialect
|
|
|
198
211
|
return `SELECT 1${tail}`;
|
|
199
212
|
}
|
|
200
213
|
|
|
201
|
-
/**
|
|
202
|
-
* Creates a new compiler context
|
|
203
|
-
* @
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Creates a new compiler context
|
|
216
|
+
* @param options - Optional compiler context options
|
|
217
|
+
* @returns Compiler context with parameter management
|
|
218
|
+
*/
|
|
219
|
+
protected createCompilerContext(options: { allowParams?: boolean } = {}): CompilerContext {
|
|
220
|
+
const params: unknown[] = [];
|
|
221
|
+
let counter = 0;
|
|
222
|
+
return {
|
|
223
|
+
params,
|
|
224
|
+
allowParams: options.allowParams ?? false,
|
|
225
|
+
addParameter: (value: unknown) => {
|
|
226
|
+
counter += 1;
|
|
227
|
+
params.push(value);
|
|
228
|
+
return this.formatPlaceholder(counter);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
217
232
|
|
|
218
233
|
/**
|
|
219
234
|
* Formats a parameter placeholder
|
|
@@ -372,13 +387,17 @@ export abstract class Dialect
|
|
|
372
387
|
* @param ctx - Compiler context
|
|
373
388
|
* @returns Compiled SQL operand
|
|
374
389
|
*/
|
|
375
|
-
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
391
|
+
const descriptor = Object.getOwnPropertyDescriptor(node, 'type');
|
|
392
|
+
const nodeType = typeof descriptor?.value === 'string'
|
|
393
|
+
? descriptor.value
|
|
394
|
+
: (typeof node.type === 'string' ? node.type : undefined);
|
|
395
|
+
const compiler = nodeType ? this.operandCompilers.get(nodeType) : undefined;
|
|
396
|
+
if (!compiler) {
|
|
397
|
+
throw new Error(`Unsupported operand node type "${nodeType ?? 'unknown'}" for ${this.constructor.name}`);
|
|
398
|
+
}
|
|
399
|
+
return compiler(node, ctx);
|
|
400
|
+
}
|
|
382
401
|
|
|
383
402
|
/**
|
|
384
403
|
* Compiles an ordering term (operand, expression, or alias reference).
|
|
@@ -453,9 +472,14 @@ export abstract class Dialect
|
|
|
453
472
|
});
|
|
454
473
|
}
|
|
455
474
|
|
|
456
|
-
private registerDefaultOperandCompilers(): void {
|
|
475
|
+
private registerDefaultOperandCompilers(): void {
|
|
457
476
|
this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
|
|
458
|
-
this.registerOperandCompiler('Param', (_param: ParamNode, ctx) =>
|
|
477
|
+
this.registerOperandCompiler('Param', (_param: ParamNode, ctx) => {
|
|
478
|
+
if (!ctx.allowParams) {
|
|
479
|
+
throw new Error('Cannot compile query with Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
|
|
480
|
+
}
|
|
481
|
+
return ctx.addParameter(null);
|
|
482
|
+
});
|
|
459
483
|
|
|
460
484
|
this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
|
|
461
485
|
void _ctx;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
CastExpressionNode,
|
|
19
19
|
WindowFunctionNode,
|
|
20
20
|
ScalarSubqueryNode,
|
|
21
|
+
ParamNode,
|
|
21
22
|
and,
|
|
22
23
|
isExpressionSelectionNode,
|
|
23
24
|
isOperandNode
|
|
@@ -258,6 +259,10 @@ export class QueryAstService {
|
|
|
258
259
|
* @returns Normalized ordering term
|
|
259
260
|
*/
|
|
260
261
|
private normalizeOrderingTerm(term: ColumnDef | OrderingTerm): OrderingTerm {
|
|
262
|
+
const paramNode = this.toParamNode(term);
|
|
263
|
+
if (paramNode) {
|
|
264
|
+
return paramNode;
|
|
265
|
+
}
|
|
261
266
|
const from = this.state.ast.from;
|
|
262
267
|
const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
|
|
263
268
|
const termType = (term as { type?: string }).type;
|
|
@@ -284,4 +289,12 @@ export class QueryAstService {
|
|
|
284
289
|
return buildColumnNode(tableRef, term as ColumnDef);
|
|
285
290
|
}
|
|
286
291
|
|
|
292
|
+
private toParamNode(value: unknown): ParamNode | undefined {
|
|
293
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
294
|
+
const type = Object.getOwnPropertyDescriptor(value, 'type')?.value;
|
|
295
|
+
if (type !== 'Param') return undefined;
|
|
296
|
+
const name = Object.getOwnPropertyDescriptor(value, 'name')?.value;
|
|
297
|
+
if (typeof name !== 'string') return undefined;
|
|
298
|
+
return { type: 'Param', name };
|
|
299
|
+
}
|
|
287
300
|
}
|
|
@@ -68,6 +68,7 @@ import { SelectCTEFacet } from './select/cte-facet.js';
|
|
|
68
68
|
import { SelectSetOpFacet } from './select/setop-facet.js';
|
|
69
69
|
import { SelectRelationFacet } from './select/relation-facet.js';
|
|
70
70
|
import { buildFilterParameters, extractSchema, SchemaOptions, OpenApiSchemaBundle } from '../openapi/index.js';
|
|
71
|
+
import { hasParamOperandsInQuery } from '../core/ast/ast-validation.js';
|
|
71
72
|
|
|
72
73
|
type ColumnSelectionValue =
|
|
73
74
|
| ColumnDef
|
|
@@ -713,120 +714,138 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
713
714
|
return this.env.table as TTable;
|
|
714
715
|
}
|
|
715
716
|
|
|
716
|
-
/**
|
|
717
|
-
* Ensures that if no columns are selected, all columns from the table are selected by default.
|
|
718
|
-
*/
|
|
719
|
-
private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
|
|
720
|
-
const columns = this.context.state.ast.columns;
|
|
721
|
-
if (!columns || columns.length === 0) {
|
|
722
|
-
const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
|
|
723
|
-
return this.select(...columnKeys);
|
|
724
|
-
}
|
|
725
|
-
return this;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
*/
|
|
740
|
-
async execute(ctx: OrmSession): Promise<T[]> {
|
|
741
|
-
if (this.entityConstructor) {
|
|
742
|
-
return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
|
|
743
|
-
}
|
|
744
|
-
const builder = this.ensureDefaultSelection();
|
|
745
|
-
return executeHydrated(ctx, builder) as unknown as T[];
|
|
746
|
-
}
|
|
717
|
+
/**
|
|
718
|
+
* Ensures that if no columns are selected, all columns from the table are selected by default.
|
|
719
|
+
*/
|
|
720
|
+
private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
|
|
721
|
+
const columns = this.context.state.ast.columns;
|
|
722
|
+
if (!columns || columns.length === 0) {
|
|
723
|
+
const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
|
|
724
|
+
return this.select(...columnKeys);
|
|
725
|
+
}
|
|
726
|
+
return this;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Validates that the query does not contain Param operands.
|
|
731
|
+
* Param proxies are only for schema generation, not execution.
|
|
732
|
+
*/
|
|
733
|
+
private validateNoParamOperands(): void {
|
|
734
|
+
const ast = this.context.hydration.applyToAst(this.context.state.ast);
|
|
735
|
+
const hasParams = hasParamOperandsInQuery(ast);
|
|
736
|
+
if (hasParams) {
|
|
737
|
+
throw new Error('Cannot execute query containing Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
747
740
|
|
|
748
|
-
/**
|
|
749
|
-
* Executes the query and returns
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
* @
|
|
754
|
-
* @
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Executes the query and returns hydrated results.
|
|
743
|
+
* If the builder was created with an entity constructor (e.g. via selectFromEntity),
|
|
744
|
+
* this will automatically return fully materialized entity instances.
|
|
745
|
+
*
|
|
746
|
+
* @param ctx - ORM session context
|
|
747
|
+
* @returns Promise of entity instances (or objects if generic T is not an entity)
|
|
748
|
+
* @example
|
|
749
|
+
* const users = await selectFromEntity(User).execute(session);
|
|
750
|
+
* // users is User[]
|
|
751
|
+
* users[0] instanceof User; // true
|
|
752
|
+
*/
|
|
753
|
+
async execute(ctx: OrmSession): Promise<T[]> {
|
|
754
|
+
this.validateNoParamOperands();
|
|
755
|
+
if (this.entityConstructor) {
|
|
756
|
+
return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
|
|
757
|
+
}
|
|
758
|
+
const builder = this.ensureDefaultSelection();
|
|
759
|
+
return executeHydrated(ctx, builder) as unknown as T[];
|
|
760
|
+
}
|
|
764
761
|
|
|
765
|
-
/**
|
|
766
|
-
* Executes the query and returns
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* @param
|
|
770
|
-
* @
|
|
771
|
-
* @
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
): Promise<TEntity[]> {
|
|
783
|
-
const builder = this.ensureDefaultSelection();
|
|
784
|
-
const results = await executeHydrated(ctx, builder);
|
|
785
|
-
return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
|
|
786
|
-
}
|
|
762
|
+
/**
|
|
763
|
+
* Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
|
|
764
|
+
* Use this if you want raw data even when using selectFromEntity.
|
|
765
|
+
*
|
|
766
|
+
* @param ctx - ORM session context
|
|
767
|
+
* @returns Promise of plain entity instances
|
|
768
|
+
* @example
|
|
769
|
+
* const rows = await selectFromEntity(User).executePlain(session);
|
|
770
|
+
* // rows is EntityInstance<UserTable>[] (plain objects)
|
|
771
|
+
* rows[0] instanceof User; // false
|
|
772
|
+
*/
|
|
773
|
+
async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
|
|
774
|
+
this.validateNoParamOperands();
|
|
775
|
+
const builder = this.ensureDefaultSelection();
|
|
776
|
+
const rows = await executeHydratedPlain(ctx, builder);
|
|
777
|
+
return rows as EntityInstance<TTable>[];
|
|
778
|
+
}
|
|
787
779
|
|
|
788
|
-
/**
|
|
789
|
-
* Executes
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
780
|
+
/**
|
|
781
|
+
* Executes the query and returns results as real class instances.
|
|
782
|
+
* Unlike execute(), this returns actual instances of the decorated entity class
|
|
783
|
+
* with working methods and proper instanceof checks.
|
|
784
|
+
* @param entityClass - The entity class constructor
|
|
785
|
+
* @param ctx - ORM session context
|
|
786
|
+
* @returns Promise of entity class instances
|
|
787
|
+
* @example
|
|
788
|
+
* const users = await selectFromEntity(User)
|
|
789
|
+
* .include('posts')
|
|
790
|
+
* .executeAs(User, session);
|
|
791
|
+
* users[0] instanceof User; // true!
|
|
792
|
+
* users[0].getFullName(); // works!
|
|
793
|
+
*/
|
|
794
|
+
async executeAs<TEntity extends object>(
|
|
795
|
+
entityClass: EntityConstructor<TEntity>,
|
|
796
|
+
ctx: OrmSession
|
|
797
|
+
): Promise<TEntity[]> {
|
|
798
|
+
this.validateNoParamOperands();
|
|
799
|
+
const builder = this.ensureDefaultSelection();
|
|
800
|
+
const results = await executeHydrated(ctx, builder);
|
|
801
|
+
return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
|
|
802
|
+
}
|
|
797
803
|
|
|
798
|
-
/**
|
|
799
|
-
* Executes
|
|
800
|
-
*
|
|
801
|
-
* @example
|
|
802
|
-
* const
|
|
803
|
-
*/
|
|
804
|
-
async
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
804
|
+
/**
|
|
805
|
+
* Executes a count query for the current builder without LIMIT/OFFSET clauses.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* const total = await qb.count(session);
|
|
809
|
+
*/
|
|
810
|
+
async count(session: OrmSession): Promise<number> {
|
|
811
|
+
this.validateNoParamOperands();
|
|
812
|
+
return executeCount(this.context, this.env, session);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Executes the query and returns both the paged items and the total.
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
|
|
820
|
+
*/
|
|
821
|
+
async executePaged(
|
|
822
|
+
session: OrmSession,
|
|
823
|
+
options: { page: number; pageSize: number }
|
|
824
|
+
): Promise<PaginatedResult<T>> {
|
|
825
|
+
this.validateNoParamOperands();
|
|
826
|
+
const builder = this.ensureDefaultSelection();
|
|
827
|
+
return executePagedQuery(builder, session, options, sess => builder.count(sess));
|
|
828
|
+
}
|
|
811
829
|
|
|
812
|
-
/**
|
|
813
|
-
* Executes the query with provided execution and hydration contexts
|
|
814
|
-
* @param execCtx - Execution context
|
|
815
|
-
* @param hydCtx - Hydration context
|
|
816
|
-
* @returns Promise of entity instances
|
|
817
|
-
* @example
|
|
818
|
-
* const execCtx = new ExecutionContext(session);
|
|
819
|
-
* const hydCtx = new HydrationContext();
|
|
820
|
-
* const users = await qb.executeWithContexts(execCtx, hydCtx);
|
|
821
|
-
*/
|
|
822
|
-
async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
+
/**
|
|
831
|
+
* Executes the query with provided execution and hydration contexts
|
|
832
|
+
* @param execCtx - Execution context
|
|
833
|
+
* @param hydCtx - Hydration context
|
|
834
|
+
* @returns Promise of entity instances
|
|
835
|
+
* @example
|
|
836
|
+
* const execCtx = new ExecutionContext(session);
|
|
837
|
+
* const hydCtx = new HydrationContext();
|
|
838
|
+
* const users = await qb.executeWithContexts(execCtx, hydCtx);
|
|
839
|
+
*/
|
|
840
|
+
async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
|
|
841
|
+
this.validateNoParamOperands();
|
|
842
|
+
const builder = this.ensureDefaultSelection();
|
|
843
|
+
const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
|
|
844
|
+
if (this.entityConstructor) {
|
|
845
|
+
return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
|
|
846
|
+
}
|
|
847
|
+
return results as unknown as T[];
|
|
848
|
+
}
|
|
830
849
|
|
|
831
850
|
/**
|
|
832
851
|
* Adds a WHERE condition to the query
|