metal-orm 1.0.58 → 1.0.60

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.
Files changed (41) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1583 -901
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +400 -129
  5. package/dist/index.d.ts +400 -129
  6. package/dist/index.js +1575 -901
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +183 -146
  11. package/src/decorators/column-decorator.ts +8 -49
  12. package/src/decorators/decorator-metadata.ts +10 -46
  13. package/src/decorators/entity.ts +30 -40
  14. package/src/decorators/relations.ts +30 -56
  15. package/src/index.ts +7 -7
  16. package/src/orm/entity-hydration.ts +72 -0
  17. package/src/orm/entity-meta.ts +13 -11
  18. package/src/orm/entity-metadata.ts +240 -238
  19. package/src/orm/entity-relation-cache.ts +39 -0
  20. package/src/orm/entity-relations.ts +207 -0
  21. package/src/orm/entity.ts +124 -410
  22. package/src/orm/execute.ts +4 -4
  23. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  24. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  25. package/src/orm/lazy-batch/has-many.ts +69 -0
  26. package/src/orm/lazy-batch/has-one.ts +68 -0
  27. package/src/orm/lazy-batch/shared.ts +125 -0
  28. package/src/orm/lazy-batch.ts +4 -492
  29. package/src/orm/relations/many-to-many.ts +2 -1
  30. package/src/query-builder/relation-cte-builder.ts +63 -0
  31. package/src/query-builder/relation-filter-utils.ts +159 -0
  32. package/src/query-builder/relation-include-strategies.ts +177 -0
  33. package/src/query-builder/relation-join-planner.ts +80 -0
  34. package/src/query-builder/relation-service.ts +119 -479
  35. package/src/query-builder/relation-types.ts +41 -10
  36. package/src/query-builder/select/projection-facet.ts +23 -23
  37. package/src/query-builder/select/select-operations.ts +145 -0
  38. package/src/query-builder/select.ts +329 -221
  39. package/src/schema/relation.ts +22 -18
  40. package/src/schema/table.ts +22 -9
  41. package/src/schema/types.ts +14 -12
@@ -1,5 +1,8 @@
1
- import { ExpressionNode } from '../core/ast/expression.js';
2
- import { JOIN_KINDS } from '../core/sql/sql.js';
1
+ import { ExpressionNode } from '../core/ast/expression.js';
2
+ import { JOIN_KINDS } from '../core/sql/sql.js';
3
+ import { TableDef } from '../schema/table.js';
4
+ import { BelongsToManyRelation, RelationDef } from '../schema/relation.js';
5
+ import { RelationTargetTable } from '../schema/types.js';
3
6
 
4
7
  /**
5
8
  * Join kinds allowed when including a relation using `.include(...)`.
@@ -11,11 +14,39 @@ export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS
11
14
  */
