UNPKG

6.46 kBJavaScriptView Raw
1//
2// # Request Dispatcher
3//
4// Handles dispatching of request and the helper chain.
5//
6
7var domain = require('domain');
8var router = require('routes');
9var routeloader = require('./bootstrap/loadroutes');
10var urllib = require('url');
11var async = require('async');
12var cookie = require('cookie');
13
14//
15// ## Constructor
16//
17// Creates a new dispatcher with a router for every relevant request method.
18// The methods that are left out are handled separately or as GET requests.
19//
20// * **app**, the main app object.
21//
22var Dispatcher = function (app) {
23 this.app = app;
24 this.routers = {};
25 this.HTTPMethods = ['get', 'post', 'put', 'delete', 'patch'];
26
27 for (var i in this.HTTPMethods) {
28 this.routers[this.HTTPMethods[i]] = router();
29 }
30
31 this.preRequest = {};
32};
33
34//
35// ## Batch load
36//
37// Setup a set of routes.
38//
39// * **routes**, object containing routes.
40//
41Dispatcher.prototype.batchLoad = function (routes, callback) {
42 routeloader(routes, this.routers, callback);
43};
44
45
46//
47// ## Set Route
48//
49// Binds a route to a controller and action.
50//
51// * **method**, HTTP method this is route is for.
52// * **route**, the path to bind.
53// * **controller**, a string in the form of `controller.action`.
54//
55Dispatcher.prototype.setRoute = function (method, route, controller) {
56 this.routers[method].addRoute(route, controller);
57};
58
59//
60// ## Add pre-request helper
61//
62// Adds a function that will be run before a request is dispatched.
63//
64// Uses async.auto to dispatch requests.
65//
66// @see https://github.com/caolan/async#auto
67//
68// * **name**, the name of the pre-request handler. Can be used by other
69// handlers to create dependency graphs.
70// * **fn**, function to run. The function will recieve the `req` and `res`
71// objects as well a callback function that needs to be called when done.
72// Can also be an array where the values are names of handlers that need to
73// run before the one being added. The last element of the array should
74// always be a function.
75//
76Dispatcher.prototype.addPreRequestHelper = function (name, fn) {
77 this.preRequest[name] = fn;
78};
79
80//
81// ## Dispatch
82//
83// Handles a request.
84//
85Dispatcher.prototype.dispatch = function (req, res) {
86 var start = Date.now();
87 var self = this;
88 var info = this.app.getLogger('info');
89 var warn = this.app.getLogger('error');
90
91 var requestDomain = domain.create();
92 requestDomain.add(req);
93 requestDomain.add(res);
94
95 req.viewData = {};
96 res.viewData = {};
97
98 req.on('finish', function () {
99 info('[%dms] %s', Date.now() - start, req.url);
100 });
101
102 // Handle request error
103 requestDomain.on('error', function (err) {
104 try {
105 //warn('Error encoutered: %s\n%s', err, err.stack);
106 var errData = {
107 code: err.code || 500,
108 msg: err.message,
109 stack: err.stack
110 };
111
112 if (!res.headersSent) {
113 res.statusCode = errData.code;
114 }
115
116 if (req.isAJAX) {
117 res.end(JSON.stringify(errData));
118 }
119 else {
120 // Error templates can be placed in a folder called errors and
121 // named after the error code it wants to "be for".
122 var views = self.app.renderer.views;
123 var View = self.app.renderer.View;
124 var templateSuggestions = [
125 errData.code,
126 'error'
127 ];
128
129 var finalTemplate = false;
130 for (var template in templateSuggestions) {
131 var t = templateSuggestions[template];
132 if (typeof views['error/' + t] === 'function') {
133 finalTemplate = new View('error/' + t, errData);
134 }
135 }
136
137 // Force JSON output if we don't have a template.
138 if (!finalTemplate) {
139 req.end(JSON.stringify(errData));
140 }
141 else {
142 self.app.renderer.render(req, res, finalTemplate);
143 }
144 }
145 }
146 catch (er) {
147 res.end('Fatal error');
148 console.log(er.stack);
149 }
150 });
151
152 // Handle request
153 requestDomain.run(function () {
154 var method = req.method.toLowerCase();
155 var url = urllib.parse(req.url);
156 var target = self.routers[method].match(url.pathname);
157
158 // Check for 404
159 if (typeof target.fn !== 'string') {
160 var e404 = new Error('Not Found');
161 e404.code = 404;
162 throw e404;
163 }
164
165 var parts = target.fn.split('.');
166 var controller = self.app.getController(parts[0]);
167 var action = parts[1];
168
169 if (req.headers && req.headers.cookie) {
170 req.cookies = cookie.parse(req.headers.cookie);
171 }
172 else {
173 req.cookies = {};
174 }
175
176 req.isAJAX = self.requestIsAJAX(req);
177
178 self.dispatchPreRequest(req, res, function (err) {
179 // @TODO: Fix real error handling.
180 if (err) throw err;
181
182 if (!controller || typeof controller[action] !== 'function') {
183 res.writeHead(404);
184 res.end(action);
185 }
186 else {
187 var hasCallback = controller[action].length === 4;
188 if (hasCallback) {
189 controller[action](req, res, target, function (err, views) {
190 self.app.renderer.render(req, res, views);
191 });
192 }
193 else {
194 var views = controller[action](req, res, target);
195 if (views) {
196 self.app.renderer.render(req, res, views);
197 }
198 }
199 }
200 });
201 });
202};
203
204//
205// ## Dispatch Pre-request
206//
207// * **req**
208// * **res**
209// * **callback**, called when done.
210//
211Dispatcher.prototype.dispatchPreRequest = function (req, res, callback) {
212 var functions = {};
213
214 function dispatch(func, req, res) {
215 return function (fn, result) {
216 func(req, res, fn);
217 };
218 }
219
220 for (var name in this.preRequest) {
221 var fn = this.preRequest[name];
222 if (Array.isArray(fn)) {
223 fn = fn.slice(0);
224 var handler = fn.pop();
225 var newHandler = dispatch(handler, req, res);
226 fn.push(newHandler);
227 }
228 else if (typeof fn === 'function') {
229 fn = dispatch(fn, req, res);
230 }
231
232 functions[name] = fn;
233 }
234
235 async.auto(functions, callback);
236};
237
238//
239// ## Determine AJAX
240//
241// Determines if a request is done with AJAX or not.
242//
243// * **req**, request object.
244//
245// **Returns** true if request is made with AJAX.
246//
247Dispatcher.prototype.requestIsAJAX = function (req) {
248 var header = 'x-requested-with';
249 var url = urllib.parse(req.url, true);
250 var query = url.query;
251 return (req.headers[header] && req.headers[header] === 'XMLHttpRequest') ||
252 (query.json && query.json === 'true');
253};
254
255module.exports = Dispatcher;
256