firebase function : update document with synchronous rest call - javascript

I have this scheduled function that :
read 1 document from collection
data: [
{
'field1' : 123,
'field2' : 456
},
{
'field1' : 123,
'field2' : 456
}
...
]
loop on all data array
read new value from rest api call
update the value of data array in firestore
NOTE from firebase function console log I have the log 'field updated ...' after the log 'firebase document updated ...', I think because the request is not sync
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const db = admin.firestore();
const fetch = require('node-fetch');
var request = require("request");
const { Headers } = fetch;
exports.onStatisticUpdated = functions.region('europe-west1')
.runWith({
timeoutSeconds: 300,
})
.pubsub.schedule('every 60 minutes')
.onRun(async context => {
const doc = await admin
.firestore()
.collection('collection')
.doc('1234')
.get();
if (doc.exists) {
for (let i = 0; i < doc.data().data.length; i++) {
let myFirebaseData = doc.data().data[i];
var options = {
method: 'GET',
url: 'https://xyz....',
qs: { abcd: 'value' },
headers: {
'x-xxxx-host': 'v1....',
'x-xxxx-key': 'xxxxxxxxxxxx'
}
}
request(options, function (error, response, body) {
if (error) throw new Error(error);
var json = JSON.parse(body);
if (json['response'].length > 0) {
console.log('field updated ...');
myFirebaseData.field1 = json['response'][0].value1
myFirebaseData.field2 = json['response'][0].value2
};
});
}
// myFirebaseData is not updated at this time with new value filled by rest api !!!
console.log(' firebase document updated ...');
await admin
.firestore()
.collection('collection')
.doc('1234')
.update({
data: doc.data(),
});
}
});
question : how i can store the final document with new values filled by the rest api ?

In Cloud Functions you need to manage asynchronous method calls via Promises (more details here in the doc). request supports callback interfaces natively but does not return a Promise.
You should use another library, like axios. In addition, since you want to execute a variable number of asynchronous Rest API calls in parallel, you need to use Promise.all().
Now, what is not clear to me in your code is how do you build the object used to update the 1234 document. I your current code, in the for (let i = 0; i < doc.data().data.length; i++) {}) loop you are actually overwriting the field1 and field2 properties of the myFirebaseData again and again...
Therefore you will find below the code structure/pattern that I think is correct and if it is not the case, just add a comment to this answer and we can fine tune the answer according to the extra details you will share.
exports.onStatisticUpdated = functions.region('europe-west1')
.runWith({
timeoutSeconds: 300,
})
.pubsub.schedule('every 60 minutes')
.onRun(async context => {
const doc = await admin
.firestore()
.collection('collection')
.doc('1234')
.get();
if (doc.exists) {
const promises = [];
for (let i = 0; i < doc.data().data.length; i++) {
let myFirebaseData = doc.data().data[i];
var options = {
method: 'get',
url: 'https://xyz....',
params: { abcd: 'value' },
headers: {
'x-xxxx-host': 'v1....',
'x-xxxx-key': 'xxxxxxxxxxxx'
}
}
promises.push(axios(options))
}
apiResponsesArray = await Promise.all(promises);
const updateObject = {};
apiResponsesArray.forEach((resp, index) => {
// THIS ENTIRE BLOCK NEEDS TO BE ADAPTED!!
// I'M JUST MAKING ASSUMPTIONS...
const responseData = resp.data;
updateObject["field1" + index] = responseData.value1;
updateObject["field2" + index] = responseData.value2;
})
console.log(updateObject)
console.log(' firebase document updated ...');
await admin
.firestore()
.collection('collection')
.doc('1234')
.update({
data: updateObject
});
return null; // IMPORTANT!!! see the link to the doc above
}
});

Related

Why does my async function always return undefined?

