metal-orm 1.0.52 → 1.0.53
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 +3 -1
- package/dist/index.cjs +66 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -7
- package/dist/index.d.ts +20 -7
- package/dist/index.js +65 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/aggregate-functions.ts +10 -0
- package/src/core/functions/standard-strategy.ts +1 -1
- package/src/orm/execute.ts +37 -36
- package/src/orm/save-graph-types.ts +4 -6
- package/src/query-builder/delete.ts +5 -4
- package/src/query-builder/select.ts +68 -10
- package/src/query-builder/update.ts +5 -4
package/package.json
CHANGED
|
@@ -45,6 +45,16 @@ export const min = buildAggregate('MIN');
|
|
|
45
45
|
*/
|
|
46
46
|
export const max = buildAggregate('MAX');
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Creates a COUNT(*) function expression.
|
|
50
|
+
* @returns Function node with COUNT(*)
|
|
51
|
+
*/
|
|
52
|
+
export const countAll = (): FunctionNode => ({
|
|
53
|
+
type: 'Function',
|
|
54
|
+
name: 'COUNT',
|
|
55
|
+
args: []
|
|
56
|
+
});
|
|
57
|
+
|
|
48
58
|
type GroupConcatOrderByInput = {
|
|
49
59
|
column: ColumnRef | ColumnNode;
|
|
50
60
|
direction?: OrderDirection;
|
|
@@ -16,7 +16,7 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
16
16
|
|
|
17
17
|
protected registerStandard() {
|
|
18
18
|
// Register ANSI standard implementations
|
|
19
|
-
this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
|
|
19
|
+
this.add('COUNT', ({ compiledArgs }) => compiledArgs.length ? `COUNT(${compiledArgs.join(', ')})` : 'COUNT(*)');
|
|
20
20
|
this.add('SUM', ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
|
|
21
21
|
this.add('AVG', ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
|
|
22
22
|
this.add('MIN', ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
|
package/src/orm/execute.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { EntityInstance } from '../schema/types.js';
|
|
|
3
3
|
import { hydrateRows } from './hydration.js';
|
|
4
4
|
import { OrmSession } from './orm-session.ts';
|
|
5
5
|
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
6
|
-
import { createEntityProxy, createEntityFromRow } from './entity.js';
|
|
7
|
-
import { EntityContext } from './entity-context.js';
|
|
8
|
-
import { ExecutionContext } from './execution-context.js';
|
|
9
|
-
import { HydrationContext } from './hydration-context.js';
|
|
6
|
+
import { createEntityProxy, createEntityFromRow } from './entity.js';
|
|
7
|
+
import { EntityContext } from './entity-context.js';
|
|
8
|
+
import { ExecutionContext } from './execution-context.js';
|
|
9
|
+
import { HydrationContext } from './hydration-context.js';
|
|
10
10
|
|
|
11
11
|
type Row = Record<string, unknown>;
|
|
12
12
|
|
|
13
|
-
const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
|
|
13
|
+
const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
|
|
14
14
|
const rows: Row[] = [];
|
|
15
15
|
for (const result of results) {
|
|
16
16
|
const { columns, values } = result;
|
|
@@ -23,20 +23,21 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
return rows;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const executeWithContexts = async <TTable extends TableDef>(
|
|
29
|
+
execCtx: ExecutionContext,
|
|
30
|
+
entityCtx: EntityContext,
|
|
31
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
32
|
+
): Promise<EntityInstance<TTable>[]> => {
|
|
33
|
+
const ast = qb.getAST();
|
|
34
|
+
const compiled = execCtx.dialect.compileSelect(ast);
|
|
35
|
+
const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
36
|
+
const rows = flattenResults(executed);
|
|
37
|
+
|
|
38
|
+
if (ast.setOps && ast.setOps.length > 0) {
|
|
39
|
+
return rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
|
|
40
|
+
}
|
|
40
41
|
|
|
41
42
|
const hydrated = hydrateRows(rows, qb.getHydrationPlan());
|
|
42
43
|
return hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
|
|
@@ -49,12 +50,12 @@ const executeWithEntityContext = async <TTable extends TableDef>(
|
|
|
49
50
|
* @param qb - The select query builder
|
|
50
51
|
* @returns Promise resolving to array of entity instances
|
|
51
52
|
*/
|
|
52
|
-
export async function executeHydrated<TTable extends TableDef>(
|
|
53
|
-
session: OrmSession,
|
|
54
|
-
qb: SelectQueryBuilder<unknown, TTable>
|
|
55
|
-
): Promise<EntityInstance<TTable>[]> {
|
|
56
|
-
return
|
|
57
|
-
}
|
|
53
|
+
export async function executeHydrated<TTable extends TableDef>(
|
|
54
|
+
session: OrmSession,
|
|
55
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
56
|
+
): Promise<EntityInstance<TTable>[]> {
|
|
57
|
+
return executeWithContexts(session.getExecutionContext(), session, qb);
|
|
58
|
+
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Executes a hydrated query using execution and hydration contexts.
|
|
@@ -64,14 +65,14 @@ export async function executeHydrated<TTable extends TableDef>(
|
|
|
64
65
|
* @param qb - The select query builder
|
|
65
66
|
* @returns Promise resolving to array of entity instances
|
|
66
67
|
*/
|
|
67
|
-
export async function executeHydratedWithContexts<TTable extends TableDef>(
|
|
68
|
-
|
|
69
|
-
hydCtx: HydrationContext,
|
|
70
|
-
qb: SelectQueryBuilder<unknown, TTable>
|
|
71
|
-
): Promise<EntityInstance<TTable>[]> {
|
|
72
|
-
const entityCtx = hydCtx.entityContext;
|
|
73
|
-
if (!entityCtx) {
|
|
74
|
-
throw new Error('Hydration context is missing an EntityContext');
|
|
75
|
-
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
68
|
+
export async function executeHydratedWithContexts<TTable extends TableDef>(
|
|
69
|
+
execCtx: ExecutionContext,
|
|
70
|
+
hydCtx: HydrationContext,
|
|
71
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
72
|
+
): Promise<EntityInstance<TTable>[]> {
|
|
73
|
+
const entityCtx = hydCtx.entityContext;
|
|
74
|
+
if (!entityCtx) {
|
|
75
|
+
throw new Error('Hydration context is missing an EntityContext');
|
|
76
|
+
}
|
|
77
|
+
return executeWithContexts(execCtx, entityCtx, qb);
|
|
78
|
+
}
|
|
@@ -27,12 +27,11 @@ type ColumnKeys<T> = Exclude<keyof T & string, FunctionKeys<T> | RelationKeys<T>
|
|
|
27
27
|
export type SaveGraphJsonScalar<T> = T extends Date ? string : T;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Input scalar type
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Input scalar type for `OrmSession.saveGraph` payloads.
|
|
31
|
+
*
|
|
32
|
+
* Note: runtime coercion is opt-in via `SaveGraphOptions.coerce`.
|
|
33
33
|
*/
|
|
34
|
-
export type SaveGraphInputScalar<T> =
|
|
35
|
-
T extends Date ? Date | string : T;
|
|
34
|
+
export type SaveGraphInputScalar<T> = T;
|
|
36
35
|
|
|
37
36
|
type ColumnInput<TEntity> = {
|
|
38
37
|
[K in ColumnKeys<TEntity>]?: SaveGraphInputScalar<TEntity[K]>;
|
|
@@ -52,6 +51,5 @@ type RelationInput<TEntity> = {
|
|
|
52
51
|
/**
|
|
53
52
|
* Typed payload accepted by `OrmSession.saveGraph`:
|
|
54
53
|
* - Only entity scalar keys + relation keys are accepted.
|
|
55
|
-
* - Scalars can use JSON-friendly values (e.g., Date fields accept ISO strings).
|
|
56
54
|
*/
|
|
57
55
|
export type SaveGraphInputPayload<TEntity> = ColumnInput<TEntity> & RelationInput<TEntity>;
|
|
@@ -127,10 +127,11 @@ export class DeleteQueryBuilder<T> {
|
|
|
127
127
|
* @param session - The ORM session to execute the query with
|
|
128
128
|
* @returns A promise that resolves to the query results
|
|
129
129
|
*/
|
|
130
|
-
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
131
|
+
const execCtx = session.getExecutionContext();
|
|
132
|
+
const compiled = this.compile(execCtx.dialect);
|
|
133
|
+
return execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
134
|
+
}
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* Returns the Abstract Syntax Tree (AST) representation of the query
|
|
@@ -533,16 +533,74 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
|
|
|
533
533
|
* @param ctx - ORM session context
|
|
534
534
|
* @returns Promise of entity instances
|
|
535
535
|
*/
|
|
536
|
-
async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
|
|
537
|
-
return executeHydrated(ctx, this);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
536
|
+
async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
|
|
537
|
+
return executeHydrated(ctx, this);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private withAst(ast: SelectQueryNode): SelectQueryBuilder<T, TTable> {
|
|
541
|
+
const nextState = new SelectQueryState(this.env.table as TTable, ast);
|
|
542
|
+
const nextContext: SelectQueryBuilderContext = {
|
|
543
|
+
...this.context,
|
|
544
|
+
state: nextState
|
|
545
|
+
};
|
|
546
|
+
return this.clone(nextContext);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async count(session: OrmSession): Promise<number> {
|
|
550
|
+
const unpagedAst: SelectQueryNode = {
|
|
551
|
+
...this.context.state.ast,
|
|
552
|
+
orderBy: undefined,
|
|
553
|
+
limit: undefined,
|
|
554
|
+
offset: undefined
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const subAst = this.withAst(unpagedAst).getAST();
|
|
558
|
+
|
|
559
|
+
const countQuery: SelectQueryNode = {
|
|
560
|
+
type: 'SelectQuery',
|
|
561
|
+
from: derivedTable(subAst, '__metal_count'),
|
|
562
|
+
columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
|
|
563
|
+
joins: []
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const execCtx = session.getExecutionContext();
|
|
567
|
+
const compiled = execCtx.dialect.compileSelect(countQuery);
|
|
568
|
+
const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
569
|
+
const value = results[0]?.values?.[0]?.[0];
|
|
570
|
+
|
|
571
|
+
if (typeof value === 'number') return value;
|
|
572
|
+
if (typeof value === 'bigint') return Number(value);
|
|
573
|
+
if (typeof value === 'string') return Number(value);
|
|
574
|
+
return value === null || value === undefined ? 0 : Number(value);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async executePaged(
|
|
578
|
+
session: OrmSession,
|
|
579
|
+
options: { page: number; pageSize: number }
|
|
580
|
+
): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
|
|
581
|
+
const { page, pageSize } = options;
|
|
582
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
583
|
+
throw new Error('executePaged: page must be an integer >= 1');
|
|
584
|
+
}
|
|
585
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
586
|
+
throw new Error('executePaged: pageSize must be an integer >= 1');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const offset = (page - 1) * pageSize;
|
|
590
|
+
const [items, totalItems] = await Promise.all([
|
|
591
|
+
this.limit(pageSize).offset(offset).execute(session),
|
|
592
|
+
this.count(session)
|
|
593
|
+
]);
|
|
594
|
+
|
|
595
|
+
return { items, totalItems };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Executes the query with provided execution and hydration contexts
|
|
600
|
+
* @param execCtx - Execution context
|
|
601
|
+
* @param hydCtx - Hydration context
|
|
602
|
+
* @returns Promise of entity instances
|
|
603
|
+
*/
|
|
546
604
|
async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<EntityInstance<TTable>[]> {
|
|
547
605
|
return executeHydratedWithContexts(execCtx, hydCtx, this);
|
|
548
606
|
}
|
|
@@ -137,10 +137,11 @@ export class UpdateQueryBuilder<T> {
|
|
|
137
137
|
* @param session - The ORM session to execute the query with
|
|
138
138
|
* @returns A promise that resolves to the query results
|
|
139
139
|
*/
|
|
140
|
-
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
141
|
+
const execCtx = session.getExecutionContext();
|
|
142
|
+
const compiled = this.compile(execCtx.dialect);
|
|
143
|
+
return execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
144
|
+
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
147
|
* Returns the Abstract Syntax Tree (AST) representation of the query
|