MongoDB update value of deeply nested array and sort - javascript

I have this document of timestamps sorted by time:
{
_id: '1',
timestamps: [
{
id: '589b32cf-28b3-4a25-8fd1-5e4f86682199',
time: '2022-04-13T19:00:00.122Z'
},
{
id: '781a47de-d21a-4c2c-9814-b46f4a3cfa30',
time: '2022-04-13T20:00:00.498Z'
}
]
};
I want to update a timestamp by id, change the time, and sort
example: update timestamp with id: '589b32cf-28b3-4a25-8fd1-5e4f86682199':
{
id: '589b32cf-28b3-4a25-8fd1-5e4f86682199',
time: '2022-04-13T20:30:00.122Z'
}
the updated document should like this:
{
_id: '1',
timestamps: [
{
id: '32bb3-2222-1111-j878-b4000a3wwa30',
time: '2022-04-13T19:30:00.122Z'
},
{
id: '589b32cf-28b3-4a25-8fd1-5e4f86682199',
time: '2022-04-13T20:30:00.122Z'
}
]
};
I came up with this method to update and sort each element in the array:
const updateResult = await TimestampCollection.updateOne(
{ _id: '1' },
{
$push: {
timestamps: {
$each: [{ id: timestamp.id, time: timestamp.time }],
$sort: { time: 1 }
}
}
}
);
However this pushed a new object to the array with the same id and updated time:
{
_id: '1',
timestamps: [
{
id: '589b32cf-28b3-4a25-8fd1-5e4f86682199',
time: '2022-04-13T19:00:00.122Z'
},
{
id: '32bb3-2222-1111-j878-b4000a3wwa30',
time: '2022-04-13T19:30:00.122Z'
},
{
id: '589b32cf-28b3-4a25-8fd1-5e4f86682199',
time: '2022-04-13T20:30:00.122Z'
}
]
};
Any idea how to fix this?

I didn't find a way to do this in a single query. Sadly combining $set and $push in a single query leads to a conflict...
db.collection.update({
_id: "1",
"timestamps.id": timestamp.id
},
{
"$set": {
"timestamps.$.time": timestamp.time
},
});
db.collection.update({
_id: "1",
},
{
"$push": {
"timestamps": {
"$each": [],
"$sort": {
"time": 1
}
}
}
})
This solution involves two queries. One for updating the element in the array and the other for sorting the array.

Related

Mongo Aggregation pipeline update or push

I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground

MongoDB aggregate join two $group

I have a model that looks like this:
{
tokens: [
{
value: {
type: String,
required: true,
unique: true,
},
origin: {
type: String,
default: 'Unknown',
},
grabbedAt: {
type: Date,
default: Date.now,
},
},
], ...
}
Now I want to format the data in the following way that all "tokens" with a date of the past 12 days are returned and grouped by their origin with a count per day.
So the result would look like this: [{ origin: 'Unknown', data: [0,1,2,...] }, { origin: 'origin2', data: [1,10,...] }]
The data array would hold the count of tokens acquired on past 12 days, beginning with the first day to the 12th day.
I already tried something like this:
Account.aggregate([
{ $unwind: '$tokens' },
{ $match: { 'tokens.grabbedAt': { $gte: beforeDate } } },
{
$group: {
_id: { origin: '$tokens.origin', date: '$tokens.grabbedAt' },
count: { $sum: 1 },
},
},
{ $project: { _id: 0, origin: '$_id.origin', date: '$_id.date', count: '$count' } },
{ $sort: { date: 1 } },
]);
But using this code each date and origin is included multiple times. So how can I "join" or merge these two $groups?
One way to do it, is $unwind and $group the tokens by origin and then use 3 steps to create a list of the 12 needed dates. Then we can use a $set step to create the empty dates objects that were missing. Now we can $concatArrays our real measurements with the "artificial" ones of the empty days. The last part is just to group and sum it up.
db.collection.aggregate([
{$unwind: "$tokens"},
{$match: {"tokens.grabbedAt": {$gte: beforeDate}}},
{$sort: {"tokens.date": 1}},
{
$group: {
_id: "$tokens.origin",
res: {
$push: {
dateString: {$dateToString: {date: "$tokens.grabbedAt",
format: "%Y-%m-%d"}}, count: 1
}
}
}
},
{
$addFields: {startDate: beforeDate, range: {$range: [0, 12, 1]}}
},
{
$set: {
dateStrings: {
$map: {
input: "$range",
in: {
dateString: {
$dateToString: {
date: {
$add: [
"$startDate",
{$multiply: ["$$this", 24, 60, 60, 1000]}
]
},
format: "%Y-%m-%d"
}
},
count: 0
}
}
}
}
},
{$project: {data: {$concatArrays: ["$dateStrings", "$res"]}}},
{$unwind: "$data"},
{$group: {
_id: {origin: "$_id", date: "$data.dateString"},
count: {$sum: "$data.count"}
}
},
{$group: {_id: "$_id.origin", date: {$push: "$count"}}}
])
Playground

