Last active
December 21, 2020 16:57
-
-
Save anasyb/c2197a0d47ef29ee7418 to your computer and use it in GitHub Desktop.
A sails hook that introduces model policies in a very similar fashion to sails existing policies (which are route policies). This can be used for logic that is model specific and route independent, and should run regardless of the way the record was retrieved (e.g. directly v.s. populate...etc). **This is still merely a proof of concept**
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
/** | |
* modelpolicies hook | |
* api/hook/modelpolicies/index.js | |
*/ | |
module.exports = function (sails) { | |
var hook; | |
//actions to be wrapped | |
var actions = []; | |
actions.push('find'); | |
actions.push('findOne'); | |
//TODO wrap the rest of actions! | |
//TODO wrap actions that are generated automatically, I mean attr actions like findFirstName... etc. | |
return { | |
// Run when sails loads-- be sure to call `next()`. | |
initialize: function (next) { | |
// Provide hook context to closures | |
hook = this; | |
// Set up listener to bind when the time is right. | |
// | |
// Always wait until after router has bound static routes. | |
// Always wait until after models are known. | |
var eventsToWaitFor = []; | |
eventsToWaitFor.push('router:after'); | |
eventsToWaitFor.push('hook:orm:loaded'); | |
sails.after(eventsToWaitFor, hook.wrapModelActions); | |
return next(); | |
}, | |
wrapModelActions: function() { | |
for (modelName in sails.models) { | |
var model = sails.models[modelName]; | |
for (i in actions) { | |
var actionName = actions[i]; | |
//waterline actions would complain about having a criteria.req, | |
//this function migrates it to the extras to keep it available for | |
//model policies without waterline complaining about it. It runs | |
//smoothly if criteria.req isn't there to start with. | |
var reqRemover = function (args, extras) { | |
if (!args || !args.length) return {args: args, extras:extras}; | |
extras = extras || {}; | |
extras.req = args[0].req; | |
delete args[0].req; | |
return {args: args, extras:extras}; | |
} | |
//a callback that wraps the original callback and applies model | |
//policies on the records before handing them off to the original | |
//callback | |
var wrappedCallback = function (originalCallback, extras) { | |
var newCallback = function (err, records){ | |
var runPols = sails.hooks.modelpolicies.modelPoliciesRunLoop; | |
if (!err) { | |
extras = extras || {}; | |
var isPolsPassed = runPols(sails.models[extras.modelName], extras.actionName, extras.req, records); | |
if (!isPolsPassed){ | |
records = undefined; | |
err = new Error(); | |
//TODO make err depend on the policy, i.e. created there not here | |
err.message('Model policies did not pass'); | |
err.status(403); | |
} | |
} | |
return originalCallback.apply(this, arguments); | |
}; | |
return newCallback; | |
} | |
var extras = { | |
modelName: modelName, | |
actionName: actionName | |
}; | |
this.actionWrapper(model, actionName, 1, wrappedCallback, reqRemover, extras ); | |
} | |
} | |
}, | |
//Given an "actionName" of an "object", this function wraps that action and (optionally) also wraps its callback | |
//object[actionName] must be expecting a callback as its arguments[callbackIndex] which we want to wrap | |
//argPreprocessor(optional): pre-processes arguments of the action and the extras; typical usecase: migrates | |
// some properties from action's arguments to extras so that they can be used by the new callback without | |
// affecting the original action ('without affecting' = without being passed to original action). | |
//callbackFactory(optional): if callback wrapping is also desired, a callbackFactory will be used to generate | |
// the callback wrapper function | |
//resultsPostprocessor(optional): an optional processor which will be passed the "return" results of originalAction | |
// before they are returned by the wrappedAction | |
actionWrapper: function (object, actionName, callbackIndex, callbackFactory, argPreprocessor, extras, resultsPostprocessor) { | |
if (!object || !object[actionName]) return; | |
var originalAction = object[actionName]; | |
var wrappedAction = function() { | |
//if's order is important, even if callback wasn't provided (in case it was optional to originalAction), | |
//we still want to make sure that argPreprocessor cleans/processes arguments and make them ready for | |
//the originalAction | |
if (typeof argPreprocessor === 'function') { | |
var argsAndExtras = argPreprocessor (arguments, extras); | |
arguments = argsAndExtras.args; | |
extras = argsAndExtras.extras; | |
} | |
if (!arguments || callbackIndex >= arguments.length || typeof callbackFactory !== 'function') { | |
;//no new callback is available, so just skip wrapping the callback | |
} else if (typeof arguments[callbackIndex] !== 'function') { | |
//original callback is not a function, let's warn just in case! | |
console.warn('The to-be-wrapped callback is not a function!' + '\n' + | |
'Callback wrapping was skipped for actionName: ' + actionName); | |
} else { | |
//let's wrap the callback | |
arguments[callbackIndex] = callbackFactory(arguments[callbackIndex], extras); | |
} | |
var resutls = originalAction.apply(this, arguments); | |
if (typeof resultsPostprocessor === 'function') { | |
return resultsPostprocessor(resutls, extras); | |
} else { | |
return resutls; | |
} | |
}; | |
object[actionName] = wrappedAction; | |
return originalAction; | |
}, | |
//returns: true if all action's modelpolicies return true (or if none exist), false otherwise | |
modelPoliciesRunLoop: function (model, actionName, req, records) { | |
if (!model || !model.policies || !model.policies[actionName] || !model.policies[actionName].length){ | |
return true; | |
} | |
var pols = model.policies.pols || {}; | |
var actionPols = model.policies[actionName]; | |
for (policyIndex in actionPols){ | |
var policyName = actionPols[policyIndex]; | |
var pol = pols[policyName]; | |
if (!pol) { | |
console.warn('Model policy \''+ policyName +'\' was not found in model \''+ | |
model.identity +'\', and was IGNORED'); | |
continue; | |
} | |
if (!pol(req, records)) return false; | |
} | |
return true; | |
} | |
}; | |
}; |
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
//in some model | |
//... | |
policies: { | |
findOne: ['somePol'], | |
pols: { | |
somePol: function(req, records){ | |
//do something to records. | |
return true; | |
}, | |
} | |
}, |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment