282 lines
7.9 KiB
JavaScript
282 lines
7.9 KiB
JavaScript
// Copyright IBM Corp. 2015,2018. All Rights Reserved.
|
|
// Node module: loopback-datasource-juggler
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
'use strict';
|
|
|
|
const async = require('async');
|
|
const utils = require('./utils');
|
|
const debug = require('debug')('loopback:observer');
|
|
|
|
module.exports = ObserverMixin;
|
|
|
|
/**
|
|
* ObserverMixin class. Use to add observe/notifyObserversOf APIs to other
|
|
* classes.
|
|
*
|
|
* @class ObserverMixin
|
|
*/
|
|
function ObserverMixin() {
|
|
}
|
|
|
|
/**
|
|
* Register an asynchronous observer for the given operation (event).
|
|
*
|
|
* Example:
|
|
*
|
|
* Registers a `before save` observer for a given model.
|
|
*
|
|
* ```javascript
|
|
* MyModel.observe('before save', function filterProperties(ctx, next) {
|
|
if (ctx.options && ctx.options.skipPropertyFilter) return next();
|
|
if (ctx.instance) {
|
|
FILTERED_PROPERTIES.forEach(function(p) {
|
|
ctx.instance.unsetAttribute(p);
|
|
});
|
|
} else {
|
|
FILTERED_PROPERTIES.forEach(function(p) {
|
|
delete ctx.data[p];
|
|
});
|
|
}
|
|
next();
|
|
});
|
|
* ```
|
|
*
|
|
* @param {String} operation The operation name.
|
|
* @callback {function} listener The listener function. It will be invoked with
|
|
* `this` set to the model constructor, e.g. `User`.
|
|
* @end
|
|
*/
|
|
ObserverMixin.observe = function(operation, listener) {
|
|
this._observers = this._observers || {};
|
|
if (!this._observers[operation]) {
|
|
this._observers[operation] = [];
|
|
}
|
|
|
|
this._observers[operation].push(listener);
|
|
};
|
|
|
|
/**
|
|
* Unregister an asynchronous observer for the given operation (event).
|
|
*
|
|
* Example:
|
|
*
|
|
* ```javascript
|
|
* MyModel.removeObserver('before save', function removedObserver(ctx, next) {
|
|
// some logic user want to apply to the removed observer...
|
|
next();
|
|
});
|
|
* ```
|
|
*
|
|
* @param {String} operation The operation name.
|
|
* @callback {function} listener The listener function.
|
|
* @end
|
|
*/
|
|
ObserverMixin.removeObserver = function(operation, listener) {
|
|
if (!(this._observers && this._observers[operation])) return;
|
|
|
|
const index = this._observers[operation].indexOf(listener);
|
|
if (index !== -1) {
|
|
return this._observers[operation].splice(index, 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Unregister all asynchronous observers for the given operation (event).
|
|
*
|
|
* Example:
|
|
*
|
|
* Remove all observers connected to the `before save` operation.
|
|
*
|
|
* ```javascript
|
|
* MyModel.clearObservers('before save');
|
|
* ```
|
|
*
|
|
* @param {String} operation The operation name.
|
|
* @end
|
|
*/
|
|
ObserverMixin.clearObservers = function(operation) {
|
|
if (!(this._observers && this._observers[operation])) return;
|
|
|
|
this._observers[operation].length = 0;
|
|
};
|
|
|
|
/**
|
|
* Invoke all async observers for the given operation(s).
|
|
*
|
|
* Example:
|
|
*
|
|
* Notify all async observers for the `before save` operation.
|
|
*
|
|
* ```javascript
|
|
* var context = {
|
|
Model: Model,
|
|
instance: obj,
|
|
isNewInstance: true,
|
|
hookState: hookState,
|
|
options: options,
|
|
};
|
|
* Model.notifyObserversOf('before save', context, function(err) {
|
|
if (err) return cb(err);
|
|
// user can specify the logic after the observers have been notified
|
|
});
|
|
* ```
|
|
*
|
|
* @param {String|String[]} operation The operation name(s).
|
|
* @param {Object} context Operation-specific context.
|
|
* @callback {function(Error=)} callback The callback to call when all observers
|
|
* have finished.
|
|
*/
|
|
ObserverMixin.notifyObserversOf = function(operation, context, callback) {
|
|
const self = this;
|
|
if (!callback) callback = utils.createPromiseCallback();
|
|
|
|
function createNotifier(op) {
|
|
return function(ctx, done) {
|
|
if (typeof ctx === 'function' && done === undefined) {
|
|
done = ctx;
|
|
ctx = context;
|
|
}
|
|
self.notifyObserversOf(op, context, done);
|
|
};
|
|
}
|
|
|
|
if (Array.isArray(operation)) {
|
|
const tasks = [];
|
|
for (let i = 0, n = operation.length; i < n; i++) {
|
|
tasks.push(createNotifier(operation[i]));
|
|
}
|
|
return async.waterfall(tasks, callback);
|
|
}
|
|
|
|
const observers = this._observers && this._observers[operation];
|
|
|
|
this._notifyBaseObservers(operation, context, function doNotify(err) {
|
|
if (err) return callback(err, context);
|
|
if (!observers || !observers.length) return callback(null, context);
|
|
|
|
async.eachSeries(
|
|
observers,
|
|
function notifySingleObserver(fn, next) {
|
|
const retval = fn(context, next);
|
|
if (retval && typeof retval.then === 'function') {
|
|
retval.then(
|
|
function() { next(); return null; },
|
|
next, // error handler
|
|
);
|
|
}
|
|
},
|
|
function(err) { callback(err, context); },
|
|
);
|
|
});
|
|
return callback.promise;
|
|
};
|
|
|
|
ObserverMixin._notifyBaseObservers = function(operation, context, callback) {
|
|
if (this.base && this.base.notifyObserversOf)
|
|
this.base.notifyObserversOf(operation, context, callback);
|
|
else
|
|
callback();
|
|
};
|
|
|
|
/**
|
|
* Run the given function with before/after observers.
|
|
*
|
|
* It's done in three serial asynchronous steps:
|
|
*
|
|
* - Notify the registered observers under 'before ' + operation
|
|
* - Execute the function
|
|
* - Notify the registered observers under 'after ' + operation
|
|
*
|
|
* If an error happens, it fails first and calls the callback with err.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```javascript
|
|
* var context = {
|
|
Model: Model,
|
|
instance: obj,
|
|
isNewInstance: true,
|
|
hookState: hookState,
|
|
options: options,
|
|
};
|
|
* function work(done) {
|
|
process.nextTick(function() {
|
|
done(null, 1);
|
|
});
|
|
}
|
|
* Model.notifyObserversAround('execute', context, work, function(err) {
|
|
if (err) return cb(err);
|
|
// user can specify the logic after the observers have been notified
|
|
});
|
|
* ```
|
|
*
|
|
* @param {String} operation The operation name
|
|
* @param {Context} context The context object
|
|
* @param {Function} fn The task to be invoked as fn(done) or fn(context, done)
|
|
* @callback {Function} callback The callback function
|
|
* @returns {*}
|
|
*/
|
|
ObserverMixin.notifyObserversAround = function(operation, context, fn, callback) {
|
|
const self = this;
|
|
context = context || {};
|
|
// Add callback to the context object so that an observer can skip other
|
|
// ones by calling the callback function directly and not calling next
|
|
if (context.end === undefined) {
|
|
context.end = callback;
|
|
}
|
|
// First notify before observers
|
|
return self.notifyObserversOf('before ' + operation, context,
|
|
function(err, context) {
|
|
if (err) return callback(err);
|
|
|
|
function cbForWork(err) {
|
|
const args = [].slice.call(arguments, 0);
|
|
if (err) {
|
|
// call observer in case of error to hook response
|
|
context.error = err;
|
|
self.notifyObserversOf('after ' + operation + ' error', context,
|
|
function(_err, context) {
|
|
if (_err && err) {
|
|
debug(
|
|
'Operation %j failed and "after %s error" hook returned an error too. ' +
|
|
'Calling back with the hook error only.' +
|
|
'\nOriginal error: %s\nHook error: %s\n',
|
|
err.stack || err,
|
|
_err.stack || _err,
|
|
);
|
|
}
|
|
callback.call(null, _err || err, context);
|
|
});
|
|
return;
|
|
}
|
|
// Find the list of params from the callback in addition to err
|
|
const returnedArgs = args.slice(1);
|
|
// Set up the array of results
|
|
context.results = returnedArgs;
|
|
// Notify after observers
|
|
self.notifyObserversOf('after ' + operation, context,
|
|
function(err, context) {
|
|
if (err) return callback(err, context);
|
|
let results = returnedArgs;
|
|
if (context && Array.isArray(context.results)) {
|
|
// Pickup the results from context
|
|
results = context.results;
|
|
}
|
|
// Build the list of params for final callback
|
|
const args = [err].concat(results);
|
|
callback.apply(null, args);
|
|
});
|
|
}
|
|
|
|
if (fn.length === 1) {
|
|
// fn(done)
|
|
fn(cbForWork);
|
|
} else {
|
|
// fn(context, done)
|
|
fn(context, cbForWork);
|
|
}
|
|
});
|
|
};
|