The question is published on by Tutorial Guruji team.
here is what I am trying to do with firebase cloud function:
-Listen to any change in one of the documents under ‘user’ collection.
-Update carbon copies of the userinfo in the relevant documents in both ‘comment’ and ‘post’ collections.
Because I will need to query in relevant documents and update them at once, I am writing codes for transaction operations.
Here is the code that I wrote. It returns the error message, ‘Function returned undefined, expected Promise or value’.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change,context) => { const olduserinfo=change.before.data(); const newuserinfo=change.after.data(); db.runTransaction(t=>{ return t.get(db.collection('comment').where('userinfo','==',olduserinfo)) .then((querysnapshot)=>{ querysnapshot.forEach((doc)=>{ doc.ref.update({userinfo:newuserinfo}) }) }) }) .then(()=>{ db.runTransaction(t=>{ return t.get(db.collection('post').where('userinfo','==',olduserinfo)) .then((querysnapshot)=>{ querysnapshot.forEach((doc)=>{ doc.ref.update({userinfo:newuserinfo}) }) }) }) }) });
I am a bit confused because as far as I know, ‘update’ method returns a promise? I might be missing something big but I picked up programming only last November, so don’t be too harsh. 🙂
Any advice on how to fix this issue? Thanks!
EDIT: Building on Renaud‘s excellent answer, I created the below code in case someone may need it. The complication with transaction is that the same data may be stored under different indices or in different formats. e.g. The same ‘map’ variable can be stored under an index in one collection, and as part of an array in another. In this case, each document returned by querying needs different update methods.
I resolved this issue using doc.ref.path, split, and switch methods. This enables application of different update methods based on the collection name. In a nutshell, something like this:
return db.runTransaction(t => { return t.getAll(...refs) .then(docs => { docs.forEach(doc => { switch (doc.ref.path.split('/')[0]) { //This returns the collection name and switch method assigns a relevant operation to be done. case 'A': t = t.update(doc.ref, **do whatever is needed for this collection**) break; case 'B': t = t.update(doc.ref, **do whatever is needed for this collection**) break; default: t = t.update(doc.ref, **do whatever is needed for this collection**) } }) }) })
Hope this helps!
Answer
Preamble: This is a very interesting use case!!
The problem identified by the error message comes from the fact that you don’t return the Promise returned by the runTransaction()
method. However there are several other problems in your code.
With the Node.js Server SDK you can indeed pass a query to the transaction’s get()
method (you cannot with the JavaScript SDK). However, in your case you want to update the documents returned by two queries. You cannot call twice db.runTransaction()
because, then, it is not a unique transaction anymore.
So you need to use the getAll()
method by passing an unpacked array of DocumentReferences
. (Again, note that this getAll()
method is only available in the Node.js Server SDK and not in the JavaScript SDK).
The following code will do the trick.
We run the two queries and transform the result in one array of DocumentReferences
. Then we call the runTransaction()
method and use the spread operator to unpack the array of DocumentReferences
and pass it to the getAll()
method.
Then we loop over the docs and we chain the calls to the transaction’s update()
method, since it returns the transaction.
However note that, with this approach, if the results of one of the two original queries change during the transaction, any new or removed documents will not be seen by the transaction.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change, context) => { const olduserinfo = change.before.data(); const newuserinfo = change.after.data(); const db = admin.firestore(); const q1 = db.collection('comment').where('userinfo', '==', olduserinfo); // See the remark below: you probably need to use a document field here (e.g. olduserinfo.userinfo) const q2 = db.collection('post').where('userinfo', '==', olduserinfo); return Promise.all([q1.get(), q2.get()]) .then(results => { refs = []; results.forEach(querySnapshot => { querySnapshot.forEach(documentSnapshot => { refs.push(documentSnapshot.ref); }) }); return db.runTransaction(t => { return t.getAll(...refs) .then(docs => { docs.forEach(doc => { t = t.update(doc.ref, { userinfo: newuserinfo }) }) }) }) }) });
Two last remarks:
- I am not sure that
db.collection('comment').where('userinfo', '==', olduserinfo);
will be valid asolduserinfo
is obtained throughchange.before.data()
. You probably need to specify one field. This is probably the same fornewuserinfo
. - Note that you cannot do
doc.ref.update()
in a transaction, you need to call the transaction’supdate()
method, not the one of aDocumentReference
.