@entity-access/entity-access 1.0.251 → 1.0.253

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 (102) hide show
  1. package/.vscode/launch.json +2 -1
  2. package/dist/common/symbols/symbols.d.ts +1 -0
  3. package/dist/common/symbols/symbols.d.ts.map +1 -1
  4. package/dist/common/symbols/symbols.js +1 -0
  5. package/dist/common/symbols/symbols.js.map +1 -1
  6. package/dist/compiler/QueryCompiler.d.ts +3 -3
  7. package/dist/decorators/ForeignKey.d.ts +8 -7
  8. package/dist/decorators/ForeignKey.d.ts.map +1 -1
  9. package/dist/decorators/ForeignKey.js +43 -8
  10. package/dist/decorators/ForeignKey.js.map +1 -1
  11. package/dist/decorators/IColumn.d.ts +6 -3
  12. package/dist/decorators/IColumn.d.ts.map +1 -1
  13. package/dist/decorators/Relate.d.ts.map +1 -1
  14. package/dist/decorators/Relate.js +8 -6
  15. package/dist/decorators/Relate.js.map +1 -1
  16. package/dist/entity-query/EntityType.d.ts +5 -1
  17. package/dist/entity-query/EntityType.d.ts.map +1 -1
  18. package/dist/entity-query/EntityType.js +57 -25
  19. package/dist/entity-query/EntityType.js.map +1 -1
  20. package/dist/migrations/postgres/PostgresAutomaticMigrations.js +1 -1
  21. package/dist/migrations/postgres/PostgresAutomaticMigrations.js.map +1 -1
  22. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js +1 -1
  23. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js.map +1 -1
  24. package/dist/model/EntityModel.js +2 -2
  25. package/dist/model/EntityModel.js.map +1 -1
  26. package/dist/model/EntitySource.d.ts +6 -1
  27. package/dist/model/EntitySource.d.ts.map +1 -1
  28. package/dist/model/EntitySource.js.map +1 -1
  29. package/dist/model/SourceExpression.d.ts +1 -22
  30. package/dist/model/SourceExpression.d.ts.map +1 -1
  31. package/dist/model/SourceExpression.js +116 -98
  32. package/dist/model/SourceExpression.js.map +1 -1
  33. package/dist/model/changes/ChangeEntry.d.ts.map +1 -1
  34. package/dist/model/changes/ChangeEntry.js +62 -25
  35. package/dist/model/changes/ChangeEntry.js.map +1 -1
  36. package/dist/model/changes/ChangeSet.d.ts +2 -1
  37. package/dist/model/changes/ChangeSet.d.ts.map +1 -1
  38. package/dist/model/changes/ChangeSet.js +4 -3
  39. package/dist/model/changes/ChangeSet.js.map +1 -1
  40. package/dist/model/identity/IdentityMap.d.ts +23 -0
  41. package/dist/model/identity/IdentityMap.d.ts.map +1 -0
  42. package/dist/model/identity/IdentityMap.js +113 -0
  43. package/dist/model/identity/IdentityMap.js.map +1 -0
  44. package/dist/model/identity/RelationMapper.d.ts +2 -3
  45. package/dist/model/identity/RelationMapper.d.ts.map +1 -1
  46. package/dist/model/identity/RelationMapper.js +60 -27
  47. package/dist/model/identity/RelationMapper.js.map +1 -1
  48. package/dist/model/identity/SearchIndex.d.ts +17 -0
  49. package/dist/model/identity/SearchIndex.d.ts.map +1 -0
  50. package/dist/model/identity/SearchIndex.js +109 -0
  51. package/dist/model/identity/SearchIndex.js.map +1 -0
  52. package/dist/model/verification/VerificationSession.d.ts +1 -1
  53. package/dist/model/verification/VerificationSession.d.ts.map +1 -1
  54. package/dist/model/verification/VerificationSession.js +18 -16
  55. package/dist/model/verification/VerificationSession.js.map +1 -1
  56. package/dist/query/ast/ExpressionToSql.d.ts.map +1 -1
  57. package/dist/query/ast/ExpressionToSql.js +74 -52
  58. package/dist/query/ast/ExpressionToSql.js.map +1 -1
  59. package/dist/query/expander/QueryExpander.d.ts.map +1 -1
  60. package/dist/query/expander/QueryExpander.js +41 -10
  61. package/dist/query/expander/QueryExpander.js.map +1 -1
  62. package/dist/query/parser/ArrowToExpression.d.ts +16 -5
  63. package/dist/query/parser/ArrowToExpression.d.ts.map +1 -1
  64. package/dist/query/parser/ArrowToExpression.js +45 -25
  65. package/dist/query/parser/ArrowToExpression.js.map +1 -1
  66. package/dist/tests/db-tests/tests/multi-fk-tests.d.ts +3 -0
  67. package/dist/tests/db-tests/tests/multi-fk-tests.d.ts.map +1 -0
  68. package/dist/tests/db-tests/tests/multi-fk-tests.js +38 -0
  69. package/dist/tests/db-tests/tests/multi-fk-tests.js.map +1 -0
  70. package/dist/tests/expressions/left-joins/child-joins.js +7 -7
  71. package/dist/tests/model/ShoppingContext.d.ts +9 -0
  72. package/dist/tests/model/ShoppingContext.d.ts.map +1 -1
  73. package/dist/tests/model/ShoppingContext.js +34 -0
  74. package/dist/tests/model/ShoppingContext.js.map +1 -1
  75. package/dist/tests/security/tests/include-items.d.ts.map +1 -1
  76. package/dist/tests/security/tests/include-items.js +1 -0
  77. package/dist/tests/security/tests/include-items.js.map +1 -1
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +1 -1
  80. package/src/common/symbols/symbols.ts +2 -1
  81. package/src/decorators/ForeignKey.ts +66 -28
  82. package/src/decorators/IColumn.ts +4 -3
  83. package/src/decorators/Relate.ts +8 -6
  84. package/src/entity-query/EntityType.ts +60 -26
  85. package/src/migrations/postgres/PostgresAutomaticMigrations.ts +1 -1
  86. package/src/migrations/sql-server/SqlServerAutomaticMigrations.ts +1 -1
  87. package/src/model/EntityModel.ts +2 -2
  88. package/src/model/EntitySource.ts +6 -1
  89. package/src/model/SourceExpression.ts +132 -132
  90. package/src/model/changes/ChangeEntry.ts +68 -25
  91. package/src/model/changes/ChangeSet.ts +4 -3
  92. package/src/model/identity/IdentityMap.ts +126 -0
  93. package/src/model/identity/RelationMapper.ts +71 -27
  94. package/src/model/identity/SearchIndex.ts +120 -0
  95. package/src/model/verification/VerificationSession.ts +19 -16
  96. package/src/query/ast/ExpressionToSql.ts +77 -61
  97. package/src/query/expander/QueryExpander.ts +52 -28
  98. package/src/query/parser/ArrowToExpression.ts +50 -26
  99. package/src/tests/db-tests/tests/multi-fk-tests.ts +46 -0
  100. package/src/tests/expressions/left-joins/child-joins.ts +7 -7
  101. package/src/tests/model/ShoppingContext.ts +32 -0
  102. package/src/tests/security/tests/include-items.ts +1 -0
