metal-orm 1.0.3 → 1.0.5

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/src/ast/query.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { ColumnNode, FunctionNode, ExpressionNode, ScalarSubqueryNode, CaseExpressionNode, WindowFunctionNode } from './expression';
1
+ import {
2
+ ColumnNode,
3
+ FunctionNode,
4
+ ExpressionNode,
5
+ ScalarSubqueryNode,
6
+ CaseExpressionNode,
7
+ WindowFunctionNode,
8
+ OperandNode
9
+ } from './expression';
2
10
  import { JoinNode } from './join';
3
11
  import { RelationType } from '../schema/relation';
4
12
  import { OrderDirection } from '../constants/sql';
@@ -27,27 +35,39 @@ export interface OrderByNode {
27
35
  direction: OrderDirection;
28
36
  }
29
37
 
30
- /**
31
- * Plan for hydrating relationship data
32
- */
33
- export interface HydrationRelationPlan {
34
- /** Name of the relationship */
35
- name: string;
36
- /** Alias prefix for the relationship */
37
- aliasPrefix: string;
38
- /** Type of relationship */
39
- type: RelationType;
40
- /** Target table name */
41
- targetTable: string;
42
- /** Target table primary key */
43
- targetPrimaryKey: string;
44
- /** Foreign key column */
45
- foreignKey: string;
46
- /** Local key column */
47
- localKey: string;
48
- /** Columns to include */
49
- columns: string[];
50
- }
38
+ /**
39
+ * Plan describing pivot columns needed for hydration
40
+ */
41
+ export interface HydrationPivotPlan {
42
+ table: string;
43
+ primaryKey: string;
44
+ aliasPrefix: string;
45
+ columns: string[];
46
+ }
47
+
48
+ /**
49
+ * Plan for hydrating relationship data
50
+ */
51
+ export interface HydrationRelationPlan {
52
+ /** Name of the relationship */
53
+ name: string;
54
+ /** Alias prefix for the relationship */
55
+ aliasPrefix: string;
56
+ /** Type of relationship */
57
+ type: RelationType;
58
+ /** Target table name */
59
+ targetTable: string;
60
+ /** Target table primary key */
61
+ targetPrimaryKey: string;
62
+ /** Foreign key column */
63
+ foreignKey: string;
64
+ /** Local key column */
65
+ localKey: string;
66
+ /** Columns to include */
67
+ columns: string[];
68
+ /** Optional pivot plan for many-to-many relationships */
69
+ pivot?: HydrationPivotPlan;
70
+ }
51
71
 
52
72
  /**
53
73
  * Complete hydration plan for a query
@@ -89,30 +109,71 @@ export interface CommonTableExpressionNode {
89
109
  /**
90
110
  * AST node representing a complete SELECT query
91
111
  */
