Mongoose - How to query field in the last object of an array of objects - node.js

I have MongoDB documents structured like this:
{
"_id": "5d8b987f9f8b9f9c8c8b9f9",
"targetsList": [
{
"target": "user",
"statusList": [
{
"date": "2018-01-01",
"type": "OK"
},
{
"date": "2018-01-02",
"type": "FAILD"
}
]
}
]
}
And I want to count all documents that in their "targetList" array, there is an object with "target"=="user" - and also that object conatin on the last element of its "statusList" array, an object with "type" != "FAILD".
Any ideas on how to implement this kind of query?
Mongo playground:
https://mongoplayground.net/p/3bCoHRnh-KQ
In this example, I expected the count to be 1, because only the second object meets the conditions.

An aggregation pipeline
1st step - Filtering out where "targetsList.target": "user"
2nd step - $unwind on targetsList to get it out of array
3rd step - getting the last element of the targetsList.statusList array using $arrayElemAt
4th step - getting the results where that last element is not FAILD
5th step - getting the count
demo - you can try removing parts of the pipeline to see what the intermediate results are
db.collection.aggregate([
{
$match: {
"targetsList.target": "user"
}
},
{
$unwind: "$targetsList"
},
{
$project: {
"targetsList.statusList": {
$arrayElemAt: [
"$targetsList.statusList",
-1
]
},
}
},
{
$match: {
"targetsList.statusList.type": {
$ne: "FAILD"
}
}
},
{
$count: "withoutFailedInLastElemCount"
}
])

Unless it's crucial that the element be the last index, this should work for your case.
db.collection.find({
"targetsList.statusList.type": {
$in: [
"FAILD"
]
}
})
This will retrieve documents where the type value is FAILD. To invert this you can swap $in for $nin.
Updated playground here

Here's another way to do it with a leading monster "$match".
db.collection.aggregate([
{
"$match": {
"targetsList.target": "user",
"$expr": {
"$reduce": {
"input": "$targetsList",
"initialValue": false,
"in": {
"$or": [
"$$value",
{
"$ne": [
{
"$last": "$$this.statusList.type"
},
"FAILD"
]
}
]
}
}
}
}
},
{
"$count": "noFailedLastCount"
}
])
Try it on mongoplayground.net.

Related

Remove Embedded Documents in an Array in MongoDB with mongoose (updateOne - $pull) not work

I have an app with MongoDB (Mongoose) in NodeJs.
In a collection I have this type of documents, defined by weeks:
{
"_id":
{"$oid":"617f3f51f883fab2de3e7260"},
"endDate":{"$date":"2021-11-07T23:59:59.000Z"},
"startDate":{"$date":"2021-11-01T00:00:00.000Z"},
"wastes":[
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":780},
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140},
{"timestamp":{"$date":"2021-11-01T03:00:58.000Z"},"duration":540},
{"timestamp":{"$date":"2021-11-01T07:00:58.000Z"},"duration":540},
{"timestamp":{"$date":"2021-11-01T09:00:58.000Z"},"duration":960},
{"timestamp":{"$date":"2021-11-01T09:00:58.000Z"},"duration":1140},
{"timestamp":{"$date":"2021-11-01T15:00:58.000Z"},"duration":180},
{"timestamp":{"$date":"2021-11-01T15:00:58.000Z"},"duration":540}
...
]}
I have a function that finds wastes with the same timestamp, for example "2021-11-01T01:00:58.000Z", gives the longest duration for this timestamp.
I want to delete all entries with that timestamp:
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":780},
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140}
And insert only the one with the highest duration:
{"timestamp":{"$date":"2021-11-01T01:00:58.000Z"},"duration":1140}
I'm using updateOne with $pull and $push, but it doesn't work.
let query = {
startDate: new Date(startDayWeek),
};
let deleteProjection = {
$pull: {
wastes: { timestamp: new Date(timestampDeleteInsertion) },
},
};
let insertProjection = {
$push: { wastes: insertRegisterForTimestamp },
};
//Delete
await coleccion.updateOne(query, deleteProjection);
//Insertion
await coleccion.updateOne(query, insertProjection);
I have also tried with {upsert: false}, {multi: true}.
If I use the same commands in the MongoDB Compass shell, it works without problems:
//Delete
db.coleccion.updateOne({startDate: ISODate('2021-11-01T00:00:00')}, {$pull: {'wastes': {timestamp: ISODate('2021-11-01T01:00:58.000Z')}}})
//Insertion
db.coleccion.updateOne({startDate: ISODate('2021-11-01T00:00:00')}, {$push: {'wastes': {'timestamp':ISODate('2021-11-01T01:00:58.000Z'), 'duration': 1140}}})
You can achieve expected behaviour with Updates with Aggregation Pipeline
The aggregation will consists of 3 steps:
find out the max duration using $reduce; stored the result into a field
$filter the wastes array by keeping only elements not equal to the selected timestamp or the duration is not the max duration
$unset the helper field created in step 1
db.collection.update({},
[
{
$addFields: {
maxDuration: {
"$reduce": {
"input": "$wastes",
"initialValue": null,
"in": {
"$cond": {
"if": {
$and: [
{
$eq: [
"$$this.timestamp",
{
"$date": "2021-11-01T01:00:58.000Z"
}
]
},
{
$gt: [
"$$this.duration",
"$$value"
]
}
]
},
"then": "$$this.duration",
"else": "$$value"
}
}
}
}
}
},
{
$set: {
wastes: {
$filter: {
input: "$wastes",
as: "w",
cond: {
$or: [
{
$ne: [
"$$w.timestamp",
{
"$date": "2021-11-01T01:00:58.000Z"
}
]
},
{
$eq: [
"$$w.duration",
"$maxDuration"
]
}
]
}
}
}
}
},
{
"$unset": "maxDuration"
}
])
Here is the Mongo playground for your reference.
I have the same issue with the updateOne and pull command, if use the updateOne with push, it works.
In the mongo shell or in the compass, both situations (push/pull) works, but with mongoose, it finds the criteria but don't update/modify.
Result
{
"acknowledged" : true,
"matchedCount" : 1.0,
"modifiedCount" : 0.0
}

