MongoDB lookup and map 2 arrays of result - javascript

There are 2 array fields after I looked up in MongoDB aggregation pipeline.
the first one
[
{
"colorId": "60828a1b216b0972da695f2a",
"name": "Exellent",
"description": "Great work"
}
]
and the second one
[
{
"_id": "60828a1b216b0972da695f2a",
"colorName": "Green",
"hexColorCodes": "#2D9D78",
"sequence": 1,
"isActivated": true,
"created_at": "2021-04-23T08:49:31.729Z",
"updated_at": "2021-04-23T08:49:31.729Z",
"__v": 0,
"isDefault": true
}
]
the result I want is
[
{
"colorId": "60828a1b216b0972da695f2a",
"name": "Exellent",
"description": "Great work",
"colorName": "Green",
"hexColorCodes": "#2D9D78"
}
]
then I want to map colorName and hexColorCodes to the first array. Here is my aggregate pipeline
db.collection.aggregate([
{
$lookup: {
from: "color_tags",
localField: "colors.colorId",
foreignField: "_id",
as: "tempColors",
},
},
{
$addFields: {
stages3: {
$map: {
input: "$colors",
in: {
$mergeObjects: [
"$$this",
{
$arrayElemAt: [
"$tempColors",
{
$indexOfArray: [
"$tempColors._id",
"$$this.colors.colorId",
],
},
],
},
],
},
},
},
},
}
])
but the result is not what I expected. It mapped with incorrect id. Please suggest.

$map to iterate loop of first array
$filter to iterate loop of second array and match colorId with _id and return matching result
$arrayElemAt to get first matching element
$mergeObjects to merge current object with return result from second array
{
$project: {
first: {
$map: {
input: "$first",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
$arrayElemAt: [
{
$filter: {
input: "$second",
cond: { $eq: ["$$this._id", "$$f.colorId"] }
}
},
0
]
}
]
}
}
}
}
}
If you want to result specific fields then add a $project stage at the end,
{
$project: {
"first.colorId": 1,
"first.name": 1,
"first.description": 1,
"first.colorName": 1,
"first.hexColorCodes": 1
}
}
Playground

Related

Mongoose/MongoDB: How can I $inc only the first value I get from an array?

I have a Mongoose Schema that looks like this:
{
_id: ObjectID,
storage: [{
location: String,
storedFood: [{
code: String,
name: String,
weight: Number
}]
}]
}
And for example in storedFood can be the same Food twice. But I only want to update one of the weights of these items.
This is my code to $inc all of the items.... How can I reduce this to only one?
try {
const deletedFoodFromStorage = await User.updateOne(
{_id: user, "storage.location": location},
{ $inc: {"storage.$.storedFood.$[food].weight": -weight}},
{ arrayFilters: [ { "food.code": code } ]},
);
res.json(deletedFoodFromStorage);
} catch(err) {
res.status(400).json('Error: ' + err)
}
Should have been a simple one. Only way I found at the moment is not simple:
db.collection.update(
{_id: user, "storage.location": location},
[
{$set: {
newItem: {
$reduce: {
input: {$getField: {
input: {$first: {$filter: {
input: "$storage",
as: "st",
cond: {$eq: ["$$st.location", location]}
}}},
field: "storedFood"
}},
initialValue: [],
in: {$concatArrays: [
"$$value",
{$cond: [
{$and: [
{$eq: ["$$this.code", code]},
{$not: {$in: [code, "$$value.code"]}}
]},
[{$mergeObjects: [
"$$this",
{weight: {$subtract: ["$$this.weight", weight]}}
]}],
["$$this"]
]
}
]
}
}
}
}},
{$set: {
storage: {
$map: {
input: "$storage",
in: {$cond: [
{$eq: ["$$this.location", location]},
{$mergeObjects: ["$$this", {storedFood: "$newItem"}]},
"$$this"
]}
}
},
newItem: "$$REMOVE"
}}
])
See how it works on the playground example
Borrowing liberally from nimrod serok's answer, here's one way to do it with a single pass through all the arrays. I suspect this can be improved, at least for clarity.
db.collection.update({
_id: user,
"storage.location": location
},
[
{
"$set": {
"storage": {
"$map": {
"input": "$storage",
"as": "store",
"in": {
"$cond": [
{"$ne": ["$$store.location", location]},
"$$store",
{
"$mergeObjects": [
"$$store",
{
"storedFood": {
"$getField": {
"field": "theFoods",
"input": {
"$reduce": {
"input": "$$store.storedFood",
"initialValue": {
"incOne": false,
"theFoods": []
},
"in": {
"$cond": [
"$$value.incOne",
{
"incOne": "$$value.incOne",
"theFoods": {
"$concatArrays": [
"$$value.theFoods",
["$$this"]
]
}
},
{
"$cond": [
{"$ne": ["$$this.code", code]},
{
"incOne": "$$value.incOne",
"theFoods": {
"$concatArrays": [
"$$value.theFoods",
["$$this"]
]
}
},
{
"incOne": true,
"theFoods": {
"$concatArrays": [
"$$value.theFoods",
[
{
"$mergeObjects": [
"$$this",
{"weight": {"$add": ["$$this.weight", -weight]}}
]
}
]
]
}
}
]
}
]
}
}
}
}
}
}
]
}
]
}
}
}
}
}
])
Try it on mongoplayground.net.
Ty for your help!
The final solution for me was to restructure addStoredFood-Function to make the storedFood unique and only add weight to it instead of adding another objects.
This makes my old update-Function work aswell.

