MongoDB $ifNull empty array still adding data - node.js

I have 2 collections, users and tracks. When I fetch the user profile I want to get his tracks.
The user object has tracks as an array of track IDs, and the second collection is of the tracks.
Now I am running this code below:
users
.aggregate([
{
$match: { _id: ObjectId(userId) },
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks",
},
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true,
},
},
{
$group: {
_id: ObjectId(userId),
username: { $first: "$username" },
profileImg: { $first: "$profileImg" },
socialLinks: { $first: "$socialLinks" },
tracks: {
$push: {
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: isLogged ? "$tracks.mediaId" : null,
thumbnailId: "$tracks.thumbnailId",
views: { $size: { $ifNull: ["$tracks.views", []] } },
downloads: { $size: { $ifNull: ["$tracks.downloads", []] } },
uploadedDate: "$tracks.uploadedDate",
},
},
},
},
])
In case the user does not have tracks or there are no tracks, the $ifNull statement returns an object only with these fields so it looks like that:
user: {
// user data
tracks: [{ views: 0, downloads: 0, mediaId: null }]
}
There are no tracks found so "tracks.views" cannot be read so I added the $ifNull statement, how can I avoid it from returning an empty data? also, the API call knows whether the user is logged or not (isLogged), I set the mediaId to null.
If there are no tracks found, why does the code still add these 3 fields only? no tracks to go through them...
Edit:
Any track has downloads and views containing user IDs whom downloaded / viewed the track, the track looks like that
{
"name": "New Track #2227",
"categoryId": "61695d57893f048528d049e5",
"links": {
"youtube": "https://www.youtube.com",
"soundcloud": null,
"spotify": null
},
"_id": "616c90651ab67bbd0b0a1172",
"creatorId": "61695b5986ed44e5c1e1d29d",
"mediaId": "616c90651ab67bbd0b0a1171",
"thumbnailId": "616c90651ab67bbd0b0a1170",
"plays": [],
"status": "pending",
"downloads": [],
"uploadedDate": 1634504805
}
When the fetched user doesn't have any track post yet, I receive an array with one object that contains the fields mentioned above.
What I expect to get when the user has no tracks is an empty array, as you can see the response above, the track object isn't full and contains only the conditional keys with zero value and null.
Bottom line I want the length of views and downloads only if there is a track or more, also for the mediaId which I want to hide it in case the user isn't logged. If there are no tracks I don't understand why it returns these 3 fields
expected result when the user has one track or more
user: {
// user data
tracks: [
{
name: "New Track #2227",
categoryId: "61695d57893f048528d049e5",
links: {
youtube: "https://www.youtube.com",
soundcloud: null,
spotify: null,
},
_id: "616c90651ab67bbd0b0a1172",
creatorId: "61695b5986ed44e5c1e1d29d",
mediaId: "616c90651ab67bbd0b0a1171",
thumbnailId: "616c90651ab67bbd0b0a1170",
plays: 0,
status: "pending",
downloads: 0,
uploadedDate: 1634504805,
},
];
}
expected result when the user has no tracks
user: {
// user data
tracks: [];
}

Append this stage to your pipeline:
{
$set: {
tracks: {
$filter: {
input: "$tracks",
cond: { $ne: [ { $type: "$$this._id" }, "missing" ] }
}
}
}
}

You could also modify your query to look something like this: (check out a live demo here)
This query uses a conditional push via $cond coupled with the $and operator to search for more than one condition. If tracks.downloads or tracks.plays are not greater than 0, we use the $$REMOVE variable (which just ignores that document and returns an empty array, like you are looking for).
Query
db.users.aggregate([
{
$match: {
_id: ObjectId("616c80793235ab5cc26dbaff")
},
},
{
$lookup: {
from: "tracks",
localField: "tracks",
foreignField: "_id",
as: "tracks"
}
},
{
$unwind: {
path: "$tracks",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: ObjectId("616c80793235ab5cc26dbaff"),
username: {
$first: "$username"
},
profileImg: {
$first: "$profileImg"
},
socialLinks: {
$first: "$socialLinks"
},
tracks: {
$push: {
// "IF" plays and downloads > 0
$cond: [
{
$and: [
{
$gt: [
"$tracks.plays",
0
]
},
{
$gt: [
"$tracks.downloads",
0
]
},
]
},
// "THEN" return document
{
_id: "$tracks._id",
name: "$tracks.name",
categoryId: "$tracks.categoryId",
links: "$tracks.links",
mediaId: "$tracks.mediaId",
thumbnailId: "$tracks.thumbnailId",
plays2: {},
plays: "$tracks.plays",
downloads: "$tracks.downloads",
uploadedDate: "$tracks.uploadedDate"
},
// "ELSE" remove
"$$REMOVE"
]
}
}
}
}
])

