mongodb - left join with conditions - node.js

I am attempting an left antijoin on these two collections.
I want all users where department is equal to 'IT' that aren't in a meeting that had an endAt time > 175. Either as a creator or receiver.
So essentially whoever hasn't been in a meeting in the last xxx time.
Based on below collections:
John would be retrieved because he is apart of department IT and has not been a receiver or creator after '175'.
Jane has an endAt time after 175 and is in IT so wouldn't be retrieved
Bill is apart of finance so even though he hasn't been it doesn't matter
Bob has an endAt time after 175 and is in IT so wouldn't be retrieved
Mary is in IT and has not been in any meetings so she is retrieved.
Users Collection:
[
{
_id: ObjectId("1"),
name: "john",
department: 'IT'
},
{
_id: ObjectId("2"),
name: "jane",
department: 'IT'
},
{
_id: ObjectId("3"),
name: "bill",
department: 'finance'
},
{
_id: ObjectId("4"),
name: "Bob",
department: 'IT'
},
{
_id: ObjectId("5"),
name: "Mary",
department: 'IT'
}
]
Meetings Collection:
[
{
_id: ObjectId("a"),
endedAt: 100,
creator_id: ObjectId("1"),
receiver_id: ObjectId("2")
},
{
_id: ObjectId("b"),
endedAt: 150,
creator_id: ObjectId("1"),
receiver_id: ObjectId("3")
},
{
_id: ObjectId("c"),
endedAt: 200,
creator_id: ObjectId("4"),
receiver_id: ObjectId("2")
},
{
_id: ObjectId("d"),
endedAt: 250,
creator_id: ObjectId("2"),
receiver_id:
}
]
Output:
[
{
_id: ObjectId("1"),
name: "john",
department: 'IT'
},
{
_id: ObjectId("5"),
name: "Mary",
department: 'IT'
}
]
My approach:
db.users.aggregate([
{
$match:
{
type: 'IT'
}
},
{
$lookup:
{
from: "meetings",
let:
{
userid: "$_id",
},
pipeline: [
{ $match:
{ $expr:
{
$and:[
{
$or: [
{ $eq: ["$receiver_id", "$$userid"] },
{ $eq: ["$creator_id", "$$userid"] },
]
},
{ $gt: ["$endAt", 175] }
]
}
}
}
],
as: "result"
}
},
{
$unwind:
{
path: "$result",
preserveNullAndEmptyArrays: true
}
},
{
$match:
{
result: {$exists:false}
}
}
])

Query
match "IT"
join if >175 AND (userid in any of the 2 (creator/receiver))
*its lookup pipeline, because multiple join creteria
reject those that are joined
Test code here
db.users.aggregate([
{
"$match": {
"department": {
"$eq": "IT"
}
}
},
{
"$lookup": {
"from": "meetings",
"let": {
"userid": "$_id"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$gt": [
"$endedAt",
175
]
},
{
"$or": [
{
"$eq": [
"$$userid",
"$creator_id"
]
},
{
"$eq": [
"$$userid",
"$receiver_id"
]
}
]
}
]
}
}
},
{
"$project": {
"_id": 1
}
}
],
"as": "meetings"
}
},
{
"$match": {
"$expr": {
"$eq": [
"$meetings",
[]
]
}
}
},
{
"$unset": [
"meetings"
]
}
])

aggregate
db.users.aggregate([
{
"$match": {
department: "IT"
}
},
{
"$lookup": {
"from": "meeting",
"localField": "_id",
"foreignField": "creator_id",
"as": "meeting_creator"
}
},
{
"$lookup": {
"from": "meeting",
"localField": "_id",
"foreignField": "receiver_id",
"as": "meeting_receiver"
}
},
{
"$match": {
"$and": [
{
"meeting_creator.endedAt": {
"$not": {
"$gt": 175
}
}
},
{
"meeting_receiver.endedAt": {
"$not": {
"$gt": 175
}
}
}
]
}
},
{
"$project": {
_id: 1,
name: 1,
department: 1
}
}
])
data
db={
"users": [
{
_id: "1",
name: "john",
department: "IT"
},
{
_id: "2",
name: "jane",
department: "IT"
},
{
_id: "3",
name: "bill",
department: "finance"
},
{
_id: "4",
name: "Bob",
department: "IT"
},
{
_id: "5",
name: "Mary",
department: "IT"
}
],
"meeting": [
{
_id: "a",
endedAt: 100,
creator_id: "1",
receiver_id: "2"
},
{
_id: "b",
endedAt: 150,
creator_id: "1",
receiver_id: "3"
},
{
_id: "c",
endedAt: 200,
creator_id: "4",
receiver_id: "2"
},
{
_id: "d",
endedAt: 250,
creator_id: "2",
receiver_id: ""
}
]
}
result
[
{
"_id": "1",
"department": "IT",
"name": "john"
},
{
"_id": "5",
"department": "IT",
"name": "Mary"
}
]
mongoplayground