It seems im using async wrong, can anybody spot what I am doing wrong?
This is the function I am waiting on:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
var tradeInstanceID;
var senderID;
var receiverID;
var senderItemsTemp;
var receiverItemsTemp;
var response;
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
return tradeOffer
.get()
.then((doc) => {
senderItemsTemp = doc.data().sendersItems;
receiverItemsTemp = doc.data().receiversItems;
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
})
.then(() => {
var itemInTrade = false;
senderItemsTemp.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
itemInTrade = true;
}
})
.then(() => {
receiverItemsTemp.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
itemInTrade = true;
}
})
.then(() => {
if (itemInTrade) {
tradeOffer.update({
status: "declined",
});
return false;
} else {
db.collection("trades")
.add({
tradeOfferID: tradeOfferID,
senderTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
receiverTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
postagePhotos: [],
inspectionPhotos: [],
senderPaid: false,
receiverPaid: false,
senderUploadedProof: false,
receiverUploadedProof: false,
senderID: senderID,
receiverID: receiverID,
messages: [
{
message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
sender: "System",
timestamp: firebase.firestore.Timestamp.fromDate(
new Date()
),
},
],
})
.then((docRef) => {
tradeInstanceID = docRef.id;
tradeOffer
.set(
{
status: "accepted",
tradeInstanceID: docRef.id,
},
{ merge: true }
)
.then(() => {
var receiver = db.collection("users").doc(senderID);
var notification = {
from: auth.currentUser.uid,
fromUsername: userData.username,
type: "tradeOfferAccepted",
time: firebase.firestore.Timestamp.fromDate(
new Date()
),
seen: false,
};
receiver
.update({
notifications: firebase.firestore.FieldValue.arrayUnion(
notification
),
})
.then(() => {
response = {
sendersItems: senderItemsTemp,
receiversItems: receiverItemsTemp,
};
return response;
});
});
})
.catch((err) => console.log(err));
}
});
});
});
});
});
}
And here is where I am calling it:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
async function acceptTradeOffer() {
var tradeOfferID = currentTradeFocus;
var senderID = "";
setLoading("loading");
if (userData !== null && tradeOfferID !== "") {
const response = await firebaseAcceptTradeOffer(
currentTradeFocus,
userData
);
console.log(
"RESPONSE FROM FIREBASE SERVICE>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>: ",
response
);
if (!response) {
setErrorMsg("One of the selected items is no longer available.");
} else if (
response.sendersItems !== null &&
response.receiversItems !== null
) {
setSenderItems(response.sendersItems);
setReceiverItems(response.receiversItems);
toggleConfirmScreen("cancel");
setLoading("idle");
setItemsSet(true);
}
fetch(
"https://europe-west2-seekio-86408.cloudfunctions.net/sendMail?type=tradeUpdate&userID=" +
senderID
).catch((err) => {
console.log(err);
setLoading("idle");
});
}
}
So basically I want to go and check if any of the items in this 'trade' are not equal to 'listed' (which means they are not available, I want to return false, if not, then I return the array of items so the trade can continue.
EDIT: I've tried to rejig it all and it's half working. A top level look at what I am trying to do:
User wants to accept a trade offer for some items >
Check through all items to make sure they are available and not sold >
If so, accept the trade >
Then once its accepted, go and cancel all remaining trade offers that include items from this accepted trade, cause they are not available anymore.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
console.log(
"----- starting firebaseAcceptTradeOffer--------- ",
unavailableItem
);
//==============
var tradeInstanceID;
var senderID;
var receiverID;
var senderItemsTemp;
var receiverItemsTemp;
var unavailableItem = false;
var response;
var itemsArray;
var notListed = false;
//==============
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
unavailableItem = tradeOffer
.get()
.then((doc) => {
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
})
.then(() => {
itemsArray.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
notListed = true;
}
});
});
})
.then(() => {
return notListed;
});
console.log(
"-----unavailableItem at the end of method --------- ",
unavailableItem
);
//^^^^^^^^^^ here i am getting a promise result of false (which is correct) but HOW CAN I ACCESS IT
if (unavailableItem) {
tradeOffer.update({
status: "declined",
});
return false;
} else {
response = await createTrade(
tradeOffer,
tradeOfferID,
senderID,
receiverID,
userData.username
);
console.log("response from createTrade", response);
return response;
}
}
I am getting a promise object back with the value false above. False is correct value I am expecting, but how can I access it? its in the form of a promise object?
I have some time on my hands, so let's break this down.
Notes on Variables
If you aren't using TypeScript (and even if you are), I highly recommend inserting the type into the name of your variables.
db # ✔ by convention, either firebase.database() or firebase.firestore()
tradeOffer # ❓ type unclear, could be a number, an object, a string, etc
tradeOfferDocRef # ✔ a DocumentReference
trades # ❓ type unclear, plural implies a collection of some sort
tradesColRef # ✔ a CollectionReference
You may also encounter these:
doc # ❓ by convention, a DocumentSnapshot, but with unknown data
tradeDoc # ✔ implies a DocumentSnapshot<TradeData> (DocumentSnapshot containing trade data)
When using just doc, you need to look around where its used for context on what this DocumentSnapshot contains.
db.collection('trades').doc(tradeOfferID).get()
.then((doc) => { // contents implied to be TradeData
const data = doc.data();
});
// or
tradeDocRef.get()
.then((doc) => { // contents implied to be TradeData
const data = doc.data();
});
You should rename doc as appropriate, especially when using async/await syntax, so you don't end up in situations like:
const doc = await db.collection('trades').doc(tradeOfferID).get();
/* ... many lines ... */
const senderID = doc.get("senderID"); // what was doc again?
As you've tagged reactjs in your question, this implies you are using modern JavaScript.
Ditch any use of var and replace it with the block-scoped versions: const (prevents reassigning the variable) or let (similar to var, but not quite). These are safer and prevents the chances of accidentally overwriting something you shouldn't.
You can also make use of Object destructuring to assign your variables.
const senderID = doc.data().senderID;
const receiverID = doc.data().receiverID;
const itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
can become:
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
If you ever need only a single property out of a document, you should use DocumentSnapshot#get() instead of DocumentSnapshot#data() so it will parse only the field you want instead of the whole document's data.
function getUserAddress(uid) {
return firebase.firestore()
.collection('users')
.doc(uid)
.get()
.then(userDoc => userDoc.get("address")); // skips username, email, phone, etc
}
Notes on Promises
var senderID;
var receiverID;
var itemsArray;
tradeOfferDocRef
.get()
.then((doc) => {
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
})
.then(() => {
/* use results from above */
});
While the above code block functions as intended, when you have many of these variables like this as you do, it becomes unclear when and where they are set.
It also leads to problems like this where you think the variable has a value:
var senderID;
var receiverID;
var itemsArray;
tradeOfferDocRef
.get()
.then((doc) => {
// this line runs after the line below
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
});
// this line before the line above
console.log(senderID); // will always log "undefined"
This can be avoided in one of three ways:
Returning data to pass through to the next handler (you wouldn't use this for this example, only if the next then() handler is elsewhere):
tradeOfferDocRef
.get()
.then((doc) => {
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
return { senderID, receiverID, itemsArray }; // pass to next step
})
.then((neededData) =>
/* use neededData.senderID, neededData.receiverID, etc */
});
Using the data within the same handler:
tradeOfferDocRef
.get()
.then((doc) => {
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
/* use results from above */
});
Using async-await syntax:
const tradeDoc = await tradeOfferDocRef.get();
const { senderID, receiverID, sendersItems, receiversItems } = tradeDoc.data();
const itemsArray = sendersItems.concat(receiversItems);
/* use results from above */
Writing to Firestore
Your current code consists of the following steps:
1. Get the trade offer document</li>
2. If successful, pull out the sender and receiver's IDs, along with any items in the trade
3. If successful, do the following for each item in the sender items array:
a) Check if any of the sender's items are unavailable</li>
b) If successful, do the following for each item in the receiver items array:
- If **any item** was unavailable prior to this, decline the trade & return `false`.
- If all items **so far** are available, do the following:
a) Create a document containing information about the trade with the needed data
b) If successful, edit the trade offer document to accept it
c) If successful, create a notification for the receiver
d) If successful, return the traded items
e) If any of a) to d) fail, log the error and return `undefined` instead
4. Return `undefined`
In the above steps, you can see some problems with your promise chaining. But aside from that, you can also see that you create and edit documents one-by-one instead of all-at-once ("atomically"). If any of these writes were to fail, your database ends up in an unknown state. As an example, you could have created and accepted a trade, but failed to create the notification.
To atomically write to your database, you need to use a batched write where you bundle a bunch of changes together and then send them off to Firestore. If any of them were to fail, no data is changed in the database.
Next, you store a user's notifications inside of their user document. For a small number of notifications this is fine, but do you need to download all of those notifications if you wanted to pull just an address or phone number like in the example in the above section? I recommend splitting them out into their own document (such as /users/{someUserId}/metadata/notifications), but ideally their own collection (such as /users/{someUserId}/notifications/{someNotificationID}). By placing them in their own collection, you can query them and use QuerySnapshot#docChanges to synchronize changes and use Cloud Firestore triggers to send push notifications.
Refactored Function
1. Get the trade offer document</li>
2. Once the retrieved, do the following depending on the result:
- If failed or empty, return an error
- If successful, do the following:
a) Pull out the sender and receiver's IDs, along with any items in the trade.
b) For each item in the trade, check if any are unavailable and once the check has completed, do the following depending on the result:
- If any item is unavailable, do the following:
a) Decline the trade
b) Return the list of unavailable items
- If all items are available, do the following:
a) Create a new write batch containing:
- Create a document about the trade
- Edit the trade offer document to accept it
- Create a notification for the receiver
b) Commit the write batch to Firestore
c) Once the commit has completed, do the following depending on the result:
- If failed, return an error
- If successful, return the traded items and the trade's ID
Because the steps here depend on each other, this is a good candidate to use async/await syntax.
To see this in action, closely study this:
import * as firebase from "firebase-admin";
// insert here: https://gist.github.com/samthecodingman/aea3bc9481bbab0a7fbc72069940e527
async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
const tradeOfferDocRef = db.collection("tradeOffers").doc(tradeOfferID);
const tradeDoc = await tradeOfferDocRef.get();
const { senderID, receiverID, sendersItems, receiversItems } =
tradeDoc.data();
const itemsArray = sendersItems.concat(receiversItems);
// TODO: Check if this is an accurate assumption
if (sendersItems.length == 0 || receiversItems.length == 0) {
success: false,
message: "One-sided trades are not permitted",
detail: {
sendersItemsIDs: sendersItems.map(({ itemID }) => itemID),
receiversItemsIDs: receiversItems.map(({ itemID }) => itemID),
},
};
const listingsColQuery = db
.collection("listings")
.where("status", "==", "listed");
const uniqueItemIds = Array.from(
itemsArray.reduce(
(set, { itemID }) => set.add(itemID),
new Set()
)
);
const foundIds = {};
await fetchDocumentsWithId(
listingsColQuery,
uniqueItemIds,
(listingDoc) => {
// if here, listingDoc must exist because we used .where("status") above
foundIds[listingDoc.id] = true;
}
);
const unavailableItemIDs = uniqueItemIds
.filter(id => !foundIds[id]);
if (unavailableItems.length > 0) {
// one or more items are unavailable!
await tradeOfferDocRef.update({
status: "declined",
});
return {
success: false,
message: "Some items were unavailable",
detail: {
unavailableItemIDs,
},
};
}
const tradeDocRef = db.collection("trades").doc();
const tradeInstanceID = tradeDocRef.id;
const batch = db.batch();
batch.set(tradeDocRef, {
tradeOfferID,
senderTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
receiverTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
postagePhotos: [],
inspectionPhotos: [],
senderPaid: false,
receiverPaid: false,
senderUploadedProof: false,
receiverUploadedProof: false,
senderID,
receiverID,
messages: [
{
message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
sender: "System",
timestamp: firebase.firestore.Timestamp.fromDate(new Date()),
},
],
});
batch.set(
tradeOfferDocRef,
{
status: "accepted",
tradeInstanceID,
},
{ merge: true }
);
const receiverNotificationRef = db
.collection("users")
.doc(senderID)
.collection("notifications")
.doc();
batch.set(receiverNotificationRef, {
from: auth.currentUser.uid,
fromUsername: userData.username,
type: "tradeOfferAccepted",
time: firebase.firestore.Timestamp.fromDate(new Date()),
seen: false,
});
await batch.commit();
return {
success: true,
message: "Trade accepted",
detail: {
tradeID: tradeInstanceID,
senderItems,
receiversItems,
},
};
}
Usage:
try {
const tradeResult = await firebaseAcceptTradeOffer(someTradeId);
} catch (err) {
// if here, one of the following things happened:
// - syntax error
// - database read/write error
// - database rejected batch write
}
In general, when you are returning a promise where it can't be resolved you must await its result. Additionally, you must be returning a value from within a promise then chain, at minimal the last .then() needs to be returning a value, this can also be done within a .finally() method.
Using Get from any firebase resource, realtime, firestore, and storage are all Async processes and must be awaited. in your case, you are missing an await for the return:
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
return tradeOffer
and you don't appear to be returning anything inside your .then() statements, I would suggest a complete rewrite of what you are trying to so you are returning values as they are needed.

