UNPKG

14.5 kBJavaScriptView Raw
1/**
2 * @copyright Copyright (c) 2019 Maxim Khorin <maksimovichu@gmail.com>
3 */
4'use strict';
5
6const Base = require('./Component');
7const StringHelper = require('../helper/StringHelper');
8
9module.exports = class Module extends Base {
10
11 static getExtendedClassProperties () {
12 return [
13 'COMPONENT_CONFIG_MAP'
14 ];
15 }
16
17 static getConstants () {
18 return {
19 NAME: this.getName(),
20 DEFAULT_COMPONENTS: {
21 'router': {},
22 'urlManager': {},
23 'view': {}
24 },
25 COMPONENT_CONFIG_MAP: {
26 'asset': {Class: require('../web/asset/AssetManager')},
27 'auth': {Class: require('../security/Auth')},
28 'bodyParser': {
29 Class: require('../web/BodyParser'),
30 extended: true
31 },
32 'cache': {Class: require('../cache/Cache')},
33 'cookie': {Class: require('../web/Cookie')},
34 'db': {Class: require('../db/Database')},
35 'filePacker': {Class: require('../web/packer/FilePacker')},
36 'forwarder': {Class: require('../web/Forwarder')},
37 'logger': {Class: require('../log/Logger')},
38 'rateLimit': {Class: require('../security/rate-limit/RateLimit')},
39 'rbac': {Class: require('../security/rbac/Rbac')},
40 'router': {Class: require('../web/Router')},
41 'scheduler': {Class: require('../scheduler/Scheduler')},
42 'session': {Class: require('../web/session/Session')},
43 'urlManager': {Class: require('../web/UrlManager')}
44 },
45 INHERITED_UNDEFINED_CONFIG_KEYS: [
46 'params',
47 'widgets'
48 ],
49 EVENT_BEFORE_INIT: 'beforeInit',
50 EVENT_BEFORE_COMPONENT_INIT: 'beforeComponentInit',
51 EVENT_AFTER_COMPONENT_INIT: 'afterComponentInit',
52 EVENT_AFTER_MODULE_INIT: 'afterModuleInit',
53 EVENT_AFTER_INIT: 'afterInit',
54 EVENT_BEFORE_ACTION: 'beforeAction',
55 EVENT_AFTER_ACTION: 'afterAction',
56 VIEW_LAYOUT: 'default' // default template layout
57 };
58 }
59
60 static getName () {
61 return StringHelper.camelToId(StringHelper.trimEnd(this.name, 'Module'));
62 }
63
64 constructor (config) {
65 super({
66 ActionView: require('../view/ActionView'),
67 ClassMapper: require('./ClassMapper'),
68 Configuration: require('./Configuration'),
69 DependentOrder: require('./DependentOrder'),
70 Engine: require('./ExpressEngine'),
71 InlineAction: require('./InlineAction'),
72 ...config
73 });
74 this.module = this;
75 this.modules = new DataMap;
76 this.components = new DataMap; // all components (with inherited)
77 this.ownComponents = new DataMap; // own module components
78 this.app = this.parent ? this.parent.app : this;
79 this.fullName = this.createFullName();
80 this.relativeName = this.createRelativeName();
81 this.engine = this.createEngine();
82 }
83
84 getTitle () {
85 return this.config.get('title') || this.NAME;
86 }
87
88 get (id) {
89 return this.components.get(id);
90 }
91
92 getParentComponent (id, defaults) {
93 return this.parent ? this.parent.components.get(id) : defaults;
94 }
95
96 getDb (id) {
97 return this.components.get(id || 'db');
98 }
99
100 getClass () {
101 return this.classMapper.get(...arguments);
102 }
103
104 getConfig () {
105 return this.config.get(...arguments);
106 }
107
108 getParam (key, defaults) {
109 return NestedHelper.get(key, this.params, defaults);
110 }
111
112 getPath () { // ignore absolute path in arguments
113 return path.join(this.CLASS_DIRECTORY, ...arguments);
114 }
115
116 resolvePath () { // not ignore absolute path in arguments
117 return path.resolve(this.CLASS_DIRECTORY, ...arguments);
118 }
119
120 require () {
121 return this.requireInternal(...arguments) || (this.original && this.original.require(...arguments));
122 }
123
124 requireInternal () {
125 try {
126 return require(this.resolvePath(...arguments));
127 } catch {}
128 }
129
130 getRelativePath (file) {
131 return FileHelper.getRelativePath(this.CLASS_DIRECTORY, file);
132 }
133
134 getControllerDirectory () {
135 return this.getPath('controller');
136 }
137
138 getControllerClass (id) {
139 return require(path.join(this.getControllerDirectory(), `${StringHelper.idToCamel(id)}Controller`));
140 }
141
142 getModule (name) { // name1.name2.name3
143 if (typeof name !== 'string') {
144 return null;
145 }
146 let module = this.modules.get(name);
147 if (module) {
148 return module;
149 }
150 const pos = name.indexOf('.');
151 if (pos === -1) {
152 return null;
153 }
154 module = this.modules.get(name.substring(0, pos));
155 return module ? module.getModule(name.substring(pos + 1)) : null;
156 }
157
158 getModules () {
159 return this.modules;
160 }
161
162 createFullName (separator = '.') { // eq - app.admin.profile
163 return this.parent.createFullName(separator) + separator + this.NAME;
164 }
165
166 createRelativeName (separator = '.') { // eq - admin.profile
167 const parent = this.parent.createRelativeName(separator);
168 return parent ? (parent + separator + this.NAME) : this.NAME;
169 }
170
171 log () {
172 CommonHelper.log(this.components.get('logger'), this.relativeName, ...arguments);
173 }
174
175 translate (message, params, source = 'app') {
176 const i18n = this.components.get('i18n');
177 return i18n ? i18n.translateMessage(message, params, source) : message;
178 }
179
180 // ROUTE
181
182 getRoute (url) {
183 if (this._route === undefined) {
184 this._route = this.parent.getRoute() + this.mountPath;
185 }
186 return url ? `${this._route}/${url}` : this._route;
187 }
188
189 getAncestry () {
190 if (!this._ancestry) {
191 this._ancestry = [this];
192 let current = this;
193 while (current.parent) {
194 current = current.parent;
195 this._ancestry.push(current);
196 }
197 }
198 return this._ancestry;
199 }
200
201 getHomeUrl () {
202 return this.params.homeUrl || '/';
203 }
204
205 // EVENTS
206
207 beforeInit () {
208 return this.trigger(this.EVENT_BEFORE_INIT);
209 }
210
211 beforeComponentInit () {
212 return this.trigger(this.EVENT_BEFORE_COMPONENT_INIT);
213 }
214
215 afterComponentInit () {
216 return this.trigger(this.EVENT_AFTER_COMPONENT_INIT);
217 }
218
219 afterModuleInit () {
220 return this.trigger(this.EVENT_AFTER_MODULE_INIT);
221 }
222
223 afterInit () {
224 return this.trigger(this.EVENT_AFTER_INIT);
225 }
226
227 beforeAction (action) {
228 return this.trigger(this.EVENT_BEFORE_ACTION, new ActionEvent(action));
229 }
230
231 afterAction (action) {
232 return this.trigger(this.EVENT_AFTER_ACTION, new ActionEvent(action));
233 }
234
235 // INIT
236
237 async init () {
238 await this.beforeInit();
239 await this.createOrigin();
240 await this.createConfiguration();
241 await this.createClassMapper();
242 this.extractConfigProperties();
243 this.setMountPath();
244 this.attachStaticSource(this.getParam('static'));
245 this.addViewEngine(this.getParam('template'));
246 this.createComponents(this.getConfig('components'));
247 await this.beforeComponentInit();
248 await this.initComponents();
249 await this.afterComponentInit();
250 await this.initModules(this.getConfig('modules'));
251 await this.afterModuleInit();
252 this.attachModule();
253 await this.afterInit();
254 this.log('info', `Configured as ${this.config.getTitle()}`);
255 }
256
257 async createOrigin () {
258 if (this.original) {
259 this.original = this.spawn(this.original, {parent: this.parent});
260 await this.original.createConfiguration();
261 await this.original.createClassMapper();
262 this.original.extractConfigProperties();
263 }
264 }
265
266 async createConfiguration () {
267 this.config = this.spawn(this.Configuration, {
268 directory: this.getPath('config'),
269 name: this.configName,
270 parent: this.parent && this.parent.config,
271 original: this.original && this.original.config,
272 data: this.config
273 });
274 await this.config.load();
275 this.config.inheritUndefined(this.INHERITED_UNDEFINED_CONFIG_KEYS);
276 }
277
278 extractConfigProperties () {
279 const params = this.getConfig('params') || {};
280 this.params = Object.assign(params, this.params);
281 }
282
283 setMountPath () {
284 this.mountPath = this.getConfig('mountPath', this.parent ? `/${this.NAME}` : '/');
285 }
286
287 async createClassMapper () {
288 this.classMapper = this.spawn(this.ClassMapper);
289 await this.classMapper.init();
290 }
291
292 // MODULES
293
294 async initModules (data = {}) {
295 for (const key of Object.keys(data)) {
296 await this.initModule(key, data[key]);
297 }
298 }
299
300 initModule (id, config) {
301 if (!config) {
302 return this.log('info', `Module skipped: ${id}`);
303 }
304 if (!config.Class) {
305 config.Class = this.require('module', id, 'Module.js');
306 }
307 const module = ClassHelper.spawn(config, {parent: this});
308 this.modules.set(id, module);
309 return module.init();
310 }
311
312 // COMPONENTS
313
314 deepAssignComponent (name, newComponent) {
315 const currentComponent = this.components.get(name);
316 for (const module of this.modules) {
317 if (module.components.get(name) === currentComponent) {
318 module.deepAssignComponent(name, newComponent);
319 }
320 }
321 this.components.set(name, newComponent);
322 }
323
324 createComponents (data = {}) {
325 if (this.parent) {
326 this.components.assign(this.parent.components); // inherit from parent
327 }
328 AssignHelper.assignUndefined(data, this.DEFAULT_COMPONENTS);
329 for (const id of Object.keys(data)) {
330 this.log('trace', `Create component: ${id}`);
331 const component = this.createComponent(id, data[id]);
332 if (component) {
333 this.ownComponents.set(id, component);
334 this.components.set(id, component); // with inherited components
335 }
336 }
337 }
338
339 createComponent (id, config) {
340 if (!config) {
341 return this.log('info', `Component skipped: ${id}`);
342 }
343 config = {
344 ...this.COMPONENT_CONFIG_MAP[id],
345 ...config
346 };
347 config.id = id;
348 config.parent = this.getParentComponent(id);
349 const name = StringHelper.idToCamel(config.componentMethodName || config.id);
350 const method = `create${name}Component`;
351 return typeof this[method] === 'function'
352 ? this[method](config)
353 : this.spawn(config);
354 }
355
356 createFormatterComponent (config) {
357 return this.spawn({
358 Class: require('../i18n/Formatter'),
359 i18n: this.components.get('i18n'),
360 ...config
361 });
362 }
363
364 createI18nComponent (config) {
365 return this.spawn({
366 Class: require('../i18n/I18n'),
367 ...config
368 });
369 }
370
371 createViewComponent (config) {
372 return this.spawn({
373 Class: require('../view/View'),
374 original: this.original && this.original.createViewComponent(config),
375 ...config
376 });
377 }
378
379 // INIT COMPONENT
380
381 async initComponents () {
382 for (const component of this.sortComponents()) {
383 await this.initComponent(component);
384 }
385 }
386
387 sortComponents () {
388 return this.spawn(this.DependentOrder).sort(this.ownComponents.values());
389 }
390
391 async initComponent (component) {
392 const name = StringHelper.idToCamel(component.componentMethodName || component.id);
393 const method = `init${name}Component`;
394 if (typeof this[method] === 'function') {
395 await this[method](component);
396 } else if (typeof component.init === 'function') {
397 await component.init();
398 }
399 }
400
401 // ENGINE
402
403 createEngine (params) {
404 return this.spawn(this.Engine, params);
405 }
406
407 addHandler () {
408 this.engine.add(...arguments);
409 }
410
411 addViewEngine (data) {
412 this.engine.addViewEngine(data);
413 }
414
415 attachStaticSource (data) {
416 if (data) {
417 this.attachStaticByModule(this, data);
418 if (this.original) {
419 this.attachStaticByModule(this.original, data);
420 }
421 }
422 }
423
424 attachStaticByModule (module, {options}) {
425 const web = module.getPath('web');
426 if (fs.existsSync(web)) {
427 // use static content handlers before others
428 this.app.mainEngine.attachStatic(this.getRoute(), web, options);
429 }
430 }
431
432 attachHandlers () {
433 for (const module of this.modules) {
434 module.attachHandlers();
435 }
436 this.engine.attachHandlers();
437 }
438
439 attachModule () {
440 if (this.parent) {
441 this.parent.engine.addChild(this.mountPath, this.engine);
442 }
443 this.engine.attach('use', this.handleModule.bind(this));
444 }
445
446 // MIDDLEWARE
447
448 handleModule (req, res, next) {
449 res.locals.module = this;
450 next();
451 }
452};
453module.exports.init();
454
455const fs = require('fs');
456const path = require('path');
457const AssignHelper = require('../helper/AssignHelper');
458const ClassHelper = require('../helper/ClassHelper');
459const CommonHelper = require('../helper/CommonHelper');
460const FileHelper = require('../helper/FileHelper');
461const NestedHelper = require('../helper/NestedHelper');
462const ActionEvent = require('./ActionEvent');
463const DataMap = require('./DataMap');
\No newline at end of file