UNPKG

6.33 kBJavaScriptView Raw
1/**
2 * @copyright Copyright (c) 2019 Maxim Khorin <maksimovichu@gmail.com>
3 */
4'use strict';
5
6const Base = require('../base/Behavior');
7
8module.exports = class RelationChangeBehavior extends Base {
9
10 _changes = {};
11 _relations = {};
12
13 constructor (config) {
14 super(config);
15 this.setHandler(ActiveRecord.EVENT_BEFORE_VALIDATE, this.beforeValidate);
16 this.setHandler(ActiveRecord.EVENT_BEFORE_INSERT, this.beforeSave);
17 this.setHandler(ActiveRecord.EVENT_BEFORE_UPDATE, this.beforeSave);
18 this.setHandler(ActiveRecord.EVENT_AFTER_INSERT, this.afterSave);
19 this.setHandler(ActiveRecord.EVENT_AFTER_UPDATE, this.afterSave);
20 }
21
22 beforeValidate () {
23 return this.resolveChanges();
24 }
25
26 beforeSave () {
27 return this.resolveChanges();
28 }
29
30 async afterSave () {
31 for (const key of Object.keys(this._changes)) {
32 await this.changeRelations(this._changes[key], key);
33 }
34 }
35
36 getChanges (name) {
37 return Object.prototype.hasOwnProperty.call(this._changes, name) ? this._changes[name] : null;
38 }
39
40 getRelation (name) {
41 if (!Object.prototype.hasOwnProperty.call(this._relations, name)) {
42 this._relations[name] = this.owner.getRelation(name);
43 }
44 return this._relations[name];
45 }
46
47 async resolveChanges () {
48 if (this._resolved) {
49 return;
50 }
51 this._resolved = true;
52 for (const name of this.getActiveRelationNames()) {
53 const value = this.owner.get(name);
54 const oldValue = this.owner.getOldAttr(name);
55 if (value && value !== oldValue) {
56 this._changes[name] = CommonHelper.parseRelationChanges(value);
57 }
58 this.restoreValue(name, oldValue);
59 if (this._changes[name]) {
60 await this.resolveLinks(name);
61 await this.resolveByRelated('unlinks', name);
62 await this.resolveByRelated('deletes', name);
63 }
64 }
65 }
66
67 getActiveRelationNames () {
68 let names = [];
69 for (const item of this.owner.getValidators()) {
70 if (item instanceof RelationValidator && item.isActive(this.owner.scenario)) {
71 names = names.concat(item.attrs);
72 }
73 }
74 return ArrayHelper.unique(names);
75 }
76
77 restoreValue (name, value) {
78 if (value !== undefined) {
79 return this.owner.set(name, value);
80 }
81 const relation = this.getRelation(name);
82 this.owner.set(name, relation.isInternalArray() ? [] : null);
83 }
84
85 async resolveLinks (name) {
86 const changes = this._changes[name];
87 if (changes.links.length) {
88 changes.links = await this.getRelation(name).model.findById(changes.links).all();
89 }
90 }
91
92 async resolveByRelated (key, name) {
93 const changes = this._changes[name];
94 if (changes[key].length) {
95 const query = this.getRelation(name);
96 changes[key] = await query.and(['ID', query.model.PK, changes[key]]).all();
97 }
98 }
99
100 async changeRelations (data, name) {
101 if (!data) {
102 return;
103 }
104 delete this._changes[name];
105 if (Array.isArray(data.deletes)) {
106 this.owner.constructor.delete(data.deletes);
107 }
108 if (Array.isArray(data.unlinks)) {
109 if (this.owner.getDeleteOnUnlink().includes(name)) {
110 this.owner.constructor.delete(data.unlinks);
111 } else {
112 for (const model of data.unlinks) {
113 await this.owner.getLinker().unlink(name, model);
114 }
115 }
116 }
117 if (Array.isArray(data.links)) {
118 for (const model of data.links) {
119 await this.owner.getLinker().link(name, model);
120 }
121 }
122 return PromiseHelper.setImmediate();
123 }
124
125 async getLinkedDocs (name) {
126 if (this._linkedDocs !== undefined) {
127 return this._linkedDocs;
128 }
129 const relation = this.getRelation(name);
130 const docs = await relation.raw().all();
131 const result = {};
132 for (const doc of docs) {
133 result[doc[relation.model.PK]] = doc;
134 }
135 const data = this._changes[name];
136 if (data) {
137 for (const model of data.links) {
138 result[model.getId()] = model.getAttrMap();
139 }
140 for (const model of data.unlinks.concat(data.deletes)) {
141 delete result[model.getId()];
142 }
143 }
144 this._linkedDocs = Object.values(result);
145 return this._linkedDocs;
146 }
147
148 // EXISTS
149
150 async checkExists (name) {
151 const relation = this.getRelation(name);
152 if (relation.isMultiple()) {
153 throw new Error(`Multiple relation cannot be checked for exist: ${name}`);
154 }
155 const docs = await this.getLinkedDocs(name);
156 if (docs.length === 0) {
157 return null;
158 }
159 if (docs.length !== 1) {
160 throw new Error('Invalid relation changes');
161 }
162 return relation.isBackRef()
163 ? await this.checkBackRefExist(relation, docs[0])
164 : await this.checkRefExist(relation, docs[0]);
165 }
166
167 async checkRefExist ({refKey, linkKey}, doc) {
168 const ids = await this.owner.find({[linkKey]: doc[refKey]}).limit(2).ids();
169 return this.isExistingId(this.owner.getId(), ids);
170 }
171
172 async checkBackRefExist ({model, refKey, linkAttr}, doc) {
173 const ids = await model.find({[refKey]: this.owner.get(linkAttr)}).limit(2).ids();
174 return this.isExistingId(doc[model.PK], ids);
175 }
176
177 isExistingId (id, ids) {
178 return ids.length === 1 ? !CommonHelper.isEqual(id, ids[0]) : ids.length > 1;
179 }
180};
181
182const ArrayHelper = require('../helper/ArrayHelper');
183const CommonHelper = require('../helper/CommonHelper');
184const PromiseHelper = require('../helper/PromiseHelper');
185const ActiveRecord = require('../db/ActiveRecord');
186const RelationValidator = require('../validator/RelationValidator');
\No newline at end of file