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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.52",
3
+ "version": "1.0.53",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -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]})`);
@@ -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 executeWithEntityContext = async <TTable extends TableDef>(
29
- entityCtx: EntityContext,
30
- qb: SelectQueryBuilder<unknown, TTable>
31
- ): Promise<EntityInstance<TTable>[]> => {
32
- const ast = qb.getAST();
33
- const compiled = entityCtx.dialect.compileSelect(ast);
34
- const executed = await entityCtx.executor.executeSql(compiled.sql, compiled.params);
35
- const rows = flattenResults(executed);
36
-
37
- if (ast.setOps && ast.setOps.length > 0) {
38
- return rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
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 executeWithEntityContext(session, qb);
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
- _execCtx: ExecutionContext,
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 executeWithEntityContext(entityCtx, qb);
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 that accepts JSON-friendly values for common runtime types.
31
- * Currently:
32
- * - Date fields accept `Date | string` (ISO string recommended)
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 compiled = this.compile(session.dialect);
132
- return session.executor.executeSql(compiled.sql, compiled.params);
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
- * Executes the query with provided execution and hydration contexts
542
- * @param execCtx - Execution context
543
- * @param hydCtx - Hydration context
544
- * @returns Promise of entity instances
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 compiled = this.compile(session.dialect);
142
- return session.executor.executeSql(compiled.sql, compiled.params);
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