MongoDB check if a key exists and has a specific value - javascript

I'm absolute beginner in MongoDB and I have a problem which I can't solve.
I have a Request collection which looks like this:
{ ->document one
id...
Category:[Car]
Color:[Black,White]
Fuel Type:[Diesel, Gasoline]
}
{ ->document two
id..
Category:[Car]
}
I have a Product object in JavaScript which looks like this:
let newObj: {
Category: 'Car',
Model: 'BMW',
'Production age': 1997,
'Fuel Type': 'Diesel',
Color: 'White',
'Mileage (km)': '75213',
Price: '9000',
Electric: 'Yes',
'Mile range': '300'
}
I want to get all documents from Request collection which contains one of the keys and a specific value. To understand better:
In my case the Category=Car and the Color=White so I want to return back both of the documents.
If the Category=Car but the Color=Red I want to return only the second document
If the other fields doesn't exists I don't care about them only about the matched ones.
let findArgs={}
for (const [key, value] of Object.entries(product.newObj)) {
filterArgs[key]={$exists: true,$in:[value]}
}
let result= await requestColl.aggregate([
{$match: filterArgs },
{$sort: {[sortBy]:order}},
]).toArray();
This is the filterArgs query which will be in {match:}
{
Category: { '$exists': true, '$in': [ 'Car' ] },
Model: { '$exists': true, '$in': [ 'BMW' ] },
'Production age': { '$exists': true, '$in': [ 1997 ] },
'Fuel Type': { '$exists': true, '$in': [ 'Diesel' ] },
Color: { '$exists': true, '$in': [ 'White' ] },
'Mileage (km)': { '$exists': true, '$in': [ '75213' ] },
Electric: { '$exists': true, '$in': [ 'Yes' ] },
'Mile range': { '$exists': true, '$in': [ '300' ] }
}
How can I acheive the above mentioned? Thanks in advance.
UPDATE
I tried to do a query like this but still due to that Car exists in both documents I can't get the specific one.
{ $or:
[ { $and: [{ Category: { '$in': [ 'Car' ]}}]},
{ $and: [ { Category: { '$in': [ 'Car' ]}}, { Color: { '$in': [ 'White' ] }}]}
]}

Related

Nested array mapping and filtering in javascript

