metal-orm 1.0.4 → 1.0.6
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 +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +77 -3
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +630 -592
- package/src/ast/query.ts +110 -49
- package/src/builder/delete-query-state.ts +42 -0
- package/src/builder/delete.ts +57 -0
- package/src/builder/hydration-manager.ts +3 -2
- package/src/builder/hydration-planner.ts +163 -107
- package/src/builder/insert-query-state.ts +62 -0
- package/src/builder/insert.ts +59 -0
- package/src/builder/operations/relation-manager.ts +1 -23
- package/src/builder/relation-conditions.ts +45 -1
- package/src/builder/relation-service.ts +81 -18
- package/src/builder/relation-types.ts +15 -0
- package/src/builder/relation-utils.ts +12 -0
- package/src/builder/select.ts +427 -394
- package/src/builder/update-query-state.ts +59 -0
- package/src/builder/update.ts +61 -0
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/dialect/abstract.ts +107 -47
- package/src/dialect/mssql/index.ts +31 -6
- package/src/dialect/mysql/index.ts +31 -6
- package/src/dialect/postgres/index.ts +45 -6
- package/src/dialect/sqlite/index.ts +45 -6
- package/src/index.ts +22 -11
- package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +6 -2
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +100 -38
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +105 -40
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/belongs-to-many.test.ts +57 -0
- package/tests/dml.test.ts +206 -0
- package/tests/orm-runtime.test.ts +254 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnDef } from '../schema/column';
|
|
3
|
+
import { ColumnNode } from '../ast/expression';
|
|
4
|
+
import { CompiledQuery, InsertCompiler } from '../dialect/abstract';
|
|
5
|
+
import { InsertQueryNode } from '../ast/query';
|
|
6
|
+
import { InsertQueryState } from './insert-query-state';
|
|
7
|
+
|
|
8
|
+
const buildColumnNode = (table: TableDef, column: ColumnDef | ColumnNode): ColumnNode => {
|
|
9
|
+
if ((column as ColumnNode).type === 'Column') {
|
|
10
|
+
return column as ColumnNode;
|
|
11
|
+
}
|
|
12
|
+
const def = column as ColumnDef;
|
|
13
|
+
return {
|
|
14
|
+
type: 'Column',
|
|
15
|
+
table: def.table || table.name,
|
|
16
|
+
name: def.name
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builder for INSERT queries
|
|
22
|
+
*/
|
|
23
|
+
export class InsertQueryBuilder<T> {
|
|
24
|
+
private readonly table: TableDef;
|
|
25
|
+
private readonly state: InsertQueryState;
|
|
26
|
+
|
|
27
|
+
constructor(table: TableDef, state?: InsertQueryState) {
|
|
28
|
+
this.table = table;
|
|
29
|
+
this.state = state ?? new InsertQueryState(table);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private clone(state: InsertQueryState): InsertQueryBuilder<T> {
|
|
33
|
+
return new InsertQueryBuilder(this.table, state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
values(rowOrRows: Record<string, unknown> | Record<string, unknown>[]): InsertQueryBuilder<T> {
|
|
37
|
+
const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows];
|
|
38
|
+
if (!rows.length) return this;
|
|
39
|
+
return this.clone(this.state.withValues(rows));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
returning(...columns: (ColumnDef | ColumnNode)[]): InsertQueryBuilder<T> {
|
|
43
|
+
if (!columns.length) return this;
|
|
44
|
+
const nodes = columns.map(column => buildColumnNode(this.table, column));
|
|
45
|
+
return this.clone(this.state.withReturning(nodes));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
compile(compiler: InsertCompiler): CompiledQuery {
|
|
49
|
+
return compiler.compileInsert(this.state.ast);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toSql(compiler: InsertCompiler): string {
|
|
53
|
+
return this.compile(compiler).sql;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getAST(): InsertQueryNode {
|
|
57
|
+
return this.state.ast;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -2,29 +2,7 @@ import { ExpressionNode } from '../../ast/expression';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
4
4
|
import { JoinKind } from '../../constants/sql';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Options for including relations in queries
|
|
9
|
-
*/
|
|
10
|
-
export interface RelationIncludeOptions {
|
|
11
|
-
/**
|
|
12
|
-
* Columns to include from the related table
|
|
13
|
-
*/
|
|
14
|
-
columns?: string[];
|
|
15
|
-
/**
|
|
16
|
-
* Alias prefix for the relation columns
|
|
17
|
-
*/
|
|
18
|
-
aliasPrefix?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Filter expression to apply to the relation
|
|
21
|
-
*/
|
|
22
|
-
filter?: ExpressionNode;
|
|
23
|
-
/**
|
|
24
|
-
* Type of join to use for the relation
|
|
25
|
-
*/
|
|
26
|
-
joinKind?: RelationIncludeJoinKind;
|
|
27
|
-
}
|
|
5
|
+
import { RelationIncludeOptions } from '../relation-types';
|
|
28
6
|
|
|
29
7
|
/**
|
|
30
8
|
* Manages relation operations (joins, includes, etc.) for query building
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table';
|
|
2
|
-
import { RelationDef, RelationKinds } from '../schema/relation';
|
|
2
|
+
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
|
|
3
3
|
import { ExpressionNode, eq, and } from '../ast/expression';
|
|
4
4
|
import { findPrimaryKey } from './hydration-planner';
|
|
5
|
+
import { JoinNode } from '../ast/join';
|
|
6
|
+
import { JoinKind } from '../constants/sql';
|
|
7
|
+
import { createJoinNode } from '../utils/join-node';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Utility function to handle unreachable code paths
|
|
@@ -36,11 +39,52 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
|
|
|
36
39
|
{ type: 'Column', table: relation.target.name, name: localKey },
|
|
37
40
|
{ type: 'Column', table: root.name, name: relation.foreignKey }
|
|
38
41
|
);
|
|
42
|
+
case RelationKinds.BelongsToMany:
|
|
43
|
+
throw new Error('BelongsToMany relations do not support the standard join condition builder');
|
|
39
44
|
default:
|
|
40
45
|
return assertNever(relation);
|
|
41
46
|
}
|
|
42
47
|
};
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Builds the join nodes required to include a BelongsToMany relation.
|
|
51
|
+
*/
|
|
52
|
+
export const buildBelongsToManyJoins = (
|
|
53
|
+
root: TableDef,
|
|
54
|
+
relationName: string,
|
|
55
|
+
relation: BelongsToManyRelation,
|
|
56
|
+
joinKind: JoinKind,
|
|
57
|
+
extra?: ExpressionNode
|
|
58
|
+
): JoinNode[] => {
|
|
59
|
+
const rootKey = relation.localKey || findPrimaryKey(root);
|
|
60
|
+
const targetKey = relation.targetKey || findPrimaryKey(relation.target);
|
|
61
|
+
|
|
62
|
+
const pivotCondition = eq(
|
|
63
|
+
{ type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
|
|
64
|
+
{ type: 'Column', table: root.name, name: rootKey }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const pivotJoin = createJoinNode(joinKind, relation.pivotTable.name, pivotCondition);
|
|
68
|
+
|
|
69
|
+
let targetCondition: ExpressionNode = eq(
|
|
70
|
+
{ type: 'Column', table: relation.target.name, name: targetKey },
|
|
71
|
+
{ type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (extra) {
|
|
75
|
+
targetCondition = and(targetCondition, extra);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const targetJoin = createJoinNode(
|
|
79
|
+
joinKind,
|
|
80
|
+
relation.target.name,
|
|
81
|
+
targetCondition,
|
|
82
|
+
relationName
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return [pivotJoin, targetJoin];
|
|
86
|
+
};
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Builds a relation join condition with optional extra conditions
|
|
46
90
|
* @param root - Root table definition
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table';
|
|
2
2
|
import { ColumnDef } from '../schema/column';
|
|
3
|
-
import { RelationDef } from '../schema/relation';
|
|
3
|
+
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
|
|
4
4
|
import { SelectQueryNode } from '../ast/query';
|
|
5
5
|
import {
|
|
6
6
|
ColumnNode,
|
|
@@ -13,11 +13,16 @@ import { QueryAstService } from './query-ast-service';
|
|
|
13
13
|
import { findPrimaryKey } from './hydration-planner';
|
|
14
14
|
import { RelationProjectionHelper } from './relation-projection-helper';
|
|
15
15
|
import type { RelationResult } from './relation-projection-helper';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildRelationJoinCondition,
|
|
18
|
+
buildRelationCorrelation,
|
|
19
|
+
buildBelongsToManyJoins
|
|
20
|
+
} from './relation-conditions';
|
|
17
21
|
import { JoinKind, JOIN_KINDS } from '../constants/sql';
|
|
18
|
-
import {
|
|
22
|
+
import { RelationIncludeOptions } from './relation-types';
|
|
19
23
|
import { createJoinNode } from '../utils/join-node';
|
|
20
24
|
import { makeRelationAlias } from '../utils/relation-alias';
|
|
25
|
+
import { buildDefaultPivotColumns } from './relation-utils';
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Service for handling relation operations (joins, includes, etc.)
|
|
@@ -82,10 +87,7 @@ export class RelationService {
|
|
|
82
87
|
* @param options - Options for relation inclusion
|
|
83
88
|
* @returns Relation result with updated state and hydration
|
|
84
89
|
*/
|
|
85
|
-
include(
|
|
86
|
-
relationName: string,
|
|
87
|
-
options?: { columns?: string[]; aliasPrefix?: string; filter?: ExpressionNode; joinKind?: RelationIncludeJoinKind }
|
|
88
|
-
): RelationResult {
|
|
90
|
+
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
89
91
|
let state = this.state;
|
|
90
92
|
let hydration = this.hydration;
|
|
91
93
|
|
|
@@ -106,16 +108,66 @@ export class RelationService {
|
|
|
106
108
|
? options.columns
|
|
107
109
|
: Object.keys(relation.target.columns);
|
|
108
110
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return acc
|
|
116
|
-
|
|
111
|
+
const buildTypedSelection = (
|
|
112
|
+
columns: Record<string, ColumnDef>,
|
|
113
|
+
prefix: string,
|
|
114
|
+
keys: string[],
|
|
115
|
+
missingMsg: (col: string) => string
|
|
116
|
+
) : Record<string, ColumnDef> => {
|
|
117
|
+
return keys.reduce((acc, key) => {
|
|
118
|
+
const def = columns[key];
|
|
119
|
+
if (!def) {
|
|
120
|
+
throw new Error(missingMsg(key));
|
|
121
|
+
}
|
|
122
|
+
acc[makeRelationAlias(prefix, key)] = def;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {} as Record<string, ColumnDef>);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const targetSelection = buildTypedSelection(
|
|
128
|
+
relation.target.columns as Record<string, ColumnDef>,
|
|
129
|
+
aliasPrefix,
|
|
130
|
+
targetColumns,
|
|
131
|
+
key => `Column '${key}' not found on relation '${relationName}'`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (relation.type !== RelationKinds.BelongsToMany) {
|
|
135
|
+
const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
|
|
136
|
+
state = relationSelectionResult.state;
|
|
137
|
+
hydration = relationSelectionResult.hydration;
|
|
117
138
|
|
|
118
|
-
|
|
139
|
+
hydration = hydration.onRelationIncluded(
|
|
140
|
+
state,
|
|
141
|
+
relation,
|
|
142
|
+
relationName,
|
|
143
|
+
aliasPrefix,
|
|
144
|
+
targetColumns
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return { state, hydration };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const many = relation as BelongsToManyRelation;
|
|
151
|
+
const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
152
|
+
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
153
|
+
const pivotColumns =
|
|
154
|
+
options?.pivot?.columns ??
|
|
155
|
+
many.defaultPivotColumns ??
|
|
156
|
+
buildDefaultPivotColumns(many, pivotPk);
|
|
157
|
+
|
|
158
|
+
const pivotSelection = buildTypedSelection(
|
|
159
|
+
many.pivotTable.columns as Record<string, ColumnDef>,
|
|
160
|
+
pivotAliasPrefix,
|
|
161
|
+
pivotColumns,
|
|
162
|
+
key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const combinedSelection = {
|
|
166
|
+
...targetSelection,
|
|
167
|
+
...pivotSelection
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
|
|
119
171
|
state = relationSelectionResult.state;
|
|
120
172
|
hydration = relationSelectionResult.hydration;
|
|
121
173
|
|
|
@@ -124,7 +176,8 @@ export class RelationService {
|
|
|
124
176
|
relation,
|
|
125
177
|
relationName,
|
|
126
178
|
aliasPrefix,
|
|
127
|
-
targetColumns
|
|
179
|
+
targetColumns,
|
|
180
|
+
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
128
181
|
);
|
|
129
182
|
|
|
130
183
|
return { state, hydration };
|
|
@@ -167,8 +220,18 @@ export class RelationService {
|
|
|
167
220
|
extraCondition?: ExpressionNode
|
|
168
221
|
): SelectQueryState {
|
|
169
222
|
const relation = this.getRelation(relationName);
|
|
170
|
-
|
|
223
|
+
if (relation.type === RelationKinds.BelongsToMany) {
|
|
224
|
+
const joins = buildBelongsToManyJoins(
|
|
225
|
+
this.table,
|
|
226
|
+
relationName,
|
|
227
|
+
relation as BelongsToManyRelation,
|
|
228
|
+
joinKind,
|
|
229
|
+
extraCondition
|
|
230
|
+
);
|
|
231
|
+
return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
|
|
232
|
+
}
|
|
171
233
|
|
|
234
|
+
const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
|
|
172
235
|
const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
|
|
173
236
|
|
|
174
237
|
return this.astService(state).withJoin(joinNode);
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
import { ExpressionNode } from '../ast/expression';
|
|
1
2
|
import { JOIN_KINDS } from '../constants/sql';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Join kinds allowed when including a relation using `.include(...)`.
|
|
5
6
|
*/
|
|
6
7
|
export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for including a relation in a query
|
|
11
|
+
*/
|
|
12
|
+
export interface RelationIncludeOptions {
|
|
13
|
+
columns?: string[];
|
|
14
|
+
aliasPrefix?: string;
|
|
15
|
+
filter?: ExpressionNode;
|
|
16
|
+
joinKind?: RelationIncludeJoinKind;
|
|
17
|
+
pivot?: {
|
|
18
|
+
columns?: string[];
|
|
19
|
+
aliasPrefix?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BelongsToManyRelation } from '../schema/relation';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a default set of pivot columns, excluding keys used for joins.
|
|
5
|
+
*/
|
|
6
|
+
export const buildDefaultPivotColumns = (
|
|
7
|
+
rel: BelongsToManyRelation,
|
|
8
|
+
pivotPk: string
|
|
9
|
+
): string[] => {
|
|
10
|
+
const excluded = new Set([pivotPk, rel.pivotForeignKeyToRoot, rel.pivotForeignKeyToTarget]);
|
|
11
|
+
return Object.keys(rel.pivotTable.columns).filter(col => !excluded.has(col));
|
|
12
|
+
};
|