Mongo Query - match and lookup combined

I’ve defined the following query which fetches me all items with an id which is in a given list of ids, and a status of either active or retracted.
const query = {
$and : [
{
$or: [
{
status: ‘active’,
},
{
status: ‘retracted’,
},
],
},
{
id: { $in: ids },
},
],
};
Each of these items has a parent_id field, which can either be null if the item does not have a parent, or can be the id of the parent.
I want my query to fetch all items with the ids I supply, as well as their parent items, if such a parent exists.
For example, if I supply the following IDs
[1,2,3]
and item 2 has a parent with id 5, while item 1 and 3 have parent_id set to null, I want my query to return the following items:
[1,2,3,5].
To achieve this I wrote the following query:
const collection = db.collection(‘myCollection’);
const data = await collection.aggregate([
{$match : query},
{
$lookup: {
from: ‘myCollection’,
let: { parentID: ‘$parent_id’},
pipeline: [
{
$match: {
$expr: {
$eq: [‘$id’, ‘$$parentID’],
},
},
},
as: ‘parent’,
},
},
]).sort(‘created_date’, ‘desc’).toArray();
return data;
However, this always returns null.
Sample Data:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 2,
parent_id: null,
data: ‘bla bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]
Input: [1]
Output:
[
{
id: 1,
parent_id: 3,
data: ‘bla bla’
},
{
id: 3,
parent_id: null,
data: ‘bla’
}
]
The approach with $lookup being run upon same collection should work however it gives you a nested array so you need few additional stages to flatten such array and get all elements as on result set:
db.collection.aggregate([
{
$match: { id: { $in: [1] } }
},
{
$lookup: {
from: "collection",
localField: "parent_id",
foreignField: "id",
as: "parent"
}
},
{
$project: {
all: {
$concatArrays: [
"$parent",
[ "$$ROOT" ]
]
}
}
},
{
$project: {
"all.parent": 0
}
},
{
$unwind: "$all"
},
{
$replaceRoot: {
newRoot: "$all"
}
}
])
Mongo Playground
Your aggregation was malformed and lack some "]" for example closing the pipeline fied.
If you fix that the query works fine for me. Example
You can try this. The input array is [2,3] where 2 has parent id=1 and that is not in the input array. But the output array has the entry.
Working Playground
db.collection.aggregate([
{
$match: {
_id: {
$in: [
2,
3
]
}
}
},
{
$lookup: {
from: "collection",
localField: "p",
foreignField: "_id",
as: "parent"
}
},
{
$project: {
_id: 0,
id: {
$concatArrays: [
[
"$_id"
],
"$parent._id"
]
}
}
},
{
$unwind: "$id"
},
{
$sort: {
id: 1
}
}
])

How to group MongoDB records by a certain date range?

