How to push notifications in firebase node.js cloud functions? - node.js

I want to create a function with node.js but I've got stuck at a point.
Explanation of what I want to do:
First, the function will trigger when a new document added to the path profile/{profileID}/posts/{newDocument}
the function will send a notification to all the following users. the problem comes here.
I've another collection in the profile collection which is followers and contains documents of the field followerID.
I want to take this followerID and use it as a document id to access the tokenID field with I've added to the profile document.
like this:
..(profile/followerID).get(); and then access the field value of tokenID field.
My current Code:- Index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.fcmTester = functions.firestore.document('profile/{profileID}/posts/{postID}').onCreate((snapshot, context) => {
const notificationMessageData = snapshot.data();
var x = firestore.doc('profile/{profileID}/followers/');
var follower;
x.get().then(snapshot => {
follower = snapshot.followerID;
});
return admin.firestore().collection('profile').get()
.then(snapshot => {
var tokens = [];
if (snapshot.empty) {
console.log('No Devices');
throw new Error('No Devices');
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().tokenID);
}
var payload = {
"notification": {
"title": notificationMessageData.title,
"body": notificationMessageData.title,
"sound": "default"
},
"data": {
"sendername": notificationMessageData.title,
"message": notificationMessageData.title
}
}
return admin.messaging().sendToDevice(tokens, payload)
}
})
.catch((err) => {
console.log(err);
return null;
})
});
my firestore database explanation.
profile | profileDocuments | posts & followers | followers collection documents & posts collection documents
I have a parent collection called profile and it contains documents as any collection these documents contain a field called tokenID and that I want to access, but I will not do this for all users only for followers (the users who follwed that profile) so I've created a new collection called followers and it contains all the followers IDs, I want to take every followerID and for each id push tokenID to tokens list.

If I understand correctly your question, you should do as follows. See the explanations below.
exports.fcmTester = functions.firestore.document('profile/{profileID}/posts/{postID}').onCreate((snapshot, context) => {
const notificationMessageData = snapshot.data();
const profileID = context.params.profileID;
// var x = firestore.doc('profile/{profileID}/followers/'); //This does not point to a document since your path is composed of 3 elements
var followersCollecRef = admin.firestore().collection('profile/' + profileID + '/followers/');
//You could also use Template literals `profile/${profileID}/followers/`
return followersCollecRef.get()
.then(querySnapshot => {
var tokens = [];
querySnapshot.forEach(doc => {
// doc.data() is never undefined for query doc snapshots
tokens.push(doc.data().tokenID);
});
var payload = {
"notification": {
"title": notificationMessageData.title,
"body": notificationMessageData.title,
"sound": "default"
},
"data": {
"sendername": notificationMessageData.title,
"message": notificationMessageData.title
}
}
return admin.messaging().sendToDevice(tokens, payload)
});
First by doing var x = firestore.doc('profile/{profileID}/followers/'); you don't declare a DocumentReference because your path is composed of 3 elements (i.e. Collection/Doc/Collection). Note also that,in a Cloud Function, you need to use the Admin SDK in order to read other Firestore documents/collections: So you need to do admin.firestore() (var x = firestore.doc(...) will not work).
Secondly, you cannot get the value of profileID just by doing {profileID}: you need to use the context object, as follows const profileID = context.params.profileID;.
So, applying the above, we declare a CollectionReference followersCollecRef and we call the get() method. Then we loop over all the docs of this Collection with querySnapshot.forEach() to populate the tokens array.
The remaining part is easy and in line with your code.
Finally, note that since v1.0 you should initialize your Cloud Functions simple with admin.initializeApp();, see https://firebase.google.com/docs/functions/beta-v1-diff#new_initialization_syntax_for_firebase-admin
Update following your comments
The following Cloud Function code will lookup the Profile document of each follower and use the value of the tokenID field from this document.
(Note that you could also store the tokenID directly in the Follower document. You would duplicate data but this is quite common in the NoSQL world.)
exports.fcmTester = functions.firestore.document('profile/{profileID}/posts/{postID}').onCreate((snapshot, context) => {
const notificationMessageData = snapshot.data();
const profileID = context.params.profileID;
// var x = firestore.doc('profile/{profileID}/followers/'); //This does not point to a document but to a collectrion since your path is composed of 3 elements
const followersCollecRef = admin.firestore().collection('profile/' + profileID + '/followers/');
//You could also use Template literals `profile/${profileID}/followers/`
return followersCollecRef.get()
.then(querySnapshot => {
//For each Follower document we need to query it's corresponding Profile document. We will use Promise.all()
const promises = [];
querySnapshot.forEach(doc => {
const followerDocID = doc.id;
promises.push(admin.firestore().doc(`profile/${followerDocID}`).get()); //We use the id property of the DocumentSnapshot to build a DocumentReference and we call get() on it.
});
return Promise.all(promises);
})
.then(results => {
//results is an array of DocumentSnapshots
//We will iterate over this array to get the values of tokenID
const tokens = [];
results.forEach(doc => {
if (doc.exists) {
tokens.push(doc.data().tokenID);
} else {
//It's up to you to decide what you want to to do in case a Follower doc doesn't have a corresponding Profile doc
//Ignore it or throw an error
}
});
const payload = {
"notification": {
"title": notificationMessageData.title,
"body": notificationMessageData.title,
"sound": "default"
},
"data": {
"sendername": notificationMessageData.title,
"message": notificationMessageData.title
}
}
return admin.messaging().sendToDevice(tokens, payload)
})
.catch((err) => {
console.log(err);
return null;
});
});

Related

.post call not returning expected response

I am building out the backend for a flash card app for which you can find the repo here. There is a table called categories. There is an endpoint for adding a category.
router.post("/", protect, createCategory);
The endpoint runs a createCategory function which has logic separated out into a categoryController.js file.
const Categories = require("../models/categoryModel");
const createCategory = async (req, res) => {
const { title } = req.body;
const userId = req.user.id;
if (!title) {
res.status(404).json({ errMsg: "Please provide a title" });
} else {
const category = await Categories.createCategory({ title, userId });
console.log("category: ", category);
res.status(201).json(category);
}
};
The createCategory controller function in turn calls a createCategory function from the categoryModel.js file. The function from the model runs the database operations. Specifically, it inserts a new category into the database and references another function--getCategoryById--to return the newly created category.
const getCategoryById = (id) => {
return db("categories").where({ id }).first();
};
const createCategory = (category) => {
return db("categories")
.insert(category, "id")
.then((ids) => {
const [id] = ids;
return getCategoryById(id);
});
};
The problem is when I make the .post to create a new category, nothing is returned in the response. The database gets updated just fine but nothing is returned. You can see that I put a console.log in the controller and that is coming back undefined. I am not sure why.

Get all documents in collection using Cloud Firestore

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))

Firebase Authentication causes cloud functions to return empty

I have a firebase function that's supposed to return Items that are sold by a seller. I want to get the seller's profile picture via firebase authentication. But whenever I AWAIT the function
edit: worth noting that mAuth is firebase authentication*
await mAuth.geUser(sellerData.UID);
the application returns me an empty json or []
Here is the full code for the function, the error occurs on line 11 or somewhere around there.
export const getHottestItems = functions.region("asia-east2").https.onRequest(async (data, response) => {
try {
var arrayItem = new Array<Item>();
let itemSeller: Seller;
const sellerSnapshot = await db.collection("users").get();
// this is the list of promises/awaitables for all items
const promises = new Array<Promise<FirebaseFirestore.QuerySnapshot<FirebaseFirestore.DocumentData>>>();
sellerSnapshot.forEach(async (sellerDoc) => {
const sellerData = sellerDoc.data();
// THIS PART CAUSES THE API TO RETURN []
const sellerAuth = await mAuth.getUser(sellerData.UID);
// check for non null / empty strings
if (sellerData.Name as string && sellerData.UID as string) {
// this is all the seller information we need
itemSeller = new Seller(sellerData.Name, sellerData.UID, sellerAuth.photoURL); // placeholder profile picture
const refItem = sellerDoc.ref.collection("Items");
// push all the promises to a list so we can run all our queries in parallel
promises.push(refItem.get());
}
});
// wait for all promises to finish and get a list of snapshots
const itemSnapshots = await Promise.all(promises);
itemSnapshots.forEach((ItemSnapshot) => {
ItemSnapshot.forEach((ItemDoc) => {
// get the data
const itemData = ItemDoc.data();
// if title is not null, the rest of the fields are unlikely to be.
if (itemData.Title as string) {
// the rest of the logic to convert from database to model is in the constructor
arrayItem.push(new Item(ItemDoc.id, itemData.Title, itemSeller, itemData.Likes, itemData.ListedTime, itemData.Rating, itemData.Description, itemData.TransactionInformation, itemData.ProcurementInformation, itemData.Category, itemData.Stock, itemData.Image1, itemData.Image2, itemData.Image3, itemData.Image4, itemData.AdvertisementPoints, itemData.isDiscounted, itemData.isRestocked));
}
});
});
// sort by performance level
arrayItem = arrayItem.sort(x => x.Performance);
if (data.body.userID) {
arrayItem = await markLikedItems(data.body.userID, arrayItem);
}
//send the responseafter all the final modifications
response.send(arrayItem);
} catch (err) {
// log the error
console.log(err);
response.status(500).send(err);
}
});

