Firebase functions: getting document data inside a function - node.js

im making a type of chat app. Im trying to write a firestore function that listens to a specific document that, when updated, will trigger the function to send a push notification to a specific user. However, im receiving an error. So far, i have:
export const messageListener = functions.firestore
.document("stingrayMessageListener/stingrayMessageListener")
.onWrite(async (change) => {
const beforeData = change?.before?.data();
const afterData = change?.before?.data();
if (beforeData?.receiverId!=afterData?.receiverId) {
const docRef = admin.firestore().doc("users/${afterData?.receiverId}");
const docSnap = await docRef.get();
const payload = {
notification: {
title: "New message!",
body: "${afterData?.senderName} has sent you a message!",
},
data: {
body: "message",
},
};
admin.messaging().sendToDevice(docSnap.data()?.token, payload);
}
});
This function is spitting out the following error:
Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array
Im pretty sure this is implying that docSnap() is returning as null, because token is a field on all user documents. Am I doing something wrong?

In addition to using template literals in your document paths as you answered, both beforeData and afterData are assigned to change?.before?.data(), meaning your function won't send notifications to any device:
const beforeData = change?.before?.data();
const afterData = change?.before?.data();
if (beforeData?.receiverId != afterData?.receiverId) {
//above condition won't be true
...
You'd only need to use the after property which holds the changed document.
const afterData = change?.after?.data();
Let me know if this was helpful.

So, turns out it was a very simple solution. In Typescript, you can dont use "" to use a variable in a string. You use ``.

Related

Unable to use 'array-contains' where clause in cloud function

I am working on a job bidding app.
Each user has a field "User job notifications preferences".
The array field stores the data to which type of job they would like to receive notifications for.
for example:
Person A has the setting to receive a notification when a job of type 'plumming' is created.
Person B has the setting to receive a notification when a job of type 'electrical' is created.
Person C creates a plumming job,
Peron A should receive a notification to let them know a new job of type 'plumming' has been created.
here is the code snip
// when a job is updated from new to open
// send notifications to the users that signed up for that jobtype notification
exports.onJobUpdateFromNewToOpen= functions.firestore
.document('job/{docId}')
.onUpdate(async (change, eventContext) => {
const beforeSnapData = change.before.data();
const afterSnapData = change.after.data();
const jobType = afterSnapData['Job type'];
const afterJobState = afterSnapData["Job state"];
const beforeJobState = beforeSnapData["Job state"];
console.log('job updated');
// only consider jobs switching from new to open
if (beforeJobState=="New" && afterJobState == "Open") {
console.log('job updated from new to open');
console.log('jobType: '+jobType);
console.log('job id: '+change.after.id )
// get users that contain the matching job type
const usersWithJobTypePreferenceList = await admin.firestore().collection("user").where("User job notifications preferences", "array-contains-any", jobType).get();
// get their userIds
const userIdsList = [];
usersWithJobTypePreferenceList.forEach((doc) => {
const userId = doc.data()["User id"];
userIdsList.push(userId);
})
// get their user tokens
const userTokenList = [];
for (var user in userIdsList) {
const userId = userIdsList[user];
const userToken = await (await admin.firestore().collection("user token").doc(userId).get()).data()["token"];
userTokenList.push(userToken);
};
// send message
const messageTitle = "new " + jobType + " has been created";
for (var token in userTokenList) {
var userToken = userTokenList[token];
const payload = {
notification: {
title: messageTitle,
body: messageTitle,
sound: "default",
},
data: {
click_action: "FLUTTER_NOTIFICATION_CLICK",
message: "Sample Push Message",
},
};
return await admin.messaging().sendToDevice(receiverToken, payload);
}
}
});
I think the issue is at the following line because I am getting the error 'Error: 3 INVALID_ARGUMENT: 'ARRAY_CONTAINS_ANY' requires an ArrayValue' (see image)
const usersWithJobTypePreferenceList = await admin.firestore().collection("user").where("User job notifications preferences", "array-contains-any", jobType).get();
below is the full error:
Error: 3 INVALID_ARGUMENT: 'ARRAY_CONTAINS_ANY' requires an ArrayValue.
at Object.callErrorFromStatus (/workspace/node_modules/#grpc/grpc-js/build/src/call.js:31:19)
at Object.onReceiveStatus (/workspace/node_modules/#grpc/grpc-js/build/src/client.js:352:49)
at Object.onReceiveStatus (/workspace/node_modules/#grpc/grpc-js/build/src/client-interceptors.js:328:181)
at /workspace/node_modules/#grpc/grpc-js/build/src/call-stream.js:188:78
at processTicksAndRejections (node:internal/process/task_queues:78:11)
I interpret the error as the following: there is no value being passed to 'jobType'.but that cant be right because I am printing the value ( see screenshot )
I found the following related questions but I dont think I am having the same issue:
Getting firestore data from a Google Cloud Function with array-contains-any
Firestore: Multiple 'array-contains'
So I am not sure what the issue is here, any ideas?
here is how the data looks in firebase:
I looked at similar questions and I printed the values being passed to the function that was creating the error
I updated the line that was giving me an issue now everything works :) ::
'''
const usersWithJobTypePreferenceList = await admin.firestore().collection("user").where("User job notifications preferences", "array-contains", jobType).get();
'''

Unable to update an item in CosmosDB using the replace method with JavaScript

I am trying to create a basic REST API using Azure functions and the cosmosDB client for JavaScript. I have been successful with all the actions except the UPDATE. The cosmosDB client uses conainter.item(id,category).replace(newObject) I am unable to get the container.item().replace method to work. When I test the function in the portal or using Postman, I get a 500 error and in the portal, I get the error: Result: Failure Exception: Error: invalid input: input is not string Stack: Error: invalid input: input is not string at trimSlashFromLeftAndRight.
Example of my basic document/item properties
{
id:002,
project:"Skip rope",
category:"task",
completed: false
}
const config = require("../sharedCode/config");
const { CosmosClient } = require("#azure/cosmos");
module.exports = async function (context, req) {
const endpoint = config.endpoint;
const key = config.key;
const client = new CosmosClient({ endpoint, key });
const database = client.database(config.databaseId);
const container = database.container(config.containerId);
const theId = req.params.id;
// I am retrieving the document/item that I want to update
const { resource: docToUpdate } = await container.item(theId).read();
// I am pulling the id and category properties from the retrieved document/item
// they are used as part of the replace method
const { id, category } = docToUpdate;
// I am updating the project property of the docToUpdate document/item
docToUpdate.project = "Go fly a kite";
// I am replacing the item referred to with the ID with the updated docToUpdate object
const { resource: updatedItem } = await container
.item(id, category)
.replace(docToUpdate);
const responseMessage = {
status: 200,
message: res.message,
data: updatedItem,
};
context.res = {
// status: 200, /* Defaults to 200 */
body: responseMessage,
};
};
I Googled the heck out of this and been through the Microsoft Azure CosmosDB documents from top-to-bottom, but I can't figure out how to get this to work. I can get the other CRUD operations to work based on the examples Microsoft docs provide, but not this. Any help would be greatly appreciated.
I believe the reason you’re getting this error is because the data type of your “id” field is numeric. The data type of “id” field should be string.
UPDATE
So I tried your code and was able to run it successfully. There was one issue I noticed in your code though:
const { resource: docToUpdate } = await container.item(theId).read();
In the above line of code, you are not specifying the partition key value. If you don't specify the value, then your docToUpdate would come as undefined. In my code I used task as partition key value (I created a container with /category as the partition key).
This is the code I wrote:
const { CosmosClient } = require("#azure/cosmos");
const endpoint = 'https://account.documents.azure.com:443/';
const key = 'accountkey==';
const databaseId = 'database-name';
const containerId = 'container-name';
// const docToUpdate = {
// 'id':'e067cbae-1700-4016-bc56-eb609fa8189f',
// 'project':"Skip rope",
// 'category':"task",
// 'completed': false
// };
async function readAndUpdateDocument() {
const client = new CosmosClient({ endpoint, key });
const database = client.database(databaseId);
const container = database.container(containerId);
const theId = 'e067cbae-1700-4016-bc56-eb609fa8189f';
const { resource: docToUpdate } = await container.item(theId, 'task').read();
console.log(docToUpdate);
console.log('==============================');
const { id, category } = docToUpdate;
docToUpdate.project = "Go fly a kite";
console.log(docToUpdate);
console.log('==============================');
const { resource: updatedItem } = await container
.item(id, category)
.replace(docToUpdate);
console.log(updatedItem);
console.log('==============================');
}
readAndUpdateDocument();
Can you try by using this code?

Firebase Functions extracting Token

Trying to grab my FCM token from my Cloud Firestore using Firebase Function
my function code:
const functions = require("firebase-functions");
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotificationToFCMToken = functions.firestore.document('Posts/{likes}').onWrite(async (event) => {
const title = event.after.get('title');
const content = event.after.get('likes');
let userDoc = await admin.firestore().doc('Users').get();
let fcmToken = userDoc.get('{token}');
var message = {
notification: {
title: title,
body: "you have a new like",
},
token: fcmToken,
}
let response = await admin.messaging().send(message);
console.log(response);
});
My Firestore
Posts:
Users:
if I manually add the token everything works but just send every "like" to one device, my goal is to send a link to only the owner of the post
It's probably more alike this:
let userRef = event.after.get('ref'); // obviously the path is mandatory ...
let userDoc = await admin.firestore().doc(userRef).get(); // then this should match
let token = userDoc.get('token'); // and the token should be accessible
Add logging to see what you get: functions.logger.info('🞬🞬🞬 ' + JSON.stringify(event)); ...viewable at https://console.cloud.google.com/logs/query. When listening for Posts/{likes} you'd likely need an additional query - and when listening for Posts, you'd need to determine changes. Getting access to ref is required to make the subsequent query work.
Martin's answer is correct, but it seems that the ref field is of type Reference, see the slash at the beginning, plus the error you get.
So, if this assumption is correct, you should use the path property, as follows (adapting Martin's code):
let userRef = event.after.get('ref'); // obviously the path is mandatory ...
let userDoc = await admin.firestore().doc(userRef.path).get(); // then this should match
let token = userDoc.get('token'); // and the token should be accessible
In addition, in order to correctly manage the life cycle of your Cloud Function, you should do, at the end:
let response = await admin.messaging().send(message);
console.log(response);
return null;
or simply
return admin.messaging().send(message);

how can i add the sendgrid webhook event Json response in a firebase cloud firestore using node.js

I have no idea how to implement this thing but before that, I have done a part of SendGrid where any document is created then it will send the email to the user. but this part what I am asking I has no idea how to proceed.this is my first part of this implementation wherein any collection if a new record is created then it will send email to the particular email and there is a response called event Object I want to write a cloud function to store the data. and I don't know how to start this function or proceed with this problem.
"use strict";
const functions = require("firebase-functions");
const admin = require("firebase-admin");
var serviceAccount1 = require("./key.json");
const newProject = admin.initializeApp({
credential: admin.credential.cert(serviceAccount1),
databaseURL: "xyz"
});
const sgMail = require("#sendgrid/mail");
const sgMailKey = "key";
sgMail.setApiKey(sgMailKey);
exports.sentMail = functions.firestore
.document("/Offices/{officeId}")
.onCreate((documentSnapshot,event) => {
const documentData = documentSnapshot.data()
const officeID = event.params.officeId;
console.log(JSON.stringify(event))
const db = newProject.firestore();
return db.collection("Offices").doc(officeID).get()
.then(doc => {
const data = doc.data();
const msg = {
to: "amarjeetkumars34#gmail.com",
from: "singhamarjeet045#gmail.com",
text: "hello from this side",
templateId: "d-8ecfa59aa9d2434eb8b7d47d58b4f2cf",
substitutionWrappers: ["{{", "}}"],
substitutions: {
name: data.name
}
};
return sgMail.send(msg);
})
.then(() => console.log("payment mail sent success"))
.catch(err => console.log(err));
});
and the expected output of my question be like a collection name XYZ wherein an object there are three fields like
{email:"xyz#gmail.com",
event:"processed",
timestamp:123555558855},
{email:"xyz#gmail.com",
event:"recieved",
timestamp:123555558855},
{email:"xyz#gmail.com",
event:"open",
timestamp:123555558855}
As you will read in the Sendgrid documentation:
SendGrid's Event Webhook will notify a URL of your choice via HTTP
POST with information about events that occur as SendGrid processes
your email
To implement the HTTP endpoint in your Firebase Project, you will implement an HTTPS Cloud Function that will be called by the Sendgrid webhook through an HTTPS POST request.
Each call from the Sendgrid webhook will concern a specific event and you will be able, in your Cloud Function, to get the value of the event (processed, delivered, etc...).
Now, you need in your Cloud Function to be able to link a specific event with a specific email that was previously sent through your Cloud Function. For that you should use custom arguments.
More precisely, you would add to your msg object (that you pass to the send() method) a unique identifier. A classical value is a Firestore document ID, like event.params.officeId but could be any other unique ID that you generate in you Cloud Function.
Example of implementation
In your Cloud Function that sends the email, pass the officeId in a custom_args object, as shown below:
exports.sentMail = functions.firestore
.document("/Offices/{officeId}")
.onCreate((documentSnapshot,event) => {
const documentData = documentSnapshot.data();
const officeId = event.params.officeId;
const msg = {
to: "amarjeetkumars34#gmail.com",
from: "singhamarjeet045#gmail.com",
text: "hello from this side",
templateId: "d-8ecfa59aa9d2434eb8b7d47d58b4f2cf",
substitutionWrappers: ["{{", "}}"],
substitutions: {
name: documentData.name
},
custom_args: {
"officeId": officeId
}
};
return sgMail.send(msg)
.then(() => {
console.log("payment mail sent success"));
return null;
})
.catch(err => {
console.log(err)
return null;
});
});
Note that you get the data of the newly created document (the one which triggers the Cloud Function) through documentSnapshot.data(): you don't need to query for the same document in your Cloud Function.
Then, create a simple HTTPS Cloud Function, as follows:
exports.sendgridWebhook = functions.https.onRequest((req, res) => {
const body = req.body; //body is an array of JavaScript objects
const promises = [];
body.forEach(elem => {
const event = elem.event;
const eventTimestamp = elem.timestamp;
const officeId = elem.officeId;
const updateObj = {};
updateObj[event] = true;
updateObj[event + 'Timestamp'] = eventTimestamp;
promises.push(admin.firestore().collection('Offices').doc(officeId).update(updateObj));
});
return Promise.all(promises)
.then(() => {
return res.status(200).end();
})
})
Deploy it and grab its URL as shown in the terminal: it should be like https://us-central1-<your-project-id>.cloudfunctions.net/sendgridWebhook.
Note that here I use admin.firestore().collection('Offices').... You may use const db = newProject.firestore(); ... db.collection('Offices')...
Also note that the body of the HTTPS POST request sent by the Sendgrid webhook contains an array of JavaScript objects, therefore we will use Promise.all() to treat these different objects, i.e. write to the Firestore document with officeId the different events.
Then you need to set-up the Webhook in the Sendgrid platform, in the "Mail Settings/Event Notification" section, as explained in the doc and as shown below.

Cloud Functions for Firestore: accessing parent collection data

Many blogs suggest to switch to Cloud Firestore because it's easy and well secured. Coming from Realtime Database and back when using Functions + RD it was easy to navigate through document triggers, like ref.parent
My setup is like this:
Users
{userid}
last_seen: "data"
{forms}
{formid}
However, i have added a document trigger with onCreate, and i want to get the value of last_seen:
exports.updateUser = functions.firestore.document('users/{userId}/forms/{formid}').onCreate((snap, context) => {
const newValue = snap.data();
console.log("test value : " + newValue.test); // works
console.log("form id: " + context.params.formid); // works
console.log("user last seen : " + newValue.last_seen); // doesn't work, can't access the parent collection data
});
I totally get the confusion with the switch to Firestore but it's almost the exact same way in this case.
In realtime, you have the snapshot:
exports.doStuff = functions.database.ref('/users/{userId}/forms/{formId}')
.onCreate((snapshot, context) => {
const ref = snapshot.ref;
const userRef = ref.parent.parent;
userRef.once('value').then(parentSnap => {
const user = parentSnap.val();
const lastSeen = user.last_seen;
});
});
In Firestore:
exports.doStuff = functions.firestore.document.onCreate('/users/{userId}/forms/{formId}')
.onCreate((snapshot, context) => {
const ref = snapshot.ref;
const userRef = ref.parent.parent;
userRef.get().then(parentSnap => {
const user = parentSnap.data();
const lastSeen = user.last_seen;
});
});
Another thing to consider is you are passing the userId in your params so you could just build your own DocumentReference (assuming you're also using firebaseAdmin)
functions.firestore.document.onCreate('/users/{userId}/forms/{formId}')
.onCreate((snapshot, context) => {
const userId = context.params.userId;
const userRef = firebaseAdmin.firestore().collection('users').doc(userId);
userRef.get().then(parentSnap => {
const user = parentSnap.data();
const lastSeen = user.last_seen;
});
});
It also allows you to decouple your logic for functions you may use often, consider it as a "helper" method: (NOTE, I switched to async/await on accident, it's a bit cleaner)
functions.firestore.document.onCreate('/users/{userId}/forms/{formId}')
.onCreate(async (snapshot, context) => {
const userId = context.params.userId;
const lastSeen = await getLastSeen(userId);
});
// == Helper Functions ==-------------------
export async getLastSeen(userId) {
if (!userId) return Promise.reject('no userId');
// User Ref
const userSnap = await firebaseAdmin.firestore().collection('users').doc(userId).get();
return userSnap.data().last_seen;
}
Now you can use getLastSeen() whenever you need it, and if you make a change you only have to adjust that one function. If it's not something you call often then don't worry about it, but I would consider maybe a getUser() helper...
In your code, snap is a DocumentSnapshot type object. As you can see from the linked API documentation, there is a ref property on that object that gets you a DocumentReference object pointing to the document that was added. That object has parent property that gives you a CollectionReference that points to the collection where the document exists, which also has a parent property. So, use these properties to navigate around your database as needed.
Get the reference where the change took place, move 2 levels up and capture data using ref.once() function:
exports.updateUser = functions.firestore.document('users/{userId}/forms/{formid}').onCreate( async (snap, context) => {
// Get the reference where the change took place
const changeRef = snap.after.ref;
// Move to grandad level (2 levels up)
const userIdRef = changeRef.parent.parent;
// Capture data
const snapshot = await userIdRef.once('value');
// Get variable
const lastSeen = snapshot.val().last_seen;
// Do your stuff...
return null;
});

Resources