UNPKG

10.1 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2018 One Hill Technologies, LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17const path = require ('path');
18const {ensureDir} = require ('fs-extra');
19const debug = require ('debug')('blueprint:app');
20const assert = require ('assert');
21const { forOwn, get, isArray, mapValues } = require ('lodash');
22
23const lookup = require ('./-lookup');
24const { BO, computed } = require ('base-object');
25const BPromise = require ('bluebird');
26
27const ApplicationModule = require ('./application-module');
28const ModuleLoader = require ('./module-loader');
29const RouterBuilder = require ('./router-builder');
30const Server = require ('./server');
31const Loader = require ('./loader');
32
33const { Events } = require ('./messaging');
34
35const DEFAULT_APPLICATION_NAME = '<unnamed>';
36const APPLICATION_MODULE_NAME = '$';
37
38/**
39 * @class Application
40 *
41 * The main application.
42 */
43module.exports = BO.extend (Events, {
44 /// The started state of the application.
45 started: false,
46
47 /// The application module for the application.
48 _appModule: null,
49
50 /// The loader used by the application module.
51 _defaultLoader: new Loader (),
52
53 /// The temporary path for the application.
54 tempPath: computed ({
55 configurable: true,
56 get () { return path.resolve (this.appPath, '.blueprint'); }
57 }),
58
59 viewsPath: computed ({
60 get () { return this._appModule.viewsPath; }
61 }),
62
63 /// Resource loaded by the application.
64 resources: computed ({
65 get () { return this._appModule.resources; }
66 }),
67
68 /// The server used by the application.
69 server: computed ({
70 get () { return this._server; }
71 }),
72
73 module: computed ({
74 get () { return this._appModule; }
75 }),
76
77 init () {
78 this._super.call (this, ...arguments);
79 this._modules = {};
80
81 // First, make sure the temp directory for the application exist. Afterwards,
82 // we can progress with configuring the application.
83 this._appModule = new ApplicationModule ({
84 name: APPLICATION_MODULE_NAME,
85 app: this,
86 modulePath: this.appPath
87 });
88
89 this._server = new Server ({app: this});
90 },
91
92 /**
93 * Configure the application.
94 */
95 configure () {
96 return ensureDir (this.tempPath)
97 .then (() => this._loadConfigurationFiles ())
98 .then (configs => {
99 // Store the loaded configuration files.
100 this.configs = configs;
101 this.name = get (configs, 'app.name', DEFAULT_APPLICATION_NAME);
102
103 // Load all the modules for the application that appear in the node_modules
104 // directory. We consider these the auto-loaded modules for the application.
105 // We handle these before the modules that are explicitly loaded by the application.
106
107 let moduleLoader = new ModuleLoader ({app: this});
108 moduleLoader.on ('loading', module => this._modules[module.name] = module);
109
110 return moduleLoader.load ();
111 })
112 // Now, we can configure the module portion of the application since we know all
113 // dependent artifacts needed by the application will be loaded.
114 .then (() => this._appModule.configure ())
115 .then (() => {
116 // Allow the loaded services to configure themselves.
117 let {services} = this.resources;
118
119 return BPromise.props (mapValues (services, (service, name) => {
120 debug (`configuring service ${name}`);
121
122 return service.configure ();
123 }));
124 })
125 .then (() => this._server.configure (this.configs.server))
126 // Import the views of the application into the server. The views of the
127 // application will overwrite any views previously imported when we loaded
128 // an application module.
129 .then (() => this._appModule.hasViews ? this._server.importViews (this._appModule.viewsPath) : null)
130 .then (() => {
131 const builder = new RouterBuilder ({app: this});
132 const {routers} = this.resources;
133
134 return builder.addRouter ('/', routers).build ();
135 })
136 .then (router => {
137 // Install the built router into the server.
138 this._server.setMainRouter (router);
139 return this.emit ('blueprint.app.initialized', this);
140 })
141 .then (() => this);
142 },
143
144 /**
145 * Destroy the application.
146 */
147 destroy () {
148 return this._server.close ().then (() => {
149 // Instruct each service to destroy itself.
150 let { services } = this.resources;
151
152 return BPromise.props (mapValues (services, (service, name) => {
153 debug (`destroying service ${name}`);
154 return service.destroy ();
155 }));
156 });
157 },
158
159 /**
160 * Add an application module to the application. An application module can only
161 * be added once. Two application modules are different if they have the same
162 * name, not module path. This will ensure we do not have the same module in
163 * different location added to the application more than once.
164 */
165 addModule (name, appModule) {
166 if (this._modules.hasOwnProperty (name))
167 throw new Error (`duplicate module ${name}`);
168
169 return this._importViewsFromModule (appModule)
170 .then (() => { this._appModule.merge (appModule) })
171 .then (() => {
172 this._modules[name] = appModule;
173 return this;
174 });
175 },
176
177 _importViewsFromModule (appModule) {
178 if (!appModule.hasViews)
179 return Promise.resolve ();
180
181 return this._server.importViews (appModule.viewsPath);
182 },
183
184 /**
185 * Start the application. This method connects to the database, creates a
186 * new server, and starts listening for incoming messages.
187 */
188 start () {
189 // Notify the listeners that we are able to start the application. This
190 // will allow them to do any preparations.
191 this.emit ('blueprint.app.starting', this);
192
193 // Start all the services.
194 let {services} = this.resources;
195 let promises = mapValues (services, (service, name) => {
196 debug (`starting service ${name}`);
197
198 return service.start ();
199 });
200
201 return BPromise.props (promises)
202 .then (() => this._server.listen ())
203 .then (() => {
204 // Notify all listeners that the application has started.
205 this.started = true;
206 return this.emit ('blueprint.app.started', this);
207 });
208 },
209
210 /**
211 * Restart the application.
212 */
213 restart () {
214 return this.emit ('blueprint.app.restart', this);
215 },
216
217 /**
218 * Lookup a component, including configurations, in the application. The component can
219 * also be located in a module. This allows the client to search for a specific component
220 * if another module overwrites it. The expected pattern for the component is:
221 *
222 * type:name
223 * type:module:name
224 *
225 * Here are a few examples:
226 *
227 * config:app
228 * controller:hello
229 * model:a.b.user
230 * model:personal:a.b.user
231 *
232 * @param component
233 * @returns {*}
234 */
235 lookup (component) {
236 // The component can be an array, or a string. We allow for an array because
237 // the name at any given level could have a period. This would be treated as a
238 // a nested name, and cause the search to go down one level.
239
240 if (isArray (component)) {
241 if (component[0] === 'config')
242 return get (this.configs, component.slice (1));
243 else
244 return lookup (this.resources, component);
245 }
246 else if (component.startsWith ('config:')) {
247 // The configuration components are a special case because we do not
248 // lump them with the other resources that can be defined in a module.
249 const name = component.slice (7);
250 return get (this.configs, name);
251 }
252 else {
253 // Split the component name into its parts. If there are 2 parts, then we
254 // can search the merged resources for the application. If there are 3 parts,
255 // then we need to locate the target module, and search its resources.
256
257 const parts = component.split (':');
258
259 if (parts.length === 2) {
260 return lookup (this.resources, component);
261 }
262 else if (parts.length === 3) {
263 // Look for the module.
264 const targetModule = this._modules[parts[1]];
265 assert (!!targetModule, `The module named ${targetModule} does not exist.`);
266
267 // Construct the name of the target component by discarding the module
268 // name from the original component name.
269
270 const name = `${parts[0]}:${parts[2]}`;
271 return targetModule.lookup (name);
272 }
273 else {
274 throw new Error ('The component name is invalid.');
275 }
276 }
277 },
278
279 /**
280 * Load an application asset.
281 *
282 * @param filename
283 * @param opts
284 * @param callback
285 * @returns {*}
286 */
287 asset (filename, opts) {
288 return this._appModule.asset (filename, opts);
289 },
290
291 assetSync (filename, opts) {
292 return this._appModule.assetSync (filename, opts);
293 },
294
295 /**
296 * Mount a router. The returned router is an Express router that can be
297 * bound to any path in the router specification via the `use` property.
298 *
299 * @param routerName
300 */
301 mount (routerName) {
302 debug (`mounting router ${routerName}`);
303
304 const router = this.lookup (`router:${routerName}`);
305 assert (!!router, `The router {${routerName}} does not exist.`);
306
307 const builder = new RouterBuilder ({app: this});
308 return builder.addRouter ('/', router).build ();
309 },
310
311 /**
312 * Load the configuration files for the application. All configuration files are located
313 * in the app/configs directory.
314 *
315 * @returns {Promise} Promise object
316 * @private
317 */
318 _loadConfigurationFiles () {
319 const dirname = path.resolve (this.appPath, 'configs');
320 return this._defaultLoader.load ({dirname});
321 }
322});