I'm a little new to MongoDB and I'm having trouble querying with it.
Suppose I have the following dataset,
[
{
_id: '1',
date: "2020-12-31T22:02:11.257Z",
},
{
_id: '2',
date: "2020-12-31T22:05:11.257Z",
},
{
_id: '3',
date: "2021-01-01T22:02:11.257Z",
},
{
_id: '4',
date: "2021-01-02T12:02:11.257Z",
},
{
_id: '5',
date: "2021-01-02T22:02:11.257Z",
}
]
I'm trying to group all records by day. From my frontend, I send over a month, and then I run the query based on that. So if the user select January, I would run the following query:
router.get('/', async (req, res) => {
const {selectedMonth, selectedYear} = req.query; // january would be '1' here
const data = await db.collection.find({"date": {
"$gt": new Date(selectedYear, parseInt(selectedMonth) - 1, 1),
"$lte": new Date(selectedYear, parseInt(selectedMonth), 1)
}}).sort({ date: -1 })
Here, I'm getting the all records that are within the selected range. So, if a user selected January 2021, I'm fetching all records that are greater than December 31, 2020 and less than or equal to January 31, 2021.
The problem here is that I want to get a count of all records per day. I'm able to fetch all records within the specified date range, but I'm looking for something like the below, to be returned:
[
"2021-01-01": [
{ _id: '3', date: "2021-01-01T22:02:11.257Z" },
],
"2021-01-02": [
{ _id: '4', date: "2021-01-02T12:02:11.257Z" },
{ _id: '5', date: "2021-01-02T22:02:11.257Z" },
]
]
I was thinking of looping through the returned data and building my own response object, but I'm wondering if there's a better way to do this? Here what I'm currently doing,
const result = []
let count = 0;
data.forEach((record, index) => {
// first record will always set the base
if (index === 0) {
result.push({
date: record.date.toLocaleDateString(),
count: 1
})
} else {
// If the record is the same date, then increase counter
if (record.date.toLocaleDateString() === result[count].date) {
result[count].count = result[count].count + 1
} else {
// push a new record and increase count
result.push({
date: record.date.toLocaleDateString(),
count: 1
})
count = count + 1
}
}
});
Which yields,
result [
{ date: '1/2/2021', count: 2 },
{ date: '1/1/2021', count: 1 }
]
You'd need aggregation pipeline for this:
db.collection.aggregate([
// First Stage: filter out dates
{
$match: {
date: { $gte: new ISODate("2020-01-01"), $lt: new ISODate("2020-12-31") },
},
},
// Second Stage: group by day of the year
{
$group: {
_id: { $dateToString: { format: "%d-%m-%Y", date: "$date" } },
count: { $sum: 1 },
},
},
// Third Stage, reshape the output documents
{
$project: {
_id: 0,
date: "$_id",
count: 1
},
},
]);
What you need can be done using the aggregation framework which has a number of operators that you can use
for the different pipelines. The first pipeline step is the filtering where you use $match pipeline stage together with
$expr query operator and the $month and $year date operators:
const pipeline = [
// First pipeline step
{ '$match': {
'$expr': {
'$and': [
{ '$eq': [ { '$month': '$date' }, parseInt(selectedMonth) ] },
{ '$eq': [ { '$year': '$date' }, parseInt(selectedYear) ] }
]
}
} }
];
The next step would be to group all the documents returned after filtering by day with $dateToString within $group as follows:
const pipeline = [
// First pipeline step
{ '$match': {
'$expr': {
'$and': [
{ '$eq': [ { '$month': '$date' }, parseInt(selectedMonth) ] },
{ '$eq': [ { '$year': '$date' }, parseInt(selectedYear) ] }
]
}
} },
// Second pipeline step
{ '$group': {
'_id': { '$dateToString': { 'format': '%Y-%m-%d', 'date': '$date' } },
'data': { '$push': '$$ROOT' },
'count': { '$sum': 1 }
} }
];
The next steps will be to reshape the documents to your desired projection where you can leverage the use of $arrayToObject operator and a $replaceRoot pipeline to get the desired result.
const pipeline = [
// First pipeline step
{ '$match': {
'$expr': {
'$and': [
{ '$eq': [ { '$month': '$date' }, parseInt(selectedMonth) ] },
{ '$eq': [ { '$year': '$date' }, parseInt(selectedYear) ] }
]
}
} },
// Second pipeline step
{ '$group': {
'_id': { '$dateToString': { 'format': '%Y-%m-%d', 'date': '$date' } },
'data': { '$push': '$$ROOT' },
'count': { '$sum': 1 }
} },
// Third pipeline step
{ '$group': {
'_id': null,
'counts': {
'$push': {
'k': '$_id',
'v': {
'data': '$data',
'count': '$count'
}
}
}
} },
// Fourth pipeline step
{ '$replaceRoot': {
'newRoot': { '$arrayToObject': '$counts' }
} }
];
Which can then be combined and ran as follows:
router.get('/', async (req, res) => {
const { selectedMonth, selectedYear } = req.query; // january would be '1' here
const pipeline = [...]; // pipeline above
const data = await db.collection.aggregate(pipeline).toArray();
console.log(data);
}
For a final result of the form:
[
{
"2021-01-01": [
{ _id: '3', date: "2021-01-01T22:02:11.257Z" },
],
"2021-01-02": [
{ _id: '4', date: "2021-01-02T12:02:11.257Z" },
{ _id: '5', date: "2021-01-02T22:02:11.257Z" },
]
}
]
update your third pipeline step to:
// Third pipeline step
{ '$group': {
'_id': null,
'counts': {
'$push': {
'k': '$_id',
'v': '$data'
}
}
} },
And for a final result of the form:
[
{
"2021-01-01": 1,
"2021-01-02": 2
}
]
your third pipeline step should be:
// Third pipeline step
{ '$group': {
'_id': null,
'counts': {
'$push': {
'k': '$_id',
'v': '$count'
}
}
} },

Trying to get data from Mongo DB with aggregate

I have "Offers" and "Requests" collections, I need to get all offers that user made, group them by requests and find the lowest "Offer.price" on each request, each offer has requestId field.
I am using aggregate to solve this,
db.Offer.aggregate([{
$match: {
ownerId: mongoose.Types.ObjectId(req.params.ownerId)
}
},
{
$group: {
_id: "$requestId",
price: {
$min: "$price"
}
}
}
])
and This is what i get :
[ { _id: 5dc47241af1406031489c65c, price: 14 },
{ _id: 5dc47241af1406031489c653, price: 3 },
{ _id: 5dc47241af1406031489c656, price: 5 },
{ _id: 5dc8add63f73953ff408f962, price: 6 },
{ _id: 5dc8add63f73953ff408f969, price: 22 },
{ _id: 5dc47241af1406031489c658, price: 1 } ]
Now I want to populate these with rest of the data from "Offer"
const OfferSchema = new Schema({
requestId: {
type: Schema.Types.ObjectId,
ref: 'Request'
},
ownerId: {
type: Schema.Types.ObjectId,
required: true,
ref: 'User'
},
price: {
type: Number,
required: true
},
createdAt: {
type: Date,
default: Date.now
},
isBest: {
type: Boolean,
default: false
},
isWinner: {
type: Boolean,
default: false,
}
});
What would be best way to do something like this?
Thank you for your help!
Consider the following dataset:
db.dum.insert({ownerId:1, requestId:'a', price:3, createdAt:3, isWinner:true})
db.dum.insert({ownerId:1, requestId:'a', price:1, createdAt:1, isWinner:false})
db.dum.insert({ownerId:1, requestId:'a', price:2, createdAt:2, isWinner:true})
db.dum.insert({ownerId:1, requestId:'b', price:4, createdAt:2, isWinner:true})
db.dum.insert({ownerId:1, requestId:'b', price:5, createdAt:1, isWinner:false})
db.dum.insert({ownerId:2, requestId:'b', price:5, createdAt:1, isWinner:false})
You could use $reduce
Here, for a grouping id, we keep all matching documents as an array (candidates).
On the project stage, for each group we iterate through the array, and reduce it to the minimal element found (by price that is)
db.dum.aggregate([{
$match: {
ownerId: 1
}
},
{
$group: {
_id: "$requestId",
candidates: { $push:'$$ROOT'}
}
},
{
$project:{
item: {
$reduce: {
input: '$candidates',
initialValue: '$candidates.0',
in: {
$cond: {
if: {
$lt: ['$$value.price', '$$this.price']
},
then:'$$value',
else:'$$this'
}
}
}
}
}
},
{
$replaceRoot:{newRoot:'$item'}
}
]).toArray()
output:
[
{
"_id" : ObjectId("5ddcc8e0eb1f0217802fb507"),
"ownerId" : 1,
"requestId" : "b",
"price" : 4,
"createdAt" : 2,
"isWinner" : true
},
{
"_id" : ObjectId("5ddcc8e0eb1f0217802fb505"),
"ownerId" : 1,
"requestId" : "a",
"price" : 1,
"createdAt" : 1,
"isWinner" : false
}
]

Categories

Resources