/
OneToOneInverseSideSubjectBuilder.ts
170 lines (144 loc) · 9.18 KB
/
OneToOneInverseSideSubjectBuilder.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
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 one-to-one non-owner relations of the given subjects.
*
* by example: post contains one-to-one non-owner relation with category in the property called "category", e.g.
* @OneToOne(type => Category, category => category.post) category: Category
* If user sets a category into the post and saves post we need to bind them.
* This operation requires updation of category table since its owner of the relation and contains a join column.
*
* note: this class shares lot of things with OneToManyUpdateBuilder, so when you change this class
* make sure to reflect changes there as well.
*/
export class OneToOneInverseSideSubjectBuilder {
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(protected subjects: Subject[]) {
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
/**
* Builds all required operations.
*/
build(): void {
this.subjects.forEach(subject => {
subject.metadata.oneToOneRelations.forEach(relation => {
// we don't need owning relations, this operation is only for inverse side of one-to-one relations
// skip relations for which persistence is disabled
if (relation.isOwning || relation.persistenceEnabled === false)
return;
this.buildForSubjectRelation(subject, relation);
});
});
}
// ---------------------------------------------------------------------
// Protected Methods
// ---------------------------------------------------------------------
/**
* Builds operations for a given subject and relation.
*
* by example: subject is "post" entity we are saving here and relation is "category" inside it here.
*/
protected buildForSubjectRelation(subject: Subject, relation: RelationMetadata) {
// prepare objects (relation id map) for the database entity
// note: subject.databaseEntity contains relation with loaded relation id only (id map)
// by example: since subject is a post, we are expecting to get post's category saved in the database here,
// particularly its relation id, e.g. category id stored in the database
let relatedEntityDatabaseRelationId: ObjectLiteral|undefined = undefined;
if (subject.databaseEntity) // related entity in the database can exist only if this entity (post) is saved
relatedEntityDatabaseRelationId = relation.getEntityValue(subject.databaseEntity);
// get related entities of persisted entity
// by example: get category from the passed to persist post entity
let relatedEntity: ObjectLiteral|null = relation.getEntityValue(subject.entity!); // by example: relatedEntity is a category here
if (relatedEntity === undefined) // if relation is undefined then nothing to update
return;
// if related entity is null then we need to check if there a bind in the database and unset it
// if there is no bind in the entity then we don't need to do anything
// by example: if post.category = null and category has this post in the database then we unset it
if (relatedEntity === null) {
// it makes sense to update database only there is a previously set value in the database
if (relatedEntityDatabaseRelationId) {
// todo: probably we can improve this in the future by finding entity with column those values,
// todo: maybe it was already in persistence process. This is possible due to unique requirements of join columns
// we create a new subject which operations will be executed in subject operation executor
const removedRelatedEntitySubject = new Subject({
metadata: relation.inverseEntityMetadata,
parentSubject: subject,
canBeUpdated: true,
identifier: relatedEntityDatabaseRelationId,
changeMaps: [{
relation: relation.inverseRelation!,
value: null
}]
});
this.subjects.push(removedRelatedEntitySubject);
}
return;
} // else means entity is bind in the database
// 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 relationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
let relatedEntitySubject = this.subjects.find(operateSubject => {
return !!operateSubject.entity && operateSubject.entity === relatedEntity;
});
// if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
if (relatedEntitySubject)
relationIdMap = relatedEntitySubject.identifier;
// if relationIdMap is undefined then it means user binds object which is not saved in the database yet
// by example: if post contains category which does not have id(s) yet (because its a new category)
// it means its always newly inserted and relation update operation always must be created for it
// it does not make sense to perform difference operation for it for both add and remove actions
if (!relationIdMap) {
// 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
// if (!relatedEntitySubject)
// throw new Error(`One-to-one inverse relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
// `entity which does not exist in the database yet, thus cannot be bind in the database. ` +
// `Please setup cascade insertion or save entity before binding it.`);
if (!relatedEntitySubject)
return;
// okay, so related subject exist and its marked for insertion, then add a new change map
// by example: this will tell category to insert into its post relation our post we are working with
// relatedEntitySubject is newly inserted CategorySubject
// relation.inverseRelation is OneToOne owner relation inside Category
// subject is Post needs to be inserted into Category
relatedEntitySubject.changeMaps.push({
relation: relation.inverseRelation!,
value: subject
});
}
// check if this binding really exist in the database
// by example: find our post if its already bind to category in the database and its not equal to what user tries to set
const areRelatedIdEqualWithDatabase = relatedEntityDatabaseRelationId && OrmUtils.compareIds(relationIdMap, relatedEntityDatabaseRelationId);
// if they aren't equal it means its a new relation and we need to "bind" them
// by example: this will tell category to insert into its post relation our post we are working with
// relatedEntitySubject is newly inserted CategorySubject
// relation.inverseRelation is ManyToOne relation inside Category
// subject is Post needs to be inserted into Category
if (!areRelatedIdEqualWithDatabase) {
// if there is no relatedEntitySubject then it means "category" wasn't persisted,
// but since we are going to update "category" table (since its an owning side of relation with join column)
// we create a new subject here:
if (!relatedEntitySubject) {
relatedEntitySubject = new Subject({
metadata: relation.inverseEntityMetadata,
canBeUpdated: true,
identifier: relationIdMap
});
this.subjects.push(relatedEntitySubject);
}
relatedEntitySubject.changeMaps.push({
relation: relation.inverseRelation!,
value: subject
});
}
}
}