How to sum values of third level nested array of objects across all documents in MongoDB?

I have a mongoose document having the following Schema:
Products
{
"section":"",
"category":"Food & Drink",
"sub_category":"Main Dish",
"product_code":"ST",
"title":"Steak",
"description":"Served with sauted vegetables",
"tags":[
],
"warranty":"None",
"product_variants":[
{
"variant_code":"ST1",
"variant_title":"Rib Eye",
"images":[
],
"status":"Active",
"variant_details":[
{
"size":"6oz",
"local_price":800,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":94
}
]
},
{
"size":"10oz",
"local_price":1000,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":147
}
]
},
{
"size":"12oz",
"local_price":1200,
"local_discount":"0",
"foreign_price":0,
"foreign_discount":"0",
"inventory":[
{
"branch_id":{
},
"quantity":199
}
]
}
]
}
]
}
The above document shows only one object in the product_variants field but please note that there could be several objects as well. I need to sum the quantity for each size and product variant.
How would I do that using aggregate function? I am using mongoose in node js environment.
Query
(its based on the last comment in the previous answer, similar query but multiplies that quantity with the local price)
Test code here
db.collection.aggregate([
{
"$unwind": "$product_variants"
},
{
"$unwind": "$product_variants.variant_details"
},
{
"$unwind": "$product_variants.variant_details.inventory"
},
{
"$set": {
"total_local_price": {
"$multiply": [
"$product_variants.variant_details.inventory.quantity",
"$product_variants.variant_details.local_price"
]
}
}
},
{
$group: {
_id: null, // or "$_id" if you want only for 1 document
total_qty: {
$sum: "$total_local_price"
}
}
}
])
You can use this aggregation query:
Fisrt $project to get only the quantity values. It generates the following output:
"array": [
[
[
94
],
[
147
],
[
199
]
]
So next step is to use $unwind three times to flat the array.
And $group by _id using $sum
yourModel.aggregate([{
"$project": {
"array": "$product_variants.variant_details.inventory.quantity"
}
},
{
"$unwind": "$array"
},
{
"$unwind": "$array"
},
{
"$unwind": "$array"
},
{
"$group": {
"_id": "$_id",
"size": {
"$sum": "$array"
}
}
}])
Example here
Edit
As Takis _ suggested into the comments if you want to get all values from your entire collection (not only for each document) you can $group using null as this example

Mongodb lookup array of elements with combined result

So these are my two documents
Order document:
{
"_id":"02a33b9a-284c-4869-885e-d46981fdd679",
"context":{
"products":[
{
"id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00"
}
],
},
}
Product document:
{
"_id":"e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"context":{
"name": "My Product",
"image": "someimage"
},
}
So I'm trying to do a lookup for a products in order document, but the result should contain combined fields, like so:
"products":[
{
"_id": "e68fc86a-b4ad-4588-b182-ae9ee3db25e4",
"version": "2020-03-14T13:18:41.296+00:00",
"name": "My Product",
"image": "someimage"
}
],
Not sure how to do this, should I do it outside of the lookup, or inside? This is my aggregation
Orders.aggregate([
{
"$lookup":{
"from":"products",
"let":{
"products":"$context.products"
},
"pipeline":[
{
"$match":{
"$expr":{
"$in":[
"$_id",
"$$products.id"
]
}
}
},
{
"$project":{
"_id":0,
"id":1,
"name":"$context.name"
}
}
],
"as":"mergedProducts"
}
},
{
"$project":{
"context":"$context",
"mergedProducts":"$mergedProducts"
}
},
]);
You need to run that mapping outside of $lookup by running $map along with $arrayElemAt to get single pair from both arrays and then apply $mergeObjects to get one object as a result:
db.Order.aggregate([
{
$lookup: {
from: "products",
localField: "context.products.id",
foreignField: "_id",
as: "productDetails"
}
},
{
$addFields: {
productDetails: {
$map: {
input: "$productDetails",
in: {
_id: "$$this._id",
name: "$$this.context.name"
}
}
}
}
},
{
$project: {
_id: 1,
"context.products": {
$map: {
input: "$context.products",
as: "prod",
in: {
$mergeObjects: [
"$$prod",
{ $arrayElemAt: [ { $filter: { input: "$productDetails", cond: { $eq: [ "$$this._id", "$$prod.id" ] } } }, 0 ] }
]
}
}
}
}
}
])
Mongo Playground
The goals of the last step is to take take two arrays: products and productDetails (the output of $lookup) and find matches between them. We know there's always one match so we can get only one item $arrayElemAt 0. As an output of $map there will be single array containing "merged" documents.

