Move elements within MongoDB document - javascript

Background:
A customer is an object that has a name field.
A line is an object that has the following fields:
inLine - an array of customers
currentCustomer - a customer
processed - an array of customers
The collection 'line' contains documents that are line objects.
Problem:
I'm trying to implement a procedure which would do the following:
Push currentCustomer to processed
Set currentCustomer to the 1st element in inLine
Pop the 1st element of inLine
Since the new value of a field depends on the previous value of another, atomicity is important here.
What I tried so far:
Naive approach
db.collection('line').findOneAndUpdate({
_id: new ObjectId(lineId),
}, {
$set: {
currentCustomer: '$inLine.0',
},
$pop: {
inLine: -1,
},
$push: {
processed: '$currentCustomer',
},
});
However, currentCustomer is set to a string which is literally "$inLine.0" and processed has a string which is literally "$currentCustomer".
Aggregation approach
db.collection('line').findOneAndUpdate({
_id: new ObjectId(lineId),
}, [{
$set: {
currentCustomer: '$inLine.0',
},
$pop: {
inLine: -1,
},
$push: {
processed: '$currentCustomer',
},
}]);
However, I got the following error:
MongoError: A pipeline stage specification object must contain exactly one field.
Multi-stage aggregation approach
db.collection('line').findOneAndUpdate({
_id: new ObjectId(lineId),
}, [{
$set: {
currentCustomer: '$inLine.0',
},
}, {
$pop: {
inLine: -1,
},
}, {
$push: {
processed: '$currentCustomer',
},
}]);
However, $pop and $push are Unrecognized pipeline stage names.
I tried making it using only $set stages, but it ended up very ugly and I still couldn't get it to work.

Based on turivishal's answer, it was solved like so:
db.collection('line').findOneAndUpdate({
_id: new ObjectId(lineId),
}, [{
$set: {
// currentCustomer = inLine.length === 0 ? null : inLine[0]
currentCustomer: {
$cond: [
{ $eq: [{ $size: '$inLine' }, 0] },
null,
{ $first: '$inLine' },
],
},
// inLine = inLine.slice(1)
inLine: {
$cond: [
{ $eq: [{ $size: '$inLine' }, 0] },
[],
{ $slice: ['$inLine', 1, { $size: '$inLine' }] },
],
},
// if currentCustomer !== null then processed.push(currentCustomer)
processed: {
$cond: [
{
$eq: ['$currentCustomer', null],
},
'$processed',
{
$concatArrays: [
'$processed', ['$currentCustomer'],
],
}
],
},
},
}]);

I don't think its possible with simple update using $push or $pop.
As per your experiment, the aggregation can not support direct $push, $pop stage in root level, so I have corrected your query,
currentCustomer check condition if size of inLine is 0 then return null otherwise get first element from inLine array using $arrayElemAt,
inLine check condition if size of inLine is 0 then return [] otherwise remove first element from inLine array using $slice and $size
processed concat both arrays using $concatArrays, $ifNull to check if field is null then return blank array, check condition if currentCustomer null then return [] otherwise return currentCustomer
db.collection('line').findOneAndUpdate(
{ _id: new ObjectId(lineId), },
[{
$set: {
currentCustomer: {
$cond: [
{ $eq: [{ $size: "$inLine" }, 0] },
null,
{ $arrayElemAt: ["$inLine", 0] }
]
},
inLine: {
$cond: [
{ $eq: [{ $size: "$inLine" }, 0] },
[],
{ $slice: ["$inLine", 1, { $size: "$inLine" }] }
]
},
processed: {
$concatArrays: [
{ $ifNull: ["$processed", []] },
{
$cond: [
{ $eq: ["$currentCustomer", null] },
[],
["$currentCustomer"]
]
}
]
}
}
}]
);
Playground

Related

Execlude certain values in the group stage in mongodb

In my code i am trying to collect the available filters however there are fields that were not
filled by the user and left as empty strings e.g the "brands" field. however, empty strings should not be considered as a valid value
this is my group stage:
{
$group: {
_id: null,
sizes: { $addToSet: '$combinations.size' },
colors: { $push: '$combinations.color' },
brands: { $addToSet: '$brand' },
topPrice: { $max: `$price.${req.query.reg || 'aud'}` },
bottomPrice: { $min: `$price.${req.query.reg || 'aud'}` },
},
},
the brands field will be:
[ "" ]
what i wanted is
[]
You can use conditional aggregating.
db.collection.aggregate([
{
$group: {
"_id": null,
"brands": {
"$addToSet": {
$cond: [
{$eq: ["$brand",""]}, //if brands is empty string,
"$$REMOVE", //then, REMOVE it
"$brand" //else, keep it
]
}
}
}
}
])
Demo

MongoDB Aggregation - match documents with array of objects, by another array of objects filter