Firebase Cloud Function error: Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array

I want to send a notification to all users who are confirmed guests when the object confirmedGuests is created in the Firebase Realtime Database.
So, I first create an array of all the users from confirmedGuests object. Then, I iterate through all these users and push their deviceTokens to an array of deviceTokens. The array allDeviceTokens is expected to be the array of device tokens of all users in confirmedGuests.
However, when confirmedGuests object is created, the function returns an error.
Below is my cloud function
exports.sendNotification = functions.database
.ref('/feed/{pushId}/confirmedGuests')
.onCreate((snapshot, context) => {
const pushId = context.params.pushId;
if (!pushId) {
return console.log('missing mandatory params for sending push.')
}
let allDeviceTokens = []
let guestIds = []
const payload = {
notification: {
title: 'Your request has been confirmed!',
body: `Tap to open`
},
data: {
taskId: pushId,
notifType: 'OPEN_DETAILS', // To tell the app what kind of notification this is.
}
};
let confGuestsData = snapshot.val();
let confGuestItems = Object.keys(confGuestsData).map(function(key) {
return confGuestsData[key];
});
confGuestItems.map(guest => {
guestIds.push(guest.id)
})
for(let i=0; i<guestIds.length; i++){
let userId = guestIds[i]
admin.database().ref(`/users/${userId}/deviceTokens`).once('value', (tokenSnapshot) => {
let userData = tokenSnapshot.val();
let userItem = Object.keys(userData).map(function(key) {
return userData[key];
});
userItem.map(item => allDeviceTokens.push(item))
})
}
return admin.messaging().sendToDevice(allDeviceTokens, payload);
});
You're loading each user's device tokens from the realtime database with:
admin.database().ref(`/users/${userId}/deviceTokens`).once('value', (tokenSnapshot) => {
This load operation happens asynchronously. This means that by the time the admin.messaging().sendToDevice(allDeviceTokens, payload) calls runs, the tokens haven't been loaded yet.
To fix this you'll need to wait until all tokens have loaded, before calling sendToDevice(). The common approach for this is to use Promise.all()
let promises = [];
for(let i=0; i<guestIds.length; i++){
let userId = guestIds[i]
let promise = admin.database().ref(`/users/${userId}/deviceTokens`).once('value', (tokenSnapshot) => {
let userData = tokenSnapshot.val();
let userItem = Object.keys(userData).map(function(key) {
return userData[key];
});
userItem.map(item => allDeviceTokens.push(item))
return true;
})
promises.push(promise);
}
return Promise.all(promises).then(() => {
return admin.messaging().sendToDevice(allDeviceTokens, payload);
})

Nested HTTP requests in Firebase cloud function

I'm using an HTTP-triggered Firebase cloud function to make an HTTP request. I get back an array of results (events from Meetup.com), and I push each result to the Firebase realtime database. But for each result, I also need to make another HTTP request for one additional piece of information (the category of the group hosting the event) to fold into the data I'm pushing to the database for that event. Those nested requests cause the cloud function to crash with an error that I can't make sense of.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require('request');
exports.foo = functions.https.onRequest(
(req, res) => {
var ref = admin.database().ref("/foo");
var options = {
url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
json: true
};
return request(
options,
(error, response, body) => {
if (error) {
console.log(JSON.stringify(error));
return res.status(500).end();
}
if ("results" in body) {
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if ("name" in result &&
"description" in result &&
"group" in result &&
"urlname" in result.group
) {
var groupOptions = {
url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
json: true
};
var categoryResult = request(
groupOptions,
(groupError, groupResponse, groupBody) => {
if (groupError) {
console.log(JSON.stringify(error));
return null;
}
if ("category" in groupBody &&
"name" in groupBody.category
) {
return groupBody.category.name;
}
return null;
}
);
if (categoryResult) {
var event = {
name: result.name,
description: result.description,
category: categoryResult
};
ref.push(event);
}
}
}
return res.status(200).send("processed events");
} else {
return res.status(500).end();
}
}
);
}
);
The function crashes, log says:
Error: Reference.push failed: first argument contains a function in property 'foo.category.domain._events.error' with contents = function (err) {
if (functionExecutionFinished) {
logDebug('Ignoring exception from a finished function');
} else {
functionExecutionFinished = true;
logAndSendError(err, res);
}
}
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1436:15)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
If I leave out the bit for getting the group category, the rest of the code works fine (just writing the name and description for each event to the database, no nested requests). So what's the right way to do this?
I suspect this issue is due to the callbacks. When you use firebase functions, the exported function should wait on everything to execute or return a promise that resolves once everything completes executing. In this case, the exported function will return before the rest of the execution completes.
Here's a start of something more promise based -
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require("request-promise-native");
exports.foo = functions.https.onRequest(async (req, res) => {
const ref = admin.database().ref("/foo");
try {
const reqEventOptions = {
url:
"https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=xxxxxx",
json: true
};
const bodyEventRequest = await request(reqEventOptions);
if (!bodyEventRequest.results) {
return res.status(200).end();
}
await Promise.all(
bodyEventRequest.results.map(async result => {
if (
result.name &&
result.description &&
result.group &&
result.group.urlname
) {
const event = {
name: result.name,
description: result.description
};
// get group information
const groupOptions = {
url:
"https://api.meetup.com/" +
result.group.urlname +
"?sign=true&photo-host=public&key=xxxxxx",
json: true
};
const categoryResultResponse = await request(groupOptions);
if (
categoryResultResponse.category &&
categoryResultResponse.category.name
) {
event.category = categoryResultResponse.category.name;
}
// save to the databse
return ref.push(event);
}
})
);
return res.status(200).send("processed events");
} catch (error) {
console.error(error.message);
}
});
A quick overview of the changes -
Use await and async calls to wait for things to complete vs. being triggered in a callback (async and await are generally much easier to read than promises with .then functions as the execution order is the order of the code)
Used request-promise-native which supports promises / await (i.e. the await means wait until the promise returns so we need something that returns a promise)
Used const and let vs. var for variables; this improves the scope of variables
Instead of doing checks like if(is good) { do good things } use a if(isbad) { return some error} do good thin. This makes the code easier to read and prevents lots of nested ifs where you don't know where they end
Use a Promise.all() so retrieving the categories for each event is done in parallel
There are two main changes you should implement in your code:
Since request does not return a promise you need to use an interface wrapper for request, like request-promise in order to correctly chain the different asynchronous events (See Doug's comment to your question)
Since you will then call several times (in parallel) the different endpoints with request-promise you need to use Promise.all() in order to wait all the promises resolve before sending back the response. This is also the case for the different calls to the Firebase push() method.
Therefore, modifying your code along the following lines should work.
I let you modifying it in such a way you get the values of name and description used to construct the event object. The order of the items in the results array is exactly the same than the one of the promises one. So you should be able, knowing that, to get the values of name and description within results.forEach(groupBody => {}) e.g. by saving these values in a global array.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
var rp = require('request-promise');
exports.foo = functions.https.onRequest((req, res) => {
var ref = admin.database().ref('/foo');
var options = {
url:
'https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****',
json: true
};
rp(options)
.then(body => {
if ('results' in body) {
const promises = [];
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if (
'name' in result &&
'description' in result &&
'group' in result &&
'urlname' in result.group
) {
var groupOptions = {
url:
'https://api.meetup.com/' +
result.group.urlname +
'?sign=true&photo-host=public&key=****',
json: true
};
promises.push(rp(groupOptions));
}
}
return Promise.all(promises);
} else {
throw new Error('err xxxx');
}
})
.then(results => {
const promises = [];
results.forEach(groupBody => {
if ('category' in groupBody && 'name' in groupBody.category) {
var event = {
name: '....',
description: '...',
category: groupBody.category.name
};
promises.push(ref.push(event));
} else {
throw new Error('err xxxx');
}
});
return Promise.all(promises);
})
.then(() => {
res.send('processed events');
})
.catch(error => {
res.status(500).send(error);
});
});
I made some changes and got it working with Node 8. I added this to my package.json:
"engines": {
"node": "8"
}
And this is what the code looks like now, based on R. Wright's answer and some Firebase cloud function sample code.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require("request-promise-native");
exports.foo = functions.https.onRequest(
async (req, res) => {
var ref = admin.database().ref("/foo");
var options = {
url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
json: true
};
await request(
options,
async (error, response, body) => {
if (error) {
console.error(JSON.stringify(error));
res.status(500).end();
} else if ("results" in body) {
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if ("name" in result &&
"description" in result &&
"group" in result &&
"urlname" in result.group
) {
var groupOptions = {
url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
json: true
};
var groupBody = await request(groupOptions);
if ("category" in groupBody && "name" in groupBody.category) {
var event = {
name: result.name,
description: result.description,
category: groupBody.category.name
};
await ref.push(event);
}
}
}
res.status(200).send("processed events");
}
}
);
}
);

