Can I access the positional $ operator in projection of findOneAndUpdate - mongodb

I have this query that works, but I want for the doc to only display network.stations.$ instead of the entire array. If I write fields: network.stations.$, I get an error. Is there a way for the doc only to return a single element from [stations]?
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"network.stations.$.free_bikes": req.body.free_bikes
}, {
new: true,
fields: "network.stations"
}, (err, doc) => console.log(doc))
// I want doc to somehow point only to a single station instead of
// several stations like it currently does.

The answer is "yes", but not in the way you are expecting. As you note in the question, putting network.stations.$ in the "fields" option to positionally return the "modified" document throws a specific error:
"cannot use a positional projection and return the new document"
This however should be the "hint", because you don't really "need" the "new document" when you know what the value was you are modifying. The simple case then is to not return the "new" document, but instead return it's "found state" which was "before the atomic modification" and simply make the same modification to the returned data as you asked to apply in the statement.
As a small contained demo:
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
const testSchema = new Schema({},{ strict: false });
const Test = mongoose.model('Test', testSchema, 'collection');
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Test.remove();
await Test.insertMany([{ a: [{ b: 1 }, { b: 2 }] }]);
for ( let i of [1,2] ) {
let result = await Test.findOneAndUpdate(
{ "a.b": { "$gte": 2 } },
{ "$inc": { "a.$.b": 1 } },
{ "fields": { "a.$": 1 } }
).lean();
console.log('returned');
log(result);
result.a[0].b = result.a[0].b + 1;
console.log('modified');
log(result);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})();
Which produces:
Mongoose: collection.remove({}, {})
Mongoose: collection.insertMany([ { __v: 0, a: [ { b: 1 }, { b: 2 } ], _id: 59af214b6fb3533d274928c9 } ])
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 2
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 4
}
]
}
So I'm doing the modifications in a loop so you can see that the update is actually applied on the server as the next iteration increments the already incremented value.
Merely by omitting the "new" option, what you get is the document in the state which it was "matched" and it then is perfectly valid to return that document state before modification. The modification still happens.
All you need to do here is in turn make the same modification in code. Adding .lean() makes this simple, and again it's perfectly valid since you "know what you asked the server to do".
This is better than a separate query because "separately" the document can be modified by a different update in between your modification and the query to return just a projected matched field.
And it's better than returning "all" the elements and filtering later, because the potential could be a "very large array" when all you really want is the "matched element". Which of course this actually does.

Try changing fields to projection and then use the network.stations.$ like you tried before.
If your query is otherwise working then that might be enough. If it's still not working you can try changing the second argument to explicitly $set.
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"$set": {
"network.stations.$.free_bikes": req.body.free_bikes
}
}, {
new: true,
projection: "network.stations.$"
}, (err, doc) => console.log(doc))

Related

Mongoose update only fields available in request body

I am trying to update one document using findOneAndUpdate and $set but I clearly missing something very crucial here because the new request is overwriting old values.
My Device schema looks like this:
{
deviceId: {
type: String,
immutable: true,
required: true,
},
version: {
type: String,
required: true,
},
deviceStatus: {
sensors: [
{
sensorId: {
type: String,
enum: ['value1', 'value2', 'value3'],
},
status: { type: Number, min: -1, max: 2 },
},
],
},
}
And I am trying to update the document using this piece of code:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ deviceId },
{ $set: req.body },
{},
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);
And when I try to send a request from the postman with the body that contains one or multiple sensors, only the last request is saved in the database.
{
"deviceStatus": {
"sensors": [
{
"sensorId": "test",
"status": 1
}
]
}
}
I would like to be able to update values that are already in the database based on req.body or add new ones if needed. Any help will be appreciated.
The documentation said:
The $set operator replaces the value of a field with the specified
value.
You need the $push operator, it appends a specified value to an array.
Having this documents:
[
{
_id: 1,
"array": [
2,
4,
6
]
},
{
_id: 2,
"array": [
1,
3,
5
]
}
]
Using $set operator:
db.collection.update({
_id: 1
},
{
$set: {
array: 10
}
})
Result:
{
"_id": 1,
"array": 10
}
Using $push operator:
db.collection.update({
_id: 1
},
{
$push: {
array: 10
}
})
Result:
{
"_id": 1,
"array": [
2,
4,
6,
10
]
}
you want to using $push and $set in one findOneAndUpdate, that's impossible, I prefer use findById() and process and save() ,so just try
let result = await Device.findById(deviceId )
//implementation business logic on result
await result.save()
If you want to push new sensors every time you make request then update your code as shown below:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ deviceId },
{
$push: {
"deviceStatus.sensors": { $each: req.body.sensors }
}
},
{},
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);
Update to the old answer:
If you want to update sensors every time you make request then update your code as shown below:
const deviceId = req.params.deviceId;
Device.findOneAndUpdate(
{ "deviceId": deviceId },
{ "deviceStatus": req.body.sensors },
{ upsert: true },
(err, docs) => {
if (err) {
res.send(err);
} else {
res.send({ success: true });
}
}
);