I have documents that consist of an array of objects, and each object in this array consists of another array of objects.
For simplicity, irrelevant fields of the documents were omitted.
It looks like this (2 documents):
{
title: 'abc',
parts: [
{
part: "verse",
progressions: [
{
progression: "62a4a87da7fdbdabf787e47f",
key: "Ab",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "62adf477ed11cbbe156d5769",
key: "C",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "62b5aaa0c9e9fe8a7d7240d2"
},
{
part: "chorus",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0c9e9fe8a7d7240d8"
}
],
}
{
title: 'def',
parts: [
{
part: "verse",
progressions: [
{
progression: "33a4a87da7fopvvbf787erwe",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "98opf477ewfscbbe156d5442",
key: "Bb",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "12r3aaa0c4r5me8a7d72oi8u"
},
{
part: "bridge",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "C#",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0rwfvse8a7d7240d8"
}
],
}
The parameters that the client sends with a request are an array of objects:
[
{ part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
{ part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
]
I want to retrieve, through mongodb aggregation, the documents that at least one of objects in the input array above is matching them:
In this example, documents that have in their parts array field, an object that has the value 'verse' in the part property and one of the progressions id's ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] in the progression property in one of the objects in the progressions property, or documents that have in their parts array field, an object that has the value 'chorus' in the part property and one of the progressions id's ['62adf477ed11cbbe156d5769'] in the progression property in one of the objects in the progressions property.
In this example, the matching document is the first one (with the title 'abc'), but in actual use, there might be many matching documents.
I tried to create an aggregation pipeline myself (using the mongoose 'aggregate' method):
// parsedProgressions = [
// { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
// { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
// ]
songs.aggregate([
{
$addFields: {
"tempMapResults": {
$map: {
input: parsedProgressions,
as: "parsedProgression",
in: {
$cond: {
if: { parts: { $elemMatch: { part: "$$parsedProgression.part", "progressions.progression": mongoose.Types.ObjectId("$$parsedProgression.progression") } } },
then: true, else: false
}
}
}
}
}
},
{
$addFields: {
"isMatched": { $anyElementTrue: ["$tempMapResults"] }
}
},
{ $match: { isMatched: true } },
{ $project: { title: 1, "parts.part": 1, "parts.progressions.progression": 1 } }
]);
But it didn't work - as I understand it, because the $elemMatch can be used only in the $match stage.
Anyway, I guess I overcomplicated the aggregation pipeline, so I will be glad if you can fix my aggregation pipeline/offer a better working one.
This is not a simple case as these are both nested arrays and we need to match both the part and the progressions, which are not on the same level
One option looks complicated a bit, but keeps your data small:
In order to make things easier, $set a new array field called matchCond which includes an array called progs containing the parts.progressions. To each sub-object inside it insert the matching progressions input array. We do need to be careful here and handle the case where there is no matching progressions input arrayprogressions input array, as this is the case for the "bridge" part on the second document.
Now we just need to check if for any of these progs items, the progression field is matching one option in input array. This is done using $filter, and $rediceing the number of results.
Just match document which have results and format the answer
db.collection.aggregate([
{
$set: {
matchCond: {
$map: {
input: "$parts",
as: "parts",
in: {progs: {
$map: {
input: "$$parts.progressions",
in: {$mergeObjects: [
"$$this",
{input: {progressions: []}},
{input: {$first: {
$filter: {
input: inputData,
as: "inputPart",
cond: {$eq: ["$$inputPart.part", "$$parts.part"]}
}
}}}
]}
}
}}
}
}
}
},
{$set: {
matchCond: {
$reduce: {
input: "$matchCond",
initialValue: 0,
in: {$add: [
"$$value",
{$size: {
$filter: {
input: "$$this.progs",
as: "part",
cond: {$in: ["$$part.progression", "$$part.input.progressions"]}
}
}
}
]
}
}
}
}
},
{$match: {matchCond: {$gt: 0}}},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example
Another option is to use $unwind, which looks simple, but will duplicate your data, thus, likely to be slower:
db.collection.aggregate([
{$addFields: {inputData: inputData, cond: "$parts"}},
{$unwind: "$cond"},
{$unwind: "$cond.progressions"},
{$unwind: "$inputData"},
{$match: {
$expr: {
$and: [
{$eq: ["$cond.part", "$inputData.part"]},
{$in: ["$cond.progressions.progression", "$inputData.progressions"]}
]
}
}
},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example - unwind
There are several options between these two...

MongoDB: aggregation, array of objects to string value

My question is pretty similar to: MongoDB Aggregation join array of strings to single string, but instead of pure Array, like: ['Batman', 'Robin'] I have Array of objects:
_id: 1,
field_value: [
{
_id: 2,
name: "Batman"
},
{
_id: 3,
name: "Robin"
}
]
I am trying to use $reduce but got error instead.
I want to receive the following result:
_id: 1,
field_value: "Batman, Robin" /** <= String value */
or at least array of property values:
_id: 1,
field_value: ["Batman", "Robin"] /** <= Array of strings (name property) */
My MongoPlayground data example
You need the same approach with $reduce, $$this represents a single field_value entity so you need $$this.name:
db.collection.aggregate([
{
$project: {
field_value: {
$reduce: {
input: "$field_value",
initialValue: "",
in: {
$concat: [
"$$value",
{ $cond: [ { $eq: [ "$$value", "" ] }, "", "," ] },
{ $toString: "$$this.name" }
]
}
}
}
}
}
])
Mongo Playground

Value added to array when using $project

I am writing an aggregation pipeline to return a win ratio. When I use $sum the value is output from $facet $project within an array. This has me confused. To solve the issue I simply run $sum on the arrays when I calculate the winRatio, which works fine. How do I use $project without it adding values into an array?
Round.aggregate([
{
$match: {
$and: query,
},
},
{
$facet: {
wins: [
{
$match: {
winner: user,
},
},
{
$group: {
_id: { user: '$scores.player', game: '$game' },
value: { $sum: 1 }, // value *not* within array
},
},
],
rounds: [
{
$unwind: '$scores',
},
{
$match: {
'scores.player': user,
},
},
{
$group: {
_id: { user: '$scores.player', game: '$game' },
value: { $sum: 1 }, // value *not* within array
},
},
],
},
},
{
$project: {
_id: '$rounds._id',
rounds: '$rounds.value', // value within an array
wins: '$wins.value', // value within an array
winRatio: { ... },
},
},
]);
Schema:
const schema = new mongoose.Schema(
{
game: { type: mongoose.Schema.ObjectId, required: true },
scores: [
{
player: { type: mongoose.Schema.ObjectId, ref: 'User', required: true },
playerName: { type: String }, // denormalise
score: { type: Number, required: true },
},
],
winner: { type: mongoose.Schema.ObjectId, required: true },
datePlayed: { type: Date },
},
{ timestamps: true },
);
Your asking why $sum 'works' and $project dosent.
Lets start off by understand the output of the $facet phase.
{
"wins" : [
{
"_id" : {
"user" : [
"player1",
"player2"
],
"game" : 1.0
},
"value" : 2.0
}
],
"rounds" : [
{
"_id" : {
"user" : "player1",
"game" : 1.0
},
"value" : 3.0
}
]
}
As we can see each document result is an array, even though you grouped at the end, imagine each result as its own aggregation, that return value is always an array (either empty or not depending on results).
so when you $project on $rounds.value you're telling mongo to keep the value field for each of the results in the array. in our case its only one but still.
$sum on the other hand is an accumulative operator, from the docs:
With a single expression as its operand, if the expression resolves to an array, $sum traverses into the array to operate on the numerical elements of the array to return a single value.
a quick fix to your 'issue' is just to add $sum while projecting:
{
$project: {
_id: '$rounds._id',
rounds: {$sum: '$rounds.value'},
wins: {$sum: '$wins.value'},
winRatio: { ... },
},
},

mongodb / mongoose - sort by two fields

I have two fields:
date (ISODate)
value (it's a number - however can be also undefined)
I want to sort them in a specific way.
Firstly - sort by value. If it exists (not undefined), let them come first. If value is undefined, push these objects at the end.
Secondly - sort by date. So at this moment, we will have everything sorted by value. But now I want to sort it by date.
Expected result:
[
{
value: 1,
date: today, // ofc it's a date but just to represent the case
},
{
value: 2,
date: yesterday,
},
{
value: undefined,
date: today,
},
{
value: undefined,
date: yesterday,
}
]
Current solution:
.sort({
value: -1,
date: -1
});
However it fails in some situations, unfortunately Im unable to detect why it sometimes sort it in a wrong way. But it does...
Thank you in advance
To achieving this you can use aggregate with $cond.
db.getCollection('collectionName').aggregate([
{
$project :
{
"id" : 1,
"value" : 1,
"date": 1,
sortValue: {
$cond: {
if: { $eq: ['$value',undefined ]},
then: 0,
else: 1,
},
}
}
},
{
$sort :
{
"sortValue" :-1,
"value" : 1,
"date" : -1
}
}])
Tested in Mongo GUI.
Aggregate In Mongoose:
Model.aggregate([
{
$project:
{
"id": 1,
"value": 1,
"date": 1,
sortValue: {
$cond: {
if: { $eq: ['$value', undefined] },
then: 0,
else: 1,
},
}
}
},
{
$sort:
{
"sortValue": -1,
"value": 1,
"date": -1
}
}
], function (err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});

Categories

Resources