1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | const Base = require('../base/Model');
|
7 |
|
8 | module.exports = class ActiveRecord extends Base {
|
9 |
|
10 | static getConstants () {
|
11 | return {
|
12 |
|
13 | PK: '_id',
|
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: [],
|
23 | UNLINK_ON_DELETE: [],
|
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 |
|
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 |
|
110 |
|
111 | beforeSave (insert) {
|
112 |
|
113 | return insert ? this.beforeInsert() : this.beforeUpdate();
|
114 | }
|
115 |
|
116 | beforeInsert () {
|
117 |
|
118 | return this.trigger(this.EVENT_BEFORE_INSERT);
|
119 | }
|
120 |
|
121 | beforeUpdate () {
|
122 |
|
123 | return this.trigger(this.EVENT_BEFORE_UPDATE);
|
124 | }
|
125 |
|
126 | afterSave (insert) {
|
127 |
|
128 | return insert ? this.afterInsert() : this.afterUpdate();
|
129 | }
|
130 |
|
131 | afterInsert () {
|
132 |
|
133 | return this.trigger(this.EVENT_AFTER_INSERT);
|
134 | }
|
135 |
|
136 | afterUpdate () {
|
137 |
|
138 | return this.trigger(this.EVENT_AFTER_UPDATE);
|
139 | }
|
140 |
|
141 | beforeDelete () {
|
142 |
|
143 | return this.trigger(this.EVENT_BEFORE_DELETE);
|
144 | }
|
145 |
|
146 | async afterDelete () {
|
147 |
|
148 | await this.unlinkRelations(this.getUnlinkOnDelete());
|
149 | await this.deleteRelatedModels(this.getDeleteOnUnlink());
|
150 | return this.trigger(this.EVENT_AFTER_DELETE);
|
151 | }
|
152 |
|
153 |
|
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 |
|
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 |
|
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 |
|
214 | async directUpdate (data) {
|
215 | this.assign(data);
|
216 | await this.findSelf().update(this.filterAttrs());
|
217 | this.assignOldAttrs();
|
218 | }
|
219 |
|
220 |
|
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 |
|
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 | };
|
447 | module.exports.init();
|
448 |
|
449 | const ArrayHelper = require('../helper/ArrayHelper');
|
450 | const CommonHelper = require('../helper/CommonHelper');
|
451 | const ObjectHelper = require('../helper/ObjectHelper');
|
452 | const StringHelper = require('../helper/StringHelper');
|
453 | const PromiseHelper = require('../helper/PromiseHelper'); |
\ | No newline at end of file |