Related

Mongoose - How to get unique data based on some fields using aggregation

I have these fields in the document,
doc: {
"id": "632ac8cba7723378033fef10",
"question": 1,
"text": "aasdfghjk,mnbvcxswertyuikmnbvcxsrtyuiknbvcdrtyujnbvcddtyjnbvfty",
"slug": "xcvbnrddfghjktdxjjydcvbyrsxcvbhytrsxggvbjkytrdgc",
"subject": 25866,
"tutorInfo": {
"tutorId": "632ac8cba7723378033fa0fe",
"tutorIncrementalId": 95947
}
}
the same tutorInfo can Occur in multiple documents.
const allQuestionBySubject = await QuestionParts.aggregate([
{
$match: {
$and: [
{
subject: subjectIncrementalId
},
{tutorInfo: {$exists: true}}
]
}
},
{ "$skip": page * limit },
{ "$limit": limit },
{
$lookup: {
from: "profiles",
localField: "tutorInfo.tutorIncrementalId",
foreignField: "incrementalId",
as: "tutorDetails"
}
}
])
Code to get a list of questions as per subject.
I am filtering documents based on subject and as I mentioned the same tutorInfo can be present in multiple documents so in the result same tutor can be present in multiple documents, How can I get a unique list of documents in which tutorInfo shouldn't be repeated.
Since the same tutorInfo is present in multiple records, You can use $group to group the document on the tutorInfo.tutorId field.
const allQuestionBySubject = await QuestionParts.aggregate(
[
{
$match: {
$and: [
{
subject: subjectIncrementalId
},
{ tutorInfo: { $exists: true } }
]
}
},
{ "$skip": page * limit },
{ "$limit": limit },
{
"$group": {
_id: "$tutorInfo.tutorId",
question: { $first: "$question" },
text: { $first: "$text" },
slug: { $first: "$slug" },
subject: { $first: "$orderId" },
tutorInfo: { $first: "$tutorInfo" },
}
},
{
$lookup: {
from: "profiles",
localField: "tutorInfo.tutorIncrementalId",
foreignField: "incrementalId",
as: "tutorDetails"
}
}
]
)

Show documents from Array with MongoDB

I would like to "create" new documents, to keep only the contents of the activities.
I've looked at $project and $unwide, but I didn't find any appropriate result.
Here is my MongoDB query:
const activities = await friendModel.aggregate([
{
$match: {
$or: [
{
ReceiverId: ActorId,
Status: 1,
},
{
RequesterId: ActorId,
Status: 1,
},
],
},
},
{
$set: {
fieldResult: {
$cond: {
if: {
$eq: [ "$ReceiverId", ActorId ],
},
then: "$RequesterId",
else: "$ReceiverId",
},
},
},
},
{
$lookup: {
from: "activities",
localField: "fieldResult",
foreignField: "ActorId",
as: "activity",
},
},
{
$unwind: {
path: "$activity",
preserveNullAndEmptyArrays: true
}
},
{ $sort: { "activity._Date": -1 } },
{ $skip: request.pageindex * 3 },
{ $limit: 3 },
]);
Here is what I would like to do: https://sourceb.in/7u0tirIstt
Here is what I get for the moment: https://sourceb.in/Ped2xY5Ml8
Thank you in advance for your help!
You should be able to add $project as follows:
...
{
$project: {
"_id":"$_id",
"ActivityId": "$activity._id",
"ActorId": "$activity.ActorId",
"Type": "$activity.Type",
"_Date": "$activity._Date",
"MovieId": "$activity.MovieId",
"FriendId": "$activity.FriendId",
"ContestId": "$activity.ContestId",
"LookId": "$activity.LookId",
"__v": "$__v"
},
},
...
You can access values with $, and map nested values by accessing them directly
Also would recommend you doing a limit before you do lookup to speed it up a bit

