Last active
October 5, 2016 13:03
-
-
Save BoLaMN/bf6a3342016732f611f9b23207039a03 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')) | |
); | |
}; |
Author
BoLaMN
commented
Oct 5, 2016
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment