Async/Await don't work in Google cloud functions - javascript

I work with google cloud functions and cloud messaging on Firebase. However, when I try to use Async/Await functions, I have an error: error Parsing error: Unexpected token =>
I work with Node.js v16.
Here is my code:
const functions = require("firebase-functions");
// const mess = require("firebase/messaging");
const admin = require("firebase-admin");
exports.sendListenerPushNotificationProductUpdate = functions.database
.ref("Products/{product}/type/")
.onUpdate(async (snapshot, context) => {
...
const tokensSnapshot = await Promise.resolve(getDeviceTokensPromise);
console.log("TOKEN: " + JSON.stringify(tokensSnapshot));
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return functions.logger.log(
"There are no notification tokens to send to."
);
}
...
// Listing all tokens as an array.
const tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
// For each message check if there was an error.
const tokensToRemove = [];
return Promise.all(tokensToRemove);
});

Does getDeviceTokensPromise return a Promise? If so, that line should just be
const tokensSnapshot = await getDeviceTokensPromise()

Related

How to reference the database that I want to write to in my cloud function?

I'm trying to write to a subset within my database and I get one console error and another error in the google cloud functions saying:
Error: Unauthorized
Your client does not have permission to the requested URL /updateFirestore.
&
Error: Value for argument "collectionPath" is not a valid resource path. Path must be a non-empty string.
at undefined. ( /workspace/index.js:18 )
at .processTicksAndRejections ( node:internal/process/task_queues:96 )
This is how my firestore is set up:
Here is where I'm trying to call my firestore database in my index.js function:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.updateFirestore = functions.database
.ref("studiopick/studio/users/{uid}")
.onWrite((change, context) => {
const uid = context.params.uid;
if (!change.after.exists()) {
return null;
}
// Grab the current value of the Realtime Database.
const data = change.after.val();
const firestoreDb = admin.firestore();
const docReference = firestoreDb.collection("studiopick/studios/" + uid);
return docReference.set(
{
TransmitterError: data.TransmitterError,
},
{merge: true},
);
});

firestore database reference doesnt work with await [duplicate]

