UNPKG

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