1 | 'use strict';
|
2 |
|
3 | const EventEmitter = require('events');
|
4 | const assert = require('assert');
|
5 | const path = require('path');
|
6 | const magico = require('magico');
|
7 | const Promise = require('any-promise');
|
8 | const onFinished = require('on-finished');
|
9 | const debug = require('debug')('baiji:Application');
|
10 |
|
11 | const Controller = require('./Controller');
|
12 | const Method = require('./Method');
|
13 | const utils = require('./utils');
|
14 |
|
15 | class 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 |
|
41 | this.set('env', env);
|
42 | this.set('adapter', 'express');
|
43 | this.enable('x-powered-by');
|
44 |
|
45 |
|
46 | this.locals = Object.create(null);
|
47 |
|
48 |
|
49 | this.mountpath = this.settings.mountpath || '/';
|
50 |
|
51 |
|
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 |
|
77 |
|
78 |
|
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 |
|
106 | app.settings = Object.assign({}, this.settings);
|
107 | app.settings.adapterOptions = Object.assign({}, this.get('adapterOptions') || {});
|
108 |
|
109 |
|
110 | app.beforeHooks = Object.assign({}, this.beforeHooks);
|
111 | app.afterHooks = Object.assign({}, this.afterHooks);
|
112 | app.afterErrorHooks = Object.assign({}, this.afterErrorHooks);
|
113 |
|
114 |
|
115 | app.locals = Object.assign({}, this.locals);
|
116 | app.locals.settings = app.settings;
|
117 |
|
118 |
|
119 | this.mountedApps.map((mountedApp => app.use(mountedApp)));
|
120 |
|
121 |
|
122 | this.methods.map((method) => app.define(method));
|
123 |
|
124 | return app;
|
125 | }
|
126 |
|
127 |
|
128 | use(fn, options) {
|
129 | if (typeof fn === 'function') {
|
130 |
|
131 | if (fn.prototype instanceof Controller) {
|
132 | return this._useController(new fn(), options);
|
133 | }
|
134 |
|
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 |
|
161 | fn(ctx.req, ctx.res, handleNext);
|
162 |
|
163 |
|
164 | onFinished(ctx.res, handleNext);
|
165 |
|
166 | function handleNext(err) {
|
167 | if (nextHandled) return;
|
168 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
244 | methodNames.forEach(function(methodName) {
|
245 |
|
246 | if (~excepts.indexOf('*') || ~excepts.indexOf(methodName)) return;
|
247 | if (~onlies.indexOf('*') || ~onlies.indexOf(methodName)) {
|
248 | hookedNames.push(methodName);
|
249 | }
|
250 | });
|
251 |
|
252 |
|
253 | if (hookedNames.length) app[hookType](hookedNames, hookConfig.fn);
|
254 | }
|
255 | });
|
256 |
|
257 |
|
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 |
|
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 |
|
338 | return magico.get(this.settings, setting);
|
339 | }
|
340 |
|
341 | debug('%s: set "%s" to %o', this.fullName(), setting, val);
|
342 |
|
343 |
|
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 |
|
376 | utils.addHookMethods(Application.prototype);
|
377 |
|
378 | module.exports = Application;
|