UNPKG

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