This question already has answers here:
await is only valid in async function
(14 answers)
Closed 11 months ago.
So I'm trying to use the twitter Api to test functionality and do specific tasks. I decided to use firestore to keep the relevant data. However when I create a database reference and try to use await later on in the code, it gives me an error. This is the code.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const {Firestore} = require('#google-cloud/firestore');
const firestore = new firestore();
const dbRef = firestore.doc('tokens/demo');
const TwitterApi = require('twitter-api-v2').default;
const twitterClient = new TwitterApi({
clientId: 'clientid',
clientSecret: 'clientsecret',
});
const callbackURL = 'http://127.0.0.1:5001/twbt-ad868/us-central1/callback';
// STEP 1 - Auth URL
exports.auth = functions.https.onRequest((request, response) => {
const { url, codeVerifier, state } = twitterClient.generateOAuth2AuthLink(
callbackURL,
{ scope: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'] }
);
// store verifier
await dbRef.set({ codeVerifier, state });
response.redirect(url);
});
exports.callback = functions.https.onRequest((request, response) => {
});
exports.tweet = functions.https.onRequest((request, respone) => {});
and this is the error I get
await dbRef.set({ codeVerifier, state });
^^^^^
SyntaxError: await is only valid in async function
I've tried using this code instead to reference the json file in firestore, but I still get the same error
const dbRef = admin.firestore().doc('tokens/demo');
I'm assuming this is because my program isn't properly accessing the database in firestore? When I run this command
gcloud firestore operations list
I get
Listed 0 items.
If this is the case I'm not sure how to fix this and have my code access the database properly
Thank you in advance for any help you can provide me.
You must create an async function in order to have an await since it requires a promise.
async function () { await dbRef.set({ codeVerifier, state });}

How to take a list of IDs from an array, match each item to the ID in the firestore, and return a JSON of the associated data to front-end site?

Like the title says, I am trying to take a list of IDs from an array, match each ID to the ID in the firestore, and return an array of JSONs of the associated data to front-end site. The code below is returning an empty array.
const functions = require("firebase-functions");
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.getPlayerDataFromTokens = functions.https.onCall(async (data, context) => {
return new Promise((resolve, reject) => {
var db = admin.firestore();
const tokens = data.tokens;
let playerArray = [];
tokens.forEach((token) => {
const tokensToTokenData = db.collection("Football_Player_Data").where("Token", "==", token);
tokensToTokenData.get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
playerDataToken = doc.data()["Token"];
playerDataJersey = doc.data()["Jersey_Number"];
playerDataMultiplier = doc.data()["Multiplier"];
playerDataPlayerID = doc.data()["PlayerID"];
playerDataPosition = doc.data()["Position"];
playerDataTeam = doc.data()["Team"];
playerData = {
"Token": playerDataToken,
"PlayerID": playerDataPlayerID,
"Jersey_Number": playerDataJersey,
"Position": playerDataPosition,
"Team": playerDataTeam,
"Multiplier": playerDataMultiplier
};
playerArray.push(playerData);
});
})
.catch((error) => {
playerArray = error;
console.log("Error getting documents: ", error)
reject(playerArray);
});
})
resolve(JSON.stringify(playerArray));
});
})
Based on your code, you are building this based on a very outdated tutorial.
In Firebase Functions V1.0.0 (April 2018), functions.config().firebase was removed. It now resolves as undefined so that the modern admin.initializeApp() (with no arguments) functions properly. When invoked this way, the Admin SDK initializes based on the presence of the appropriate environment variables.
Next, you are using the Explicit Promise Construction Antipattern. In short, do not wrap your code inside new Promise((resolve, reject) => {}).
As this functions code is likely to be deployed to Google Cloud Functions, which runs on Node 10/12/14 you can use modern ES2017 syntax like async/await, deconstruction patterns, import, let, const and Promise.all.
So this means the top of your script consists of:
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
admin.initializeApp();
As you need to fetch a list of players for each token given, you can break this logic out into it's own function:
/**
* For the given token, return an array of players linked with
* that token.
*/
async function getFootballPlayersWithToken(db, token) {
const playersWithTokenQuerySnapshot = await db
.collection("Football_Player_Data")
.where("Token", "==", token)
.get();
if (playersWithTokenQuerySnapshot.empty) {
console.log("Given token did not match any players: ", token);
return []; // or throw error
}
const playerArray = await Promise.all(
playersWithTokenQuerySnapshot.docs
.map((playerDoc) => {
const { Token, Jersey_Number, Multiplier, PlayerID, Position, Team } = playerDoc.data();
return { Token, Jersey_Number, Multiplier, PlayerID, Position, Team };
});
);
return playerArray;
}
In the above code block, it's important to note that playersWithTokenQuerySnapshot is a QuerySnapshot object, not an array. While forEach is available on the QuerySnapshot object, to use normal Array methods like .map and .reduce, you need to use the docs property instead.
This makes your Cloud Function:
exports.getPlayerDataFromTokens = functions.https.onCall(async (data, context) => {
const { tokens } = data;
try {
const db = admin.firestore();
const playersGroupedByTokenArray = await Promise.all(
tokens.map(token => getFootballPlayersWithToken(db, token))
);
// playersGroupedByTokenArray is an array of arrays
// e.g. [[token1Player1, token1Player2], [token2Player1], [], ...]
// flatten this array
// Node 12+:
// const playerArray = playersGroupedByTokenArray.flat()
// Node 10+:
const playerArray = [];
playersGroupedByTokenArray.forEach(
groupOfPlayers => playerArray.push(...groupOfPlayers)
);
return playerArray;
} catch (error) {
console.error("Error finding players for one or more of these tokens: ", tokens.join(", "))
throw new functions.https.HttpsError(
"unknown",
"Could not get players for one or more of the tokens provided",
error.code || error.message
);
}
);
As shown in the above code block, for errors to be handled properly, you must wrap them in a HttpsError as documented here.
If a token would return only one player, you can tweak the above get players function to just the following and then edit your Cloud Function as needed:
/**
* For the given token, return the matching player.
*/
async function getFootballPlayerByToken(db, token) {
const playersWithTokenQuerySnapshot = await db
.collection("Football_Player_Data")
.where("Token", "==", token)
.get();
if (playersWithTokenQuerySnapshot.empty) {
console.log("Given token did not match any players: ", token);
return null; // or throw error
}
const playerDoc = playersWithTokenQuerySnapshot.docs[0];
const { Token, Jersey_Number, Multiplier, PlayerID, Position, Team } = playerDoc.data();
return { Token, Jersey_Number, Multiplier, PlayerID, Position, Team };
}

Cloud Function to export new documents from Firestore to GCP bucket

I am trying to write a cloud function to export only the new documents getting added to my 'reviews' sub-collection. The trigger for this cloud function is: Cloud Firestore. However, my cloud function deployment fails through the console. Could someone please help me understand what's wrong with my cloud function?
Error message:
Deployment failure:
Build failed: /workspace/index.js:26
}
^
SyntaxError: missing ) after argument list
at new Script (vm.js:83:7)
at checkScriptSyntax (internal/bootstrap/node.js:620:5)
at startup (internal/bootstrap/node.js:280:11)
at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3); Error ID: d984e68f
Cloud function code:
const firestore = require('#google-cloud/firestore');
const client = new firestore.v1.FirestoreAdminClient();
const bucket = 'gs://bucket_name'
exports.scheduledFirestoreBackup = (event, context) => {
const databaseName = client.databasePath(
// process.env.GCLOUD_PROJECT,
"fs124",
'(default)'
);
return client
.exportDocuments({
name: databaseName,
outputUriPrefix: bucket,
collectionIds: ['reviews'],
})
.onSnapshot()
.then(snap => {
snap.forEach(doc => {
const response = doc.data();
console.log(doc.data());
return response;
}
});
Console snippet:
The message you are getting, SyntaxError: missing ) after argument list is pretty clear. You are missing the closing curly bracket} and parenthesis) of then(). It should look something like this:
const firestore = require('#google-cloud/firestore');
const client = new firestore.v1.FirestoreAdminClient();
const bucket = 'gs://bucket_name'
exports.scheduledFirestoreBackup = (event, context) => {
const databaseName = client.databasePath(
// process.env.GCLOUD_PROJECT,
"fs124",
'(default)'
);
return client
.exportDocuments({
name: databaseName,
outputUriPrefix: bucket,
collectionIds: ['reviews'],
})
.onSnapshot()
.then(snap => {
snap.forEach(doc => {
const response = doc.data();
console.log(doc.data());
return response;
});
});
};