@@ -0,0 +1,126 @@
1
+ import { entityTypeSymbol } from "../../common/symbols/symbols.js";
2
+ import { IColumn } from "../../decorators/IColumn.js";
3
+ import type EntityType from "../../entity-query/EntityType.js";
4
+
5
+ /**
6
+ * Locally cache uniquely identifiable entities
7
+ */
8
+ export default class IdentityMap {
9
+
10
+ public get indexedColumns() {
11
+ return this.keys.keys();
12
+ }
13
+
14
+ private map = new Map<string, any>();
15
+
16
+ private keys = new Map<IColumn, Map<any, any[]>>();
17
+
18
+ public delete(jsonKey) {
19
+ const item = this.map.get(jsonKey);
20
+ this.map.delete(jsonKey);
21
+ if (item) {
22
+ const type = item[entityTypeSymbol] as EntityType;
23
+ for (const column of type.columns) {
24
+ const values = this.keys.get(column);
25
+ if (!values) {
26
+ continue;
27
+ }
28
+ const value = item[column.name];
29
+ if (value === void 0 || value === null) {
30
+ continue;
31
+ }
32
+ const entries = values.get(value);
33
+ if (!entries) {
34
+ continue;
35
+ }
36
+ const index = entries.findIndex(item);
37
+ entries.splice(index, 1);
38
+ }
39
+ }
40
+ }
41
+
42
+ public get(jsonKeys) {
43
+ return this.map.get(jsonKeys);
44
+ }
45
+
46
+ public set(jsonKey, entity, type: EntityType) {
47
+ entity[entityTypeSymbol] = type;
48
+ this.map.set(jsonKey, entity);
49
+ this.updateSearchIndex(type, entity);
50
+ }
51
+
52
+ public clear() {
53
+ this.map.clear();
54
+ this.keys.clear();
55
+ }
56
+
57
+ public build(key: IColumn) {
58
+ return this.getKeyEntry(key, true);
59
+ }
60
+
61
+ searchByKeys(pairs: { key: IColumn, value}[], create = true) {
62
+ let results: any[];
63
+ for (const { key, value } of pairs) {
64
+ const items = this.getAll(key, value, create);
65
+ if (!items?.length) {
66
+ return;
67
+ }
68
+ if (!results) {
69
+ results = [].concat(items);
70
+ continue;
71
+ }
72
+ const old = results;
73
+ results = [];
74
+ for (const item of items) {
75
+ if (old.includes(item)) {
76
+ results.push(item);
77
+ }
78
+ }
79
+ }
80
+ return results[0];
81
+ }
82
+
83
+ private getAll(key: IColumn, value: any, create = true) {
84
+ const keyEntry = this.getKeyEntry(key, create);
85
+ return keyEntry.get(value);
86
+ }
87
+
88
+ private getKeyEntry(key: IColumn, create = false) {
89
+ let keyEntry = this.keys.get(key);
90
+ if (keyEntry) {
91
+ return keyEntry;
92
+ }
93
+ if (!create) {
94
+ return;
95
+ }
96
+ keyEntry = new Map<any, any[]>();
97
+ this.keys.set(key, keyEntry);
98
+ for (const entry of this.map.values()) {
99
+ this.updateSearchIndex(entry[entityTypeSymbol], entry);
100
+ }
101
+ return keyEntry;
102
+ }
103
+
104
+ private updateSearchIndex(type: EntityType, entity: any) {
105
+ for (const key of this.keys.keys()) {
106
+ if (type.getField(key.name) !== key) {
107
+ continue;
108
+ }
109
+ const keyEntry = this.getKeyEntry(key, true);
110
+ const value = entity[key.name];
111
+ if (value === void 0 || value === null) {
112
+ continue;
113
+ }
114
+ let values = keyEntry.get(value);
115
+ if (!values) {
116
+ values = [];
117
+ keyEntry.set(value, values);
118
+ }
119
+ if (values.includes(entity)) {
120
+ continue;
121
+ }
122
+ values.push(entity);
123
+ }
124
+ }
125
+
126
+ }
@@ -1,26 +1,33 @@
1
+ import EventEmitter from "events";
1
2
  import type ChangeEntry from "../changes/ChangeEntry.js";
