retrieve Firestore document from onCreate trigger with Cloud Functions - node.js

I need to retrieve information from a Firestore Document when another document is created. When I try to do this I get hit with an error about the function not being async. It has been so long since I used javascript I am basically a novice again and have no idea how to fix this.
ok, so I am using Firebase Cloud Functions and the function in question is a Firestore .onCreate() trigger.
When the function is triggered I set a sender variable (which is the document ID from a different collection that I need to retrieve)
then I try to get the document as per the documentation.
The function ends up like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
when I run this in the emulator I get this error:
const doc = await requestRef.get();//get user data of sender
^^^^^
SyntaxError: await is only valid in async functions and the top level bodies of modules
I have absolutely no idea where to go from here.
Can anyone help me with this?
Thanks

The await keyword is valid only in an async function.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// ^^^^^
})
If you are (or need to) use synchronous function then you would have to use promise chaining.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
return requestRef.get().then((snapshot) => {
if (snapshot.exists) { ... }
})
})
Apart from that, the order of variables/statements looks incorrect. With the current code (as in original question), you may end up getting an error: "Cannot access 'doc' before initialization" Try refactoring it like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// accessing data from newly created doc
const newDocData = snap.data()
// const sender = "" // ??
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
})
Where is the sender coming from? I've just commented it above but if the sender is present in new document then you can access it by: const sender = newDocData.sender

If your using await you have to specify that function is asynchronous. Otherwise it will throw error.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}').onCreate(async (snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
Yet some of your references is unknown to us. Maybe this code is not completed.
The main point is you need to understand when you can access async/await or Promise