I am designing a system and I have some bottlenecks.
I have user array such like that:
const users = [
{
name: "Jack",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
},
{
name: "Joe",
workspaces: [
{
_id: "71216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE"],
},
],
},
];
And I have activeWorkspace object such like that:
const activeWorkspace = {
name: "W1",
_id: "61216512315615645jbk",
};
I need to filter the objects in the users array whose workspace _id is equal to activeWorkspace _id.
Output must be like that:
{
name: "Jack",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
}
How can I do that?
In addition:
If we want to return an array, not an object, how should we do it? Like that:
[{
name: "Jack",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
}]
Thanks
If there is only one match. You need to use find(). Inside of the find method, you want to use some() to look for an _id match.
const users = [
{
name: "Jack",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
},
{
name: "Joe",
workspaces: [
{
_id: "CHANGED_ID",
permissions: ["CAN_DELETE_WORKSPACE"],
},
],
},
];
const activeWorkspace = {
name: "W1",
_id: "61216512315615645jbk",
};
const active = users.find(function (user) {
return user.workspaces.some( function (workspace) {
return workspace._id === activeWorkspace._id;
});
});
console.log(active);
// Same thing as above, just done with a modern approach
const active2 = users.find(({workspaces}) => workspaces.some(({_id}) => _id === activeWorkspace._id));
console.log(active2);
Now if there could be more than one match (your orginal code before the typo, you would use filter() and some() to find all users that have the workspace in their array.
const users = [
{
name: "Jack",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
},
{
name: "Joe",
workspaces: [
{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE"],
},
],
},
];
const activeWorkspace = {
name: "W1",
_id: "61216512315615645jbk",
};
const active = users.filter(function (user) {
return user.workspaces.some( function (workspace) {
return workspace._id === activeWorkspace._id;
});
});
console.log(active);
// Same thing as above, just done with a modern approach
const active2 = users.filter(({workspaces}) => workspaces.some(({_id}) => _id === activeWorkspace._id));
console.log(active2);
I adjusted the provided data from Joe so he doesn't have permissions
const users = [{
name: "Jack",
workspaces: [{
_id: "61216512315615645jbk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
{
_id: "41ss16512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE", "CAN_EDIT_PROJECT"],
},
],
},
{
name: "Joe",
workspaces: [{
_id: "61216512315615645bk",
permissions: ["CAN_DELETE_WORKSPACE"],
}, ],
},
];
const activeWorkspace = {
name: "W1",
_id: "61216512315615645jbk",
};
function findPermittedUser() {
return users.filter(user => {
let hasPermission = false
user.workspaces.forEach(workspace => {
if (activeWorkspace._id == workspace._id) {
hasPermission = true
}
})
return hasPermission
})
}
console.log(findPermittedUser())
You can use map and filter to "filter" out the unwanted ids from the users object. Something like :
const users = [
{
"name": "Jack",
"workspaces": [
{
"_id": "61216512315615645jbk",
"permissions": [
"CAN_DELETE_WORKSPACE",
"CAN_EDIT_PROJECT"
]
},
{
"_id": "41ss16512315615645bk",
"permissions": [
"CAN_DELETE_WORKSPACE",
"CAN_EDIT_PROJECT"
]
}
]
},
{
"name": "Joe",
"workspaces": [
{
"_id": "61216512315615645jbk",
"permissions": [
"CAN_DELETE_WORKSPACE"
]
}
]
}
]
const activeWorkspace = {
name: "W1",
_id: "61216512315615645jbk",
};
const filteredUsers = users.map(item => ({
name : item.name,
workspaces: item.workspaces.filter(user => user._id === activeWorkspace._id)}
));
console.log(filteredUsers);
This should work (tested):
const filteredUsers = users.filter(
user => user.workspaces.reduce(
(acc, workspace) => acc || workspace._id === activeWorkspace._id, false)
)
)
Explanation:
We are using filter and reduce as evident from the code. What the code is doing is pretty simple, first, we want to apply filter on the user array. Now in the filter, we need to define the logic, which should return true whenever our condition happens to be true.
Since we have an array of workspaces, we need to iterate over all of them to check if our activeWorkspace._id exists in any of them. For this, you can use a for loop and return true when you find it, else return false if not. But the functional way of doing it would be to use reduce and initialize the accumulator with false. Every time you access a workspace, you return acc || <our condition>. Notice how if even once our condition returns true, the accumulator becomes true for the rest of the execution of reduce. This is slightly poor in performance since you are not exiting as soon as you have found your workspace._id as you would have done in case of a for loop.
users.map(u => u.workspaces).flat().filter(w => w._id === activeWorkspaceId);
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat

converting the datestring in a nested array of objects (MongoDB)

What I want to achieve is finding a specific document on that current month based on the provided date. The date is stored as a string, in order for me to compare the date I need to convert the date first. However I have trouble on converting the datestring in a nested array of objects.
My collections:
{
sections: [{
fields: [{
name: 'Date',
value: '2020-11-30T15:59:59.999Z' // this is string
},
{
name: 'Title',
value: 'My book'
},
{
name: 'Author',
value: 'Henry'
}
]
]
}
}
What I have tried:
1)
const existingReport = await Report.find({
$expr: {
$gte: [
{
$dateFromString: {
dateString: "$sections.field[0].value",
},
},
moment(payload.forPeriod).startOf("month").toDate(),
],
$lt: [
{
$dateFromString: {
dateString: "$sections.field[0].value",
},
},
moment(payload.forPeriod).endOf("month").toDate(),
],
},
});
const existingReport1 = await Report.aggregate([
{
$addFields: {
formattedData: {
$cond: {
if: {
$eq: ["$sections.field.value", "Date"],
},
then: {
$dateFromString: {
dateString: "$sections.field.value",
},
},
else: "$sections.field.value",
},
},
},
},
]);
You can simply do a $toDate with the help of 2 $reduce to iterate the sections and fields array.
db.collection.aggregate([
{
"$match": {
$expr: {
$eq: [
true,
{
"$reduce": {
"input": "$sections",
"initialValue": false,
"in": {
"$reduce": {
"input": "$$this.fields",
"initialValue": false,
"in": {
$or: [
"$$value",
{
$and: [
{
$gte: [
{
"$toDate": "$$this.value"
},
new Date("2020-11-01")
]
},
{
$lte: [
{
"$toDate": "$$this.value"
},
new Date("2020-11-30")
]
}
]
}
]
}
}
}
}
}
]
}
}
}
])
Here is the Mongo playground for your reference.