Get Firebase Database Value into a Cloud Function

I'm currently using Firebase Functions to send automatic push notifications when the database is uploaded. It's working perfectly, I'm just wondering how I can get a specific value from my database, for example PostTitle and display it on, for example title.
In Firebase my database is /post/(postId)/PostTitle
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// database tree
exports.sendPushNotification = functions.database.ref('/posts/{id}').onWrite(event =>{
const payload = {
notification: {
title: 'This is the title.',
body: 'There is a new post available.',
badge: '0',
sound: 'default',
}
};
return admin.database().ref('fcmToken').once('value').then(allToken => {
if (allToken.val()){
const token = Object.keys(allToken.val());
console.log(`token? ${token}`);
return admin.messaging().sendToDevice(token, payload).then(response =>{
return null;
});
}
return null;
});
});
If I understand correctly that you want to get the PostTitle from the node that triggers the Cloud Function, the following should do the trick:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// database tree
exports.sendPushNotification = functions.database.ref('/posts/{id}').onWrite(event =>{
const afterData = event.data.val();
const postTitle = afterData.PostTitle; //You get the value of PostTitle
const payload = {
notification: {
title: postTitle, //You then use this value in your payload
body: 'There is a new post available.',
badge: '0',
sound: 'default',
}
};
return admin.database().ref('fcmToken').once('value').then(allToken => {
if (allToken.val()){
const token = Object.keys(allToken.val());
console.log(`token? ${token}`);
return admin.messaging().sendToDevice(token, payload)
} else {
throw new Error('error message to adapt');
}
})
.catch(err => {
console.error('ERROR:', err);
return false;
});
});
Note the following points:
You are using the old syntax for Cloud Functions, i.e. the one of versions <= v0.9.1. You should migrate to the new version and syntax, as explained here: https://firebase.google.com/docs/functions/beta-v1-diff#realtime-database
I have re-organised your promise chaining and also added a catch() at the end of the chain.
I'd use ...
var postTitle = event.data.child("PostTitle").val;
while possibly checking, it the title even has a value
... before sending out any notifications.

