Apply unique only inside array or sub-document mongoose - javascript

Hope you are doing good.
I have a schema like this:
const Person = new mongoose.Schema({
username: {
type: String,
required: [true, "Name is a required field"],
unique: [true, "Another Person with the same name already exists"],
trim: true
},
friends: [
{
name: {
type: String,
required: [true, "Every friend must have a name"],
unique: [true, "Another friend with the same name already exists"],
trim: true
},
favoriteFood: String
}
],
createdAt: {
type: Date,
default: Date.now()
}
});
here I want name just be unique inside friends array, obviously different persons can have the same friend. but MongoDB does not let me define two person with the same name inside different persons object with this implementation. how can I do that?how to force friends name only be unique for one person?
another thing I found is if I try to add two friends with the same name under one person it will be accepted, but if you try to add same friend name across two different persons, it will throw the error of duplication. it is the exact opposite of what it should be, or at least opposite of what I want.
thanks.

It seems there is no default way, or some sort of indexing that can do that, so we have to check for duplication manually. I done it this way:
//check for duplications
Person.pre("save", function (next) {
const friends = this.friends;
const lookup = friends.reduce((a, e) => {
a[e.name] = ++a[e.name] || 0;
return a;
});
const duplicates = friends.filter(friend => lookup[friend.name]);
if (duplicates.length) next(new Error("There are two friends with the same name"));
else next();
});

Related

Trying to display "trade options" to a user based on the games they have in their library. Is there a faster way to do this?

I'm trying to build a platform where each user is able to add games to their library, under either the "owned" or "wanted" categories, and then it is the platform's responsibility to find possible trade options.
For example, if user A has game A in their owned list, and user B wants game A, and owns games C and D, then user A will see games C and D under their possible trade options, in return for game A.
Currently, I've implemented this somewhat crudely using too many nested DB queries and I feel like there's a faster way to do this.
The following is what happens every time a user wants to see all of their trade options.
let foundUser = await User.findById({_id: req.session.user_id})
let gamesList = []
//for each game in the users have library, search it up in the game DB and add wantedBy users to an array
for (let i = 0; i < foundUser.library.have.length; i++){
let foundGame = await Game.findById({_id: foundUser.library.have[i].gameID})
//for this game, add each user in the WantedBy List to a local array
for (let j = 0; j < foundGame.wantedBy.length; j++){
//for each of these users, use their have list to build the trade object
let newFoundUser = await User.findById({_id: foundGame.wantedBy[j]})
//check if the other user wants the game for the same platform as foundUser.library.have[i].platform
console.log(newFoundUser)
console.log(foundGame.wantedBy)
let platformMatch = newFoundUser.library.want.find(game => (game.platform === foundUser.library.have[i].platform) && (game.gameID == foundUser.library.have[i].gameID))
if (platformMatch){
for (let k = 0; k < newFoundUser.library.have.length; k++){
//for each of these games in the other users have list, make a trade object
let newFoundGame = await Game.findById({_id: newFoundUser.library.have[k].gameID})
let tradeObject = {
gameOffered: newFoundUser.library.have[k].gameID,
gameOfferedBackImg: newFoundGame.backImg,
gameOfferedPlatform: newFoundUser.library.have[k].platform,
gameOfferedTitle: newFoundGame.title,
inReturnForTitle: foundGame.title,
inReturnForPlatform: foundUser.library.have[i].platform,
inReturnFor: foundGame._id,
otherUserID: newFoundUser._id
}
gamesList.push(tradeObject)
}
}
}
}
res.render('trade', {gamesList: gamesList, mainUser: foundUser._id})
The nested loops and DB queries make this really slow, especially if the no. of users and the no. games in their library increases.
The following are the DB schemas:
The User Schema:
username: {
type: String,
required: ['true', 'Username cannot be blank'],
},
email: {
type: String,
required: ['true', 'Email cannot be blank']
},
password: {
type: String,
required: ['true', 'Password cannot be blank'],
},
phoneNum: {
type: String,
required: true,
unique: true
},
isVerified: false,
address: {
city: String,
area: String,
street: String,
nearestLandmark: String
},
verificationCode: String,
library: {
have: [{
gameID: String,
platform: String
}],
want: [{
gameID: String,
platform: String
}]
},
notifications: [
{
dateTime: Date,
message: String
}
]
})
The Game Schema:
const gameSchema = new mongoose.Schema({
title: String,
backImg: String,
wantedBy: [String]
})
Any help optimising this would be greatly appreciated. Thank you!

How to return an error when $addToSet does not add an item because it already exists in Mongoose?