This is the solution I came up with that ended up working, does anyone have any details what would be the most efficient?
db.users.aggregate([
{
$match:
{
type: 'IT'
}
},
{
$lookup:
{
from: "meetings",
let:
{
userid: "$_id",
},
pipeline: [
{ $match:
{ $expr:
{
$and:[
{
$or: [
{ $eq: ["$receiver_id", "$$userid"] },
{ $eq: ["$creator_id", "$$userid"] },
]
},
{ $gt: ["$endAt", 175] }
]
}
}
}
],
as: "result"
}
},
{
$unwind:
{
path: "$result",
preserveNullAndEmptyArrays: true
}
},
{
$match:
{
result: {$exists:false}
}
}
])

Related

Aggregation: Return documents based on fields in a subdocument

I’m using an aggregation to return data via a lookup to build the links between documents.
At the moment, the linking is working when User A creates links between their own assets to navigate.
But if User A is viewing an asset that’s been shared with them by User B and navigates to one that has a link to an asset that hasn’t been shared with them, those are the documents I need to exclude from the results.
So, I need the documents for assets that have a document in attributes that contains my userId, or — as in the $match — the $_id of an attribute that's in the attributes array in assets. When an asset is shared with someone, a document in attributes is created.
The data for a Link is:
{
"_id": {
"$oid": "63769c377615fe4cdb4995a6"
},
"userId": "620920aa9ddac2074a50472f",
"toAsset": {
"$oid": "63769c117615fe4cdb499515"
},
"fromAsset": {
"$oid": "63769c067615fe4cdb4994d9"
},
"comment": "<p>Linking of Note 0001 to Note 0002.</p>",
"createdAt": {
"$date": {
"$numberLong": "1668717623761"
}
},
"updatedAt": {
"$date": {
"$numberLong": "1668717623761"
}
},
"isEmbedded": false,
"isActive": true,
"__v": 0
}
The data for an Asset, as in toAsset and fromAsset, is:
{
"_id": {
"$oid": "6377a8d834671794449f0dca"
},
"userId": "636b73f31527830f7bd7a47e",
"folderId": "636b73f31527830f7bd7a482",
"title": "Note that hasn't been shared",
"note": "<p>Here's a Note that hasn't been shared.</p>",
"typeOfAsset": "note",
"createdAt": {
"$date": {
"$numberLong": "1668786392389"
}
},
"updatedAt": {
"$date": {
"$numberLong": "1668786392389"
}
},
"isActive": 3,
"meta": [...],
"preferences": [...],
"sequence": 1,
"tags": [],
"attributes": [
{
"$oid": "6377a8d834671794449f0dc8"
}
],
"__v": 0
}
I’m using attributes to manage what assets have been shared with whom, and the data is:
{
"_id": {
"$oid": "6377a8d834671794449f0dc8"
},
"userId": "636b73f31527830f7bd7a47e",
"numberOfViews": 2,
"isFavourite": false,
"isToRead": false,
"typeOfAccess": "isOwner",
"sharing": {
"typeOfShare": "withUsers",
"sharedWith": [],
"segementsForUrl": []
},
"__v": 0
}
Now, the task here is to somehow how return the assets that have been shared, but after a bunch of different attempts (as per the code that’s been commented out), I’ve so far failed.
The code is:
const match = {
$match: {
[args.directionOfLink]: new mongoose.Types.ObjectId(args.assetId)
}
}
const project = {
$project: {
_id: 0,
id: '$_id',
userId: 1,
[directionOfLink]: 1,
comment: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1,
score: {
$meta: 'searchScore'
}
}
}
const lookup = {
$lookup: {
from: 'assets',
localField: directionOfLink,
foreignField: '_id',
let: { attributesInAsset: '$attributes' },
pipeline: [
{
$lookup: {
from: 'attributes',
as: 'attributes',
pipeline: [{
$match: {
$expr: {
$in: [ '$_id', '$$attributesInAsset' ]
// $and: [
// { $eq: [ '$userId', context.body.variables.userId ] },
// { $in: [ '$typeOfAccess', ['isOwner', 'asAuthor', 'asReader'] ] },
// ]
}
}
}]
}
},
{
$project: {
_id: 1,
userId: 1,
folderId: 1,
title: 1,
typeOfAsset: 1,
attributes: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1
}
}
],
as: directionOfLink
}
}
Here, directionOfLink is either "toAsset" or "fromAsset".
Any thoughts would be appreciated.
As a non expert in MongoDB, it's possible this isn't the most performant approach, but at least it works:
const lookup = {
$lookup: {
from: 'assets',
localField: directionOfLink,
foreignField: '_id',
as: directionOfLink,
pipeline: [
{
$lookup: {
from: 'assets_attributes',
as: 'attributesInAssets',
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ '$userId', context.body.variables.userId ] },
{ $in: [ '$typeOfAccess', ['isOwner', 'asAuthor', 'asReader'] ] },
]
}
}
}
]
}
},
{
$unwind: '$attributesInAssets'
},
{
$match: {
$expr: {
$in: [ '$attributesInAssets._id', '$attributes' ]
}
}
},
{
$group: {
_id: '$_id',
userId: { $first: '$userId' },
folderId: { $first: '$folderId' },
title: { $first: '$title' },
typeOfAsset: { $first: '$typeOfAsset' },
createdAt: { $first: '$createdAt' },
updatedAt: { $first: '$updatedAt' },
isActive: { $first: '$isActive' },
attributes: { $first: '$attributes' },
attributesInAssets: {
$push: '$attributesInAssets._id'
}
}
},
{
$project: {
_id: 1,
userId: 1,
folderId: 1,
title: 1,
typeOfAsset: 1,
attributes: 1,
attributesInAssets: 1,
createdAt: 1,
updatedAt: 1,
isActive: 1
}
}
]
}
}
const redact = {
$redact: {
$cond: {
if: {
$gt: [ {
$size: `$${directionOfLink}`
}, 0 ]
},
then: '$$KEEP',
else: '$$PRUNE'
}
}
}