NodeJS Async/Await doesn't await (returns Promise pending)

I'm writing an API that gets past transactions on the Stellar network for a user, looks up the corresponding users in my database, and put's it all together into a JSON (to respond to the request - later).
Problem: looking up the username corresponding to the accountID ("from" field) is an async method with mongoose and only returns data after the JSON has been assembled.
I've tried to use async/await, promises, .thens but nothing seems to work.
server.payments()
.forAccount(accountId)
.cursor('now')
.order('desc')
.limit(2)
.call()
.then(function (page) {
var history = []
for(var i = 0; i<page.records.length; i++){
var obj = page.records[i]
//Get the username corresponding to the key from my database
//FIXME
var username
findUser(obj["from"]).then(result => {
username = result
})
var payment = {
"currency":obj["asset_code"],
"from": obj["from"],
"username":username,
"amount": obj["amount"],
"timestamp":obj["created_at"]
}
history.push(payment)
}
console.log(history)
//console.log(JSON.stringify(history))
})
.catch((err) => {
console.log(err)
})
async function findUser(senderKey){
var user = await mongoose.User.findOne({publicKey: senderKey})
console.log(user.username)
return user.username
}
Expected result: findUser returns value, payment variable uses it and gets logged together.
What happens: findUser starts looking for data, payment variable gets put together (username as undefined) and logged, findUser returns data.
Here's the log: (spiderman is the actual username from my database)
[ { currency: 'MEUR',
from: 'GACRQARPR2OMWRG6IH7HM5DYTA3FMM6UKA7NKS4BIJIADRIKFRPAIE7G',
username: undefined,
amount: '3.0000000',
timestamp: '2019-05-07T13:37:04Z' },
{ currency: 'MEUR',
from: 'GACRQARPR2OMWRG6IH7HM5DYTA3FMM6UKA7NKS4BIJIADRIKFRPAIE7G',
username: undefined,
amount: '2.0000000',
timestamp: '2019-05-07T13:34:21Z' } ]
spiderman
spiderman
Highlight recommend, you can make it easy with async/await syntax for your case.
You will wait until the server responses the page, then each of obj in page.records (.map or .forEach... will make you some mess), you wait for findUser function returns the username, then do your business.
try {
const page = await server.payments()
.forAccount(accountId)
.cursor('now')
.order('desc')
.limit(2)
.call()
const history = [];
for (const obj of page.records) {
const username = await findUser(obj["from"]);
const payment = {
"currency": obj["asset_code"],
"from": obj["from"],
"username": username,
"amount": obj["amount"],
"timestamp": obj["created_at"]
}
history.push(payment)
}
console.log(history)
} catch (e) {
console.log(e)
}
Remind: await expression only allowed within an async function.
You can use the new "for of" loop along with async await :-
server.payments()
.forAccount(accountId)
.cursor('now')
.order('desc')
.limit(2)
.call()
.then(async function (page) {
var history = []
for(const obj of page.records) {
const username = await findUser(obj["from"]);
var payment = {
"currency":obj["asset_code"],
"from": obj["from"],
"username":username,
"amount": obj["amount"],
"timestamp":obj["created_at"]
}
history.push(payment)
}
console.log(history)
//console.log(JSON.stringify(history))
})
.catch((err) => {
console.log(err)
})
You need a conditional that checks whether the function you are awaiting has completed
async function findUser(senderKey){
var user = await mongoose.User.findOne({publicKey: senderKey})
if (user){
console.log(user.username)
return user.username
}
}