I have the following schema which contains a property with an array:
const projectSchema = new mongoose.Schema(
{
title: {
type: String,
required: [true, "Please add a title"],
},
users: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
},
);
In my project controller, I'm trying to add users to this array without getting a duplicate user. So I use $addToSet. It works fine but it doesn't return an error when there is a duplicate user.
const project = await Project.findOneAndUpdate(
{ _id: id },
{ $addToSet: { users: userID } },
{ new: true }
);
How can I detect that it didn't add a user (because the user already exists in the array) and return an error?

Mongoose auto increment for existing collection

I have an existing collection named users with the following schema. I want to add a new auto incremented field named userNumber in it. I have seen the counter based solution but failed to implement those mainly because I don't see the working where it will do the numbering for the existing documents plus where to place that code. So my question is how to add userNumber field with auto incrementing values and how to populate values for this column in existing records
user.model
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
userType: {
type: String,
required: false,
},
uid: {
type: String,
required: false,
},
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
},
email: {
type: String,
lowercase: true,
},
});
module.exports = mongoose.model('User', userSchema);
here is how it works with counter:
export const CounterSchema = new mongoose.Schema({
_id: {type: String, required: true},
seq: {type: Number, default: 0}
},
the _id could be anything you want. In that case i would name it user
the seq (sequence) is your current count, starting at 0.
to update the counter you will have to call it every time you create a new user.
const callBack: MongooseDocument = await counterModel.findOneAndUpdate({_id:
'user'}, {$inc: {seq: 1}}, {new: true});
to access the new count you call: newCount = callBack.toJSON().seq
to make it work for you current situation, you will have to loop through your users and update them one by one. When that is done, you update your counter and after that you do it whenever you create a user..
First you can add userNumber to your User schema and when you want to create new user then add value to userNumber automatically by get old value from database and incremented.

Update or push object into nested array in found document mongoose

I have this mongoose Schema:
const UserSchema = new mongoose.Schema({
name: {
type: String,
trim: true
},
userId: {
type: String,
required: [true, "user ID required."],
unique: [true, "user ID must be unique"]
},
votes: [
{
pollId: type: mongoose.Schema.Types.ObjectId,
candidates: [
{
candidateId: mongoose.Schema.Types.ObjectId
}
]
}
],
role: {
type: String,
enum: ["user", "admin"],
default: "user"
}
});
I find user first, because I want to authorize the user. and now I want to update some parts. just want to be clear, I want to update some parts of a found user document.
I have multiple polls that user can vote for multiple candidates in each one. so if user has not voted at all, votes array will be empty, and we have to push first pollId and also first candidateId that he/she votes. and if the pollId exists,we have to find that subdocument first by pollId then we should just add candidateId inside candidates array.
how can I do this? preferred is just one operation not multiple. and if I can get updated user its better.
if it's not clear let me know. I'll try to explain more.
thanks.
I would do something like this
function updateUsersVotes(userId, newVote) {
User.findById(userId)
.exec()
.then((dbUser) => {
if (!dbUser.votes.length) { // no votes yet
dbUser.votes.push(newVote);
} else { // check current votes
const voteIndex = dbUser.votes.findIndex((vote) => vote.pollId.toString() === newVote.pollId.toString());
if (voteIndex > 0) { // if the incoming vote pollId matches an existing pollId
dbUser.votes[voteIndex].candidates.push(...newVote.candidates);
} else {
dbUser.votes.push(newVote); // if the incoming pollId can't be found, then add it.
}
}
dbUser.save({ validateBeforeSave: true }); // update the user.
})
.catch((error) => {
handleError(error); // you should always handle your errors.
});
}

How to assert types in an array of objects with Chai?

I have the following context:
const data = [
{
id: 1,
name: 'thenamefoo',
modified: new Date() // random date
},
{
id: 2,
name: 'namebar',
modified: new Date() // random date
},
...
];
expect(data)...
I want to assert that my data will always be an array that have objects with fixed keys (and types).
For instance, I want something like
expect(data)
.to.be.an('array')
.that.all.have.types.like({
id: Number,
name: String,
modified: Date
});
Is it possible? How? Any libs?
In my opinion you should focus on validating the data instead of toying with a clunky assertion DSL. With simple true/false checks, all you need is the humble assert:
test('my data is valid', () => {
data.forEach(({id, name, modified}) => {
assert(typeof id === 'number', `${id} is not a number`);
assert(typeof name === 'string', `${name} is not a string`);
assert(modified instanceof Date, `${modified} is not a date`);
});
});
Going further
This of course doesn't help much if you need to check other things:
The array is not empty
Each object has exactly id, name and modified as properties. No less no more
id is a positive integer
name is not an empty string
...
For more fined-grained control you should definitely look into jsonschema.

Categories