Reputation: 1808
I have an object which looks like this:
const ROUTES = {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
PASSWORD: {
TO: '/account/profile/password',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL', 'ADMIN'],
},
},
},
},
COLLECTIONS: {
TO: '/account/collections',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['ADMIN'],
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
};
I want to create a function (getRoutes
) which filters/reduces that object depending on the RESTRICTIONS
passed in, all permissions
must match.
function getRoutes(routes, restrictions){
//...
}
const USER_RESTRICTIONS = {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
}
const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)
allowedRoutes === {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
} ? 'YAY' : 'NAY'
Upvotes: 0
Views: 107
Reputation: 50797
My version is algorithmically no different from the one by user3297291. But the code design is a bit different.
I try to be more generic both in the object traversal and in the testing for matches. I hope that both would be reusable functions. The traversal takes a predicate and a property name for the children to recurse on (in your case 'ROUTES'
) and returns a function that filters the object supplied to it.
For the predicate, I pass the result of calling matchesRestrictions
with something like your USER_RESTRICTIONS
object. The thought is that there will likely be other restrictions possible. I assume that that if the value is a boolean, then the object must have the same boolean value for that key. If it is an array, then every item in it must appear in the array at that key. It's easy enough to add other types. This might be too generic, though; I really don't know what else might appear in USER_PERMMISSIONS
or a RESTRICTIONS
section.
This is the code I came up with:
const filterObj = (pred, children) => (obj) =>
Object .fromEntries (
Object .entries (obj)
.filter ( ([k, v]) => pred (v))
.map ( ([k, v]) => [
k,
v [children]
? {
...v,
[children]: filterObj (pred, children) (v [children])
}
: v
]
)
)
const matchesRestrictions = (config) => ({RESTRICTIONS = {}}) =>
Object .entries (RESTRICTIONS) .every (([key, val]) =>
typeof val == 'boolean'
? config [key] === val
: Array.isArray (val)
? val .every (v => (config [key] || []) .includes (v))
: true // What else do you want to handle?
)
const ROUTES = {ACCOUNT: {TO: "/account", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {PROFILE: {TO: "/account/profile", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {INFORMATION: {TO: "/account/profile/information", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL"]}}, PASSWORD: {TO: "/account/profile/password", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL", "ADMIN"]}}}}, COLLECTIONS: {TO: "/account/collections", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["ADMIN"]}}, LIKES: {TO: "/account/likes", RESTRICTIONS: {shouldBeLoggedIn: true}}}}};
const USER_RESTRICTIONS = {shouldBeLoggedIn: true, permissions: ['EMAIL']}
console .log (
filterObj (matchesRestrictions (USER_RESTRICTIONS), 'ROUTES') (ROUTES)
)
I don't know how generic filterObj
ended up being. But I did test it with another object and a different path to the children:
const obj = {x: {foo: 1, val: 20, kids: {a: {foo: 2, val: 15, kids: {b: {foo: 3, val: 8}, c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}}, e: {foo: 6, val: 5, kids: {f: {foo: 7, val: 23}, g: {foo: 8, val: 17}}}, h: {foo: 9, val: 11, kids: {i: {foo: 10, val: 3}, j: {foo: 11, val: 7}}}}}, y: {foo: 12, val: 8}, z: {foo: 13, val: 25, kids: {k: {foo: 14, val: 18, kids: {l: {foo: 5, val: 3}, m: {foo: 11, val: 7}}}}}}
const pred = ({val}) => val > 10
filterObj ( pred, 'kids') (obj)
getting this result:
{x: {foo: 1, kids: {a: {foo: 2, kids: {c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}, val: 15}, h: {foo: 9, kids: {}, val: 11}}, val: 20}, z: {foo: 13, kids: {k: {foo: 14, kids: {}, val: 18}}, val: 25}}
so it's at least somewhat reusable.
Upvotes: 0
Reputation: 1808
I "solved" it like this:
export const checkLoggedIn = (shouldBeLoggedIn, isAuthenticated) => {
if (!shouldBeLoggedIn) {
return true;
}
return isAuthenticated;
};
function isRouteAllowed(route, restrictions) {
const routeShouldBeLoggedIn = route.RESTRICTIONS.shouldBeLoggedIn;
const passedLoggedInCheck = checkLoggedIn(
routeShouldBeLoggedIn,
restrictions.get('shouldBeLoggedIn')
);
if (!passedLoggedInCheck) {
return false;
} else {
const routePermissions = route.RESTRICTIONS.permissions;
if (!routePermissions) {
return true;
} else {
const passedPermissions = routePermissions.every((permission) => {
const restrictPermissions = restrictions.get('permissions');
return (
restrictPermissions &&
restrictPermissions.find &&
restrictPermissions.find(
(userPermission) => userPermission === permission
)
);
});
return passedLoggedInCheck && passedPermissions;
}
}
}
function forEachRoute(
routes,
restrictions,
routesToDelete = [],
parentPath = []
) {
const routeSize = Object.keys(routes).length - 1;
Object.entries(routes).forEach(([key, route], index) => {
const childRoutes = route.ROUTES;
if (childRoutes) {
parentPath.push(key);
parentPath.push('ROUTES');
forEachRoute(childRoutes, restrictions, routesToDelete, parentPath);
} else {
const allowed = isRouteAllowed(route, restrictions);
if (!allowed) {
const toAdd = [...parentPath, key];
routesToDelete.push(toAdd);
}
}
if (routeSize === index) {
// new parent
parentPath.pop();
parentPath.pop();
}
});
}
const deletePropertyByPath = (object, path) => {
let currentObject = object;
let parts = path.split('.');
const last = parts.pop();
for (const part of parts) {
currentObject = currentObject[part];
if (!currentObject) {
return;
}
}
delete currentObject[last];
};
export function removeRestrictedRoutes(routes, restrictions) {
let routesToDelete = [];
forEachRoute(routes, restrictions, routesToDelete);
let allowedRoutes = routes;
routesToDelete.forEach((path) => {
deletePropertyByPath(allowedRoutes, path.join('.'));
});
return allowedRoutes;
}
To be used like:
const USER_RESTRICTIONS = {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
}
const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)
Not the most performant solution but it worked. @user3297291 solution seems much better so will refactor to that, just have to make it a bit more readable. I thought a solution with .reduce()
would have been the best one, but maybe not possible.
Upvotes: 0
Reputation: 23382
First, without thinking about the recursive stuff, make sure you have your rule logic well defined.
I attempted to write a validation function using your required API, but don't think it's very readable. You might want to refactor it later. (Tip: write some unit tests!)
The example below takes a rule configuration object and a node from your tree. It returns a boolean indicating whether the node matches the requirements.
const includedIn = xs => x => xs.includes(x);
// RuleSet -> Path -> bool
const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) =>
({ RESTRICTIONS }) => (
(shouldBeLoggedIn ? RESTRICTIONS.shouldBeLoggedIn : true) &&
RESTRICTIONS.permissions.every(includedIn(permissions))
);
console.log(
[
{ RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ ] } },
{ RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL' ] } },
{ RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL', 'ADMIN' ] } }
].map(
isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] })
)
)
With this piece of code sorted, you can start thinking about how to traverse the tree. What you're basically defining is how to loop over each path and when to return.
If we just want to log, it's a matter of (1) checking ROUTES
, and (2) looping over the entries inside the v.ROUTES
object.
const traverse = obj => {
Object
.entries(obj)
.forEach(
([k, v]) => {
console.log(v.TO);
if (v.ROUTES) traverse(v.ROUTES)
}
)
};
traverse(getRoutes());
function getRoutes() {
return {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
PASSWORD: {
TO: '/account/profile/password',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL', 'ADMIN'],
},
},
},
},
COLLECTIONS: {
TO: '/account/collections',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['ADMIN'],
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
};
};
Then comes the hardest part: creating a new tree structure.
I chose to take two steps:
filter
out the values that don't pass validation,If there are child routes, we create a new path object that has a filtered ROUTES value.
const traverse = (obj, pred) => Object
.fromEntries(
Object
.entries(obj)
.filter(
([k, v]) => pred(v) // Get rid of the paths that don't match restrictions
)
.map(
([k, v]) => [
k, v.ROUTES
// If there are child paths, filter those as well (i.e. recurse)
? Object.assign({}, v, { ROUTES: traverse(v.ROUTES, pred) })
: v
]
)
);
const includedIn = xs => x => xs.includes(x);
const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) =>
({ RESTRICTIONS }) => (
(shouldBeLoggedIn ? RESTRICTIONS.shouldBeLoggedIn : true) &&
(RESTRICTIONS.permissions || []).every(includedIn(permissions))
);
console.log(
traverse(
getRoutes(),
isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] })
)
)
function getRoutes() {
return {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
PASSWORD: {
TO: '/account/profile/password',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL', 'ADMIN'],
},
},
},
},
COLLECTIONS: {
TO: '/account/collections',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['ADMIN'],
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
};
};
I hope this example can get you started and enables you write your own/polished version. Let me know if I missed any requirements.
Upvotes: 2