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.
- package/README.md +34 -31
- package/dist/index.cjs +1583 -901
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +400 -129
- package/dist/index.d.ts +400 -129
- package/dist/index.js +1575 -901
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ddl/schema-generator.ts +44 -1
- package/src/decorators/bootstrap.ts +183 -146
- package/src/decorators/column-decorator.ts +8 -49
- package/src/decorators/decorator-metadata.ts +10 -46
- package/src/decorators/entity.ts +30 -40
- package/src/decorators/relations.ts +30 -56
- package/src/index.ts +7 -7
- package/src/orm/entity-hydration.ts +72 -0
- package/src/orm/entity-meta.ts +13 -11
- package/src/orm/entity-metadata.ts +240 -238
- package/src/orm/entity-relation-cache.ts +39 -0
- package/src/orm/entity-relations.ts +207 -0
- package/src/orm/entity.ts +124 -410
- package/src/orm/execute.ts +4 -4
- package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
- package/src/orm/lazy-batch/belongs-to.ts +108 -0
- package/src/orm/lazy-batch/has-many.ts +69 -0
- package/src/orm/lazy-batch/has-one.ts +68 -0
- package/src/orm/lazy-batch/shared.ts +125 -0
- package/src/orm/lazy-batch.ts +4 -492
- package/src/orm/relations/many-to-many.ts +2 -1
- package/src/query-builder/relation-cte-builder.ts +63 -0
- package/src/query-builder/relation-filter-utils.ts +159 -0
- package/src/query-builder/relation-include-strategies.ts +177 -0
- package/src/query-builder/relation-join-planner.ts +80 -0
- package/src/query-builder/relation-service.ts +119 -479
- package/src/query-builder/relation-types.ts +41 -10
- package/src/query-builder/select/projection-facet.ts +23 -23
- package/src/query-builder/select/select-operations.ts +145 -0
- package/src/query-builder/select.ts +329 -221
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|