1 | const path = require('path');
|
2 | const debugLogger = require('debug');
|
3 | const debugNamespace = Symbol('debugNamespace');
|
4 | const debug = Symbol('debug');
|
5 | const ControllerCache = require('./controller-cache');
|
6 | const { chalk: {info}} = require('../namespace/console');
|
7 | const object = require('../namespace/object');
|
8 | const types = require('../namespace/type');
|
9 |
|
10 | /**
|
11 | * Controller Class.
|
12 | * API:
|
13 | * ---
|
14 | * ```
|
15 | * - cache Caching Class
|
16 | * - actionCache Action Caching Store
|
17 | * - cacheStore Current Cache Store
|
18 | * - req Express request object
|
19 | * - res Express response object
|
20 | * - params Alias for `this.req.params`
|
21 | * - body Alias for `this.req.body`
|
22 | * - redisClient Redis Connection
|
23 | * - permit A method that removes non-whitelisted objects. Nested Objects are not allowed without using dot notation to specify the nested keys.
|
24 | * - deepPermit A method that removes non-whitelisted objects. Nested Objects are allowed.
|
25 | * - permitted Boolean, has this action called permit. This may be useful to enforce permitting.
|
26 | * - render Renders a view.
|
27 | * ```
|
28 | */
|
29 | class Controller {
|
30 |
|
31 | constructor (req, res, redisClient) {
|
32 | let { params, body } = req;
|
33 | this.req = req;
|
34 | this.res = res;
|
35 | this.params = params;
|
36 | this.body = body;
|
37 | this.redisClient = redisClient;
|
38 | this.cacheStore = {};
|
39 | this.viewPath = req.controller.replace('Controller', '').toLowerCase();
|
40 | this[debugNamespace] = debugLogger('glad');
|
41 | this[debug]('Created Controller Instance');
|
42 | }
|
43 |
|
44 | [debug] (name) {
|
45 | this[debugNamespace]("Controller: %s > %s", name, this.req.id);
|
46 | }
|
47 |
|
48 | /**
|
49 | * Whitelist allowable data in a request body. This is a good idea if you mass assigning things.
|
50 | * Ex: Shallow
|
51 | * ```javascript
|
52 | * this.req.body = { name: 'Fooze', admin: true };
|
53 | * this.permit('name', 'email', 'phone');
|
54 | * // this.req.body === { name: 'Fooze' }
|
55 | * ```
|
56 | *
|
57 | * Ex: Deep
|
58 | * ```javascript
|
59 | * this.req.body = { user: { name: 'Fooze', admin: true } };
|
60 | * this.permit('user.name', 'user.email', 'user.phone');
|
61 | * // this.req.body === { user: { name: 'Fooze' } }
|
62 | * ```
|
63 | * You must be specific when the key contains an object.
|
64 | * You can not permit the whole user object at once. In order to permit "sub documents"
|
65 | * you need to use the `deepPermit` method instead. This is intentional because it can defeat
|
66 | * the purpose of `permit` when you permit a subdocument that could potentially contain things
|
67 | * that shouldnt be allowed. For example: { user : { admin: true } }. Assume that admin was injected by a malicious
|
68 | * user trying to gain admin access to the site, and admin access is determined by the admin key.
|
69 | */
|
70 | permit (...keys) {
|
71 | this[debug]('permit');
|
72 | let i = 0;
|
73 | let len = keys.length;
|
74 | let ref = {};
|
75 |
|
76 | for (i; i < len; i += 1) {
|
77 | let val = object.get(this.body, keys[i]);
|
78 |
|
79 | if (types.isNotObject(val)) {
|
80 | if (val) {
|
81 | object.set(ref, keys[i], val);
|
82 | }
|
83 | }
|
84 | }
|
85 | this.body = this.req.body = ref;
|
86 | this.permitted = true;
|
87 | }
|
88 |
|
89 | /**
|
90 | * This method is similar to permit, only it also permits subdocuments.
|
91 | */
|
92 | deepPermit(...keys) {
|
93 | this[debug]('deepPermit');
|
94 | let i = 0;
|
95 | let len = keys.length;
|
96 | let ref = {};
|
97 |
|
98 | for (i; i < len; i += 1) {
|
99 | let val = object.get(this.body, keys[i]);
|
100 | if (val) {
|
101 | object.set(ref, keys[i], val);
|
102 | }
|
103 | }
|
104 | this.body = this.req.body = ref;
|
105 | this.permitted = true;
|
106 | }
|
107 |
|
108 | /**
|
109 | * The default error response for the controller
|
110 | * @param {object} error - The error that occured. Providing `status` on the error object will set the HTTP Status code on the respone.
|
111 | */
|
112 | error (err = {}) {
|
113 | this[debug]('error');
|
114 | this.res.status(err.status || 500).json(err)
|
115 | }
|
116 |
|
117 | /**
|
118 | * ## The controller level cache.
|
119 | * ---
|
120 | * All options are optional.
|
121 | *
|
122 | * [ *Number* ] `max`: Maximum amount of items the cache can hold. This option is required; if no
|
123 | * other option is needed, it can be passed directly as the second parameter when creating
|
124 | * the cache.
|
125 | *
|
126 | * [ *String* ] `namespace`: Prefix appended to all keys saved in Redis, to avoid clashes with other applications
|
127 | * and to allow multiple instances of the cache. Given a controller called "Products" and an action called "find", the default
|
128 | * namespace is `Products#find`. By setting this value to 'server007' it would be `Products#find-server007`.
|
129 | *
|
130 | * [ *Number* ] `maxAge`: Maximum amount of milliseconds the key will be kept in the cache; after that getting/peeking will
|
131 | * resolve to `null`. Note that the value will be removed from Redis after `maxAge`, but the key will
|
132 | * be kept in the cache until next time it's accessed (i.e. it will be included in `count`, `keys`, etc., although not in `has`.).
|
133 | *
|
134 | * [ *Function* ] `score`: function to customize the score used to order the elements in the cache. Defaults to `() => new Date().getTime()`
|
135 | *
|
136 | * [ *Boolean* ] `increment`: if `true`, on each access the result of the `score` function is added to the previous one,
|
137 | * rather than replacing it.
|
138 | *
|
139 | * [ *String* ] `strategy`: If 'LRU', the scoring function will be set to LRU Cache.
|
140 | * If 'LFU', the scoring function will be set to 'LFU'. The default strategy is 'LFU'
|
141 | *
|
142 | * [ *String* ] `type`: Overrides the default response type (json). Set this to any mime type express supports. [Docs Here](https://expressjs.com/en/api.html#res.type)
|
143 | *
|
144 | * ---
|
145 | *
|
146 | * #### Call back method
|
147 | * ```javascript
|
148 | * this.cache({ max: 100, strategy: 'LFU' }, cache => {
|
149 | * Products.find({category: 'widgets'}).exec( (err, widgets) => {
|
150 | * this.res.status(200).json(widgets);
|
151 | * cache(doc);
|
152 | * });
|
153 | * });
|
154 | * ```
|
155 | * Using the call back method using LFU cache. In the event of a cache miss, The callback function recieves a cache method
|
156 | * that you can call to set the cache for identical requests that may happen in the future. Once this is set, the call back
|
157 | * will be skipped and the default response method will be called automatically.
|
158 | *
|
159 | * #### Chained Methods offer more control.
|
160 | * ```javascript
|
161 | * this.cache({ max: 100, strategy: 'LRU' })
|
162 | * .miss(cache => {
|
163 | * Products.find({category: 'widgets'}).exec( (err, widgets) => {
|
164 | * this.res.status(200).json(widgets);
|
165 | * cache(widgets);
|
166 | * });
|
167 | * })
|
168 | * .hit(data => this.res.status(200).json(data))
|
169 | * .exec();
|
170 | * ```
|
171 | * In the above example, we don't pass in a callback. Instead we use chaining to express
|
172 | * how we want to handle the control flow.
|
173 | *
|
174 | * - `miss` registers a callback to allow you to store the item in the cache
|
175 | *
|
176 | * - `hit` registers a callback to allow you to handle your own response
|
177 | *
|
178 | * - `exec` runs the function and is a required function call.
|
179 | *
|
180 | * @param {object} options - (optional) Caching options. See options in the description.
|
181 | * @param {function} fn - (optional) the method to call for a cache miss.
|
182 | */
|
183 | cache (options, fn) {
|
184 | this[debug]('cache');
|
185 | var cache = new ControllerCache(this.redisClient, this.req.controller, this.req.action, options);
|
186 | var hitFn, missFn;
|
187 |
|
188 | this.res.cache = function (content) {
|
189 | return cache.cache.set(this.req.url, content);
|
190 | }
|
191 |
|
192 | this.cacheStore = cache.cache;
|
193 |
|
194 | if (fn) {
|
195 | return cache.cachedVersion(this.req).then(result => {
|
196 | if (result) {
|
197 | this[debug]('cache:callback:hit');
|
198 | this.res.set('X-GLAD-CACHE-HIT', 'true');
|
199 | return this.res.type(options.type || 'json').send(result);
|
200 | } else {
|
201 | this[debug]('cache:callback:miss');
|
202 | return fn.call(this, (content) => {
|
203 | return cache.cache.set(this.req.url, content);
|
204 | });
|
205 | }
|
206 | });
|
207 | } else {
|
208 | return {
|
209 | hit (func) { hitFn = func; return this;},
|
210 | miss (func) { missFn = func; return this;},
|
211 | exec : () => {
|
212 | return new Promise( (resolve, reject) => {
|
213 | cache.cachedVersion(this.req).then(result => {
|
214 |
|
215 | if (result) {
|
216 | this[debug]('cache:exec:hit');
|
217 | this.res.set('X-Glad-Cache-Hit', 'true');
|
218 | }
|
219 |
|
220 | if (result && hitFn) {
|
221 | hitFn(result);
|
222 | resolve(result);
|
223 | } else if (result) {
|
224 | this.res.type(options.type || 'json').send(result);
|
225 | resolve(result);
|
226 | } else if (missFn) {
|
227 | this[debug]('cache:exec:miss');
|
228 | missFn(data => {
|
229 | cache.cache.set(this.req.url, data);
|
230 | resolve(data);
|
231 | });
|
232 | } else {
|
233 | this[debug]('cache:exec:error');
|
234 | reject({
|
235 | err: `
|
236 | Missing method Error.
|
237 | Please provide the '.miss' portion of your cache chain.
|
238 | this.cache({..opts}).miss((cache) => { ..do stuff >> res.json(stuff) >> cache(stuff) })
|
239 | `
|
240 | });
|
241 | }
|
242 | });
|
243 | });
|
244 | }
|
245 | };
|
246 | }
|
247 | }
|
248 |
|
249 | /**
|
250 | * Returns a Cache instance for a give controller action.
|
251 | * This makes the following cache methods available.
|
252 | * This is needed when you need to manage a cache for another action on the same controller.
|
253 | *
|
254 | * <strong>Example:</strong> You may want to expire individual items from cache.
|
255 | * ```javascript
|
256 | * this.actionCache('FindOne').del('/widgets/12').then(function () {});
|
257 | *
|
258 | * ```
|
259 | * <strong>Example:</strong> A DELETE request to `/api/widgets/12` might delete the 12th widget in your database.
|
260 | * After removing the item from your database, you may want to reset some caches. The simplest form of this might be to
|
261 | * clear the cache for the `Get` action, and remove it from the `FindOne` cache as well. This would ensure that (starting immediately)
|
262 | * future requests for widgets will not include this data.
|
263 | *
|
264 | * ```javascript
|
265 | * this.actionCache('Get').reset().then( () => {
|
266 | * this.actionCache('FindOne').del('/widgets/12').then(res);
|
267 | * });
|
268 | * ```
|
269 | *
|
270 | * TODO:
|
271 | * -----------
|
272 | * <strong>Example:</strong> You may have objects in your cache that will change over time, but always need to live in cache.
|
273 | * ```javascript
|
274 | * this.actionCache('Get').find({url : '/widgets/100'}).update(cache => {
|
275 | * //..do stuff then call cache(widget);
|
276 | * });
|
277 | * ```
|
278 | *
|
279 | * TODO:
|
280 | * -----------
|
281 | * <strong>Example:</strong> You may want to create cache re-warmers for actions.
|
282 | * Furthermore after your data changes, you may need to clear a cache and rewarm it.
|
283 | * the forEach method will iterate over every item in your action's cache, and provide
|
284 | * the item and a cacher specific to the item which you can call to update that item.
|
285 | * ```javascript
|
286 | * this.actionCache('Get').forEach((item, cache, next) => {
|
287 | * //..do stuff then call cache(updatedItem);
|
288 | * });
|
289 | * ```
|
290 | */
|
291 | actionCache (action) {
|
292 | this[debug]('actionCache');
|
293 | let _cache = new ControllerCache(this.redisClient, this.req.controller, action);
|
294 | let { cache } = _cache;
|
295 | return cache;
|
296 | }
|
297 |
|
298 | /**
|
299 | * Renders a view. In the end, this maps to express res.render. By default it looks for a view residing in views/:controller/:action.pug.
|
300 | * This can be overridden by passing in the path to the view to render.
|
301 | *
|
302 | * A controller named User would look for views in the `view/user` folder. If you were rendering from an action called `findOne`, the complete
|
303 | * view path would be `views/user/findone`. If you would prefer to specify your own viewpath, there are a two options for you.
|
304 | *
|
305 | * #### From a controller method:
|
306 | * ```
|
307 | * // Specify the viewPath on the controller instance. This applies to a request instance.
|
308 | * this.viewPath = 'my-views/wigets/widget.pug';
|
309 | *
|
310 | * // Use this.res.render (from express)
|
311 | * this.res.render('path/to/view', data);
|
312 | * ```
|
313 | */
|
314 | render (...args) {
|
315 | this[debug]('render');
|
316 | args[0] = path.join(this.viewPath, args[0]);
|
317 | this.res.render.apply(this.res, args);
|
318 | }
|
319 |
|
320 | }
|
321 |
|
322 | module.exports = Controller;
|