UNPKG

6.2 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 for (const model of data.deletes) {
107 await model.delete();
108 }
109 }
110 if (Array.isArray(data.unlinks)) {
111 for (const model of data.unlinks) {
112 await this.owner.getLinker().unlink(name, model);
113 }
114 }
115 if (Array.isArray(data.links)) {
116 for (const model of data.links) {
117 await this.owner.getLinker().link(name, model);
118 }
119 }
120 return PromiseHelper.setImmediate();
121 }
122
123 async getLinkedDocs (name) {
124 if (this._linkedDocs !== undefined) {
125 return this._linkedDocs;
126 }
127 const relation = this.getRelation(name);
128 const docs = await relation.raw().all();
129 const result = {};
130 for (const doc of docs) {
131 result[doc[relation.model.PK]] = doc;
132 }
133 const data = this._changes[name];
134 if (data) {
135 for (const model of data.links) {
136 result[model.getId()] = model.getAttrMap();
137 }
138 for (const model of data.unlinks.concat(data.deletes)) {
139 delete result[model.getId()];
140 }
141 }
142 this._linkedDocs = Object.values(result);
143 return this._linkedDocs;
144 }
145
146 // EXISTS
147
148 async checkExists (name) {
149 const relation = this.getRelation(name);
150 if (relation.isMultiple()) {
151 throw new Error(`Multiple relation cannot be checked for exist: ${name}`);
152 }
153 const docs = await this.getLinkedDocs(name);
154 if (docs.length === 0) {
155 return null;
156 }
157 if (docs.length !== 1) {
158 throw new Error('Invalid relation changes');
159 }
160 return relation.isBackRef()
161 ? await this.checkBackRefExist(relation, docs[0])
162 : await this.checkRefExist(relation, docs[0]);
163 }
164
165 async checkRefExist ({refKey, linkKey}, doc) {
166 const ids = await this.owner.find({[linkKey]: doc[refKey]}).limit(2).ids();
167 return this.isExistingId(this.owner.getId(), ids);
168 }
169
170 async checkBackRefExist ({model, refKey, linkAttr}, doc) {
171 const ids = await model.find({[refKey]: this.owner.get(linkAttr)}).limit(2).ids();
172 return this.isExistingId(doc[model.PK], ids);
173 }
174
175 isExistingId (id, ids) {
176 return ids.length === 1 ? !CommonHelper.isEqual(id, ids[0]) : ids.length > 1;
177 }
178};
179
180const ArrayHelper = require('../helper/ArrayHelper');
181const CommonHelper = require('../helper/CommonHelper');
182const PromiseHelper = require('../helper/PromiseHelper');
183const ActiveRecord = require('../db/ActiveRecord');
184const RelationValidator = require('../validator/RelationValidator');
\No newline at end of file