Fetch records if field exists in mongodb - mongodb

I have this query
db.invites.find({$and: [{primaryowner: mynumber}, {invited: mynumber}]})
which i am using to fetch records if they satisfy the conditions above.However, i would like to fetch records where {eventtype:4} and the field primaryowner:$exists: true
This is what i have
{ eventtype: 4 },{ primaryowner: { $exists: true }}
but i know this is not it because i need to fetch records where eventtype = 4 and the field primaryowner exists.
How can i represent the query?.

From your question, I couldnot clearly understand what is the required business scenario, but with the sample data provided
I have given an pseudo code and the respective mongodb implementation of that pseudo code.
if invited field is "0800123456"
if event type is 3 and event status is 11
select matching documents
or if event type is 4 and event status is 11 and primary owner exists
select matching documents
Mongo DB Query
db.collection.find({
$or: [
{
$and: [
{
"invited": "0800123456"
},
{
"eventtype": {
$eq: "3"
}
},
{
"eventstatus": {
$eq: 11
}
}
]
},
{
$and: [
{
"eventtype": {
$eq: "4"
}
},
{
"eventstatus": {
$eq: 11
}
},
{
primaryowner: {
$exists: true
}
}
]
}
]
})
If this is not matching your scenario, then write your own query with the help of this sample code.
Hope it Helps!

Related

Conditionally update/upsert embedded array with findOneAndUpdate in MongoDB

I have a collection in the following format:
[
{
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c"),
"state": [
{
"id": ObjectId("62dffc49cb17483cf0153220"),
"notes": "these are my custom notes!",
"lvl": 3,
},
{
"id": ObjectId("62dffc49cb17483cf0153221"),
"notes": "hello again",
"lvl": 0,
},
]
},
]
My goal is to be able to update and add an element in this array in the following situation:
If the ID of the new element is not in the state array, push the new element in the array
If the ID of the new element is in the state array and its lvl field is 0, update that element with the new information
If the ID of the new element exists in the array, and its lvl field is not 0, then nothing should happen. I will throw an error by seeing that no documents were matched.
Basically, to accomplish this I was thinking about using findOneAndUpdate with upsert, but I am not sure how to tell the query to update the state if lvl is 0 or don't do anything if it is bigger than 0 when the match is found.
For solving (1) this is what I was able to come up with:
db.collection.findOneAndUpdate(
{
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c"),
"state.id": {
"$ne": ObjectId("62dffc49cb17483cf0153222"),
},
},
{
"$push": {"state": {"id": ObjectId("62dffc49cb17483cf0153222"), "lvl": 1}}
},
{
"new": true,
"upsert": true,
}
)
What is the correct way to approach this issue? Should I just split the query into multiple ones?
Edit: as of now I have done this in more than one query (one to fetch the document, then I iterate over its state array to check if the ID exists in it, and then I perform (1), (2) and (3) in a normal if-else clause)
If the ID of the new element exists in the array, and its lvl field is not 0, then nothing should happen. I will throw an error by seeing that no documents where matched.
First thing FYI,
upsert is not possible in the nested array
upsert will not add new elements to the array
upsert can add a new document with the new element
if you want to throw an error if the record does not present then you don't need upsert
Second thing, you can achieve this in one query by using an update with aggregation pipeline in MongoDB 4.2,
Note: Here i must inform you, this query will respond updated document but there will be no flag or any clue if this query fulfilled your first situation or second situation, or the third situation out of 3, you have to check in your client-side code through query response.
check conditions for postId and userId fields only
we are going to update state field under $set stage
check the condition if the provided id is present in state's id?
true, $map to iterate loop of state array
check conditions for id and lvl: 0?
true, $mergeObjects to merge current object with the new information
false, it will not do anything
false, then add that new element in state array, by $concatArrays operator
db.collection.findOneAndUpdate(
{
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c")
},
[{
$set: {
state: {
$cond: [
{ $in: [ObjectId("62dffc49cb17483cf0153221"), "$state.id"] },
{
$map: {
input: "$state",
in: {
$cond: [
{
$and: [
{ $eq: ["$$this.id", ObjectId("62dffc49cb17483cf0153221")] },
{ $eq: ["$$this.lvl", 0] }
]
},
{
$mergeObjects: [
"$$this",
{
// update your new fields here
"notes": "new note"
}
]
},
"$$this"
]
}
}
},
{
$concatArrays: [
"$state",
[
// add new element
{
"id": ObjectId("62dffc49cb17483cf0153221"),
"lvl": 1
}
]
]
}
]
}
}
}],
{ returnNewDocument: true }
)
Playrgound
Third thing, you can execute 2 update queries,
The first query, for the case: element does not present and it will push a new element in state
let response = db.collection.findOneAndUpdate({
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c"),
"state.id": { $ne: ObjectId("62dffc49cb17483cf0153221") }
},
{
$push: {
state: {
id: ObjectId("62dffc49cb17483cf0153221"),
lvl: 1
}
}
},
{
returnNewDocument: true
})
The second query on the base of if the response of the above query is null then this query will execute,
This will check state id and lvl: 0 conditions if conditions are fulfilled then execute the update fields operation, it will return null if the document is not found
You can throw if this will return null otherwise do stuff with response data and response success
if (response == null) {
response = db.collection.findOneAndUpdate({
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c"),
state: {
$elemMatch: {
id: ObjectId("62dffc49cb17483cf0153221"),
lvl: 0
}
}
},
{
$set: {
// add your update fields
"state.$.notes": "new note"
}
},
{
returnNewDocument: true
});
// not found and throw an error
if (response == null) {
return {
// throw error;
};
}
}
// do stuff with "response" data and return result
return {
// success;
};
Note: As per the above options, I would recommend you that I explained in the Third thing that you can execute 2 update queries.
What you're trying became possible with the introduction pipelined updates, here is how I would do it by using $concatArrays to concat the exists state array with the new input and $ifNull in case of an upsert to init the empty value, like so:
const inputObj = {
"id": ObjectId("62dffc49cb17483cf0153222"),
"lvl": 1
};
db.collection.findOneAndUpdate({
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c")
},
[
{
$set: {
state: {
$ifNull: [
"$state",
[]
]
},
}
},
{
$set: {
state: {
$concatArrays: [
{
$map: {
input: "$state",
in: {
$mergeObjects: [
{
$cond: [
{
$and: [
{
$in: [
inputObj.id,
"$state.id"
]
},
{
$eq: [
inputObj.lvl,
0
]
}
]
},
inputObj,
{},
]
},
"$$this"
]
}
}
},
{
$cond: [
{
$not: {
$in: [
inputObj.id,
"$state.id"
]
}
},
[
],
[]
]
}
]
}
}
}
],
{
"new": true,
"upsert": true
})
Mongo Playground
Prior to version 4.2 and the introduction of this feature what you're trying to do was not possible using the naive update syntax, If you are using an older version then you'd have to split this into 2 separate calls, first a findOne to see if the document exists, and only then an update based on that. obviously this can cause stability issue's if you have high update volume.

