MongoDB - How to compute field based on other fields existence? - node.js

Suppose I have a trips collection where I would like to compute the status of each trip. The status is calculated based on other fields.
If the trip has no driver it should be pending.
If the trip has a driver it should be in transit.
If the trip has a driver and an invoice number it should be completed.
If the trip has a driver and an invoice number and a bill number it should be billed.
How could I implement this using the aggregation pipeline?
Example docs :
[{
// This has both driver and invoice, should be completed
"_id" : ObjectId("5e24fbfd44621900c5730a48"),
"customer" : ObjectId("5dd7eaf7ef8a7b00ba8f090b"),
"date" : ISODate("2020-01-17T03:00:00.000Z"),
"distance" : 24, // in km
"driver" : ObjectId("5e1e302e26f00000c451923e"),
"invoice" : "0001-00001234",
"status": "completed" // this should be calculated
},
{
// This has only driver, should be in transit
"_id" : ObjectId("5e24fbfd44621900c5730a48"),
"customer" : ObjectId("5dd7eaf7ef8a7b00ba8f090b"),
"date" : ISODate("2020-01-17T03:00:00.000Z"),
"distance" : 24, // in km
"driver" : ObjectId("5e1e302e26f00000c451923e"),
"status": "in transit" // this should be calculated
},
{
// This is missing both driver and invoice, should be pending
"_id" : ObjectId("5e24fbfd44621900c5730a48"),
"customer" : ObjectId("5dd7eaf7ef8a7b00ba8f090b"),
"date" : ISODate("2020-01-17T03:00:00.000Z"),
"distance" : 24, // in km
"status": "pending" // this should be calculated
}]

You can try this, it uses $switch to check all cases in series & $type check for field existence :
db.trips.aggregate([{
$addFields: {
status: {
$switch:
{
branches: [
{
case: { $eq: [{ $type: '$driver' }, 'missing'] },
then: 'pending'
},
{
case: { $eq: [{ $type: '$driver' }, 'objectId'] },
then: 'in transit'
},
{
case: {
$and: [{ $eq: [{ $type: '$driver' }, 'objectId'] },
{ $eq: [{ $type: '$invoice' }, 'string'] }]
},
then: "completed"
},
{
case: {
$and: [{ $eq: [{ $type: '$driver' }, 'objectId'] },
{ $eq: [{ $type: '$invoice' }, 'string'] },
{ $eq: [{ $type: '$billNumber' }, 'string'] }]
},
then: "billed"
}
],
default: '$status'
}
}
}
}
])
Ref : MongoDB-Playground

One solution would be this one:
db.col.updateMany(
{
driver: { $exists: true },
invoice: { $exists: true }
},
{ $set: {status: "completed"}}
)
db.col.updateMany(
{
driver: { $exists: false },
invoice: { $exists: false }
},
{ $set: {status: "pending"}}
)
db.col.updateMany(
{
driver: { $exists: true },
invoice: { $exists: false }
},
{ $set: {status: "in transit"}}
)
Or in a single command witn Aggregation pipeline (Starting in MongoDB 4.2):
db.col.updateMany(
{},
[{
$set: {
status: {
$switch: {
branches: [
{ case: { $and: ["$driver", "$invoice"] }, then: "completed" },
{ case: { $and: ["$driver", { $not: "$invoice" }] }, then: "in transit" },
{ case: { $and: [{ $not: "$driver" }, { $not: "$invoice" }] }, then: "pending" },
],
default: "$status"
}
}
}
}]
)

Related

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

Update object with value of array