2
3
  import type ChangeSet from "../changes/ChangeSet.js";
3
4
  import IdentityService, { identityMapSymbol } from "./IdentityService.js";
5
+ import { IColumn } from "../../decorators/IColumn.js";
6
+
7
+
8
+
4
9
 
5
10
  export default class RelationMapper {
6
11
 
7
- private map: Map<string, ChangeEntry[]> = new Map();
12
+ // private map: Map<string, ChangeEntry[]> = new Map();
13
+
14
+ private events: EventEmitter = new EventEmitter();
8
15
 
9
16
  constructor(
10
17
  private changeSet: ChangeSet,
11
- private identityMap: Map<string, ChangeEntry> = changeSet[identityMapSymbol]
18
+ private identityMap = changeSet[identityMapSymbol]
12
19
  ) {
13
20
 
14
21
  }
15
22
 
16
- push(id: string, waiter: ChangeEntry) {
17
- let queue = this.map.get(id);
18
- if (!queue) {
19
- queue = [];
20
- this.map.set(id, queue);
21
- }
22
- queue.push(waiter);
23
- }
23
+ // push(id: string, waiter: ChangeEntry) {
24
+ // let queue = this.map.get(id);
25
+ // if (!queue) {
26
+ // queue = [];
27
+ // this.map.set(id, queue);
28
+ // }
29
+ // queue.push(waiter);
30
+ // }
24
31
 
25
32
  fix(entry: ChangeEntry, nest = true) {
26
33
 
@@ -30,23 +37,48 @@ export default class RelationMapper {
30
37
  if (iterator.isInverseRelation) {
31
38
  continue;
32
39
  }
33
- const fkColumn = iterator.fkColumn.name;
34
- const fkValue = entity[fkColumn];
35
- if (fkValue === void 0) {
36
- continue;
37
- }
40
+ // const fkColumn = iterator.fkColumn.name;
41
+ // const fkValue = entity[fkColumn];
42
+ // if (fkValue === void 0) {
43
+ // continue;
44
+ // }
45
+
38
46
  // get from identity...
39
- const id = IdentityService.buildIdentity(iterator.relatedEntity, fkValue);
40
- const parent = this.identityMap.get(id);
47
+ // const id = IdentityService.buildIdentity(iterator.relatedEntity, fkValue);
48
+ // const parent = this.identityMap.get(id);
49
+ // if (!parent) {
50
+ // let waiters = this.map.get(id);
51
+ // if (!waiters) {
52
+ // waiters = [];
53
+ // this.map.set(id, waiters);
54
+ // }
55
+ // waiters.push(entry);
56
+ // continue;
57
+ // }
58
+
59
+ const pairs = [] as { key: IColumn, value: any}[];
60
+
61
+ for (const { fkColumn, relatedKeyColumn } of iterator.fkMap) {
62
+ this.identityMap.build(relatedKeyColumn);
63
+ const fkValue = entity[fkColumn.name];
64
+ if (fkValue === void 0) {
65
+ continue;
66
+ }
67
+ pairs.push({ key: relatedKeyColumn, value: fkValue});
68
+ }
69
+
70
+ const parent = this.identityMap.searchByKeys(pairs, true);
41
71
  if (!parent) {
42
- let waiters = this.map.get(id);
43
- if (!waiters) {
44
- waiters = [];
45
- this.map.set(id, waiters);
72
+ if (nest) {
73
+ for (const { key, value } of pairs) {
74
+ this.events.once(`${key.entityType.name}-${key.name}-${value}`, (k) => {
75
+ this.fix(entry, false);
76
+ });
77
+ }
46
78
  }
47
- waiters.push(entry);
48
79
  continue;
49
80
  }
81
+
50
82
  entity[iterator.name] = parent;
51
83
 
52
84
  if (iterator.relatedRelation.isCollection) {
@@ -64,12 +96,24 @@ export default class RelationMapper {
64
96
  }
65
97
 
66
98
  // see if anyone is waiting for us or not...
67
- const identity = IdentityService.getIdentity(entry.type, entry.entity);
68
- const pending = this.map.get(identity);
69
- if (pending && pending.length) {
70
- for (const iterator of pending) {
71
- this.fix(iterator, false);
99
+ // const identity = IdentityService.getIdentity(entry.type, entry.entity);
100
+ // const pending = this.map.get(identity);
101
+ // if (pending && pending.length) {
102
+ // for (const iterator of pending) {
103
+ // this.fix(iterator, false);
104
+ // }
105
+ // }
106
+
107
+ for (const iterator of this.identityMap.indexedColumns) {
108
+ if (iterator.entityType !== entry.type) {
109
+ continue;
110
+ }
111
+ const value = entry.entity[iterator.name];
112
+ if (value === void 0 || value === null) {
113
+ continue;
72
114
  }
115
+ const key = `${iterator.entityType.name}-${iterator.name}-${value}`;
116
+ this.events.emit(key, key);
73
117
  }
74
118
  }
75
119
  }
@@ -0,0 +1,120 @@
1
+ import type { IColumn } from "../../decorators/IColumn.js";
2
+ import type ChangeEntry from "../changes/ChangeEntry.js";
3
+
4
+ export default class SearchIndex {
5
+
6
+ private keys = new Map<string, Map<any, any[]>>();
7
+
8
+ constructor(private entries: ChangeEntry[]) {
9
+
10
+ }
11
+
12
+ getByKeys(pairs: { key, value}[], create = true) {
13
+ let results: any[];
14
+ for (const { key, value } of pairs) {
15
+ const items = this.getAll(key, value, create);
16
+ if (!items?.length) {
17
+ return;
18
+ }
19
+ if (!results) {
20
+ results = [].concat(items);
21
+ continue;
22
+ }
23
+ const old = results;
24
+ results = [];
25
+ for (const item of items) {
26
+ if (old.includes(item)) {
27
+ results.push(item);
28
+ }
29
+ }
30
+ }
31
+ return results;
32
+ }
33
+
34
+ getAll(key: string, value: any, create = true) {
35
+ const keyEntry = this.getKeyEntry(key, create);
36
+ return keyEntry.get(value);
37
+ }
38
+
39
+ delete(entry) {
40
+ for (const [key,values] of this.keys) {
41
+ const value = entry[key];
42
+ if (value === void 0 || value === null) {
43
+ continue;
44
+ }
45
+ const entries = values.get(value);
46
+ if (!entries) {
47
+ continue;
48
+ }
49
+ const index = entries.findIndex(entry);
50
+ entries.splice(index, 1);
51
+ }
52
+ }
53
+
54
+ get(keys: IColumn[], entry) {
55
+ let results: any[];
56
+ for (const { name } of keys) {
57
+ const items = this.getAll(name, entry[name]);
58
+ if (!items?.length) {
59
+ return;
60
+ }
61
+ if (!results) {
62
+ results = [].concat(items);
63
+ continue;
64
+ }
65
+ const old = results;
66
+ results = [];
67
+ for (const item of items) {
68
+ if (old.includes(item)) {
69
+ results.push(item);
70
+ }
71
+ }
72
+ }
73
+ return results[0];
74
+ }
75
+
76
+ update(keys: IColumn[], entry) {
77
+ for (const key of keys) {
78
+ const keyEntry = this.getKeyEntry(key.name, true);
79
+ const value = entry[key.name];
80
+ if (value === void 0 || value === null) {
81
+ continue;
82
+ }
83
+ let values = keyEntry.get(value);
84
+ if (!values) {
85
+ values = [];
86
+ keyEntry.set(value, values);
87
+ }
88
+ if (values.includes(entry)) {
89
+ continue;
90
+ }
91
+ values.push(entry);
92
+ }
93
+ }
94
+
95
+ private getKeyEntry(key: string, create = false) {
96
+ let keyEntry = this.keys.get(key);
97
+ if (keyEntry) {
98
+ return keyEntry;
99
+ }
100
+ if (!create) {
101
+ return;
102
+ }
103
+ keyEntry = new Map<any, any[]>();
104
+ this.keys.set(key, keyEntry);
105
+ for (const entry of this.entries) {
106
+ const value = entry[key];
107
+ if (value === void 0 || value === null) {
108
+ continue;
109
+ }
110
+ let values = keyEntry.get(value);
111
+ if (!values) {
112
+ values = [];
113
+ keyEntry.set(value, values);
114
+ }
115
+ values.push(value);
116
+ }
117
+ return keyEntry;
118
+ }
119
+
120
+ }
@@ -63,30 +63,33 @@ export default class VerificationSession {
63
63
  continue;
64
64
  }
65
65
 
66
- const fk = relation.fkColumn;
66
+ const fk = relation.fkMap;
67
67
  if (!fk) {
68
68
  continue;
69
69
  }
70
70
 
71
- const fkValue = entity[fk.name];
72
- if (fkValue === void 0) {
73
- // not set... ignore..
74
- continue;
75
- }
76
- if (isKeyEmpty(fkValue, relation.fkColumn)) {
77
- continue;
78
- }
71
+ for (const { fkColumn , relatedKeyColumn } of fk) {
79
72
 
80
- // only if it is modified...
81
- if (change.status !== "inserted") {
82
- if (!change.isModified(fk.name)) {
73
+ const fkValue = entity[fkColumn.name];
74
+ if (fkValue === void 0) {
75
+ // not set... ignore..
83
76
  continue;
84
77
  }
78
+ if (isKeyEmpty(fkValue, fkColumn)) {
79
+ continue;
80
+ }
81
+
82
+ // only if it is modified...
83
+ if (change.status !== "inserted") {
84
+ if (!change.isModified(fkColumn.name)) {
85
+ continue;
86
+ }
87
+ }
88
+ this.queueEntityForeignKey(change, relation, fkColumn, relatedKeyColumn, fkValue);
85
89
  }
86
- this.queueEntityForeignKey(change, relation, fkValue);
87
90
  }
88
91
  }
89
- queueEntityForeignKey(change: ChangeEntry, relation: IEntityRelation, value) {
92
+ queueEntityForeignKey(change: ChangeEntry, relation: IEntityRelation, fkColumn, relatedKeyColumn, value) {
90
93
  const relatedModel = relation.relatedEntity;
91
94
  const type = relation.relatedEntity.typeClass;
92
95
  const events = this.context.eventsFor(change.type.typeClass);
@@ -97,7 +100,7 @@ export default class VerificationSession {
97
100
  events: relatedEvents,
98
101
  type: relatedModel,
99
102
  name: relation.name,
100
- fkName: relation.fkColumn.name,
103
+ fkName: fkColumn.name,
101
104
  entity: change.entity
102
105
  });
103
106
  let query = events.onForeignKeyFilter(fk);
@@ -110,7 +113,7 @@ export default class VerificationSession {
110
113
 
111
114
  const eq = query as EntityQuery;
112
115
  const compare = Expression.equal(
113
- Expression.member(eq.selectStatement.sourceParameter, relatedModel.keys[0].columnName),
116
+ Expression.member(eq.selectStatement.sourceParameter, relatedKeyColumn.columnName),
114
117
  Expression.constant(value)
115
118
  );
116
119
  const typeName = TypeInfo.nameOfType(type);
@@ -1,3 +1,4 @@
1
+ import EntityAccessError from "../../common/EntityAccessError.js";
1
2
  import QueryCompiler from "../../compiler/QueryCompiler.js";
2
3
  import EntityType, { IEntityProperty } from "../../entity-query/EntityType.js";
3
4
  import EntityQuery from "../../model/EntityQuery.js";
@@ -225,33 +226,27 @@ export default class ExpressionToSql extends Visitor<ITextQuery> {
225
226
 
226
227
  this.scope.create({ parameter: select.sourceParameter, model: relatedModel, selectStatement: select });
227
228
  select[filteredSymbol] = true;
228
- const targetKey = MemberExpression.create({
229
- target: parameter,
230
- property: Identifier.create({
231
- value: targetType.keys[0].columnName
232
- })
233
- });
234
-
235
- const relatedKey = MemberExpression.create({
236
- target: select.sourceParameter,
237
- property: Identifier.create({
238
- value: relation.relation.fkColumn.columnName
239
- })
240
- });
241
-
242
-
243
- const join = Expression.equal(targetKey, relatedKey);
244
229
 
245
230
  let where = select.where;
246
231
 
247
- if (where) {
248
- where = BinaryExpression.create({
249
- left: select.where,
250
- operator: "AND",
251
- right: join
232
+ for (const { fkColumn, relatedKeyColumn } of relation.relation.fkMap) {
233
+ const targetKey = MemberExpression.create({
234
+ target: parameter,
235
+ property: Identifier.create({
236
+ value: relatedKeyColumn.columnName
237
+ })
252
238
  });
253
- } else {
254
- where = join;
239
+
240
+ const relatedKey = MemberExpression.create({
241
+ target: select.sourceParameter,
242
+ property: Identifier.create({
243
+ value: fkColumn.columnName
244
+ })
245
+ });
246
+ const join = Expression.equal(targetKey, relatedKey);
247
+ where = where
248
+ ? Expression.logicalAnd(where, join)
249
+ : join;
255
250
  }
256
251
 
257
252
  select.where = where;
@@ -282,36 +277,33 @@ export default class ExpressionToSql extends Visitor<ITextQuery> {
282
277
  this.scope.alias(param1, select.sourceParameter, select);
283
278
  select.sourceParameter = param1;
284
279
  select[filteredSymbol] = true;
285
- const targetKey = MemberExpression.create({
286
- target: parameter,
287
- property: Identifier.create({
288
- value: targetType.keys[0].columnName
289
- })
290
- });
291
-
292
- const relatedKey = MemberExpression.create({
293
- target: param1,
294
- property: Identifier.create({
295
- value: relation.relation.fkColumn.columnName
296
- })
297
- });
298
280
 
281
+ let where = select.where;
282
+ where = where
283
+ ? Expression.logicalAnd(where, body.body)
284
+ : body.body;
299
285
 
300
- const join = Expression.logicalAnd(
301
- Expression.equal(targetKey, relatedKey),
302
- body.body
303
- );
286
+ for (const { fkColumn, relatedKeyColumn } of relation.relation.relatedRelation.fkMap) {
304
287
 
305
- let where = select.where;
288
+ const targetKey = MemberExpression.create({
289
+ target: parameter,
290
+ property: Identifier.create({
291
+ value: relatedKeyColumn.columnName
292
+ })
293
+ });
306
294
 
307
- if (where) {
308
- where = BinaryExpression.create({
309
- left: select.where,
310
- operator: "AND",
311
- right: join
295
+ const relatedKey = MemberExpression.create({
296
+ target: param1,
297
+ property: Identifier.create({
298
+ value: fkColumn.columnName
299
+ })
312
300
  });
313
- } else {
314
- where = join;
301
+
302
+ const join = Expression.equal(targetKey, relatedKey);
303
+
304
+ where = where
305
+ ? Expression.logicalAnd(where, join)
306
+ : join;
315
307
  }
316
308
 
317
309
  select.where = where;
@@ -723,16 +715,18 @@ export default class ExpressionToSql extends Visitor<ITextQuery> {
723
715
  }
724
716
  if (relation) {
725
717
 
726
- const { fkColumn } = relation;
727
-
728
718
  if (!relation.isCollection) {
729
719
 
730
- let columnName = fkColumn.columnName;
731
- // for inverse relation, we need to
732
- // use primary key of current model
733
- if (relation.isInverseRelation) {
734
- columnName = peModel.keys[0].columnName;
735
- }
720
+ // let columnName = fkColumn.columnName;
721
+ // // for inverse relation, we need to
722
+ // // use primary key of current model
723
+ // if (relation.isInverseRelation) {
724
+ // columnName = peModel.keys[0].columnName;
725
+ // }
726
+
727
+ const fkMap = relation.fkMap ?? relation.relatedRelation.fkMap;
728
+
729
+ const isNullable = fkMap.some(({ fkColumn }) => fkColumn.nullable);
736
730
 
737
731
  const select = scope?.selectStatement ?? this.source?.selectStatement;
738
732
  if (select) {
@@ -743,20 +737,42 @@ export default class ExpressionToSql extends Visitor<ITextQuery> {
743
737
  this.scope.create({ parameter: join.as as ParameterExpression, model: join.model, selectStatement: select });
744
738
  return join.as;
745
739
  }
746
- const joinType = select.preferLeftJoins ? "LEFT" : (fkColumn.nullable ? "LEFT" : "INNER");
740
+ const joinType = select.preferLeftJoins ? "LEFT" : (isNullable ? "LEFT" : "INNER");
747
741
  const joinParameter = ParameterExpression.create({
748
742
  name: relation.relatedEntity.name[0],
749
743
  model: relation.relatedEntity
750
744
  });
745
+ let where: Expression;
746
+ for (const {fkColumn, relatedKeyColumn} of fkMap) {
747
+ let peColumn;
748
+ let joinColumn;
749
+ if (fkColumn.entityType === pe.model) {
750
+ peColumn = fkColumn;
751
+ } else if (relatedKeyColumn.entityType === pe.model) {
752
+ peColumn = relatedKeyColumn;
753
+ } else {
754
+ throw new EntityAccessError(`Invalid configuration`);
755
+ }
756
+ if (fkColumn.entityType === joinParameter.model) {
757
+ joinColumn = fkColumn;
758
+ } else if (relatedKeyColumn.entityType === joinParameter.model) {
759
+ joinColumn = relatedKeyColumn;
760
+ } else {
761
+ throw new EntityAccessError(`Invalid configuration`);
762
+ }
763
+ const joinOn = Expression.equal(
764
+ Expression.member(pe, peColumn.columnName),
765
+ Expression.member(joinParameter, joinColumn.columnName));
766
+ where = where
767
+ ? Expression.logicalAnd(where, joinOn)
768
+ : joinOn;
769
+ }
751
770
  join = JoinExpression.create({
752
771
  as: joinParameter,
753
772
  joinType,
754
773
  model: joinParameter.model,
755
774
  source: Expression.identifier(relation.relatedEntity.name),
756
- where: Expression.equal(
757
- Expression.member(pe, columnName),
758
- Expression.member(joinParameter, relation.relatedEntity.keys[0].columnName)
759
- )
775
+ where
760
776
  });
761
777
  select.joins.push(join);
762
778
  this.scope.create({ parameter: joinParameter, model: relation.relatedEntity, selectStatement: select});