Mongoose and express insert many many-to-many relationships in one request - node.js

I want to make a request in which I add several meals to order and a request that removes several meals from order. It is many-to-many relationship because order can have several meals and meal can have several orders.
This is my order model:
const mongoose = require("mongoose");
const orderObject = {
cook: { type: mongoose.Types.ObjectId, ref: "CookRoleUser" },
remark: { type: String },
delivery_address: { type: String, required: true },
orderer: {
type: mongoose.Types.ObjectId,
ref: "RegularRoleUser",
required: true,
},
meals: [{ type: mongoose.Types.ObjectId, ref: "Meal" }],
active: { type: Boolean, default: true },
date_ordered: { type: Date, default: () => Date.now() },
};
const Order = mongoose.model("Order", new mongoose.Schema(orderObject));
module.exports = { Order };
And this is my meal model:
const mongoose = require("mongoose");
const mealCategories = {
MAIN: "MAIN",
DESSERT: "DESSERT",
APPETIZER: "APPETIZER",
DRINK: "DRINK",
};
const mealTypes = {
REGULAR: "REGULAR",
SPECIAL: "SPECIAL",
};
const mealObject = {
name: { type: String },
ingredients: [
{ name: { type: String }, allergen: { type: Boolean, default: false } },
],
is_offered: { type: Boolean, default: false },
orders: [{ type: mongoose.Types.ObjectId, ref: "Order" }],
cook: { type: mongoose.Types.ObjectId, ref: "CookRoleUser" },
category: {
type: String,
enum: Object.values(mealCategories),
default: mealCategories.MAIN,
},
type: {
type: String,
enum: Object.values(mealTypes),
default: mealTypes.REGULAR,
},
date_created: { type: Date, default: () => Date.now() },
};
const mealSchema = new mongoose.Schema(mealObject);
const Meal = mongoose.model("Meal", mealSchema);
module.exports = { Meal, mealTypes };
I've tried to do this:
For addition:
const addMealsToOrder = async (req, res, next) => {
const order = await Order.findById(req.body.orderId);
let mealCount = 0;
await req.body.mealIds?.forEach(async (mealId, index) => {
const meal = await Meal.findById(mealId);
if (order.meals.includes(mealId)) {
return next(
new HttpError(
`Meal ${meal.name} already in order ${order.remark}.`,
400
)
);
}
if (order.cook.toString() != meal.cook.toString()) {
return next(
new HttpError(
`Meal ${meal.name} cook not same as order ${order.remark} cook.`
)
);
}
meal.orders.push(order.id);
order.meals.push(meal.id);
await meal.save();
mealCount = index;
});
if (mealCount == req.meals.length - 1) {
order.save();
res.json(order);
}
};
or without checking is are meals already in order and are cooks the same:
const addMealToOrderTest = async (req, res, next) => {
await req.body.mealIds.forEach(async (mealId) => {
const order = await Order.findById(req.body.orderId);
const meal = await Meal.findById(mealId);
order.meals.push(mealId);
meal.orders.push(order.id);
await order.save();
await meal.save();
});
res.json(2);
};
And for deletion:
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
await req.body.mealIds.forEach(async (mealId, index) => {
const meal = Meal.findById(mealId);
meal.orders = meal.orders.filter((oId) => oId != order.id);
order.meals = order.meals.filter((mId) => mId != mealId);
await meal.save()
});
await order.save()
res.json(order);
};
I've tested adding and deleting in postman. Behaviour is very weird and random. Some times it does not add meals, or adds only few, etc. I test the result by fetching single order and/or single meal.
My question:
What is the best way to implement many-to-many update where you want to add for a single entity (order) multiple entities (meals)?
Furthermore I would also want to check, in addtion request, does the certain meal already exist in that order and are the order cook and certain meal cook not the same. If some of these condition is true, I want to inform the request issuer about it and stop all adding and in deletion request does the order contain certain meal and also inform the request issuer if that is true.
EDIT 04/08/22
Now i've tried to make my controllers as such
Addition
const addMealToOrderTest = async (req, res, next) => {
const { orderId, mealIds } = req.body;
await mealIds.forEach(async (mealId) => {
const order = await Order.findById(orderId);
const meal = await Meal.findById(mealId);
order.meals.push(mealId);
meal.orders.push(order.id);
await order.save();
await meal.save();
});
res.json(2);
};
Removal
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
await mealIds.forEach(async (mealId) => {
const order = await Order.findById(orderId);
const meal = await Meal.findById(mealId);
order.meals.pull(mealId);
meal.orders.pull(orderId);
await order.save();
await meal.save();
});
res.json(2);
};
Behaviour is very random. Sometimes it works all fine but sometimes I get:
VersionError if order.save() is in mealIds.forEach function or
ParrallelSaveError if the fetching and saving of the order are outside mealIds.forEach function.
EDIT no:2 04/08/22
Addition
const addMealToOrderTest = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
let addedMealCount = 0;
mealIds.forEach((mealId) => {
if (!order.meals.includes(mealId)) {
order.meals.push(mealId);
addedMealCount += 1;
}
});
if (addedMealCount != mealIds.length) {
return res.json({ error: "Meal already in that order" });
}
await mealIds.forEach(async (mealId) => {
const meal = await Meal.findById(mealId);
if (!meal.orders.includes(mealId)) {
meal.orders.push(order.id);
await meal.save();
}
});
await order.save();
res.json(order);
};
Removal
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
mealIds.forEach((mealId) => {
order.meals.pull(mealId);
});
await mealIds.forEach(async (mealId) => {
const meal = await Meal.findById(mealId);
meal.orders.pull(orderId);
await meal.save();
});
await order.save();
res.json(order);
};
Addition and removal both seem to work now. But I am wondering how could I check is meal cook and order cook the same when I add meals to order.
Problem with this is that I would have to fetch each meal, and then check are cooks the same as in the order.
When I do so, usually I get Cannot set headers or such. I've tried to initialize counter before forEach loop in which I fetch an meal for mealId and increment it only if cooks are the same. And if they are not, counter is not incremented and response is sent. I sent an ok response with order only if the counter is the same as mealIds length. But, counter does not seem to change if I fetch meal by id.

