Get a single sub-object from a nested object array with MongoDB - javascript

I have a node.js server using express and mongoose and I also have a Json structure of :
[
{
"_id": "63dbaf5daabee478202ae59f",
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766",
"experimentData": [
{
"scanId": 1652890241,
"scanData": [
{
"areaName": "A1",
"areaData": [],
"_id": "63dbaf94aabee478202ae5a5"
},
...
],
"_id": "63dbaf6caabee478202ae5a3"
},
...
],
"__v": 2
},
...
]
How can I create a query to return a single object from the scanData array like
{
"areaName": "A1",
"areaData": [],
"_id": "63dbb006e322869df811eea4"
}
The best I was able to do was:
// * Get all shapes in a well/area
// ! uses the experiment id, scan id and areaName
router.get("/:id/scan/:scanId/area/:areaName", async (req, res) => {
try {
const experiment = await Experiment.findOne({
experimentId: req.params.id,
experimentData: {
$elemMatch: {
scanId: req.params.scanId,
scanData: {
$elemMatch: {
areaName: req.params.areaName
}
}
}
}
}, {'experimentData.scanData.$': 1})
console.log(experiment)
if (!experiment || experiment.length === 0) res.status(404).json({})
else {
res.send(experiment.experimentData[0])
}
} catch (err) {
res.status(500).json({ message: err.message })
}
})
But that just returned the scanData array it would be great if I could go one level deeper and just get the object with the areaName.
I also tried some solutions with $aggregate but was not able to get any data displayed it kept returning an empty array

You can $match by your criteria layer-by-layer and $unwind to get the final scanData object in an aggregation pipeline. Use $replaceRoot to get only the scanData object.
db.collection.aggregate([
{
"$match": {
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766"
}
},
{
"$unwind": "$experimentData"
},
{
"$match": {
"experimentData.scanId": 1652890241
}
},
{
"$unwind": "$experimentData.scanData"
},
{
"$match": {
"experimentData.scanData.areaName": "A1"
}
},
{
"$replaceRoot": {
"newRoot": "$experimentData.scanData"
}
}
])
Mongo Playground

Related

Mongoose get objects which match an object item from an array of objects

I would like to find a single document matching the courseID but inside the document only objects from the materials array whose moduleNo matches the one I give. But the query I currently use seems to return the correct document but also returns all the objects in materials array. Any help would be appreciated.
My schema,
const materialSchema = new mongoose.Schema({
courseID: String,
materials: [
{
moduleNo: Number,
moduleMaterial: String,
},
],
});
My code/query,
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.findOne(
{ courseID, "materials.moduleNo": moduleNo },
(err, result) => {
if (err) {
console.error(err);
} else {
res.json(result);
}
}
);
};
Use the $elemMatch operator, something lik this:
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.findOne(
{ courseID },
{"materials": { $elemMatch: {moduleNo: moduleNo}},
(err, result) => {
if (err) {
console.error(err);
} else {
res.json(result);
}
}
);
};
Update: To return all matching elements in the array, you will have to use an aggregation pipeline, having $filter stage, to filter out array elements. Like this:
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.aggregate(
[
{
$match: {
courseID: courseID
}
},
{
"$project": {
courseID: 1,
materials: {
"$filter": {
"input": "$materials",
"as": "material",
"cond": {
"$eq": [
"$$material.moduleNo",
moduleNo
]
}
}
}
}
}
]
);
};
Here's the playground link.
Way 1 : Use $elemMatch operator in project field
The $elemMatch operator limits the contents of an array field from
the query results to contain only the first element matching the
$elemMatch condition
Result : Returns only one matching array element
syntax :
db.collection.find(query,projection)
db.collection.find({
"field": field_value
},{
"array_name":{
$elemMatch:{"key_name": value }
},
field:1,
field_2:1,
field_3:0
})
https://mongoplayground.net/p/HKT1Gop32Pq
Way 2 : Array Field Limitations array.$ in project field
*
Result : Returns only one matching array element
db.collection.find({
"field": field_value,
"array_name.key_name": value
},{
"array_name.$":1,
field:1,
field_2:1,
field_3:0
});
https://mongoplayground.net/p/Db0azCakQg9
Update : Using MongoDB Aggregation
Result : Returns multiple matching array elements
db.collection.aggregate([
{
"$unwind": "$materials"
},
{
"$match": {
"courseID": "Demo",
"materials.moduleNo": 1
}
}
])
will return output as :
[
{
"_id": ObjectId("5a934e000102030405000000"),
"courseID": "Demo",
"materials": {
"moduleMaterial": "A",
"moduleNo": 1
}
},
{
"_id": ObjectId("5a934e000102030405000000"),
"courseID": "Demo",
"materials": {
"moduleMaterial": "B",
"moduleNo": 1
}
}
]
And If you want to format output :
db.collection.aggregate([
{
"$unwind": "$materials"
},
{
"$match": {
"courseID": "Demo",
"materials.moduleNo": 1
}
},
{
"$group": {
"_id": {
"courseID": "$courseID",
"moduleNo": "$materials.moduleNo"
},
"materials": {
"$push": "$materials.moduleMaterial"
}
}
},
{
"$project": {
"_id": 0,
"courseID": "$_id.courseID",
"moduleNo": "$_id.moduleNo",
"materials": "$materials"
}
}
])
will return result as :
[
{
"courseID": "Demo",
"materials": [
"A",
"B"
],
"moduleNo": 1
}
]
https://mongoplayground.net/p/vdPVbce6WkX

