mongoose concat and single element from array - mongodb

This is a two part mongoDB/mongoose question
I want to concat a First Name/Last Name into "name" AND I only want to show the single "current" item from an array.
So if my data looks like this
[{
"fname":"bob",
"lname":"jones",
"role":"professional",
"active":true,
"jobs":[{
"job":"janitor",
"current":true
},{
"job":"dog groomer"
"current":false
}]
},{
"fname":"sally",
"lname":"peterson",
"role":"professional",
"active":true,
"jobs":[{
"job":"engineer",
"current":false
},{
"job":"college admin"
"current":true
}]
},{
"fname":"jackson",
"lname":"smiley",
"role":"professional",
"active":true,
"jobs":[{
"job":"car salesman",
"current":false
},{
"job":"street sweeper"
"current":false
}{
"job":"house painter"
"current":true
}]
},{
"fname":"katie",
"lname":"smiley",
"role":"amature",
"active":true,
"jobs":[{
"job":"drone entheuast",
"current":true
}]
}]
And I want my return data to be
[{
name:"bob jones",
job:"janitor"
},{
name:"sally peterson",
job:"college admin"},
{
name:"jackson smiley",
job:"house painter"
}]
Currently - I am using this mongoose syntax - but it's not enough...
module.exports.getActiveList = function( callback ) {
const query = { "role":"professional", "active":true }
People.find( query, 'name job', callback );
}
How would I do that?

You can try below aggregation
You can use $concat to combine both fname and lname as a name and $filter to obtain current active job from the jobs array
People.aggregate([
{ "$match": { "role": "professional", "active": true }},
{ "$project": {
"name": { "$concat": ["$fname", " ", "$lname"] },
"jobs": {
"$filter": {
"input": "$jobs",
"as": "job",
"cond": { "$eq": ["$$job.current", true] }
}
}
}},
{ "$project": { "name": 1, "job": { "$arrayElemAt": ["$jobs.job", 0] }}}
])
Or using $let to complete in a single stage
People.aggregate([
{ "$match": { "role": "professional", "active": true }},
{ "$project": {
"name": { "$concat": ["$fname", " ", "$lname"] },
"job": {
"$let": {
"vars": {
"jobs": {
"$filter": {
"input": "$jobs",
"as": "job",
"cond": { "$eq": ["$$job.current", true] }
}
}
},
"in": { "$arrayElemAt": ["$$jobs.job", 0] }
}
}
}}
])

Related

Replace items in array with property from matching object in another array

There's a permissions collection that contains permissions and the users or groups that are assigned that permission for a given resource.
permissions
[{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:abc",
"user:def",
"group:abc",
"group:bff"
]
},
{
"permission": "document.read",
"users": ["user:xxx"]
},
{
"permission": "document.update",
"users": ["user:xxx"]
}
]
}]
And a groups collection that assigns users to a group.
groups
[
{
"_id": 1,
"id": "abc",
"name": "Test Group",
"users": ["cpo", "yyy"]
},
{
"_id": 2,
"id": "bff",
"name": "Another Group",
"users": ["xxx"]
}
]
I'm trying to query the permissions collection for resource:docs/61 and for each permission, resolve any groups in the users property to the matching group's users. See below for desired result.
Desired Result
{
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:abc",
"user:def",
"user:cpo",
"user:yyy",
"user:xxx"
]
},
{
"permission": "document.read",
"users": ["user:xxx"]
},
{
"permission": "document.update",
"users": ["user:xxx"]
}
]
}
I've setup a Mongo Playground where I've been trying to get this to work... unsuccessfully. Below is my current attempt. I'm unsure how to map the groups to their respectful users and then reverse the $unwind. Or maybe I don't even need the $unwind 🤷‍♂️
db.permissions.aggregate([
{
"$match": {
"resource": "resource:docs/61"
}
},
{
$unwind: "$permissions"
},
{
"$lookup": {
"from": "groups",
"let": {
"users": {
"$filter": {
"input": "$permissions.users",
"as": "user",
"cond": {
"$ne": [
-1,
{
"$indexOfCP": [
"$$user",
"group:"
]
}
]
}
}
}
},
"pipeline": [
{
"$match": {
"$expr": {
"$in": [
{
"$concat": [
"group:",
"$id"
]
},
"$$users"
]
}
}
},
{
"$project": {
"_id": 0,
"id": {
"$concat": [
"group:",
"$id"
]
},
"users": 1
}
}
],
"as": "groups"
}
},
{
"$project": {
"groups": 1,
"permissions": {
"permission": "$permissions.permission",
"users": "permissions.users"
}
}
}
])
You will need to:
$unwind the permissions for easier processing in later stages
$lookup with "processed" key:
remove the prefix group: for the group key
use the $lookup result to perform $setUnion with your permissions.users array. Remember to $filter out the group entries first.
$group to get back the original / expected structure.
db.permissions.aggregate([
{
"$match": {
"resource": "resource:docs/61"
}
},
{
"$unwind": "$permissions"
},
{
"$lookup": {
"from": "groups",
"let": {
"groups": {
"$map": {
"input": "$permissions.users",
"as": "u",
"in": {
"$replaceAll": {
"input": "$$u",
"find": "group:",
"replacement": ""
}
}
}
}
},
"pipeline": [
{
$match: {
$expr: {
"$in": [
"$id",
"$$groups"
]
}
}
}
],
"as": "groupsLookup"
}
},
{
"$addFields": {
"groupsLookup": {
"$reduce": {
"input": "$groupsLookup",
"initialValue": [],
"in": {
$setUnion: [
"$$value",
{
"$map": {
"input": "$$this.users",
"as": "u",
"in": {
"$concat": [
"user:",
"$$u"
]
}
}
}
]
}
}
}
}
},
{
"$project": {
resource: 1,
permissions: {
permission: 1,
users: {
"$setUnion": [
{
"$filter": {
"input": "$permissions.users",
"as": "u",
"cond": {
$eq: [
-1,
{
"$indexOfCP": [
"$$u",
"group:"
]
}
]
}
}
},
"$groupsLookup"
]
}
}
}
},
{
$group: {
_id: "$_id",
resource: {
$first: "$resource"
},
permissions: {
$push: "$permissions"
}
}
}
])
Mongo Playground

