UNPKG

11.9 kBJavaScriptView Raw
1var fs = require('fs');
2var path = require('path');
3var cache = {};
4
5var ctlSuffix = /_controller\.(js|coffee)/g;
6
7var debug = function(){};
8
9/**
10 * @export ControllerBridge
11 */
12module.exports = ControllerBridge;
13
14/**
15 * Bridge to kontroller package
16 *
17 * @param {Compound} rw - compound descriptor.
18 * @constructor
19 */
20function ControllerBridge(rw) {
21 this.compound = rw;
22 this.root = rw.root + '/app/controllers';
23 this.pool = {};
24
25 if (process.env.NODE_DEBUG && /controller/.test(process.env.NODE_DEBUG)) {
26 debug = function(x) {
27 rw.log(x);
28 };
29 }
30
31}
32
33/**
34 * Bridge config
35 *
36 * @deprecated Should be removed to instance.
37 */
38ControllerBridge.config = {
39 subdomain: {
40 tld: 2
41 }
42};
43
44/**
45 * Prints information about current pool size
46 */
47ControllerBridge.prototype.poolSize = function() {
48 var bridge = this;
49 Object.keys(bridge.pool).forEach(function(i) {
50 console.log(i, bridge.pool[i].length);
51 });
52};
53
54/**
55 * Caller for request handler
56 *
57 * @param {String} ns - namespace.
58 * @param {String} controller - name of controller.
59 * @param {String} action - name of action.
60 * @param {Object} conf - optional params (comes from route definition).
61 *
62 * TODO: remove NS param, move subdomain logic to compound-routes
63 *
64 * @return {Function(req, res, next)} request handler.
65 */
66ControllerBridge.prototype.uniCaller = function(ns, controller, action, conf) {
67 var app = this.compound.app;
68
69 return function(req, res, next) {
70
71 // Request hostname, or null if unknown
72 var hostname =
73 (req.headers && req.headers.host && req.headers.host.length)
74 ? req.headers.host.toLowerCase()
75 : null;
76
77 var subdomain = req.headers && req.headers.host &&
78 req.headers.host
79 .split('.')
80 .slice(0, -1 * ControllerBridge.config.subdomain.tld);
81
82 req.subdomain = subdomain && subdomain.join('.');
83
84 // Virtual Host match?
85 if (conf && conf.vhost && conf.vhost.length && hostname) {
86
87 // Virtual host is a wildcard?
88 var wildcard = (conf.vhost.charAt(0) === '.');
89
90 // Virtual host name (without leading wildcard character, if any)
91 var vhost =
92 (wildcard)
93 ? conf.vhost.substring(1).toLowerCase()
94 : conf.vhost.toLowerCase();
95
96 // Compare without regard to port number if the configuration vhost
97 // does not specify a port number. This lets us work with a proxy-
98 // server with little complication.
99 if (vhost.indexOf(":") < 0) {
100 // Strip port number from the request hostname
101 var portIndex = hostname.indexOf(":");
102 if (portIndex >= 0) {
103 hostname = hostname.substring(0, portIndex);
104 }
105 }
106
107 // Does the request match the Virtual Host?
108 var match;
109
110 // Wildcard virtual host: ".example.com" (12 chars) should match:
111 // 1. "www.example.com" (15 chars)
112 // 2. "example.com" (11 chars)
113 if (wildcard) {
114 // Condition #1 above
115 match =
116 (hostname.length > vhost.length) &&
117 (hostname.indexOf("." + vhost) === (hostname.length - conf.vhost.length));
118
119 // Condition #2 above
120 match = match || (vhost === hostname);
121 }
122 // Virtual Host exact match
123 else {
124 match = (vhost === hostname);
125 }
126
127 if (!match) {
128 // Virtual Host mismatch: go to the next route
129 return next();
130 }
131 }
132 // Subdomain match?
133 else if (conf && conf.subdomain && req.subdomain) {
134 if (conf.subdomain !== req.subdomain) {
135 if (conf.subdomain.match(/\*/)) {
136 var matched = true;
137 conf.subdomain.split('.').forEach(function(part, i) {
138 if (part === '*') return;
139 if (part !== subdomain[i]) matched = false;
140 });
141
142 if (!matched) return next(); // next route
143 } else return next();
144 }
145 }
146
147 var controllerName = ns + (controller || req.params.controller);
148
149 this.callControllerAction(controllerName, action || req.params.action, req, res, next);
150 }.bind(this);
151};
152
153ControllerBridge.prototype.callControllerAction = function (controller, action, req, res, next) {
154 debug('call controller ' + controller + ' action ' + action);
155 var ctl = this.loadController(controller);
156
157 var resEnd = res.end;
158 var endCalled = false;
159 res.end = function () {
160 endCalled = true;
161 resEnd.apply(res, [].slice.call(arguments));
162 };
163 // TODO: move all before-processing calls to separate method
164 if (ctl && ctl._helpers) {
165 delete ctl._helpers;
166 }
167 this.compound.emit('before controller', ctl, action, res, res, next);
168 debug('perform ' + controller + '#' + action);
169 ctl.perform(action, req, res, function(err) {
170 debug(controller + '#' + action + ' completed');
171 // console.log((new Error).stack);
172 if (ctl && ctl._backToPool) {
173 ctl._backToPool();
174 } else {
175 ctl = null;
176 }
177 if (err) {
178 next(err);
179 } else if (!endCalled) {
180 next();
181 }
182 });
183}
184
185/**
186 * Load controller (from pool, or create new)
187 *
188 * @param {String} controllerFullName - name of controller including namespace.
189 * @return {Controller} - controller instance.
190 */
191ControllerBridge.prototype.loadController = function(controllerFullName) {
192 if (this.compound.app.enabled('watch')) {
193 return this.getInstance(controllerFullName);
194 }
195 var pool = this.pool;
196 if (!pool[controllerFullName]) {
197 pool[controllerFullName] = [];
198 }
199 if (pool[controllerFullName].length) {
200 debug('found controller ' + controllerFullName + ' from pool');
201 return pool[controllerFullName].shift();
202 }
203 debug('creating new controller');
204 var ctl = this.getInstance(controllerFullName);
205 ctl._backToPool = function() {
206 debug('return controller ' + controllerFullName + ' to pool');
207 pool[controllerFullName].push(ctl);
208 };
209 return ctl;
210};
211
212/**
213 * Create new instance of controller
214 *
215 * @param {String} name - name of controller.
216 * @return {Controller} - instance of controller.
217 */
218ControllerBridge.prototype.getInstance = function getInstance(name) {
219 var compound = this.compound;
220 var app = compound.app;
221 var exts = this.compound.controllerExtensions;
222 var controllers = compound.structure.controllers;
223
224 var Controller, buildFromEval;
225 if (name in controllers && !name.match(/_controller$/)) {
226 Controller = compound.controller.constructClass(name, controllers[name]);
227 Controller.skipLogging = controllers[name].skipLogging;
228 } else {
229 Controller = compound.controller.constructClass(name);
230 buildFromEval = controllers[name + '_controller'];
231 }
232
233 // add controller extensions
234 if (exts) {
235 Object.keys(exts).forEach(function(k) {
236 Controller.prototype[k] = exts[k];
237 });
238 }
239
240 Controller.prototype.compound = compound;
241 Controller.prototype.app = app;
242 Controller.prototype.pathTo = compound.map.pathTo;
243 // TODO: deprecate path_to
244 Controller.prototype.path_to = compound.map.pathTo;
245
246 // add models
247 for (var m in this.compound.models) {
248 Controller.prototype[m] = compound.models[m];
249 }
250
251 // instantiate
252 var ctl = new Controller;
253 if (buildFromEval) {
254 ctl.reset();
255
256 // and run through configurator code
257 // TODO: pass patched compound.rootModule for more accurate require('./...')
258 ctl.build(buildFromEval, {
259 id: compound.rootModule.filename,
260 filename: compound.rootModule.filename,
261 paths: [path.join(compound.root, 'node_modules')]
262 });
263 }
264
265 compound.emit('controller instance', ctl);
266
267 if (compound.app.disabled('quiet') || compound.app.get('quiet') === 'all') {
268 var $ = compound.utils.stylize.$;
269 var log = compound.utils.debug;
270 var skipLogging = ctl.constructor.skipLogging;
271 var logger = ctl.getLogger();
272 logger.on('beforeProcessing', function(ctl) {
273 if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) {
274 return;
275 }
276 var req = ctl.context.req;
277
278 log(
279 '\n' + $((new Date).toString()).yellow + ' ' +
280 $(ctl.id || '').bold +
281 '\n' +
282 $(req.method).bold + ' ' +
283 $(req.app && req.app.path()).blue +
284 $(req.url).grey +
285 ' controller: ' + $(ctl.controllerName).cyan +
286 ' action: ' + $(ctl.actionName).blue
287 );
288 if (req.query && Object.keys(req.query).length) {
289 log($('Query: ').bold + JSON.stringify(req.query));
290 }
291 if (req.body && req.method !== 'GET') {
292 var filterParams = ctl.constructor.filterParams || [];
293 var filteredBody = (function filter(obj) {
294 if (typeof obj !== 'object' || obj == null) {
295 return obj;
296 }
297 var filtered = {};
298 Object.keys(obj).forEach(function(param) {
299 if (!filterParams.some(function(f) {
300 return param.search(f) !== -1;
301 })) {
302 filtered[param] = filter(obj[param]);
303 } else {
304 filtered[param] = '[FILTERED]';
305 }
306 });
307 return filtered;
308 })(req.body);
309 log($('Body: ').bold + JSON.stringify(filteredBody));
310 }
311 if (req.params && typeof req.params === 'object' &&
312 Object.keys(req.params).length) {
313 var filteredParams = {};
314 Object.keys(req.params).forEach(function (param) {
315 filteredParams[param] = req.params[param];
316 });
317 log($('Params: ').bold + JSON.stringify(filteredParams));
318 }
319 });
320 logger.on('afterProcessing', function(ctl, duration) {
321 if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) {
322 return;
323 }
324 var req = ctl.context.req;
325 log('Handling ' + $(req.method).bold + ' ' + $(req.url).grey +
326 ' completed in ' + duration + ' ms');
327 });
328 logger.on('beforeHook', function(ctl, action) {
329 if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) {
330 return;
331 }
332 if (ctl.context.inAction) {
333 log('>>> perform ' + $(action).bold.cyan);
334 } else {
335 log('>>> perform ' + $(action).bold.grey);
336 }
337 });
338 logger.on('afterHook', function(ctl, action, duration) {
339 if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) {
340 return;
341 }
342 log('<<< ' + action + ' [' + duration + ' ms]');
343 });
344 logger.on('render', function(file, layout, duration) {
345 if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) {
346 return;
347 }
348 log('Rendered ' + $(file).grey + ' using layout ' + $(layout).grey +
349 ' in ' + duration + 'ms');
350 });
351 }
352
353 return ctl;
354};