mongoose find and update removes the other fields

I have schema like this:
this.schema = new Schema({
userEmail: String
environments: [
{
envId: String,
appPreference: String,
language: String,
timeZone: String,
summaryNotificationSchedule: {
timeOfTheDay: String
}
}
]
});
Update request:
{
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
}
As you can see, I am not sending "language": "abc", in the update request and in the result I see the language field is removed. I want to update the fields but not remove the other fields
Mongoose find and update call:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: { 'environments.$': setPreferenceFields } }, { new: true });
You can create update object from your request first:
let request = {
"envId": "u2",
"appPreference": "put2",
"timeZone": "gmt",
"summaryNotificationSchedule.timeOfTheDay": "32400",
};
let update = Object.keys(request).reduce((acc, cur) => {
acc[`environments.$.${cur}`] = request[cur];
return acc;
}, {})
console.log(update);
Then pass it to the update:
await this.model.findOneAndUpdate({ userEmail, 'environments.envId': envId }, { $set: update }, { new: true });
You have to specify property with parent key name of an array, it should be like this way,
await this.model.findOneAndUpdate(
{
userEmail,
'environments.envId': envId
},
{
$set: {
'environments.$.envId': "u2",
'environments.$.appPreference': "put2",
'environments.$.timeZone': "gmt",
'environments.$.summaryNotificationSchedule.timeOfTheDay': "32400"
}
},
{ new: true }
)
Another option, update with aggregation pipeline start from MongoDB v4.2, this little lengthy process then above method,
$map to iterate loop of environments array
$cond check condition if envId is equal to matching envId then merge objects update objects and current objects using $mergeObjects otherwise return current object
await this.model.findOneAndUpdate(
{ userEmail },
[
{
$set: {
environments: {
$map: {
input: "$environments",
in: {
$cond: [
{$eq: ["$$this.envId", envId]}, // add update id
{
$mergeObjects: [
"$$this",
setPreferenceFields // your update fields
]
},
"$$this"
]
}
}
}
}
}
],
{new: true}
)

Mongoose: consistently save document order

Suppose we have a schema like this:
const PageSchema = new mongoose.Schema({
content: String
order: Number
})
We want order to be always a unique number between 0 and n-1, where n is the total number of documents.
How can we ensure this when documents are inserted or deleted?
For inserts I currently use this hook:
PageSchema.pre('save', async function () {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 })
this.order = lastPage ? lastPage.order + 1 : 0
}
})
This seems to work when new documents are inserted.
When documents are removed, I would have to decrease the order of documents of higher order. However, I am not sure which hooks are called when documents are removed.
Efficiency is not an issue for me: there are not many inserts and deletes.
It would be totally ok if I could somehow just provide one function, say fix_order, that iterates over the whole collection. How can I install this function such that it gets called whenever documents are inserted or deleted?
You can use findOneAndDelete pre and post hooks to accomplish this.
As you see in the pre findOneAndDelete hook, we save a reference to the deleted document, and pass it to the postfindOneAndDelete, so that we can access the model using constructor, and use the updateMany method to be able to adjust orders.
PageSchema.pre("findOneAndDelete", async function(next) {
this.page = await this.findOne();
next();
});
PageSchema.post("findOneAndDelete", async function(doc, next) {
console.log(doc);
const result = await this.page.constructor.updateMany(
{ order: { $gt: doc.order } },
{
$inc: {
order: -1
}
}
);
console.log(result);
next();
});
Let's say you have these 3 documents:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad282"),
"content": "content2",
"order": 1,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 2,
"__v": 0
}
]
When you delete the content2 with "_id": ObjectId("5e830a6d0dec1443e82ad282") with findOneAndDelete method like this:
router.delete("/pages/:id", async (req, res) => {
const result = await Page.findOneAndDelete({ _id: req.params.id });
res.send(result);
});
The middlewares will run, and adjust the orders, the remaining 2 documents will look like this:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 1, => DECREASED FROM 2 to 1
"__v": 0
}
]
Also you had better to include next in your pre save middleware so that other middlewares also work if you add later.
PageSchema.pre("save", async function(next) {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 });
this.order = lastPage ? lastPage.order + 1 : 0;
}
next();
});
Based on the answer of SuleymanSah, I wrote a mongoose plugin that does the job. This way, it can be applied to multiple schemas without unnessecary code duplication.
It has two optional arguments:
path: pathname where the ordinal number is to be stored (defaults to order)
scope: pathname or array of pathnames relative to which numbers should be given (defaults to [])
Example. Chapters should not be numbered globally, but relative to the book to which they belong:
ChapterSchema.plugin(orderPlugin, { path: 'chapterNumber', scope: 'book' })
File orderPlugin.js:
function getConditions(doc, scope) {
return Object.fromEntries([].concat(scope).map((path) => [path, doc[path]]))
}
export default (schema, options) => {
const path = (options && options.path) || 'order'
const scope = (options && options.scope) || {}
schema.add({
[path]: Number,
})
schema.pre('save', async function () {
if (!this[path]) {
const last = await this.constructor
.findOne(getConditions(this, scope))
.sort({ [path]: -1 })
this[path] = last ? last[path] + 1 : 0
}
})
schema.post('findOneAndDelete', async function (doc) {
await this.model.updateMany(
{ [path]: { $gt: doc[path] }, ...getConditions(doc, scope) },
{ $inc: { [path]: -1 } }
)
})
}