how to update array of objects if it matches while recursively search in JavaScript

I would like to update the object with values after a recursive search finds a particular node.
Where do I need to add logic to achieve this?
I would like to get the first found object from the nested array of objects and update the data with selected:true based on the iterations to get the value showTree: true.
Function:
let findDeep = function(data, label) {
return data.filter(function(e) {
if (e.label.includes(label)) {
data.map(el=> el.selected= "true"); // logic to select the first found value
return e;
}
else if (e.item)
//logic for showTree: true
return findDeep(e.item, label);
});
};
Data:
let testData = [
{
id: 1,
label: 'parent1',
item: [
{
id: 21,
label: 'child1',
item: [
{
id: 211,
label: 'child31',
item: [
{
id: 2111,
label: 'child2211',
item: [
{
id: 21111,
label: 'child22111'
}
]
}
]
},
{
id: 222,
label: 'child32'
}
]
},
{
id: 22,
label: 'child2',
item: [
{
id: 221,
label: 'child421',
item: [
{
id: 2211,
label: 'child2211'
}
]
},
{
id: 222,
label: 'child222'
}
]
}
]
},
{
id: 2,
label: 'parent2',
item: [
{
id: 21,
label: 'child2',
item: [
{
id: 511,
label: 'child51',
item: [
{
id: 5111,
label: 'child5211',
item: [
{
id: 51111,
label: 'child52111'
}
]
}
]
},
{
id: 522,
label: 'child352'
}
]
}
]
}
];
I would like to achieve something in the output
console.log(findDeep(testData, 'child3')[0]);
//output list
[
{
"id":1,
"label":"parent1",
"showTree": true,
"item":[
{
"id":21,
"label":"child1",
"showTree": true,
"item":[
{
"id":211,
"label":"child31",
"selected" true,
"item":[
{
"id":2111,
"label":"child2211",
"item":[
{
"id":21111,
"label":"child22111"
}
]
}
]
},
{
"id":222,
"label":"child32"
}
]
},
{
"id":22,
"label":"child2",
"item":[
{
"id":221,
"label":"child421",
"item":[
{
"id":2211,
"label":"child2211"
}
]
},
{
"id":222,
"label":"child222"
}
]
}
]
},
{
"id":2,
"label":"parent2",
"item":[
{
"id":21,
"label":"child2",
"item":[
{
"id":511,
"label":"child51",
"item":[
{
"id":5111,
"label":"child5211",
"item":[
{
"id":51111,
"label":"child52111"
}
]
}
]
},
{
"id":522,
"label":"child352"
}
]
}
]
}
]
//ouptput selected value
{
"id":211,
"label":"child31",
"selected":true,
"item":[
{
"id":2111,
"label":"child2211",
"item":[
{
"id":21111,
"label":"child22111"
}
]
}
]
}
You can do a normal tree search, modify the found property, then pass true back up the tree towards the root, applying showTree: true along the way.
Note that this is an in-place approach, so the semantics are a little different than you show on your call. That's probably most appropriate for an algorithm like this that is just modifying a few properties on an existing structure rather than reallocating the whole thing from scratch. It's an antipattern to return the original structure for in-place algorithms as .sort() and .reverse() do for the purposes of chaining -- this can lead to surprising and subtle bugs.
const expandPath = (nodes, targetLabel) => {
for (const node of nodes || []) {
if (node.label.includes(targetLabel)) {
return node.selected = true;
}
else if (expandPath(node.item, targetLabel)) {
return node.showTree = true;
}
}
};
const testData = [ { id: 1, label: 'parent1', item: [ { id: 21, label: 'child1', item: [ { id: 211, label: 'child31', item: [ { id: 2111, label: 'child2211', item: [ { id: 21111, label: 'child22111' } ] } ] }, { id: 222, label: 'child32' } ] }, { id: 22, label: 'child2', item: [ { id: 221, label: 'child421', item: [ { id: 2211, label: 'child2211' } ] }, { id: 222, label: 'child222' } ] } ] }, { id: 2, label: 'parent2', item: [ { id: 21, label: 'child2', item: [ { id: 511, label: 'child51', item: [ { id: 5111, label: 'child5211', item: [ { id: 51111, label: 'child52111' } ] } ] }, { id: 522, label: 'child352' } ] } ] } ];
expandPath(testData, "child3");
console.log(testData[0]);
Note that the added properties are at the bottom of each node.
Also, in your original attempt, please avoid using map for in-place operations -- its purpose is to allocate a new array, not modify it. Use forEach instead.
I would suggest to use string methods, like
- `includes` or
- `startsWith`
along with the wanted parameter.
search = (array, type, value) => array.some(o => {
if (o.label[type](value)) return o.selected = true;
return search(o.item || [], type, value);
})