92
- export interface SelectQueryNode {
93
- type: 'SelectQuery';
94
- /** Optional CTEs (WITH clauses) */
95
- ctes?: CommonTableExpressionNode[];
96
- /** FROM clause table */
97
- from: TableNode;
98
- /** SELECT clause columns */
99
- columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
100
- /** JOIN clauses */
101
- joins: JoinNode[];
102
- /** Optional WHERE clause */
103
- where?: ExpressionNode;
104
- /** Optional GROUP BY clause */
105
- groupBy?: ColumnNode[];
106
- /** Optional HAVING clause */
107
- having?: ExpressionNode;
108
- /** Optional ORDER BY clause */
109
- orderBy?: OrderByNode[];
110
- /** Optional LIMIT clause */
111
- limit?: number;
112
- /** Optional OFFSET clause */
113
- offset?: number;
114
- /** Optional query metadata */
115
- meta?: QueryMetadata;
116
- /** Optional DISTINCT clause */
117
- distinct?: ColumnNode[];
118
- }
112
+ export interface SelectQueryNode {
113
+ type: 'SelectQuery';
114
+ /** Optional CTEs (WITH clauses) */
115
+ ctes?: CommonTableExpressionNode[];
116
+ /** FROM clause table */
117
+ from: TableNode;
118
+ /** SELECT clause columns */
119
+ columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
120
+ /** JOIN clauses */
121
+ joins: JoinNode[];
122
+ /** Optional WHERE clause */
123
+ where?: ExpressionNode;
124
+ /** Optional GROUP BY clause */
125
+ groupBy?: ColumnNode[];
126
+ /** Optional HAVING clause */
127
+ having?: ExpressionNode;
128
+ /** Optional ORDER BY clause */
129
+ orderBy?: OrderByNode[];
130
+ /** Optional LIMIT clause */
131
+ limit?: number;
132
+ /** Optional OFFSET clause */
133
+ offset?: number;
134
+ /** Optional query metadata */
135
+ meta?: QueryMetadata;
136
+ /** Optional DISTINCT clause */
137
+ distinct?: ColumnNode[];
138
+ }
139
+
140
+ export interface InsertQueryNode {
141
+ type: 'InsertQuery';
142
+ /** Target table */
143
+ into: TableNode;
144
+ /** Column order for inserted values */
145
+ columns: ColumnNode[];
146
+ /** Rows of values to insert */
147
+ values: OperandNode[][];
148
+ /** Optional RETURNING clause */
149
+ returning?: ColumnNode[];
150
+ }
151
+
152
+ export interface UpdateAssignmentNode {
153
+ /** Column to update */
154
+ column: ColumnNode;
155
+ /** Value to set */
156
+ value: OperandNode;
157
+ }
158
+
159
+ export interface UpdateQueryNode {
160
+ type: 'UpdateQuery';
161
+ /** Table being updated */
162
+ table: TableNode;
163
+ /** Assignments for SET clause */
164
+ set: UpdateAssignmentNode[];
165
+ /** Optional WHERE clause */
166
+ where?: ExpressionNode;
167
+ /** Optional RETURNING clause */
168
+ returning?: ColumnNode[];
169
+ }
170
+
171
+ export interface DeleteQueryNode {
172
+ type: 'DeleteQuery';
173
+ /** Table to delete from */
174
+ from: TableNode;
175
+ /** Optional WHERE clause */
176
+ where?: ExpressionNode;
177
+ /** Optional RETURNING clause */
178
+ returning?: ColumnNode[];
179
+ }
@@ -0,0 +1,42 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { ColumnNode, ExpressionNode } from '../ast/expression';
3
+ import { TableNode, DeleteQueryNode } from '../ast/query';
4
+
5
+ const createTableNode = (table: TableDef): TableNode => ({
6
+ type: 'Table',
7
+ name: table.name
8
+ });
9
+
10
+ /**
11
+ * Maintains immutable state for DELETE queries
12
+ */
13
+ export class DeleteQueryState {
14
+ public readonly table: TableDef;
15
+ public readonly ast: DeleteQueryNode;
16
+
17
+ constructor(table: TableDef, ast?: DeleteQueryNode) {
18
+ this.table = table;
19
+ this.ast = ast ?? {
20
+ type: 'DeleteQuery',
21
+ from: createTableNode(table)
22
+ };
23
+ }
24
+
25
+ private clone(nextAst: DeleteQueryNode): DeleteQueryState {
26
+ return new DeleteQueryState(this.table, nextAst);
27
+ }
28
+
29
+ withWhere(expr: ExpressionNode): DeleteQueryState {
30
+ return this.clone({
31
+ ...this.ast,
32
+ where: expr
33
+ });
34
+ }
35
+
36
+ withReturning(columns: ColumnNode[]): DeleteQueryState {
37
+ return this.clone({
38
+ ...this.ast,
39
+ returning: [...columns]
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,57 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { ColumnDef } from '../schema/column';
3
+ import { ColumnNode, ExpressionNode } from '../ast/expression';
4
+ import { CompiledQuery, DeleteCompiler } from '../dialect/abstract';
5
+ import { DeleteQueryNode } from '../ast/query';
6
+ import { DeleteQueryState } from './delete-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 DELETE queries
22
+ */
23
+ export class DeleteQueryBuilder<T> {
24
+ private readonly table: TableDef;
25
+ private readonly state: DeleteQueryState;
26
+
27
+ constructor(table: TableDef, state?: DeleteQueryState) {
28
+ this.table = table;
29
+ this.state = state ?? new DeleteQueryState(table);
30
+ }
31
+
32
+ private clone(state: DeleteQueryState): DeleteQueryBuilder<T> {
33
+ return new DeleteQueryBuilder(this.table, state);
34
+ }
35
+
36
+ where(expr: ExpressionNode): DeleteQueryBuilder<T> {
37
+ return this.clone(this.state.withWhere(expr));
38
+ }
39
+
40
+ returning(...columns: (ColumnDef | ColumnNode)[]): DeleteQueryBuilder<T> {
41
+ if (!columns.length) return this;
42
+ const nodes = columns.map(column => buildColumnNode(this.table, column));
43
+ return this.clone(this.state.withReturning(nodes));
44
+ }
45
+
46
+ compile(compiler: DeleteCompiler): CompiledQuery {
47
+ return compiler.compileDelete(this.state.ast);
48
+ }
49
+
50
+ toSql(compiler: DeleteCompiler): string {
51
+ return this.compile(compiler).sql;
52
+ }
53
+
54
+ getAST(): DeleteQueryNode {
55
+ return this.state.ast;
56
+ }
57
+ }
@@ -52,10 +52,11 @@ export class HydrationManager {
52
52
  relation: RelationDef,
53
53
  relationName: string,
54
54
  aliasPrefix: string,
55
- targetColumns: string[]
55
+ targetColumns: string[],
56
+ pivot?: { aliasPrefix: string; columns: string[] }
56
57
  ): HydrationManager {
57
58
  const withRoots = this.planner.captureRootColumns(state.ast.columns);
58
- const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns);
59
+ const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns, pivot);
59
60
  return this.clone(next);
60
61
  }
61
62
 
@@ -1,8 +1,9 @@
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 { ProjectionNode } from './select-query-state';
4
4
  import { HydrationPlan, HydrationRelationPlan } from '../ast/query';
5
5
  import { isRelationAlias } from '../utils/relation-alias';
6
+ import { buildDefaultPivotColumns } from './relation-utils';
6
7
 
7
8
  /**
8
9
  * Finds the primary key column name for a table
@@ -62,15 +63,21 @@ export class HydrationPlanner {
62
63
  * @param columns - Columns to include from the relation
63
64
  * @returns Updated HydrationPlanner with included relation
64
65
  */
65
- includeRelation(rel: RelationDef, relationName: string, aliasPrefix: string, columns: string[]): HydrationPlanner {
66
- const currentPlan = this.getPlanOrDefault();
67
- const relations = currentPlan.relations.filter(r => r.name !== relationName);
68
- relations.push(this.buildRelationPlan(rel, relationName, aliasPrefix, columns));
69
- return new HydrationPlanner(this.table, {
70
- ...currentPlan,
71
- relations
72
- });
73
- }
66
+ includeRelation(
67
+ rel: RelationDef,
68
+ relationName: string,
69
+ aliasPrefix: string,
70
+ columns: string[],
71
+ pivot?: { aliasPrefix: string; columns: string[] }
72
+ ): HydrationPlanner {
73
+ const currentPlan = this.getPlanOrDefault();
74
+ const relations = currentPlan.relations.filter(r => r.name !== relationName);
75
+ relations.push(this.buildRelationPlan(rel, relationName, aliasPrefix, columns, pivot));
76
+ return new HydrationPlanner(this.table, {
77
+ ...currentPlan,
78
+ relations
79
+ });
80
+ }
74
81
 
75
82
  /**
76
83
  * Gets the current hydration plan
@@ -96,31 +103,80 @@ export class HydrationPlanner {
96
103
  * @param columns - Columns to include from the relation
97
104
  * @returns Hydration relation plan
98
105
  */
99
- private buildRelationPlan(rel: RelationDef, relationName: string, aliasPrefix: string, columns: string[]): HydrationRelationPlan {
100
- const localKeyFallback =
101
- rel.type === RelationKinds.HasMany ? findPrimaryKey(this.table) : findPrimaryKey(rel.target);
106
+ private buildRelationPlan(
107
+ rel: RelationDef,
108
+ relationName: string,
109
+ aliasPrefix: string,
110
+ columns: string[],
111
+ pivot?: { aliasPrefix: string; columns: string[] }
112
+ ): HydrationRelationPlan {
113
+ switch (rel.type) {
114
+ case RelationKinds.HasMany: {
115
+ const localKey = rel.localKey || findPrimaryKey(this.table);
116
+ return {
117
+ name: relationName,
118
+ aliasPrefix,
119
+ type: rel.type,
120
+ targetTable: rel.target.name,
121
+ targetPrimaryKey: findPrimaryKey(rel.target),
122
+ foreignKey: rel.foreignKey,
123
+ localKey,
124
+ columns
125
+ };
126
+ }
127
+ case RelationKinds.BelongsTo: {
128
+ const localKey = rel.localKey || findPrimaryKey(rel.target);
129
+ return {
130
+ name: relationName,
131
+ aliasPrefix,
132
+ type: rel.type,
133
+ targetTable: rel.target.name,
134
+ targetPrimaryKey: findPrimaryKey(rel.target),
135
+ foreignKey: rel.foreignKey,
136
+ localKey,
137
+ columns
138
+ };
139
+ }
140
+ case RelationKinds.BelongsToMany: {
141
+ const many = rel as BelongsToManyRelation;
142
+ const localKey = many.localKey || findPrimaryKey(this.table);
143
+ const targetPk = many.targetKey || findPrimaryKey(many.target);
144
+ const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
145
+ const pivotAliasPrefix = pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
146
+ const pivotColumns =
147
+ pivot?.columns ??
148
+ many.defaultPivotColumns ??
149
+ buildDefaultPivotColumns(many, pivotPk);
102
150
 
103
- return {
104
- name: relationName,
105
- aliasPrefix,
106
- type: rel.type,
107
- targetTable: rel.target.name,
108
- targetPrimaryKey: findPrimaryKey(rel.target),
109
- foreignKey: rel.foreignKey,
110
- localKey: rel.localKey || localKeyFallback,
111
- columns
112
- };
151
+ return {
152
+ name: relationName,
153
+ aliasPrefix,
154
+ type: rel.type,
155
+ targetTable: many.target.name,
156
+ targetPrimaryKey: targetPk,
157
+ foreignKey: many.pivotForeignKeyToRoot,
158
+ localKey,
159
+ columns,
160
+ pivot: {
161
+ table: many.pivotTable.name,
162
+ primaryKey: pivotPk,
163
+ aliasPrefix: pivotAliasPrefix,
164
+ columns: pivotColumns
165
+ }
166
+ };
167
+ }
168
+ }
113
169
  }
114
170
  }
115
171
 
116
- /**
117
- * Builds a default hydration plan for a table
118
- * @param table - Table definition
119
- * @returns Default hydration plan
120
- */
172
+ /**
173
+ * Builds a default hydration plan for a table
174
+ * @param table - Table definition
175
+ * @returns Default hydration plan
176
+ */
121
177
  const buildDefaultHydrationPlan = (table: TableDef): HydrationPlan => ({
122
- rootTable: table.name,
123
- rootPrimaryKey: findPrimaryKey(table),
124
- rootColumns: [],
125
- relations: []
126
- });
178
+ rootTable: table.name,
179
+ rootPrimaryKey: findPrimaryKey(table),
180
+ rootColumns: [],
181
+ relations: []
182
+ });
@@ -0,0 +1,62 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { InsertQueryNode, TableNode } from '../ast/query';
3
+ import { ColumnNode, OperandNode, valueToOperand } from '../ast/expression';
4
+
5
+ const createTableNode = (table: TableDef): TableNode => ({
6
+ type: 'Table',
7
+ name: table.name
8
+ });
9
+
10
+ const buildColumnNodes = (table: TableDef, names: string[]): ColumnNode[] =>
11
+ names.map(name => ({
12
+ type: 'Column',
13
+ table: table.name,
14
+ name
15
+ }));
16
+
17
+ /**
18
+ * Maintains immutable state for building INSERT queries
19
+ */
20
+ export class InsertQueryState {
21
+ public readonly table: TableDef;
22
+ public readonly ast: InsertQueryNode;
23
+
24
+ constructor(table: TableDef, ast?: InsertQueryNode) {
25
+ this.table = table;
26
+ this.ast = ast ?? {
27
+ type: 'InsertQuery',
28
+ into: createTableNode(table),
29
+ columns: [],
30
+ values: []
31
+ };
32
+ }
33
+
34
+ private clone(nextAst: InsertQueryNode): InsertQueryState {
35
+ return new InsertQueryState(this.table, nextAst);
36
+ }
37
+
38
+ withValues(rows: Record<string, unknown>[]): InsertQueryState {
39
+ if (!rows.length) return this;
40
+
41
+ const definedColumns = this.ast.columns.length
42
+ ? this.ast.columns
43
+ : buildColumnNodes(this.table, Object.keys(rows[0]));
44
+
45
+ const newRows: OperandNode[][] = rows.map(row =>
46
+ definedColumns.map(column => valueToOperand(row[column.name]))
47
+ );
48
+
49
+ return this.clone({
50
+ ...this.ast,
51
+ columns: definedColumns,
52
+ values: [...this.ast.values, ...newRows]
53
+ });
54
+ }
55
+
56
+ withReturning(columns: ColumnNode[]): InsertQueryState {
57
+ return this.clone({
58
+ ...this.ast,
59
+ returning: [...columns]
60
+ });
61
+ }
62
+ }
@@ -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 { RelationIncludeJoinKind } from '../relation-types';
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