MongoDB - Unable to add timestamp fields to subdocuments in an array

I recently updated my subschemas (called Courses) to have timestamps and am trying to backfill existing documents to include createdAt/updatedAt fields.
Courses are stored in an array called courses in the user document.
// User document example
{
name: "Joe John",
age: 20,
courses: [
{
_id: <id here>,
name: "Intro to Geography",
units: 4
} // Trying to add timestamps to each course
]
}
I would also like to derive the createdAt field from the Course's Mongo ID.
This is the code I'm using to attempt adding the timestamps to the subdocuments:
db.collection('user').updateMany(
{
'courses.0': { $exists: true },
},
{
$set: {
'courses.$[elem].createdAt': { $toDate: 'courses.$[elem]._id' },
},
},
{ arrayFilters: [{ 'elem.createdAt': { $exists: false } }] }
);
However, after running the code, no fields are added to the Course subdocuments.
I'm using mongo ^4.1.1 and mongoose ^6.0.6.
Any help would be appreciated!
Using aggregation operators and referencing the value of another field in an update statement requires using the pipeline form of update, which is not available until MongoDB 4.2.
Once you upgrade, you could use an update like this:
db.collection.updateMany({
"courses": {$elemMatch: {
_id:{$exists:true},
createdAt: {$exists: false}
}}
},
[{$set: {
"courses": {
$map: {
input: "$courses",
in: {
$mergeObjects: [
{createdAt: {
$convert: {
input: "$$this._id",
to: "date",
onError: {"error": "$$this._id"}
}
}},
"$$this"
]
}
}
}
}
}
])

MongoDB conditional query depending on possible dates

I have a scenario where I want to pull documents that have a lastAlertSentDate field that's over 30 days old. This will run in a daily cron job. Upon querying, this field will then be reset to NOW. So it's meant to act as a "rotating 30 day window" if you will.
The complication here is that the field won't exist if it hasn't been set yet. In this edge case, we'll then have to use a createdDate field of the document to do the 30-day comparison against.
So effectively, I want something like, "If lastAlertSentDate exists, then get all docs where it's older than 30days from now. ---Otherwise, get all docs where createdDate is older than 30days from now"
So the logic between both fields are the same, it's just the field itself that can be different. Because of this, I was thinking to first USE addFields a dateToUseField and then do a match on the second stage based on this.
[
{
'$addFields': {
'dateToUse': {
'$cond': {
'if': {
'$ne': [
'$lastAlertSentDate', undefined
]
},
'then': '$lastAlertSentDate',
'else': '$createdDate'
}
}
}
}, {
'$match': {
'dateToUse': {
'$lte': '30_DAYS_PRIOR'
}
}
}
]
So the else part doesn't seem to work. It doesn't assign $createdDate to dateToUse.
What am I missing? Also, how can I condense this? I'm sure I don't need the addFields first and I can do everything within the $match
You have two options here:
Use a $or query with two predicates, where each of them is a $and predicate:
Either lastAlertSentDate does not exists and createdDate > n
Or lastAlertSentDate exists and it is > n
Playground Link
db.collection.find({
$or: [
{
$and: [
{
"lastAlertSentDate": {
"$exists": false
}
},
{
"createdDate": {
$gt: 5
}
}
]
},
{
$and: [
{
"lastAlertSentDate": {
"$exists": true
}
},
{
"lastAlertSentDate": {
$gt: 5
}
}
]
}
]
})
Use an aggregation using the $ifNull
Playground Link
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
"$ifNull": [
"$lastAlertSentDate",
"$createdDate"
]
},
5
]
}
}
}
])

