Cloud firebase batch update - firebase

I am trying to use a batch update to update a count within each snapshot. But it seems like the function doesnt even run. I know it has something to do with the second promise but I am not sure where.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const replyCreated = functions
.firestore
.document(`/Reply/{replyId}`)
.onCreate((change: any, context: functions.EventContext) => {
const promises = [];
promises.push(admin.firestore().doc(`Challenge/${change.data().challenge_id}`).update({replyCount: admin.firestore.FieldValue.increment(1)}))
promises.push(admin.firestore()
.collection(`User`)
.where('following', 'array-contains', change.data().user_id).get().then((snapshot: any) => {
if (!snapshot.empty) {
const batch = admin.firestore().batch();
snapshot.forEach((doc: any) => {
const tempObject = doc.data()
console.log(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
const myChallenge = admin.firestore().doc(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
batch.update(myChallenge, {replyCount: admin.firestore.FieldValue.increment(1)})
})
return batch.commit().catch((err: any) => {
console.log('Batch Error', err)
});
}
else {
return Promise.resolve()
}
}))
return Promise.all(promises)
.then(() => {
return "upvote complete";
})
})

If I correctly understand you code, you don't need to use Promise.all() but you need to correctly chain the different Promises returned by the asynchronous Firestore methods.
The following should do the trick (untested):
export const replyCreated = functions
.firestore
.document(`/Reply/{replyId}`)
.onCreate((change: any, context: functions.EventContext) => {
return admin.firestore().doc(`Challenge/${change.data().challenge_id}`).update({ replyCount: admin.firestore.FieldValue.increment(1) })
.then(() => {
return admin.firestore()
.collection(`User`)
.where('following', 'array-contains', change.data().user_id).get()
})
.then((snapshot: any) => {
if (!snapshot.empty) {
const batch = admin.firestore().batch();
snapshot.forEach((doc: any) => {
const tempObject = doc.data()
console.log(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
const myChallenge = admin.firestore().doc(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
batch.update(myChallenge, { replyCount: admin.firestore.FieldValue.increment(1) })
})
return batch.commit()
}
else {
throw new Error('Snapshot empty')
}
})
.catch((err: any) => {
console.log('Error', err);
return null;
});
})
You would use Promise.all() if you need to execute a number of asynchronous methods (which return a Promise) in parallel. In your case (if I am not mistaking) the only case where you need to execute asynchronous methods in parallel is in the block where you use the batched write, therefore the parallel execution is executed by the batched write itself. For the other methods, it is more a sequential execution and you have to chain the promises with the then() method.

Related

Jest Calling async function

I have a function in redux store.
Func1(){
Dispatch(some action)
Dispatch(some action1)
result = Dispatch(some async action)\\promise retured
result.then(()=>
Dispatch(some other action)
)
}
My test case
Test(()=>{
Expectedactions= all 3 actions.
Axios.post.mockimplimetation(()=>
Promise.resolve(data))
calling Func1() from wrapper.
Expect(store.getActions()).toequal(expectedactions)
})
Test result shows only 2 actions.
Async action is not captured.
Please help me how to test this behaviour or what approach I shoud take.
Edited.
TestConrainer.js
const mapstatetoprops= (state)=>{
return {}
}
const mergeprops =(stateprops,dispatchprops,ownprops)=>{
const {dispatch}= dispatchprops
const f1=()=>{
dispatch(configActions.pause("Pause"))
let result = dispatch(DataActions.save(stateProps.data)
dispatch(configActions.setClick(false));
result.then((result) => {
dispatch(configActions.show(false));
dispatch(DataActions.StatusValue(""))
})
return Object.assign({},stateprops,ownprops,{
f1:f1
})
}
export default connect(mapstatetoprops,null,mergeprops)(Test)
DataActions.js
export const save = (data) => {
return (dispatch,getState) =>{
return axios.post(url, payload))
.then(data => {
return data;
},error => {
return error
}).catch(err){
return err
}
My test case
Test(()=>{
Expectedactions= all 3 actions.
Axios.post.mockimplimetation(()=>
Promise.resolve(data))
calling f1() from wrapper like
Wrapper.shallow(<TestContainer store=store/>)
Wrapper.find('Test').props().f1()
Expect(store.getActions()).toequal(expectedactions)
})
Test result shows only 2 actions.
Async action is not captured.
Please help me how to test this behaviour or what approach I shoud take.

Trouble reading data in Firebase Cloud Function

Trying to read a pushToken from a given user in the users collection (after an update operation on another collection) returns undefined
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
})
.then(r => {
getToken(idTo).then(token => {
// sendMsg...
})
}).catch(updateErr => {
console.log("updateErr: " + updateErr)
})
async function getToken(id) {
let response = "getTokenResponse"
console.log("id in getToken: " + id)
return db.collection('users').doc(id).get()
.then(user => {
console.log("user in getToken: " + user.data())
response = user.data().pushToken
})
.catch(e => {
console.log("error get userToken: " + e)
response = e
});
return response
}
return null
});
And this is from the FB console log:
-1:43:33.906 AM Function execution started
-1:43:36.799 AM Function execution took 2894 ms, finished with status: 'ok'
-1:43:43.797 AM id in getToken: Fm1RwJaVfmZoSgNEFHq4sbBgoEh1
-1:43:49.196 AM user in getToken: undefined
-1:43:49.196 AM error get userToken: TypeError: Cannot read property 'pushToken' of undefined
-1:43:49.196 AM returned token: undefined
And we can see in this screenshot from the db that the doc does exist:
Hope someone can point me to what I'm doing wrong here.
added screenshot of second example of #Renaud as deployed:
As Doug wrote in his comment, you need to "return a promise from the top level function that resolves when all the async work is complete". He also explains that very well in the official video series: https://firebase.google.com/docs/functions/video-series/ (in particular the 3 videos titled "Learn JavaScript Promises"). You should definitely watch them, highly recommended!
So, the following modifications to your code should work (untested):
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite(async (snap, context) => { // <- note the async keyword
try {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
await db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
});
const userToSnapshot = await db.collection('users').doc(idTo).get();
const token = userToSnapshot.data().pushToken;
await sendMsg(token); // <- Here you should take extra care to correctly deal with the asynchronous character of the sendMsg operation
return null; // <-- This return is key, in order to indicate to the Cloud Function platform that all the asynchronous work is done
} catch (error) {
console.log(error);
return null;
}
});
Since you use an async function in your code, I've used the async/await syntax but we could very well write it by chaining the promises with the then() method, as shown below.
Also, I am not sure, in your case, that it adds any value to put the code that gets the token in a function (unless you want to call it from other Cloud Functions but then you should move it out of the addDenuncia Cloud Function). That's why it has been replaced by two lines of code within the main try block.
Version with chaining promises via the then() method
In this version we chain the different promises returned by the asynchronous methods with the then() method. Compared to the async/await version above, it shows very clearly what means "to return a promise from the top level function that resolves when all the asynchronous work is complete".
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => { // <- no more async keyword
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
return db.collection('Classificados').doc(classificadoId) // <- we return a promise from the top level function
.update({
aprovado: false
})
.then(() => {
return db.collection('users').doc(idTo).get();
})
.then(userToSnapshot => {
if {!userToSnapshot.exists) {
throw new Error('No document for the idTo user');
}
const token = userToSnapshot.data().pushToken;
return sendMsg(token); // Again, here we make the assumption that sendMsg is an asynchronous function
})
.catch(error => {
console.log(error);
return null;
})
});

