I'm finding document by _id from array. Then I change the field 'balance' and save the document. This action is performed five times, but the balance is changed only once. Code:
users.forEach(async(user) => {
if (user.active) {
/* SOME STUFF */
const owner = await Owner.findById(user.owner)
owner.balance = (owner.balance * 100 - 0.01 * 100) / 100
await owner.save()
}
})
Tried like that:
Owner.findById(user.owner).then((owner) => {
owner.balance = (owner.balance * 100 - 0.01 * 100) / 100
owner.save(err => {
if(err) console.log(err)
})
})
Also tried:
owner.markModified(balance);
Owner schema:
const ownerSchema = new Schema({
balance: {
type: Number
}
});
Test output from console
On this picture you can see test output. Balance before is 99.53.
As a result, the balance should be 99.48, but it is 99.52.
You might be expecting that each user would be processed one by one inside forEach(), but this is not what would happen, everything would rather run simultaneously.
What you would need to do for your code to run in order is to use for...of instead, which would ensure execution order. Something like below:
for(let user of users) {
if (user.active) {
/* SOME STUFF */
const owner = await Owner.findById(user.owner)
owner.balance = (owner.balance * 100 - 0.01 * 100) / 100
await owner.save()
}
}
why don't you try something like
Owner.findByIdAndUpdate()
it will find owner by id & update it it is shortened and better (clean) version of your code
The problem lies in your use of the forEach function.
when you write async in-front of a function you basically wrap it as a promise meaning its return value is changed to be a promise.
forEach does not have a return value, meaning when you wrap it as a promise no-one is waiting for that promise, that leads to unexpected behaviour. and in your case it actually does update 5 times, but not synchronicity as you expect.
i recommend using a library like bluebird as it gives alot of power, the code would look like this:
let Promise = require("bluebird")
....
let users = [user array];
await Promise.mapSeries(users, (user) => {
if (user.active) {
/* SOME STUFF */
const owner = await Owner.findById(user.owner)
owner.balance = (owner.balance * 100 - 0.01 * 100) / 100
await owner.save()
}
})
it will now work as you want.
Related
exports.resetDailyFinalKills = functions.pubsub
.schedule("58 16 * * *")
.onRun(async (context) => {
const players = firestore.collection("players");
const goodtimes = await players.where("final_kills", ">", 0);
goodtimes.forEach((snapshot) => {
snapshot.ref.update({final_kills: 0});
});
return null;
});
So I have this cloud function, and when I force run it nothing happens at all like it just says the function was successful but the final_kills field never gets updated. Can anyone help?
Like I obviously have a player here which has a final_kills value that is greater than 0. So why doesn't this reset that back down to zero?
Note sure if I am missing something here but:
You actually try to iterate over the Query object firebase creates when using the where() function on your collections. You actually never fetch the data from the database.
const players = firestore.collection("players");
// fetch the objects from firestore
const goodtimes = await players.where("final_kills", ">", 0).get();
// iterate over the docs you receive
goodtimes.docs.forEach((snapshot) => {
snapshot.ref.update({ final_kills: 0 });
});
Edit (regarding your comment):
Make sure you set your timezone properly after your .schedule() function:
// timezone in my case
functions.pubsub.schedule('5 11 * * *').timeZone('Europe/Berlin')
Check this list as a reference for your correct timezone.
I have a collection of teams containing around 80 000 documents. Every Monday I would like to reset the scores of every team using firebase cloud functions. This is my function:
exports.resetOrgScore = functions.runWith(runtimeOpts).pubsub.schedule("every monday 00:00").timeZone("Europe/Oslo").onRun(async (context) => {
let batch = admin.firestore().batch();
let count = 0;
let overallCount = 0;
const orgDocs = await admin.firestore().collection("teams").get();
orgDocs.forEach(async(doc) => {
batch.update(doc.ref, {score:0.0});
if (++count >= 500 || ++overallCount >= orgDocs.docs.length) {
await batch.commit();
batch = admin.firestore().batch();
count = 0;
}
});
});
I tried running the function in a smaller collection of 10 documents and it's working fine, but when running the function in the "teams" collection it returns "Cannot modify a WriteBatch that has been committed". I tried returning the promise like this(code below) but that doesn't fix the problem. Thanks in advance :)
return await batch.commit().then(function () {
batch = admin.firestore().batch();
count = 0;
return null;
});
There are three problems in your code:
You use async/await with forEach() which is not recommended: The problem is that the callback passed to forEach() is not being awaited, see more explanations here or here.
As detailed in the error you "Cannot modify a WriteBatch that has been committed". With await batch.commit(); batch = admin.firestore().batch(); it's exactly what you are doing.
As important, you don't return the promise returned by the asynchronous methods. See here for more details.
You'll find in the doc (see Node.js tab) a code which allows to delete, by recursively using a batch, all the docs of a collection. It's easy to adapt it to update the docs, as follows. Note that we use a dateUpdated flag to select the docs for each new batch: with the original code, the docs were deleted so no need for a flag...
const runtimeOpts = {
timeoutSeconds: 540,
memory: '1GB',
};
exports.resetOrgScore = functions
.runWith(runtimeOpts)
.pubsub
.schedule("every monday 00:00")
.timeZone("Europe/Oslo")
.onRun((context) => {
return new Promise((resolve, reject) => {
deleteQueryBatch(resolve).catch(reject);
});
});
async function deleteQueryBatch(resolve) {
const db = admin.firestore();
const snapshot = await db
.collection('teams')
.where('dateUpdated', '==', "20210302")
.orderBy('__name__')
.limit(499)
.get();
const batchSize = snapshot.size;
if (batchSize === 0) {
// When there are no documents left, we are done
resolve();
return;
}
// Delete documents in a batch
const batch = db.batch();
snapshot.docs.forEach((doc) => {
batch.update(doc.ref, { score:0.0, dateUpdated: "20210303" });
});
await batch.commit();
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(() => {
deleteQueryBatch(resolve);
});
}
Note that the above Cloud Function is configured with the maximum value for the time out, i.e. 9 minutes.
If it appears that all your docs cannot be updated within 9 minutes, you will need to find another approach, for example using the Admin SDK from one of your server, or cutting the work into pieces and run the CF several times.
I am trying to update a field in my document in Firestore. The general location of the document would be "/games/{userId}/userGames/{gameId}. And in this game, there is a property called "status" which changes accordingly to the games start and end time.
As you can guess, the if the start time is bigger than the "now" timestamp and the status is "TO_BE_PLAYED", the game will begin and the status will be 1, "BEING_PLAYED". Also, if the end time is bigger than the "now" timestamp and the status is "BEING_PLAYED", the game will end, therefore the status will be 2, "PLAYED". I want to create a cloud function that is capable to do so.
However, even if the function logs output 'ok', the values are never updated. Unfortunately, I do not have that much experience in Javascript too.
THE CODE
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const STATUS_PLAYED = 2;
const STATUS_BEING_PLAYED = 1;
const STATUS_TO_BE_PLAYED = 0;
exports.handleBeingPlayedGames = functions.runWith({memory: "2GB"}).pubsub.schedule('* * * * *')
.timeZone('Europe/Istanbul') // Users can choose timezone - default is America/Los_Angeles
.onRun(async () => {
// current time & stable
// was Timestamp.now();
const now = admin.firestore.Timestamp.fromDate( new Date());
const querySnapshot = await db.collection("games").get();
const promises = [];
querySnapshot.forEach( doc => {
const docRef = doc.ref;
console.log(docRef);
promises.push(docRef.collection("userGames").where("status", "==", STATUS_BEING_PLAYED).where("endtime", "<", now).get());
});
const snapshotArrays = await Promise.all(promises);
const promises1 = [];
snapshotArrays.forEach( snapArray => {
snapArray.forEach(snap => {
promises1.push(snap.ref.update({
"status": STATUS_PLAYED,
}));
});
});
return Promise.all(promises1);
});
exports.handleToBePlayedGames = functions.runWith({memory: "2GB"}).pubsub.schedule('* * * * *')
.onRun(async () => {
// current time & stable
// was Timestamp.now();
const now = admin.firestore.Timestamp.fromDate(new Date());
const querySnapshot = await db.collection("games").get();
const promises = [];
querySnapshot.forEach( async doc => {
const docData = await doc.ref.collection("userGames").where("status", "==", STATUS_TO_BE_PLAYED).where("startTime", ">", now).get();
promises.push(docData);
});
const snapshotArrays = await Promise.all(promises);
const promises1 = [];
snapshotArrays.forEach( snapArray => {
snapArray.forEach(snap => {
promises1.push(snap.ref.update({
"status": STATUS_BEING_PLAYED,
}));
});
});
return Promise.all(promises1);
});
Okay, so this answer goes to lurkers trying to solve this problem.
First I tried to solve this problem by brute force and not including much thinking and tried to acquire the value in subcollection. However, as I searched, I've found that denormalizing (flattening) data actually solves the problem a bit.
I created a new directory under /status/{gameId} with the properties
endTime, startTime, and status field and I actually did it on a single level by using promises. Sometimes denormalizing data can be your savior.
How can startTime be greater than now? Is it set by default to a date in the future?
My current assumption is that a game cannot set it's status to STATUS_BEING_PLAYED because of the inconsistency with startTime. Moreover, a game cannot have the status STATUS_PLAYED because it depends on having STATUS_BEING_PLAYED, which cannot have.
My recommendation would be to set the field startTime and endTime to null by default. If you do so you can check if a game has to be set to STATUS_BEING_PLAYED with this:
doc.ref.collection("userGames")
.where("status", "==", STATUS_TO_BE_PLAYED)
.where("startTime", "<", now)
.where("endTime", "==", null)
.get();
You could check if a game has to be on STATUS_PLAYED with this (exactly as you did):
docRef.collection("userGames")
.where("status", "==", STATUS_BEING_PLAYED)
.where("endtime", "<", now)
.get();
Now there's something that you should wonder, is this the best approach to change a game's status? You are querying the whole game library of a user every single minute as you know read operations are charged so this approach would imply meaningful charges. Maybe you should simply use update the game's status when the game is started and closed.
Also notice that the equals operation is ==, not =.
I have a function (called rankCheck), which takes three parameters:
Guild object (aka a Discord server)
UserId
Callback Function
The function will fetch the last 500 messages from every text channel in the provided guild. It will then will then only keep any messages that start with "!rank" and were sent by the provided UserId. Finally, it will count the remaining messages and pass the integer to the callback function.
async function rankChecks(guild, userId = *REMOVED FOR PRIVACY*, callback){
sumOfRankChecks = 0;
guild.channels.cache.each(channel => { //for each text channel, get # of rank checks for userId in last 500 msgs.
if (channel.type === "text"){
fetchMessages(channel, 500).then(msgs => {
let filteredMsgs = msgs.filter(msg => msg.content.startsWith("!rank") && msg.member.user.id == userId);
sumOfRankChecks = sumOfRankChecks + filteredMsgs.length;
});
}
});
callback(sumOfRankChecks);
}
Since discord only allows fetching 100 messages at once, I use this function (fetchMessages) to bypass this limit, by sending multiple requests, and then combining the results into one.
async function fetchMessages(channel, limit) {
const sum_messages = [];
let last_id;
while (true) {
const options = { limit: 100 };
if (last_id) {
options.before = last_id;
}
const messages = await channel.messages.fetch(options);
sum_messages.push(...messages.array());
last_id = messages.last().id;
if (messages.size != 100 || sum_messages >= limit) {
break;
}
}
return sum_messages;
}
When I call the rankCheck function, the return value is always 0
rankChecks(msg.guild, *REMOVED FOR PRIVACY*, function(int){
console.log(int);
});
Output:
0
However when I add a console.log into my rankCheck function:
async function rankChecks(guild, userId = *REMOVED FOR PRIVACY*, callback){
sumOfRankChecks = 0;
guild.channels.cache.each(channel => { //for each text channel, get # of rank checks for userId in last 500 msgs.
if (channel.type === "text"){
fetchMessages(channel, 500).then(msgs => {
let filteredMsgs = msgs.filter(msg => msg.content.startsWith("!rank") && msg.member.user.id == userId);
sumOfRankChecks = sumOfRankChecks + filteredMsgs.length;
console.log(sumOfRankChecks) //NEW CONSOLE.LOG!!!!!!!!!!!!!!!
});
}
});
callback(sumOfRankChecks);
}
Output:
3
5
This is the output I was expecting. Since I have 2 text channels in my server, I got 2 logs. If you had 3 channels, you would get 3 logs, etc. 3 messages from channel #1, and 2 messages from channel #2, therefore in total, there are 5 messages.
5 should be the integer that is passed into the callback function, but 0 is passed instead. Why is this?
Your callback function is being called before you even change sumOfRankChecks. Collection#each (and Map#forEach() and the gang) cannot wait for Promises to resolve because of the way they're built. Your code also wouldn't wait anyway, because you're not using await.
Despite what one might think is happening, guild.channels.each() is called, and callback() is called immediately after. This is the source of your confusion.
For more about async vs sync, you can check out the explanation in my answer here. You must use a for loop and await, or refactor your code so that async/await syntax is not necessary.
NOTE: The Discord.js documentation hyperlinked is for recently released v12. If your Discord.js isn't up to date, switch to the correct version at the top of the page for accurate info.
I have orders collection and products collection in my application. The user can have multiple products in their single order. What I want to do is calculating the amount of each product reading through products collection and then perform the further action. Below is what I got as of now.
exports.myfunc = functions.firestore.document('collection/{collid}')
.onCreate(event => {
let data = event.data.data();
const products = data.products;
const prices = [];
_.each(products, (data1, index) => {
const weight = data1.weight;
const isLess = data1.isLess;
firebaseAdmin.firestore().collection('collection').doc(data1.productId).onSnapshot(data2 => {
let amount = weight === '1/2' ? data2.data().price1 : data2.data().price1 * weight;
amount += isLess ? 50 : 0;
prices.push(amount);
});
});
//Do some task after _.each with new total
});
But am not able to achieve synchronous task here, so that I can store actual amount for the product against its order and calculate total to store in document.
Could anyone please tell me how I achieve the above-said scenarios? How I can work along with promise and then callback?
You can map the products array to promises, like this:
var productPromises = products.map(product => {
return new Promise((resolve, reject) => {
firebaseOperation()...onSnapshot(resolve)
})
})
Promise.all(productPromises).then(results => {
// process all results at once
})
First, don't use onSnapshot() with Cloud Functions. That attaches a listener that stay listening indefinitely, until you remove it. That's not what you want at all, because functions can't execute indefinitely.
Instead, use get(), which returns a promise when the fetch is complete.
Also, you could consider accumulating all the documents you want to access into an array and use getAll() (with the spread operator on the array) to fetch them all.