Query by data already in the object

I'm writing a query that gets data from "coll2" based on data that is inside "coll1".
Coll1 has the following data structure:
{
"_id": "asdf",
"name": "John",
"bags": [
{
"type": "typ1",
"size": "siz1"
},
{
"type": "typ2",
"size": "siz2"
}
]
}
Coll2 has the following data structure:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
{
_id: "fghj",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz4"]
}
I want to get all the documents in coll2 that have the same Type+Size combo as in coll1 using the $lookup stage of the aggregation pipeline. I understand that this can be achieved by using the $lookup pipeline and $expr but I cant seem to figure out how to dynamically make a query to pass into the $match stage.
The output I would like to get for the above data would be:
{
_id: "qwer",
coll1Name: "John",
types: ["typ1", "typ3"],
sizes: ["siz1", "siz4"]
}
{
_id: "zxcv",
coll1Name: "John",
types: ["typ2", "typ3"],
sizes: ["siz1", "siz2"]
}
You can use $lookup to get the data from Col2. Then you need to check if there's any element in Col2 ($anyElemenTrue) that matches with Col1. $map and $in can be used here. Then you just need to $unwind and promote Col2 to root level using $replaceRoot
db.Col1.aggregate([
{
$lookup: {
from: "Col2",
localField: "name",
foreignField: "coll1Name",
as: "Col2"
}
},
{
$project: {
Col2: {
$filter: {
input: "$Col2",
as: "c2",
cond: {
$anyElementTrue: {
$map: {
input: "$bags",
as: "b",
in: {
$and: [
{ $in: [ "$$b.type", "$$c2.types" ] },
{ $in: [ "$$b.size", "$$c2.sizes" ] },
]
}
}
}
}
}
}
}
},
{
$unwind: "$Col2"
},
{
$replaceRoot: {
newRoot: "$Col2"
}
}
])
You are correct in your approach to use $lookup with the pipeline field to filter the input documents in the $match pipeline
The $expr expression should typically follow
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
where the first match expression in the $and conditional { "$eq": [ "$name", "$$coll1_name" ] } checks to see if the name field in coll1 collection matches the coll1Name field in the input documents from coll2.
Of course the fields from coll2 should be defined in a variable in the pipeline with the let field for the $lookup pipeline to access them.
The other match filters are basically checking if the arrays are equal where "$bags.type" from coll1 resolves to an array of types i.e. [ "typ1", "typ3" ] for example.
On getting the output field from $lookup which happens to be an array, you can filter the documents in coll2 on that array field where there can be some empty lists as a resul of the above $lookup pipeline $match filter:
{ "$match": { "coll1Data.0": { "$exists": true } } }
Overall your aggregate pipeline operation would be as follows:
db.getCollection('coll2').aggregate([
{ "$lookup" : {
"from": "coll1",
"let": { "coll1_name": "$coll1Name", "types": "$types", "sizes": "$sizes" },
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$eq": [ "$name", "$$coll1_name" ] },
{ "$setEquals": [ "$bags.type", "$$types" ] },
{ "$setEquals": [ "$bags.size", "$$sizes" ] }
]
}
} }
],
"as": "coll1Data"
} },
{ "$match": { "coll1Data.0": { "$exists": true } } },
{ "$project": { "coll1Data": 0 } }
])

