Mongoose unique NanoID - node.js

const user = new mongoose.Schema(
{
nano_id: {
type: String,
required: true,
default: () => nanoid(7),
index: { unique: true },
},
...
}
How to run again nanoid(7) if is not unique? (run automatically and not get any error in console)

There are two ways to do this:
Prevent the error from happening in the first place by searching for a document with a similar Nano ID, if a document exists, regenerate a new Nano ID using a recursive function.
const { customAlphabet } = require('nanoid');
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
const nanoid = customAlphabet(alphabet, 8);
// userRegistration controller or route...
async function uniqueNanoId(query): Promise<string> {
const nanoId = nanoid();
const sameNanoId = await User.findOne({ nano_id:nanoId });
if (sameNanoId) {
return uniqueNanoId(query);
}
return nanoId;
}
const nanoId = await uniqueNanoId();
const user = User.create({...userBody,nanoId});
//...
Catch the error - as #cachius hinted - and regenerate the unique Nano ID accordingly (not tested). Catching a duplicate key has been discussed here
Bonus: Ask yourself the question, do I really need both Default Mongoose IDs and Nano IDs? If not, then this is a simple solution.
// ...
_id: {
type: String,
default: () => nanoid(),
},
// ...

The database will throw an error you have to catch and react to by generating the same record again with a newly generated nanoid.

Related

Removing an element from the array in MongoDB through mongoose

I've defined a MongoDB model using mongoose in my NodeJS application as:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
rToken: [String]
})
module.exports = mongoose.model("User", userSchema);
Every rToken is unique. I want to remove an element from the rToken array specified by the user in the request body, therefore, I've created a route below:
module.exports.logout = async (req, res) => {
const token = req.body.token;
User.updateOne({}, {$pull: {$in: [token]}});
res.sendStatus(204);
}
But it isn't removing the element and I'm still getting 204 status. Please help.
If rToken is an object you can remove element like that, assuming you remove a password key:
const { password, ...rTokenWithoutPassword } = rToken;
Then use rTokenWithoutPassword to do anything.
If you want to remove more than one key, salt and password for example:
const { password, salt, ...rTokenWithoutPassword } = rToken;
If you need to remove from within mongoDB, assuming rToken is an array of strings, you need to modify your Mongoose model because actually it is a String, not an array.
Then, according to mongodb documentation: https://www.mongodb.com/docs/manual/reference/operator/update/pull/
Modify your update request, assuming "abcd" is the value to pull off:
User.updateOne({}, { $pull: { rToken: { $eq: 'abcd' } } });
Here is a playground: https://mongoplayground.net/p/Fq-bH7vBUF7

Mongoose document _id is null, so when I try to save I get MongooseError: document must have and id before saving

I'm making a discord bot to scrape prices from Amazon. Im using a mongoDB database to store links users give to the bot to track the price of the item the link leads to.
My issue is when I run my code and use the add command, my console reads...
Starting...
Online! Logged in as Amazon Price Tracker#6927
Connected to Database
null
MongooseError: document must have an _id before saving
at C:\Users\logic\Documents\Disc Bot\node_modules\mongoose\lib\model.js:291:18
at processTicksAndRejections (node:internal/process/task_queues:78:11)
Disconnected from Database
I've read the doc's and my understanding is mongoose generates a unique id automatically. I am aware that you can override this my defining an id in your schema, but I haven't done this so I don't know why console.log(a) prints null, and the .save() errors out.
My add.js file
//add function using mongoose for mongodb
const { SlashCommandBuilder } = require("#discordjs/builders");
const mongoose = require("mongoose");
const { MongoDBurl } = require("../config.json");
const Link = require("../Schemas/Link.js");
module.exports = {
//Build the slash command
data: new SlashCommandBuilder()
.setName("add")
.setDescription("add a url to watch list")
.addStringOption(option =>
option.setName("url")
.setDescription("url to add to watch list")
.setRequired(true),
),
//Function that runs when the command is used
async execute (interaction) {
const URL = interaction.options.getString("url");
const user = interaction.user.username;
await interaction.reply(`On it! Adding ${URL} to your watch list`)
//Connect to the database, throws an error if it can't connect
await mongoose.connect(MongoDBurl)
.then( () => console.log("Connected to Database"))
.catch(err => console.log(err));
//Check if the link is already in the database
var exists = await Link.exists({ link: URL}).exec()
.catch(err => console.log(err))
if (exists) {
console.log("This Document Already Exists")
interaction.editReply(`Oops! That link is already in my database.`)
} else {
//If the link dosen't exist, create a document and save it to the database
var newLink = new Link({ user: user }, { link: URL }, { price: "N/A" })
// Debuging variable
var a = newLink.id;
console.log(a)
await newLink.save()
.then( () => {
console.log("Document Saved")
interaction.editReply(`All done! I have saved ${URL} to your watch list.`)
})
.catch(err => {
console.log(err)
interaction.editReply("Oops! Something went wrong, I wasen't able to save this link.")
})
}
//Close the connection when we finish
await mongoose.connection.close()
.then( () => console.log("Disconnected from Database"))
}
};
My Link.js file
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const LinkSchema = new Schema({
user: {
type: String,
requiered: true
},
link: {
type: String,
requiered: true
},
price: {
type: String,
requiered: true
},
})
module.exports = mongoose.model("Link", LinkSchema);
When creating a new modal, the options must be within the same pair of curly braces, however when updating, its separate since you are changing multiple elements.
That's why the error was occurring. You have already shared a working piece of code so I'm guessing you no longer need one.
So I found my issue. I changed this line
var newLink = new Link({ user: user }, { link: URL }, { price: "N/A" })
To
const newLink = new Link({ user: user, link: URL, price: "N/A" });
I don't know why this fixed it, I don't think its because I changed var -> const, and looking at the documentation I thought the first line was the correct way to do this
The line I originally used from the documentation
Tank.updateOne({ size: 'large' }, { name: 'T-90' }, function(err, res) {
// Updated at most one doc, `res.nModified` contains the number
// of docs that MongoDB updated
});
Is this an error in the documentation? or a possible bug? either way the issue is now resolved.

