UNPKG

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