I have been trying to use updatemany with mongoose. I want to update the values in database using an array of objects.
[
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
I want to use variantId as filter.
Model schema is:
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
I want to filter the models using variantId and then decrease the stocks.
As you need to update multiple documents with multiple criteria then .updateMany() wouldn't work - it will work only if you need to update multiple documents with same value, Try this below query which will help you to get it done in one DB call :
const Mongoose = require("mongoose");
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
const Variant = mongoose.model('variant', variantSchema, 'variant');
let input = [
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
let bulkArr = [];
for (const i of input) {
bulkArr.push({
updateOne: {
"filter": { "_id": Mongoose.Types.ObjectId(i.variantId) },
"update": { $inc: { "stocks": - i.quantity } }
}
})
}
Variant.bulkWrite(bulkArr)
Ref : MongoDB-bulkWrite
I don't think this can be done with a single Model.updateMany query. You will need to loop the array and use Model.update instead.
for (const { variantId, quantity } of objects) {
Model.update({ _id: variantId }, { $inc: { stocks: -quantity } });
}
To run this in a transaction (https://mongoosejs.com/docs/transactions.html), the code should look something like this (however I have not tried or tested this):
mongoose.startSession().then(async session => {
session.startTransaction();
for (const { variantId, quantity } of objects) {
await Model.update({ _id: variantId }, { $inc: { stocks: -quantity } }, { session });
}
await session.commitTransaction();
});
Related
I'm working on a project and currently using Mongodb Time-Series and aggregation.
I connected my Apollo Graphql to retrieve the data but i'm stuck with an error that i can not solve no matter what.
Float cannot represent non numeric value:
On my db the numbers are saved as Double. If i try to run the query in MongoDB Aggregation with MongoDBCompass it's working perfectly but not on my Nodejs database.
I tried googling my problem first and looking ad docs but all the different solution i found do not solve my problem, any suggestions?
This is my Query:
module.exports = async (root, { limit }, { models }) => {
const keyx = await models.Weather.aggregate([
{
$group: {
_id: {
yearMonthDay: {
$dateToString: { format: "%Y-%m-%d", date: "$timestamp" },
},
},
temp: {
$push: { temp: "$temp" },
},
},
},
])
.exec();
return keyx;
};
This is my model:
const mongoose = require("mongoose");
const { Schema } = mongoose;
mongoose.pluralize(null);
const weather = new Schema({
timestamp: {
type: String,
trim: true,
},
temp: {
type: Number,
trim: true,
},
});
const Weather = mongoose.model("time_weather", weather);
module.exports = { Weather };
and this is my types:
const { gql } = require("apollo-server");
module.exports = gql`
type Weather {
timestamp: String
_id: ID
temp: Float
}
type Query {
weather(limit: Int): [Weather]
}
`;
and finally this is the response i'm getting from apollo studio:
{
"errors": [
{
"message": "Float cannot represent non numeric value: [{ temp: 11.7 }, { temp: 11.7 }, { temp: 12.01 }, { temp: 13.21 }, { temp: 13.21 }, { temp: 11.93 }, { temp: 13.21 }, { temp: 12.73 }, { temp: 14.21 }, { temp: 11.7 }, ... 14 more items]",
"locations": [
{
"line": 6,
"column": 5
}
],
"path": [
"weather",
0,
"temp"
],
}
},
MongoDb documents looks like this:
{
"_id": {
"$oid": "60abf5ffc4b1cb61e05bdc48"
},
"club": "FC Midtjylland",
"number": 6,
"__v": 0,
"name": "Joel Andersson"
}
If I create a filter in MongoDB Compass like:
{number: { $in: [6,11] } }
I get all documents with number 6 and 11
In mongose my schema is like:
const Player = new Schema ({
club: { type: String, required: true },
name: { type: String, required: true },
number: { type: Number, required: true },
image: { type: String, required: false },}, { collection: 'players' });
If I try to find the same documents in node.js with mongoose i get no results:
var test = "6,11"
Player.find({number: { $in: [test] }}, function (err, player) {
if (!player || player.length <= 0) {
console.log('No player found!')
ws.send('No player found!');
}
else {
//ws.send(player.number+','+player.name+','+player.image);
console.log(player)
}
})
If I input the numbers directly in the query everything is working fine like:
Player.find({number: { $in: [6,11] }}, funct
What can I do to solve this?
try this:
var test = [6,11]
Player.find({number: { $in: test }}, function (err, player) {
if (!player || player.length <= 0) {
console.log('No player found!')
ws.send('No player found!');
}
else {
//ws.send(player.number+','+player.name+','+player.image);
console.log(player)
}
})
I am trying to filter a collection with 3 optional 'and' conditions.
Here is my model:
const Company = mongoose.model(
'Company',
new Schema({
name: { type: String },
sectors: [{ type: Schema.Types.ObjectId, ref: 'Sector' }],
industries: [{ type: Schema.Types.ObjectId, ref: 'Industry' }],
countries: [{ type: Schema.Types.ObjectId, ref: 'Country' }],
})
And my component:
const getCompanies = (skip, limit, filter) =>
Company.find({
...filter.countries && { countries: filter.countries },
...filter.sectors && { sectors: filter.sectors },
...filter.industries && { industries: filter.industries },
})
.skip(skip)
.limit(limit)
.sort({ date: -1 })
.populate('countries')
.populate('sectors')
.populate('industries');
const getAll = async (req, res) => {
try {
const countries = req.query.country;
const sectors = req.query.sector;
const industries = req.query.industry;
const skip = parseInt(req.query.skip, 10);
const limit = parseInt(req.query.limit, 10);
const filter = {
...countries && { countries },
...sectors && { sectors },
...industries && { industries },
};
const result = await getCompanies(skip, limit, filter);
return res.status(200).json(result);
} catch (e) {
return res.status(500).send({ message: (e.message) });
}
};
This is working when the filter is empty, but when there is one or more items in the filter, I get an empty array.
If I hard code data in getCompanies like so:
Company.find({
countries: '5d5e913e20c01070fef5c77e',
sectors: '5d5e913e20c01070fef5c754',
industries: '5d5e913e20c01070fef5c7ad',
})
or :
Company.find({
countries: '5d5e913e20c01070fef5c77e'
})
I get the data I want.
I also tried to console.log the filter in getCompanies to make sure the data was correct, and I get this if all fields are requested:
{
countries: '5d5e913e20c01070fef5c77e',
sectors: '5d5e913e20c01070fef5c754',
industries: '5d5e913e20c01070fef5c7ad',
}
and this for just one:
{ countries: '5d5e913e20c01070fef5c77e' }
So it seems fine to me.
I also tried using '$and' like so:
Company.find({ $and: [
{ ...filter.countries && { countries: filter.countries } },
{ ...filter.sectors && { sectors: filter.sectors } },
{ ...filter.industries && {industries: filter.industries } },
],
})
or using '$in':
Company.find({
...filter.countries && { countries: { $in: filter.countries } },
...filter.sectors && { sectors: { $in: filter.sectors } },
...filter.industries && { industries: { $in: filter.industries } },
})
But no luck either.
Here is a sample URL:
GET /api/internal/member/get?skip=12&limit=6&country=5d5e913e20c01070fef5c77e§or=&industry=
I have found some other threads with questions somewhat similar to mine, but they were to different to help me solve my case.
Looking forward to your helpful advice.
I finally got it to work. Turns out I was not clearing the data in the frontend at each change, which caused an issue with the skip/limit fields.
I also changed the find as advised by #whoami like so:
Company.find({ $and: [
{
...filter.countries && { countries: { $in: [mongoose.Types.ObjectId(filter.countries)] } },
...filter.sectors && { sectors: { $in: [mongoose.Types.ObjectId(filter.sectors)] } },
...filter.sdgs && { sdgs: { $in: [mongoose.Types.ObjectId(filter.sdgs)] } },
}
],
})
Context:
I am trying to upsert in bulk an array of data, with an additional computed field: 'status'.
Status should be either :
- 'New' for newly inserted docs;
- 'Removed' for docs present in DB, but inexistent in incoming dataset;
- a percentage explaining the evolution for the field price, comparing the value in DB to the one in incoming dataset.
Implementations:
data.model.ts
import { Document, model, Model, models, Schema } from 'mongoose';
import { IPertinentData } from './site.model';
const dataSchema: Schema = new Schema({
sourceId: { type: String, required: true },
name: { type: String, required: true },
price: { type: Number, required: true },
reference: { type: String, required: true },
lastModified: { type: Date, required: true },
status: { type: Schema.Types.Mixed, required: true }
});
export interface IData extends IPertinentData, Document {}
export const Data: Model<IData> = models.Data || model<IData>('Data', dataSchema);
data.service.ts
import { Data, IPertinentData } from '../models';
export class DataService {
static async test() {
// await Data.deleteMany({});
const data = [
{
sourceId: 'Y',
reference: `y0`,
name: 'y0',
price: 30
},
{
sourceId: 'Y',
reference: 'y1',
name: 'y1',
price: 30
}
];
return Data.bulkWrite(
data.map(function(d) {
let status = '';
// #ts-ignore
console.log('price', this);
// #ts-ignore
if (!this.price) status = 'New';
// #ts-ignore
else if (this.price !== d.price) {
// #ts-ignore
status = (d.price - this.price) / this.price;
}
return {
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: {
$set: {
// Set percentage value when current price is greater/lower than new price
// Set status to nothing when new and current prices match
status,
name: d.name,
price: d.price
},
$currentDate: {
lastModified: true
}
},
upsert: true
}
};
}
)
);
}
}
... then in my backend controller, i just call it with some route :
try {
const results = await DataService.test();
return new HttpResponseOK(results);
} catch (error) {
return new HttpResponseInternalServerError(error);
}
Problem:
I've tried lot of implementation syntaxes, but all failed either because of type casting, and unsupported syntax like the $ symbol, and restrictions due to the aggregation...
I feel like the above solution might be closest to a working scenario but i'm missing a way to grab the value of the price field BEFORE the actual computation of status and the replacement with updated value.
Here the value of this is undefined while it is supposed to point to current document.
Questions:
Am i using correct Mongoose way for a bulk update ?
if yes, how to get the field value ?
Environment:
NodeJS 13.x
Mongoose 5.8.1
MongoDB 4.2.1
EUREKA !
Finally found a working syntax, pfeeeew...
...
return Data.bulkWrite(
data.map(d => ({
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: [
{
$set: {
lastModified: Date.now(),
name: d.name,
status: {
$switch: {
branches: [
// Set status to 'New' for newly inserted docs
{
case: { $eq: [{ $type: '$price' }, 'missing'] },
then: 'New'
},
// Set percentage value when current price is greater/lower than new price
{
case: { $ne: ['$price', d.price] },
then: {
$divide: [{ $subtract: [d.price, '$price'] }, '$price']
}
}
],
// Set status to nothing when new and current prices match
default: ''
}
}
}
},
{
$set: { price: d.price }
}
],
upsert: true
}
}))
);
...
Explanations:
Several problems were blocking me :
the '$field_value_to_check' instead of this.field with undefined 'this' ...
the syntax with $ symbol seems to work only within an aggregation update, using update: [] even if there is only one single $set inside ...
the first condition used for the inserted docs in the upsert process needs to check for the existence of the field price. Only the syntax with BSON $type worked...
Hope it helps other devs in same scenario.
I am creating a list of scores for a user in mongoDB by adding a new score 1 at a time and sorting the list. I want to remove the lowest score when the list grows larger than 5 elements.
The reason for this is because I want to store the top 5 scores of the user.
What would be the best way to do this? Is there a way to make the whole thing an atomic operation?
My code is below. I'm using NodeJS with Mongoose and MongoDB.
const maxScoresToStore = 5
var scoreEntrySchema = new Schema({
score: Number,
when: { type: Date, default: Date.now }
})
var scoreSchema = new Schema({
_userid: { type: Schema.Types.ObjectId, ref: 'Users' },
username: {type: String, index:{unique: true}},
scores: [scoreEntrySchema]
})
const scoreModel = mongoose.model("Scores", scoreSchema)
exports.addUserScore = (uid, uname, score) => {
var query = {_userid:uid, username:uname},
update = { $push : {"scores" : {$each: [{"score": score}], $sort: {"score":-1}}} }, // sorts in descending order after pushing
options = { upsert: true, new: true, setDefaultsOnInsert: true };
scoreModel.findOneAndUpdate(query, update, options).then(
(result)=>{
if(result.scores.length > maxScoresToStore)
{
// ToDo:
// result.update({$pop: {"scores" : 1 }}) // pops the last element of the list
}
}
)
}
You can use $slice operator, And your query looks like:
let score = await scoreModel.findOneAndUpdate({ _userid: uid, username: uname },
{
$push: {
scores: {
$each: [{ score: score }],
$sort: { score: -1 },
$slice: maxScoresToStore
}
}
},
{
upsert: true,
new: true,
setDefaultsOnInsert: true,
multi: true
});
[DO VOTE TO THIS ANSWER, IF ITS HELPFUL TO YOU]
You can add slice option to your update option:
update = {
$push: {
scores: { $each: [{ score: score }], $sort: { score: -1 }, $slice: maxScoresToStore }
}
}
Here is the full method code written in async/await style:
exports.addUserScore = async (uid, uname, score) => {
const query = { _userid: uid, username: uname };
const update = {
$push: {
scores: {
$each: [{ score: score }],
$sort: { score: -1 },
$slice: maxScoresToStore
}
}
};
const options = {
upsert: true,
new: true,
setDefaultsOnInsert: true,
multi: true
};
try {
let score = await scoreModel.findOneAndUpdate(query, update, options);
if (!score) res.send(404).send("Score not found");
res.send("Everything is ok");
} catch (err) {
console.log(err);
res.status(500).send("Something went wrong");
}
};
I'm not certain If this would help, but it might work
scoreModel.update(
{ "scores.5": { "$exists": 1 } },
{ "$pop": { "scores": 1 } },
{ "multi": true }
)
As you are already sorting by descending, you can check if the array length is greater than 5 by using scores.5, If this returns true then you can pop the last element using $pop.
If $exists return false then it will skip the query. you can run this update after .then() and you won't have to use if condition.
But keep in mind $pop will only remove 1 element.