Can't access Firestore docs data after getting the doc object - google-cloud-firestore

I'm trying to fetch a single field value from a doc in a collection (stored in Firestore).
The following function is called (by the triggered function) to perform this query and return the result.
Firestore data structure:
After I fetch the query result into helper_token object - I cannot access the DATA (fields) in it.
I tried many things, including:
helper_token[0].device_token;
helper_token.data().device_token;
JSON.stringify(helper_token);
Nothing works for me.
The log always shows results like these:
helper_token = {}
helper_token = undefined
What am I missing? how can I get the device_token based on user?
const admin = require('firebase-admin'); //required to access the FB RT DB
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
function getHelperToken(helperId) {
//Get token from Firestore
const tokensRef = db.collection('tokens');
const helper_token = tokensRef.where('user', '==', 'TM1EOV4lYlgEIly0cnGHVmCnybT2').get();
if (helper_token.empty) {
functions.logger.log('helper_token EMPTY');
}
functions.logger.log('helper_token=' + JSON.stringify(helper_token));
return helper_token.device_token;
};
For completeness, adding here the full calling function to the above function:
//DB triggered function - upon writing a child in the HElpersInvitations reference
exports.sendHelperInvitation = functions.database.ref('/HelpersInvitations/{helper_invitation_id}')
.onCreate((snapshot, context) => {
const helperId = snapshot.val().helperId;
const title = snapshot.val().title;
const body = snapshot.val().body;
//Get the helper token by Id
functions.logger.log('HelperID=' + helperId);
functions.logger.log('getHelperToken=' + getHelperToken(helperId));
const helper_token2 = getHelperToken(helperId);
//Notification payload
const payload = {
notification: {
title: title,
body: body,
icon: 'default',
click_action: 'com.skillblaster.app.helperinvitationnotification'
}
}
// //Send the notification
functions.logger.log('helper_token [BEFORE sendToDevice]=' + helper_token2);
return admin.messaging().sendToDevice(helper_token2, payload);
});

You need to consider that the get() call is asynchornous and also that you get a list of documents and not a single doc. Can you try it with this code:
const admin = require("firebase-admin"); //required to access the FB RT DB
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
async function getHelperToken(helperId) {
//Get token from Firestore
const tokensRef = db.collection("tokens");
const helperTokens = await tokensRef
.where("user", "==", "TM1EOV4lYlgEIly0cnGHVmCnybT2")
.get();
let helper_token = "";
helperTokens.forEach((token) => {
helper_token = token.data();
});
functions.logger.log("helper_token=" + JSON.stringify(helper_token));
return helper_token.device_token;
}

As the get() call in Firestore is asynchronous you need to use an asynchronous function. You can go through this article to know more about why Firebase APIs are asynchronous. Next when we query with the where clause in Firestore we get a list of documents even if there is only one document in the list. So we have to run a for loop to get the document inside the list of documents. Now as you are returning the value from an asynchronous function the return value will be a promise in pending state. So to get the value from the promise we need to use the then() block while calling the function.
Also I think the parameter helperId you are using in the function definition is not used anywhere in the function. Though it will not make a difference I would suggest you remove it if it is not required in the function.
So consider using the following code -
const admin = require(‘firebase-admin’);
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
async function getHelperToken() {
//Get token from Firestore
const tokensRef = db.collection(‘tokens’);
const helper_token = await tokensRef.where(‘user’, ‘==’, ‘TM1EOV4lYlgEIly0cnGHVmCnybT2’).get();
let helper_token_needed;
helper_token.forEach((token) => {
helper_token_needed = token.data();
});
console.log(helper_token_needed.device_token);
return helper_token_needed.device_token;
}
//when calling to the function use then() block to get the value as a promise is returned from asynchronous function
getHelperToken().then((value)=>{console.log(value)});

Related

Flutter Firebase Google Cloud functions error with .onCreate trigger to send notification to device with FCM token Type Error

