Skip to content

Instantly share code, notes, and snippets.

@anasyb
Last active December 21, 2020 16:57
Show Gist options
  • Save anasyb/c2197a0d47ef29ee7418 to your computer and use it in GitHub Desktop.
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**
/**
* 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;
}
};
};
//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