MongoDB conditionial update

I am running a bit in circles here and would appreciate some help. What I am looking to do is either update or create a nested object contained in an array depending on whether this object exists.
I have a users collection and a user document has the following structure:
{
schema_version: 1,
display_name: 'xxxxxx',
email: 'xxxxxx',
email_verified:'xxxxxx',
...
custom_data: {
stripe_id: 'xxx',
subscriptions: [{
subscription_id: xxxx,
....
}],
...
},
}
In webhook calls from Stripe I am getting a subscription object with a subscription_id and a stripe_id.
What I want to do is check if subscription_id exists, if so, update the document, if not then create the document in the subscriptions array for the user document where stripe_id matches.
If I do something along the lines of:
db.collection.update(
{subscription_id: subscription.id},
{ $set: { 'custom_data.subscriptions': subscriptionData } },
{ upsert: true }
)
The problem is that I am creating subscription objects not bound to my user document where stripeID matches.
On the other hand, if I do something like this:
db.collection.update(
{'custom_data.stripe_id': stripe_id},
{ $set: { 'custom_data.subscriptions': subscriptionData } },
{ upsert: true }
)
I will potentially end up creating dupes in the subscriptions array when, in fact I would want to update the existing object where subscription_id matches.
Is there any way to do that in one query with Mongo, or will I have to resort to using 2 queries in an if statement?
Thanks in advance for any clarification on this.
You can do the followings with an aggregation pipeline:
$match with $or condition to search for custom_data.subscriptions.subscription_id or custom_data.stripe_id
$addFields with $map to conditional update your subscription object when matched
$addFields with $setUnion to insert an entry of incoming subscription object for the insert case
$merge to update the back into the original collection
db.collection.aggregate([
{
$match: {
$expr: {
$or: [
{
$eq: [
"$custom_data.subscriptions.subscription_id",
"xxxx"
]
},
{
$eq: [
"$custom_data.stripe_id",
"xxx"
]
}
]
}
}
},
{
"$addFields": {
"custom_data": {
subscriptions: {
"$map": {
"input": "$custom_data.subscriptions",
"as": "s",
"in": {
"$cond": {
// if subscription_id matched, replace with your incoming object
"if": {
$eq: [
"$$s.subscription_id",
"xxxx"
]
},
"then": {
subscription_id: "incoming_sub_id"
},
// if not matched, keep the original object
"else": "$$s"
}
}
}
}
}
}
},
{
"$addFields": {
"custom_data": {
subscriptions: {
// insert case; if the subscription array is empty, then union with your incoming object
$setUnion: [
"$custom_data.subscriptions",
[
{
subscription_id: "incoming_sub_id"
}
]
]
}
}
}
},
{
"$merge": {
"into": "collection",
"on": "_id",
"whenMatched": "replace"
}
}
])
Here is the Mongo playground for your reference.

MongoDB, How Do I combine a find and sort with the $cond in aggregation?

I have written a find query, which works, the find query returns records where name and level exist
db.docs.find( { $and: [{name:{$exists:true}},{level:{ $exists:true}} ] },{_id:0, name:1}).sort({"name":1})
and now want to combine it with something like the code below which also works, but needs to be merged with the above to pull the correct data
db.docs.aggregate(
[
{
$project:
{
_id:0,
name: 1,
Honours:
{
$cond: { if: { $gte: [ "$level", 8 ] }, then: "True", else: "False" }
}
}
}
]
)
The find query returns records where name and level exist, but I need to enhance the result with new column called Honours, showing True of False depending on whether the level is gte (greater than or equal to 8)
So I am basically trying to combine the above find filter with the $cond function (which I found and modified example here : $cond)
I tried the below and a few other permutations to try and make find and sort with the $project and$cond aggregate, but it returned errors. I am just very new to how to construct mongodb syntax to make it all fit together. Can anyone please help?
db.docs.aggregate(
[{{ $and: [{name:{$exists:true}},{level:{ $exists:true}} ] },{_id:0, name:1}).sort({"name":1}
{
$project:
{
_id:0,
name: 1,
Honours:
{
$cond: { if: { $gte: [ "$level", 8 ] }, then: "True", else: "False" }
}
}
}
}
]
)
Try below aggregation pipeline :
db.docs.aggregate([
/** $match is used to filter docs kind of .find(), lessen the dataset size for further stages */
{
$match: {
$and: [{ name: { $exists: true } }, { level: { $exists: true } }]
}
},
/** $project works as projection - w.r.t. this projection it will lessen the each document size for further stages */
{
$project: {
_id: 0,
name: 1,
Honours: {
$cond: { if: { $gte: ["$level", 8] }, then: "True", else: "False" }
}
}
},
/** $sort should work as .sort() */
{ $sort: { name: 1 } }
]);