Percentage of amount in a subdocument grouped per type in Mongoose/NodeJS

I have the following MongoDB schema:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required.']
},
transactions: [
{
categoryName: {
type: String,
required: [true, 'Category name in transaction is required.']
},
categoryType: {
type: String,
required: [true, 'Category type in transaction is required.']
},
amount: {
type: Number,
required: [true, 'Transaction amount is required.']
}
}
]})
transactions.categoryType can only be Income or Expense. Now per queried _id, I want to return the ratio/percentage of transactions.CategoryName per Income and Expense. Meaning if I have the following data:
{
"_id": 000001,
"email": "asdasd#email.com"
"transactions": [
{
"categoryName": "Food",
"categoryType": "Expense",
"amount": 200
},
{
"categoryName": "Rent",
"categoryType": "Expense",
"amount": 1000
},
{
"categoryName": "Salary",
"categoryType": "Income",
"amount": 15000
}
]
}
the result that I would want is:
{ "email": "asdasd#email.com",
"Income": [["Salary", 100]],
"Expense": [["Food", 16.67],["Rent",83.33]],
}
Now, I have the following query:
return User.aggregate([
{ $match: { _id : ObjectId(request.params.id) } },
{ $unwind : "$transactions"},
{ $group : { _id : { type: "$transactions.categoryType" },
        total: {$sum : "$transactions.amount"},
transactionsArray: { $push: "$transactions"}
        }
},
{ $project: {
_id: 0,
transactionsArray:1,
    type: "$_id.type",
total:1
}
}
])
which returns a data like this:
[
{
"total": 1200,
"transactions": [
{
"categoryName": "Food",
"categoryType": "Expense",
"amount": 200,
},
{
"categoryName": "Rent",
"categoryType": "Expense",
"amount": 1000,
}
],
"type": "Expense"
},
{
"total": 15000,
"transactions": [
{
"categoryName": "Salary",
"categoryType": "Income",
"amount": 15000,
}
],
"type": "Income"
}
]
Now, I do not know how am I going to further process the result set to divide the transactions.amount by the total to get the result that I want.
You may go with multiple steps in aggregations
$unwind to deconstruct the array
$group- first group to group by _id and $categoryType. So we can get the total amount and an amount for particular transaction. This helps to calculate the ratio.
$map helps to loop over the array and calculate the ratio
$reduce- You need comma separated string array of objects. So loop it and get the structure.
$group to group by _id only so we can get the key value pair of category type and Income/Expense when we push
$replaceRoot to make the $grp object as root which should be merged with already existing fields ($mergeObjects)
$project for remove unwanted fields
Here is the code
db.collection.aggregate([
{ "$unwind": "$transactions" },
{
"$group": {
"_id": { id: "$_id", catType: "$transactions.categoryType" },
"email": { "$first": "$email" },
"amount": { "$sum": "$transactions.amount" },
"category": {
$push: { k: "$transactions.categoryName", v: "$transactions.amount" }
}
}
},
{
$addFields: {
category: {
$map: {
input: "$category",
in: {
k: "$$this.k",
v: {
"$multiply": [
{ "$divide": [ "$$this.v","$amount" ]},
100
]
}
}
}
}
}
},
{
"$addFields": {
category: {
"$reduce": {
"input": "$category",
"initialValue": [],
"in": {
"$concatArrays": [
[
[ "$$this.k", { $toString: "$$this.v" } ]
],
"$$value"
]
}
}
}
}
},
{
"$group": {
"_id": "$_id.id",
"email": { "$first": "$email" },
"grp": { "$push": { k: "$_id.catType", v: "$category" } }
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [ { "$arrayToObject": "$grp" }, "$$ROOT" ]
}
}
},
{ "$project": { grp: 0 } }
])
Working Mongo playground