I've been stuck a while now and would appreciate any help. I've never worked with Cloud Functions before and there may be a fairly easy solution here. I save the FCM token and other proper variables in my documents. The way my firestore database is organized is the following. Trips/{tripId}/proposedRides/{proposedRideId}/. The proposedRides subCollection creates a new document when a potential passenger requests to join a trip. This is where I want a notification sent to the driver via cloud functions and FCM.
I thought I could make a simple function like this. It is authenticated and setup properly to my knowledge. I added the .json credentials in an env variable and all that jazz. Here's what I've been trying:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendProposedRideNotification =
functions.region('southamerica-east1').firestore.document('proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
// Get the tripId and passengerName associated with the proposedRide
const tripData = snapshot.data();
if (!tripData.proposedByName || !tripData.proposedToDriverFCMToken) {
console.error('Required fields are missing');
return;
}
const passengerName = tripData.proposedByName;
const proposedToDriverFCMToken = tripData.proposedToDriverFCMToken;
// Send the notification to the driver's device
const payload = {
notification: {
title: 'New Proposed Ride',
body: `A new proposed ride has
been created from ${passengerName}!`
}
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToDevice(proposedToDriverFCMToken, payload, options);
});
I'll then check out firebase functions:logs and get the error
Snapshot has no readTime. Using now()
and
TypeError: Cannot read properties of undefined (reading 'proposedByName')
This is strange because when I test the 'proposedByName' field is created in the document. I've tried changing the beginning of the function to
functions.region('southamerica-east1').firestore.document('trips/{tripId}/proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
but I had the same errors.

What is the proper way to run fetch calls which use reactive components from a store?

I am getting two reactive variables I need from a store to use for my fetch calls. I need these fetch calls to rerun when the data in these store values change. I am able to make this work however when I reload the page it causes my app to crash because there are no values that are getting from the store. I am able to make it work if I disable ssr on the +page.js file.
I also believe it is relevant to mention that I am using a relative URL (/api) to make the fetch call because I have a proxy server to bypass CORS
What is the proper way to get this data by rerunning the fetch calls using a reactive component from a store without disabling ssr? Or is this the best/only solution?
+page.svelte
<script>
import { dateStore, shiftStore } from '../../../lib/store';
$: shift = $shiftStore
$: date = $dateStore
/**
* #type {any[]}
*/
export let comments = []
/**
* #type {any[]}
*/
let areas = []
//console.log(date)
async function getComments() {
const response = await fetch(`/api/${date.toISOString().split('T')[0]}/${shift}/1`)
comments = await response.json()
console.log(comments)
}
async function getAreas() {
const response = await fetch(`/api/api/TurnReportArea/1/${date.toISOString().split('T')[0]}/${shift}`)
areas = await response.json()
console.log(areas)
}
// both of these call function if date or shift value changes
$: date && shift && getAreas()
$: date , shift , getComments()
</script>
I tried to use the +page.js file for my fetch calls, however I cannot use the reactive values in the store in the +page.js file. Below the date variable is set as a 'Writble(Date)' When I try to add the $ in front of the value let dare = $dateStore, I get the error 'Cannot find name '$dateSrote'' If i put the $ in the fetch call I get the error 'Cannot find $date'. Even if I were able to make this work, I do not understand how my page would know to rerender if these fetch calls were ran so I do not think this is the solution. As I mentioned, the only solution I have found is to disable ssr on the +page.js, which I do not think is the best way to fix this issue.
import { dateStore, shiftStore } from "../../../lib/store"
export const load = async ({ }) => {
let shift = shiftStore
let date = dateStore
const getComments = async() => {
const commentRes = await fetch(`/api/${date.toISOString().split('T')[0]}/${shift}/1`)
const comments = await commentRes.json()
console.log(comments)
}
const getAreas = async () => {
const areasRes = await fetch(`/api/api/TurnReportArea/1/${date.toISOString().split('T')[0]}/${shift}`)
const areas = await areasRes.json()
console.log(areas)
}
return {
comments: getComments(),
areas: getAreas()
}
}

Dialogflow to Firestore using Inline Fulfillment - Store all user data in one document

How do we store all user input data in one document per one chat session?
I tried this code:
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
process.env.DEBUG = 'dialogflow:debug';
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
function getAge(agent) {
let age = agent.parameters.age;
db.collection("users").add({age: age});
}
function getLocation(agent) {
let location = agent.parameters.location;
db.collection("users").add({location: location});
}
function getCustomerExperience(agent) {
let customerExperience = agent.query;
db.collection("users").add({customerExperience: customerExperience});
}
let intentMap = new Map();
intentMap.set('age', age);
intentMap.set('location', getLocation);
intentMap.set('customer-experience', getCustomerExperience);
agent.handleRequest(intentMap);
});
but the data were stored in different document IDs:
What I'm trying to achieve is something like this:
If I'm not being clear, please let me know. I'm new to Dialogflow, Firebase, as well as the JS language. Cheers!
You're on the right track! The fundamental problem with your original code is that collection.add() will create a new document. But you want it to create a new document sometimes, and save it in a previous document other times.
This means that, during the entire Dialogflow session, you'll need some way to know what the document name is or should be. There are a few possible ways to do this.
Use a document based on the session
Dialogflow provides a session identifier that you can get as part of the agent.session property using the dialogflow-fulfillment library, or in the session property if you're parsing the JSON request body directly.
However, this string includes forward slash / characters, which should be avoided in document names. Fortunately, the format of this string is documented to be one of the two formats:
projects/Project ID/agent/sessions/Session ID
projects/Project ID/agent/environments/Environment ID/users/User ID/sessions/Session ID
In each case, the Session ID is the last portion of this path, so you can probably use code something like this to get the ID for the session, use it as your document name, and then save an attribute (for example, age) for it:
function documentRef( agent ){
const elements = agent.session.split('/');
const lastElement = elements[elements.length - 1];
return db.collection('users').doc(lastElement);
}
async function getCourier(agent) {
const ref = documentRef( agent );
const age = agent.parameters.age;
return await ref.update({age: age});
}
Note that I have also made getCourier() an async function, because the function calls that change the database (such as ref.update()) are async functions and Dialogflow requires you to either make it an async function or explicitly return a Promise. If you wish to return a Promise instead, this would be something more like this:
function getCourier(agent) {
const ref = documentRef( agent );
const age = agent.parameters.age;
return ref.update({age: age});
}
Use the document name generated by Firestore
With this method, you'll store a document name as a Context parameter. When you go to save a value, you'll check if this document name is set. If it is, you'll do an update() using this document name. If not, you'll do an add(), get the document name, and save it in the Context parameter.
It might look something like this (untested), again for the age:
async function getCourier( agent ){
const ref = db.collection('users');
const age = agent.parameters.age;
const docContext = agent.context.get('doc');
if( !docContext ){
// We don't previously have a document, so create it
const res = await ref.add({age:age});
// And set a long-lasting context with a "name" parameter
// containing the document id
agent.context.set( 'doc', 99, {
'name': ref.id
} );
} else {
// There is a context with the document name already set
// so get the name
const docName = docContext.parameters['name'];
const docRef = ref.doc(docName);
// And save the data at this location
await docRef.update({age: age});
}
}
Again, this uses an async function. If you'd rather use a Promise, it might be something more like this:
function getCourier( agent ){
const ref = db.collection('users');
const age = agent.parameters.age;
const docContext = agent.context.get('doc');
if( !docContext ){
// We don't previously have a document, so create it
return ref.add({age:age})
.then( ref => {
// And set a long-lasting context with a "name" parameter
// containing the document id
agent.context.set( 'doc', 99, {
'name': ref.id
} );
});
} else {
// There is a context with the document name already set
// so get the name
const docName = docContext.parameters['name'];
const docRef = ref.doc(docName);
// And save the data at this location
return docRef.update({age: age});
}
}
Use a document name you've generated and saved in the context
You don't need to use the session id from the first alternative. If you have some ID or name that makes sense on your own (a username or a timestamp, for example, or some combination), then you can save this in a Context parameter and use this each time as the document name. This is a combination of the first and second approaches above (but probably simpler than the second one, since you don't need to get the document name from creating the document the fist time).

