JS: Compare strings of an array with object fields - javascript

There is an object, which represents the role of an user:
const user = {
isEditor: false,
isAdmin: true,
isTranslator: false
}
I need to pass some elements to a function an check if any of this role has a true value in the user object.
result = hasPermission(user, ['editor', 'admin']) // true
result = hasPermission(user, ['editor', 'translator']) // false
I've some problems with that, as the roles are named a bit differently.
I thought also about using _.some() as I need to check for only one true value.
function hasPermission (user, roles) {
roles.forEach(role => {
user[role] // is not working as user keys named differently
})
}

What you are doing is not the proper way to think about it.
A users role are not individual properties, rather a collection of roles.
So your user object should be model like
const MyUser = {
roles: ["Admin","Editor"]
}
So then you can check if a user has a given role by working with a function like so :
function UserHasAnyRole(user,roleKeys)
{
if(user && user.roles && roleKeys && roleKeys.length)
{
return user.roles.filter(function(r){
return roleKeys.filter(function(k){ return k == r;}).length>0;
}).length > 0;
}
return false;
}
var neededRoles = ["Admin","Translator"];
if(UserHasAnyRole(MyUser,neededRoles))
{
// do stuff
}
This way of thinking about it scales much better, as individual properties is not a sustainable way in the long term
EDIT: to account for arrray of input roles. This is not tested tho, so there might be some syntax errors in there, but you get the idea...

Here is a solution the closest to your situation, without changing the shape of your objects and functions.
You can actually use some like this, it is quite simple:
const user = {
isEditor: false,
isAdmin: true,
isTranslator: false
};
function hasPermission (user, roles) {
return roles.some(role => user["is" + role[0].toUpperCase() + role.substring(1)]);
}
console.log(hasPermission(user, ['editor', 'admin']));
console.log(hasPermission(user, ['editor', 'translator']));
Hoping this will help you!

You can use the same property names, Object.entries() to get an array of property, values pairs of the object and Array.prototype.some() and Array.prototype.includes() to check if the values match
const user = {
isEditor: false,
isAdmin: true,
isTranslator: false
}
const hasPermission = (o, keys) =>
Object.entries(o).some(([key, prop]) => prop && keys.includes(key));
let result = hasPermission(user, ["isEditor", "isAdmin"]);
console.log(result);
result = hasPermission(user, ["isEditor", "isTranslator"]);
console.log(result);

Here is yet another solution.
const user = {
isEditor: false,
isAdmin: true,
isTranslator: false
};
function hasPermission (user, roles) {
const userRoles = Object
.entries(user)
.filter(([_, val]) => val)
.map(([key, _]) => key.replace('is', '').toLowerCase());
return roles.some(x => userRoles.includes(x));
}
console.log(hasPermission(user, ['editor', 'admin']));
console.log(hasPermission(user, ['editor', 'translator']));

Related

How can I only add a key to an object if its corresponding function argument exists without excessive if statements?

I would like to only add the values to the return object if they hold a value otherwise I don't even want to send them. I know this can be achieved with if statement, but how would I do this if there were even more?
As you can see based on the required key being true, in the code I have a few of these arguments that can but don't actually need to exist. I'd like to avoid adding them if that is true.
I am using MongoDB so if I don't need to add the field I don't want to have to, but the only way I know how to achieve this is by writing a bunch of if statements to check if they exist. Is there a better way to do this?
The reason why MongoDB is relevant is that MongoDB is a no SQL database so just because something is an option doesn't mean it has to exist.
What i tried so far:
I tried returning undefined if the item doesn't exist this does not work however Example Below
resolve: (_, { input: { createdBy, name, date, description, cost, link, weights, eventApplicants } }) => {
return dbMutations.createEvent({
createdBy, name, date, description,
cost: cost ? cost : undefined, link: link ? link : undefined, weights: weights && JSON.parse(weights),
eventApplicants: eventApplicants && JSON.parse(eventApplicants)
})
}
Code:
You really just need to look at the resolve function, but I put the rest of the code there for reference.
createEvent: t.fieldWithInput({
input: {
createdBy: t.input.field({
type: 'mongoId',
required: true
}),
name: t.input.string({
required: true
}),
date: t.input.field({
type: 'Date',
required: true
}),
description: t.input.string({
required: true
}),
cost: t.input.string(),
link: t.input.string(),
weights: t.input.string(),
applicants: t.input.string(),
},
type: 'mongoId',
// #ts-ignore
resolve: (_, {
input: {createdBy, name, date, description, cost, link, weights, applicants}
}) => {
let parsedWeights
let parsedApplicants
if (weights) {
parsedWeights = JSON.parse(weights)
}
if (applicants) {
parsedApplicants = JSON.parse(applicants)
}
return dbMutations.createEvent({
createdBy,
name,
date,
description,
cost,
link,
weights: parsedWeights,
applicants: parsedApplicants
})
}
})
My Idea:
I think what would work is just assigning these variables to an arbitrary object. Once we do that we can do some kind of object map and set the result to the return statement.
Here's a thing that removes null or undefined values. You may want to tweak the condition to take out empty strings or other falsy things.
const clean = obj =>
Object.fromEntries(
Object.entries(obj).filter(([k, v]) => v != null)
);
let o = { a: 0, b: null, c: '' };
console.log( clean(o) )
With it, the resolve function can say...
resolve: (_, {
input: {createdBy, name, date, description, cost, link, weights, applicants}
}) => {
// or promote this in scope to use elsewhere...
const clean = obj =>
Object.fromEntries(
Object.entries(obj).filter(([k, v]) => v != null)
);
const event = clean({
createdBy,
name,
date,
description,
cost,
link,
weights: weights ? JSON.parse(weights) : null,
applicants: applicants ? JSON.parse(applicants) : null
});
console.log(event); // examine this to prove event is the object you want
return dbMutations.createEvent(event)
}