MongoDB : Push or merge one lookup's result in other lookup's result

I have 3 collection namely constants, activities, applications with mentioned properties.
Now, querying constants collection with activities and activities with applications with matching Id's. I am getting correct results. But now activity_types are shown at per data level.
But expecting the output should be at per item level inside data whichever is matching with item. Because activities are matching for Item and it should be shown in per item level not at data level. I tried with $push and $group but not getting expected results.
Constants
{
_id: id
value : {
categories: [
{
id: 001,
title: "test 1"
},
{
id: 002,
title: "test 2"
},
{
id: 003,
title: "test 3"
}
]
}
}
Activity
{
propert1: "",
propert2: "",
config: {
agenda_item_category_ids: [ObjectId(001), ObjectId(002)]
},
activity_type_id: ObjectId(123)
}
{
propert1: "",
propert2: "",
activity_type_id: ObjectId(456)
config: {
agenda_item_category_ids: [ObjectId(002)]
}
}
applications
{
_id: ObjectId(123),
prop1: "",
prop2: ""
}
{
_id: ObjectId(456),
prop1: "",
prop2: ""
}
Current query
const results = await Constants.aggregate([
{
$match: query,
},
{
$unwind: {
path: '$value.categories',
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: 'activity',
localField: 'value.categories.id',
foreignField: 'config.agenda_item_category_ids',
as: 'data',
},
},
{
$lookup: {
from: 'applications',
localField: 'items.activity_type_id',
foreignField: '_id',
as: 'activity_type',
},
},
{
$project: {
_id: 0,
category_id: '$value.categories.id',
title: '$value.categories.title',
description: '$value.categories.description',
icon_src: '$value.categories.icon_src',
data: 1,
activity_type: 1,
},
},
]);
Current output
[
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
},
{
data: [
{item1},
{item2}
],
activity_type,
title
_id
}
]
Expected output
[
{
data: [
{
item1,
activity_type
},
{
item2,
activity_type
}
],
title
_id
},
]
Tried method
{
"_id": "$_id",
"activity_type": {
"$push": "$activity_type"
}
}

Mongoose join two collections and get referenced data in two properties

