UNPKG

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