metal-orm 1.0.2 → 1.0.4

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 (37) hide show
  1. package/README.md +20 -0
  2. package/package.json +1 -1
  3. package/src/ast/expression.ts +433 -175
  4. package/src/ast/join.ts +8 -1
  5. package/src/ast/query.ts +64 -9
  6. package/src/builder/hydration-manager.ts +42 -11
  7. package/src/builder/hydration-planner.ts +80 -31
  8. package/src/builder/operations/column-selector.ts +37 -1
  9. package/src/builder/operations/cte-manager.ts +16 -0
  10. package/src/builder/operations/filter-manager.ts +32 -0
  11. package/src/builder/operations/join-manager.ts +17 -7
  12. package/src/builder/operations/pagination-manager.ts +19 -0
  13. package/src/builder/operations/relation-manager.ts +58 -3
  14. package/src/builder/query-ast-service.ts +100 -29
  15. package/src/builder/relation-conditions.ts +30 -1
  16. package/src/builder/relation-projection-helper.ts +43 -1
  17. package/src/builder/relation-service.ts +68 -13
  18. package/src/builder/relation-types.ts +6 -0
  19. package/src/builder/select-query-builder-deps.ts +64 -3
  20. package/src/builder/select-query-state.ts +72 -0
  21. package/src/builder/select.ts +166 -0
  22. package/src/codegen/typescript.ts +142 -44
  23. package/src/constants/sql-operator-config.ts +36 -0
  24. package/src/constants/sql.ts +125 -58
  25. package/src/dialect/abstract.ts +97 -22
  26. package/src/dialect/mssql/index.ts +27 -0
  27. package/src/dialect/mysql/index.ts +22 -0
  28. package/src/dialect/postgres/index.ts +22 -0
  29. package/src/dialect/sqlite/index.ts +22 -0
  30. package/src/runtime/als.ts +15 -1
  31. package/src/runtime/hydration.ts +20 -15
  32. package/src/schema/column.ts +45 -5
  33. package/src/schema/relation.ts +49 -2
  34. package/src/schema/table.ts +27 -3
  35. package/src/utils/join-node.ts +20 -0
  36. package/src/utils/raw-column-parser.ts +32 -0
  37. package/src/utils/relation-alias.ts +43 -0
@@ -1,25 +1,61 @@
1
1
  import { ExpressionNode } from '../../ast/expression';
2
2
  import { SelectQueryNode } from '../../ast/query';
3
3
  import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
4
- import { JoinKind, JOIN_KINDS } from '../../constants/sql';
5
-
6
- type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
4
+ import { JoinKind } from '../../constants/sql';
5
+ import { RelationIncludeJoinKind } from '../relation-types';
7
6
 
7
+ /**
8
+ * Options for including relations in queries
9
+ */
8
10
  export interface RelationIncludeOptions {
11
+ /**
12
+ * Columns to include from the related table
13
+ */
9
14
  columns?: string[];
15
+ /**
16
+ * Alias prefix for the relation columns
17
+ */
10
18
  aliasPrefix?: string;
19
+ /**
20
+ * Filter expression to apply to the relation
21
+ */
11
22
  filter?: ExpressionNode;
23
+ /**
24
+ * Type of join to use for the relation
25
+ */
12
26
  joinKind?: RelationIncludeJoinKind;
13
27
  }
14
28
 
