UNPKG

8.1 kBJavaScriptView Raw
1// Copyright IBM Corp. 2015,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const async = require('async');
9const utils = require('./utils');
10const debug = require('debug')('loopback:observer');
11
12module.exports = ObserverMixin;
13
14/**
15 * ObserverMixin class. Use to add observe/notifyObserversOf APIs to other
16 * classes.
17 *
18 * @class ObserverMixin
19 */
20function ObserverMixin() {
21}
22
23/**
24 * Register an asynchronous observer for the given operation (event).
25 *
26 * Example:
27 *
28 * Registers a `before save` observer for a given model.
29 *
30 * ```javascript
31 * MyModel.observe('before save', function filterProperties(ctx, next) {
32 if (ctx.options && ctx.options.skipPropertyFilter) return next();
33 if (ctx.instance) {
34 FILTERED_PROPERTIES.forEach(function(p) {
35 ctx.instance.unsetAttribute(p);
36 });
37 } else {
38 FILTERED_PROPERTIES.forEach(function(p) {
39 delete ctx.data[p];
40 });
41 }
42 next();
43});
44 * ```
45 *
46 * @param {String} operation The operation name.
47 * @callback {function} listener The listener function. It will be invoked with
48 * `this` set to the model constructor, e.g. `User`.
49 * @end
50 */
51ObserverMixin.observe = function(operation, listener) {
52 this._observers = this._observers || {};
53 if (!this._observers[operation]) {
54 this._observers[operation] = [];
55 }
56
57 this._observers[operation].push(listener);
58};
59
60/**
61 * Unregister an asynchronous observer for the given operation (event).
62 *
63 * Example:
64 *
65 * ```javascript
66 * MyModel.removeObserver('before save', function removedObserver(ctx, next) {
67 // some logic user want to apply to the removed observer...
68 next();
69 });
70 * ```
71 *
72 * @param {String} operation The operation name.
73 * @callback {function} listener The listener function.
74 * @end
75 */
76ObserverMixin.removeObserver = function(operation, listener) {
77 if (!(this._observers && this._observers[operation])) return;
78
79 const index = this._observers[operation].indexOf(listener);
80 if (index !== -1) {
81 return this._observers[operation].splice(index, 1);
82 }
83};
84
85/**
86 * Unregister all asynchronous observers for the given operation (event).
87 *
88 * Example:
89 *
90 * Remove all observers connected to the `before save` operation.
91 *
92 * ```javascript
93 * MyModel.clearObservers('before save');
94 * ```
95 *
96 * @param {String} operation The operation name.
97 * @end
98 */
99ObserverMixin.clearObservers = function(operation) {
100 if (!(this._observers && this._observers[operation])) return;
101
102 this._observers[operation].length = 0;
103};
104
105/**
106 * Invoke all async observers for the given operation(s).
107 *
108 * Example:
109 *
110 * Notify all async observers for the `before save` operation.
111 *
112 * ```javascript
113 * var context = {
114 Model: Model,
115 instance: obj,
116 isNewInstance: true,
117 hookState: hookState,
118 options: options,
119 };
120 * Model.notifyObserversOf('before save', context, function(err) {
121 if (err) return cb(err);
122 // user can specify the logic after the observers have been notified
123 });
124 * ```
125 *
126 * @param {String|String[]} operation The operation name(s).
127 * @param {Object} context Operation-specific context.
128 * @callback {function(Error=)} callback The callback to call when all observers
129 * have finished.
130 */
131ObserverMixin.notifyObserversOf = function(operation, context, callback) {
132 const self = this;
133 if (!callback) callback = utils.createPromiseCallback();
134
135 function createNotifier(op) {
136 return function(ctx, done) {
137 if (typeof ctx === 'function' && done === undefined) {
138 done = ctx;
139 ctx = context;
140 }
141 self.notifyObserversOf(op, context, done);
142 };
143 }
144
145 if (Array.isArray(operation)) {
146 const tasks = [];
147 for (let i = 0, n = operation.length; i < n; i++) {
148 tasks.push(createNotifier(operation[i]));
149 }
150 return async.waterfall(tasks, callback);
151 }
152
153 const observers = this._observers && this._observers[operation];
154
155 this._notifyBaseObservers(operation, context, function doNotify(err) {
156 if (err) return callback(err, context);
157 if (!observers || !observers.length) return callback(null, context);
158
159 async.eachSeries(
160 observers,
161 function notifySingleObserver(fn, next) {
162 const retval = fn(context, next);
163 if (retval && typeof retval.then === 'function') {
164 retval.then(
165 function() { next(); return null; },
166 next, // error handler
167 );
168 }
169 },
170 function(err) { callback(err, context); },
171 );
172 });
173 return callback.promise;
174};
175
176ObserverMixin._notifyBaseObservers = function(operation, context, callback) {
177 if (this.base && this.base.notifyObserversOf)
178 this.base.notifyObserversOf(operation, context, callback);
179 else
180 callback();
181};
182
183/**
184 * Run the given function with before/after observers.
185 *
186 * It's done in three serial asynchronous steps:
187 *
188 * - Notify the registered observers under 'before ' + operation
189 * - Execute the function
190 * - Notify the registered observers under 'after ' + operation
191 *
192 * If an error happens, it fails first and calls the callback with err.
193 *
194 * Example:
195 *
196 * ```javascript
197 * var context = {
198 Model: Model,
199 instance: obj,
200 isNewInstance: true,
201 hookState: hookState,
202 options: options,
203 };
204 * function work(done) {
205 process.nextTick(function() {
206 done(null, 1);
207 });
208 }
209 * Model.notifyObserversAround('execute', context, work, function(err) {
210 if (err) return cb(err);
211 // user can specify the logic after the observers have been notified
212 });
213 * ```
214 *
215 * @param {String} operation The operation name
216 * @param {Context} context The context object
217 * @param {Function} fn The task to be invoked as fn(done) or fn(context, done)
218 * @callback {Function} callback The callback function
219 * @returns {*}
220 */
221ObserverMixin.notifyObserversAround = function(operation, context, fn, callback) {
222 const self = this;
223 context = context || {};
224 // Add callback to the context object so that an observer can skip other
225 // ones by calling the callback function directly and not calling next
226 if (context.end === undefined) {
227 context.end = callback;
228 }
229 // First notify before observers
230 return self.notifyObserversOf('before ' + operation, context,
231 function(err, context) {
232 if (err) return callback(err);
233
234 function cbForWork(err) {
235 const args = [].slice.call(arguments, 0);
236 if (err) {
237 // call observer in case of error to hook response
238 context.error = err;
239 self.notifyObserversOf('after ' + operation + ' error', context,
240 function(_err, context) {
241 if (_err && err) {
242 debug(
243 'Operation %j failed and "after %s error" hook returned an error too. ' +
244 'Calling back with the hook error only.' +
245 '\nOriginal error: %s\nHook error: %s\n',
246 err.stack || err,
247 _err.stack || _err,
248 );
249 }
250 callback.call(null, _err || err, context);
251 });
252 return;
253 }
254 // Find the list of params from the callback in addition to err
255 const returnedArgs = args.slice(1);
256 // Set up the array of results
257 context.results = returnedArgs;
258 // Notify after observers
259 self.notifyObserversOf('after ' + operation, context,
260 function(err, context) {
261 if (err) return callback(err, context);
262 let results = returnedArgs;
263 if (context && Array.isArray(context.results)) {
264 // Pickup the results from context
265 results = context.results;
266 }
267 // Build the list of params for final callback
268 const args = [err].concat(results);
269 callback.apply(null, args);
270 });
271 }
272
273 if (fn.length === 1) {
274 // fn(done)
275 fn(cbForWork);
276 } else {
277 // fn(context, done)
278 fn(context, cbForWork);
279 }
280 });
281};