how to use lookup on array object mongodb

I'm new on mongodb. so I try design the schema for my collection is like below
all the ObjectId is not real
stockIn documents
{
serial:"stk0001",
date:'2021-06-11',
productInTransation:[
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb2"
qty:2
},
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb1",
qty:2
}
]
}
and I have a products collection
[
{
_id:"60ae220b066b8d9861118cb5",
name:"sepatu"
},
{
_id:"60ae220b066b8d9861118cb4",
name:"sendal"
}
]
so what I expect from those documents is just like below
{
serial:"stk0001",
date:'2021-06-11',
productInTransation:[
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb2"
qty:2,
product:
{
_id:"60ae220b066b8d9861118cb5",
name:"sepatu"
},
},
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb1",
qty:2,
product:
{
_id:"60ae220b066b8d9861118cb4",
name:"sendal"
}
}
]
}
this collection is just simplified from the real case.
and the problem I don't know how to do a query on mongodb, so the output will same as the expected. thank's for any help
You can use $lookup
$unwind to deconstruct the array
$lookup to join collections
$ifNull to make sure this doesn't give any NPE when we take from first element from the joined array using $arrayElemAt
$group to reconstruct the array
Here is the code
db.stockIn.aggregate([
{ $unwind: "$productInTransation" },
{
"$lookup": {
"from": "products",
"localField": "productInTransation.productId",
"foreignField": "_id",
"as": "productInTransation.product"
}
},
{
"$addFields": {
"productInTransation.product": {
"$ifNull": [ { "$arrayElemAt": [ "$productInTransation.product", 0 ] }, [] ]
}
}
},
{
"$group": {
"_id": "$_id",
"date": { "$first": "$date" },
"serial": { "$first": "$serial" },
"productInTransation": { $push: "$productInTransation" }
}
}
])
Working Mongo playground

Destructure arrays within the MongoDB aggregation pipeline

I was wondering if it was possible to destructure arrays while I am still in the MongoDB aggregation pipeline which would make my code alot neater.
For example, I have the following aggregation pipeline.
await User.aggregate([
{ $match: { _id: userID } },
{
$project: { chatLogs: 1, username: 1, profilePicURL: 1 },
},
{ $unwind: "$chatLogs" },
{
$lookup: {
from: "users",
let: { recipientID: "$chatLogs.recipientID" },
pipeline: [
{
$match: { $expr: { $eq: ["$_id", "$$recipientID"] } },
},
{ $project: { profilePicURL: 1 } },
],
as: "chatLogs.recipientID",
},
},
]);
This gives the following results when queried:
{
"_id": "5f2ffb54eea9c2180a732afa",
"username": "joe",
"profilePicURL": "/images/profile/default_profile.png",
"chatLogs": {
"recipientID": [
{
"_id": "5f2faf5ad18a76073729f475",
"profilePicURL": "/images/profile/default_profile.png"
}
],
"chat": "5f30b6c3d117441c2abda1ba"
}
}
In my case, because "recipientID" represents a default MongoDB id, it will always be unique. Hence I would prefer the following, where the resulting recipientID field is no longer a meaningless array
Desired results:
{
"_id": "5f2ffb54eea9c2180a732afa",
"username": "joe",
"profilePicURL": "/images/profile/default_profile.png",
"chatLogs": {
"recipientID": {
"_id": "5f2faf5ad18a76073729f475",
"profilePicURL": "/images/profile/default_profile.png"
}
"chat": "5f30b6c3d117441c2abda1ba"
}
}
You can deconstruct recipientID array using $unwind in last pipeline,
await User.aggregate([
... // your all pipelines
// add this line
{ $unwind: "$chatLogs.recipientID" }
]);

