metal-orm 1.1.9 → 1.1.11

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 (77) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2255 -284
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +559 -39
  5. package/dist/index.d.ts +559 -39
  6. package/dist/index.js +2227 -284
  7. package/dist/index.js.map +1 -1
  8. package/package.json +17 -12
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +87 -73
  11. package/scripts/generate-entities/tree-detection.mjs +67 -61
  12. package/src/bulk/bulk-context.ts +83 -0
  13. package/src/bulk/bulk-delete-executor.ts +87 -0
  14. package/src/bulk/bulk-executor.base.ts +73 -0
  15. package/src/bulk/bulk-insert-executor.ts +74 -0
  16. package/src/bulk/bulk-types.ts +70 -0
  17. package/src/bulk/bulk-update-executor.ts +192 -0
  18. package/src/bulk/bulk-upsert-executor.ts +93 -0
  19. package/src/bulk/bulk-utils.ts +91 -0
  20. package/src/bulk/index.ts +18 -0
  21. package/src/codegen/typescript.ts +30 -21
  22. package/src/core/ast/expression-builders.ts +107 -10
  23. package/src/core/ast/expression-nodes.ts +52 -22
  24. package/src/core/ast/expression-visitor.ts +23 -13
  25. package/src/core/ddl/introspect/mysql.ts +113 -36
  26. package/src/core/dialect/abstract.ts +30 -17
  27. package/src/core/dialect/mysql/index.ts +20 -5
  28. package/src/core/execution/db-executor.ts +96 -64
  29. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  30. package/src/core/execution/executors/mssql-executor.ts +66 -34
  31. package/src/core/execution/executors/mysql-executor.ts +98 -66
  32. package/src/core/execution/executors/postgres-executor.ts +33 -11
  33. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  34. package/src/decorators/bootstrap.ts +482 -398
  35. package/src/decorators/column-decorator.ts +87 -96
  36. package/src/decorators/decorator-metadata.ts +100 -24
  37. package/src/decorators/entity.ts +27 -24
  38. package/src/decorators/relations.ts +231 -149
  39. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  40. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  41. package/src/dto/apply-filter.ts +568 -551
  42. package/src/index.ts +16 -9
  43. package/src/orm/entity-hydration.ts +116 -72
  44. package/src/orm/entity-metadata.ts +347 -301
  45. package/src/orm/entity-relations.ts +264 -207
  46. package/src/orm/entity.ts +199 -199
  47. package/src/orm/execute.ts +13 -13
  48. package/src/orm/lazy-batch/morph-many.ts +70 -0
  49. package/src/orm/lazy-batch/morph-one.ts +69 -0
  50. package/src/orm/lazy-batch/morph-to.ts +59 -0
  51. package/src/orm/lazy-batch.ts +4 -1
  52. package/src/orm/orm-session.ts +170 -104
  53. package/src/orm/pooled-executor-factory.ts +99 -58
  54. package/src/orm/query-logger.ts +49 -40
  55. package/src/orm/relation-change-processor.ts +198 -96
  56. package/src/orm/relations/belongs-to.ts +143 -143
  57. package/src/orm/relations/has-many.ts +204 -204
  58. package/src/orm/relations/has-one.ts +174 -174
  59. package/src/orm/relations/many-to-many.ts +288 -288
  60. package/src/orm/relations/morph-many.ts +156 -0
  61. package/src/orm/relations/morph-one.ts +151 -0
  62. package/src/orm/relations/morph-to.ts +162 -0
  63. package/src/orm/save-graph.ts +116 -1
  64. package/src/query-builder/expression-table-mapper.ts +5 -0
  65. package/src/query-builder/hydration-manager.ts +345 -345
  66. package/src/query-builder/hydration-planner.ts +178 -148
  67. package/src/query-builder/relation-conditions.ts +171 -151
  68. package/src/query-builder/relation-cte-builder.ts +5 -1
  69. package/src/query-builder/relation-filter-utils.ts +9 -6
  70. package/src/query-builder/relation-include-strategies.ts +44 -2
  71. package/src/query-builder/relation-join-strategies.ts +8 -1
  72. package/src/query-builder/relation-service.ts +250 -241
  73. package/src/query-builder/select/select-operations.ts +110 -105
  74. package/src/query-builder/update-include.ts +4 -0
  75. package/src/schema/relation.ts +296 -188
  76. package/src/schema/types.ts +138 -123
  77. package/src/tree/tree-decorator.ts +127 -137