Firebase Cloud Functions / Each then() should return a value or throw promise/always-return

I was following the official firebase tutorial on promises (https://www.youtube.com/watch?v=7IkUgCLr5oA) but in my case, I cannot make it work.
const promise = userRef.push({text:text});
const promise2 = promise.then((snapshot) => {
res.status(200).json({message: 'ok!'});
});
promise2.catch(error => {
res.status(500).json({message: 'error'});
});
What am I doing wrong? Each then() should have its response in case something goes wrong, but that is why I am writing the promise2 catch.
Just add the return before sending the response.
const promise = userRef.push({text:text});
const promise2 = promise.then((snapshot) => {
return res.status(200).json({message: 'ok!'});
});
promise2.catch(error => {
return res.status(500).json({message: 'error'});
});
Also you can chain the promises as follows:
return userRef.push({text:text})
.then((snapshot) => {
return res.status(200).json({message: 'ok!'});
}).catch(error => {
return res.status(500).json({message: 'error'});
});

what should I do If I want to do nothing in the one of my execution path in Background trigger cloud function?

as far as I know, background trigger cloud function should return a promise,right? but what if I want to do nothing in the one of my execution path ?
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete((snap, context) => {
const eventID = context.params.eventId
const eventRef = snap.ref.firestore.collection('events').doc(eventID)
const db = admin.firestore()
return db.runTransaction(async t => {
const doc = await t.get(eventRef)
if (doc) {
const eventRankPoint = doc.data().rankPoint
let eventCapacity = doc.data().capacity
return t.update(eventRef,{
isFullCapacity : false,
capacity : eventCapacity + 1,
rankPoint: eventRankPoint - 1
})
} else {
// what should I write in here? empty promise?
return new Promise()
}
})
})
I want to my function worked only if the document is exist. so what should I do ? I write new Promise but .... I don't know what to do actually. thanks in advance
You can just return null if there's no asynchronous work to perform in some code path of your functions. You only truly need a promise if it tracks some async work.
Alternatively, you could return a promise that's resolved immediately with Promise.resolve(null)
Because db.runTransaction is an async function it will return a Promise all the time.
You can drop the else statement and the method will perform as expected because runTransaction will return Promise<void> which is a valid response for Cloud Functions
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete((snap, context) => {
const eventID = context.params.eventId;
const eventRef = snap.ref.firestore.collection('events').doc(eventID);
const db = admin.firestore();
return db.runTransaction(async t => {
const doc = await t.get(eventRef);
if (doc) {
const eventRankPoint = doc.data().rankPoint;
let eventCapacity = doc.data().capacity ;
return t.update(eventRef,{
isFullCapacity : false,
capacity : eventCapacity + 1,
rankPoint: eventRankPoint - 1
});
}
});
});
You can also make the onDelete function async which means you can force it to always return a Promise - the below is valid and will exit the function correctly.
export const updateDataWhenUserUnattendTheEvent = functions.firestore
.document('events/{eventId}/Attendee/{userId}')
.onDelete(async (snap, context) => {
// Do Nothing
return;
});

firestore cloud functions onCreate/onDelete sometimes immediately triggered twice

I have observed this behavior occasionally with both onCreate and onDelete triggers.
Both the executions happened for the same document created in firestore. There's only one document there so I don't understand how it could trigger the handler twice. the handler itself is very simple:
module.exports = functions.firestore.document('notes/{noteId}').onCreate((event) => {
const db = admin.firestore();
const params = event.params;
const data = event.data.data();
// empty
});
this doesn't happen all the time. What am I missing?
See the Cloud Firestore Triggers Limitations and Guarantees:
Delivery of function invocations is not currently guaranteed. As the
Cloud Firestore and Cloud Functions integration improves, we plan to
guarantee "at least once" delivery. However, this may not always be
the case during beta. This may also result in multiple invocations
for a single event, so for the highest quality functions ensure that
the functions are written to be idempotent.
There is a Firecast video with tips for implementing idempotence.
Also two Google Blog posts: the first, the second.
Based on #saranpol's answer we use the below for now. We have yet to check if we actually get any duplicate event ids though.
const alreadyTriggered = eventId => {
// Firestore doesn't support forward slash in ids and the eventId often has it
const validEventId = eventId.replace('/', '')
const firestore = firebase.firestore()
return firestore.runTransaction(async transaction => {
const ref = firestore.doc(`eventIds/${validEventId}`)
const doc = await transaction.get(ref)
if (doc.exists) {
console.error(`Already triggered function for event: ${validEventId}`)
return true
} else {
transaction.set(ref, {})
return false
}
})
}
// Usage
if (await alreadyTriggered(context.eventId)) {
return
}
In my case I try to use eventId and transaction to prevent onCreate sometimes triggered twice
(you may need to save eventId in list and check if it exist if your function actually triggered often)
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const db = admin.firestore()
exports = module.exports = functions.firestore.document('...').onCreate((snap, context) => {
const prize = 1000
const eventId = context.eventId
if (!eventId) {
return false
}
// increment money
const p1 = () => {
const ref = db.doc('...')
return db.runTransaction(t => {
return t.get(ref).then(doc => {
let money_total = 0
if (doc.exists) {
const eventIdLast = doc.data().event_id_last
if (eventIdLast === eventId) {
throw 'duplicated event'
}
const m0 = doc.data().money_total
if(m0 !== undefined) {
money_total = m0 + prize
}
} else {
money_total = prize
}
return t.set(ref, {
money_total: money_total,
event_id_last: eventId
}, {merge: true})
})
})
}
// will execute p2 p3 p4 if p1 success
const p2 = () => {
...
}
const p3 = () => {
...
}
const p4 = () => {
...
}
return p1().then(() => {
return Promise.all([p2(), p3(), p4()])
}).catch((error) => {
console.log(error)
})
})
Late to the party, I had this issue but having a min instance solved the issue for me
Upon looking #xaxsis attached screenshot, my function took almost the amount of time about 15 seconds for the first request and about 1/4 of that for the second request

Resources