UNPKG

10.9 kBJavaScriptView Raw
1/**
2 * Copyright 2014 Skytap Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 **/
16
17var _ = require('underscore'),
18 extend = require('extend'),
19 Promise = require('bluebird'),
20 events = require('events'),
21 Logger = require('./logger'),
22 Config = require('./config'),
23 Template = require('./template'),
24 Environment = require('./environment'),
25 Indentifier = require('./identifier'),
26 RenderError = require('./errors/rendererror');
27
28/**
29 * Module with common controller functionality.
30 **/
31function Controller () {}
32
33_.extend(Controller.prototype, events.EventEmitter.prototype, {
34
35 DEFAULT_ERROR_MESSAGE : 'An error occurred. Please try again.',
36
37 ERROR_TEMPLATE: 'error',
38
39 before : {},
40
41 templatePath : '/lib/template',
42
43 filtersByRoute : [],
44
45 //////////////////////////////////////////////////////////////////////////
46 // Boilerplate REST methods /////////////////////////////////////////////
47 ////////////////////////////////////////////////////////////////////////
48
49 /**
50 * Controller.index(request, response, next)
51 * - request (Object): Express request object
52 * - response (Object): Express response object
53 * - next (Function): Express function to invoke the next handler
54 *
55 * GET /resource
56 **/
57 index : function(request, response, next) {
58 next();
59 },
60
61 /**
62 * Controller.new(request, response, next)
63 * - request (Object): Express request object
64 * - response (Object): Express response object
65 * - next (Function): Express function to invoke the next handler
66 *
67 * GET /resource/new
68 **/
69 new : function(request, response, next) {
70 next();
71 },
72
73 /**
74 * Controller.create(request, response, next)
75 * - request (Object): Express request object
76 * - response (Object): Express response object
77 * - next (Function): Express function to invoke the next handler
78 *
79 * POST /resource
80 **/
81 create : function(request, response, next) {
82 next();
83 },
84
85 /**
86 * Controller.show(request, response, next)
87 * - request (Object): Express request object
88 * - response (Object): Express response object
89 * - next (Function): Express function to invoke the next handler
90 *
91 * GET /resource/:id
92 **/
93 show : function(request, response, next) {
94 next();
95 },
96
97 /**
98 * Controller.edit(request, response, next)
99 * - request (Object): Express request object
100 * - response (Object): Express response object
101 * - next (Function): Express function to invoke the next handler
102 *
103 * GET /resource/:id/edit
104 **/
105 edit : function(request, response, next) {
106 next();
107 },
108
109 /**
110 * Controller.update(request, response, next)
111 * - request (Object): Express request object
112 * - response (Object): Express response object
113 * - next (Function): Express function to invoke the next handler
114 *
115 * PUT /resource/:id
116 **/
117 update : function(request, response, next) {
118 next();
119 },
120
121 /**
122 * Controller.destroy(request, response, next)
123 * - request (Object): Express request object
124 * - response (Object): Express response object
125 * - next (Function): Express function to invoke the next handler
126 *
127 * DELETE /resource/:id
128 **/
129 destroy : function(request, response, next) {
130 next();
131 },
132
133 //////////////////////////////////////////////////////////////////////////
134 // Public methods ///////////////////////////////////////////////////////
135 ////////////////////////////////////////////////////////////////////////
136
137 /**
138 * Controller.addFiltersForHandler(url, handler)
139 * - url (String)
140 * - handler (String)
141 **/
142 addFiltersForHandler : function (url, handler) {
143 var self = this;
144
145 if (this.filtersByRoute[url] === undefined) {
146 this.filtersByRoute[url] = {};
147 }
148
149 this.filtersByRoute[url][handler] = [];
150
151 _.each(this.before, function cacheFilters (handlers, filter) {
152 if (handlers.indexOf(handler) !== -1 || handlers[0] === 'all') {
153 self.filtersByRoute[url][handler].push(filter);
154 }
155 });
156 },
157
158 /**
159 * Controller.getFilters(url, handler) -> Array
160 * - url (String)
161 * - handler (String)
162 **/
163 getFilters : function(url, handler) {
164 return this.filtersByRoute[url][handler];
165 },
166
167 /**
168 * Controller.render(request, response, templateName, templateValues, send) -> Promise
169 * - request (Object): Express request object
170 * - response (Object): Express response object
171 * - templateName (String)
172 * - templateValues (Object)
173 * - send (Boolean)
174 **/
175 render : function (request, response, templateName, templateValues, send) {
176 var self = this,
177 defaults = {
178 layout : 'layouts/application',
179 title : '',
180 stylesheets : [],
181 response : response,
182 request : request,
183 flash : {},
184 Config : Config,
185 _content : {}
186 },
187 templateMixins = Template.getMixins(),
188 send = (typeof send === 'undefined') ? true : send,
189 startTime;
190
191 return new Promise(function (resolve, reject) {
192 templateValues = _.extend(
193 defaults,
194 templateValues
195 );
196
197 _.each(templateMixins, function (Helper, name) {
198 templateValues[name] = Helper.bind(templateValues);
199 });
200
201 _.each(templateValues, function (value, key) {
202 response.locals[key] = value;
203 });
204
205 startTime = Date.now();
206
207 response.on('finish', function () {
208 self.emit('render-finished', self, request, startTime, templateName);
209 self._requestFinished(request);
210 });
211
212 response.render(templateName, { layout : templateValues.layout }, function (error, result) {
213 Logger.debug(request.method + ' ' + request.url + ' DONE', request);
214
215 Logger.info(
216 'Request for ' + request.minorjs.controller.name + '#' +
217 request.minorjs.controller.action + ' done in ' +
218 (Date.now() - request.minorjs.start) + 'ms',
219 request
220 );
221
222 if (error) {
223 self._handleRenderError(request, response, error, templateName);
224 reject(new RenderError());
225 } else {
226 if (send) {
227 response.send(result);
228 }
229 resolve(result);
230 }
231 });
232 });
233 },
234
235 //////////////////////////////////////////////////////////////////////////
236 // Psuedo-private methods ///////////////////////////////////////////////
237 ////////////////////////////////////////////////////////////////////////
238
239 /**
240 * Controller._getFullError() -> String
241 **/
242 _getFullError : function (request, response, error, incidentId) {
243 return 'Incident ' + incidentId + ' - ' + error.stack;
244 },
245
246 /**
247 * Controller._getUserError() -> String
248 **/
249 _getUserError : function (request, response, error, incidentId) {
250 return this.DEFAULT_ERROR_MESSAGE + ' (' + incidentId + ')';
251 },
252
253 /**
254 * Controller._handleError(request, response, error)
255 **/
256 _handleError : function (request, response, error) {
257 if (error instanceof RenderError) {
258 // error was already handled
259 return;
260 }
261
262 var incidentId = Indentifier.generate(),
263 fullError = this._getFullError(request, response, error, incidentId),
264 userError;
265
266 Logger.error(fullError, request);
267
268 if (request.xhr) {
269 this._handleXhrError(request, response, error);
270 } else {
271 this._handleNormalError(request, response, error, incidentId);
272 }
273 },
274
275 /**
276 * Controller._handleXhrError(request, response, error)
277 **/
278 _handleXhrError : function (request, response, error) {
279 response.status(500).send(
280 {
281 success : false,
282 error : error
283 }
284 );
285 },
286
287 /**
288 * Controller._handleNormalError(request, response, error, incidentId)
289 **/
290 _handleNormalError : function (request, response, error, incidentId) {
291 var userError = this._getUserError(request, response, error, incidentId);
292
293 response.status(500);
294
295 this.render(
296 request,
297 response,
298 this.ERROR_TEMPLATE,
299 {
300 userError : userError,
301 error : Environment.isDevelopment() ? error : null,
302 layout : 'layouts/error'
303 }
304 );
305 },
306
307 /**
308 * Controller._handleRenderError(request, response, error, templateName)
309 * - request (Object): Request object.
310 * - response (Object): Response object.
311 * - error (Object)
312 * - templateName (String)
313 **/
314 _handleRenderError: function (request, response, error, templateName) {
315 if (templateName === this.ERROR_TEMPLATE) {
316 var incidentId = Indentifier.generate(),
317 fullError = this._getFullError(request, response, error, incidentId),
318 userError = this._getUserError(request, response, error, incidentId);
319 Logger.error('Could not render error page. ' + fullError, request);
320 response.send(userError);
321 } else {
322 this._handleError(request, response, error);
323 }
324 },
325
326 /**
327 * Controller._requestFinished(request)
328 * - request (Object): Request object.
329 **/
330 _requestFinished : function (request) {
331 var results = extend(
332 {
333 method : request.method,
334 time : Date.now() - request.minorjs.start
335 },
336 request.minorjs
337 );
338 this.emit('request-finished', request, results);
339 }
340});
341
342module.exports = Controller;