29
+ /**
30
+ * Manages relation operations (joins, includes, etc.) for query building
31
+ */
15
32
  export class RelationManager {
33
+ /**
34
+ * Creates a new RelationManager instance
35
+ * @param env - Query builder environment
36
+ */
16
37
  constructor(private readonly env: SelectQueryBuilderEnvironment) {}
17
38
 
39
+ /**
40
+ * Matches records based on a relation with an optional predicate
41
+ * @param context - Current query context
42
+ * @param relationName - Name of the relation to match
43
+ * @param predicate - Optional predicate expression
44
+ * @returns Updated query context with relation match
45
+ */
18
46
  match(context: SelectQueryBuilderContext, relationName: string, predicate?: ExpressionNode): SelectQueryBuilderContext {
19
47
  const result = this.createService(context).match(relationName, predicate);
20
48
  return { state: result.state, hydration: result.hydration };
21
49
  }
22
50
 
51
+ /**
52
+ * Joins a relation to the query
53
+ * @param context - Current query context
54
+ * @param relationName - Name of the relation to join
55
+ * @param joinKind - Type of join to use
56
+ * @param extraCondition - Additional join condition
57
+ * @returns Updated query context with relation join
58
+ */
23
59
  joinRelation(
24
60
  context: SelectQueryBuilderContext,
25
61
  relationName: string,
@@ -30,6 +66,13 @@ export class RelationManager {
30
66
  return { state: result.state, hydration: result.hydration };
31
67
  }
32
68
 
69
+ /**
70
+ * Includes a relation in the query result
71
+ * @param context - Current query context
72
+ * @param relationName - Name of the relation to include
73
+ * @param options - Options for relation inclusion
74
+ * @returns Updated query context with included relation
75
+ */
33
76
  include(
34
77
  context: SelectQueryBuilderContext,
35
78
  relationName: string,
@@ -39,10 +82,22 @@ export class RelationManager {
39
82
  return { state: result.state, hydration: result.hydration };
40
83
  }
41
84
 
85
+ /**
86
+ * Applies relation correlation to a query AST
87
+ * @param context - Current query context
88
+ * @param relationName - Name of the relation
89
+ * @param ast - Query AST to modify
90
+ * @returns Modified query AST with relation correlation
91
+ */
42
92
  applyRelationCorrelation(context: SelectQueryBuilderContext, relationName: string, ast: SelectQueryNode): SelectQueryNode {
43
93
  return this.createService(context).applyRelationCorrelation(relationName, ast);
44
94
  }
45
95
 
96
+ /**
97
+ * Creates a relation service instance
98
+ * @param context - Current query context
99
+ * @returns Relation service instance
100
+ */
46
101
  private createService(context: SelectQueryBuilderContext) {
47
102
  return this.env.deps.createRelationService(this.env.table, context.state, context.hydration);
48
103
  }
@@ -8,12 +8,20 @@ import {
8
8
  CaseExpressionNode,
9
9
  WindowFunctionNode,
10
10
  ScalarSubqueryNode,
11
- and
11
+ and,
12
+ isExpressionSelectionNode
12
13
  } from '../ast/expression';
13
14
  import { JoinNode } from '../ast/join';
14
15
  import { SelectQueryState, ProjectionNode } from './select-query-state';
15
16
  import { OrderDirection } from '../constants/sql';
16
-
17
+ import { parseRawColumn } from '../utils/raw-column-parser';
18
+
19
+ /**
20
+ * Builds a column node from a column definition or existing column node
21
+ * @param table - Table definition
22
+ * @param col - Column definition or column node
23
+ * @returns Column node
24
+ */
17
25
  export const buildColumnNode = (table: TableDef, col: ColumnDef | ColumnNode): ColumnNode => {
18
26
  if ((col as ColumnNode).type === 'Column') {
19
27
  return col as ColumnNode;
@@ -27,14 +35,36 @@ export const buildColumnNode = (table: TableDef, col: ColumnDef | ColumnNode): C
27
35
  };
28
36
  };
29
37
 
38
+ /**
39
+ * Result of column selection operation
40
+ */
30
41
  export interface ColumnSelectionResult {
42
+ /**
43
+ * Updated query state
44
+ */
31
45
  state: SelectQueryState;
46
+ /**
47
+ * Columns that were added
48
+ */
32
49
  addedColumns: ProjectionNode[];
33
50
  }
34
51
 
52
+ /**
53
+ * Service for manipulating query AST (Abstract Syntax Tree)
54
+ */
35
55
  export class QueryAstService {
56
+ /**
57
+ * Creates a new QueryAstService instance
58
+ * @param table - Table definition
59
+ * @param state - Current query state
60
+ */
36
61
  constructor(private readonly table: TableDef, private readonly state: SelectQueryState) {}
37
62
 
63
+ /**
64
+ * Selects columns for the query
65
+ * @param columns - Columns to select (key: alias, value: column definition or expression)
66
+ * @returns Column selection result with updated state and added columns
67
+ */
38
68
  select(
39
69
  columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
40
70
  ): ColumnSelectionResult {
@@ -45,11 +75,7 @@ export class QueryAstService {
45
75
  const newCols = Object.entries(columns).reduce<ProjectionNode[]>((acc, [alias, val]) => {
46
76
  if (existingAliases.has(alias)) return acc;
47
77
 
48
- if (
49
- (val as any).type === 'Function' ||
50
- (val as any).type === 'CaseExpression' ||
51
- (val as any).type === 'WindowFunction'
52
- ) {
78
+ if (isExpressionSelectionNode(val)) {
53
79
  acc.push({ ...(val as FunctionNode | CaseExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
54
80
  return acc;
55
81
  }
@@ -68,12 +94,25 @@ export class QueryAstService {
68
94
  return { state: nextState, addedColumns: newCols };
69
95
  }
70
96
 
97
+ /**
98
+ * Selects raw column expressions (best-effort parser for simple references/functions)
99
+ * @param cols - Raw column expressions
100
+ * @returns Column selection result with updated state and added columns
101
+ */
71
102
  selectRaw(cols: string[]): ColumnSelectionResult {
72
- const newCols = cols.map(c => this.parseRawColumn(c));
103
+ const newCols = cols.map(col => parseRawColumn(col, this.table.name, this.state.ast.ctes));
73
104
  const nextState = this.state.withColumns(newCols);
74
105
  return { state: nextState, addedColumns: newCols };
75
106
  }
76
107
 
108
+ /**
109
+ * Adds a Common Table Expression (CTE) to the query
110
+ * @param name - Name of the CTE
111
+ * @param query - Query for the CTE
112
+ * @param columns - Optional column names for the CTE
113
+ * @param recursive - Whether the CTE is recursive
114
+ * @returns Updated query state with CTE
115
+ */
77
116
  withCte(name: string, query: SelectQueryNode, columns?: string[], recursive = false): SelectQueryState {
78
117
  const cte: CommonTableExpressionNode = {
79
118
  type: 'CommonTableExpression',
@@ -86,70 +125,102 @@ export class QueryAstService {
86
125
  return this.state.withCte(cte);
87
126
  }
88
127
 
128
+ /**
129
+ * Selects a subquery as a column
130
+ * @param alias - Alias for the subquery
131
+ * @param query - Subquery to select
132
+ * @returns Updated query state with subquery selection
133
+ */
89
134
  selectSubquery(alias: string, query: SelectQueryNode): SelectQueryState {
90
135
  const node: ScalarSubqueryNode = { type: 'ScalarSubquery', query, alias };
91
136
  return this.state.withColumns([node]);
92
137
  }
93
138
 
139
+ /**
140
+ * Adds a JOIN clause to the query
141
+ * @param join - Join node to add
142
+ * @returns Updated query state with JOIN
143
+ */
94
144
  withJoin(join: JoinNode): SelectQueryState {
95
145
  return this.state.withJoin(join);
96
146
  }
97
147
 
148
+ /**
149
+ * Adds a WHERE clause to the query
150
+ * @param expr - Expression for the WHERE clause
151
+ * @returns Updated query state with WHERE clause
152
+ */
98
153
  withWhere(expr: ExpressionNode): SelectQueryState {
99
154
  const combined = this.combineExpressions(this.state.ast.where, expr);
100
155
  return this.state.withWhere(combined);
101
156
  }
102
157
 
158
+ /**
159
+ * Adds a GROUP BY clause to the query
160
+ * @param col - Column to group by
161
+ * @returns Updated query state with GROUP BY clause
162
+ */
103
163
  withGroupBy(col: ColumnDef | ColumnNode): SelectQueryState {
104
164
  const node = buildColumnNode(this.table, col);
105
165
  return this.state.withGroupBy([node]);
106
166
  }
107
167
 
168
+ /**
169
+ * Adds a HAVING clause to the query
170
+ * @param expr - Expression for the HAVING clause
171
+ * @returns Updated query state with HAVING clause
172
+ */
108
173
  withHaving(expr: ExpressionNode): SelectQueryState {
109
174
  const combined = this.combineExpressions(this.state.ast.having, expr);
110
175
  return this.state.withHaving(combined);
111
176
  }
112
177
 
178
+ /**
179
+ * Adds an ORDER BY clause to the query
180
+ * @param col - Column to order by
181
+ * @param direction - Order direction (ASC/DESC)
182
+ * @returns Updated query state with ORDER BY clause
183
+ */
113
184
  withOrderBy(col: ColumnDef | ColumnNode, direction: OrderDirection): SelectQueryState {
114
185
  const node = buildColumnNode(this.table, col);
115
186
  return this.state.withOrderBy([{ type: 'OrderBy', column: node, direction }]);
116
187
  }
117
188
 
189
+ /**
190
+ * Adds a DISTINCT clause to the query
191
+ * @param cols - Columns to make distinct
192
+ * @returns Updated query state with DISTINCT clause
193
+ */
118
194
  withDistinct(cols: ColumnNode[]): SelectQueryState {
119
195
  return this.state.withDistinct(cols);
120
196
  }
121
197
 
198
+ /**
199
+ * Adds a LIMIT clause to the query
200
+ * @param limit - Maximum number of rows to return
201
+ * @returns Updated query state with LIMIT clause
202
+ */
122
203
  withLimit(limit: number): SelectQueryState {
123
204
  return this.state.withLimit(limit);
124
205
  }
125
206
 
207
+ /**
208
+ * Adds an OFFSET clause to the query
209
+ * @param offset - Number of rows to skip
210
+ * @returns Updated query state with OFFSET clause
211
+ */
126
212
  withOffset(offset: number): SelectQueryState {
127
213
  return this.state.withOffset(offset);
128
214
  }
129
215
 
216
+ /**
217
+ * Combines expressions with AND operator
218
+ * @param existing - Existing expression
219
+ * @param next - New expression to combine
220
+ * @returns Combined expression
221
+ */
130
222
  private combineExpressions(existing: ExpressionNode | undefined, next: ExpressionNode): ExpressionNode {
131
223
  return existing ? and(existing, next) : next;
132
224
  }
133
225
 
134
- private parseRawColumn(col: string): ColumnNode {
135
- if (col.includes('(')) {
136
- const [fn, rest] = col.split('(');
137
- const colName = rest.replace(')', '');
138
- const [table, name] = colName.includes('.') ? colName.split('.') : [this.table.name, colName];
139
- return { type: 'Column', table, name, alias: col };
140
- }
141
-
142
- if (col.includes('.')) {
143
- const [potentialCteName, columnName] = col.split('.');
144
- const hasCte = this.state.ast.ctes && this.state.ast.ctes.some(cte => cte.name === potentialCteName);
145
-
146
- if (hasCte) {
147
- return { type: 'Column', table: this.table.name, name: col };
148
- }
149
-
150
- return { type: 'Column', table: potentialCteName, name: columnName };
151
- }
152
-
153
- return { type: 'Column', table: this.table.name, name: col };
154
- }
155
226
  }
@@ -1,13 +1,29 @@
1
1
  import { TableDef } from '../schema/table';
2
2
  import { RelationDef, RelationKinds } from '../schema/relation';
3
3
  import { ExpressionNode, eq, and } from '../ast/expression';
4
+ import { findPrimaryKey } from './hydration-planner';
4
5
 
6
+ /**
7
+ * Utility function to handle unreachable code paths
8
+ * @param value - Value that should never occur
9
+ * @throws Error indicating unhandled relation type
10
+ */
5
11
  const assertNever = (value: never): never => {
6
12
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
7
13
  };
8
14
 
15
+ /**
16
+ * Builds the base condition for a relation join
17
+ * @param root - Root table definition
18
+ * @param relation - Relation definition
19
+ * @returns Expression node representing the join condition
20
+ */
9
21
  const baseRelationCondition = (root: TableDef, relation: RelationDef): ExpressionNode => {
10
- const localKey = relation.localKey || 'id';
22
+ const defaultLocalKey =
23
+ relation.type === RelationKinds.HasMany
24
+ ? findPrimaryKey(root)
25
+ : findPrimaryKey(relation.target);
26
+ const localKey = relation.localKey || defaultLocalKey;
11
27
 
12
28
  switch (relation.type) {
13
29
  case RelationKinds.HasMany:
@@ -25,6 +41,13 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
25
41
  }
26
42
  };
27
43
 
44
+ /**
45
+ * Builds a relation join condition with optional extra conditions
46
+ * @param root - Root table definition
47
+ * @param relation - Relation definition
48
+ * @param extra - Optional additional expression to combine with AND
49
+ * @returns Expression node representing the complete join condition
50
+ */
28
51
  export const buildRelationJoinCondition = (
29
52
  root: TableDef,
30
53
  relation: RelationDef,
@@ -34,6 +57,12 @@ export const buildRelationJoinCondition = (
34
57
  return extra ? and(base, extra) : base;
35
58
  };
36
59
 
60
+ /**
61
+ * Builds a relation correlation condition for subqueries
62
+ * @param root - Root table definition
63
+ * @param relation - Relation definition
64
+ * @returns Expression node representing the correlation condition
65
+ */
37
66
  export const buildRelationCorrelation = (root: TableDef, relation: RelationDef): ExpressionNode => {
38
67
  return baseRelationCondition(root, relation);
39
68
  };
@@ -3,25 +3,52 @@ import { ColumnDef } from '../schema/column';
3
3
  import { SelectQueryState } from './select-query-state';
4
4
  import { HydrationManager } from './hydration-manager';
5
5
  import { ColumnNode } from '../ast/expression';
6
- import { findPrimaryKey, isRelationAlias } from './hydration-planner';
6
+ import { findPrimaryKey } from './hydration-planner';
7
+ import { isRelationAlias } from '../utils/relation-alias';
7
8
 
9
+ /**
10
+ * Result of a relation operation
11
+ */
8
12
  export interface RelationResult {
13
+ /**
14
+ * Updated query state
15
+ */
9
16
  state: SelectQueryState;
17
+ /**
18
+ * Updated hydration manager
19
+ */
10
20
  hydration: HydrationManager;
11
21
  }
12
22
 
23
+ /**
24
+ * Callback function for selecting columns
25
+ */
13
26
  type SelectColumnsCallback = (
14
27
  state: SelectQueryState,
15
28
  hydration: HydrationManager,
16
29
  columns: Record<string, ColumnDef>
17
30
  ) => RelationResult;
18
31
 
32
+ /**
33
+ * Helper class for managing relation projections in queries
34
+ */
19
35
  export class RelationProjectionHelper {
36
+ /**
37
+ * Creates a new RelationProjectionHelper instance
38
+ * @param table - Table definition
39
+ * @param selectColumns - Callback for selecting columns
40
+ */
20
41
  constructor(
21
42
  private readonly table: TableDef,
22
43
  private readonly selectColumns: SelectColumnsCallback
23
44
  ) {}
24
45
 
46
+ /**
47
+ * Ensures base projection is included in the query
48
+ * @param state - Current query state
49
+ * @param hydration - Hydration manager
50
+ * @returns Relation result with updated state and hydration
51
+ */
25
52
  ensureBaseProjection(state: SelectQueryState, hydration: HydrationManager): RelationResult {
26
53
  const primaryKey = findPrimaryKey(this.table);
27
54
 
@@ -38,10 +65,21 @@ export class RelationProjectionHelper {
38
65
  return { state, hydration };
39
66
  }
40
67
 
68
+ /**
69
+ * Checks if base projection exists in the query
70
+ * @param state - Current query state
71
+ * @returns True if base projection exists
72
+ */
41
73
  private hasBaseProjection(state: SelectQueryState): boolean {
42
74
  return state.ast.columns.some(col => !isRelationAlias((col as ColumnNode).alias));
43
75
  }
44
76
 
77
+ /**
78
+ * Checks if primary key is selected in the query
79
+ * @param state - Current query state
80
+ * @param primaryKey - Primary key name
81
+ * @returns True if primary key is selected
82
+ */
45
83
  private hasPrimarySelected(state: SelectQueryState, primaryKey: string): boolean {
46
84
  return state.ast.columns.some(col => {
47
85
  const alias = (col as ColumnNode).alias;
@@ -50,6 +88,10 @@ export class RelationProjectionHelper {
50
88
  });
51
89
  }
52
90
 
91
+ /**
92
+ * Gets all base columns for the table
93
+ * @returns Record of all table columns
94
+ */
53
95
  private getBaseColumns(): Record<string, ColumnDef> {
54
96
  return Object.keys(this.table.columns).reduce((acc, key) => {
55
97
  acc[key] = (this.table.columns as Record<string, ColumnDef>)[key];
@@ -7,7 +7,6 @@ import {
7
7
  ExpressionNode,
8
8
  and
9
9
  } from '../ast/expression';
10
- import { JoinNode } from '../ast/join';
11
10
  import { SelectQueryState } from './select-query-state';
12
11
  import { HydrationManager } from './hydration-manager';
13
12
  import { QueryAstService } from './query-ast-service';
@@ -16,22 +15,40 @@ import { RelationProjectionHelper } from './relation-projection-helper';
16
15
  import type { RelationResult } from './relation-projection-helper';
17
16
  import { buildRelationJoinCondition, buildRelationCorrelation } from './relation-conditions';
18
17
  import { JoinKind, JOIN_KINDS } from '../constants/sql';
18
+ import { RelationIncludeJoinKind } from './relation-types';
19
+ import { createJoinNode } from '../utils/join-node';
20
+ import { makeRelationAlias } from '../utils/relation-alias';
19
21
 
20
- type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
21
-
22
+ /**
23
+ * Service for handling relation operations (joins, includes, etc.)
24
+ */
22
25
  export class RelationService {
23
26
  private readonly projectionHelper: RelationProjectionHelper;
24
27
 
28
+ /**
29
+ * Creates a new RelationService instance
30
+ * @param table - Table definition
31
+ * @param state - Current query state
32
+ * @param hydration - Hydration manager
33
+ */
25
34
  constructor(
26
35
  private readonly table: TableDef,
27
36
  private readonly state: SelectQueryState,
28
- private readonly hydration: HydrationManager
37
+ private readonly hydration: HydrationManager,
38
+ private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
29
39
  ) {
30
40
  this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
31
41
  this.selectColumns(state, hydration, columns)
32
42
  );
33
43
  }
34
44
 
45
+ /**
46
+ * Joins a relation to the query
47
+ * @param relationName - Name of the relation to join
48
+ * @param joinKind - Type of join to use
49
+ * @param extraCondition - Additional join condition
50
+ * @returns Relation result with updated state and hydration
51
+ */
35
52
  joinRelation(
36
53
  relationName: string,
37
54
  joinKind: JoinKind,
@@ -41,6 +58,12 @@ export class RelationService {
41
58
  return { state: nextState, hydration: this.hydration };
42
59
  }
43
60
 
61
+ /**
62
+ * Matches records based on a relation with an optional predicate
63
+ * @param relationName - Name of the relation to match
64
+ * @param predicate - Optional predicate expression
65
+ * @returns Relation result with updated state and hydration
66
+ */
44
67
  match(
45
68
  relationName: string,
46
69
  predicate?: ExpressionNode
@@ -53,6 +76,12 @@ export class RelationService {
53
76
  return { state: nextState, hydration: joined.hydration };
54
77
  }
55
78
 
79
+ /**
80
+ * Includes a relation in the query result
81
+ * @param relationName - Name of the relation to include
82
+ * @param options - Options for relation inclusion
83
+ * @returns Relation result with updated state and hydration
84
+ */
56
85
  include(
57
86
  relationName: string,
58
87
  options?: { columns?: string[]; aliasPrefix?: string; filter?: ExpressionNode; joinKind?: RelationIncludeJoinKind }
@@ -82,7 +111,7 @@ export class RelationService {
82
111
  if (!def) {
83
112
  throw new Error(`Column '${key}' not found on relation '${relationName}'`);
84
113
  }
85
- acc[`${aliasPrefix}__${key}`] = def;
114
+ acc[makeRelationAlias(aliasPrefix, key)] = def;
86
115
  return acc;
87
116
  }, {} as Record<string, ColumnDef>);
88
117
 
@@ -101,6 +130,12 @@ export class RelationService {
101
130
  return { state, hydration };
102
131
  }
103
132
 
133
+ /**
134
+ * Applies relation correlation to a query AST
135
+ * @param relationName - Name of the relation
136
+ * @param ast - Query AST to modify
137
+ * @returns Modified query AST with relation correlation
138
+ */
104
139
  applyRelationCorrelation(
105
140
  relationName: string,
106
141
  ast: SelectQueryNode
@@ -117,6 +152,14 @@ export class RelationService {
117
152
  };
118
153
  }
119
154
 
155
+ /**
156
+ * Creates a join node for a relation
157
+ * @param state - Current query state
158
+ * @param relationName - Name of the relation
159
+ * @param joinKind - Type of join to use
160
+ * @param extraCondition - Additional join condition
161
+ * @returns Updated query state with join
162
+ */
120
163
  private withJoin(
121
164
  state: SelectQueryState,
122
165
  relationName: string,
@@ -126,17 +169,18 @@ export class RelationService {
126
169
  const relation = this.getRelation(relationName);
127
170
  const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
128
171
 
129
- const joinNode: JoinNode = {
130
- type: 'Join',
131
- kind: joinKind,
132
- table: { type: 'Table', name: relation.target.name },
133
- condition,
134
- relationName
135
- };
172
+ const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
136
173
 
137
174
  return this.astService(state).withJoin(joinNode);
138
175
  }
139
176
 
177
+ /**
178
+ * Selects columns for a relation
179
+ * @param state - Current query state
180
+ * @param hydration - Hydration manager
181
+ * @param columns - Columns to select
182
+ * @returns Relation result with updated state and hydration
183
+ */
140
184
  private selectColumns(
141
185
  state: SelectQueryState,
142
186
  hydration: HydrationManager,
@@ -149,6 +193,12 @@ export class RelationService {
149
193
  };
150
194
  }
151
195
 
196
+ /**
197
+ * Gets a relation definition by name
198
+ * @param relationName - Name of the relation
199
+ * @returns Relation definition
200
+ * @throws Error if relation is not found
201
+ */
152
202
  private getRelation(relationName: string): RelationDef {
153
203
  const relation = this.table.relations[relationName];
154
204
  if (!relation) {
@@ -158,8 +208,13 @@ export class RelationService {
158
208
  return relation;
159
209
  }
160
210
 
211
+ /**
212
+ * Creates a QueryAstService instance
213
+ * @param state - Current query state
214
+ * @returns QueryAstService instance
215
+ */
161
216
  private astService(state: SelectQueryState = this.state): QueryAstService {
162
- return new QueryAstService(this.table, state);
217
+ return this.createQueryAstService(this.table, state);
163
218
  }
164
219
  }
165
220
 
@@ -0,0 +1,6 @@
1
+ import { JOIN_KINDS } from '../constants/sql';
2
+
3
+ /**
4
+ * Join kinds allowed when including a relation using `.include(...)`.
5
+ */
6
+ export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;