Related

MONGODB store value in variable to use later

i have a code that inserts information from a 3rd party API every 24hours the api information is ID, Name, Status, Position, Score the only information that can get updated if there is any change are Name, Status, Position, Score, ID will always be the same.
I'm trying to show information based on if there is any change when new data is inserted to the database for example
08/14/2022 ------------- 08/15/2022
Name: yandr1 ========== NO CHANGES
position 1 ========== NO CHANGES
id 123 ========== NO CHANGES
score 100 ========== CHANGES TO: 200 ( POINTS GAINED 100 )
based on the new data inserted to MongoDB Database Score changed from 100 to 200, how can i show on the website the OLD SCORE AND POINTS GAINED? Position, Name,old score, POINTS GAINED
CODE TO INSERT DATA INTO DB FROM API
//POST DAILY PLAYERS DATA//
cron.schedule('56 23 * * *', async () => {
const postSchema = new mongoose.Schema({
id: {
type: Number,
required: true
},
name: {
type: String,
required: true
},
status: {
type: String,
required: false
},
});
const Post = mongoose.model('players', postSchema);
async function getPosts() {
const getPlayers = await fetch("http://localhost:3008/api/players");
const response = await getPlayers.json();
for (let i = 0; i < response.players.length; i++) {
const post = new Post({
id: response.players[i]['id'],
name: response.players[i]['name'],
status: response.players[i]['status'],
});
post.save();
}
}
console.log("Table submitted successfully")
await getPosts();
});
//POST DAILY HIGHSCORE DATA//
cron.schedule('55 23 * * *', async () => {
const postSchema = new mongoose.Schema({
position: {
type: Number,
required: false
},
id: {
type: Number,
required: true
},
score: {
type: Number,
required: false
},
});
const Post = mongoose.model('highscores', postSchema);
async function getPosts() {
const getHighscore = await fetch("http://localhost:3008/api/highscore/players");
const response = await getHighscore.json();
for (let i = 0; i < response.players.length; i++) {
const post = new Post({
position: response.players[i]['position'],
id: response.players[i]['id'],
score: response.players[i]['score'],
});
post.save();
}
}
console.log("Table submitted successfully")
await getPosts();
});
router.get('/export', async function(req, res, next) {
let data
try {
data = await Promise.all([
Posts.list(),
Events.list(),
Messages.list(),
Users.list()
]);
// at this point, data is an array. data[0] = Posts.list result, data[1] = Events.list result etc..
res.status(200).json(data)
} catch (e) {
res.status(500).send('error');
}
});
Hey Yandry try this code-
async function getPosts() {
const getPlayers = await fetch("http://localhost:3008/api/players");
const response = await getPlayers.json();
for( let i = 0;i < response.players.length; i++){
id= response.players[i]['id'];
name= response.players[i]['name'];
status= response.players[i]['status'];
const updateScore = await findOneAndUpdate({id:id},{$set:{name:name,status:status}})
console.log("Updated Score",updateScore)
}
}
Please, try this code, hope you will get solution, if you still facing issue just lemme know, i will help you more.
Thank

