I am trying to process my payment with firebase and stripe and have come across a problem when trying to deploy my function to the cloud saying 'Promises must be handled appropriately. I know this is a tlint compilation error but can't figure out why the error is being triggered.
Here is my code
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
const stripe = require('stripe')(functions.config().stripe.testkey);
exports.stripeCharge = functions.firestore
.document('/payments/{userId}/mypayments/{paymentId}')
.onCreate((snap,event) => {
const payment = snap.data()
const userId = event.params.userId
const paymentId = event.params.paymentId
// checks if payment exists or if it has already been charged
if (!payment || payment.charge) return null;
return admin.firestore()
.doc(`/users/${userId}`)
.get()
.then(snapshot => {
return snapshot
})
.then(customer => {
const amount = payment.price * 100 // amount must be in cents
const idempotency_key = paymentId // prevent duplicate charges
const source = payment.token.id
const currency = 'usd'
const charge = {amount, currency, source}
return stripe.charges.create(charge, { idempotency_key })
})
.then((charge) => {
admin.firestore()//The error keeps referring me to this line
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
})
})
The line generating the error is stated above
Actually, with the latest version(s) of Cloud Functions you are not obliged to include a catch() in your Promises chaining. The platform where the Cloud Function runs will handle the error itself.
Based on this post What could this be about? [TsLint Error: "Promises must be handled appropriately"] it is apparently an error generated by TsLint (EsLint?).
However, independently of this "error" detected by TsLint, I think you may encounter problems with your Cloud Function because you don't return the last promise of your chain:
return admin.firestore() //HERE YOU RETURN CORRECTLY
.doc(`/users/${userId}`)
.get()
.then(snapshot => {
return snapshot //HERE YOU RETURN CORRECTLY
})
.then(customer => {
const amount = payment.price * 100 // amount must be in cents
const idempotency_key = paymentId // prevent duplicate charges
const source = payment.token.id
const currency = 'usd'
const charge = {amount, currency, source}
return stripe.charges.create(charge, { idempotency_key }) //HERE YOU RETURN CORRECTLY
})
.then((charge) => {
return admin.firestore() //HERE, IN YOUR CODE, YOU DON'T RETURN
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
})
})
finally figure it out
Whenever you make a promise function, it has to end with an error handler so i fixed this by using a simple catch
.then((charge) => {
admin.firestore()
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
.catch(er=>{
console.log(er);
return er
}
)
})
Related
Description:
I have created a Firebase app where a user can insert a Firestore document. When this document is created a timestamp is added so that it can be automatically deleted after x amount of time, by a cloud function.
After the document is created, a http/onCreate cloud function is triggered successfully, and it creates a cloud task. Which then deletes the document on the scheduled time.
export const onCreatePost = functions
.region(region)
.firestore.document('/boxes/{id}')
.onCreate(async (snapshot) => {
const data = snapshot.data() as ExpirationDocData;
// Box creation timestamp.
const { timestamp } = data;
// The path of the firebase document('/myCollection/{docId}').
const docPath = snapshot.ref.path;
await scheduleCloudTask(timestamp, docPath)
.then(() => {
console.log('onCreate: cloud task created successfully.');
})
.catch((error) => {
console.error(error);
});
});
export const scheduleCloudTask = async (timestamp: number, docPath: string) => {
// Convert timestamp to seconds.
const timestampToSeconds = timestamp / 1000;
// Doc time to live in seconds
const documentLifeTime = 20;
const expirationAtSeconds = timestampToSeconds + documentLifeTime;
// The Firebase project ID.
const project = 'my-project';
// Cloud Tasks -> firestore time to life queue.
const queue = 'my-queue';
const queuePath: string = tasksClient.queuePath(project, region, queue);
// The url to the callback function.
// That gets envoked by Google Cloud tasks when the deadline is reached.
const url = `https://${region}-${project}.cloudfunctions.net/callbackFn`;
const payload: ExpirationTaskPayload = { docPath };
// Google cloud IAM & ADMIN principle account.
const serviceAccountEmail = 'myServiceAccount#appspot.gserviceaccount.com';
// Configuration for the Cloud Task
const task = {
httpRequest: {
httpMethod: 'POST',
url,
oidcToken: {
serviceAccountEmail,
},
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: expirationAtSeconds,
},
};
await tasksClient.createTask({
parent: queuePath,
task,
});
};
export const callbackFn = functions
.region(region)
.https.onRequest(async (req, res) => {
const payload = req.body as ExpirationTaskPayload;
try {
await admin.firestore().doc(payload.docPath).delete();
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
Problem:
The user can also extend the time to live for the document. When that happens the timestamp is successfully updated in the Firestore document, and a http/onUpdate cloud function runs like expected.
Like shown below I tried to update the cloud tasks "time to live", by calling again
the scheduleCloudTask function. Which obviously does not work and I guess just creates another task for the document.
export const onDocTimestampUpdate = functions
.region(region)
.firestore.document('/myCollection/{docId}')
.onUpdate(async (change, context) => {
const before = change.before.data() as ExpirationDocData;
const after = change.after.data() as ExpirationDocData;
if (before.timestamp < after.timestamp) {
const docPath = change.before.ref.path;
await scheduleCloudTask(after.timestamp, docPath)
.then((res) => {
console.log('onUpdate: cloud task created successfully.');
return;
})
.catch((error) => {
console.error(error);
});
} else return;
});
I have not been able to find documentation or examples where an updateTask() or a similar method is used to update an existing task.
Should I use the deleteTask() method and then use the createTask() method and create a new task after the documents timestamp is updated?
Thanks in advance,
Cheers!
Yes, that's how you have to do it. There is no API to update a task.
I read several documentation but I don't understand why I should use an extra layer(foreach) in my code when I read all of the data inside a collection using Firebase (Cloud Firestore).
Here is the original documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#get_all_documents_in_a_collection
Here is my code:
async loadUsers(): Promise<User[]> {
const users = new Array<User>();
const snapshot = await this.firestore.collection('users').get();
snapshot.forEach((collection) => {
collection.docs.forEach(doc => {
users.push(doc.data() as User);
});
});
return users;
}
As I understand it should work like this:
async loadUsers(): Promise<User[]> {
const users = new Array<User>();
const snapshot = await this.firestore.collection('users').get();
snapshot.forEach(doc => {
users.push(doc.data() as User);
});
return users;
}
Error message:
"Property 'data' does not exist on type 'QuerySnapshot'."
.collection().get() does NOT return an array; it returns a QuerySnapshot, which has a property .docs, which is an array of QueryDocumentSnapshot, each of which has a property .data, which is the data read from the document.
Documentation
https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference
In new modular firebase firestore(version 9.+) it should be like this:
import { getFirestore, collection, query, getDocs } from 'firebase/firestore/lite'
async readAll() {
const firestore = getFirestore()
const collectionRef = collection(firestore, '/users')
let q = query(collectionRef, orderBy('createTimestamp', 'desc'))
const querySnapshot = await getDocs(q)
const items = []
querySnapshot.forEach(document => {
items.push(document.data())
})
return items
}
I could not find any parameter on querySnapshot directly that is something like .docs was and included whole array before. So it is kinda like onSnapshot is and was.
Based on #LeadDreamer answer, I could manage to simplify the code
async loadUsers(): Promise<User[]> {
const users = new Array<User>();
await this.firestore.collection('users').get().subscribe(querySnapshot => {
querySnapshot.docs.forEach(doc => {
users.push(doc.data() as User);
});
});
return users;
}
There seems to be no other way but to iterate.
const q = query(collection(db, "item"));
getDocs(q).then( response => {
const result = response.docs.map(doc=>({
id: doc.id,
...doc.data(),
}))
console.log(result);
}).catch(err=>console.log(err))
How do you get the value of a specific key-value pair in firebase using javascript? I am creating a function for firebase cloud messaging. My function looks like this:
'use strict'
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/notifications/{receiver_user_id}/{notification_key}').onWrite((event, context)=>{
const receiver_user_id = context.params.receiver_user_id;
const notification_key = context.params.notification_key;
console.log('We have a notification to send to : ', receiver_user_id);
// Grab the current value of what was written to the Realtime Database.
const snapshot = event.after.val();
console.log('Uppercasing', context.params.notification_key, snapshot);
console.log('original value : ', snapshot);
if(!event.after.val()){
console.log('A notification has been deleted: ', notification_key);
return null;
}
const sender_fullname = admin.database().ref(`/notifications/${receiver_user_id}/{notification_key}/notifying_user_fullname`).once('value').toString();
console.log('full name value : ', sender_fullname);
const DeviceToken = admin.database().ref(`/tokens/${receiver_user_id}/device_token`).once('value');
return DeviceToken.then(result=>{
const token_id = result.val();
console.log('token id value : ', token_id);
const payload = {
notification: {
title: sender_fullname.toString(),
body: "You have a new message!",
icon: "default"
}
};
return admin.messaging().sendToDevice(token_id, payload).then(response=>{
console.log('Message has been sent');
});
});
});
Right now sender_fullname produces [object Promise] in the console log and the notification that is sent. I am uncertain how to get the exact value. An example entry in my realtime database looks like this:
original value : { date_created: '02-21-2020T17:50:32',
my_id: '0ntpUZDGJnUExiaJpR4OdHSNPkL2',
notification_key: '-M0dwVL3w1rKyPYbzUtL',
notification_type: 'liked',
notifying_user: 'OiBmjJ7yAucbKhKNSHtYHsawwhF2',
notifying_user_fullname: 'Captain Proton',
post_key: '-LzSJrOq9Y7hGgoECHRK',
read: 'false' }
Is there any way to get the exact value of say, "notifying_user_fullname"? Any help would be appreciated.
To get the value of sender_fullname, you have to do exactly the way you do for DeviceToken!
The once() method returns a promise which resolves with a DataSnapshot, so you need to use the then() method in order to get the DataSnapshot and then, use the val() method.
So the following should do the trick (untested):
exports.sendNotification = functions.database.ref('/notifications/{receiver_user_id}/{notification_key}')
.onWrite((event, context) => {
const receiver_user_id = context.params.receiver_user_id;
const notification_key = context.params.notification_key;
console.log('We have a notification to send to : ', receiver_user_id);
// Grab the current value of what was written to the Realtime Database.
const snapshot = event.after.val();
console.log('Uppercasing', context.params.notification_key, snapshot);
console.log('original value : ', snapshot);
if (!event.after.val()) {
console.log('A notification has been deleted: ', notification_key);
return null;
}
let sender_fullname;
return admin.database().ref(`/notifications/${receiver_user_id}/${notification_key}/notifying_user_fullname`).once('value')
.then(dataSnapshot => {
sender_fullname = dataSnapshot.val();
return admin.database().ref(`/tokens/${receiver_user_id}/device_token`).once('value');
})
.then(dataSnapshot => {
const token_id = dataSnapshot.val();
console.log('token id value : ', token_id);
const payload = {
notification: {
title: sender_fullname,
body: "You have a new message!",
icon: "default"
}
};
return admin.messaging().sendToDevice(token_id, payload)
})
.then(() => {
console.log('Message has been sent');
return null; // <-- Note the return null here, to indicate to the Cloud Functions platform that the CF is completed
})
.catch(error => {
console.log(error);
return null;
})
});
Note how we chain the different promises returned by the asynchronous methods, in order to return, in the Cloud Function, a Promise, which will indicate to the platform that the Cloud Function work is complete.
I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series which explains the importance of this point.
This question already has answers here:
How can I update more than 500 docs in Firestore using Batch?
(8 answers)
Closed 3 years ago.
I am trying to delete entire user records from firestore using cloud functions but encounters the next error
INVALID_ARGUMENT: maximum 500 writes allowed per request
How to workaround this?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.deleteUserContacts = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall((data,context) => {
// ...
return admin.firestore().collection('contacts').where('uid','==',context.auth.uid).get()
.then(snap => {
if (snap.size === 0) {
console.log(`User ${context.auth.uid} has no contacts to delete`);
return 'user has no contacts to delete';
}
let batch = admin.firestore().batch();
snap.forEach(doc => {
batch.delete(doc.ref)
});
return batch.commit(); //INVALID_ARGUMENT: maximum 500 writes allowed per request
})
.then(() => {
console.log(`Transaction success on user ${context.auth.uid}`);
return 'Transaction success';
})
.catch(error => {
console.log(`Transaction failure on user ${context.auth.uid}`,error);
throw new functions.https.HttpsError(
'unknown',
'Transaction failure'
);
});
});
To summarize final solution according to Stefan as worked for me.
If any error, please comment
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.deleteUserContacts = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall((data,context) => {
//...
return admin.firestore().collection('contacts').where('uid','==',context.auth.uid).get()
.then(snap => {
if (snap.size === 0) {
console.log(`User ${context.auth.uid} has no contacts to delete`);
return 'user has no contacts to delete';
}
const batchArray = [admin.firestore().batch()];
let operationCounter = 0;
let batchIndex = 0;
snap.forEach(doc => {
batchArray[batchIndex].delete(doc.ref);
operationCounter++;
if (operationCounter === 499) {
batchArray.push(admin.firestore().batch());
batchIndex++;
operationCounter = 0;
}
});
batchArray.forEach(
async batch => await batch.commit()
);
return 'function ended';
})
.then(() => {
console.log(`Transaction success on user ${context.auth.uid}`);
return 'Transaction success';
})
.catch(error => {
console.log(`Transaction failure on user ${context.auth.uid}`,error);
throw new functions.https.HttpsError(
'unknown',
'Transaction failure'
);
});
});
As you might know this is due to the limit to transactions and batched writes, in order to circumvent that limitation I found this SO post of another person that had a similar issue to you.
Most of the recommendations are batching it under 500 and then commiting.
Here, have a look at the solution here.
Hope this helps.
I have an angular app that is using firestore as the DB and Google cloud functions to handle the backend. When I run my app and click on pay to make the call to the Stripe API I get the following message on the log for the cloud functions.
Function returned undefined, expected Promise or value
I have been reading several stackoverflow questions and they talk about me returning whatever in the Promise .then() but I keep getting the same error. The good thing is that the actual value gets store in Firestore with no problem, so it seems to be more like a warning rather than an error since nothing breaks.
What am I missing?
exports.stripeCharges = functions.firestore
.document("/payments/users/TAMO/{paymentId}")
.onWrite((event, context) => {
const payment = event.after.data();
const paymentId = context.params.paymentId;
if (!payment || payment.charge) return;
return admin
.firestore()
.doc(`/payments/users/TAMO/${paymentId}`)
.get()
.then(snapshot => {
return snapshot.data();
})
.then(customer => {
const amount = payment.amount * 100;
const idempotency_key = paymentId;
const source = payment.token.id;
const currency = "usd";
const description = "Test Charge";
const charges = {
amount,
currency,
description,
source
};
return stripe.charges.create(charges, { idempotency_key });
})
.then(charges => {
return admin
.firestore()
.doc(`/payments/users/TAMO/${paymentId}`)
.set(
{
charge: charges
},
{
merge: true
}
);
});
});
Looks like either you don't have payment or payment.charge.
if (!payment || payment.charge) return;
I solved this warning by doing the following:
if (!payment || payment.charge) return null;
The line above checks if payment exists or if it has already been charged