chain logical AND without fixed length given an array in Node.js and Typescript

Scenario:
I am making a generic function that returns a boolean depending on logical AND statements, however, the function being generic accept multiple type of objects and arrays, and the statements can vary depending on the objects.
at the moment I have something like this
private async myFunction(
myArray: myArrObj[],
myObj : myObj,
): Promise<boolean> {
return (
myArr.some(
(a) =>
a.status1=== "*" ||
a.status1 === myObj.status1.status1Id
) &&
myArr.some(
(a) =>
a.status2=== "*" ||
a.status2 === myObj.status2.status2Id
) &&
myArr.some(
(a) =>
a.status3=== "*" ||
a.status3 === myObj.status3.status3Id
) &&
myArr.some(
(a) =>
a.status4=== "*" ||
a.status4 === myObj.status4.status4Id
)
)
}
the issue is not being able to know what kind of array is passed and how many checks are needed, how can I make a return? My idea was storing each array.some method in an array and join them with " && ", this approach would require to execute something from a string, which I'm not sure is the most secure thing to do, since eval is not secure at all.
to get the myObj statuses I could just use a for loop and store the the the property in a string.
I can't come up with a good solution, so feel free to propose something new if my idea is not good enough
As noted by others in the comments, it would help if you had a reproducible example with sample data. That being said, from your comment:
but the statuses and id's have different names, some id's are .nameId, and some are just .id , but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
Breaking this down:
but the statuses and id's have different names, some id's are .nameId, and some are just .id
You could try to see if nameId exists and fall back to id.
but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
When myArr entries share keys with myObj, then you could simply loop through myObj's keys.
async function myFunction(myArr, myObj) {
// Fallback value for if .nameId and .id both don't exist.
// Falling back to `undefined` would cause a bug / false positives.
const notFound = Symbol();
// Loop through every key:value pair in the input object.
return Object.entries(myObj).every(([myObjKey, myObjValue]) => {
// Handle both `.nameId` and `.id`
const id = myObjValue[`${myObjKey}Id`] ?? myObjValue.id ?? notFound;
// If `myArrObj`'s children only ever contain exactly
// a single key { status2: { someRandomKey: 123 } }, then you
// could use myObjValue[Object.keys(myObjValue)[0]];
// For this key--for example "status1"--is there *any* array entry
// in `myArrObj` that has the same key and value or "*"?
return myArr.some((a) => {
return a[myObjKey] === '*' || a[myObjKey] === id;
});
});
}
With the following sample data:
const sampleArr = [
{ status3: "*" },
{ status2: 234 },
{ status1: 123, thisIsAnUnusedKey: true },
{ status4: 456 },
{ name: "Foobar" },
{ thisIsAnUnusedArrayEntry: true },
];
const sampleObj = {
status1: {
status1Id: 123,
},
status2: {
status2Id: 234,
},
status3: {
status3Id: 345,
},
status4: {
// Different key
id: 456,
},
name: {
// Different dataType
nameId: "Foobar"
}
};
myFunction(sampleArr, sampleObj).then(console.log); // Logs `true`

Process multiple request queries

