Schedule cloud function to update a list of maps - javascript

I'm trying to write a scheduled cloud function to reset the value of "status" every day at 12 am. Here's my firestore structure:
I haven't really tried coding in javascript before but here's what I managed with my little knowledge:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const database = admin.firestore();
exports.Rst = functions.pubsub.schedule("0 0 * * *").onRun((context) => {
const alist =
database.collection("SA1XAoC2A7RYRBeAueuBL92TJEk1")
.doc("afternoon").get().then((snapshot)=>snapshot.data["list"]);
for (let i=0; i<alist.length; i++) {
alist[i]["status"]=0;
}
database.collection("SA1XAoC2A7RYRBeAueuBL92TJEk1")
.doc("afternoon").update({
"list": alist,
});
return null;
});
I get the following error when I deploy this function:
Expected Result:
Set the values of all "status" fields to 0.

Your alist will return a Promise { <pending> }. It needs to be fulfilled with a value or rejected with a reason (error). You should use the .then method to fulfill or use the .catch method to get any errors of all the pending promises. See code below for reference:
const collectionName = "SA1XAoC2A7RYRBeAueuBL92TJEk1";
const documentName = "afternoon";
// created a reference to call between functions
const docRef = database.collection(collectionName).doc(documentName);
// Initialized a new array that will be filled later.
const tasks = [];
// Gets the data from the document reference
docRef.get()
// Fulfills the promise from the `.get` method
.then((doc) => {
// doc.data.list contains the array of your objects. Looping it to construct a `tasks` array.
doc.data().list.forEach((task) => {
// Setting the status to 0 for every object on your list
task.status = 0;
// Push it to the initialized array to use it on your update function.
tasks.push(task);
})
docRef.update({
// The `tasks` structure here must be the same as your Firestore to avoid overwritten contents. This should be done as you're updating a nested field.
list: tasks
}, { merge: true });
})
// Rejects the promise if it returns an error.
.catch((error) => {
console.log("Error getting document:", error);
});
I left some comments on the code for better understanding.
You may also wanna check these documentations:
Promise
Get data with Cloud Firestore
Update fields in nested objects

It seems that alist is an object that Firestore can't handle. To get rid of any of the parts that Firestore can't handle, you can do:
database.collection("SA1XAoC2A7RYRBeAueuBL92TJEk1")
.doc("afternoon").update({
"list": JSON.parse(JSON.stringify(alist)) // 👈
});

Related

Perform fetch request within a Firestore transaction: receiving "Cannot modify a WriteBatch that has been committed"

I'm trying to perform a fetch request within a transaction but when the code executes I receive the following error.
Error: Cannot modify a WriteBatch that has been committed.
The steps the function is performing are the following:
Compute document references (taken from an external source)
Query the documents available in Firestore
Verify if document exists
Fetch for further details (lazy loading mechanism)
Start populating first level collection
Start populating second level collection
Below the code I'm using.
await firestore.runTransaction(async (transaction) => {
// 1. Compute document references
const docRefs = computeDocRefs(colName, itemsDict);
// 2. Query the documents available in Firestore
const snapshots = await transaction.getAll(...docRefs);
snapshots.forEach(async (snapshot) => {
// 3. Verify if document exists
if (!snapshot.exists) {
console.log(snapshot.id + " does not exists");
const item = itemsDict[snapshot.id];
if (item) {
// 4. Fetch for further details
const response = await fetchData(item.detailUrl);
const detailItemsDict = prepareDetailPageData(response);
// 5. Start populating first level collection
transaction.set(snapshot.ref, {
index: item.index,
detailUrl: item.detailUrl,
title: item.title,
});
// 6. Start populating second level collection
const subColRef = colRef.doc(snapshot.id).collection(subColName);
detailItemsDict.detailItems.forEach((detailItem) => {
const subColDocRef = subColRef.doc();
transaction.set(subColDocRef, {
title: detailItem.title,
pdfUrl: detailItem.pdfUrl,
});
});
}
} else {
console.log(snapshot.id + " exists");
}
});
});
computeDocRefs is described below
function computeDocRefs(colName, itemsDict) {
const identifiers = Object.keys(itemsDict);
const docRefs = identifiers.map((identifier) => {
const docId = `${colName}/${identifier}`
return firestore.doc(docId);
});
return docRefs;
}
while fetchData uses axios under the hood
async function fetchData(url) {
const response = await axios(url);
if (response.status !== 200) {
throw new Error('Fetched data failed!');
}
return response;
}
prepareMainPageData and prepareDetailPageData are functions that prepare the data normalizing them.
If I comment the await fetchData(item.detailUrl), the first level collection with all the documents associated to it are stored correctly.
On the contrary with await fetchData(item.detailUrl) the errors happens below the following comment: // 5. Start populating first level collection.
The order of the operation are important since I do now want to make the second call if not necessary.
Are you able to guide me towards the correct solution?
The problem is due to the fact that forEach and async/await do not work well together. For example: Using async/await with a forEach loop.
Now I've completely changed the approach I'm following and now it works smoothly.
The code now is like the following:
// Read transaction to retrieve the items that are not yet available in Firestore
const itemsToFetch = await readItemsToFetch(itemsDict, colName);
// Merge the items previously retrieved to grab additional details through fetch network calls
const fetchedItems = await aggregateItemsToFetch(itemsToFetch);
// Write transaction (Batched Write) to save items into Firestore
const result = await writeFetchedItems(fetchedItems, colName, subColName);
A big thanks goes to Doug Stevenson and Renaud Tarnec.