How Should I Store Data In SubSchemas in MongoDB and Node JS?

In MongoDB, you store data by writing a schema and you use to do all your operation through some routing. My question is how should I store user data. Something like this:
const Page = new mongoose.Schema({
username: String
Password: String
lists: [{
list_name: String,
etc.
}]
});
This is the way that I want to do it because no one else would be able to access anyone else's Lists. My Issue is let's say they update a certain list_name how would I update ONLY that certain list_name instead of updating everything. Im sort of new to MongoDB so any help would be appreciated.
do thaun helped me out by linking me to the docs. After looking at the docs I found a bunch of things that I was not using. With that being said here is the Schama that I ended up using.
const Task = new mongoose.Schema({
text: {type: String, required: true}
})
const Page = new mongoose.Schema({
list_name : {type: String, unique: true},
task: [Task]
});
For accessing the Task schema for a post I did this.
router.post('/lists/:list', async (req, res) => {
try {
console.log(await Question.findOne({_id: req.params.list}))
const question = await Question.findOneAndUpdate({"_id": req.params.list}, {"$push": {task:{"text": req.body.text}}}, {new:true})
if (!question)
return res.status(404).json({})
else
return res.status(200).json(question)
} catch (error) {
return res.status(500).json({"error":error})
}
})
The get was a little bit more tricky because I couldn't find use findOne so I used a for loop. here is the code for the get.
router.get('/lists/:list/:task', async (req, res) => {
try {
const _list = req.params.list
const _task = req.params.task
console.log(_task)
const question = await Question.findOne({_id: _list})
for (let index = 0; index < question.task.length; index++) {
if(req.params.task = question.task[index]._id)
return res.status(200).json(question.task[index])
}
return res.status(404).json({})
} catch (error) {
return res.status(500).json({"error":error})
}
})
I hope this helps someone because it was a bit annoying to get this information. Cheers!

mongoose filter by multiple conditions and execute to update data

