Skip to content

Instantly share code, notes, and snippets.

@BoLaMN
Last active October 5, 2016 13:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BoLaMN/bf6a3342016732f611f9b23207039a03 to your computer and use it in GitHub Desktop.
Save BoLaMN/bf6a3342016732f611f9b23207039a03 to your computer and use it in GitHub Desktop.
let debug = require('debug')('loopback:hooks:wrap');
import async from 'async';
import { createPromiseCallback } from 'loopback-datasource-juggler/lib/utils';
import { inspect } from 'util';
export default function(server) {
let callback = function(args) {
if (typeof args[args.length - 1] === 'function') {
var cb = args.pop();
} else {
var cb = createPromiseCallback();
}
return cb;
};
let fns = {
around(base, wrapped) {
return function(...args) {
return wrapped.apply(this, [ base.bind(this) ].concat(args));
};
},
before(base, before) {
return this.around(base, function(fn, ...args) {
let that = this;
let cb = callback(args);
args.push(function(err, ctx) {
if (err) { return cb.apply(that, [ err ]); }
let context = ctx.toArray();
context.push((err, resp) => cb.apply(that, [ err, resp, ctx ]));
return fn.apply(that, context);
});
before.apply(this, args);
return cb.promise;
}
);
},
after(base, after) {
return this.around(base, function(fn, ...args) {
let that = this;
let cb = callback(args);
args.push(function(err, result, ctx) {
if (err) { ctx.error = err; }
ctx.results = result;
return after.apply(that, [ ctx, (...args3) => cb.apply(that, args3)
]);});
fn.apply(this, args);
return cb.promise;
}
);
}
};
let hooks = function(obj) {
let ret = {};
[ 'before', 'after', 'around' ].forEach(type =>
ret[type] = function(method, fn) {
if (typeof obj[method] === 'function') {
obj[method] = fns[type](obj[method], fn);
} else {
obj[method] = fn;
}
return this;
}
);
return ret;
};
let define = (proto, prop, val) =>
Object.defineProperty(proto, prop, {
value: val,
enumerable: false,
writable: true
}
)
;
let notify = function(Model, methodName, ctx, event, done) {
ctx.event = event;
let run = [
next => Model.notifyObserversOf(event + ' ' + methodName, ctx, next),
next => Model.notifyObserversOf(event + ' *', ctx, next),
next => Model.notifyObserversOf('*', ctx, next)
];
return async.parallel(run, done);
};
let SCOPE_METHOD_REGEX = /^prototype.__([^_]+)__(.+)$/;
class Context {
constructor(Model, methodString, methodName, args) {
debug('created context with args', args);
this.modelName = Model.modelName;
this.methodName = methodName;
define(this, 'methodString', methodString);
define(this, 'Model', Model);
let method = this.findMethod(Model.sharedClass, this.methodString, this.methodName);
let accepts = this.accepts(method);
if (!accepts) {
let [ input, op, scopeName ] = this.methodString.match(SCOPE_METHOD_REGEX);
let targetModel = Model.sharedClass.ctor.prototype;
let targetClass = targetModel[scopeName]._targetClass;
let { sharedClass } = server.models[targetClass];
method = this.findMethod(sharedClass, input, op);
accepts = this.accepts(method);
}
define(this, 'args', accepts || []);
this.args.forEach(accept => {
let value = args.shift();
return this[accept.arg || accept.name] = value;
}
);
}
findMethod(sharedClass, methodString, methodName) {
return sharedClass._methods.find(({ stringName, name, aliases }) =>
stringName === methodString ||
name === methodString ||
stringName === methodName ||
name === methodName ||
aliases.indexOf(methodName) > -1
);
}
accepts({ isStatic, restClass, accepts } = {}) {
if (!isStatic && restClass) {
({ accepts } = restClass.ctor);
}
return accepts;
}
toArray() {
return this.args.map(accept => {
return this[accept.arg || accept.name] || {};
});
}
}
let log = ctx => debug(inspect(ctx, { showHidden: false, colors: true }));
let mixin = function(Model, options) {
let wrap = function(model, methodName, methodString) {
debug(`wrapping ${methodString}`);
return hooks(model)
.before(methodName, function(...args) {
let cb = args.pop();
let ctx = new Context(Model, methodString, methodName, args);
let done = function(err) {
log(ctx);
return cb(err, ctx);
};
notify(Model, methodName, ctx, 'before', done);
}
)
.after(methodName, function(ctx, cb) {
let event = 'after';
if (ctx.error) { event += ' error'; }
let done = function(err) {
log(ctx);
return cb(err, ctx.results);
};
notify(Model, methodName, ctx, event, done);
}
);
};
let fn = function() {
let methods = Model.sharedClass.methods({includeDisabled: true});
return methods.forEach(function({ isStatic, name, stringName }) {
let model = isStatic ? Model : Model.prototype;
wrap(model, name, stringName);
});
};
if (Model.app) {
return fn();
}
Model.once('attached', fn);
};
server.loopback.modelBuilder.mixins.define('Hooks', mixin);
process.nextTick(() =>
server.models().forEach(model => model.mixin('Hooks'))
);
};
@BoLaMN
Copy link
Author

BoLaMN commented Oct 5, 2016

Model.observe('before findById', ctx => console.log(ctx));
Model.observe('before *', ctx => console.log(ctx));
Model.observe('*', ctx => console.log(ctx));
Model.observe('after findById', ctx => console.log(ctx));
Model.observe('after *', ctx => console.log(ctx));
Model.observe('*', ctx => console.log(ctx));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment