UNPKG

11.5 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 Controller extends Base {
9
10 static getExtendedClassProperties () {
11 return [
12 'METHODS',
13 'ACTIONS'
14 ];
15 }
16
17 static getConstants () {
18 return {
19 // declare allowed methods for action if not set then all
20 METHODS: {
21 // 'logout': ['POST']
22 },
23 // declare external actions for the controller
24 ACTIONS: {
25 // 'captcha': { Class: require('areto/security/captcha/CaptchaAction'), ... }
26 },
27 EVENT_BEFORE_ACTION: 'beforeAction',
28 EVENT_AFTER_ACTION: 'afterAction',
29 DEFAULT_ACTION: 'index',
30 CONTROLLER_DIRECTORY: 'controller',
31 MODEL_DIRECTORY: 'model',
32 // inherited from module by default
33 // ACTION_VIEW: require('./ActionView'),
34 // VIEW_LAYOUT: 'default',
35 // INLINE_ACTION: require('./InlineAction')
36 };
37 }
38
39 static getBaseName () {
40 if (!this.hasOwnProperty('_baseName')) {
41 this._baseName = StringHelper.camelToId(StringHelper.trimEnd(this.name, 'Controller'));
42 }
43 return this._baseName;
44 }
45
46 static getActionKeys () {
47 const keys = Object.keys(this.ACTIONS);
48 for (const key of ObjectHelper.getAllFunctionNames(this.prototype)) {
49 if (key.indexOf('action') === 0) {
50 keys.push(StringHelper.camelToId(key.substring(6)));
51 }
52 }
53 return keys;
54 }
55
56 static getModelClass () {
57 if (!this.hasOwnProperty('_MODEL_CLASS')) {
58 const closest = FileHelper.getClosestDirectory(this.CONTROLLER_DIRECTORY, this.CLASS_DIRECTORY);
59 const dir = path.join(this.MODEL_DIRECTORY, this.getNestedDirectory(), this.getModelClassName());
60 this._MODEL_CLASS = require(path.join(path.dirname(closest), dir));
61 }
62 return this._MODEL_CLASS;
63 }
64
65 static getModelClassName () {
66 return StringHelper.trimEnd(this.name, 'Controller');
67 }
68
69 static getViewDirectory () {
70 if (!this.hasOwnProperty('_VIEW_DIRECTORY')) {
71 const dir = this.getNestedDirectory();
72 this._VIEW_DIRECTORY = dir ? `${dir}/${this.getBaseName()}/` : `${this.getBaseName()}/`;
73 }
74 return this._VIEW_DIRECTORY;
75 }
76
77 static getNestedDirectory () {
78 if (!this.hasOwnProperty('_NESTED_DIRECTORY')) {
79 this._NESTED_DIRECTORY = FileHelper.getRelativePathByDirectory(this.CONTROLLER_DIRECTORY, this.CLASS_DIRECTORY);
80 }
81 return this._NESTED_DIRECTORY;
82 }
83
84 constructor (config) {
85 super(config);
86 this.response = new Response;
87 this.response.controller = this;
88 this.formatter = this.module.components.get('formatter');
89 this.i18n = this.module.components.get('i18n');
90 this.language = this.language || this.i18n && this.i18n.language;
91 this.timestamp = Date.now();
92 this.urlManager = this.module.components.get('urlManager');
93 }
94
95 createModel (params) {
96 return this.spawn(this.getModelClass(), params);
97 }
98
99 getBaseName () {
100 return this.constructor.getBaseName();
101 }
102
103 getModelClass () {
104 return this.constructor.getModelClass();
105 }
106
107 assignSource (controller) {
108 this.module = controller.module;
109 this.req = controller.req;
110 this.res = controller.res;
111 this.err = controller.err;
112 this.user = controller.user;
113 this.language = controller.language;
114 this.timestamp = controller.timestamp;
115 return this;
116 }
117
118 async execute (name) {
119 this.action = this.createAction(name);
120 if (!this.action) {
121 throw new Error(`Unable to create action: ${name}`);
122 }
123 const modules = this.module.getAncestry();
124 // trigger module's beforeAction from root to current
125 for (let i = modules.length - 1; i >= 0; --i) {
126 await modules[i].beforeAction(this.action);
127 }
128 await this.beforeAction();
129 if (!this.response.has()) {
130 await this.action.execute();
131 }
132 await this.afterAction();
133 // trigger module's afterAction from current to root
134 for (const module of modules) {
135 await module.afterAction(this.action);
136 }
137 this.response.end();
138 }
139
140 spawn (config, params) {
141 return ClassHelper.spawn(config, {
142 module: this.module,
143 user: this.user,
144 ...params
145 });
146 }
147
148 // ACTION
149
150 createAction (name) {
151 name = name || this.DEFAULT_ACTION;
152 return this.createInlineAction(name) || this.createMappedAction(name);
153 }
154
155 createInlineAction (name) {
156 const method = this[`action${StringHelper.idToCamel(name)}`];
157 if (typeof method === 'function') {
158 const config = this.INLINE_ACTION || this.module.InlineAction;
159 return this.spawn(config, {controller: this, method, name});
160 }
161 }
162
163 createMappedAction (name) {
164 if (Object.prototype.hasOwnProperty.call(this.ACTIONS, name)) {
165 return this.spawn(this.ACTIONS[name], {controller: this, name});
166 }
167 }
168
169 // EVENTS
170
171 beforeAction () {
172 // call await super.beforeAction() if override it
173 return this.trigger(this.EVENT_BEFORE_ACTION, new ActionEvent(this.action));
174 }
175
176 afterAction () {
177 // call await super.afterAction() if override it
178 return this.trigger(this.EVENT_AFTER_ACTION, new ActionEvent(this.action));
179 }
180
181 // REQUEST
182
183 isAjax () {
184 return this.req.xhr;
185 }
186
187 isGet () {
188 return this.req.method === 'GET';
189 }
190
191 isPost () {
192 return this.req.method === 'POST';
193 }
194
195 getHttpHeader (name) {
196 return this.req.get(name);
197 }
198
199 getCurrentRoute () {
200 return this.req.baseUrl + this.req.path;
201 }
202
203 getQueryParam (key, defaults) {
204 return ObjectHelper.getValue(key, this.req.query, defaults);
205 }
206
207 getQueryParams () {
208 return this.req.query;
209 }
210
211 getPostParam (key, defaults) {
212 return ObjectHelper.getValue(key, this.req.body, defaults);
213 }
214
215 getPostParams () {
216 return this.req.body;
217 }
218
219 // FLASH MESSAGES
220
221 setFlash (key, message, params) {
222 typeof this.req.flash === 'function'
223 ? this.req.flash(key, this.translate(message, params))
224 : this.log('error', 'Session flash not found', message);
225 }
226
227 getFlash (key) {
228 return typeof this.req.flash === 'function'
229 ? this.req.flash(key)
230 : null;
231 }
232
233 // RESPONSE
234
235 goHome () {
236 this.redirect(this.module.getHomeUrl());
237 }
238
239 goLogin () {
240 this.redirect(this.user.getLoginUrl());
241 }
242
243 goBack (url) {
244 this.redirect(this.user.getReturnUrl(url));
245 }
246
247 reload () {
248 this.setHttpStatus(200);
249 this.response.redirect(this.req.originalUrl);
250 }
251
252 redirect (url) {
253 this.response.redirect(this.createUrl(url));
254 }
255
256 setHttpStatus (code) {
257 this.response.code = code;
258 return this;
259 }
260
261 setHttpHeader () {
262 this.res.set(...arguments);
263 return this;
264 }
265
266 // RENDER
267
268 async render () {
269 this.send(await this.renderOnly(...arguments));
270 }
271
272 renderOnly (template, data) {
273 const model = this.createViewModel(template, {data});
274 return model
275 ? this.renderViewModel(model, template)
276 : this.renderTemplate(template, data);
277 }
278
279 async renderViewModel (model, template) {
280 const data = await model.getTemplateData();
281 return this.renderTemplate(template, data);
282 }
283
284 async renderTemplate (template, data) {
285 return this.getView().render(this.getViewFileName(template), data);
286 }
287
288 createViewModel (name, config = {}) {
289 return this.getView().createViewModel(this.getViewFileName(name), config);
290 }
291
292 getView () {
293 if (!this._view) {
294 this._view = this.createView();
295 }
296 return this._view;
297 }
298
299 createView (params) {
300 return this.spawn(this.ACTION_VIEW || this.module.ActionView, {
301 controller: this,
302 theme: this.module.get('view').getTheme(),
303 ...params
304 });
305 }
306
307 getViewFileName (name) {
308 name = typeof name !== 'string' ? String(name) : name;
309 return path.isAbsolute(name) ? name : (this.constructor.getViewDirectory() + name);
310 }
311
312 // SEND
313
314 send (data, code) {
315 this.response.send('send', data, code);
316 }
317
318 sendText (data, code) {
319 this.send(typeof data === 'string' ? data : String(data), code);
320 }
321
322 sendFile (data, code) {
323 this.response.send('sendFile', data, code);
324 }
325
326 sendJson (data, code) {
327 this.response.send('json', data, code);
328 }
329
330 sendStatus (code) {
331 this.response.send('sendStatus', code);
332 }
333
334 sendData (data, encoding) {
335 this.response.sendData(data, encoding);
336 }
337
338 // URL
339
340 getOriginalUrl () {
341 return this.req.originalUrl;
342 }
343
344 createUrl (...data) {
345 return this.urlManager.resolve(data.length > 1 ? data : data[0], this.getBaseName());
346 }
347
348 getHostUrl () {
349 return this.req.protocol +'://'+ this.req.get('host');
350 }
351
352 // SECURITY
353
354 getCsrfToken () {
355 return this.user.getCsrfToken();
356 }
357
358 async can (name, params) {
359 if (!await this.user.can(name, params)) {
360 throw new Forbidden;
361 }
362 }
363
364 // I18N
365
366 translate (message, params, source = 'app') {
367 if (Array.isArray(message)) {
368 return this.translate(...message);
369 }
370 if (message instanceof Message) {
371 return message.translate(this.i18n, this.language);
372 }
373 return source
374 ? this.i18n.translate(message, params, source, this.language)
375 : this.i18n.format(message, params, this.language);
376 }
377
378 translateMessageMap (data, ...args) {
379 data = {...data};
380 for (const key of Object.keys(data)) {
381 data[key] = this.translate(data[key], ...args);
382 }
383 return data;
384 }
385
386 format (value, type, params) {
387 if (this.language) {
388 params = {language: this.language, ...params};
389 }
390 return this.formatter.format(value, type, params);
391 }
392};
393module.exports.init();
394
395const path = require('path');
396const ClassHelper = require('../helper/ClassHelper');
397const FileHelper = require('../helper/FileHelper');
398const ObjectHelper = require('../helper/ObjectHelper');
399const StringHelper = require('../helper/StringHelper');
400const Forbidden = require('../error/http/Forbidden');
401const ActionEvent = require('./ActionEvent');
402const Response = require('../web/Response');
403const Message = require('../i18n/Message');
\No newline at end of file