Compare two array of objects based on a properties value and return the matched

HI All I am having two array of object my aim is to compare them and filter out the matched result
my data looks like this
let data1 = [
{
name:'tom',
process:'flipkart',
master:'pharma',
profiles: [
{
level:'begginer',
language:'hindi',
role:['flp_admin','flp_teacher']
}
]
},
{
name:'jeo',
process:'amazon',
master:'science',
profiles: [
{
level:'begginer',
language:'english',
role:['amz_admin']
}
]
},
{
name:'jerry',
process:'email',
master:'it',
profiles: [
{
level:'begginer',
language:'urdu',
role:['eml_teacher']
}
]
}
]
let data2 = [
{
masterName:'Pharma',
businessProcess: [
{ label:'flipkart', value:'flipkart' },
{ label:'amazon', value:'amazon' }
]
},
{
masterName:'science',
businessProcess: [
{ label:'flipkart', value:'flipkart' },
{ label:'amazon', value:'amazon' }
]
},
{
masterName:'it',
businessProcess: [
{ label:'email', value:'email' },
{ label:'amazon', value:'amazon' }
]
}
I want to compare data1 with data2 and return the match from data2 if master of data1 matches with masterName of data2 and if business of data1 matches with businessProcess.label of data2.
Could anyone please tell me how can I do it?
You can use Array.filter and Array.find to loop over and find the matching items:
let data1 = [{
name: 'tom',
process: 'flipkart',
master: 'pharma',
profiles: [{
level: 'begginer',
language: 'hindi',
role: ['flp_admin', 'flp_teacher']
}]
},
{
name: 'jeo',
process: 'amazon',
master: 'science',
profiles: [{
level: 'begginer',
language: 'english',
role: ['amz_admin']
}]
},
{
name: 'jerry',
process: 'email',
master: 'it',
profiles: [{
level: 'begginer',
language: 'urdu',
role: ['eml_teacher']
}]
}
]
let data2 = [{
masterName: 'Pharma',
businessProcess: [{
label: 'flipkart',
value: 'flipkart'
},
{
label: 'amazon',
value: 'amazon'
}
]
},
{
masterName: 'science',
businessProcess: [{
label: 'flipkart',
value: 'flipkart'
},
{
label: 'amazon',
value: 'amazon'
}
]
},
{
masterName: 'it',
businessProcess: [{
label: 'email',
value: 'email'
},
{
label: 'amazon',
value: 'amazon'
}
]
}
];
console.log(data1.filter((d) => {
return data2.find((d2) => {
//check if data matername equals data1 master
// or if data1.process value exists in one of the item of businessProcess as value
return d2.masterName == d.master || d2.businessProcess.find(b => b.value === d.process);
});
}));

How to find parents based on child fields in mongo using aggregation?

Here is a code I have:
const _ = require('lodash')
const Box = require('./models/Box')
const boxesToBePicked = await Box.find({ status: 'ready', client: 27 })
const boxesOriginalIds = _(boxesToBePicked).map('original').compact().uniq().value()
const boxesOriginal = boxesOriginalIds.length ? await Box.find({ _id: { $in: boxesOriginalIds } }) : []
const attributes = ['name']
const boxes = [
...boxesOriginal,
...boxesToBePicked.filter(box => !box.original)
].map(box => _.pick(box, attributes))
Let's say, we have following data in "boxes" collection:
[
{ _id: 1, name: 'Original Box #1', status: 'pending' },
{ _id: 2, name: 'Nested box', status: 'ready', original: 1 },
{ _id: 3, name: 'Nested box', status: 'ready', original: 1 },
{ _id: 4, name: 'Nested box', status: 'pending', original: 1 },
{ _id: 5, name: 'Original Box #2', status: 'ready' },
{ _id: 6, name: 'Original Box #3', status: 'pending' },
{ _id: 7, name: 'Nested box', status: 'ready', original: 6 },
{ _id: 8, name: 'Original Box #4', status: 'pending' }
]
Workflow
Find all boxes, which are ready to be picked:
const boxesToBePicked = await Box.find({ status: 'ready' })
// Returns:
[
{ _id: 2, name: 'Nested box', status: 'ready', original: 1 },
{ _id: 3, name: 'Nested box', status: 'ready', original: 1 },
{ _id: 5, name: 'Original Box #2', status: 'ready' },
{ _id: 7, name: 'Nested box', status: 'ready', original: 6 }
]
Get all the IDs of original (parent) boxes of those:
const boxesOriginalIds = _(boxesToBePicked).map('original').compact().uniq().value()
// Returns:
[1, 6]
Get those boxes by their IDs:
const boxesOriginal = boxesOriginalIds.length ? await Box.find({ _id: { $in: boxesOriginalIds } }) : []
// Returns
[
{ _id: 1, name: 'Original Box #1', status: 'pending' },
{ _id: 6, name: 'Original Box #3', status: 'pending' }
]
Join those boxes with not nested boxes to be picked:
const boxes = [
...boxesOriginal,
...boxesToBePicked.filter(box => !box.original)
].map(box => _.pick(box, attributes))
// Returns
[
{ name: 'Original Box #1' },
{ name: 'Original Box #3' },
{ name: 'Original Box #2' }
]
So basically what we are doing here is getting all the original boxes if they have at least one nested box with status "ready", and all not nested boxes with status "ready".
I think it can be simplified by using aggregation pipeline and projection. But how?
You can try something like below. Uses $lookUp to self join to collection and $match stage with $or in combination with $and for second condition and the next part of $or for first condition and $group stage to remove duplicates and $project stage to format the response.
db.boxes.aggregate([{
$lookup: {
from: "boxes",
localField: "original",
foreignField: "_id",
as: "nested_orders"
}
}, {
$unwind: {
path: "$nested_orders",
preserveNullAndEmptyArrays: true
}
}, {
$match: {
$or: [{
$and: [{
"status": "ready"
}, {
"nested_orders": {
$exists: false,
}
}]
}, {
"nested_orders.status": "pending"
}]
}
}, {
$group: {
"_id": null,
"names": {
$addToSet: {
name: "$name",
nested_name: "$nested_orders.name"
}
}
}
}, {
$unwind: "$names"
}, {
$project: {
"_id": 0,
"name": {
$ifNull: ['$names.nested_name', '$names.name']
}
}
}]).pretty();
Sample Response
{ "name" : "Original Box #1" }
{ "name" : "Original Box #2" }
{ "name" : "Original Box #3" }
To decompose the aggregation :
a $group which creates
an array ids which match ready status for which it will add the *original value
an array box_ready which match ready status and keep the other fields as is (it will be used later)
an array document which contain the whole original document ($$ROOT)
{
$group: {
_id: null,
ids: {
$addToSet: {
$cond: [
{ $eq: ["$status", "ready"] },
"$original", null
]
}
},
box_ready: {
$addToSet: {
$cond: [
{ $eq: ["$status", "ready"] },
{ _id: "$_id", name: "$name", original: "$original", status: "$status" },
null
]
}
},
document: { $push: "$$ROOT" }
}
}
$unwind document field to remove the array
{
$unwind: "$document"
}
use a $redact aggregation to keep or remove records based on matching of $document._id in the array ids previously created (that contain the matching original and status)
{
$redact: {
"$cond": {
"if": {
"$setIsSubset": [{
"$map": {
"input": { "$literal": ["A"] },
"as": "a",
"in": "$document._id"
}
},
"$ids"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}
}
$group to push all documents that matched the previous $redact to another array named filtered (we have now 2 array which can be united)
{
$group: {
_id: null,
box_ready: { $first: "$box_ready" },
filtered: { $push: "$document" }
}
}
use a $project with a setUnion to union the arrays box_ready and filtered
{
$project: {
union: {
$setUnion: ["$box_ready", "$filtered"]
},
_id: 0
}
}
$unwind the array you have obtained to get distinct records
{
$unwind: "$union"
}
$match only those which have original missing and that are not null (as initially a the status:ready condition has obliged to get a null value on the first $group
{
$match: {
"union.original": {
"$exists": false
},
"union": { $nin: [null] }
}
}
The whole aggregation query is :
db.collection.aggregate(
[{
$group: {
_id: null,
ids: {
$addToSet: {
$cond: [
{ $eq: ["$status", "ready"] },
"$original", null
]
}
},
box_ready: {
$addToSet: {
$cond: [
{ $eq: ["$status", "ready"] },
{ _id: "$_id", name: "$name", original: "$original", status: "$status" },
null
]
}
},
document: { $push: "$$ROOT" }
}
}, {
$unwind: "$document"
}, {
$redact: {
"$cond": {
"if": {
"$setIsSubset": [{
"$map": {
"input": { "$literal": ["A"] },
"as": "a",
"in": "$document._id"
}
},
"$ids"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}
}, {
$group: {
_id: null,
box_ready: { $first: "$box_ready" },
filtered: { $push: "$document" }
}
}, {
$project: {
union: {
$setUnion: ["$box_ready", "$filtered"]
},
_id: 0
}
}, {
$unwind: "$union"
}, {
$match: {
"union.original": {
"$exists": false
},
"union": { $nin: [null] }
}
}]
)
It gives you :
{ "union" : { "_id" : 1, "name" : "Original Box #1", "status" : "pending" } }
{ "union" : { "_id" : 5, "name" : "Original Box #2", "status" : "ready" } }
{ "union" : { "_id" : 6, "name" : "Original Box #3", "status" : "pending" } }
Use an additional $project if you want to select specific fields
For mongoose, you should be able to do like this to perform aggregation :
Box.aggregate([
//the whole aggregation here
], function(err, result) {
});
Several of the answers are close but here's the most efficient way. It accumulates the "_id" values of boxes to be picked up and then uses $lookup to "rehydrate" the full details of each (top-level) box.
db.boxes.aggregate(
{$group: {
_id:null,
boxes:{$addToSet:{$cond:{
if:{$eq:["$status","ready"]},
then:{$ifNull:["$original","$_id"]},
else:null
}}}
}},
{$lookup: {
from:"boxes",
localField:"boxes",
foreignField:"_id",
as:"boxes"
}}
)
Your result based on sample data:
{
"_id" : null,
"boxIdsToPickUp" : [
{
"_id" : 1,
"name" : "Original Box #1",
"status" : "pending"
},
{
"_id" : 5,
"name" : "Original Box #2",
"status" : "ready"
},
{
"_id" : 6,
"name" : "Original Box #3",
"status" : "pending"
}
] }
Note that the $lookup is done only for the _id values of boxes to be picked up which is far more efficient than doing it for all boxes.
If you wanted the pipeline to be more efficient you would need to store more details about original box in the nested box documents (like its name).
To achieve your goal you can follow bellow steps:
First of all select record for status is ready (because you want to get parent who has no nested box but status is ready and who
has nested box at least one with stats is ready )
Find parent box using $lookup
then $group to get unique parent box
then $project box name
So can try this query:
db.getCollection('boxes').aggregate(
{$match:{"status":'ready'}},
{$lookup: {from: "boxes", localField: "original", foreignField: "_id", as: "parent"}},
{$unwind: {path: "$parent",preserveNullAndEmptyArrays: true}},
{$group:{
_id:null,
list:{$addToSet:{"$cond": [ { "$ifNull": ["$parent.name", false] }, {name:"$parent.name"}, {name:"$name"} ]}}
}
},
{$project:{name:"$list.name", _id:0}},
{$unwind: "$name"}
)
OR
get record for status is ready
get desired recordID
get name according to recordID
db.getCollection('boxes').aggregate(
{$match:{"status":'ready'}},
{$group:{
_id:null,
parent:{$addToSet:{"$cond": [ { "$ifNull": ["$original", false] }, "$original", "$_id" ]}}
}
},
{$unwind:"$parent"},
{$lookup: {from: "boxes", localField: "parent", foreignField: "_id", as: "parent"}},
{$project: {"name" : { $arrayElemAt: [ "$parent.name", 0 ] }, _id:0}}
)
Using mongoose (4.x)
Schema:
const schema = mongoose.Schema({
_id: Number,
....
status: String,
original: { type: Number, ref: 'Box'}
});
const Box = mongoose.model('Box', schema);
Actual Query:
Box
.find({ status: 'ready' })
.populate('original')
.exec((err, boxes) => {
if (err) return;
boxes = boxes.map((b) => b.original ? b.original : b);
boxes = _.uniqBy(boxes, '_id');
console.log(boxes);
});
Docs on Mongoose#populate: http://mongoosejs.com/docs/populate.html

Categories