Mongoose query using if else possible?

I have this Schema:
const guestSchema = new Schema({
id: String,
cart: [
{
product: {
type: mongoose.Schema.ObjectId,
ref: "products"
},
quantity: Number
}
]
});
I have this query:
Guest.findOneAndUpdate(
{ id: req.sessionID },
{
$cond: [
{ "cart.product": { $ne: req.body.itemID } },
{ $push: { "cart": { product: req.body.itemID, quantity: 1 } } },
{ $inc: { "cart.quantity": 1 } }
]
},
{ upsert: true, new: true }
).exec(function(err, docs) {
err ? console.log(err) : res.send(docs);
});
Basically, what I'm trying to do is update based on a condition. I tried using $cond, but found out that operator isn't used for querys like I'm doing.
Based on this:
{ $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
I want something similar to the functionality of this operator for my query.
Let's break down my condition:
For my boolean expression: I want to check if req.body.itemID is $ne to any of the values in my cart
If true then: $push the itemID and quantity into the cart
Else (then item already exists): $inc the quantity by 1
Question: How would achieve this result? Do I need to make two seperate querys? I'm trying to avoid doing that if possible
I went through all their Update Field Operators, and there's probably no way to do this in the way I want.
I wonder why there is no $cond for update operators. Nonetheless, I have the solution to what I wanted the functionality accomplish. Just not in the elegant fashion that I would like it.
Guest.findOneAndUpdate(
{ id: req.sessionID },
{ id: req.sessionID }, //This is here in case need to upsert new guest
{ upsert: true, new: true }
).exec(function(err, docs) {
if (err) {
console.log(err);
} else {
//Find the index of the item in my cart
//Returns (-1) if not found
const item = doc.cart.findIndex(
item => item.product == req.body.itemID
);
if (item !== -1) {
//Item found, so increment quantity by 1
doc.cart[item].quantity += 1;
} else {
//Item not found, so push into cart array
doc.cart.push({ product: req.body.itemID, quantity: 1 });
}
doc.save();
}
});
This type of logic does not belong within the database query. It should happen in the application layer. MongoDB is also very fast at retrieving and updating single records with an index so that should not be a concern.
Please try doing something like this:
try {
const guest = await Guest.findOne().where({
id: req.sessionID
}).exec();
// your cond logic, and update the object
await guest.save();
res.status(200).json(guest);
} catch (error) {
handleError(res, error.message);
}

Get ID of affected document by using update()

I'm using this for doing an upsert:
Articles.update(
{ title: title },
{
title: title,
parent: type
},
{ upsert: true },
function(res) {
return console.log(res);
}
);
console.log(needToGetID);
Now I need to get the _id of the document which has been updated or inserted. I thought I can get that via callback, but res is undefined.
I assume there is just one unique document which is defined by the query.
Update
Forgot to mention that I'm using meteor...
The intent of .update() is to basically just "update" the matching document(s) ( as "multi" can also be applied here ) and be done with it, therefore that especially considering this "could" be applied to multiple documents then returning such information would not really make sense in the terms of that operation.
However if your intent is to modifiy a single "specific docucment", then the .findOneAndUpdate() method would apply assuming you are using mongoose:
Articles.findOneAndUpdate(
{ title: title },
{
title: title,
parent: type
},
{ upsert: true, new: true },
function(res) {
return console.log(res);
}
);
Also note the new: true which is important, as without it the default behavior is to return the document "before" it was modified by the statement.
At any rate, as the return here is the document that is matched and modified, then the _id and any other value is present in the response.
With meteor you can add a plugin to enable .findAndModify() which is the root method:
meteor add fongandrew:find-and-modify
And then in code:
Articles.findAndModify(
{
"query": { title: title },
"update": {
title: title,
parent: type
},
"upsert": true,
"new": true
},
function(res) {
return console.log(res);
}
);
Note that you should also really be using the $set operator, as without it the changes basically "overwrite" the target document:
Articles.findAndModify(
{
"query": { "title": title },
"update": {
"$set": {
"parent": type
}
},
"upsert": true,
"new": true
},
function(res) {
return console.log(res);
}
);