Mongo DB SORTING data nodejs

i created a code that Backsup/Deletes and Inserts information from the Third Party API every 24hours to update the Third party api changes daily on the database.
how can i sort the information i get from the MongoDB by which score changed the most?
API LOOKS LIKE
{
"_id": "6365e1dbde0dd3639536f4b7",
"position": 1,
"id": "105162",
"score": 2243536903,
"__v": 0
},
MY CODE
app.get('/api/TopFlops', async (req, res) => {
const topflops = await TopFlops.find({}).sort({_id: +1}).limit(5)
res.json(topflops);
})
CODE TO INSERT DATA FROM THE 3RD PARTY API TO DB
cron.schedule('59 23 * * *', async () => {
const postSchema = new mongoose.Schema({
id: {
type: Number,
required: true
},
name: {
type: String,
required: true
},
status: {
type: String,
required: false
},
});
const Post = mongoose.model('players', postSchema);
async function getPosts() {
const getPlayers = await fetch("http://localhost:3008/api/players");
const response = await getPlayers.json();
for( let i = 0;i < response.players.length; i++){
const post = new Post({
id: response.players[i]['id'],
name: response.players[i]['name'],
status: response.players[i]['status'],
});
post.save();
}
}
console.log("Table submitted successfully")
await getPosts();
});
CODE TO FETCH API
const [playerName, setPlayerName] = useState([]);
const [playerRank, setPlayerRank] = useState([]);
const [player, setPlayer] = useState([]);
const [perPage, setPerPage] = useState(10);
const [size, setSize] = useState(perPage);
const [current, setCurrent] = useState(1);
const [players, setPlayers] = useState();
const fetchData = () => {
const playerAPI = 'http://localhost:3001/api/topflops';
const playerRank = 'http://localhost:3001/api/topflops';
const getINFOPlayer = axios.get(playerAPI)
const getPlayerRank = axios.get(playerRank)
axios.all([getINFOPlayer, getPlayerRank]).then(
axios.spread((...allData) => {
const allDataPlayer = allData[0].data
const getINFOPlayerRank = allData[1].data
const newPlayer = allDataPlayer.map(name => {
const pr = getINFOPlayerRank.find(rank => name.id === rank.id)
return {
id: name.id,
name: name.name,
alliance: name.alliance,
position: pr?.position,
score: pr?.score
}
})
setPlayerName(allDataPlayer)
setPlayerRank(getINFOPlayerRank)
console.log(getINFOPlayerRank)
console.log(newPlayer)
setPlayer(newPlayer)
})
)
}
useEffect(() => {
fetchData()
}, [])
const getData = (current, pageSize) => {
// Normally you should get the data from the server
return player?.slice((current - 1) * pageSize, current * pageSize);
};

Creating stub for sequeilze models with association