Google cloud function onCreate not writing to the Database

I have created a google function for firebase that when A new conversation is added the function attaches it to the Users table under a new collection for each user in the conversation but when the function gets triggered nothing happens in the database, So far I have console logged the values tp make sure they ware being set right and they ware I have also tried looking at the google function logs and there are no errors according to the logs the script ran with no errors
Here is the code for the function
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const onConversationCreated = functions.firestore.document("Conversations/{conversationID}").onCreate((snapshot, context) => {
let data = snapshot.data();
let conversationID = context.params.conversationID;
if(data){
let members = data.members;
for(let index = 0; index < members.length; index++){
let currentUserID = members[index];
let remainingUserIDs = members.filter((u: string) => u !== currentUserID)
remainingUserIDs.forEach((m: string) => {
return admin.firestore().collection("Users").doc(m).get().then((_doc) => {
let userData = _doc.data();
if(userData){
return admin.firestore().collection("Users").doc(currentUserID).collection("Conversations").doc(m).create({
"conversationID": conversationID,
"image": userData.image,
"unseenCount": 1,
});
}
return null;
}).catch(() => {return null})
});
}
}
return null;
});
Can someone tell me if there is something wrong with my code or do I have to give functions permission to write to the cloud firestore database?
You are not dealing with promises correctly. Your function needs to return a promise that resolves with all of the asynchronous work is complete. Right now, it's just returning null, and not waiting for any work to complete.
All Firestore operations are asynchronous and return a promise. You will need to use these promises to build a single promise to return from the function. That means each time you query and write a document, that's generating another promise to handle.
Also, you should know that there is no method create() that you're trying to use to add a document. Perhaps you meant to use set() instead. The code will crash if it tries to execute create().

How to save firebase returned data to a variable

I am coding for my React Native app and I am having trouble getting the data from the firebase return outside of the firebase.firestore().collection("test_data").doc(ID) loop. Whenever I check the dataArray variable after the loop it is empty. If I check it within the loop, the data is there. I think that it is a scope problem, but I just do not understand it. I also cannot call any user defined functions inside of the loop.
try {
let dataArray = [];
// get the document using the user's uid
firebase.firestore().collection("users").doc(uid).get()
.then((userDoc) =>{
// if the document exists loop through the results
if (userDoc.exists) {
data = userDoc.data().saved_data; // an array store in firebase
data.forEach(ID => { // loop through array
firebase.firestore().collection("test_data").doc(ID).get()
.then((doc) => {
dataArray.push(doc.data().test_data);
console.log(dataArray) // the data shows
})
console.log(dataArray) // the data does not show
})
}
})
}
catch (error) {
}
}
You're looping through asynchronous calls, so your final console.log will trigger before the data has been received. Your first console.log only triggers after the data has been received.
So the code is working, but the function (promise) will resolve (as undefined, or void) before all the data has been received from your firebase calls.
If you want to return the array to the caller, you could do something like this:
function getDataFromServer() {
// get the document using the user's uid
return firebase.firestore().collection('users').doc(uid).get().then(userDoc => {
// now we're returning this promise
const dataArray = []; // can move this to lower scope
// if the document exists loop through the results
if (userDoc.exists) {
const savedData = userDoc.data().saved_data; // an array store in firebase
return Promise.all(
// wait for all the data to come in using Promise.all and map
savedData.map(ID => {
// this will create an array
return firebase.firestore().collection('test_data').doc(ID).get().then(doc => {
// each index in the array is a promise
dataArray.push(doc.data().test_data);
console.log(dataArray); // the data shows
});
})
).then(() => {
console.log(dataArray);
return dataArray; // now data array gets returned to the caller
});
}
return dataArray; // will always be the original empty array
});
}
Now the function returns the promise of an array, so you could do...
const dataArray = await getDataFromServer()
or
getDataArrayFromServer().then(dataArray => {...})

