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 | ;
|
7 |
|
8 | const async = require('async');
|
9 | const utils = require('./utils');
|
10 | const debug = require('debug')('loopback:observer');
|
11 |
|
12 | module.exports = ObserverMixin;
|
13 |
|
14 | /**
|
15 | * ObserverMixin class. Use to add observe/notifyObserversOf APIs to other
|
16 | * classes.
|
17 | *
|
18 | * @class ObserverMixin
|
19 | */
|
20 | function 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 | */
|
51 | ObserverMixin.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 | */
|
76 | ObserverMixin.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 | */
|
99 | ObserverMixin.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 | */
|
131 | ObserverMixin.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 |
|
176 | ObserverMixin._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 | */
|
221 | ObserverMixin.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 | };
|