UNPKG

5.89 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 { BO, computed } = require ('base-object');
18const debug = require ('debug') ('blueprint:application-module');
19const path = require ('path');
20const assert = require ('assert');
21
22const lookup = require ('./-lookup');
23
24const { merge, reduce, omit, get } = require ('lodash');
25const { readFile, readFileSync, statSync } = require ('fs-extra');
26
27const Loader = require ('./loader');
28const ListenerLoader = require ('./listener-loader');
29const Router = require ('./router');
30
31module.exports = BO.extend ({
32 /// Name of the application module.
33 name: null,
34
35 /// Application hosting the module.
36 app: null,
37
38 /// The path for the application module.
39 modulePath: null,
40
41 /// The path where the views are located.
42 _viewsPath: null,
43
44 /// The loader used by the application module.
45 _defaultLoader: new Loader (),
46
47 _entities: null,
48
49 /// Collection of resources loaded by the application module.
50 _resources: null,
51
52 viewsPath: computed ({
53 get () {
54 return path.join (this.modulePath, 'views');
55 }
56 }),
57
58 hasViews: computed ({
59 get () {
60 try {
61 return statSync (this.viewsPath).isDirectory ();
62 }
63 catch (ex) {
64 return false;
65 }
66 }
67 }),
68
69 assetsPath: computed ({
70 get () { return path.resolve (this.modulePath, 'assets') }
71 }),
72
73 resources: computed ({
74 get () { return this._resources; }
75 }),
76
77 init () {
78 this._super.call (this, ...arguments);
79 this._resources = {};
80
81 assert (!!this.app, "You must define the 'app' property.");
82 assert (!!this.modulePath, "You must define the 'modulePath' property.");
83
84 // The entities that are loaded by the application module. The order of the
85 // entities is important because come classes of components will be dependent
86 // on other classes of components.
87
88 this._entities = [
89 { name: 'models'},
90 {
91 name: 'services',
92 opts: {
93 resolve: this._instantiateComponent.bind (this)
94 }
95 },
96 { name: 'listeners', loader: new ListenerLoader ({app: this.app}) },
97 {
98 name: 'controllers',
99 opts: {
100 resolve: this._instantiateComponent.bind (this)
101 }
102 },
103 { name: 'policies'},
104 { name: 'validators'},
105 { name: 'sanitizers'},
106 {
107 name: 'routers',
108 mergeable: false,
109 opts: {
110 resolve (router) {
111 return router.prototype && !!router.prototype.build ? new router () : new Router ({specification: router});
112 }
113 }
114 }
115 ];
116 },
117
118 _instantiateComponent (Component) {
119 return new Component ({app: this.app});
120 },
121
122 /**
123 * Configure the application module. This method will load all the resource
124 * entities for the application module into memory.
125 *
126 * @returns {Promise<ApplicationModule>}
127 */
128 configure () {
129 debug (`configuring the application module ${this.modulePath}`);
130
131 let promise = reduce (this._entities, (promise, entity) => {
132 return promise.then (() => {
133 // Compute the location of the resources we are loading. Then load the resources
134 // into memory and save the resources to our application module
135 let {name} = entity;
136 let location = entity.location || name;
137 let rcPath = path.resolve (this.modulePath, location);
138 let opts = merge ({dirname: rcPath}, entity.opts);
139
140 debug (`loading ${name} in ${rcPath}`);
141 let loader = entity.loader || this._defaultLoader;
142
143 return loader.load (opts).then (resources => {
144 // Merge the resources into the application module, and then merge them
145 // into the parent application. This means we will have two copies of the
146 // resources, but that is fine. We need the ability to load a resource from
147 // its parent module, if necessary.
148 debug (`merging ${name} into both application module and application`);
149 const mergeable = get (entity, 'mergeable', true);
150
151 this._resources[name] = merge (this._resources[name] || {}, resources);
152
153 if (mergeable) {
154 this.app.resources[name] = merge (this.app.resources[name] || {}, resources);
155 }
156 });
157 });
158 }, Promise.resolve ());
159
160 return promise.then (() => this);
161 },
162
163 /**
164 * Lookup a loaded component. The format of the name is
165 *
166 * type:name
167 *
168 * For example:
169 *
170 * policy:a.b.c
171 *
172 * @param component
173 */
174 lookup (component) {
175 return lookup (this.resources, component);
176 },
177
178 /**
179 * Read an application asset. If the callback is undefined, then the data in the
180 * resource is returned to the caller.
181 *
182 * @returns {*}
183 */
184 asset (filename, opts) {
185 let fullPath = path.resolve (this.assetsPath, filename);
186 return readFile (fullPath, opts);
187 },
188
189 assetSync (filename, opts) {
190 let fullPath = path.resolve (this.assetsPath, filename);
191 return readFileSync (fullPath, opts);
192 },
193
194 /**
195 * Merge an module with this module. It will copy all the entities from the
196 * source module into this module. Any entity in the source module will overwrite
197 * the entity in this module.
198 *
199 * @param module
200 */
201 merge (module) {
202 return merge (this._resources, omit (module.resources, ['routers']));
203 }
204});