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 |
|
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 |
|
36 | this.defaultConfiguration();
|
37 | }
|
38 |
|
39 | defaultConfiguration() {
|
40 | let env = process.env.NODE_ENV || 'development';
|
41 |
|
42 |
|
43 | this.set('env', env);
|
44 | this.set('adapter', 'express');
|
45 | this.enable('x-powered-by');
|
46 |
|
47 |
|
48 | this.locals = Object.create(null);
|
49 |
|
50 |
|
51 | this.mountPath = this.settings.mountPath || '/';
|
52 |
|
53 |
|
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 |
|
88 |
|
89 |
|
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 |
|
117 | app.settings = Object.assign({}, this.settings);
|
118 | app.settings.adapterOptions = Object.assign({}, this.get('adapterOptions') || {});
|
119 |
|
120 |
|
121 | app.beforeHooks = Object.assign({}, this.beforeHooks);
|
122 | app.afterHooks = Object.assign({}, this.afterHooks);
|
123 | app.afterErrorHooks = Object.assign({}, this.afterErrorHooks);
|
124 |
|
125 |
|
126 | app.locals = Object.assign({}, this.locals);
|
127 | app.locals.settings = app.settings;
|
128 |
|
129 |
|
130 | this.mountedApps.map((mountedApp => app.use(mountedApp)));
|
131 |
|
132 |
|
133 | this.methods.map((method) => app.define(method));
|
134 |
|
135 | return app;
|
136 | }
|
137 |
|
138 |
|
139 | use(fn, options) {
|
140 | if (typeof fn === 'function') {
|
141 |
|
142 | if (fn.prototype instanceof Controller) {
|
143 | return this._useController(new fn(), options);
|
144 | }
|
145 |
|
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 |
|
174 | fn(ctx.req, ctx.res, handleNext);
|
175 |
|
176 |
|
177 | onFinished(ctx.res, handleNext);
|
178 |
|
179 | function handleNext(err) {
|
180 | if (nextHandled) return;
|
181 |
|
182 | nextHandled = true;
|
183 |
|
184 |
|
185 | if (err) return reject(err);
|
186 |
|
187 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
239 | app.parent = this;
|
240 | app.mountPath = options.mountPath || ctrl.getMountPath() || '/';
|
241 |
|
242 | let methodNames = Object.keys(ctrl.__configs);
|
243 |
|
244 |
|
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 |
|
250 | return ctrl[methodName](ctx, next);
|
251 | });
|
252 | }
|
253 |
|
254 |
|
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 |
|
266 | methodNames.forEach(function(methodName) {
|
267 |
|
268 | if (~excepts.indexOf('*') || ~excepts.indexOf(methodName)) return;
|
269 | if (~onlies.indexOf('*') || ~onlies.indexOf(methodName)) {
|
270 | hookedNames.push(methodName);
|
271 | }
|
272 | });
|
273 |
|
274 |
|
275 | if (hookedNames.length) app[hookType](hookedNames, hookConfig.fn);
|
276 | }
|
277 | });
|
278 |
|
279 |
|
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 |
|
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 |
|
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 |
|
360 | let adapter = new Adapter(this, options);
|
361 |
|
362 | return adapter;
|
363 | }
|
364 |
|
365 | set(setting, val) {
|
366 | if (arguments.length === 1) {
|
367 |
|
368 | return magico.get(this.settings, setting);
|
369 | }
|
370 |
|
371 | debug('%s: set "%s" to %o', this.fullName(), setting, val);
|
372 |
|
373 |
|
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 |
|
406 | utils.addHookMethods(Application.prototype);
|
407 |
|
408 |
|
409 | utils.addHttpMethods(Application.prototype);
|
410 |
|
411 | module.exports = Application;
|