metal-orm 1.0.58 → 1.0.59

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 (40) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1463 -1003
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +148 -129
  5. package/dist/index.d.ts +148 -129
  6. package/dist/index.js +1459 -1003
  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/orm/entity-hydration.ts +72 -0
  16. package/src/orm/entity-meta.ts +13 -11
  17. package/src/orm/entity-metadata.ts +240 -238
  18. package/src/orm/entity-relation-cache.ts +39 -0
  19. package/src/orm/entity-relations.ts +207 -0
  20. package/src/orm/entity.ts +124 -410
  21. package/src/orm/execute.ts +4 -4
  22. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  23. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  24. package/src/orm/lazy-batch/has-many.ts +69 -0
  25. package/src/orm/lazy-batch/has-one.ts +68 -0
  26. package/src/orm/lazy-batch/shared.ts +125 -0
  27. package/src/orm/lazy-batch.ts +4 -492
  28. package/src/orm/relations/many-to-many.ts +2 -1
  29. package/src/query-builder/relation-cte-builder.ts +63 -0
  30. package/src/query-builder/relation-filter-utils.ts +159 -0
  31. package/src/query-builder/relation-include-strategies.ts +177 -0
  32. package/src/query-builder/relation-join-planner.ts +80 -0
  33. package/src/query-builder/relation-service.ts +119 -479
  34. package/src/query-builder/relation-types.ts +41 -10
  35. package/src/query-builder/select/projection-facet.ts +23 -23
  36. package/src/query-builder/select/select-operations.ts +145 -0
  37. package/src/query-builder/select.ts +351 -422
  38. package/src/schema/relation.ts +22 -18
  39. package/src/schema/table.ts +22 -9
  40. package/src/schema/types.ts +14 -12