I have an API that serves JSON data. Currently if you do api/weapons for example it gives you all the weapons available, api/weapons/weaponName gives information about that specific weapon. What I want to do is be able to api/weapons?type=sword&rarity=5 for example. I managed to pull of api/weapons?type=sword and api/weapons?rarity=5 on their own but not together.
Here's what I'm currently doing:
let filtered = [];
if (query.type) {
filtered = filtered.concat((await weapons).filter(w => formatName(w.weaponType) === formatName(query.type)));
}
if (query.rarity) {
filtered = filtered.concat((await weapons).filter(w => w.rarity == query.rarity));
}
if (!filtered.length) filtered = [await weapons]
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
formatName is just a function that makes the string all lowercase and trims it and removes all spaces.
If we take api/weapons?type=sword&rarity=5
I think what's happening right now is:
It is getting all the weapons with the type "sword"
It is getting all the weapons with the rarity "5"
It is joining all the results together, so all the weapons with the type sword (regardless of rarity) and al the weapons with the rarity 5 (regardless of type).
I want it to filter weapons with ONLY that rarity AND ONLY that type. So only 5 rarity swords for example. What is the most beneficial way of handling this
I'd suggest retrieving "weapons" once and then running any filters on them without concatenating the results:
let filtered = [ ...(await weapons) ];
if (query.type) {
filtered = filtered.filter(w => w => formatName(w.weaponType) === formatName(query.type));
}
if (query.rarity) {
filtered = filtered.filter(w => w.rarity == query.rarity);
}
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
Your current logic is testing whether one constraint OR another matches, what you actually need to do is to do an AND, which means you must perform the test in a single pass of filter.
I would slightly modify your code so that you compare all constraints that you're sending...you could further modify the logic below to accept a logical operator to test whether the rarity is >= or <= to a certain number for example.
const weapons = [{
type: 'sword',
name: 'swift blade of zek',
rarity: 5
},
{
type: 'mace',
name: 'hammer of kromzek kings',
rarity: 1
},
{
type: 'sword',
name: 'split blade of thunder',
rarity: 2
},
{
type: 'sword',
name: 'blade of carnage',
rarity: 5
},
]
const getWeapons = (query = {}) => {
let filtered = [];
let constraints = [];
// We could build this object dynamically but I just wanted
// to demonstrate it using your current approach
if (query.hasOwnProperty('type')) {
constraints.push({
name: 'type',
value: query.type
})
}
if (query.hasOwnProperty('rarity')) {
constraints.push({
name: 'rarity',
value: query.rarity
})
}
// Compare all of the conditions and only return weapons
// that match all of the conditions passed.
filtered = weapons.filter(w => {
let matches = 0
constraints.forEach(c => {
if (w[c.name] === c.value) {
matches += 1
}
})
// ensures we only return complete matches
return matches === constraints.length
});
return filtered
}
console.log(getWeapons({
type: 'sword',
rarity: 5
}))
Create an object which has the same property keys as the filters you want to use. Assign a function to each property where the evaluation for that specific filter is specified.
const filters = {
type: (weapon, type) => formatName(weapon.weaponType) === formatName(type),
rarity: (weapon, rarity) => weapon.rarity === rarity,
};
Then loop over the weapons with filter. Inside the filter loop, loop over the keys of the query variable with the every method. This method will return true or false based on if every evaluation is true or not.
In the every loop, use the keys of the query to select the filter from the filters list. Pass the weapon and the values of the query object to these filter functions and return result.
By doing this you can use one, two or no filters at all. And any new filters can be added in the filters object.
const filteredWeapons = weapons.filter((weapon) =>
Object.keys(query).every((filterKey) => {
if (!(filterKey in filters)) {
return false;
}
const filter = filters[filterKey]
const value = query[filterKey]
return filter(weapon, value);
})
);
res.status(HttpStatusCodes.ACCEPTED).send(filteredWeapons);

Typescript - if conditrional inside a map

I am mapping a subset of user data to an object of a refined data set. Inside the map i want to check if a variable is null or undefined, and if yes, then to set this variable to a placeholder value.
The issue I am facing is that declaring an if statement inside the map is causing an error, but even though a map can have an index as a parameter, how can we use it functionally with a conditional Statement? Any insight most appreciated.
return this.UserService.search(url)
.map((data) => {
console.log(data);
data.result = <any> data.result.map((user, index) => ({
// if statement causing error here
if(user.name === null || user.name === undefined){
// error with this if condition
},
id: user.id,
name: user.name,
type: user.type,
password: user.password,
}));
return data;
}).map(data => ({
meta: { totalItems: data.size },
data: data.result,
}));
You're attempting to use an object literal as the return type, but naturally, an if statement (or any statement) can't be inside object literal syntax.
So instead, define a function body, which also uses curly braces, and put your code inside with an explicit return statement.
// Starts function body instead of single expression-v
data.result = <any> data.result.map((user, index) => {
if (some_condition) {
return "some value"; // The object?
} else {
return "other value"; // A different object?
}
/*
// I assume these are to be used in the object you return
id: user.id,
name: user.name,
type: user.type,
password: user.password,
*/
});
You can express conditions in literal maps, but it is somewhat ugly.
return {
a: 1,
...(some_condition && {
b: 1,
})
};
As far as i know you can't do that with JUST a map.
however you could follow it up with a filter() function:
const newArray = oldArray.map((value, index) => condition ? value : null).filter(v => v);
you basicaly iterate over each item and then return the value or null depending on your condition.
Now once you have the map you just filter it by removing the null values from the array.
Notice that the original array is not altered and a new one is returned.
thanks for the idea #user8897421 for the idea. i just wanted to turn it into a one liner.

