MongoDB Atlas triggers - access different collection - mongodb

I have a trigger on new documents in collection A, in which I want to get a related record from the users collections.
I (think) I followed the documentation to the "t", but both the collection itself comes out empty and the actual user I'm trying to fetch. And ideas what I'm doing wrong?
exports = async function trigger(changeEvent) {
const toolRecord = changeEvent.fullDocument;
const usersCollection = await context.services
.get("Cluster0")
.db("mind-tools")
.collection("users");
const user = await usersCollection.find({ _id: toolRecord.userId });
const users = await usersCollection.find({});
const text = [
"New tool record - " + toolRecord._id,
toolRecord.toolKey,
"users",
JSON.stringify(usersCollection),
JSON.stringify(users[0]),
"user",
toolRecord.userId,
JSON.stringify(user),
user.name,
user.email,
]
.filter(Boolean)
.join("\n");
}

Try to use findOne instead of find for the user.
Replace:
const user = await usersCollection.find({ _id: toolRecord.userId });
with:
const user = await usersCollection.findOne({ _id: toolRecord.userId });

Is it possible to console.log() different variables in your script to see what value they got in run time?
Also in the example provided in Atlas tutorial, the first line is
exports = async function (changeEvent) { ...
Without the keyword "trigger"

Related

How do you find a document by id in a MongoDB Realm Function?

I'm writing a function in MongoDB Realm that I want to use as a custom resolver for GraphQL. Part of the function should get a specific document by its id. According to the documentation I can get access to the Atlas DB by using context.services.get('mongodb-atlas'). Here's the simplified code extract:
exports = async () => {
const cluster = context.services.get('mongodb-atlas');
const collection = cluster.db('my-db').collection('my-coll');
return await collection.findOne({"_id": "61d33d2059c38ef0584c94f8"});
}
However, the findOne() call returns null. I know that the id above is valid, because I can successfully call collection.find() and list all the documents with their ids.
What I've tried
findOne({"_id": "61d..."}) - returns null
findOne({"_id": new ObjectId("61d...")}) - error: "ObjectId" is not defined.
Explicitly declaring ObjectId with const ObjectId = require('mongodb').ObjectId as recommended here - error: "MongoDB Node.js Driver is not supported".
Declaring ObjectId with const ObjectId = require('bson').ObjectId - findOne() returns null again.
Can you recommend any other avenue by which to do this lookup?
First thing I would suggest is - let's identify where the script is failing.
We know for certain, the below should work.
exports = async () => {
let tmp_collection = context.services.get("mongodb-atlas").db("my-db").collection("my-coll");
return await tmp_collection.findOne({});
}
After you can confirm that the above works, give this a try:
return await context.services.get("mongodb-atlas").db("my-db").collection("my-coll").findOne({"_id":new BSON.ObjectId("61d33d2059c38ef0584c94f8")});
Here is your code, but fixed:
exports = async () => {
const cluster = context.services.get('mongodb-atlas');
const collection = cluster.db('my-db').collection('my-coll');
return await collection.findOne({"_id": new BSON.ObjectId("61d33d2059c38ef0584c94f8")});
}

Why does express/mongodb updateOne post allow $set of some values, but will not update others?

I am baffled by this post method, it will update fields 'x and y', but any attempt to set an array of widgets fails.
It is finding the correct item to update, passing all the required information through, but it will not allow insertion of, or update to 'widgets' fields.
Even if I remove the data intended for widgets and arbitrarily send through 'foo' it will not update with a field 'widgets'.
What am I doing wrong here???
API Call to Update Widgets. The Arbitrary X and Y values will update on the database, but any attempt to update widget makes no change
const saveUpdatedWidgets = async (update, _id) => {
console.log("called to update widgets ",update.widgets," in pagecard saveUpdatedWidgets")
let widgetObject = []
for(let u=0;u<update.widgets.length;u++){
widgetObject.push({
id: update.widgets[u].id,
text: update.widgets[u].text
})
}
Api.withToken().post('/pagewidget/'+_id,
{widgets: widgetObject, x:250, y:250}
).then(function (response) {
console.log("?worked ",response.data)
}).catch(function (error) {
console.log("page save failed for some reason on pagecard: ",error.response);
});
};
This will return the following in the console:
Code for post method is:
//THIS ROUTER WILL NOT UPDATE ANY WIDGETS FOR SOME REASON
router.post('/pagewidget/:_id',auth, async(req,res)=>{
console.log("request to update ",req.body," for id ",req.params," in pagewidgetsave post")
const query = { "_id": req.params };
const addedWidgets = req.body;
const newValues = { $set: addedWidgets }
try {
const thePage = await Pages.updateOne( query, newValues);
res.status(201).send(thePage)
console.log("updated Page: ",thePage);
}
catch(e){
console.log(e);
res.status(400).send(e)
}
})
Results from the console running node shows that values are going through, but only x and y actually update in database..
Here is the axios api.js file if there are any issues here:
import axios from 'axios';
const baseURL = process.env.REACT_APP_BASE_URL || "http://localhost:3001"
export default {
noToken() {
return axios.create({
baseURL: baseURL
});
},
withToken() {
const tokenStr = window.sessionStorage.getItem("token")
return axios.create({
baseURL: baseURL,
headers: {"Authorization" : `Bearer ${tokenStr}`}
});
}
}
What is going on!!?? It finds the page OK, and updates x and y values, but can't update widgets, even if the values for widget are just a string or number...
I found the issue. the MongoDB documentation doesn't mention this too well, and in its examples for updateOne() it passes an object for the update argument.
BUT, if you are setting a new field, this argument must be wrapped inside an array to use $set, this is because it can accept both methods to $set and to $unset. (see mongoDB docs)
(i.e. updateOne({query} , [{$set: {field:"value"}, {$unset: {otherfield:"othervalue"}])
In the end the post method just had to change to const thePage = await Pages.updateOne( query, [newValues]); (with newValues stored as an object inside an array, to allow addition of $unset if it was needed.
This is why it would update existing values OK, but it would not set new values into the database.
What a journey....
Full code for post method here
router.post('/pagewidget/:_id',auth, async(req,res)=>{
const query = {"_id": req.params._id};
const addedWidgets = req.body;
const newValues = { $set: addedWidgets }
try {
const thePage = await Pages.updateOne( query, [newValues]);
res.status(201).send(thePage)
console.log("updated Page: ",thePage);
}
catch(e){
console.log(e);
res.status(400).send(e)
}
})

Discord.js/Mongodb How to update all database values

I am currently creating an economy bot for Discord, and i have a problem i don't know how to solve:
All users have a salary, that will be set with a command. I want to create a command that can update all users account with the set salary amount.
Instead of having to ping a specific user, how could i make it that the command would update values of all found users in the MongoDB Database?
Here is current code:
const profileModel = require("../models/profileSchema");
module.exports = {
name: 'pay',
aliases: [],
permissions: ["ADMINISTRATOR"],
description: "pay users their salary",
async execute(message, args, cmd, client, discord, profileData) {
const amount = profileData.pay;
const target = message.mentions.users.first();
try{
await profileModel.findOneAndUpdate({
userID: target.id,
}, {
$inc: {
bank: amount,
},
}
);
return message.channel.send(`Users have been paid.`);
} catch(err) {
console.log(err);
}
},
};
As you can see, currently its waiting for the user to ping a user. But i would want it to just update all found users inside the Database without needing to specify who it is.
I would really appereciate help!
Mongo has the method updateMany, docs found here, which updates all documents if the query is set to an empty object. So, just try:
await profileModel.updateMany({}, {
$inc: {
bank: amount,
},
}
);

Cloud Function that moves documents between collection

I have three collections: "past", today", and "future".
"today" collection is supposed to have only one document.
At midnight, I need to find in my "future" collection a document that has "next" field, or, if there is no such document, find one that have value in "number" field closest grater than value in "number" field of the document in my "today" collection. Then I need to move the "today"'s document into "past" collection, and also to move that found document from "future" collection to the "today" collection.
As far as I understand, there is no "move" method, so I have to use combination of deletes and creates which need to be done in one transaction.
I figured out how to do a "scheduler" part, but can't figure out how to code the rest (the actual moving of the documents).
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firestore = admin.firestore();
exports.scheduledFunction = functions.pubsub.schedule('0 0 * * *')
.onRun((context) => {
//I need to move my documents...
});
Could you help me with the code, please?
It might me that you are looking for a documentation in a wrong place. Its not in the Firestore/Extend with cloud functions. Its in Firestore basic documentation, but you have to switch the code type to node-js. https://firebase.google.com/docs/firestore/query-data/order-limit-data
You have to collect data by two queries: in today and future collections.
By these queries you get the docs and its' data.
Than you just need to make a doc in past, delete and make new a doc (or rewrite the existing one) in today, and to delete in future.
There is how I would do it in a simple callable function:
exports.scheduledFunction = functions.pubsub.schedule('0 0 * * *')
.onRun(async (context) => {
try {
let queryToday = admin.firestore().collection('today'); //you can add .limit(1)
const todaySnapshot = await queryToday.get();
const todayDoc = todaySnapshot.docs[0];
const todayData = todayDoc.data();
const todayToPastRef = admin.firestore().doc(`past/${todayData.documentUid}`);
/* or how the id is stored? you can just call
const todayToPastRef = admin.firestore().collection('past').doc()
and it will be generated automatically
*/
const promises = [];
promises.push(todayToPastRef.set(todayData));
let queryFuture = admin.firestore().collection('future').orderBy('date').limit(1);
/*
or how is the date stored? Idk if firebase allows to query by Timestamp
you just want to fetch the closest date after today so the order is ascending
*/
const futureSnapshot = await queryFuture.get();
const futureDoc = futureSnapshot.docs[0];
const futureData = futureDoc.data();
const futureToTodayRef = admin.firestore().doc(`today/${futureData.documentUid}`);
promises.push(futureToTodayRef.set(todayData));
promises.push(futureDoc.ref.delete());
promises.push(todayDoc.ref.delete());
/*
or you can try to change today's doc data, but the id will remain the same
promises.push(todayDoc.ref.update(futureData))
*/
return Promise.all(promises); // function will be executed after all the promises are fullfilled or rejected
} catch (err) {
return Promise.reject(err);
}
});
Note that instead of .then() and .catch() im using async/await.
Use console.log() for debugging, and try VSCode, so you can inspect methods and properties on the objects, which is rly helpful
UPDATE:
Yes, you can do it with a batch. There is another example:
exports.scheduledFunction = functions.pubsub.schedule('0 0 * * *').onRun((context) => {
const db = admin.firestore();
let queryToday = db.collection('today');
let queryFuture = db.collection('future').orderBy('date').limit(1);
const batch = db.batch();
return queryToday
.get()
.then(todaySnapshot => {
const todayDoc = todaySnapshot.docs[0];
const todayData = todayDoc.data();
const todayToPastRef = db.doc(`past/${todayData.docUid}`);
batch.set(todayToPastRef, todayData);
batch.delete(todayDoc.ref);
return queryFuture.get();
})
.then(futureSnapshot => {
const futureDoc = futureSnapshot.docs[0];
const futureData = futureDoc.data();
const futureToTodayRef = db.doc(`today/${futureData.docUid}`);
batch.set(futureToTodayRef, futureData);
batch.delete(futureDoc.ref);
// now two operations are completed, you just can commit the batch
return batch.commit();
})
.catch(err => {
// if todaySnapshot or futureSnapshot were not fetched, batch wont be commited
// or, for example, if snapshots were empty
return Promise.reject(err)
});
});
You can also fetch documents in parallel with .getAll() or something like that. You should test and experiment

how to read back result when using mongodb transaction?

All mongodb transaction examples I have seen so far don't allow reading results back. For example (pseudo code):
begin_transaction
collection_one.update(...)
collection_two.update(...)
commit_transaction
The problem is, what if I want to update collection_two based on the result of updating collection_one?
For example?
begin_transaction
result = collection_one.update(...)
if (result.update_count() > 0)
{
collection_two.update(...)
}
commit_transaction
I have never seen an example like the above? It seems that when use transaction, I can't get the result back.
Another example,
begin_transaction
result = collection_children.find({name: 'xxx'})
collection_team.delete({name in result})
commit_transaction
Basically, I want to perform a find on a collection, and based the find result to perform a second action on a different collection.
And I want the 2 actions together be atomic.
Here is an example of how this works as expected with Mongoose. The same example obviously is possible without Mongoose.
var author = new Author({ email: "test22#test.com", other: [{a: 1}});
var book = new Book({ title: 'ABC' })
let doFoo = async () => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const opts = { session, new: true };
let _author = await author.save() // Notice we get back the record
let _book = await book.save()
// Notice we are using now the new id of the just saved record
await Author.findOneAndUpdate({ _id: _author.id }, { $set: { other: { foo: 2 } }}, opts);
await Book.findOneAndUpdate({ _id: _book.id }, { $set: { title: "ABC" }}, opts);
await session.commitTransaction();
session.endSession();
} catch (error) {
await session.abortTransaction();
session.endSession();
throw error; // Rethrow so calling function sees error
}
}
doFoo()
So in the example above we create/save two different records in their respective collections and after that based on the new records we go back and update them.