I am wondering what would be the best approach to make schema functions using mongoose. I have never used this so the way I think is somewhat limited, same goes for looking for docs, without knowing what's available, is not very efficient.
Through docs I found that either using findOneAndUpdate might solve the problem; but there are some constraints.
Here is the code I am planning to run:
models/Bookmark.js
const mongoose = require('mongoose')
const bookmarkItemSchema = new mongoose.Schema({
restaurantId: String,
cachedAttr: {
name: String,
latitude: Number,
longitude: Number,
},
})
const bookmarkListSchema = new mongoose.Schema({
listName: String,
items: [bookmarkItemSchema],
})
const bookmarkSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
lists: [bookmarkListSchema],
})
// const add = (lists, userId) => {
// let bookmark = Bookmark.findOne({userId})
// bookmark.lists.listName === lists.listName //current, new
// ? bookmark.lists.items.push(lists.items)
// : bookmark.lists.push(lists)
// return bookmark
// }
mongoose.model('Bookmark', bookmarkSchema)
Routes/bookmark.js
router.post('/bookmarks', async (req, res) => {
const {lists} = req.body
console.log(lists)
if (!lists) {
return res.status(422).send({error: 'You must provide lists'})
}
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
try {
// const bookmark = Bookmark.add(lists, req.user._id, obj)
// await bookmark.save()
// res.send(bookmark)
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){ // THIS IS UNDEFINED. How to get this object?
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
} catch (e) {
res.status(422).send({error: e.message})
}
})
The req.body looks like this:
{
"lists": {
"listName": "My Saved List",
"items": {
"restaurantId": "abcdefg",
"cachedAttr": {
"name": "abcdefg",
"latitude": 200,
"longitude": 200
}
}
}
}
Basically what I commented out in the models/Bookmark.js file is what I would really like to do.
If the userId's list name already exists, then I would like to just add an item to the list.
Otherwise, I would like to add a new list to the object.
What is the best approach for doing this? Is there a straight forward mongoose api that I could use for this problem? or do I need to make two separated function that would handle each case and make that as schema methods and handle it in the routes file?

TypeError: expected string but received array postman

I was trying to send form data that has multiple fields with the same name, I'm getting back "TypeError: expected string but received array".
I think the problem is with postman, I want to have multiple participant fields, and those would be added to the should be added to the array.
final results of array
// this is from models/Battle
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create Schema
const BattleSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
date: {
type: Date,
default: Date.now
},
category: {
type: Number,
required: true // this will come from the selected category
},
winner: {
type: Number,
default: 0
},
status: {
type: Number,
default: 0 // 0 means the battle is closed, 1 means the battle is open for votes, the status will stay 0 until all participants dropped
},
participants: [
{
participant: {
type: Schema.Types.ObjectId,
required: true
}
}
]
});
module.exports = Battle = mongoose.model('battles', BattleSchema);
//this is from routes/api/battles
// #route POST api/battles
// #desc Create battle
// #access Private
router.post(
'/create-battle',
passport.authenticate('jwt', { session: false }),
(req, res) => {
const { errors, isValid } = validateBattleInput(req.body);
// Check Validation
if (!isValid) {
// If any errors, send 400 with errors object
return res.status(400).json(errors);
console.log(errors);
}
const newBattle = new Battle({
user: req.user.id,
category: req.body.category,
participant: req.body.participant
});
//save
newBattle.save().then(battle => {
// const participant = req.body.participant;
const participant = req.body.participant;
// add participants to array
battle.participants.push( participant );
console.log(typeof req.body.participant);
// get the inserted id
const battleId = battle._id;
res.json(battle);
});
}
);
// this is battle validation
const Validator = require('validator');
const isEmpty = require('./is-empty');
var bodyParser = require('body-parser');
module.exports = function validateBattleInput(data) {
let errors = {};
data.category = !isEmpty(data.category) ? data.category : '';
data.participant = !isEmpty(data.participant) ? data.participant : '';
if (Validator.isEmpty(data.category)) {
errors.category = 'Category field is required';
}
// if (Validator.isEmpty(data.challenger)) {
// errors.challenger = 'Challenger field is required';
// }
if (Validator.isEmpty(data.participant)) {
errors.participant = 'Participant field is required';
}
return {
errors,
isValid: isEmpty(errors)
};
};
TypeError: Expected string but received Array. ---throws an error in postman as well as in a terminal window. I suspect it could be the user schema definition mismatch
Please check your user model user schema eg
name: {
type: String,
required: true
}
it's receiving something else than expected.
try in your "body" tab, selecting "raw", and then to the right, select "JSON (application/json)" instead of "text".
I'm assuming your API endpoint uses JSON instead of a url-encoded form data, just because you are running an API using express and mongoose. but you should clarify that on the question if it isn't the case.
Write a proper JSON body, I mean, use double quotes for keys as in:
{"model": { "property": "value", "property2": 1}}
and try with the wrapping object {"model": <YOUR BODY HERE>} or without to see what works for you, as it's typical to wrap the object, but sometimes people don't use them. (seeing this in your code: req.body.participant makes me think you probably don't).
(PS: not related with the question, but personally prefer ARC or Insomnia for rest clients, as the interface for them is cleaner)
If you want data to be sent in participants array all the fields should be participants and not participant
try sending data through raw data and then selecting application/data for better formatting
When testing in postman - Just figured out Key value must match your validation function defined variables. It's better to be consistent across your development.

Resources