Find subdocument nested inside a document by id in mondogb

I have a mongodb document like this
{
"_id": {
"$oid": "6241dd90891458501c17d627"
},
"A": [
{
"_id": {
"$oid": "6241ddb1891458501c17d63e"
},
"B": [
{
"_id": {
"$oid": "6241ddc4891458501c17d674"
}
},
{
"_id": {
"$oid": "6241ddda891458501c17d675"
}
}
]
},
{
"_id": {
"$oid": "6241ddbe891458501c17d63f"
},
"B": [
{
"_id": {
"$oid": "6241ddda891458501c17d678"
}
},
{
"_id": {
"$oid": "6241ddda891458501c17d679"
}
}
]
}
]
}
This document has 2 nested arrays: an array of "A" elements, inside each element of "A" there's an array of "B" elements. I need to search by an _id of a "B" element, let's say 6241ddda891458501c17d679. I need a way to obtain this structure in mongodb
{
"_id": {
"$oid": "6241dd90891458501c17d627"
},
"A": [
{
"_id": {
"$oid": "6241ddbe891458501c17d63f"
},
"B": [
{
"_id": {
"$oid": "6241ddda891458501c17d679"
}
}
]
}
]
}
How can I achieve this? Thanks very much
Maybe something like this:
Option 1, Find:
db.collection.find({
"A.B._id": {
"$oid": "6241ddda891458501c17d679"
}
},
{
"A": {
"$filter": {
"input": {
"$map": {
"input": "$A",
"as": "a",
"in": {
"_id": "$$a._id",
"B": {
"$filter": {
"input": "$$a.B",
"as": "b",
"cond": {
"$eq": [
{
"$oid": "6241ddda891458501c17d679"
},
"$$b._id"
]
}
}
}
}
}
},
"as": "an",
"cond": {
"$ne": [
"$$an.B",
[]
]
}
}
}
})
Explained:
Use find() with match query on "A.B._id" ( good to have index on this filed for best performance)
In the filter part add $filter/map/filter combination to filter only the matching _id for array B elements and preserve the array A _id , also in the initial filter condition use only non-empty arrays [] to avoid having elements from empty arrays in the final result.
playground1
Option 2 , aggregation:
db.collection.aggregate([
{
$match: {
"A.B._id": {
"$oid": "6241ddda891458501c17d679"
}
}
},
{
"$addFields": {
"A": {
"$filter": {
"input": {
"$map": {
"input": "$A",
"as": "a",
"in": {
"_id": "$$a._id",
"B": {
"$filter": {
"input": "$$a.B",
"as": "b",
"cond": {
"$eq": [
{
"$oid": "6241ddda891458501c17d679"
},
"$$b._id"
]
}
}
}
}
}
},
"as": "an",
"cond": {
"$ne": [
"$$an.B",
[]
]
}
}
}
}
}
])
playground2

How to filter an array of objects in mongoose by date field only selecting the most recent date

