UNPKG

12.2 kBJavaScriptView Raw
1/**
2 * @copyright Copyright (c) 2019 Maxim Khorin <maksimovichu@gmail.com>
3 */
4'use strict';
5
6const Base = require('../base/Model');
7
8module.exports = class ActiveRecord extends Base {
9
10 static getConstants () {
11 return {
12 // TABLE: 'tableName',
13 PK: '_id', // primary key
14 LINKER_CLASS: require('./ActiveLinker'),
15 QUERY_CLASS: require('./ActiveQuery'),
16 EVENT_BEFORE_INSERT: 'beforeInsert',
17 EVENT_BEFORE_UPDATE: 'beforeUpdate',
18 EVENT_BEFORE_DELETE: 'beforeDelete',
19 EVENT_AFTER_INSERT: 'afterInsert',
20 EVENT_AFTER_UPDATE: 'afterUpdate',
21 EVENT_AFTER_DELETE: 'afterDelete',
22 // UNLINK_ON_DELETE: [], // unlink relations after model deletion
23 };
24 }
25
26 _isNew = true;
27 _oldAttrMap = {};
28 _related = {};
29
30 isNew () {
31 return this._isNew;
32 }
33
34 isPrimaryKey (key) {
35 return this.PK === key;
36 }
37
38 getDb () {
39 return this.db || this.module.getDb();
40 }
41
42 getTable () {
43 return this.TABLE
44 }
45
46 getId () {
47 return this.get(this.PK);
48 }
49
50 getTitle () {
51 return String(this.getId());
52 }
53
54 toString () {
55 return `${this.constructor.name}: ${this.getId()}`;
56 }
57
58 toJSON () {
59 const id = this.getId();
60 return id && id.toJSON ? id.toJSON() : id;
61 }
62
63 // ATTRIBUTES
64
65 isAttrChanged (name) {
66 return !CommonHelper.isEqual(this._attrMap[name], this._oldAttrMap[name]);
67 }
68
69 get (name) {
70 if (Object.prototype.hasOwnProperty.call(this._attrMap, name)) {
71 return this._attrMap[name];
72 }
73 if (typeof name !== 'string') {
74 return;
75 }
76 const index = name.indexOf('.');
77 if (index === -1) {
78 return this.rel(name);
79 }
80 const related = this._related[name.substring(0, index)];
81 name = name.substring(index + 1);
82 if (related instanceof ActiveRecord) {
83 return related.get(name);
84 }
85 if (Array.isArray(related)) {
86 return related.map(item => item instanceof ActiveRecord
87 ? item.get(name)
88 : item ? item[name] : item
89 );
90 }
91 return related ? related[name] : related;
92 }
93
94 getOldAttr (name) {
95 if (Object.prototype.hasOwnProperty.call(this._oldAttrMap, name)) {
96 return this._oldAttrMap[name];
97 }
98 }
99
100 assignOldAttrs () {
101 this._oldAttrMap = {...this._attrMap};
102 }
103
104 // EVENTS
105
106 beforeSave (insert) {
107 // call on override: await super.beforeSave(insert)
108 return insert ? this.beforeInsert() : this.beforeUpdate();
109 }
110
111 beforeInsert () {
112 // call on override: await super.beforeInsert()
113 return this.trigger(this.EVENT_BEFORE_INSERT);
114 }
115
116 beforeUpdate () {
117 // call on override: await super.beforeUpdate()
118 return this.trigger(this.EVENT_BEFORE_UPDATE);
119 }
120
121 afterSave (insert) {
122 // call on override: await super.afterSave(insert)
123 return insert ? this.afterInsert() : this.afterUpdate();
124 }
125
126 afterInsert () {
127 // call on override: await super.afterInsert()
128 return this.trigger(this.EVENT_AFTER_INSERT);
129 }
130
131 afterUpdate () {
132 // call on override: await super.afterUpdate()
133 return this.trigger(this.EVENT_AFTER_UPDATE);
134 }
135
136 beforeDelete () {
137 // call await super.beforeDelete() if override it
138 return this.trigger(this.EVENT_BEFORE_DELETE);
139 }
140
141 async afterDelete () {
142 // call on override: await super.afterDelete()
143 await this.unlinkRelations(this.getUnlinkOnDelete());
144 return this.trigger(this.EVENT_AFTER_DELETE);
145 }
146
147 // POPULATE
148
149 populate (doc) {
150 this._isNew = false;
151 Object.assign(this._attrMap, doc);
152 this.assignOldAttrs();
153 }
154
155 filterAttrs () {
156 const result = {};
157 for (const key of this.ATTRS) {
158 if (Object.prototype.hasOwnProperty.call(this._attrMap, key)) {
159 result[key] = this._attrMap[key];
160 }
161 }
162 return result;
163 }
164
165 // FIND
166
167 findById (id) {
168 return this.find(['ID', this.PK, id]);
169 }
170
171 findSelf () {
172 return this.find({[this.PK]: this.getId()});
173 }
174
175 find () {
176 return (new this.QUERY_CLASS({model: this})).and(...arguments);
177 }
178
179 // SAVE
180
181 async save () {
182 if (await this.validate()) {
183 await this.forceSave();
184 return true;
185 }
186 }
187
188 forceSave () {
189 return this._isNew ? this.insert() : this.update();
190 }
191
192 async insert () {
193 await this.beforeSave(true);
194 this.set(this.PK, await this.find().insert(this.filterAttrs()));
195 this._isNew = false;
196 await this.afterSave(true);
197 this.assignOldAttrs();
198 }
199
200 async update () {
201 await this.beforeSave(false);
202 await this.findSelf().update(this.filterAttrs());
203 await this.afterSave(false);
204 this.assignOldAttrs();
205 }
206
207 // skip before and after save triggers
208 async directUpdate (data) {
209 this.assign(data);
210 await this.findSelf().update(this.filterAttrs());
211 this.assignOldAttrs();
212 }
213
214 // DELETE
215
216 static async delete (models) {
217 for (const model of models) {
218 await model.delete();
219 await PromiseHelper.setImmediate();
220 }
221 }
222
223 async delete () {
224 await this.beforeDelete();
225 if (!this.hasError()) {
226 await this.findSelf().delete();
227 await this.afterDelete();
228 }
229 }
230
231 // RELATIONS
232
233 static async resolveRelation (name, models) {
234 const relations = [];
235 for (const model of models) {
236 relations.push(await model.resolveRelation(name));
237 }
238 return relations;
239 }
240
241 static async resolveRelations (names, models) {
242 const relations = [];
243 for (const model of models) {
244 relations.push(await model.resolveRelations(names));
245 }
246 return relations;
247 }
248
249 rel (name) {
250 return this.isRelationPopulated(name)
251 ? this._related[name]
252 : this.executeRelatedMethod('rel', name);
253 }
254
255 call (name, ...args) {
256 return typeof this[name] === 'function'
257 ? this[name](...args)
258 : this.executeRelatedMethod('call', name, ...args);
259 }
260
261 isRelationPopulated (name) {
262 return Object.prototype.hasOwnProperty.call(this._related, name);
263 }
264
265 getRelated (name) {
266 return this.isRelationPopulated(name) ? this._related[name] : undefined;
267 }
268
269 setRelatedViewAttr (name) {
270 this.setViewAttr(name, this.getRelatedTitle(name));
271 }
272
273 getRelatedTitle (name) {
274 if (!this.isRelationPopulated(name)) {
275 return this.executeRelatedMethod('getRelatedTitle', name) || this.get(name);
276 }
277 const related = this._related[name];
278 return Array.isArray(related)
279 ? related.map(model => model instanceof ActiveRecord ? model.getTitle() : null)
280 : related ? related.getTitle() : this.get(name);
281 }
282
283 getAllRelationNames () {
284 const pattern = new RegExp('^rel[A-Z]{1}');
285 const names = [];
286 for (const name of ObjectHelper.getAllFunctionNames(this)) {
287 if (pattern.test(name)) {
288 names.push(name.substring(3));
289 }
290 }
291 return names;
292 }
293
294 getRelation (name) {
295 if (!name || typeof name !== 'string') {
296 return null;
297 }
298 name = 'rel' + StringHelper.toFirstUpperCase(name);
299 return this[name] ? this[name]() : null;
300 }
301
302 hasMany (RefClass, refKey, linkKey) {
303 return this.spawn(RefClass).find().relateMany(this, refKey, linkKey);
304 }
305
306 hasOne (RefClass, refKey, linkKey) {
307 return this.spawn(RefClass).find().relateOne(this, refKey, linkKey);
308 }
309
310 executeRelatedMethod (method, name, ...args) {
311 if (typeof name !== 'string') {
312 return;
313 }
314 const index = name.indexOf('.');
315 if (index < 1) {
316 return;
317 }
318 const related = this._related[name.substring(0, index)];
319 name = name.substring(index + 1);
320 if (related instanceof ActiveRecord) {
321 return related[method](name, ...args);
322 }
323 if (Array.isArray(related)) {
324 return related.map(item => item instanceof ActiveRecord ? item[method](name, ...args) : null);
325 }
326 }
327
328 populateRelation (name, data) {
329 this._related[name] = data;
330 }
331
332 async resolveRelation (name) {
333 const index = name.indexOf('.');
334 if (index === -1) {
335 return this.resolveRelationOnly(name);
336 }
337 let nestedName = name.substring(index + 1);
338 let result = await this.resolveRelationOnly(name.substring(0, index));
339 if (result instanceof ActiveRecord) {
340 return result.resolveRelation(nestedName);
341 }
342 if (!Array.isArray(result)) {
343 return result;
344 }
345 result = result.filter(model => model instanceof ActiveRecord);
346 const models = [];
347 for (const model of result) {
348 models.push(await model.resolveRelation(nestedName));
349 }
350 return ArrayHelper.concat(models);
351 }
352
353 async resolveRelationOnly (name) {
354 if (this.isRelationPopulated(name)) {
355 return this._related[name];
356 }
357 const relation = this.getRelation(name);
358 if (relation) {
359 this.populateRelation(name, await relation.resolve());
360 await PromiseHelper.setImmediate();
361 return this._related[name];
362 }
363 if (relation === null) {
364 throw new Error(this.wrapMessage(`Unknown relation: ${name}`));
365 }
366 return null;
367 }
368
369 async resolveRelations (names) {
370 const result = [];
371 for (const name of names) {
372 result.push(await this.resolveRelation(name));
373 }
374 return result;
375 }
376
377 async handleEachRelatedModel (names, handler) {
378 const models = await this.resolveRelations(names);
379 for (const model of ArrayHelper.concat(models)) {
380 if (model) {
381 await handler(model);
382 }
383 }
384 }
385
386 unsetRelated (name) {
387 if (Array.isArray(name)) {
388 for (const key of name) {
389 delete this._related[key];
390 }
391 } else if (!arguments.length) {
392 this._related = {};
393 }
394 delete this._related[name];
395 }
396
397 getLinker () {
398 if (!this._linker) {
399 this._linker = this.spawn(this.LINKER_CLASS, {owner: this});
400 }
401 return this._linker;
402 }
403
404 getUnlinkOnDelete () {
405 return this.UNLINK_ON_DELETE;
406 }
407
408 async unlinkRelations (relations) {
409 if (Array.isArray(relations)) {
410 const linker = this.getLinker();
411 for (const relation of relations) {
412 await linker.unlinkAll(relation);
413 }
414 }
415 }
416
417 log () {
418 CommonHelper.log(this.module, `${this.constructor.name}: ID: ${this.getId()}`, ...arguments);
419 }
420
421 wrapMessage (message) {
422 return `${this.constructor.name}: ID: ${this.getId()}: ${message}`;
423 }
424};
425module.exports.init();
426
427const ArrayHelper = require('../helper/ArrayHelper');
428const CommonHelper = require('../helper/CommonHelper');
429const ObjectHelper = require('../helper/ObjectHelper');
430const StringHelper = require('../helper/StringHelper');
431const PromiseHelper = require('../helper/PromiseHelper');
\No newline at end of file