I am using mocha and chai for writing test for RESTful APIs
I have read some articles where people suggests to create stubs for queries, and you shouldn't be actually making a database query.
But How would I make sure if it works?
See below controller.
const Op = require('sequelize').Op
//Models
const {
Item,
Location,
Combo,
Service,
ComboItem,
ItemLocation
} = require('../models')
const _ = require('lodash')
//Services
const paginate = require('../services/PaginationService')
const getAllItems = async function(req, res) {
if(req.query.location_id){
let items
const item = await Location.findOne({
where: {
id: 1
},
include: {
model: Item,
through: {
model: ItemLocation,
attributes: []
},
as: 'itemsAtLocation',
include: [
{
model: Service,
as: 'service',
attributes: ["id"]
},
{
model: Combo,
as: 'combo',
attributes: ["start_date", "expiry_date"]
}
]
}
})
if(!item)
return res.status(200).send({
status: true,
message: "No item found at location!",
data: {}
})
items = item.itemsAtLocation
let data = {}
data.services = []
data.combos = []
_.forEach(items, item => {
let itemData = {
id: item.id,
name: item.name,
price: item.price,
discount_per: item.discount_per,
}
if(item.service)
data.services.push(itemData)
if(item.combo) {
itemData.start_date = item.combo.start_date
itemData.expiry_date = item.combo.expiry_date
data.combos.push(itemData)
}
})
return res.status(200).send({
status: true,
message: "Successfully fetch all items!",
data: data
})
} else {
const items = await Item.findAll({
include: [
{
model: Service,
as: 'service',
attributes: ["id"]
},
{
model: Combo,
as: 'combo',
attributes: ["start_date", "expiry_date"]
}
],
attributes: ["id", "name", "price", "discount_per", "description"],
...paginate(+req.query.page, +req.query.per_page)
})
let data = {}
data.services = []
data.combos = []
_.forEach(items, item => {
let itemData = {
id: item.id,
name: item.name,
price: item.price,
discount_per: item.discount_per,
}
if(item.service)
data.services.push(itemData)
if(item.combo) {
itemData.start_date = item.combo.start_date
itemData.expiry_date = item.combo.expiry_date
data.combos.push(itemData)
}
})
return res.status(200).send({
status: true,
message: "Successfully fetch all items!",
data: data
})
}
}
module.exports = {
getAllItems
}
As you can see from above code. I need queries to return data in a specific form. If it won't be in that form things won't work.
Can someone suggest how can I create stubs for such kind of functions so that structure also be preserved?
Below is the test that I have wrote, But it uses actual db calls.
describe('GET /api/v1/items', function () {
it('should fetch all items orgianized by their type', async () => {
const result = await request(app)
.get('/api/v1/items')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
expect(result)
.to.be.a('Object')
expect(result.body.status)
.to.be.a('Boolean').true
expect(result.body.data, "data should be an Object and every key should an Array")
.to.satisfy(data => {
expect(data).to.be.a('Object')
.to.not.be.null
if(!_.isEmpty(data)) {
expect(data).to.have.any.keys('services', 'combos')
_.forOwn(data, (value, key) => {
expect(data[key]).to.be.a('Array')
})
return true
}
return true
})
})
})
One way you can do that is by stubbing the methods from your models, i.e. Location.findOne and Item.findAll. So your tests could look a bit like the code below:
const sinon = require('sinon');
const Location = require('../models/location'); // Get your location model
const Item = require('../models/item'); // Get your item model
describe('myTest', () => {
let findOneLocationStub;
let findAllItemsStub;
beforeEach(() => {
findOneLocationStub = sinon.stub(Location, 'findOne');
findAllItemsStub = sinon.stub(Item, 'findAll');
});
afterEach(() => {
findOneLocationStub.verifyAndRestore();
findAllItemsStub.verifyAndRestore();
});
it('returns 200 when location not found', () => {
findOneLocationStub.resolves(null);
expects...
});
});
I did not run the test, but something like that should work. But note that I had to split the models into their own file to do the stub. Probably there's a way to do the same using your current implementation.
Another thing I would suggest is having some kind of use case into your method that is responsible for database implementation. Something like:
const getAllItemsUseCase = (params, queryService) => {
if(params.locationId){
let items
const item = await queryService.findOneLocation({
};
So when you call this method from your controller, you can do call:
const getAllItems = async function(req, res) {
const params = {
locationId: req.query.location_id,
// and more parameters
};
const queryService = {
findOneLocation: Location.findOne,
};
const results = await getAllItemsUseCase(params, queryService);
}
This way you will detach your business logic from the controller and you will have a much easier time to mock your query: you just change the methods provided to queryService.
You can find some interesting read from this blog post: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

How to get the array of object instead of just _id in Mongoose

I am very new to mongoose.
I am currently building a backend using Node.js, express.js, GraphQL, and mongoose.
I have a Drink schema and DrinkType Schema. I defined DrinkType schema as "alcohol", "juice", "tea". And I have added many drinks and each drink has DrinkType reference. Then, I would like to reference all the drinks from DrinkType.
This is the schema for drinkType
const drinkTypeSchema = new Schema({
name: {
type: String,
required: true,
},
createdDrinks: [
{
type: Schema.Types.ObjectId,
ref: 'Drink',
},
],
Here is the schema for drink
const drinkSchema = new Schema({
name: {
type: String,
required: true,
},
drinkType: {
type: Schema.Types.ObjectId,
ref: 'DrinkType',
},
})
This is the drink resolver. Whenever a new drink is created, I am pushing it to drinkType.
try {
const result = await drink.save()
createdDrink = transformDrink(result)
const drinkType = await DrinkType.findById(args.addDrinkInput.drinkTypeId)
if (!drinkType) {
throw new Error('DrinkType not found.')
}
drinkType.createdDrinks.push(drink)
await drinkType.save()
const drinkLoader = new DataLoader(drinkIds => {
return drinks(drinkIds)
})
const drinks = async drinkIds => {
try {
const drinks = await Drink.find({ _id: { $in: drinkIds } })
return drinks.map(drink => {
return transformDrink(drink)
})
} catch (err) {
throw err
}
}
const transformDrink = drink => {
return {
...drink._doc,
_id: drink.id,
drinkType: drinkType.bind(this, drink.drinkType),
}
}
const drinkType = async drinkTypeId => {
try {
const drinkType = await drinkTypeLoader.load(drinkTypeId.toString())
return {
...drinkType._doc,
_id: drinkType.id,
createdDrinks: () => drinkLoader.loadMany(drinkType._doc.createdDrinks),
}
I want this createdDrinks part to return the array of drink objects, but it is only returning the array of _ids.
I have been reading the mongoose documentation and it seems that using ObjectId is the correct way. Would you mind helping me out?
Thank you in advance.

Refactoring several mongoose models to similar collections

I have several collections that have the same documents type, except for the language.
Let's say imagesES, imagesEN, imagesFR, and so on....
I just thought about definig just one schema, but also one model that get the proper collection with a parameter:
var mongoose = require('mongoose')
var Schema = mongoose.Schema
let authorSchema = require('./Authors').authorSchema
const imageSchema = new Schema({
authors: [authorSchema],
status: Number, // published (1), unpublished (0)
created: { type: Date, default: Date.now },
lastUpdated: { type: Date, default: Date.now },
license: {
type: String,
enum: ['Creative Commons BY-NC-SA'], //just one license right now
default: 'Creative Commons BY-NC-SA'
},
downloads: {
type: Number,
default: 0
},
tags: [String]
})
module.exports = locale => {
return mongoose.model('Image', imageSchema, `image${locale}`)
}
However in the controller I should require the model inside the controller (when I know the locale):
getImageById: (req, res) => {
const id = req.swagger.params.id.value
const locale = req.swagger.params.locale.value
const Images = require('../models/Images')(locale)
Images.findOne({_id: id}).lean().exec( (err, image) => {
I'm not sure if this is the proper way as each request I get I have to require the model module (syncronously) or should I require all the different models previous to the use in the function.
const locales = ['es', 'en', 'fr']
const Images = []
locales.map(locale=>Images[locale] = require('../models/Images')(locale))
getImageById: (req, res) => {
const id = req.swagger.params.id.value
const locale = req.swagger.params.locale.value
Images[locale].findOne({_id: id}).lean().exec( (err, image) => {
Finally this is how I resolved it. Where it says Pictograms, could be Images as in the question
const setPictogramModel = require('../models/Pictograms')
const languages = [
'ar',
'bg',
'en',
'es',
'pl',
'ro',
'ru',
'zh'
]
const Pictograms = languages.reduce((dict, language)=> {
dict[language]= setPictogramModel(language)
return dict
}, {})
module.exports = {
getPictogramById: (req, res) => {
const id = req.swagger.params.idPictogram.value
const locale = req.swagger.params.locale.value
// Use lean to get a plain JS object to modify it, instead of a full model instance
Pictograms[locale].findOne({id_image: id}).exec( (err, pictogram) => {
if(err) {
return res.status(500).json({
message: 'Error getting pictograms. See error field for detail',
error: err
})
}
if(!pictogram) {
return res.status(404).json( {
message: `Error getting pictogram with id ${id}`,
err
})
}
return res.json(pictogram)
})
},

Resources