MongoDB: Concatenate Multiple Arrays

I have 3 arrays of ObjectIds I want to concatenate into a single array, and then sort by creation date. $setUnion does precisely what I want, but I'd like to try without using it.
Schema of object I want to sort:
var chirpSchema = new mongoose.Schema({
interactions: {
_liked : ["55035390d3e910505be02ce2"] // [{ type: $oid, ref: "interaction" }]
, _shared : ["507f191e810c19729de860ea", "507f191e810c19729de860ea"] // [{ type: $oid, ref: "interaction" }]
, _viewed : ["507f1f77bcf86cd799439011"] // [{ type: $oid, ref: "interaction" }]
}
});
Desired result: Concatenate _liked, _shared, and _viewed into a single array, and then sort them by creation date using aggregate pipeline. See below
["507f1f77bcf86cd799439011", "507f191e810c19729de860ea", "507f191e810c19729de860ea", "55035390d3e910505be02ce2"]
I know I'm suppose to use $push, $each, $group, and $unwind in some combination or other, but I'm having trouble piecing together the documenation to make this happen.
Update: Query
model_user.aggregate([
{ $match : { '_id' : { $in : following } } }
, { $project : { 'interactions' : 1 } }
, { $project : {
"combined": { $setUnion : [
"$interactions._liked"
, "$interactions._shared"
, "$interactions._viewed"
]}
}}
])
.exec(function (err, data) {
if (err) return next(err);
next(data); // Combined is returning null
})
If all the Object _id values are "unique" then $setUnion is your best option. It is of course not "ordered" in any way as it works with a "set", and that does not guarantee order. But you can always unwind and $sort.
[
{ "$project": {
"combined": { "$setUnion": [
{ "$ifNull": [ "$interactions._liked", [] ] },
{ "$ifNull": [ "$interactions._shared", [] ] },
{ "$ifNull", [ "$interactions._viewed", [] ] }
]}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
Of course again since this is a "set" of distinct values you can do the old way instead with $addToSet, after processing $unwind on each array:
[
{ "$unwind": "$interactions._liked" },
{ "$unwind": "$interactions._shared" },
{ "$unwind": "$interactions._viewed" },
{ "$project": {
"interactions": 1,
"type": { "$const": [ "liked", "shared", "viewed" ] }
}}
{ "$unwind": "$type" },
{ "$group": {
"_id": "$_id",
"combined": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$type", "liked" ] },
"$interactions._liked",
{ "$cond": [
{ "$eq": [ "$type", "shared" ] },
"$interactions._shared",
"$interactions._viewed"
]}
]
}
}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
But still the same thing applies to ordering.
Future releases even have the ability to concatenate arrays without reducing to a "set":
[
{ "$project": {
"combined": { "$concatArrays": [
"$interactions._liked",
"$interactions._shared",
"$interactions._viewed"
]}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
But still there is no way to re-order the results without procesing $unwind and $sort.
You might therefore consider that unless you need this grouped across multiple documents, that the basic "contenate and sort" operation is best handled in client code. MongoDB has no way to do this "in place" on the array at present, so per document in client code is your best bet.
But if you do need to do this grouping over multiple documents, then the sort of approaches as shown here are for you.
Also note that "creation" here means creation of the ObjectId value itself and not other properties from your referenced objects. If you need those, then you perform a populate on the id values after the aggregation or query instead, and of course sort in client code.

Resources