forked from typeorm/typeorm
/
ManyToManySubjectBuilder.ts
243 lines (200 loc) · 12.5 KB
/
ManyToManySubjectBuilder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import {Subject} from "../Subject";
import {OrmUtils} from "../../util/OrmUtils";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* Builds operations needs to be executed for many-to-many relations of the given subjects.
*
* by example: post contains owner many-to-many relation with categories in the property called "categories", e.g.
* @ManyToMany(type => Category, category => category.posts) categories: Category[]
* If user adds categories into the post and saves post we need to bind them.
* This operation requires updation of junction table.
*/
export class ManyToManySubjectBuilder {
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(protected subjects: Subject[]) {
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
/**
* Builds operations for any changes in the many-to-many relations of the subjects.
*/
build(): void {
this.subjects.forEach(subject => {
// if subject doesn't have entity then no need to find something that should be inserted or removed
if (!subject.entity)
return;
// go through all persistence enabled many-to-many relations and build subject operations for them
subject.metadata.manyToManyRelations.forEach(relation => {
// skip relations for which persistence is disabled
if (relation.persistenceEnabled === false)
return;
this.buildForSubjectRelation(subject, relation);
});
});
}
/**
* Builds operations for removal of all many-to-many records of all many-to-many relations of the given subject.
*/
buildForAllRemoval(subject: Subject) {
// if subject does not have a database entity then it means it does not exist in the database
// if it does not exist in the database then we don't have anything for deletion
if (!subject.databaseEntity)
return;
// go through all persistence enabled many-to-many relations and build subject operations for them
subject.metadata.manyToManyRelations.forEach(relation => {
// skip relations for which persistence is disabled
if (relation.persistenceEnabled === false)
return;
// get all related entities (actually related entity relation ids) bind to this subject entity
// by example: returns category ids of the post we are currently working with (subject.entity is post)
const relatedEntityRelationIdsInDatabase: ObjectLiteral[] = relation.getEntityValue(subject.databaseEntity!);
// go through all related entities and create a new junction subject for each row in junction table
relatedEntityRelationIdsInDatabase.forEach(relationId => {
const junctionSubject = new Subject({
metadata: relation.junctionEntityMetadata!,
parentSubject: subject,
mustBeRemoved: true,
identifier: this.buildJunctionIdentifier(subject, relation, relationId)
});
// we use unshift because we need to perform those operations before post deletion is performed
// but post deletion was already added as an subject
// this is temporary solution, later we need to implement proper sorting of subjects before their removal
this.subjects.push(junctionSubject);
});
});
}
// ---------------------------------------------------------------------
// Protected Methods
// ---------------------------------------------------------------------
/**
* Builds operations for a given subject and relation.
*
* by example: subject is "post" entity we are saving here and relation is "categories" inside it here.
*/
protected buildForSubjectRelation(subject: Subject, relation: RelationMetadata) {
// load from db all relation ids of inverse entities that are "bind" to the subject's entity
// this way we gonna check which relation ids are missing and which are new (e.g. inserted or removed)
let databaseRelatedEntityIds: ObjectLiteral[] = [];
// if subject don't have database entity it means all related entities in persisted subject are new and must be bind
// and we don't need to remove something that is not exist
if (subject.databaseEntity)
databaseRelatedEntityIds = relation.getEntityValue(subject.databaseEntity);
// extract entity's relation value
// by example: categories inside our post (subject.entity is post)
let relatedEntities: ObjectLiteral[] = relation.getEntityValue(subject.entity!);
if (relatedEntities === null) // if value set to null its equal if we set it to empty array - all items must be removed from the database
relatedEntities = [];
if (!(Array.isArray(relatedEntities)))
return;
// from all related entities find only those which aren't found in the db - for them we will create operation subjects
relatedEntities.forEach(relatedEntity => { // by example: relatedEntity is category from categories saved with post
// todo: check how it will work for entities which are saved by cascades, but aren't saved in the database yet
// extract only relation id from the related entities, since we only need it for comparision
// by example: extract from category only relation id (category id, or let's say category title, depend on join column options)
let relatedEntityRelationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity);
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
const relatedEntitySubject = this.subjects.find(subject => {
return subject.entity === relatedEntity;
});
// if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
if (relatedEntitySubject)
relatedEntityRelationIdMap = relatedEntitySubject.identifier;
// if related entity relation id map is empty it means related entity is newly persisted
if (!relatedEntityRelationIdMap) {
// we decided to remove this error because it brings complications when saving object with non-saved entities
// if related entity does not have a subject then it means user tries to bind entity which wasn't saved
// in this persistence because he didn't pass this entity for save or he did not set cascades
// but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
// we decided to remove this error because it brings complications when saving object with non-saved entities
// if (!relatedEntitySubject)
// throw new Error(`Many-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
// `entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
// `Please setup cascade insertion or save entities before binding it.`);
if (!relatedEntitySubject)
return;
}
// try to find related entity in the database
// by example: find post's category in the database post's categories
const relatedEntityExistInDatabase = databaseRelatedEntityIds.find(databaseRelatedEntityRelationId => {
return OrmUtils.compareIds(databaseRelatedEntityRelationId, relatedEntityRelationIdMap);
});
// if entity is found then don't do anything - it means binding in junction table already exist, we don't need to add anything
if (relatedEntityExistInDatabase)
return;
const ownerValue = relation.isOwning ? subject : (relatedEntitySubject || relatedEntity); // by example: ownerEntityMap is post from subject here
const inverseValue = relation.isOwning ? (relatedEntitySubject || relatedEntity) : subject; // by example: inverseEntityMap is category from categories array here
// create a new subject for insert operation of junction rows
const junctionSubject = new Subject({
metadata: relation.junctionEntityMetadata!,
parentSubject: subject,
canBeInserted: true,
});
this.subjects.push(junctionSubject);
relation.junctionEntityMetadata!.ownerColumns.forEach(column => {
junctionSubject.changeMaps.push({
column: column,
value: ownerValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(ownerEntityMap),
});
});
relation.junctionEntityMetadata!.inverseColumns.forEach(column => {
junctionSubject.changeMaps.push({
column: column,
value: inverseValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(inverseEntityMap),
});
});
});
// get all inverse entities relation ids that are "bind" to the currently persisted entity
const changedInverseEntityRelationIds: ObjectLiteral[] = [];
relatedEntities.forEach(relatedEntity => {
// relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity)
let relatedEntityRelationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity);
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
const relatedEntitySubject = this.subjects.find(subject => {
return subject.entity === relatedEntity;
});
// if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
if (relatedEntitySubject)
relatedEntityRelationIdMap = relatedEntitySubject.identifier;
if (relatedEntityRelationIdMap !== undefined && relatedEntityRelationIdMap !== null)
changedInverseEntityRelationIds.push(relatedEntityRelationIdMap);
});
// now from all entities in the persisted entity find only those which aren't found in the db
const removedJunctionEntityIds = databaseRelatedEntityIds.filter(existRelationId => {
return !changedInverseEntityRelationIds.find(changedRelationId => {
return OrmUtils.compareIds(changedRelationId, existRelationId);
});
});
// finally create a new junction remove operations for missing related entities
removedJunctionEntityIds.forEach(removedEntityRelationId => {
const junctionSubject = new Subject({
metadata: relation.junctionEntityMetadata!,
parentSubject: subject,
mustBeRemoved: true,
identifier: this.buildJunctionIdentifier(subject, relation, removedEntityRelationId)
});
this.subjects.push(junctionSubject);
});
}
/**
* Creates identifiers for junction table.
* Example: { postId: 1, categoryId: 2 }
*/
protected buildJunctionIdentifier(subject: Subject, relation: RelationMetadata, relationId: ObjectLiteral) {
const ownerEntityMap = relation.isOwning ? subject.entity! : relationId;
const inverseEntityMap = relation.isOwning ? relationId : subject.entity!;
const identifier: ObjectLiteral = {};
relation.junctionEntityMetadata!.ownerColumns.forEach(column => {
OrmUtils.mergeDeep(identifier, column.createValueMap(column.referencedColumn!.getEntityValue(ownerEntityMap)));
});
relation.junctionEntityMetadata!.inverseColumns.forEach(column => {
OrmUtils.mergeDeep(identifier, column.createValueMap(column.referencedColumn!.getEntityValue(inverseEntityMap)));
});
return identifier;
}
}