UNPKG

14.7 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._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.createOrigin();
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 createOrigin () {
260 if (this.original) {
261 this.original = this.spawn(this.original, {parent: this.parent});
262 await this.original.createConfiguration();
263 await this.original.createClassMapper();
264 this.original.extractConfigProperties();
265 }
266 }
267
268 async createConfiguration () {
269 this.config = this.spawn(this.Configuration, {
270 directory: this.getPath('config'),
271 name: this.configName,
272 parent: this.parent && this.parent.config,
273 original: this.original && this.original.config,
274 data: this.config
275 });
276 await this.config.load();
277 this.config.inheritUndefined(this.INHERITED_UNDEFINED_CONFIG_KEYS);
278 }
279
280 extractConfigProperties () {
281 const params = this.getConfig('params') || {};
282 this.params = Object.assign(params, this.params);
283 }
284
285 setMountPath () {
286 this.mountPath = this.getConfig('mountPath', this.parent ? `/${this.getBaseName()}` : '/');
287 }
288
289 async createClassMapper () {
290 this.classMapper = this.spawn(this.ClassMapper);
291 await this.classMapper.init();
292 }
293
294 // MODULES
295
296 async initModules (data = {}) {
297 for (const key of Object.keys(data)) {
298 await this.initModule(key, data[key]);
299 }
300 }
301
302 initModule (id, config) {
303 if (!config) {
304 return this.log('info', `Module skipped: ${id}`);
305 }
306 if (!config.Class) {
307 config.Class = this.require('module', id, 'Module.js');
308 }
309 const module = ClassHelper.spawn(config, {parent: this});
310 this.modules.set(id, module);
311 return module.init();
312 }
313
314 // COMPONENTS
315
316 deepAssignComponent (name, newComponent) {
317 const currentComponent = this.components.get(name);
318 for (const module of this.modules) {
319 if (module.components.get(name) === currentComponent) {
320 module.deepAssignComponent(name, newComponent);
321 }
322 }
323 this.components.set(name, newComponent);
324 }
325
326 createComponents (data = {}) {
327 if (this.parent) {
328 this.components.assign(this.parent.components); // inherit from parent
329 }
330 AssignHelper.assignUndefined(data, this.DEFAULT_COMPONENTS);
331 for (const id of Object.keys(data)) {
332 this.log('trace', `Create component: ${id}`);
333 const component = this.createComponent(id, data[id]);
334 if (component) {
335 this.ownComponents.set(id, component);
336 this.components.set(id, component); // with inherited components
337 }
338 }
339 }
340
341 createComponent (id, config) {
342 if (!config) {
343 return this.log('info', `Component skipped: ${id}`);
344 }
345 config = {
346 ...this.COMPONENT_CONFIG_MAP[id],
347 ...config
348 };
349 config.id = id;
350 config.parent = this.getParentComponent(id);
351 const name = StringHelper.idToCamel(config.componentMethodName || config.id);
352 const method = `create${name}Component`;
353 return typeof this[method] === 'function'
354 ? this[method](config)
355 : this.spawn(config);
356 }
357
358 createFormatterComponent (config) {
359 return this.spawn({
360 Class: require('../i18n/Formatter'),
361 i18n: this.components.get('i18n'),
362 ...config
363 });
364 }
365
366 createI18nComponent (config) {
367 return this.spawn({
368 Class: require('../i18n/I18n'),
369 ...config
370 });
371 }
372
373 createViewComponent (config) {
374 return this.spawn({
375 Class: require('../view/View'),
376 original: this.original && this.original.createViewComponent(config),
377 ...config
378 });
379 }
380
381 // INIT COMPONENT
382
383 async initComponents () {
384 for (const component of this.sortComponents()) {
385 await this.initComponent(component);
386 }
387 }
388
389 sortComponents () {
390 return this.spawn(this.DependentOrder).sort(this.ownComponents.values());
391 }
392
393 async initComponent (component) {
394 const name = StringHelper.idToCamel(component.componentMethodName || component.id);
395 const method = `init${name}Component`;
396 if (typeof this[method] === 'function') {
397 await this[method](component);
398 } else if (typeof component.init === 'function') {
399 await component.init();
400 }
401 }
402
403 // ENGINE
404
405 createEngine (params) {
406 return this.spawn(this.Engine, params);
407 }
408
409 addHandler () {
410 this.engine.add(...arguments);
411 }
412
413 addViewEngine (data) {
414 this.engine.addViewEngine(data);
415 }
416
417 attachStaticSource (data) {
418 if (data) {
419 this.attachStaticByModule(this, data);
420 if (this.original) {
421 this.attachStaticByModule(this.original, data);
422 }
423 }
424 }
425
426 attachStaticByModule (module, {options}) {
427 const web = module.getPath('web');
428 if (fs.existsSync(web)) {
429 // use static content handlers before others
430 this.app.mainEngine.attachStatic(this.getRoute(), web, options);
431 }
432 }
433
434 attachHandlers () {
435 for (const module of this.modules) {
436 module.attachHandlers();
437 }
438 this.engine.attachHandlers();
439 }
440
441 attachModule () {
442 if (this.parent) {
443 this.parent.engine.addChild(this.mountPath, this.engine);
444 }
445 this.engine.attach('use', this.handleModule.bind(this));
446 }
447
448 // MIDDLEWARE
449
450 handleModule (req, res, next) {
451 res.locals.module = this;
452 next();
453 }
454};
455module.exports.init();
456
457const fs = require('fs');
458const path = require('path');
459const AssignHelper = require('../helper/AssignHelper');
460const ClassHelper = require('../helper/ClassHelper');
461const CommonHelper = require('../helper/CommonHelper');
462const FileHelper = require('../helper/FileHelper');
463const NestedHelper = require('../helper/NestedHelper');
464const StringHelper = require('../helper/StringHelper');
465const ActionEvent = require('./ActionEvent');
466const DataMap = require('./DataMap');
\No newline at end of file