@@ -0,0 +1,159 @@
1
+ import { ExpressionNode, OperandNode, isOperandNode } from '../core/ast/expression.js';
2
+ import { OrderingTerm } from '../core/ast/query.js';
3
+
4
+ type FilterTableCollector = {
5
+ tables: Set<string>;
6
+ hasSubquery: boolean;
7
+ };
8
+
9
+ export type SplitFilterExpressionsResult = {
10
+ selfFilters: ExpressionNode[];
11
+ crossFilters: ExpressionNode[];
12
+ };
13
+
14
+ export const splitFilterExpressions = (
15
+ filter: ExpressionNode | undefined,
16
+ allowedTables: Set<string>
17
+ ): SplitFilterExpressionsResult => {
18
+ const terms = flattenAnd(filter);
19
+ const selfFilters: ExpressionNode[] = [];
20
+ const crossFilters: ExpressionNode[] = [];
21
+
22
+ for (const term of terms) {
23
+ if (isExpressionSelfContained(term, allowedTables)) {
24
+ selfFilters.push(term);
25
+ } else {
26
+ crossFilters.push(term);
27
+ }
28
+ }
29
+
30
+ return { selfFilters, crossFilters };
31
+ };
32
+
33
+ const flattenAnd = (node?: ExpressionNode): ExpressionNode[] => {
34
+ if (!node) return [];
35
+ if (node.type === 'LogicalExpression' && node.operator === 'AND') {
36
+ return node.operands.flatMap(operand => flattenAnd(operand));
37
+ }
38
+ return [node];
39
+ };
40
+
41
+ const isExpressionSelfContained = (expr: ExpressionNode, allowedTables: Set<string>): boolean => {
42
+ const collector = collectReferencedTables(expr);
43
+ if (collector.hasSubquery) return false;
44
+ if (collector.tables.size === 0) return true;
45
+ for (const table of collector.tables) {
46
+ if (!allowedTables.has(table)) {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ };
52
+
53
+ const collectReferencedTables = (expr: ExpressionNode): FilterTableCollector => {
54
+ const collector: FilterTableCollector = {
55
+ tables: new Set(),
56
+ hasSubquery: false
57
+ };
58
+ collectFromExpression(expr, collector);
59
+ return collector;
60
+ };
61
+
62
+ const collectFromExpression = (expr: ExpressionNode, collector: FilterTableCollector): void => {
63
+ switch (expr.type) {
64
+ case 'BinaryExpression':
65
+ collectFromOperand(expr.left, collector);
66
+ collectFromOperand(expr.right, collector);
67
+ break;
68
+ case 'LogicalExpression':
69
+ expr.operands.forEach(operand => collectFromExpression(operand, collector));
70
+ break;
71
+ case 'NullExpression':
72
+ collectFromOperand(expr.left, collector);
73
+ break;
74
+ case 'InExpression':
75
+ collectFromOperand(expr.left, collector);
76
+ if (Array.isArray(expr.right)) {
77
+ expr.right.forEach(value => collectFromOperand(value, collector));
78
+ } else {
79
+ collector.hasSubquery = true;
80
+ }
81
+ break;
82
+ case 'ExistsExpression':
83
+ collector.hasSubquery = true;
84
+ break;
85
+ case 'BetweenExpression':
86
+ collectFromOperand(expr.left, collector);
87
+ collectFromOperand(expr.lower, collector);
88
+ collectFromOperand(expr.upper, collector);
89
+ break;
90
+ case 'ArithmeticExpression':
91
+ case 'BitwiseExpression':
92
+ collectFromOperand(expr.left, collector);
93
+ collectFromOperand(expr.right, collector);
94
+ break;
95
+ default:
96
+ break;
97
+ }
98
+ };
99
+
100
+ const collectFromOperand = (node: OperandNode, collector: FilterTableCollector): void => {
101
+ switch (node.type) {
102
+ case 'Column':
103
+ collector.tables.add(node.table);
104
+ break;
105
+ case 'Function':
106
+ node.args.forEach(arg => collectFromOperand(arg, collector));
107
+ if (node.separator) {
108
+ collectFromOperand(node.separator, collector);
109
+ }
110
+ if (node.orderBy) {
111
+ node.orderBy.forEach(order => collectFromOrderingTerm(order.term, collector));
112
+ }
113
+ break;
114
+ case 'JsonPath':
115
+ collectFromOperand(node.column, collector);
116
+ break;
117
+ case 'ScalarSubquery':
118
+ collector.hasSubquery = true;
119
+ break;
120
+ case 'CaseExpression':
121
+ node.conditions.forEach(({ when, then }) => {
122
+ collectFromExpression(when, collector);
123
+ collectFromOperand(then, collector);
124
+ });
125
+ if (node.else) {
126
+ collectFromOperand(node.else, collector);
127
+ }
128
+ break;
129
+ case 'Cast':
130
+ collectFromOperand(node.expression, collector);
131
+ break;
132
+ case 'WindowFunction':
133
+ node.args.forEach(arg => collectFromOperand(arg, collector));
134
+ node.partitionBy?.forEach(part => collectFromOperand(part, collector));
135
+ node.orderBy?.forEach(order => collectFromOrderingTerm(order.term, collector));
136
+ break;
137
+ case 'Collate':
138
+ collectFromOperand(node.expression, collector);
139
+ break;
140
+ case 'ArithmeticExpression':
141
+ case 'BitwiseExpression':
142
+ collectFromOperand(node.left, collector);
143
+ collectFromOperand(node.right, collector);
144
+ break;
145
+ case 'Literal':
146
+ case 'AliasRef':
147
+ break;
148
+ default:
149
+ break;
150
+ }
151
+ };
152
+
153
+ const collectFromOrderingTerm = (term: OrderingTerm, collector: FilterTableCollector): void => {
154
+ if (isOperandNode(term)) {
155
+ collectFromOperand(term, collector);
156
+ return;
157
+ }
158
+ collectFromExpression(term, collector);
159
+ };
@@ -0,0 +1,177 @@
1
+ import { TableDef } from '../schema/table.js';
2
+ import { ColumnDef } from '../schema/column-types.js';
3
+ import {
4
+ RelationDef,
5
+ RelationKinds,
6
+ BelongsToManyRelation,
7
+ HasManyRelation,
8
+ HasOneRelation,
9
+ BelongsToRelation
10
+ } from '../schema/relation.js';
11
+ import { ColumnNode } from '../core/ast/expression.js';
12
+ import { SelectQueryState } from './select-query-state.js';
13
+ import { HydrationManager } from './hydration-manager.js';
14
+ import type { RelationResult } from './relation-projection-helper.js';
15
+ import { RelationIncludeOptions } from './relation-types.js';
16
+ import { makeRelationAlias } from './relation-alias.js';
17
+ import { buildDefaultPivotColumns } from './relation-utils.js';
18
+ import { findPrimaryKey } from './hydration-planner.js';
19
+
20
+ type RelationWithForeignKey =
21
+ | HasManyRelation
22
+ | HasOneRelation
23
+ | BelongsToRelation;
24
+
25
+ type IncludeStrategyContext = {
26
+ rootTable: TableDef;
27
+ state: SelectQueryState;
28
+ hydration: HydrationManager;
29
+ relation: RelationDef;
30
+ relationName: string;
31
+ aliasPrefix: string;
32
+ options?: RelationIncludeOptions;
33
+ selectColumns: (
34
+ state: SelectQueryState,
35
+ hydration: HydrationManager,
36
+ columns: Record<string, ColumnDef>
37
+ ) => RelationResult;
38
+ };
39
+
40
+ type IncludeStrategy = (context: IncludeStrategyContext) => RelationResult;
41
+
42
+ const buildTypedSelection = (
43
+ columns: Record<string, ColumnDef>,
44
+ prefix: string,
45
+ keys: string[],
46
+ missingMsg: (col: string) => string
47
+ ): Record<string, ColumnDef> => {
48
+ return keys.reduce((acc, key) => {
49
+ const def = columns[key];
50
+ if (!def) {
51
+ throw new Error(missingMsg(key));
52
+ }
53
+ acc[makeRelationAlias(prefix, key)] = def;
54
+ return acc;
55
+ }, {} as Record<string, ColumnDef>);
56
+ };
57
+
58
+ const resolveTargetColumns = (relation: RelationDef, options?: RelationIncludeOptions): string[] => {
59
+ const requestedColumns = options?.columns?.length
60
+ ? [...options.columns]
61
+ : Object.keys(relation.target.columns);
62
+ const targetPrimaryKey = findPrimaryKey(relation.target);
63
+ if (!requestedColumns.includes(targetPrimaryKey)) {
64
+ requestedColumns.push(targetPrimaryKey);
65
+ }
66
+ return requestedColumns;
67
+ };
68
+
69
+ const ensureRootForeignKeySelected = (
70
+ context: IncludeStrategyContext,
71
+ relation: RelationWithForeignKey
72
+ ): RelationResult => {
73
+ const fkColumn = context.rootTable.columns[relation.foreignKey];
74
+ if (!fkColumn) {
75
+ return { state: context.state, hydration: context.hydration };
76
+ }
77
+
78
+ const hasForeignKeySelected = context.state.ast.columns.some(col => {
79
+ if ((col as ColumnNode).type !== 'Column') return false;
80
+ const node = col as ColumnNode;
81
+ const alias = node.alias ?? node.name;
82
+ return alias === relation.foreignKey;
83
+ });
84
+
85
+ if (hasForeignKeySelected) {
86
+ return { state: context.state, hydration: context.hydration };
87
+ }
88
+
89
+ return context.selectColumns(context.state, context.hydration, {
90
+ [relation.foreignKey]: fkColumn
91
+ });
92
+ };
93
+
94
+ const standardIncludeStrategy: IncludeStrategy = context => {
95
+ const relation = context.relation as RelationWithForeignKey;
96
+ let { state, hydration } = context;
97
+
98
+ const fkSelectionResult = ensureRootForeignKeySelected(context, relation);
99
+ state = fkSelectionResult.state;
100
+ hydration = fkSelectionResult.hydration;
101
+
102
+ const targetColumns = resolveTargetColumns(relation, context.options);
103
+ const targetSelection = buildTypedSelection(
104
+ relation.target.columns as Record<string, ColumnDef>,
105
+ context.aliasPrefix,
106
+ targetColumns,
107
+ key => `Column '${key}' not found on relation '${context.relationName}'`
108
+ );
109
+
110
+ const relationSelectionResult = context.selectColumns(state, hydration, targetSelection);
111
+ state = relationSelectionResult.state;
112
+ hydration = relationSelectionResult.hydration;
113
+
114
+ hydration = hydration.onRelationIncluded(
115
+ state,
116
+ relation,
117
+ context.relationName,
118
+ context.aliasPrefix,
119
+ targetColumns
120
+ );
121
+
122
+ return { state, hydration };
123
+ };
124
+
125
+ const belongsToManyStrategy: IncludeStrategy = context => {
126
+ const relation = context.relation as BelongsToManyRelation;
127
+ let { state, hydration } = context;
128
+
129
+ const targetColumns = resolveTargetColumns(relation, context.options);
130
+ const targetSelection = buildTypedSelection(
131
+ relation.target.columns as Record<string, ColumnDef>,
132
+ context.aliasPrefix,
133
+ targetColumns,
134
+ key => `Column '${key}' not found on relation '${context.relationName}'`
135
+ );
136
+
137
+ const pivotAliasPrefix = context.options?.pivot?.aliasPrefix ?? `${context.aliasPrefix}_pivot`;
138
+ const pivotPk = relation.pivotPrimaryKey || findPrimaryKey(relation.pivotTable);
139
+ const defaultPivotColumns = relation.defaultPivotColumns ?? buildDefaultPivotColumns(relation, pivotPk);
140
+ const pivotColumns = context.options?.pivot?.columns
141
+ ? [...context.options.pivot.columns]
142
+ : [...defaultPivotColumns];
143
+
144
+ const pivotSelection = buildTypedSelection(
145
+ relation.pivotTable.columns as Record<string, ColumnDef>,
146
+ pivotAliasPrefix,
147
+ pivotColumns,
148
+ key => `Column '${key}' not found on pivot table '${relation.pivotTable.name}'`
149
+ );
150
+
151
+ const combinedSelection = {
152
+ ...targetSelection,
153
+ ...pivotSelection
154
+ };
155
+
156
+ const relationSelectionResult = context.selectColumns(state, hydration, combinedSelection);
157
+ state = relationSelectionResult.state;
158
+ hydration = relationSelectionResult.hydration;
159
+
160
+ hydration = hydration.onRelationIncluded(
161
+ state,
162
+ relation,
163
+ context.relationName,
164
+ context.aliasPrefix,
165
+ targetColumns,
166
+ { aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
167
+ );
168
+
169
+ return { state, hydration };
170
+ };
171
+
172
+ export const relationIncludeStrategies: Record<RelationDef['type'], IncludeStrategy> = {
173
+ [RelationKinds.HasMany]: standardIncludeStrategy,
174
+ [RelationKinds.HasOne]: standardIncludeStrategy,
175
+ [RelationKinds.BelongsTo]: standardIncludeStrategy,
176
+ [RelationKinds.BelongsToMany]: belongsToManyStrategy
177
+ };
@@ -0,0 +1,80 @@
1
+ import { TableDef } from '../schema/table.js';
2
+ import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
3
+ import { SelectQueryState } from './select-query-state.js';
4
+ import { QueryAstService } from './query-ast-service.js';
5
+ import { TableSourceNode } from '../core/ast/query.js';
6
+ import { ExpressionNode } from '../core/ast/expression.js';
7
+ import { JoinKind } from '../core/sql/sql.js';
8
+ import { buildRelationJoinCondition, buildBelongsToManyJoins } from './relation-conditions.js';
9
+ import { createJoinNode } from '../core/ast/join-node.js';
10
+
11
+ export class RelationJoinPlanner {
12
+ constructor(
13
+ private readonly table: TableDef,
14
+ private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
15
+ ) {}
16
+
17
+ withJoin(
18
+ state: SelectQueryState,
19
+ relationName: string,
20
+ relation: RelationDef,
21
+ joinKind: JoinKind,
22
+ extraCondition?: ExpressionNode,
23
+ tableSource?: TableSourceNode
24
+ ): SelectQueryState {
25
+ const rootAlias = state.ast.from.type === 'Table' ? state.ast.from.alias : undefined;
26
+ if (relation.type === RelationKinds.BelongsToMany) {
27
+ const targetTableSource: TableSourceNode = tableSource ?? {
28
+ type: 'Table',
29
+ name: relation.target.name,
30
+ schema: relation.target.schema
31
+ };
32
+ const targetName = this.resolveTargetTableName(targetTableSource, relation);
33
+ const joins = buildBelongsToManyJoins(
34
+ this.table,
35
+ relationName,
36
+ relation as BelongsToManyRelation,
37
+ joinKind,
38
+ extraCondition,
39
+ rootAlias,
40
+ targetTableSource,
41
+ targetName
42
+ );
43
+ return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
44
+ }
45
+
46
+ const targetTable: TableSourceNode = tableSource ?? {
47
+ type: 'Table',
48
+ name: relation.target.name,
49
+ schema: relation.target.schema
50
+ };
51
+ const targetName = this.resolveTargetTableName(targetTable, relation);
52
+ const condition = buildRelationJoinCondition(
53
+ this.table,
54
+ relation,
55
+ extraCondition,
56
+ rootAlias,
57
+ targetName
58
+ );
59
+ const joinNode = createJoinNode(joinKind, targetTable, condition, relationName);
60
+
61
+ return this.astService(state).withJoin(joinNode);
62
+ }
63
+
64
+ private astService(state: SelectQueryState): QueryAstService {
65
+ return this.createQueryAstService(this.table, state);
66
+ }
67
+
68
+ private resolveTargetTableName(target: TableSourceNode, relation: RelationDef): string {
69
+ if (target.type === 'Table') {
70
+ return target.alias ?? target.name;
71
+ }
72
+ if (target.type === 'DerivedTable') {
73
+ return target.alias;
74
+ }
75
+ if (target.type === 'FunctionTable') {
76
+ return target.alias ?? relation.target.name;
77
+ }
78
+ return relation.target.name;
79
+ }
80
+ }