Flutter Cloud Messaging: how to send notification from the app (not from firebase console)

Is it possible to send the notification from within the app instead of a cloud function on firebase?
The reason is, I want to do something similar to: FCM Push Notifications for Flutter, where they have this function that will be deployed to firebase:
export const sendToTopic = functions.firestore
.document('puppies/{puppyId}')
.onCreate(async snapshot => {
const puppy = snapshot.data();
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'New Puppy!',
body: `${puppy.name} is ready for adoption`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK' // required only for onResume or onLaunch callbacks
}
};
return fcm.sendToTopic('puppies', payload);
});
this method works as intended on firebase cloud functions, however I need the path
.document('puppies/{puppyId}')
to be dynamic depending on which chatroom a user is in, so he would get a notification everytime i new message is send, so the 'chatroom22' would be a variable:
.document('chatroom22/{roomId}')
So is it possible to do this in the app-code, or can this be done in the deployed function?
In response to Doug Stevensons answer
Okay, that makes sence, and works. However, now everybody get the notifications. I want only the people in a given chatroom to receive the notification. I've tried something like this, where the users device token is saved for a given chat-room, then I want to notiffy all those tokens:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('messages/{messageId}/room/{roomId}/message/{messageId2}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = snapshot.data();
const deviceIdTokens = await admin
.firestore()
.collection('messages/{messageId}/room/{roomId}/tokens/{tokenId}')
.get();
var tokens = [];
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().device_token);
}
var payload = {
notification: {
title: `${newData.sender}`,
body: `${newData.message}`,
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData.message,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
But it doesnt seem to work with wildcards.. how can I get the specific destination for each chatroom?
You can use a wildcard for the collection in the path:
functions.firestore.document('{collectionId}/{documentId}')
But this will trigger for all documents in all top-level collecitons, which is probably not what you want.
In fact, using variable names for top-level collections is actually not the preferred way to model data in Firestore. Consider instead having a top-level collection for all rooms, then use subcollections to contain their messages. If you do that, then you function becomes more more clearly defined as:
functions.firestore.document('rooms/{roomId}/messages/{messageId}')
Cloud Fuctions only allows wildcards for full path segments like this. There are no other patterns or regular expressions.

How to query firestore with the Dialogflow inline editor to get information

I am using the inline editor within Dialogflow with the aim of making queries to the database I have created within Firestore.
In short, the user requests a list of courses, I'd like the chatbot to then grab that information form the db and display that back to the user.
Below I have tried to create a function that will do this, I want to take the user input, say "Art Courses" and have my db return those results.
So far, I have created a function that is triggered when the intent is matched, like so;
function getCourses(agent){
let courseRequest = agent.parameters.courseRequest;
if (getCourses){
console.log('Here is the list you requested for ${getCourses}' + parameters.courseRequest);
return admin.firestore().collection('Course_Information').doc.where('CoureTypes').get();
}
}
Are there any notable things I need to add to my function to perform what I wish to achieve?
Thank you.
UPDATE
This code deploys fine, but when I communicate with my bot and trigger the CourseEnquiry intent, cloud Functions shows this error:
admin.collection is not a function
Whilst this seems self explanatory I can't make sure of what it means, I thought declaring const admin = require('firebase-admin');enables me to use admin.collection
// See https://github.com/dialogflow/dialogflow-fulfillment-nodejs
// for Dialogflow fulfillment library docs, samples, and to report issues
'use strict';
const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');
const admin = require('firebase-admin');
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
function getDate(agent){
var today = new Date();
}
function welcome(agent) {
agent.add(`Welcome to my agent!`);
}
function test(agent){
agent.add("The test is successful");
}
function getCourses(agent){
// Get the database collection and document
const getCourseDoc = admin.collection('Course_Information').doc('Course_Types');
return getCourseDoc.get()
.then(doc => {
if (!doc.exists) {
agent.add('No data found in the database!');
} else {
agent.add(doc.data().entry);
}
return Promise.resolve('Here is the information you wanted');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
});
}
function getSubmissionDateSep(agent){
agent.add('Your next submission date is for coursework 1 is');
}
let intentMap = new Map();
intentMap.set('Default Welcome Intent', welcome);
intentMap.set('Test_Test', test);
intentMap.set('CourseEnquiry', getCourses);
intentMap.set('Submission_Dates - sept', getSubmissionDateSep);
agent.handleRequest(intentMap);
});
UPDATE #2
Hey guys, still not got anywhere with this, I have tried adding:
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
According to this document but I get this error when deploying:
The deployment of your Cloud Function failed:
Function load error: Code in file index.js can't be loaded.
Is there a syntax error in your code?
Detailed stack trace: Error: Firebase config variables are not available. Please use the latest version of the Firebase CLI to deploy this function.
You don't show how you're responding to the user with your results, but you'll want to make sure you handle that as part of the then() clause in a Promise. Since the get() in the firestore collection returns a Promise, and you are returning it from your function, you need to make sure that the calling function treats it as a Promise, has a then() clause, and sends back the result as part of something inside this clause.