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