UNPKG

10.4 kBJavaScriptView Raw
1'use strict';
2
3const EventEmitter = require('events');
4const assert = require('assert');
5const path = require('path');
6const magico = require('magico');
7const Promise = require('any-promise');
8const onFinished = require('on-finished');
9const debug = require('debug')('baiji:Application');
10
11const Controller = require('./Controller');
12const Method = require('./Method');
13const utils = require('./utils');
14
15class Application extends EventEmitter {
16 constructor(name, settings) {
17 super();
18 name = name || utils.getName();
19 settings = settings || {};
20
21 assert(typeof name === 'string', `${name} is not valid name`);
22 this.name = name;
23
24 this.beforeHooks = {};
25 this.afterHooks = {};
26 this.afterErrorHooks = {};
27
28 this.settings = Object.assign({}, settings);
29
30 this.methods = [];
31
32 this.mountedApps = [];
33
34 this.defaultConfiguration();
35 }
36
37 defaultConfiguration() {
38 let env = process.env.NODE_ENV || 'development';
39
40 // Default settings
41 this.set('env', env);
42 this.set('adapter', 'express');
43 this.enable('x-powered-by');
44
45 // Setup locals
46 this.locals = Object.create(null);
47
48 // Top-most app is mounted at /
49 this.mountpath = this.settings.mountpath || '/';
50
51 // Default locals
52 this.locals.settings = this.settings;
53 }
54
55 setName(name) {
56 this.name = name;
57 return this;
58 }
59
60 getName() {
61 return this.name;
62 }
63
64 define(method) {
65 if (method instanceof Method) {
66 method = method.clone();
67 } else {
68 method = Method.create.apply(Method, arguments);
69 }
70
71 method.parent = this;
72 this.methods.push(method);
73 return this;
74 }
75
76 // Provides a nice, tested, standardized way of adding plugins to a
77 // `Baiji` instance, injecting the current instance into the plugin,
78 // which should be a module.exports.
79 plugin(plugin, options) {
80 if (typeof plugin === 'string') {
81 try {
82 require(path.join(__dirname, 'plugins', plugin))(this, options);
83 } catch (e) {
84 if (e.code !== 'MODULE_NOT_FOUND') {
85 throw e;
86 }
87 }
88 } else if (Array.isArray(plugin)) {
89 plugin.each((p) => {
90 this.plugin(p, options);
91 });
92 } else if (typeof plugin === 'function') {
93 plugin(this, options);
94 } else {
95 throw new Error(`Invalid plugin: ${plugin} with options ${options}`);
96 }
97 return this;
98 }
99
100 clone() {
101 let app = new Application(this.getName());
102 app.parent = this.parent;
103 app.mountpath = this.mountpath;
104
105 // Merge settings
106 app.settings = Object.assign({}, this.settings);
107 app.settings.adapterOptions = Object.assign({}, this.get('adapterOptions') || {});
108
109 // Merge hooks
110 app.beforeHooks = Object.assign({}, this.beforeHooks);
111 app.afterHooks = Object.assign({}, this.afterHooks);
112 app.afterErrorHooks = Object.assign({}, this.afterErrorHooks);
113
114 // Merge locals
115 app.locals = Object.assign({}, this.locals);
116 app.locals.settings = app.settings;
117
118 // Remount subapps
119 this.mountedApps.map((mountedApp => app.use(mountedApp)));
120
121 // Redefine methods
122 this.methods.map((method) => app.define(method));
123
124 return app;
125 }
126
127 // support Controller, Application, Express
128 use(fn, options) {
129 if (typeof fn === 'function') {
130 // if fn extends Controller, initialize a new instance
131 if (fn.prototype instanceof Controller) {
132 return this._useController(new fn(), options);
133 }
134 // otherwise use as middleware function
135 else {
136 return this._useFunctionMiddleware(fn, options);
137 }
138 }
139 if (fn instanceof Application) return this._useApplication(fn, options);
140 if (fn instanceof Controller) return this._useController(fn, options);
141
142 assert(false, `${fn} is not a valid middleware or baiji app`);
143 }
144
145 _useFunctionMiddleware(fn, options) {
146 assert(typeof fn === 'function', `${fn} is not a valid middleware function`);
147 if (!options) options = { name: null, mountpath: '/' };
148 options.http = options.http || { path: options.mountpath || '/', verb: 'use' };
149 options.adapter = options.adapter || this.get('adapter');
150
151 let name = options.name || utils.getName(fn);
152 let method;
153
154 switch (options.adapter) {
155 case 'express':
156 method = new Method(name, options, function(ctx, next) {
157 return new Promise(function(resolve, reject) {
158 let nextHandled = false;
159
160 // Execute main function middleware
161 fn(ctx.req, ctx.res, handleNext);
162
163 // Handle after hooks when response finished
164 onFinished(ctx.res, handleNext);
165
166 function handleNext(err) {
167 if (nextHandled) return;
168 // Mark as handled
169 nextHandled = true;
170 if (err) return reject(err);
171 resolve(next());
172 }
173 });
174 });
175 break;
176 case 'socketio':
177 method = new Method(name, options, function(ctx, next) {
178 return new Promise(function(resolve, reject) {
179 // Call middleware and let baiji handle error
180 fn(ctx.socket, function(err) {
181 if (err) return reject(err);
182 resolve(next());
183 });
184 });
185 });
186 break;
187 default:
188 method = new Method(name, options, fn);
189 break;
190 }
191
192 return this.define(method);
193 }
194
195 _useApplication(app, options) {
196 assert(app instanceof Application, `${app} is not a valid Application instance`);
197 if (!options) options = {};
198 app = app.clone();
199 app.parent = this;
200 app.mountpath = options.mountpath || app.mountpath || '/';
201
202 // Merge settings
203 let adapterOptions = app.get('adapterOptions');
204 app.settings = Object.assign(app.settings, this.settings);
205 app.settings.adapterOptions = Object.assign(adapterOptions, this.get('adapterOptions'));
206
207 this.mountedApps.push(app);
208
209 return this;
210 }
211
212 // Transfer Controller instance into an Application instance
213 _useController(ctrl, options) {
214 assert(ctrl instanceof Controller, `${ctrl} is not a valid Controller instance`);
215 if (!options) options = {};
216 let name = options.name || ctrl.name || utils.getName();
217 let app = new Application(name, Object.assign({}, this.settings));
218 app.parent = this;
219 app.mountpath = options.mountpath || ctrl.mountpath || '/';
220
221 let methodNames = Object.keys(ctrl.__routes);
222
223 // Loop through ctrl.__routes and add methods
224 for (let methodName in ctrl.__routes) {
225 let methodConfig = ctrl.route(methodName);
226 assert(typeof ctrl[methodName] === 'function', `No method named '${methodName}' defined for Controller '${name}'`);
227 app.define(methodName, methodConfig, function(ctx, next) {
228 return ctrl[methodName](ctx, next);
229 });
230 }
231
232 // Loop through ctrl.__hooksConfig and add hook for app
233 utils.hookTypes.forEach(function(hookType) {
234 let hooks = ctrl.__hooksConfig[hookType] || {};
235 for (let hookName in hooks) {
236 let hookConfig = hooks[hookName];
237
238 let onlies = hookConfig.options.only;
239 let excepts = hookConfig.options.except;
240
241 let hookedNames = [];
242
243 // add hook for allowed methods
244 methodNames.forEach(function(methodName) {
245 // except has higher priority
246 if (~excepts.indexOf('*') || ~excepts.indexOf(methodName)) return;
247 if (~onlies.indexOf('*') || ~onlies.indexOf(methodName)) {
248 hookedNames.push(methodName);
249 }
250 });
251
252 // apply hook
253 if (hookedNames.length) app[hookType](hookedNames, hookConfig.fn);
254 }
255 });
256
257 // Mount app
258 this.mountedApps.push(app);
259
260 return this;
261 }
262
263 searchHooksByType(type) {
264 assert(typeof type === 'string' && ~utils.hookTypes.indexOf(type), `${type} is not a valid type`);
265
266 let typeName = `${type}Hooks`;
267
268 let hooks = utils.addPrefix(this[typeName], this.name);
269
270 this.mountedApps.forEach((app) => {
271 hooks = Object.assign(hooks, utils.addPrefix(app.searchHooksByType(type), this.name));
272 });
273
274 return hooks;
275 }
276
277 allMethods() {
278 let methods = [].concat(this.methods);
279 this.mountedApps.forEach(function(app) {
280 methods = methods.concat(app.allMethods());
281 });
282 return methods;
283 }
284
285 composedMethods() {
286 let beforeHooks = this.searchHooksByType('before');
287 let afterHooks = this.searchHooksByType('after');
288 let afterErrorHooks = this.searchHooksByType('afterError');
289
290 return this.allMethods()
291 .map((method) => {
292 let name = method.fullName();
293
294 method.compose(
295 utils.filterHooks(beforeHooks, name),
296 utils.filterHooks(afterHooks, name),
297 utils.filterHooks(afterErrorHooks, name)
298 );
299
300 return method;
301 });
302 }
303
304 callback() {
305 let adapter = this.adapter();
306 return adapter.callback();
307 }
308
309 listen() {
310 let adapter = this.adapter();
311 return adapter.listen.apply(adapter, arguments);
312 }
313
314 adapter(name, options) {
315 name = name || this.get('adapter');
316 options = options || {};
317 let Adapter;
318 try {
319 Adapter = require(path.join(__dirname, 'adapters', name));
320 } catch (e) {
321 if (e.code === 'MODULE_NOT_FOUND') assert(false, `Adapter for '${name}' not found!`);
322 throw e;
323 }
324
325 // Initialize Adapter instance with baiji instance
326 let adapter = new Adapter(this, options);
327
328 return adapter;
329 }
330
331 get(setting) {
332 return this.set(setting);
333 }
334
335 set(setting, val) {
336 if (arguments.length === 1) {
337 // app.get(setting)
338 return magico.get(this.settings, setting);
339 }
340
341 debug('%s: set "%s" to %o', this.fullName(), setting, val);
342
343 // Set value by setting
344 magico.set(this.settings, setting, val);
345
346 return this;
347 }
348
349 enabled(setting) {
350 return Boolean(this.set(setting));
351 }
352
353 disabled(setting) {
354 return !this.set(setting);
355 }
356
357 enable(setting) {
358 return this.set(setting, true);
359 }
360
361 disable(setting) {
362 return this.set(setting, false);
363 }
364
365 fullName() {
366 return this.parent ? `${this.parent.fullName()}.${this.name}` : this.name;
367 }
368
369 fullPath() {
370 let mountpath = this.mountpath || '/';
371 return this.parent ? path.join(this.parent.fullPath(), mountpath) : mountpath;
372 }
373}
374
375// Add hooks
376utils.addHookMethods(Application.prototype);
377
378module.exports = Application;