Tracking state of a chain of promises

I'm currently trying to track the progress of a chain of native es6 promises, and am wondering what the 'correct' way to go about this is.
I've simplified the actual code to thie following example, which is a basic set of chained promises (in reality, the promise chain is longer, and the session status value changes in more places depending on progress through the chain):
let sessions = {}
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(res => {
// Return the 'url' property of the fetched data
return res.url
})
.then (url => {
// Fetch this second url
let data = api.get(url)
sessions.session_id = 'SUCCESS'
// Return the whole data object
return data
})
.catch(err => {
console.log("ERR", err)
sessions.session_id = 'ERROR'
})
return result
}
asyncFunc()
.then(res => {
console.log("URL", url)
})
This code tracks the state of the functions and stores them to the global sessions object - but the session_id isn't being passed back for inspection of status while the function is 'in-flight'.
One option I'm considering is adding the session_id as a property of the promise when it is returned, so this can be inspected - however I'm not sure if adding a property to a promise is a risky/hacky thing to do? Something like (simplified from above):
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(...)
.then(...)
.catch(...)
// Add the session_id to the promise
result.session_id = session_id
return result
}
let func = asyncFunc()
let status =sessions[func.session_id]
func.then(...)
Any thoughts on the validity of this approach? I can see that I would probably also need to push the session id into the final return value as well, (so that the property exists in both the promise, and the resulting value of the resolved/rejected promise).
Alternatively, any other ways of handling this?
The obvious one is to make the function always return an array of arguments (promise and session_id) but I'd prefer to avoid having to always do e.g.:
let func = asyncFunc()
let status =sessions[func[1]]
func[0].then(...)

Firebase Promises in array not resolving in time

I am trying to return 3 promises from my firebase DB and once all three promises have been fulfilled I basically want to render a new page or do whatever. So I do a Promise.All(...) but my lists are still empty afterwards.
My queries are correct because when I console.log() within each of those functions, I get the objects returned from my DB but my Promise.All isn't waiting for those promises to resolve and instead executes the code within the Promise.All which is returning empty lists.
app.get('...', function (req, res) {
//Return first promise from DB save to zone_obj list
var zone_key = req.params.id;
var zone_obj = [];
firebase.database().ref(...).once('value').then((snapme) => {
zone_obj.push(snapme.val());
});
//Return second promise from DB save to members list
var members = [];
firebase.database().ref(...).on("value", function (snapshott) {
snapshott.forEach((snapper) => {
members.push(snapper.val());
});
});
//Return third promise from DB save to experiences list
var experiences = [];
firebase.database().ref(...).on("value", function (snapshot) {
snapshot.forEach((snap) => {
firebase.database().ref(...).once("value").then((snapit) => {
experiences.push(snapit);
});
});
});
//once all promises have resolved
Promise.all([experiences,zone_obj,members]).then(values => {
console.log(values[0]); //returns []
console.log(values[1]); //returns []
console.log(values[2]); //returns []
});
});
This is not actually firebase's problem. Firebase method .on("value") is actually a listener that will be bound to firebase to get real-time updates and that is not actually a promise and your callback function will be called every time when data on that node is changed. so if you want to save or get data only once use firebase.database().ref(...).set() and firebase.database().ref(...).once() method respectively.
According to firebase docs
On method
on(eventType, callback, cancelCallbackOrContext, context) returns function()
once method
once(eventType, successCallback, failureCallbackOrContext, context) returns firebase.Promise containing any type
So change your code to following
app.get('...', function (req, res) {
var promises = []
//Return first promise from DB save to zone_obj list
promises.push(firebase.database().ref(...).once('value'));
//Return second promise from DB save to members list
promises.push(firebase.database().ref(...).once('value'));
//Return third promise from DB save to experiences list
promises.push(firebase.database().ref(...).once('value'));
//once all promises have resolved
Promise.all(promises).then(values => {
console.log(values[0]); // zone_obj
console.log(values[1]); // members
console.log(values[2]); // experiences
});
});

Categories

Resources