12
15
  export interface RelationIncludeOptions {
13
16
  columns?: readonly string[];
14
- aliasPrefix?: string;
15
- filter?: ExpressionNode;
16
- joinKind?: RelationIncludeJoinKind;
17
- pivot?: {
18
- columns?: string[];
19
- aliasPrefix?: string;
20
- };
21
- }
17
+ aliasPrefix?: string;
18
+ filter?: ExpressionNode;
19
+ joinKind?: RelationIncludeJoinKind;
20
+ pivot?: {
21
+ columns?: readonly string[];
22
+ aliasPrefix?: string;
23
+ };
24
+ }
25
+
26
+ type ColumnKeys<T> =
27
+ T extends { columns: infer Columns }
28
+ ? keyof Columns & string
29
+ : string;
30
+
31
+ type PivotColumnKeys<TPivot> = ColumnKeys<TPivot> extends never ? string : ColumnKeys<TPivot>;
32
+
33
+ export type RelationTargetColumns<TRel extends RelationDef> =
34
+ ColumnKeys<RelationTargetTable<TRel>>;
35
+
36
+ export type BelongsToManyPivotColumns<TRel extends RelationDef> =
37
+ TRel extends BelongsToManyRelation<TableDef, infer TPivot>
38
+ ? PivotColumnKeys<TPivot>
39
+ : never;
40
+
41
+ export type TypedRelationIncludeOptions<TRel extends RelationDef> =
42
+ TRel extends BelongsToManyRelation
43
+ ? Omit<RelationIncludeOptions, 'columns' | 'pivot'> & {
44
+ columns?: readonly RelationTargetColumns<TRel>[];
45
+ pivot?: {
46
+ columns?: readonly BelongsToManyPivotColumns<TRel>[];
47
+ aliasPrefix?: string;
48
+ };
49
+ }
50
+ : Omit<RelationIncludeOptions, 'columns' | 'pivot'> & {
51
+ columns?: readonly RelationTargetColumns<TRel>[];
52
+ };
@@ -22,12 +22,12 @@ export class SelectProjectionFacet {
22
22
  * @param columns - Columns to select
23
23
  * @returns Updated query context with selected columns
24
24
  */
25
- select(
26
- context: SelectQueryBuilderContext,
27
- columns: Record<string, ColumnSelectionValue>
28
- ): SelectQueryBuilderContext {
29
- return { ...context, state: this.columnSelector.select(context, columns).state };
30
- }
25
+ select(
26
+ context: SelectQueryBuilderContext,
27
+ columns: Record<string, ColumnSelectionValue>
28
+ ): SelectQueryBuilderContext {
29
+ return this.columnSelector.select(context, columns);
30
+ }
31
31
 
32
32
  /**
33
33
  * Selects raw column expressions
@@ -35,9 +35,9 @@ export class SelectProjectionFacet {
35
35
  * @param cols - Raw column expressions
36
36
  * @returns Updated query context with raw column selections
37
37
  */
38
- selectRaw(context: SelectQueryBuilderContext, cols: string[]): SelectQueryBuilderContext {
39
- return { ...context, state: this.columnSelector.selectRaw(context, cols).state };
40
- }
38
+ selectRaw(context: SelectQueryBuilderContext, cols: string[]): SelectQueryBuilderContext {
39
+ return this.columnSelector.selectRaw(context, cols);
40
+ }
41
41
 
42
42
  /**
43
43
  * Selects a subquery as a column
@@ -46,13 +46,13 @@ export class SelectProjectionFacet {
46
46
  * @param query - Subquery to select
47
47
  * @returns Updated query context with subquery selection
48
48
  */
49
- selectSubquery(
50
- context: SelectQueryBuilderContext,
51
- alias: string,
52
- query: SelectQueryNode
53
- ): SelectQueryBuilderContext {
54
- return { ...context, state: this.columnSelector.selectSubquery(context, alias, query).state };
55
- }
49
+ selectSubquery(
50
+ context: SelectQueryBuilderContext,
51
+ alias: string,
52
+ query: SelectQueryNode
53
+ ): SelectQueryBuilderContext {
54
+ return this.columnSelector.selectSubquery(context, alias, query);
55
+ }
56
56
 
57
57
  /**
58
58
  * Adds DISTINCT clause to the query
@@ -60,11 +60,11 @@ export class SelectProjectionFacet {
60
60
  * @param cols - Columns to make distinct
61
61
  * @returns Updated query context with DISTINCT clause
62
62
  */
63
- distinct(
64
- context: SelectQueryBuilderContext,
65
- cols: (ColumnDef | ColumnNode)[]
66
- ): SelectQueryBuilderContext {
67
- return { ...context, state: this.columnSelector.distinct(context, cols).state };
68
- }
69
- }
63
+ distinct(
64
+ context: SelectQueryBuilderContext,
65
+ cols: (ColumnDef | ColumnNode)[]
66
+ ): SelectQueryBuilderContext {
67
+ return this.columnSelector.distinct(context, cols);
68
+ }
69
+ }
70
70
 