For a project where we have actions and donations. We store the donations in an array in the related action. For the connection we use Mongoose.
The schema for an action is as follows, for readability I've removed some fields which are not related to this problem:
const donationSchema = new Schema(
{
id: {
type: String,
unique: true,
required: true,
index: true,
},
amount: { type: Number },
status: {
type: String,
enum: ['pending', 'collected', 'failed'],
default: 'pending',
},
},
{ timestamps: true, versionKey: false, _id: false },
);
const schema = new Schema(
{
donations: { type: [donationSchema], default: [] },
target: { type: Number, default: 0 },
collected: { type: Number, default: 0 },
},
{
timestamps: true,
versionKey: false,
},
);
const Action = model<IAction>('Action', schema);
Let say I have an Action with three donations, one in every state:
{
"_id": "6098fb22101f22cfcbd31e3b"
"target": 10000,
"collected": 25,
"donations": [
{
"uuid": "dd90f6f1-56d7-4d8b-a51f-f9e5382d3cd9",
"amount": 25,
"status": "collected"
},
{
"uuid": "eea0ac5e-1e52-4eba-aa1f-c1f4d072a37a",
"amount": 10,
"status": "failed"
},
{
"uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1",
"amount": 50,
"status": "pending"
}
]
}
Now I want to update the pending donation to collected.
This would be
Action.findOneAndUpdate(
{
_id: '6098fb22101f22cfcbd31e3b',
'donations.id': '215237bd-bfe6-4d5a-934f-90e3ec9d2aa1',
},
{
$set: {
'donations.$.status': 'collected',
},
},
{
upsert: false,
returnOriginal: false,
}
).then((action) => console.log(action);
I want to update the status to collected, but also update the collected so that it is the same as all the donations with status equal to collected. I thought of using the $inc operator, but this keeps saying that donations.$.amount is not a number and therefore not able to increment collected.
Is there a way to do this in the same update call? The reason why I cannot get the object and just count collected amount is that maybe two donation callbacks occur at the same time, so we don't want the to overwrite the previous given amount.
This aggregation can help you I believe:
db.collection.aggregate([
{
"$match": {
_id: "6098fb22101f22cfcbd31e3b"
}
},
{
"$set": {
"donations.status": {
"$reduce": {
"input": "$donations",
"initialValue": {
uuid: "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
},
"in": {
$cond: [
{
$eq: [
"$$this.uuid",
"$$value.uuid"
]
},
"collected",
"$$this.status"
]
}
}
}
}
},
{
"$set": {
"collected": {
"$reduce": {
"input": "$donations",
"initialValue": "$collected",
"in": {
$cond: [
{
$eq: [
"$$this.status",
"collected"
]
},
{
$sum: [
"$$value",
"$$this.amount"
]
},
"$$value"
]
}
}
}
}
}
])
Edit: Above aggregation wasn't properly update status field to "collected" dunno why..
But update query below should work. I couldn't test it too. So, please let me know if something goes wrong.
db.collection.update({
"_id": "6098fb22101f22cfcbd31e3b"
},
{
"$set": {
"donations.$[element].status": "collected",
"$inc": {
"donations.$[element].amount": {
"$cond": [
{
"$eq": [
"donations.$[element].status",
"collected"
]
},
"donations.$[element].amount",
"collected"
]
}
}
}
},
{
"arrayFilters": [
{
"element.uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
}
]
})

MongoDB aggregate lookup match not working the same without lookup

Can you please help? I'm trying to aggregate data over the past 12 months by both ALL publication data specific to a certain publisher and per publication to return a yearly graph data analysis based on the subscription type.
Here's a snapshot of the Subscriber model:
const SubscriberSchema = new Schema({
publication: { type: Schema.Types.ObjectId, ref: "publicationcollection" },
subType: { type: String }, // Print, Digital, Bundle
subStartDate: { type: Date },
subEndDate: { type: Date },
});
Here's some data for the reader (subscriber) collection:
{
_id: ObjectId("5dc14d3fc86c165ed48b6872"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-20T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6871"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6870"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-18T00:00:00.000Z",
subtype: "digital"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6869"),
publication: ObjectId("5d8b36c3148c1e5aec64662c"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
}
The publication model has plenty of fields but the _id and user fields are the only point of reference in the following queries.
Here's some data for the publication collection:
// Should use
{ "_id": {
"$oid": "5d8b36c3148c1e5aec64662c"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Should use
{ "_id": {
"$oid": "5d89db9d82273f1d18970deb"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Shouldn't use as deleted === true
{ "_id": {
"$oid": "5d89db9d82273f1d18970dec"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": true
},
// Shouldn't use as different user ID
{ "_id": {
"$oid": "5d89db9d82273f1d18970dfc"
},
"user": {
"$oid": "5d24bbd89f09024590db9efd"
},
"isDeleted": true
}
When I do a lookup on a publication ID with the following, I'm getting perfect results:
Subscriber.aggregate([
{
$match: {
$and: [
{ 'publication': mongoose.Types.ObjectId(req.params.id) },
],
"$expr": { "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
}
},
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$subType" },
count: { $sum: 1 }
},
},
{
/* sort descending (latest subscriptions first) */
$sort: {
'_id.year': -1,
'_id.month': -1
}
},
{
$limit: 100,
},
])
However, when I want to receive data from the readercollections (Subscriber Model) for ALL year data, I'm not getting the desired results (if any) from all of the things I'm trying (I'm posting the best attempt result below):
Publication.aggregate([
{
$match:
{
user: mongoose.Types.ObjectId(id),
isDeleted: false
}
},
{
$project: {
_id: 1,
}
},
{
$lookup: {
from: "readercollections",
let: { "id": "$_id" },
pipeline: [
{
$match:
{
$expr: {
$and: [
{ $eq: ["$publication", "$$id"] },
{ "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
],
}
}
},
{ $project: { subStartDate: 1, subType: 1 } }
],
as: "founditem"
}
},
// {
// /* group by year and month of the subscription event */
// $group: {
// _id: { year: { $year: "$founditem.subStartDate" }, month: { $month: "$foundtitem.subStartDate" }, subType: "$founditem.subType" },
// count: { $sum: 1 }
// },
// },
// {
// /* sort descending (latest subscriptions first) */
// $sort: {
// '_id.year': -1,
// '_id.month': -1
// }
// },
], function (err, result) {
if (err) {
console.log(err);
} else {
res.json(result);
}
})
Which returns the desired data without the $group (commented out) but I need the $group to work or I'm going to have to map a dynamic array based on month and subtype which is completely inefficient.
When I'm diagnosing, it looks like this $group is the issue but I can't see how to fix as it works in the singular $year/$month group. So I tried the following:
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$founditem.subType" },
count: { $sum: 1 }
},
},
And it returned the $founditem.subType fine, but any count or attempt to get $year or $month of the $founditem.subStartDate gave a BSON error.
The output from the single publication ID lookup in the reader collection call that works (and is plugging into the line graph perfectly) is:
[
{
"_id": {
"year": 2019,
"month": 11,
"subType": "digital"
},
"count": 1
},
{
"_id": {
"year": 2019,
"month": 11,
"subType": "print"
},
"count": 3
}
]
This is the output I'd like for ALL publications rather than just a single lookup of a publication ID within the reader collection.
Thank you for any assistance and please let me know if you need more details!!

MongoDB: query multiple $elemmatch with $all

In my app, I have a polls collection and the a document looks like this;
/* POLLS */
{
...
"type": "COMMON",
"__v": {
"$numberInt": "0"
},
"targets": [
{
"code": "city",
"logic": "eq",
"value": "4"
},
{
"code": "city",
"logic": "eq",
"value": "15"
}
]
}
And the targeting key is an array contains of objects of targets.
Each target's code can be city or gender.
So it means, "gender" may not be found in a Poll document.
Depending on that when fetching Polls, I want the ones matching with my User's fields, if the code exists.
For example; When fetching the Polls from a User's feed, If a Poll contains gender targeting, I want to find polls matching with my User's gender.
My code is like this:
Poll.find({
state: "PUBLISHED",
approval: "APPROVED",
$or: [
{
targets: {
$exists: true,
$eq: []
}
},
{
targets: {
$exists: true,
$ne: [],
$all: [
{
$elemMatch: {
code: {
$exists: true,
$eq: "city"
},
value: {
$eq: foundUser.cityCode // equals to 4 in this example
}
}
},
{
$elemMatch: {
code: {
$exists: true,
$eq: "gender"
},
value: {
$eq: foundUser.gender // equals to m in this example
}
}
}
]
}
}
]
})
However, the list returns empty for the Poll document I wrote above.
When the part below commented out, the code returns the correct polls.
Poll.find({
state: "PUBLISHED",
approval: "APPROVED",
$or: [
{
targets: {
$exists: true,
$eq: []
}
},
{
targets: {
$exists: true,
$ne: [],
$all: [
{
$elemMatch: {
code: {
$exists: true,
$eq: "city"
},
value: {
$eq: foundUser.cityCode // equals to 4 in this example
}
}
},
// {
// $elemMatch: {
// code: {
// $exists: true,
// $eq: "gender"
// },
// value: {
// $eq: foundUser.gender // equals to m in this example
// }
// }
// }
]
}
}
]
})
May anyone help me with the situation that return the right polls when gender or country targeted?
Thanks, Onur.
Return targets field is an array whose elements match the $elemMatch criteria in array
Try that query
Poll.find({
state: "PUBLISHED",
approval: "APPROVED",
$or: [
{
targets: {
$exists: true,
$eq: []
}
},
{
targets: {
$exists: true,
$ne: [],
$all: [
{
$elemMatch: {
code: {
$exists: true,
$eq: "city"
},
value: {
$eq: "4" // equals to 4 in this example
}
}
}
]
}
},
{
targets: {
$exists: true,
$ne: [],
$all: [
{
$elemMatch: {
code: {
$exists: true,
$eq: "gender"
},
value: {
$eq: 'm' // equals to m in this example
}
}
}
]
}
}
]
})

Mongo Aggregate - Get count of how many times multiple values appear in query results

I'm attempting to count the number of times two separate fields are true. I have two values "clickedWouldRecommend" and "clickedWouldNotRecommend". These values are defaulted too FALSE. When a button is clicked in the interface, they are set too TRUE. I'm trying to see how many clickedWouldRecommend = true and how many clickedWouldNotRecommend = true for each branch.name.
db.appointments.aggregate([
{
$match: {
$and: [
{
'branch.org_id': '100000'
},
{ "analytics.clickedWouldRecommend": true },
// Add OR statement to include analytics.clickedWouldNotRecommend = true?
]
}
},
{
$group: {
_id: '$branch.name',
wouldRecommend: { $sum: 1 }
}
}
])
This provides results similar to:
{
"_id": [ 'Clinic Name' ],
"wouldRecommend": 115.0
}
I need to modify the query to also look for cases where analytics.clickedWouldNotRecommend is set to true. I'm trying to get output similar to this ( also notice removing the array from _id if possible ):
{
"name": 'Clinic Name'
"wouldRecommend": 115,
"wouldNotRecommend": 10
},
{
"name": 'Second Clinic Name'
"wouldRecommend": 200,
"wouldNotRecommend": 12
}
Here is the truncated model / schema:
{
branch: [
{
name: {
type: String,
required: true
},
clinic_id: {
type: String,
required: true
},
org_id: {
type: String
}
}
],
analytics: {
clickedWouldRecommend: {
type: Boolean,
default: false
},
clickedWouldNotRecommend: {
type: Boolean,
default: false
}
},
date: {
type: Date,
default: Date.now
}
};
You can use below aggregations
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$facet": {
"wouldRecommend": [
{ "$match": { "analytics.clickedWouldRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
],
"wouldNotRecommend": [
{ "$match": { "analytics.clickedWouldNotRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
]
}}
])
Or
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$group": {
"_id": "$branch.name",
"wouldRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true] }, 1, 0]
}
},
"wouldNotRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true]}, 1, 0]
}
}
}}
])

Resources