@@ -1,151 +1,171 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
3
- import { ExpressionNode, eq, and } from '../core/ast/expression.js';
4
- import { TableSourceNode } from '../core/ast/query.js';
5
- import { findPrimaryKey } from './hydration-planner.js';
6
- import { JoinNode } from '../core/ast/join.js';
7
- import { JoinKind } from '../core/sql/sql.js';
8
- import { createJoinNode } from '../core/ast/join-node.js';
9
-
10
- /**
11
- * Utility function to handle unreachable code paths
12
- * @param value - Value that should never occur
13
- * @throws Error indicating unhandled relation type
14
- */
15
- const assertNever = (value: never): never => {
16
- throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
17
- };
18
-
19
- /**
20
- * Builds the base condition for a relation join
21
- * @param root - Root table definition
22
- * @param relation - Relation definition
23
- * @returns Expression node representing the join condition
24
- */
25
- const baseRelationCondition = (
26
- root: TableDef,
27
- relation: RelationDef,
28
- rootAlias?: string,
29
- targetTableName?: string
30
- ): ExpressionNode => {
31
- const rootTable = rootAlias || root.name;
32
- const targetTable = targetTableName ?? relation.target.name;
33
- const defaultLocalKey =
34
- relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
35
- ? findPrimaryKey(root)
36
- : findPrimaryKey(relation.target);
37
- const localKey = relation.localKey || defaultLocalKey;
38
-
39
- switch (relation.type) {
40
- case RelationKinds.HasMany:
41
- case RelationKinds.HasOne:
42
- return eq(
43
- { type: 'Column', table: targetTable, name: relation.foreignKey },
44
- { type: 'Column', table: rootTable, name: localKey }
45
- );
46
- case RelationKinds.BelongsTo:
47
- return eq(
48
- { type: 'Column', table: targetTable, name: localKey },
49
- { type: 'Column', table: rootTable, name: relation.foreignKey }
50
- );
51
- case RelationKinds.BelongsToMany:
52
- throw new Error('BelongsToMany relations do not support the standard join condition builder');
53
- default:
54
- return assertNever(relation);
55
- }
56
- };
57
-
58
- /**
59
- * Builds the join nodes required to include a BelongsToMany relation.
60
- * @param root - The root table definition
61
- * @param relationName - Name of the relation being joined
62
- * @param relation - The BelongsToMany relation definition
63
- * @param joinKind - The type of join to perform
64
- * @param extra - Optional additional conditions for the target join
65
- * @param rootAlias - Optional alias for the root table
66
- * @returns Array of join nodes for the pivot and target tables
67
- */
68
- export const buildBelongsToManyJoins = (
69
- root: TableDef,
70
- relationName: string,
71
- relation: BelongsToManyRelation,
72
- joinKind: JoinKind,
73
- extra?: ExpressionNode,
74
- rootAlias?: string,
75
- targetTable?: TableSourceNode,
76
- targetTableName?: string
77
- ): JoinNode[] => {
78
- const rootKey = relation.localKey || findPrimaryKey(root);
79
- const targetKey = relation.targetKey || findPrimaryKey(relation.target);
80
- const rootTable = rootAlias || root.name;
81
-
82
- const pivotCondition = eq(
83
- { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
84
- { type: 'Column', table: rootTable, name: rootKey }
85
- );
86
-
87
- const pivotJoin = createJoinNode(
88
- joinKind,
89
- { type: 'Table', name: relation.pivotTable.name, schema: relation.pivotTable.schema },
90
- pivotCondition
91
- );
92
-
93
- const targetSource: TableSourceNode = targetTable ?? {
94
- type: 'Table',
95
- name: relation.target.name,
96
- schema: relation.target.schema
97
- };
98
- const effectiveTargetName = targetTableName ?? relation.target.name;
99
- let targetCondition: ExpressionNode = eq(
100
- { type: 'Column', table: effectiveTargetName, name: targetKey },
101
- { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
102
- );
103
-
104
- if (extra) {
105
- targetCondition = and(targetCondition, extra);
106
- }
107
-
108
- const targetJoin = createJoinNode(
109
- joinKind,
110
- targetSource,
111
- targetCondition,
112
- relationName
113
- );
114
-
115
- return [pivotJoin, targetJoin];
116
- };
117
-
118
- /**
119
- * Builds a relation join condition with optional extra conditions
120
- * @param root - Root table definition
121
- * @param relation - Relation definition
122
- * @param extra - Optional additional expression to combine with AND
123
- * @param rootAlias - Optional alias for the root table
124
- * @returns Expression node representing the complete join condition
125
- */
126
- export const buildRelationJoinCondition = (
127
- root: TableDef,
128
- relation: RelationDef,
129
- extra?: ExpressionNode,
130
- rootAlias?: string,
131
- targetTableName?: string
132
- ): ExpressionNode => {
133
- const base = baseRelationCondition(root, relation, rootAlias, targetTableName);
134
- return extra ? and(base, extra) : base;
135
- };
136
-
137
- /**
138
- * Builds a relation correlation condition for subqueries
139
- * @param root - Root table definition
140
- * @param relation - Relation definition
141
- * @param rootAlias - Optional alias for the root table
142
- * @returns Expression node representing the correlation condition
143
- */
144
- export const buildRelationCorrelation = (
145
- root: TableDef,
146
- relation: RelationDef,
147
- rootAlias?: string,
148
- targetTableName?: string
149
- ): ExpressionNode => {
150
- return baseRelationCondition(root, relation, rootAlias, targetTableName);
151
- };
1
+ import { TableDef } from '../schema/table.js';
2
+ import { RelationDef, RelationKinds, BelongsToManyRelation, MorphOneRelation, MorphManyRelation } from '../schema/relation.js';
3
+ import { ExpressionNode, eq, and } from '../core/ast/expression.js';
4
+ import { TableSourceNode } from '../core/ast/query.js';
5
+ import { findPrimaryKey } from './hydration-planner.js';
6
+ import { JoinNode } from '../core/ast/join.js';
7
+ import { JoinKind } from '../core/sql/sql.js';
8
+ import { createJoinNode } from '../core/ast/join-node.js';
9
+
10
+ /**
11
+ * Utility function to handle unreachable code paths
12
+ * @param value - Value that should never occur
13
+ * @throws Error indicating unhandled relation type
14
+ */
15
+ const assertNever = (value: never): never => {
16
+ throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
17
+ };
18
+
19
+ /**
20
+ * Builds the base condition for a relation join
21
+ * @param root - Root table definition
22
+ * @param relation - Relation definition
23
+ * @returns Expression node representing the join condition
24
+ */
25
+ const baseRelationCondition = (
26
+ root: TableDef,
27
+ relation: RelationDef,
28
+ rootAlias?: string,
29
+ targetTableName?: string
30
+ ): ExpressionNode => {
31
+ const rootTable = rootAlias || root.name;
32
+
33
+ if (relation.type === RelationKinds.MorphTo) {
34
+ throw new Error('MorphTo relations do not support the standard join condition builder');
35
+ }
36
+
37
+ const targetTable = targetTableName ?? relation.target.name;
38
+
39
+ switch (relation.type) {
40
+ case RelationKinds.HasMany:
41
+ case RelationKinds.HasOne: {
42
+ const defaultLocalKey = findPrimaryKey(root);
43
+ const localKey = relation.localKey || defaultLocalKey;
44
+ return eq(
45
+ { type: 'Column', table: targetTable, name: relation.foreignKey },
46
+ { type: 'Column', table: rootTable, name: localKey }
47
+ );
48
+ }
49
+ case RelationKinds.BelongsTo: {
50
+ const defaultLocalKey = findPrimaryKey(relation.target);
51
+ const localKey = relation.localKey || defaultLocalKey;
52
+ return eq(
53
+ { type: 'Column', table: targetTable, name: localKey },
54
+ { type: 'Column', table: rootTable, name: relation.foreignKey }
55
+ );
56
+ }
57
+ case RelationKinds.BelongsToMany:
58
+ throw new Error('BelongsToMany relations do not support the standard join condition builder');
59
+ case RelationKinds.MorphOne:
60
+ case RelationKinds.MorphMany: {
61
+ const morphRel = relation as MorphOneRelation | MorphManyRelation;
62
+ const morphLocalKey = morphRel.localKey || findPrimaryKey(root);
63
+ const baseCondition = eq(
64
+ { type: 'Column', table: targetTable, name: morphRel.idField },
65
+ { type: 'Column', table: rootTable, name: morphLocalKey }
66
+ );
67
+ const discriminatorCondition = eq(
68
+ { type: 'Column', table: targetTable, name: morphRel.typeField },
69
+ { type: 'Literal', value: morphRel.typeValue }
70
+ );
71
+ return and(baseCondition, discriminatorCondition);
72
+ }
73
+ default:
74
+ return assertNever(relation);
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Builds the join nodes required to include a BelongsToMany relation.
80
+ * @param root - The root table definition
81
+ * @param relationName - Name of the relation being joined
82
+ * @param relation - The BelongsToMany relation definition
83
+ * @param joinKind - The type of join to perform
84
+ * @param extra - Optional additional conditions for the target join
85
+ * @param rootAlias - Optional alias for the root table
86
+ * @returns Array of join nodes for the pivot and target tables
87
+ */
88
+ export const buildBelongsToManyJoins = (
89
+ root: TableDef,
90
+ relationName: string,
91
+ relation: BelongsToManyRelation,
92
+ joinKind: JoinKind,
93
+ extra?: ExpressionNode,
94
+ rootAlias?: string,
95
+ targetTable?: TableSourceNode,
96
+ targetTableName?: string
97
+ ): JoinNode[] => {
98
+ const rootKey = relation.localKey || findPrimaryKey(root);
99
+ const targetKey = relation.targetKey || findPrimaryKey(relation.target);
100
+ const rootTable = rootAlias || root.name;
101
+
102
+ const pivotCondition = eq(
103
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
104
+ { type: 'Column', table: rootTable, name: rootKey }
105
+ );
106
+
107
+ const pivotJoin = createJoinNode(
108
+ joinKind,
109
+ { type: 'Table', name: relation.pivotTable.name, schema: relation.pivotTable.schema },
110
+ pivotCondition
111
+ );
112
+
113
+ const targetSource: TableSourceNode = targetTable ?? {
114
+ type: 'Table',
115
+ name: relation.target.name,
116
+ schema: relation.target.schema
117
+ };
118
+ const effectiveTargetName = targetTableName ?? relation.target.name;
119
+ let targetCondition: ExpressionNode = eq(
120
+ { type: 'Column', table: effectiveTargetName, name: targetKey },
121
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
122
+ );
123
+
124
+ if (extra) {
125
+ targetCondition = and(targetCondition, extra);
126
+ }
127
+
128
+ const targetJoin = createJoinNode(
129
+ joinKind,
130
+ targetSource,
131
+ targetCondition,
132
+ relationName
133
+ );
134
+
135
+ return [pivotJoin, targetJoin];
136
+ };
137
+
138
+ /**
139
+ * Builds a relation join condition with optional extra conditions
140
+ * @param root - Root table definition
141
+ * @param relation - Relation definition
142
+ * @param extra - Optional additional expression to combine with AND
143
+ * @param rootAlias - Optional alias for the root table
144
+ * @returns Expression node representing the complete join condition
145
+ */
146
+ export const buildRelationJoinCondition = (
147
+ root: TableDef,
148
+ relation: RelationDef,
149
+ extra?: ExpressionNode,
150
+ rootAlias?: string,
151
+ targetTableName?: string
152
+ ): ExpressionNode => {
153
+ const base = baseRelationCondition(root, relation, rootAlias, targetTableName);
154
+ return extra ? and(base, extra) : base;
155
+ };
156
+
157
+ /**
158
+ * Builds a relation correlation condition for subqueries
159
+ * @param root - Root table definition
160
+ * @param relation - Relation definition
161
+ * @param rootAlias - Optional alias for the root table
162
+ * @returns Expression node representing the correlation condition
163
+ */
164
+ export const buildRelationCorrelation = (
165
+ root: TableDef,
166
+ relation: RelationDef,
167
+ rootAlias?: string,
168
+ targetTableName?: string
169
+ ): ExpressionNode => {
170
+ return baseRelationCondition(root, relation, rootAlias, targetTableName);
171
+ };
@@ -1,5 +1,5 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { RelationDef } from '../schema/relation.js';
2
+ import { RelationDef, isSingleTargetRelation } from '../schema/relation.js';
3
3
  import { ColumnNode, ExpressionNode } from '../core/ast/expression.js';
4
4
  import { SelectQueryNode, TableNode } from '../core/ast/query.js';
5
5
  import { SelectQueryState } from './select-query-state.js';
@@ -22,6 +22,10 @@ export class RelationCteBuilder {
22
22
  throw new Error('Unable to build filter CTE without predicates.');
23
23
  }
24
24
 
25
+ if (!isSingleTargetRelation(relation)) {
26
+ throw new Error('Polymorphic MorphTo relations do not support filter CTEs');
27
+ }
28
+
25
29
  const columns: ColumnNode[] = Object.keys(relation.target.columns).map(name => ({
26
30
  type: 'Column',
27
31
  table: relation.target.name,
@@ -65,12 +65,15 @@ const collectFromExpression = (expr: ExpressionNode, collector: FilterTableColle
65
65
  collectFromOperand(expr.left, collector);
66
66
  collectFromOperand(expr.right, collector);
67
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;
68
+ case 'LogicalExpression':
69
+ expr.operands.forEach(operand => collectFromExpression(operand, collector));
70
+ break;
71
+ case 'NotExpression':
72
+ collectFromExpression(expr.operand, collector);
73
+ break;
74
+ case 'NullExpression':
75
+ collectFromOperand(expr.left, collector);
76
+ break;
74
77
  case 'InExpression':
75
78
  collectFromOperand(expr.left, collector);
76
79
  if (Array.isArray(expr.right)) {
@@ -6,7 +6,10 @@ import {
6
6
  BelongsToManyRelation,
7
7
  HasManyRelation,
8
8
  HasOneRelation,
9
- BelongsToRelation
9
+ BelongsToRelation,
10
+ MorphOneRelation,
11
+ MorphManyRelation,
12
+ isSingleTargetRelation
10
13
  } from '../schema/relation.js';
11
14
  import { ColumnNode } from '../core/ast/expression.js';
12
15
  import { SelectQueryState } from './select-query-state.js';
@@ -61,6 +64,9 @@ const buildTypedSelection = (
61
64
  };
62
65
 
63
66
  const resolveTargetColumns = (relation: RelationDef, options?: RelationIncludeOptions): string[] => {
67
+ if (!isSingleTargetRelation(relation)) {
68
+ return [];
69
+ }
64
70
  const requestedColumns = options?.columns?.length
65
71
  ? [...options.columns]
66
72
  : Object.keys(relation.target.columns);
@@ -180,9 +186,45 @@ const belongsToManyStrategy: IncludeStrategy = context => {
180
186
  return { state, hydration };
181
187
  };
182
188
 
189
+ const morphIncludeStrategy: IncludeStrategy = context => {
190
+ let { state, hydration } = context;
191
+
192
+ const relation = context.relation as MorphOneRelation | MorphManyRelation;
193
+ const targetColumns = resolveTargetColumns(relation, context.options);
194
+ const tableOverride = getJoinCorrelationName(state, context.relationName, relation.target.name);
195
+ const targetSelection = buildTypedSelection(
196
+ relation.target.columns as Record<string, ColumnDef>,
197
+ context.aliasPrefix,
198
+ targetColumns,
199
+ key => `Column '${key}' not found on relation '${context.relationName}'`,
200
+ tableOverride
201
+ );
202
+
203
+ const relationSelectionResult = context.selectColumns(state, hydration, targetSelection);
204
+ state = relationSelectionResult.state;
205
+ hydration = relationSelectionResult.hydration;
206
+
207
+ hydration = hydration.onRelationIncluded(
208
+ state,
209
+ relation,
210
+ context.relationName,
211
+ context.aliasPrefix,
212
+ targetColumns
213
+ );
214
+
215
+ return { state, hydration };
216
+ };
217
+
218
+ const morphToIncludeStrategy: IncludeStrategy = () => {
219
+ throw new Error('MorphTo relations do not support JOIN-based include. Use lazy loading instead.');
220
+ };
221
+
183
222
  export const relationIncludeStrategies: Record<RelationDef['type'], IncludeStrategy> = {
184
223
  [RelationKinds.HasMany]: standardIncludeStrategy,
185
224
  [RelationKinds.HasOne]: standardIncludeStrategy,
186
225
  [RelationKinds.BelongsTo]: standardIncludeStrategy,
187
- [RelationKinds.BelongsToMany]: belongsToManyStrategy
226
+ [RelationKinds.BelongsToMany]: belongsToManyStrategy,
227
+ [RelationKinds.MorphOne]: morphIncludeStrategy,
228
+ [RelationKinds.MorphMany]: morphIncludeStrategy,
229
+ [RelationKinds.MorphTo]: morphToIncludeStrategy
188
230
  };
@@ -3,7 +3,7 @@ import type { JoinNode } from '../core/ast/join.js';
3
3
  import { createJoinNode } from '../core/ast/join-node.js';
4
4
  import type { TableSourceNode } from '../core/ast/query.js';
5
5
  import { JoinKind } from '../core/sql/sql.js';
6
- import { RelationDef, RelationKinds, type BelongsToManyRelation } from '../schema/relation.js';
6
+ import { RelationDef, RelationKinds, type BelongsToManyRelation, isSingleTargetRelation } from '../schema/relation.js';
7
7
  import type { TableDef } from '../schema/table.js';
8
8
  import { findPrimaryKey } from './hydration-planner.js';
9
9
  import { buildBelongsToManyJoins, buildRelationJoinCondition } from './relation-conditions.js';
@@ -64,6 +64,10 @@ export const addRelationJoin = (params: AddRelationJoinParams): SelectQueryState
64
64
  return joins.reduce((curr, join) => curr.withJoin(join), state);
65
65
  }
66
66
 
67
+ if (!isSingleTargetRelation(relation)) {
68
+ throw new Error('Polymorphic MorphTo relations do not support join-based strategies');
69
+ }
70
+
67
71
  let targetSource: TableSourceNode = tableSource ?? {
68
72
  type: 'Table',
69
73
  name: relation.target.name,
@@ -89,6 +93,9 @@ type UpdateRelationJoinParams = {
89
93
  export const updateRelationJoin = (params: UpdateRelationJoinParams): JoinNode[] => {
90
94
  const { joins, joinIndex, relation, currentTable, currentAlias, options } = params;
91
95
  const join = joins[joinIndex];
96
+ if (!isSingleTargetRelation(relation)) {
97
+ throw new Error('Polymorphic MorphTo relations do not support join updates');
98
+ }
92
99
  const targetName = resolveTargetTableName(join.table, relation.target.name);
93
100
  const extra = remapExpressionTable(options.filter, relation.target.name, targetName);
94
101