I'm doing a simple follow friend functionality.
Please see my codes below:
Following schema:
{
"userId": { type: String },
"followers": [{ "followerId": type: String }],
"followings": [{ "followingId": type: String }],
}
User schema:
{
"fullName": { type: String }
}
Note: user 8 has 1 follower and 2 followings.
Now, my expected output should be like this:
"userId": 8,
"followers": [
{
"followerId": 4,
"fullName": "Rose Marriott",
},
{
"followerId": 5,
"fullName": "James Naismith",
}
],
"followings": [
{
"followingId": 1,
"fullName": "Russell Oakham",
},
{
"followingId": 5,
"fullName": "James Naismith",
}
]
This is what I tried so far:
db.followings.aggregate([
{ $unwind: "$followers" },
{
$lookup: {
from: "users",
localField: "followers.followerId",
foreignField: "_id",
as: "users"
}
},
{
$addFields:
{
users: { $arrayElemAt: ["$users", 0] },
},
},
{ $unwind: "$followings" },
{
$lookup: {
from: "users",
localField: "followings.followingId",
foreignField: "_id",
as: "users2"
}
},
{
$addFields:
{
users2: { $arrayElemAt: ["$users2", 0] },
},
},
{ $match: {"userId": mongoose.Types.ObjectId(userId) } },
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
followers: {
$push: {
followerId: "$followers.followerId",
fullName: "$users.fullName",
}
},
followings: {
$push: {
followingId: "$followings.followingId",
fullName: "$users2.fullName",
}
}
}
}
]);
But I'm getting 2 followers and 2 followings. I wonder what's causing this issue. Appreciate any help. Thanks!
You can try,
$addFields to make a unique array called userIds form both arrays followers and followings, $setUnion to get unique ids,
$lookup with users collection
$project to show fields,
followers get fullName, $map to iterate loop of followers and get the name of followerId from users array using $reduce and $cond
followings get fullName, $map to iterate loop of followings and get the name of followingId from users array using $reduce and $cond
db.followings.aggregate([
{
$addFields: {
userIds: {
$setUnion: [
{
$map: {
input: "$followers",
in: "$$this.followerId"
}
},
{
$map: {
input: "$followings",
in: "$$this.followingId"
}
}
]
}
}
},
{
$lookup: {
from: "users",
localField: "userIds",
foreignField: "_id",
as: "users"
}
},
{
$project: {
userId: 1,
followers: {
$map: {
input: "$followers",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followerId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
},
followings: {
$map: {
input: "$followings",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followingId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
Playground
when working with relations on Mongoose you should create the relationship based on a unique id, and then reference the document. In your case, it would be:
followingSchema = new mongoose.Schema({
{
"followers": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
"followings": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
}
})
userSchema = new mongoose.Schema({
fullname: String,
})
be aware that the userId will be created automatically by Mongoose with a field called _id. so, the end result of creating a new following documents would be:
{
_id: "klajdsfñalkdjf" //random characters created by mongoose,
followers: ["adfadf134234", "adfadte345"] //same as before, these are Ids of the users randomly generated by mongoose
followers: ["adfadf134234", "adfadte345"]
}
{
_id: adfadf134234,
fullName: "alex",
}
now, because there is no use for us to have a random number as information in the fields of following and followers in the following object, we can now use the method .populate() that can be used over the document itself to transform those Ids into actual information. You can see more about it here: mongoose documentation
our final result would be something like this:
{
_id: "añfakdlsfja",
followers : [{_id: "adlfadsfj", fullName: "alex"}],
following : [{_id: "adfadfad" , fullName: "other"}, {_id: "dagadga", fullName: "another"}]
}

Followers - mongodb query check

I have 2 collections in my database. One is called User
{
_id: storeUserId,
name: ...,
etc
}
the other one called Following
{
userId: ...,
followingUserId: ...
}
the userId is the current user id and the followingUserId is the id that current user wants to follow.
For example, in User collection I have:
{
_id: userIdOne,
etc
},
{
_id: userIdTwo,
etc
}
and in Following collection I have:
{
userId: userIdThousand,
followingUserId: userIdTwo
}
When I run find query
db.bios.find();
I get
{
"_id": userIdTwo,
"username": "random27005688"
},
{
"_id": userIdThree
"username": "random232111"
},
{
"_id": userIdOne
"username": "random2702"
}
]
The result is what I want but I want to add a 'isFollowed' field to each result item to check following status. I have a user id let say: 'userIdThousand' which I want to use it to check against each result item based on my Following collection. E.g,
check if userIdThousand is following userIdOne
check if userIdThousand is following userIdTwo, etc.
Below is my expected result. Thanks!
[
{
_id: userIdTwo,
"username": "random27005688",
"isFollowed": true
},
{
"_id": userIdThree
"username": "random232111",
"isFollowed": false
},
{
"_id": userIdOne
"username": "random2702",
"isFollowed": false
},
]
You need $lookup to get the data from second collection matching by followingUserId then you can use $filter to get only followers with particular _id and check if new array has any elements (using $size) which means that user is followed by other user:
db.User.aggregate([
{
$match: {
_id: { $ne: "userIdOne" }
}
},
{
$lookup: {
from: "Following",
localField: "_id",
foreignField: "followingUserId",
as: "followers"
}
},
{
$addFields: {
followers: {
$filter: { input: "$followers", as: "follower", cond: { $eq: [ "$$follower._id", "userIdOne" ] } }
}
}
},
{
$project: {
_id: 1,
username: 1,
isFollowed: { $gt: [ { $size: "$followers" }, 0 ] }
}
}
])
maybe you could split it into two step.
1. query User and get the result
for example, you get a user.
{
_id: userIdTwo,
"username": "random27005688",
}
2. query Following and get if the user has been followed.
such as
has_followed=db.Following.find({"userId":userIdTwo}).count()
not the best solution but it might help u.

Resources