Firebase Cloud Messaging for sending notification to all the registered devices not working

For my magazine app,I am using Firebase service.One function of this android app is whenever new article is published;notification of new article is sent to all the devices.
I am saving all the device tokens in db like this:
FCMToken
{
userid:deviceToken
}
So whenever new node is added in "published" key in firebase db,FCM function is triggered and messages is sent to all the devices:
Below is my code in javascript for FCM function:
'use strict'
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/published/{msg_id}').onWrite(event => {
const snapshot = event.data;
// Only send a notification when a new message has been created.
if (snapshot.previous.val()) {
return;
}
const msg_id = event.params.msg_id;
const msg_val=admin.database().ref(`messages/${msg_id}`).once('value');
return msg_val.then(msgResult =>{
const msg_title=msgResult.val().title;
const user_id=msgResult.val().userId;
console.log('msg title is',msg_title);
console.log('We have a new article : ', msg_id);
const payload={
data : {
title:"New Article",
body: msg_title,
msgid : msg_id,
userid : user_id
}
};
// const deviceToken = admin.database().ref('/FCMToken/{user_id}').once('value');
admin.database().ref('/FCMToken').on("value", function(dbsnapshot)
{
dbsnapshot.forEach(function(childSnapshot) {
//var childKey = childSnapshot.key;
const childData = childSnapshot.val();
const deviceToken=console.log("device token" + childSnapshot.val());
return admin.messaging().sendToDevice(childData,payload).then(response=>{
console.log("This was notification feature")
console.log("response: ", response);
})
.catch(function(error)
{
console.log("error sending message",error)
});
});
});
});
});
For some reason,notification is only sent to only 1 device(the first token in FCM node).
Update:
I have updated my code and using promise,but for some reason it is still not working,just sending notification to first device token.
'use strict'
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/published/{msg_id}').onWrite(event => {
const snapshot = event.data;
// Only send a notification when a new message has been created.
if (snapshot.previous.val()) {
return;
}
const msg_id = event.params.msg_id;
const msg_val=admin.database().ref(`messages/${msg_id}`).once('value');
return msg_val.then(msgResult =>{
const msg_title=msgResult.val().title;
const user_id=msgResult.val().userId;
console.log('msg title is',msg_title);
console.log('We have a new article : ', msg_id);
const payload={
data : {
title:"New Article",
body: msg_title,
msgid : msg_id,
userid : user_id
}
};
const promises=[];
// const deviceToken = admin.database().ref('/FCMToken/{user_id}').once('value');
admin.database().ref('/FCMToken').once('value').then(function(dbsnapshot)
{
dbsnapshot.forEach(function(childSnapshot) {
//var childKey = childSnapshot.key;
const childData = childSnapshot.val();
const deviceToken=console.log("device token" + childSnapshot.val());
const promise = admin.messaging().sendToDevice(childData,payload).then(response=>{
promises.push(promise)
console.log("This was notification feature")
console.log("response: ", response);
})
return Promise.all(promises)
.catch(function(error)
{
console.log("error sending message",error)
});
});
});
});
});
Response object is giving this output: response: { results: [ { error: [Object] } ],
canonicalRegistrationTokenCount: 0,
failureCount: 1,
successCount: 0,
multicastId: 6411440389982586000 }
You're not using promises correctly throughout your function. There are two things wrong.
First, you should be querying the database using once() instead of on(), and using the promise returned from it in order to proceed to the next item of work:
admin.database().ref('/FCMToken').on("value")
.then(result => /* continue your work here */)
Also, you can't return a promise out of the forEach loop. Instead, you need to return a promise at the top level of the function, as the very last step in the function. This promise needs to resolve when all of the work is done in this function. For your function, this means when all of the messages are sent. You'll have to collect all the promises for all of the messages in an array, then return a single promise that resolves when they all resolve. The general form of that looks like this:
const promises = []
dbsnapshot.forEach(function(childSnapshot) {
// remember each promise for each message sent
const promise = return admin.messaging().sendToDevice(...)
promises.push(promise)
})
// return a single promise that resolves when everything is done
return Promise.all(promises)
Please take care to learn how promises work in JavaScript. You won't be able to write effective functions without dealing with promises correctly.
So I figured out another method to get values.
const tokens= Object.keys(tokensSnapshot.val()).map(e => tokensSnapshot.val()[e]);
Below is my complete method:
'use strict'
const functions = require('firebase-functions');
const admin = require('firebase-admin');
//Object.values = require('object.values');
admin.initializeApp(functions.config().firebase);
exports.sendNotification = functions.database.ref('/published/{msg_id}').onWrite(event => {
const snapshot = event.data;
// Only send a notification when a new message has been created.
if (snapshot.previous.val()) {
return;
}
const msg_id = event.params.msg_id;
const msg_val=admin.database().ref(`messages/${msg_id}`).once('value');
return msg_val.then(msgResult =>{
const msg_title=msgResult.val().title;
const user_id=msgResult.val().userId;
console.log('msg title is',msg_title);
console.log('We have a new article : ', msg_id);
const payload={
data : {
title:"New Article",
body: msg_title,
msgid : msg_id,
userid : user_id
}
};
const getDeviceTokensPromise = admin.database().ref('/FCMToken').once('value');
return Promise.all([getDeviceTokensPromise, msg_title]).then(results => {
const tokensSnapshot = results[0];
const msgi = results[1];
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
console.log('There are', tokensSnapshot.numChildren(), 'tokens to send notifications to.');
console.log("tokenslist",tokensSnapshot.val());
const tokens= Object.keys(tokensSnapshot.val()).map(e => tokensSnapshot.val()[e]);
//var values = Object.keys(o).map(e => obj[e])
return admin.messaging().sendToDevice(tokens, payload).then(response => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
}
});
return Promise.all(tokensToRemove);
});
});
});
});

Categories