How to get object value with dynamic key in $project in mongoDB

I want to retrieve a value from an object with a dynamic key
[
{
"_id": 1,
"item": "sweatshirt",
"price": {
"INR": 45.99
},
"currency": 'INR'
}
]
db.collection.aggregate([
{
"$project": {
"pricenew": "$price.currency"
}
}
])
If I do price.INR it will work fine but here I have currency dynamic, so I want something like price.currency but here currency is coming like "INR" and it gives no data.
I really appreciate any help you can provide.
You need to convert the price object to an array using $objectToArray, filter it and then convert it back, like so:
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
"$mergeObjects": [
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
"$objectToArray": "$price"
},
cond: {
$eq: [
"$$this.k",
"$currency"
]
}
}
},
in: {
k: "pricenew",
v: "$$this.v"
}
}
}
},
{
_id: "$_id"
}
]
}
}
}
])
Mongo Playground

How do I get all attributes which are numeric types in mongo db?

I need to extract all the attributes which are of numeric types. For example, if the different attributes are
{
age: 32
gender: "female"
year: 2020
name: "Abc"
}
My query should return ["age","year"]
I think the below query should help you out.
db.test.aggregate([
// Remove this `$limit` stage if your Collection schema is dynamic and you want to process all the documents instead of just one
{
"$limit": 1
},
{
"$project": {
"arrayofkeyvalue": {
"$filter": {
"input": {"$objectToArray":"$$ROOT"},
"as": "keyValPairs",
"cond": {
"$in": [{"$type": "$$keyValPairs.v"}, ["double", "int", "long"]],
// Change the above line to the following to get only `int` keys instead of `int, double` and `long`:
// "$eq": [{"$type": "$$keyValPairs.v"}, "int"],
}
}
}
}
},
{
"$group": {
"_id": null,
"unique": {"$addToSet": "$arrayofkeyvalue.k"}
}
},
{
"$project": {
"_id": 0,
"intKeyNames": {
"$reduce": {
input: "$unique",
initialValue: [],
in: {$setUnion : ["$$value", "$$this"]}
}
}
}
},
])
The above query result will be something like this:
{
"intKeyNames" : [
"_id",
"abc",
"paymentMonth",
"paymentYear",
"value"
]
}

MongoDB aggregation sum according to document value

I want to create a mongo db view from two collections with a new value that is a sum of values from one of the collection according to an operation from another collection.
Below is the structure:
/* First collection */
{
"product": "test",
"labels" : [
{"code": "label1", "value": 42},
{"code": "label2", "value": 50}
]
}
/* Second collection */
{
"code": "label3",
"calculation" : [
{"label" : "label1", "operation":"+"},
{"label" : "label2", "operation":"-"}
]
}
In my aggregated collection i want a new field that would be label1 - label2.
{
"product" : "test",
"labels" : [
{"code": "label1", "value": 42},
{"code": "label2", "value": 50}
],
"vlabels" : [
{"code": "label3", "value": -8}
]
}
Although it is possible. I doubt it would be optimal, if you don't need further processing on the database, I suggest you do this at the application layer.
However, I have attempted to do this as an exercise. This approach would check only for the "-" operator and assign a negative value, other operator will use the existing value.
/* First collection: "products" */
/* Second collection: "vlabels" */
db.products.aggregate([
{
$lookup: {
from: "vlabels", // lookup calculation from vlabels
let: {
labels: "$labels"
},
pipeline: [
{
$set: {
calculation: {
$map: {
input: "$calculation", // map over calculation in vlabels
as: "calc",
in: {
operation: "$$calc.operation",
product: {
$arrayElemAt: [
{
$filter: {
input: "$$labels", // filter for matching product labels and get the first element using $arrayAlemAt to get the value
as: "label",
cond: {
$eq: ["$$calc.label", "$$label.code"]
}
}
},
0
]
}
}
}
}
}
},
{
$project: {
_id: false,
code: "$code",
value: {
$reduce: { // reducing by adding all values in calculation array, use negative value on "-" operator
input: "$calculation",
initialValue: 0,
in: {
$add: [
"$$value",
{
$cond: [
{
$eq: ["-", "$$this.operation"]
},
{
$multiply: [
-1,
{ $ifNull: ["$$this.product.value", 0] }
]
},
{ $ifNull: ["$$this.product.value", 0] }
]
}
]
}
}
}
}
}
],
as: "vlabels"
}
}
])
Mongo Playground

mongodb to return object from facet

Is it possible to have facet to return as an object instead of an array? It seems a bit counter intuitive to need to access result[0].total instead of just result.total
code (using mongoose):
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.exec()
Each field you get using $facet represents separate aggregation pipeline and that's why you always get an array. You can use $addFields to overwrite existing total with single element. To get that first item you can use $arrayElemAt
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.addFields({
"total": {
$arrayElemAt: [ "$total", 0 ]
}
})
.exec()
You can try this as well
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.addFields({
"total": {
"$ifNull": [{ "$arrayElemAt": [ "$total.total", 0 ] }, 0]
}
})
.exec()
imagine that you want to pass the result of $facet to the next stage, let's say $match. well $match accepts an array of documents as input and return an array of documents that matched an expression, if the output of $facet was just an element we can't pass its output to $match because the type of output of $facet is not the same as the type of input of $match ($match is just an example). In my opinion it's better to keep the output of $facet as array to avoid handling those types of situations.
PS : nothing official in what i said

Categories

Resources