Fetch multiple documents in a stored procedure (Azure DocumentDB)

I have two document types, Listing and Products. A Listing object contains a list of Products for certain countries, like this:
Listing:
{
"Name": "Default",
"Countries": {
"_default": [
"4QlxAPFcCAAPAAAAAAAAAA==",
"4QlxAPFcCAAHAAAAAAAAAA=="
],
"US": [
"4QlxAPFcCAAIAAAAAAAAAA==",
"4QlxAPFcCAAHAAAAAAAAAA=="
]
},
"Type": "Listing",
"id": "dfed1839-07c5-482b-81c5-669b1dbcd0b6",
"_rid": "4QlxAPFcCAAEAAAAAAAAAA=="
}
Product:
{
"Name": "Widget",
"Price": 3.45,
"Type": "Product",
"_rid": "4QlxAPFcCAAHAAAAAAAAAA=="
}
My goal was to create a stored procedure in the Azure DocumentDB collection taking two parameters, ridand country, which would essentially fetch the Listing document, and the documents for that country, in the most efficient manner possible. My presumption is that loading a Document by its resource Id using getContext().getCollection().readDocument(...) would be the fastest way, thus attempting to create a stored procedure for this.
My attempts have been to nest the consecutive calls (callback hell?), using generator/iterators with yield and then with a pure Promise approach. All of the attempts have given the same result:
It will fetch the first document, but will end quite abruptly after the document has been received.
For reference, here's my latest attempt:
function test(rid, country) {
var collection = getContext().getCollection();
var collectionSelfLink = collection.getSelfLink();
var docsLink = collectionSelfLink + "docs/";
var body = getContext().getResponse().setBody;
function getDocument(rid) {
return new Promise(function(resolve, reject) {
var accepted = collection.readDocument(docsLink + rid, (err, doc, opts) => {
resolve(doc);
});
if (!accepted)
reject("Not accepted");
});
}
getDocument(rid)
.then(doc => {
body("0. First step"); // set test body
// Countries is a Dictionary<string, string[]> with resource ids
return doc.Countries[country] || doc.Countries["_default"];
})
// This is how far it gets, resulting in response "1. Documents to fetch: 2"
.then(a => body("1. Documents to fetch: " + a.length))
.then(a => a.map(function(productId) { return getDoument(productId); }))
.then(a => body("2. It should come this far, right?"))
.then(a => Promise.all(a))
.then(a => body(a))
.catch(function(e) { throw new Error(JSON.stringify(e)); });
}
It turns out that nesting the calls do in fact work, if you alter the response body frequently(?)
The following procedure worked as expected:
function test(rid, country) {
var collection = getContext().getCollection();
var collectionSelfLink = collection.getSelfLink();
var docsLink = collectionSelfLink + "docs/";
var body = getContext().getResponse().setBody;
var accepted = collection.readDocument(docsLink + rid, (err, doc, opts) => {
if (err) throw new Error(err.message);
// Countries is a Dictionary<string, string[]> with resource ids
var offerIds = doc.Countries[country] || doc.Countries["_default"];
var result = [];
for (var docId of offerIds) {
var subAccepted =
collection.readDocument(docsLink + docId, (err, doc, opts) => {
if (err) throw new Error(err.message);
result.push(doc);
});
if (!subAccepted)
throw new Error("A subsequent request was not accepted");
body(result); // <-- Note, setting body in each iteration.
}
});
if (!accepted)
throw new Error("The request was not accepted");
}

Resources