Why can't I delete a mongoose model's object properties?

When a user registers with my API they are returned a user object. Before returning the object I remove the hashed password and salt properties. I have to use
user.salt = undefined;
user.pass = undefined;
Because when I try
delete user.salt;
delete user.pass;
the object properties still exist and are returned.
Why is that?
To use delete you would need to convert the model document into a plain JavaScript object by calling toObject so that you can freely manipulate it:
user = user.toObject();
delete user.salt;
delete user.pass;
Non-configurable properties cannot be re-configured or deleted.
You should use strict mode so you get in-your-face errors instead of silent failures:
(function() {
"use strict";
var o = {};
Object.defineProperty(o, "key", {
value: "value",
configurable: false,
writable: true,
enumerable: true
});
delete o.key;
})()
// TypeError: Cannot delete property 'key' of #<Object>
Another solution aside from calling toObject is to access the _doc directly from the mongoose object and use ES6 spread operator to remove unwanted properties as such:
user = { ...user._doc, salt: undefined, pass: undefined }
Rather than converting to a JavaScript object with toObject(), it might be more ideal to instead choose which properties you want to exclude via the Query.prototype.select() function.
For example, if your User schema looked something like this:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
name: {
type: String,
required: true
},
pass: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
module.exports = {
User: mongoose.model("user", userSchema)
};
Then if you wanted to exclude the pass and salt properties in a response containing an array of all users, you could do so by specifically choosing which properties to ignore by prepending a minus sign before the property name:
users.get("/", async (req, res) => {
try {
const result = await User
.find({})
.select("-pass -salt");
return res
.status(200)
.send(result);
}
catch (error) {
console.error(error);
}
});
Alternatively, if you have more properties to exclude than include, you can specifically choose which properties to add instead of which properties to remove:
const result = await User
.find({})
.select("email name");
The delete operation could be used on javascript objects only. Mongoose models are not javascript objects. So convert it into a javascript object and delete the property.
The code should look like this:
const modelJsObject = model.toObject();
delete modlelJsObject.property;
But that causes problems while saving the object. So what I did was just to set the property value to undefined.
model.property = undefined;
Old question, but I'm throwing my 2-cents into the fray....
You question has already been answered correctly by others, this is just a demo of how I worked around it.
I used Object.entries() + Array.reduce() to solve it. Here's my take:
// define dis-allowed keys and values
const disAllowedKeys = ['_id','__v','password'];
const disAllowedValues = [null, undefined, ''];
// our object, maybe a Mongoose model, or some API response
const someObject = {
_id: 132456789,
password: '$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/',
name: 'John Edward',
age: 29,
favoriteFood: null
};
// use reduce to create a new object with everything EXCEPT our dis-allowed keys and values!
const withOnlyGoodValues = Object.entries(someObject).reduce((ourNewObject, pair) => {
const key = pair[0];
const value = pair[1];
if (
disAllowedKeys.includes(key) === false &&
disAllowedValues.includes(value) === false
){
ourNewObject[key] = value;
}
return ourNewObject;
}, {});
// what we get back...
// {
// name: 'John Edward',
// age: 29
// }
// do something with the new object!
server.sendToClient(withOnlyGoodValues);
This can be cleaned up more once you understand how it works, especially with some fancy ES6 syntax. I intentionally tried to make it extra-readable, for the sake of the demo.
Read docs on how Object.entries() works: MDN - Object.entries()
Read docs on how Array.reduce() works: MDN - Array.reduce()
I use this little function just before i return the user object.
Of course i have to remember to add the new key i wish to remove but it works well for me
const protect = (o) => {
const removes = ['__v', '_id', 'salt', 'password', 'hash'];
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
and i use it as I said, just before i return the user.
return res.json({ success: true, user: await protect(user) });
Alternativly, it could be more dynamic when used this way:
const protect = (o, removes) => {
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
return res.json({ success: true, user: await protect(user, ['salt','hash']) });

Categories

Resources