How to populate deeply nested array of ids after aggregate lookup in mongodb?

This question is an extension of previous question.
I have following documents in collection A, B, and C
"A":
{_id: "A_id1", labelA: "LabelA1"},
{_id: "A_id2", labelA: "LabelA2"}
"C":
{ _id: "C_id1", labelC: "LabelC1"},
{ _id: "C_id2",labelC: "LabelC2"}
"B":
{
_id: "B_id1",
labelB: "LabelB1",
refToA: "A_id1",
items: [
{
itemLabel: "a",
options: [ { optionLabel: "opt1", codes: [ "C_id1"] },
{ optionLabel: "opt2", codes: [ "C_id2"] } ]
}
]
},
{
_id: "B_id4",
labelB: "LabelB4",
refToA: "A_id2",
items: [
{ itemLabel: "b",
options: [ { optionLabel: "opt3", codes: [ "C_id1", "C_id2"]
}
]
}
The collection B has nested array of sub-documents in the field 'items', further nested arrya of sub-sub-document as 'items.options'. Finally, the third sub-level 'items.options.codes' contain list of ids of document C.
I want to Aggregate A to collect all B as that refer to A. I do it using the command:
db.A.aggregate([
{
$match: {
_id: "A_id1"
}
},
{
$lookup: {
from: "B",
let: {
refToA: "$_id"
},
pipeline: [
{
$match: { $expr: { $eq: ["$refToA", "$$refToA"]}}
},
],
as: "BCollection"
}
}
])
which gives the following results
{
"BCollection": [
{
"_id": "B_id1",
"items": [
{
"itemLabel": "a",
"options": [
{
"codes": [ "C_id1" ],
"optionLabel": "opt1"
},
{
"codes": [ "C_id2"],
"optionLabel": "opt2"
}
]
}
],
"labelB": "LabelB1",
"refToA": "A_id1"
}
],
"_id": "A_id1",
"labelA": "LabelA1"
}
Now, I want to preserve the above structure and also populate the field 'codes' with details from collection C. The desired result is as follows
{
"BCollection": [
{
"_id": "B_id1",
"items": [
{
"itemLabel": "a",
"options": [
{
"codes": [ { _id: "C_id1", labelC: "LabelC1"} ],
"optionLabel": "opt1"
},
{
"codes": [ { _id: "C_id2", labelC: "LabelC2"}],
"optionLabel": "opt2"
}
]
}
],
"labelB": "LabelB1",
"refToA": "A_id1"
}
],
"_id": "A_id1",
"labelA": "LabelA1"
}
I have tried the following query, but it does not produce the desired result:
db.A.aggregate([
{
$match: {
_id: "A_id1"
}
},
{
$lookup: {
from: "B",
let: {
refToA: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$refToA",
"$$refToA"
]
}
}
},
{
$lookup: {
from: "C",
localField: "items.options.codes",
foreignField: "_id",
as: "items.option.codes"
}
}
],
as: "BCollection"
}
}
])
You can see the output of above query in here: https://mongoplayground.net/p/ZJVU6PQF6MZ
Try this:
db.A.aggregate([
{
$lookup: {
from: "B",
let: { refToA: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$refToA", "$$refToA"] }
}
},
{ $unwind: "$items" },
{ $unwind: "$items.options" },
{
$lookup: {
from: "C",
localField: "items.options.codes",
foreignField: "_id",
as: "items.options.codes"
}
},
{
$group: {
_id: {
id: "$_id",
itemLabel: "$items.itemLabel"
},
labelB: { $first: "$labelB" },
refToA: { $first: "$refToA" },
items: {
$push: {
"itemLabel": "$items.itemLabel",
"options": "$items.options"
}
}
}
},
{
$group: {
_id: "$_id.id",
labelB: { $first: "$labelB" },
refToA: { $first: "$refToA" },
items: {
$push: {
itemLabel: "$_id.itemLabel",
"options": "$items.options"
}
}
}
}
],
as: "BCollection"
}
}
]);