I'm trying to filter through an array of objects in a user collection on MongoDB. The structure of this particular collection looks like this:
name: "John Doe"
email: "john#doe.com"
progress: [
{
_id : ObjectId("610be25ae20ce4872b814b24")
challenge: ObjectId("60f9629edd16a8943d2cab9b")
date_unlocked: 2021-08-05T12:15:32.129+00:00
completed: true
date_completed: 2021-08-06T12:15:32.129+00:00
}
{
_id : ObjectId("611be24ae32ce4772b814b32")
challenge: ObjectId("60g6723efd44a6941l2cab81")
date_unlocked: 2021-08-06T12:15:32.129+00:00
completed: true
date_completed: 2021-08-07T12:15:32.129+00:00
}
]
date: 2021-08-04T13:06:34.129+00:00
How can I query the database using mongoose to return only the challenge with the most recent 'date_unlocked'?
I have tried: User.findById(req.user.id).select('progress.challenge progress.date_unlocked').sort({'progress.date_unlocked': -1}).limit(1);
but instead of returning a single challenge with the most recent 'date_unlocked', it is returning the whole user progress array.
Any help would be much appreciated, thank you in advance!
You can try this.
db.collection.aggregate([
{
"$unwind": {
"path": "$progress"
}
},
{
"$sort": {
"progress.date_unlocked": -1
}
},
{
"$limit": 1
},
{
"$project": {
"_id": 0,
"latestChallenge": "$progress.challenge"
}
}
])
Test the code here
Alternative solution is to use $reduce in that array.
db.collection.aggregate([
{
"$addFields": {
"latestChallenge": {
"$arrayElemAt": [
{
"$reduce": {
"input": "$progress",
"initialValue": [
"0",
""
],
"in": {
"$let": {
"vars": {
"info": "$$value",
"progress": "$$this"
},
"in": {
"$cond": [
{
"$gt": [
"$$progress.date_unlocked",
{
"$arrayElemAt": [
"$$info",
0
]
}
]
},
[
{
"$arrayElemAt": [
"$$info",
0
]
},
"$$progress.challenge"
],
"$$info"
]
}
}
}
}
},
1
]
}
}
},
{
"$project": {
"_id": 0,
"latestChallenge": 1
}
},
])
Test the code here
Mongoose can use raw MQL so you can use it.

MongoDB how to filter in nested array

I have below data. I want to find value=v2 (remove others value which not equals to v2) in the inner array which belongs to name=name2. How to write aggregation for this? The hard part for me is filtering the nestedArray which only belongs to name=name2.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
}
]
}
And the desired output is below. Please note the value=v1 remains under name=name1 while value=v1 under name=name2 is removed.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v2"
}
]
}
]
}
You can try,
$set to update array field, $map to iterate loop of array field, check condition if name is name2 then $filter to get matching value v2 documents from nestedArray field and $mergeObject merge objects with available objects
let name = "name2", value = "v2";
db.collection.aggregate([
{
$set: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.name", name] }, //name add here
{
nestedArray: {
$filter: {
input: "$$this.nestedArray",
cond: { $eq: ["$$this.value", value] } //value add here
}
}
},
{}
]
}
]
}
}
}
}
}
])
Playground
You can use the following aggregation query:
db.collection.aggregate([
{
$project: {
"array": {
"$concatArrays": [
{
"$filter": {
"input": "$array",
"as": "array",
"cond": {
"$ne": [
"$$array.name",
"name2"
]
}
}
},
{
"$filter": {
"input": {
"$map": {
"input": "$array",
"as": "array",
"in": {
"name": "$$array.name",
"nestedArray": {
"$filter": {
"input": "$$array.nestedArray",
"as": "nestedArray",
"cond": {
"$eq": [
"$$nestedArray.value",
"v2"
]
}
}
}
}
}
},
"as": "array",
"cond": {
"$eq": [
"$$array.name",
"name2"
]
}
}
}
]
}
}
}
])
MongoDB Playground

MongoDB: Get all $matched elements individually from an array

I'm trying to get all matched elements individually, here is the sample data and the query.
// json
[
{
"name": "Mr Cool",
"ican": [
{
"subcategory": [
{
"id": "5bffdba824488b182ec86f8d", "name": "Cricket"
},
{
"id": "5bffdba824488b182ec86f8c", "name": "Footbal"
}
],
"category": "5bffdba824488b182ec86f88",
"name": "Sports"
}
]
}
]
// query
db.collection.aggregate([
{
"$match": {
"ican.subcategory.name": { $in: ["Cricket","Football"] }
}
},
{
"$project": { "_id": 1, "name": 1, }
}
])
I'm getting the combined result, I need the individual match record. I tried $all and $elementMatch but getting the same response. how can I get the results as below. I'm using $aggregate because I will be using $geoNear pipeline for getting the nearby users.
// current result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool"
}
]
// expected result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Cricket"
},
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Footbal"
}
]
Thank you
Try this Mongo Playground
db.col.aggregate([
{"$unwind" : "$ican"},
{"$unwind" : "$ican.subcategory"},
{"$match" : {"ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{"$group" : {"_id" : null,"data" : {"$push" : {"_id" : "$_id","name" : "$name","subcategory" : "$ican.subcategory.name"}}}},
{"$unwind" : "$data"},
{"$replaceRoot" : {"newRoot" : "$data"}}
])
You can use below aggregation without the $unwind and for better performance
db.collection.aggregate([
{ "$match": { "ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{ "$project": {
"ican": {
"$reduce": {
"input": "$ican",
"initialValue": [],
"in": {
"$concatArrays": [
{ "$filter": {
"input": {
"$map": {
"input": "$$this.subcategory",
"as": "s",
"in": { "name": "$name", "subcategory": "$$s.name" }
}
},
"as": "fil",
"cond": { "$in": ["$$fil.subcategory", ["Football"]] }
}},
"$$value"
]
}
}
}
}},
{ "$unwind": "$ican" },
{ "$replaceRoot": { "newRoot": "$ican" }}
])