Calling a Firebase Cloud Function 'ForEach' child of a Snapshot

I'm trying to deploy a Firebase Cloud Function that sends a text message to its associated recipient for x number of text messages. The function is triggered in my iOS app when an update is made to the 'send' Realtime Database reference, indicating that the user has pressed the 'send' button.
My Firebase structure is
{
"user1uid": {
"send": false
"messagesToSend": {
"messageuid1": {
"messageText": "What's for dinner?",
"recipientNumber": "+18017378888",
}
"messageuid2:
"messageText": "Who won the Cowboys game?",
"recipientNumber": "+18017377787",
}
}
"user2uid": {
"send": false
"messagesToSend": {
"messageuid1": {
"messageText": "What's for dinner?",
"recipientNumber": "+18017378888",
}
"messageuid2:
"messageText": "Who won the Cowboys game?",
"recipientNumber": "+18017377787",
}
}
}
My code currently only sends one message, and I'm not sure how I can properly iterate through the messagesToSend node for each user and send all the messages in it.
I've been trying to follow the tutorial located here. I have looked at the following Stack Overflow responses but am unable to decipher or derive a solution from them:
Firebase cloud function promises
Am I using ForEach correctly?
My index.js code that sends one message is as follows:
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
const twilio = require('twilio')
const accountSid = functions.config().twilio.sid;
const authToken = functions.config().twilio.token;
const client = new twilio(accountSid, authToken);
const twilioNumber = functions.config().twilio.number;
// Start cloud function
exports.sendSecrets = functions.database
.ref('/{uid}/send')
.onUpdate((change,context) => {
const uid = context.params.uid;
return admin.database().ref(uid+'/messagesToSend').once('value').then(snapshot => {
snapshot.forEach(function(childSnapshot) {
var key = childSnapshot.key;
var messageData = childSnapshot.val();
**if (messageData.sanitized) return true;**
var message = messageData.messageText;
var phoneNumber = messageData.recipientNumber;
const textMessage = {
body: `From My App - ${message}`,
from: twilioNumber, // From Twilio number
to: phoneNumber // Text to this number
}
return client.messages.create(textMessage)
})
**return snapshot.ref.toString();**
});
});
Please note that the lines marked with ** at either end indicate that I know I need to return something based on error messages I received indicating that 'Each then() should return a value or throw'.
I make the assumption that you are using the twilio-node library that use promises: https://www.npmjs.com/package/twilio.
Since you want to send several messages in parallel, you have to use Promise.all(), as follows:
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
const twilio = require('twilio')
const accountSid = functions.config().twilio.sid;
const authToken = functions.config().twilio.token;
const client = new twilio(accountSid, authToken);
const twilioNumber = functions.config().twilio.number;
// Start cloud function
exports.sendSecrets = functions.database
.ref('/{uid}/send')
.onUpdate((change,context) => {
const uid = context.params.uid;
return admin.database().ref(uid+'/messagesToSend').once('value')
.then(snapshot => {
const promises = [];
snapshot.forEach(function(childSnapshot) {
var key = childSnapshot.key;
var messageData = childSnapshot.val();
//**if (messageData.sanitized) return true;**
var message = messageData.messageText;
var phoneNumber = messageData.recipientNumber;
const textMessage = {
body: `From My App - ${message}`,
from: twilioNumber, // From Twilio number
to: phoneNumber // Text to this number
}
promises.push(client.messages.create(textMessage));
})
return Promise.all(promises);
})
// Edits made below to parentheses/brackets
.then(results => {
//Do whatever you want !!
// e.g. print the results which will be an array of messages
// (see https://www.twilio.com/docs/libraries/node#testing-your-installation)
})
});
You can also simply return Promise.all() as follows:
....
return Promise.all(promises);
})
});

Categories

Resources