add key to nested array with condition

I have a simple datastructure in mongodb:
{
_id: ObjectID,
name: 'Name',
birthday: '25.05.2001'
items: [
{
_id: ObjectID,
name: 'ItemName',
info: 'ItemInfo',
},
{
_id: ObjectID,
name: 'ItemName',
info: 'ItemInfo',
}
]
}
Now i want a query, that takes a ObjectID (_id) of an item as criteria and gives me back the object with all items in the array AND projects a new field "selected" with value true or false into a field in the result of each array item:
I tried that with this query:
{ $unwind: '$items' },
{
$project: {
selected: {
$cond: { if: { 'items._id': itemObjectID }, then: true, else: false },
},
},
},
but MongoDB gives me back an error:
MongoError: FieldPath field names may not contain '.'.
Have no clue why its not working, any help or ideas? Thank you very much!
What you are missing here is $eq aggregation operator which checks the condition for the equality.
You can try below aggregation here if you want to check for ObjectId then you need to put mongoose.Types.ObjectId(_id)
db.collection.aggregate([
{ "$unwind": "$items" },
{ "$addFields": {
"items.selected": {
"$eq": [
1111,
"$items._id"
]
}
}},
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"items": {
"$push": {
"_id": "$items._id",
"selected": "$items.selected"
}
}
}}
])
Will give following output
[
{
"_id": ObjectId("5a934e000102030405000000"),
"items": [
{
"_id": 1111,
"selected": true
},
{
"_id": 2222,
"selected": false
}
],
"name": "Name"
}
]
You can check it here
#Ashish: Thank you very much for your help! Your answer helped me to build the right query for me:
db.collection.aggregate([
{
$unwind: "$items"
},
{
$project: {
"items.name": 0,
"birthday": 0
}
},
{
"$addFields": {
"items.selected": {
"$eq": [
1111,
"$items._id"
]
}
}
},
{
$group: {
_id: "$_id",
"name": {
"$first": "$name"
},
items: {
$push: "$items"
}
}
},
{
$match: {
"items._id": {
$eq: 1111
}
}
},
])
and leads to a result that looks like:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"items": [
{
"_id": 1111,
"selected": true
},
{
"_id": 2222,
"selected": false
}
],
"name": "Name"
}
]

Mongoose Aggregate with Lookup

I have a simple two collections like below :
assignments:
[
{
"_id": "593eff62630a1c35781fa325",
"topic_id": 301,
"user_id": "59385ef6d2d80c00d9bdef97"
},
{
"_id": "593eff62630a1c35781fa326",
"topic_id": 301,
"user_id": "59385ef6d2d80c00d9bdef97"
}
]
and users collection:
[
{
"_id": "59385ef6d2d80c00d9bdef97",
"name": "XX"
},
{
"_id": "59385b547e8918009444a3ac",
"name": "YY"
}
]
and my intent is, an aggregate query by user_id on assignment collection, and also I would like to include user.name in that group collection. I tried below:
Assignment.aggregate([{
$match: {
"topic_id": "301"
}
},
{
$group: {
_id: "$user_id",
count: {
$sum: 1
}
}
},
{
$lookup: {
"from": "kullanicilar",
"localField": "user_id",
"foreignField": "_id",
"as": "user"
}
},
{
$project: {
"user": "$user",
"count": "$count",
"_id": "$_id"
}
},
But the problem is that user array is always blank.
[ { _id: '59385ef6d2d80c00d9bdef97', count: 1000, user: [] } ]
I want something like :
[ { _id: '59385ef6d2d80c00d9bdef97', count: 1000, user: [_id:"59385ef6d2d80c00d9bdef97",name:"XX"] } ]

Resources