@@ -0,0 +1,145 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { ColumnDef } from '../../schema/column-types.js';
3
+ import { OrderingTerm, SelectQueryNode } from '../../core/ast/query.js';
4
+ import { FunctionNode, ExpressionNode, exists, notExists } from '../../core/ast/expression.js';
5
+ import { derivedTable } from '../../core/ast/builders.js';
6
+ import { SelectQueryState } from '../select-query-state.js';
7
+ import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
8
+ import { SelectPredicateFacet } from './predicate-facet.js';
9
+ import { SelectRelationFacet } from './relation-facet.js';
10
+ import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
11
+ import { OrmSession } from '../../orm/orm-session.js';
12
+ import { EntityInstance } from '../../schema/types.js';
13
+ import type { SelectQueryBuilder } from '../select.js';
14
+
15
+ export type WhereHasOptions = {
16
+ correlate?: ExpressionNode;
17
+ };
18
+
19
+ export type RelationCallback = <TChildTable extends TableDef>(
20
+ qb: SelectQueryBuilder<unknown, TChildTable>
21
+ ) => SelectQueryBuilder<unknown, TChildTable>;
22
+
23
+ type ChildBuilderFactory = <R, TChild extends TableDef>(table: TChild) => SelectQueryBuilder<R, TChild>;
24
+
25
+ /**
26
+ * Builds a new query context with an ORDER BY clause applied.
27
+ */
28
+ export function applyOrderBy(
29
+ context: SelectQueryBuilderContext,
30
+ predicateFacet: SelectPredicateFacet,
31
+ term: ColumnDef | OrderingTerm,
32
+ directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string }
33
+ ): SelectQueryBuilderContext {
34
+ const options =
35
+ typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
36
+ const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
37
+ return predicateFacet.orderBy(context, term, dir, options.nulls, options.collation);
38
+ }
39
+
40
+ /**
41
+ * Runs the count query for the provided context and session.
42
+ */
43
+ export async function executeCount(
44
+ context: SelectQueryBuilderContext,
45
+ env: SelectQueryBuilderEnvironment,
46
+ session: OrmSession
47
+ ): Promise<number> {
48
+ const unpagedAst: SelectQueryNode = {
49
+ ...context.state.ast,
50
+ orderBy: undefined,
51
+ limit: undefined,
52
+ offset: undefined
53
+ };
54
+
55
+ const nextState = new SelectQueryState(env.table as TableDef, unpagedAst);
56
+ const nextContext: SelectQueryBuilderContext = {
57
+ ...context,
58
+ state: nextState
59
+ };
60
+
61
+ const subAst = nextContext.hydration.applyToAst(nextState.ast);
62
+ const countQuery: SelectQueryNode = {
63
+ type: 'SelectQuery',
64
+ from: derivedTable(subAst, '__metal_count'),
65
+ columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
66
+ joins: []
67
+ };
68
+
69
+ const execCtx = session.getExecutionContext();
70
+ const compiled = execCtx.dialect.compileSelect(countQuery);
71
+ const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
72
+ const value = results[0]?.values?.[0]?.[0];
73
+
74
+ if (typeof value === 'number') return value;
75
+ if (typeof value === 'bigint') return Number(value);
76
+ if (typeof value === 'string') return Number(value);
77
+ return value === null || value === undefined ? 0 : Number(value);
78
+ }
79
+
80
+ /**
81
+ * Executes paged queries using the provided builder helpers.
82
+ */
83
+ export async function executePagedQuery<T, TTable extends TableDef>(
84
+ builder: SelectQueryBuilder<T, TTable>,
85
+ session: OrmSession,
86
+ options: { page: number; pageSize: number },
87
+ countCallback: (session: OrmSession) => Promise<number>
88
+ ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
89
+ const { page, pageSize } = options;
90
+
91
+ if (!Number.isInteger(page) || page < 1) {
92
+ throw new Error('executePaged: page must be an integer >= 1');
93
+ }
94
+ if (!Number.isInteger(pageSize) || pageSize < 1) {
95
+ throw new Error('executePaged: pageSize must be an integer >= 1');
96
+ }
97
+
98
+ const offset = (page - 1) * pageSize;
99
+
100
+ const [items, totalItems] = await Promise.all([
101
+ builder.limit(pageSize).offset(offset).execute(session),
102
+ countCallback(session)
103
+ ]);
104
+
105
+ return { items, totalItems };
106
+ }
107
+
108
+ /**
109
+ * Builds an EXISTS or NOT EXISTS predicate for a related table.
110
+ */
111
+ export function buildWhereHasPredicate<TTable extends TableDef>(
112
+ env: SelectQueryBuilderEnvironment,
113
+ context: SelectQueryBuilderContext,
114
+ relationFacet: SelectRelationFacet,
115
+ createChildBuilder: ChildBuilderFactory,
116
+ relationName: keyof TTable['relations'] & string,
117
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
118
+ maybeOptions?: WhereHasOptions,
119
+ negate = false
120
+ ): ExpressionNode {
121
+ const relation = env.table.relations[relationName as string];
122
+ if (!relation) {
123
+ throw new Error(`Relation '${relationName}' not found on table '${env.table.name}'`);
124
+ }
125
+
126
+ const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined;
127
+ const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as
128
+ | WhereHasOptions
129
+ | undefined;
130
+
131
+ let subQb = createChildBuilder<unknown, TableDef>(relation.target);
132
+ if (callback) {
133
+ subQb = callback(subQb);
134
+ }
135
+
136
+ const subAst = subQb.getAST();
137
+ const finalSubAst = relationFacet.applyRelationCorrelation(
138
+ context,
139
+ relationName,
140
+ subAst,
141
+ options?.correlate
142
+ );
143
+
144
+ return negate ? notExists(finalSubAst) : exists(finalSubAst);
145
+ }