UNPKG

12 kBJavaScriptView Raw
1/**
2 * @copyright Copyright (c) 2019 Maxim Khorin <maksimovichu@gmail.com>
3 */
4'use strict';
5
6const Base = require('./Component');
7
8module.exports = class Model extends Base {
9
10 static getExtendedClassProperties () {
11 return [
12 'BEHAVIORS',
13 'SCENARIOS',
14 'ATTR_HINTS',
15 'ATTR_LABELS',
16 'ATTR_VALUE_LABELS'
17 ];
18 }
19
20 static getConstants () {
21 return {
22 RULES: [
23 // [['attr1', 'attr2'], '{type}', {...params}]
24 // [['attr1', 'attr2'], '{model method name}']
25 // [['attr1', 'attr2'], {validator class} ]
26 // [['attr1', 'attr2'], '{type}', {on: ['scenario1']} ]
27 // [['attr1', 'attr2'], '{type}', {except: ['scenario2']} ]
28 // [['attr1'], 'unsafe'] // skip attribute loading
29 ],
30 SCENARIOS: {
31 // default: ['attr1', 'attr2']
32 // scenario1: ['attr2']
33 },
34 DEFAULT_SCENARIO: 'default',
35 ATTR_LABELS: {},
36 ATTR_VALUE_LABELS: {},
37 ATTR_HINTS: {},
38 EVENT_BEFORE_VALIDATE: 'beforeValidate',
39 EVENT_AFTER_VALIDATE: 'afterValidate',
40 NAME: this.getName(),
41 CONTROLLER_DIRECTORY: 'controller',
42 MODEL_DIRECTORY: 'model'
43 }
44 }
45
46 static getName () {
47 return this.name;
48 }
49
50 static getAttrValueLabels (name) {
51 return this.ATTR_VALUE_LABELS[name];
52 }
53
54 static getAttrValueLabel (name, value) {
55 return this.ATTR_VALUE_LABELS[name] && this.ATTR_VALUE_LABELS[name][value];
56 }
57
58 _attrMap = {};
59 _viewAttrMap = {};
60 _errorMap = {};
61 _validators = null;
62
63 has (name) {
64 return Object.prototype.hasOwnProperty.call(this._attrMap, name);
65 }
66
67 isAttrActive (name) {
68 return this.getActiveAttrNames().includes(name);
69 }
70
71 isAttrRequired (name) {
72 for (const validator of this.getActiveValidators(name)) {
73 if (validator instanceof Validator.BUILTIN.required && validator.when === null) {
74 return true;
75 }
76 }
77 return false;
78 }
79
80 isAttrSafe (name) {
81 return this.getSafeAttrNames().includes(name);
82 }
83
84 get (name) {
85 if (Object.prototype.hasOwnProperty.call(this._attrMap, name)) {
86 return this._attrMap[name];
87 }
88 }
89
90 getAttrMap () {
91 return this._attrMap;
92 }
93
94 getAttrLabel (name) {
95 return Object.prototype.hasOwnProperty.call(this.ATTR_LABELS, name)
96 ? this.ATTR_LABELS[name]
97 : this.generateAttrLabel(name);
98 }
99
100 getAttrHint (name) {
101 return ObjectHelper.getValue(name, this.ATTR_HINTS, '');
102 }
103
104 getFormAttrId (name, prefix) {
105 return prefix ? `${prefix}-${this.NAME}-${name}` : `${this.NAME}-${name}`;
106 }
107
108 getFormAttrName (name) {
109 return `${this.NAME}[${name}]`;
110 }
111
112 getSafeAttrNames () {
113 const data = this.getUnsafeAttrMap();
114 return this.getActiveAttrNames().filter(name => !Object.prototype.hasOwnProperty.call(data, name));
115 }
116
117 getUnsafeAttrMap () {
118 const data = {};
119 for (const validator of this.getActiveValidatorsByClass(Validator.BUILTIN.unsafe)) {
120 for (const attr of validator.attrs) {
121 data[attr] = true;
122 }
123 }
124 return data;
125 }
126
127 getActiveAttrNames () {
128 return this.getScenarioAttrNames(this.scenario);
129 }
130
131 getScenarioAttrNames (scenario) {
132 const names = {};
133 const only = Array.isArray(this.SCENARIOS[scenario])
134 ? this.SCENARIOS[scenario]
135 : this.SCENARIOS[this.DEFAULT_SCENARIO];
136 for (const validator of this.getValidators()) {
137 if (validator.isActive(scenario)) {
138 for (const name of validator.attrs) {
139 if (!Array.isArray(only) || only.includes(name)) {
140 names[name] = true;
141 }
142 }
143 }
144 }
145 return Object.keys(names);
146 }
147
148 set (name, value) {
149 this._attrMap[name] = value;
150 }
151
152 setFromModel (name, model) {
153 this._attrMap[name] = model.get(name);
154 }
155
156 setSafeAttrs (data) {
157 if (data) {
158 for (const name of this.getSafeAttrNames()) {
159 if (data && Object.prototype.hasOwnProperty.call(data, name)) {
160 this.set(name, data[name]);
161 }
162 }
163 }
164 }
165
166 setAttrs (data, except) {
167 data = data instanceof Model ? data.getAttrMap() : data;
168 if (data) {
169 for (const key of Object.keys(data)) {
170 if (Array.isArray(except) ? !except.includes(key) : (except !== key)) {
171 this._attrMap[key] = data[key];
172 }
173 }
174 }
175 }
176
177 assign (data) {
178 Object.assign(this._attrMap, data instanceof Model ? data.getAttrMap() : data);
179 }
180
181 unset (...names) {
182 for (const name of names) {
183 delete this._attrMap[name];
184 }
185 }
186
187 // VIEW ATTRIBUTES
188
189 getViewAttr (name) {
190 return Object.prototype.hasOwnProperty.call(this._viewAttrMap, name)
191 ? this._viewAttrMap[name]
192 : this.get(name);
193 }
194
195 setViewAttr (name, value) {
196 this._viewAttrMap[name] = value;
197 }
198
199 // LABELS
200
201 generateAttrLabel (name) {
202 this.ATTR_LABELS[name] = StringHelper.generateLabel(name);
203 return this.ATTR_LABELS[name];
204 }
205
206 getAttrValueLabel (name, data) {
207 return ObjectHelper.getValueOrKey(this.get(name), data || this.ATTR_VALUE_LABELS[name]);
208 }
209
210 setAttrValueLabel (name, data) {
211 this.setViewAttr(name, this.getAttrValueLabel(name, data));
212 }
213
214 // LOAD
215
216 load (data) {
217 if (data) {
218 this.setSafeAttrs(data[this.NAME]);
219 }
220 return this;
221 }
222
223 // EVENTS
224
225 beforeValidate () {
226 // call await super.beforeValidate() if override it
227 return this.trigger(this.EVENT_BEFORE_VALIDATE);
228 }
229
230 afterValidate () {
231 // call await super.afterValidate() if override it
232 return this.trigger(this.EVENT_AFTER_VALIDATE);
233 }
234
235 // VALIDATION
236
237 getValidationRules () {
238 return this.RULES;
239 }
240
241 getActiveValidatorsByClass (Class, attr) {
242 return this.getValidators().filter(validator => {
243 return validator instanceof Class
244 && validator.isActive(this.scenario)
245 && (!attr || validator.attrs.includes(attr));
246 });
247 }
248
249 getActiveValidators (attr) {
250 return this.getValidators().filter(validator => {
251 return validator.isActive(this.scenario) && (!attr || validator.attrs.includes(attr));
252 });
253 }
254
255 getValidatorsByClass (Class, attr) {
256 return this.getValidators().filter(validator => {
257 return validator instanceof Class && (!attr || validator.attrs.includes(attr));
258 });
259 }
260
261 getValidators () {
262 if (!this._validators) {
263 this._validators = this.createValidators();
264 }
265 return this._validators;
266 }
267
268 addValidator (rule) {
269 rule = this.createValidator(rule);
270 if (rule) {
271 this.getValidators().push(rule);
272 }
273 }
274
275 async setDefaultValues () {
276 for (const validator of this.getActiveValidatorsByClass(Validator.BUILTIN.default)) {
277 await validator.validateModel(this);
278 }
279 }
280
281 async validate (attrNames) {
282 await this.beforeValidate();
283 attrNames = attrNames || this.getActiveAttrNames();
284 for (const validator of this.getActiveValidators()) {
285 await validator.validateModel(this, attrNames);
286 }
287 await this.afterValidate();
288 return !this.hasError();
289 }
290
291 createValidators () {
292 const validators = [];
293 for (const rule of this.getValidationRules()) {
294 const validator = this.createValidator(rule);
295 if (validator) {
296 validators.push(validator);
297 }
298 }
299 return validators;
300 }
301
302 createValidator (rule) {
303 if (rule instanceof Validator) {
304 return rule;
305 }
306 if (Array.isArray(rule) && rule[0] && rule[1]) {
307 return Validator.createValidator(rule[1], this, rule[0], rule[2]);
308 }
309 this.log('error', 'Invalid validation rule', rule);
310 }
311
312 // ERRORS
313
314 hasError (attr) {
315 return attr
316 ? Object.prototype.hasOwnProperty.call(this._errorMap, attr)
317 : Object.values(this._errorMap).length > 0;
318 }
319
320 getErrors (attr) {
321 return !attr ? this._errorMap : this.hasError(attr) ? this._errorMap[attr] : [];
322 }
323
324 getFirstError (attr) {
325 if (attr) {
326 return this.hasError(attr) ? this._errorMap[attr][0] : '';
327 }
328 for (const data of Object.values(this._errorMap)) {
329 if (data.length) {
330 return data[0];
331 }
332 }
333 return '';
334 }
335
336 getFirstErrorMap () {
337 const result = {};
338 for (const attr of Object.keys(this._errorMap)) {
339 if (this._errorMap[attr].length) {
340 result[attr] = this._errorMap[attr][0];
341 }
342 }
343 return result;
344 }
345
346 addError (attr, error) {
347 if (!error) {
348 return false;
349 }
350 if (!this.hasError(attr)) {
351 this._errorMap[attr] = [];
352 }
353 this._errorMap[attr].push(error);
354 }
355
356 addErrors (data) {
357 if (data) {
358 for (const attr of Object.keys(data)) {
359 if (Array.isArray(data[attr])) {
360 for (const value of data[attr]) {
361 this.addError(attr, value);
362 }
363 } else {
364 this.addError(attr, data[attr]);
365 }
366 }
367 }
368 }
369
370 clearErrors (attr) {
371 if (attr) {
372 delete this._errorMap[attr]
373 } else {
374 this._errorMap = {};
375 }
376 }
377
378 // MODEL CONTROLLER
379
380 static getControllerClass () {
381 if (!this.hasOwnProperty('_CONTROLLER_CLASS')) {
382 const closest = FileHelper.getClosestDirectory(this.MODEL_DIRECTORY, this.CLASS_DIRECTORY);
383 const dir = path.join(this.CONTROLLER_DIRECTORY, this.getNestedDirectory(), this.getControllerClassName());
384 this._CONTROLLER_CLASS = require(path.join(path.dirname(closest), dir));
385 }
386 return this._CONTROLLER_CLASS;
387 }
388
389 static getControllerClassName () {
390 return this.NAME + 'Controller';
391 }
392
393 static getNestedDirectory () {
394 if (!this.hasOwnProperty('_NESTED_DIRECTORY')) {
395 this._NESTED_DIRECTORY = FileHelper.getRelativePathByDirectory(this.MODEL_DIRECTORY, this.CLASS_DIRECTORY);
396 }
397 return this._NESTED_DIRECTORY;
398 }
399
400 getControllerClass () {
401 return this.constructor.getControllerClass();
402 }
403
404 createController (config) {
405 return this.spawn(this.getControllerClass(), config);
406 }
407};
408module.exports.init();
409
410const path = require('path');
411const FileHelper = require('../helper/FileHelper');
412const ObjectHelper = require('../helper/ObjectHelper');
413const StringHelper = require('../helper/StringHelper');
414const Validator = require('../validator/Validator');
\No newline at end of file