Aggregate match pipeline not equal to in MongoDB

I am working on an aggregate pipeline for MongoDB, and I am trying to retrieve items where the user is not equal to a variable.
For some reason, I couldn't make it work. I tried to use $not, $ne and $nin in different possible way but can't make it to work.
This is how it looks like:
Data sample:
[{
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "565674832b85ce78732b7529" }
}, {
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "565674832b85ce78732b7529" }
}, {
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "56f9dfc5cc03ec883f7675d0" }
}]
Pipeline sample (simplified for this question):
Where req.query.user.id = "565674832b85ce78732b7529"
collection.aggregate([
{
$match: {
user: {
$nin: [ req.query.user.id ],
}
}
}
]
This should return only the last item.
Do you have any idea how to retrieve the data that doesn't match the user?
Thanks
Edit:
The following doesn't work either:
collection.aggregate([
{
$match: {
'user.$oid': {
$nin: [ req.query.user.id ],
}
}
}
]);
I also tried with ObjectID() and mongodb complains: [MongoError: Argument must be a string]
var ObjectID = require('mongodb').ObjectID;
// Waterline syntax here
MyCollection.native(function (err, collection) {
collection.aggregate([
{
$match: {
'user': {
$nin: [ ObjectID(req.query.user.id) ],
}
}
}
], function (err, result) {
console.log(err, result);
});
});
But this line works in the shell:
db.collection.aggregate([{$match:{"user":{"$nin":[ObjectId("565674832b85ce78732b7529")]}}}])
Based on the answer here, you can change
var ObjectId = require('mongodb'). ObjectID;
to
var ObjectId = require('sails-mongo/node_modules/mongodb').ObjectID;

Mongodb find() only include non-empty arrays

What I'm trying to achieve with a find query is to only include "someArray"s if it's inner array is not empty. For example the JSON below:
{
"document": "some document",
"someArray": [
{
"innerArray": [
"not empty"
]
},
{
"innerArray": [
[] //empty
]
}
]
}
Would return this:
{
"document": "some document",
"someArray": [
{
"innerArray": [
"not empty"
]
}
]
}
I'm using the following find:
Visit.find({'someArray.innerArray.0': {$exists: true}}, function(err, data){});
However, this returns all data.
Have also tried:
Visit.find({}, {'someArray.innerArray': {$gt: 0}}, function(err, data) {});
But this returns nothing
Any ideas on how to approach this?
Cheers
The general case here to check for a non-empty array is to check to see if the "first" element actually exists. For single matches you can project with the positional $ operator:
Vist.find(
{ "someArray.innerArray.0": { "$exists": true } },
{ "document": 1,"someArray.$": 1},
function(err,data) {
}
);
If you need more than a single match or have arrays nested more deeply than this, then the aggregation framework is what you need to handle the harder projection and/or "filter" the array results for more than one match:
Visit.aggregate(
[
// Match documents that "contain" the match
{ "$match": {
"someArray.innerArray.0": { "$exists": true }
}},
// Unwind the array documents
{ "$unwind": "$someArray" },
// Match the array documents
{ "$match": {
"someArray.innerArray.0": { "$exists": true }
}},
// Group back to form
{ "$group": {
"_id": "$_id",
"document": { "$first": "$document" },
"someArray": { "$push": "$someArray" }
}}
],function(err,data) {
}
)
Worth noting here that you are calling this "empty" but in fact is is not, as it actually contains another empty array. You probably don't want to do that with real data, but if you have then you would need to filter like this:
Visit.aggregate(
[
{ "$match": {
"someArray": { "$elemMatch": { "innerArray.0": { "$ne": [] } } }
}},
{ "$unwind": "$someArray" },
{ "$match": {
"someArray.innerArray.0": { "$ne": [] }
}},
{ "$group": {
"_id": "$_id",
"document": { "$first": "$document" },
"someArray": { "$push": "$someArray" }
}}
],function(err,data) {
}
);

Categories

Resources