All await methods must be inside an async block or be handled in an async manor using .then() promises
in this case, the parent function is on this line .onCreate((snap, context) => {
simply inserting an async at the start of the variables will upgrade the arrow function to an async arrow function
.onCreate(async (snap, context) => {

Related

How to get inner collection in firebase firestore

I'm trying to get the device token of a particular user in firestore which is stored in tokens collection inside either "clients" or "lawyers" collection.
When i remove the second .collection("tokens") from the chain i get the user object back but with the token collection in the chain i just can't seem to get any user (client or lawyer) back, even though the user and it's token exist. what am i doing wrong
exports.onReceiveChatMessage = functions.database
.ref("/messages/{uid}")
.onCreate(async (snapshot, context) => {
const newMessage = snapshot.val();
console.log("NEW_MESSAGE", newMessage);
const senderName = newMessage.sender_name;
const messageContent = newMessage.content;
console.log("SENDER'S_NAME", senderName);
console.log("MESSAGE_BODY", messageContent);
const uid = context.params.uid;
console.log("RECEIVERS_ID", uid);
if (newMessage.sender_id == uid) {
//if sender is receiver, don't send notification
console.log("sender is receiver, dont send notification...");
return;
} else if (newMessage.type === "text") {
console.log(
"LETS LOOK FOR THIS USER, STARTING WITH CLIENTS COLLECTION..."
);
let userDeviceToken;
await firestore
.collection("clients")
.doc(uid)
.collection("tokens")
.get()
.then(async (snapshot) => {
if (!snapshot.exists) {
console.log(
"USER NOT FOUND IN CLIENTS COLLECTION, LETS CHECK LAWYERS..."
);
await firestore
.collection("lawyers")
.doc(uid)
.collection("tokens")
.get()
.then((snapshot) => {
if (!snapshot.exists) {
console.log(
"SORRY!!!, USER NOT FOUND IN LAWYERS COLLECTION EITHER"
);
return;
} else {
snapshot.forEach((doc) => {
console.log("LAWYER_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
} else {
snapshot.forEach((doc) => {
console.log("CLIENT_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
// console.log("CLIENT_DEVICE_TOKEN", userDeviceToken);
} else if (newMessage.type === "video_session") {
}
})
This line
if (!snapshot.exists) {
should be:
if (snapshot.empty) {
because you're calling get() on a CollectionReference (which returns a QuerySnapshot), not on a DocumentReference (which returns a DocumentSnapshot).
If you remove the .collection('tokens') from the chain in your example, it does work because a DocumentSnapshot does have the member exists, but a CollectionReference doesn't.
Take a look at their members here:
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#get
Then:
https://googleapis.dev/nodejs/firestore/latest/QuerySnapshot.html
As a suggestion, I used to confuse snapshots and got that problem because of working with Javascript instead of Typescript. So I got used to calling the result snap when called on a document, and snaps when called on collections. That reminds me of what kind of response I'm working on. Like this:
// single document, returns a DocumentSnapshot
const snap = await db.collection('xyz').doc('123').get();
if (snap.exists) {
snap.data()...
}
// multiple documents, returns a QuerySnapshot
const snaps = await db.collection('xyz').get();
if (!snaps.empty) { // 'if' actually not needed if iterating over docs
snaps.forEach(...);
// or, if you need to await, you can't use the .forEach loop, use a plain for:
for (const snap of snaps.docs) {
await whatever(snap);
}
}

Why does Async firebase fetching is not working? (NODE JS)

Building a NodeJS REST API.
Trying to send load data from FireBase collection, then sending it to the user (as API response).
Looks like the problem is that it's not waits for the firebase fetch to resolve, but send back a response without the collection data. (tried to use ASYNC-AWAIT but its not working)
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {
snapshot.docs.forEach(msg => {
console.log(msg.data().messageContent)
return {
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}
})
})
}
try {
const chatData = await getChatData()
console.log(chatData)
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData
})
} catch (err) {
if (!err.statusCode) {
err.statusCode(500)
}
next(err)
}
}
As you can see, I've used 2 console.logs to realize what the problem, Terminal logs looks like:
[] (from console.logs(chatData))
All messages (from console.log(msg.data().messageContent))
Is there any way to block the code unti the firebase data realy fetched?
If I correctly understand, you want to send back an array of all the documents present in the messages subcollection. The following should do the trick.
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId;
const collectionRef = db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc');
try {
const chatsQuerySnapshot = await collectionRef.get();
const chatData = [];
chatsQuerySnapshot.forEach((msg) => {
console.log(msg.data().messageContent);
chatData.push({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
});
});
console.log(chatData);
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData,
});
} catch (err) {
if (!err.statusCode) {
err.statusCode(500);
}
next(err);
}
};
The asynchronous get() method returns a QuerySnapshot on which you can call forEach() for enumerating all of the documents in the QuerySnapshot.
You can only await a Promise. Currently, getChatData() does not return a Promise, so awaiting it is pointless. You are trying to await a fixed value, so it resolves immediately and jumps to the next line. console.log(chatData) happens. Then, later, your (snapshot) => callback happens, but too late.
const getChatData = () => new Promise(resolve => { // Return a Promise, so it can be awaited
db.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot(resolve) // Equivalent to .onSnapshot((snapshot) => resolve(snapshot))
})
const snapshot = await getChatData();
console.log(snapshot)
// Put your transform logic out of the function that calls the DB. A function should only do one thing if possible : call or transform, not both.
const chatData = snapshot.map(msg => ({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}));
res.status(200).json({
message: 'Chat Has Found',
chatData
})
Right now, getChatData is this (short version):
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {}) // some things inside
}
What that means is that the getChatData function calls some db query, and then returns void (nothing). I bet you'd want to return the db call (hopefully it's a Promise), so that your await does some work for you. Something along the lines of:
const getChatData = async () =>
db
.collection('chats')
// ...
Which is the same as const getChatData = async() => { return db... }
Update: Now that I've reviewed the docs once again, I see that you use onSnapshot, which is meant for updates and can fire multiple times. The first call actually makes a request, but then continues to listen on those updates. Since that seems like a regular request-response, and you want it to happen only once - use .get() docs instead of .onSnapshot(). Otherwise those listeners would stay there and cause troubles. .get() returns a Promise, so the sample fix that I've mentioned above would work perfectly and you don't need to change other pieces of the code.

Retrieve JSON from URL and convert it to Cloud Firestore Collection with Cloud Functions

Here is what I want to achieve : I want to get a JSON on a daily basis from a URL and convert it to a cloud firestore collection in order to be able to use it in my Flutter app. Ideally, the script would only add new data to the collection.
I saw that I can use scheduler from Firebase cloud functions to run tasks daily. That's not the problem for now.
However, I don't know how to use Firebase cloud functions properly to get data from URL and convert it to collection. Maybe that's not the point of cloud functions and I misunderstood something. So first question : Can I run classic nodeJS stuff inside cloud functions? I suppose I can
Next, I initialized a cloud function project locally, connected it to my Google account and started to write code into index.js.
const functions = require("firebase-functions");
const admin = require('firebase-admin');
const fetch = require('node-fetch');
const db = admin.firestore();
const collectionToiletRef = db.collection('mycollection');
let settings = { method: "Get" };
let url = "my-url.com"
fetch(url, settings)
.then(res => res.json())
.then((json) => {
print(json);
// TODO for each json object, add new document
});
Second question : How can I run this code to see if it works ? I saw that emulator can be used but how can I check visually my cloud firestore collection ? On this simple example, I only want to print my json to see if I can get the data correctly. Where would the printing be done ?
Maybe cloud functions is not what I need for this task. Maybe my code is bad. I don't know. Thanks for your help.
EDIT
I tried this but the call never ends. I think it's waiting for a promise that never returns or something like that.
const functions = require("firebase-functions");
const admin = require('firebase-admin');
const fetch = require('node-fetch');
admin.initializeApp();
const db = admin.firestore();
exports.tempoCF = functions
.firestore.document('/tempo/{docId}')
.onCreate(async (snap, context) => {
console.log("onCreate");
let settings = { method: "Get" };
let url = "https://opendata.paris.fr/api/records/1.0/search/?dataset=sanisettesparis&q=&rows=-1"
try {
let response = await fetch(url, settings);
let json = await response.json();
// TODO for each json object, add new document
await Promise.all(json["records"].map(toiletJsonObject => {
return db.collection('toilets').doc(toiletJsonObject["recordid"]).set({}); // Only to create documents, I will deal with the content later
}));
}
catch(error) {
console.log(error);
return null;
}
}
);
This code works and create all the documents I want but never return. However, the async (snap, context) => {} passed to onCreate is a Promise. And this promise ends when Promise.all ends. I'm missing something but I don't know why. I'm struggling a lot with async programming with Dart or JS. Not very clear in my mind.
Can I run classic nodeJS stuff inside cloud functions?
Sure! Since the fetch method returns a Promise you can very well use it in a background triggered or a scheduled Cloud Function.
How can I run this code to see if it works?
Your code will work perfectly in the emulator suite, but you will need to trigger the Cloud Function with one of the Firebase services that can run in the emulator. For example you can trigger the Cloud Function by creating a document in the Firestore emulator console.
The following Cloud Function will do the trick: just create a doc in a dummy tempo collection and the CF will add a new doc in a newDocscollection. It's up to you to adapt the fields values for this doc, I've just used the entire JSON object.
exports.tempoCF = functions
.firestore.document('/tempo/{docId}')
.onCreate((snap, context) => {
let settings = { method: "Get" };
let url = "https://..."
return fetch(url, settings)
.then(res => res.json())
.then((json) => {
console.log(json);
// TODO for each json object, add new document
return admin.firestore().collection('newDocs').add(json);
})
.catch(error => {
console.log(error);
return null;
});
});
You could also deploy your Cloud Function to the Firebase backend, and if you want to schedule it, just change the code as follows (change the trigger):
exports.scheduledFunction = functions.pubsub.schedule('every 5 minutes').onRun((context) => {
let settings = { method: "Get" };
let url = "https://..."
return fetch(url, settings)
.then(res => res.json())
.then((json) => {
console.log(json);
// TODO for each json object, add new document
return admin.firestore().collection('newDocs').add(json);
})
.catch(error => {
console.log(error);
return null;
});
});
Edit following your edit:
The following code does work correctly in the emulator, creating docs in the toilets collection.
exports.tempoCF = functions.firestore
.document('/tempo/{docId}')
.onCreate(async (snap, context) => {
console.log('onCreate');
let settings = { method: 'Get' };
let url =
'https://opendata.paris.fr/api/records/1.0/search/?dataset=sanisettesparis&q=&rows=-1';
try {
let response = await fetch(url, settings);
let json = await response.json();
return Promise.all( // Here we return the promise returned by Promise.all(), so the life cycle of the CF is correctly managed
json['records'].map((toiletJsonObject) => {
admin
.firestore()
.collection('toilets')
.doc(toiletJsonObject['recordid'])
.set({ adresse: toiletJsonObject.fields.adresse });
})
);
} catch (error) {
console.log(error);
return null;
}
});

Synchronously iterate through firestore collection

I have a firebase callable function that does some batch processing on documents in a collection.
The steps are
Copy document to a separate collection, archive it
Run http request to third party service based on data in document
If 2 was successful, delete document
I'm having trouble with forcing the code to run synchronously. I can't figure out the correct await syntax.
async function archiveOrders (myCollection: string) {
//get documents in array for iterating
const currentOrders = [];
console.log('getting current orders');
await db.collection(myCollection).get().then(querySnapshot => {
querySnapshot.forEach(doc => {
currentOrders.push(doc.data());
});
});
console.log(currentOrders);
//copy Orders
currentOrders.forEach (async (doc) => {
if (something about doc data is true ) {
let id = "";
id = doc.id.toString();
await db.collection(myCollection).doc(id).set(doc);
console.log('this was copied: ' + id, doc);
}
});
}
To solve the problem I made a separate function call which returns a promise that I can await for.
I also leveraged the QuerySnapshot which returns an array of all the documents in this QuerySnapshot. See here for usage.
// from inside cloud function
// using firebase node.js admin sdk
const current_orders = await db.collection("currentOrders").get();
for (let index = 0; index < current_orders.docs.length; index++) {
const order = current_orders.docs[index];
await archive(order);
}
async function archive(doc) {
let docData = await doc.data();
if (conditional logic....) {
try {
// await make third party api request
await db.collection("currentOrders").doc(id).delete();
}
catch (err) {
console.log(err)
}
} //end if
} //end archive
Now i'm not familiar with firebase so you will have to tell me if there is something wrong with how i access the data.
You can use await Promise.all() to wait for all promises to resolve before you continue the execution of the function, Promise.all() will fire all requests simultaneously and will not wait for one to finish before firing the next one.
Also although the syntax of async/await looks synchronous, things still happen asynchronously
async function archiveOrders(myCollection: string) {
console.log('getting current orders')
const querySnapshot = await db.collection(myCollection).get()
const currentOrders = querySnapshot.docs.map(doc => doc.data())
console.log(currentOrders)
await Promise.all(currentOrders.map((doc) => {
if (something something) {
return db.collection(myCollection).doc(doc.id.toString()).set(doc)
}
}))
console.log('copied orders')
}

NodeJS Promise Firebase

Got to love nodeJS and asynchronous nature! With that, I'm dumbfounded how to to continue bc I can't keep nesting promises and of course that's a not go so I'm throwing up my hands bc each step requires a completed action with data from the previous step.
This is what I'm trying to accomplish and code is below.
A new college comes into /sessions/college
After getting the value of that key, go find advisors that subscribe to that college.
Get the FCM tokens for the subscribing advisors
Haven't even gotten to this part obviously, but send a FCM notification to subscribers.
Tada!
exports.newSessionNotifer = functions.database.ref('/sessions/college').onCreate((snap, context) => {
const college = snap.val();
var promises = [];
var getAdvisors = admin.database().ref('colleges').child(college).once('value').then((snapshot) => {
const people = snapshot.val();
var advisors = Object.keys(people);
return advisors;
}).then((advisors) => {
return advisors.forEach((token) => {
var advisorToken = admin.database().ref('users').child(token).child('fcmtoken').child('token').once('value');
return console.log(advisorToken);
});
});
return Promise.all(promises).then((values) => {
console.log(promises);
return console.log('Hi');
});
You're on the right track. once() returns a promise, and it is the set of promises from repeated calls to once that must be collected and run with Promise.all().
exports.newSessionNotifer = functions.database.ref('/sessions/college').onCreate((snap, context) => {
const college = snap.val();
return admin.database().ref('colleges').child(college).once('value');
}).then(snapshot => {
const people = snapshot.val();
let advisors = Object.keys(people);
let promises = advisors.map(token => {
return admin.database().ref('users').child(token).child('fcmtoken').child('token').once('value');
});
return Promise.all(promises);
});
EDIT Editing again, this time with the OP's answer in hand. On style, I'm not sure what lint says, but my definition of bad nesting style is when a then() block contains another then() block. Also regarding style, my approach to making this stuff comprehensible is to build (and test) small functions, one per async task.
On structure, the OP's new answer unnecessarily chains a second block after return advisors. Since advisors isn't a promise, we can carry on from there with synchronous code. Also on structure, the OP's solution creates a series of promises -- two for each advisor (get advisor token and push) -- but these are not certain to complete unless Promise.all is applied and returned.
Summing all that, my advice would be as follows...
On create, get the advisors for the college, send each a message.
exports.newSessionNotifer = functions.database.ref('/sessions/{sessionID}/college').onCreate((snap, context) => {
const college = snap.val();
return advisorsForCollege(college).then(advisors => {
let promises = advisors.map(advisor => sendAdvisorMessage(advisor, college));
return Promise.all(promises);
});
});
Advisors for a college are apparently the keys from that college object
function advisorsForCollege(college) {
return admin.database().ref('colleges').child(college).once('value').then(snapshot => Object.keys(snapshot.val()));
}
Sending an advisor message means getting the advisors token and doing a push. Return a two-promise chain that does that...
function sendAdvisorMessage(advisor, college) {
return tokenForAdvisor(advisor).then(token => {
let title = `There's a new session for ${college}!`;
let body = 'Go to the middle tab and swipe right to accept the session before your peers do!'
return sendToDevice(token, title, body);
});
}
Now we just need one to get an advisor's token and one to do a push...
function tokenForAdvisor(advisor) {
return admin.database().ref('users').child(advisor).child('fcmtoken').child('token').once('value');
}
function sendToDevice(token, title, body) {
const payload = { notification: { title: title, body: body } };
return admin.messaging().sendToDevice(token, payload);
};
I think lint should report all of the foregoing as just fine, even with promise nesting warning turned on.
Thanks to danh, here's my final code. Comment/feedback away! I decided to disable the promise nesting option within lint and viola!
exports.newSessionNotifer = functions.database.ref('/sessions/{sessionID}/college').onCreate((snap, context) => {
const college = snap.val();
return admin.database().ref('colleges').child(college).once('value').then((snapshot) => {
const people = snapshot.val();
let advisors = Object.keys(people);
return advisors;
}).then((advisors) => {
return advisors.map(advisor => {
return admin.database().ref('users').child(advisor).child('fcmtoken').child('token').once('value').then((snapshot) => {
const token = snapshot.val();
const payload = {
notification: {
title: `There's a new session for ${college}!`,
body: 'Go to the middle tab and swipe right to accept the session before your peers do!'
}
};
return admin.messaging().sendToDevice(token, payload);
});
});
});
});

Resources