Get data from firestore document and use in cloud function - node.js

In the user's collection, each user has a document with a customer_id.
I would like to retrieve this customer_id and use it to create a setup intent.
The following code has worked for me in the past. However, all of a sudden it throws the error:
Object is possibly 'undefined'
The error is on the following line under snapshot.data() in this line:
const customerId = snapshot.data().customer_id;
Here is the entire code snippet:
exports.createSetupIntent = functions.https.onCall(async (data, context) => {
const userId = data.userId;
const snapshot = await db
.collection("development")
.doc("development")
.collection("users")
.doc(userId).get();
const customerId = snapshot.data().customer_id;
const setupIntent = await stripe.setupIntents.create({
customer: customerId,
});
const clientSecret = setupIntent.client_secret;
const intentId = setupIntent.id;
return {
clientsecret: clientSecret,
intentId: intentId,
};
});
Any help is appreciated :)

this is because snapshot.data() may return undefined
there are 2 ways to solve this
first is assert as non-null, if you have high confident that the data exist
const customerId = snapshot.data()!.customer_id;
second if check for undefined
const customerId = snapshot.data()?.customer_id;
if(customerId){
// ....
}
I recommend the 2nd method, it is safer

I can see you are using a sub collection order,You need to loop through the snapshot data using the forEach loop.
const customerId = snapshot.data()
customerId.forEach((id)=> {
console.log(id.customer_id)
});
Try this out but.

The document you're trying to load may not exist, in which case calling data() on the snapshot will return null, and thus this line would give an error:
const customerId = snapshot.data().customer_id;
The solution is to check whether the document you loaded exists, and only then force to get the data from it:
if (snapshot.exists()) {
const customerId = snapshot.data()!.customer_id;
...
}

if you want to fetch user data from docId then you can use something like this:
const functions = require("firebase-functions");
var admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var db = admin.firestore();
db.settings({ timestampsInSnapshots: true });
exports.demoFunction = functions.https.onRequest((request, response) => {
var userId = request.body.userId;
db.collection("user").doc(userId).get().then(snapshot => {
if (snapshot) {
var data = snapshot.data();
// use data to get firestore data
var yourWantedData = data.name;
// use it in your functionality
}
});
});

Related

Firebase Function onCreate add to new collection works, but update does not

I have this onCreate Trigger, I am using it to aggregate and add record or update record. First it takes minutes to add the record and then the update never runs just keeps adding, not sure why my query is not bringing back a record to update.
Any suggestion would be great.
exports.updateTotals = functions.runWith({tinmeoutSeconds: 540})
.firestore.document("user/{userID}/CompletedTasks/{messageId}")
.onCreate(async (snap, context) => {
const mycompleted = snap.data();
const myuserid = context.params.userID;
console.log("USER: "+myuserid);
const mygroup = mycompleted.groupRef;
const myuser = mycompleted.userRef;
const newPoints = mycompleted.pointsEarned;
console.log("POINTS: "+newPoints);
const data = {
groupRef: mygroup,
userRef: myuser,
pointsTotal: newPoints,
};
const mytotalsref = db.collection("TaskPointsTotals")
.where("groupRef", "==", mygroup)
.where("userRef", "==", myuser);
const o = {};
await mytotalsref.get().then(async function(thisDoc) {
console.log("NEW POINTS: "+thisDoc.pointsTotal);
const newTotalPoints = thisDoc.pointsTotal + newPoints;
console.log("NEW TOTAL: "+newTotalPoints);
if (thisDoc.exists) {
console.log("MYDOC: "+thisDoc.id);
o.pointsTotal = newTotalPoints;
await mytotalsref.update(o);
} else {
console.log("ADDING DOCUMENT");
await db.collection("TaskPointsTotals").doc().set(data);
}
});
});
You are experiencing this behavior because while querying for updates you are getting more than 1 document and you are using thisDoc.exists on more than one document. If you have used typescript this could have been catched while writing the code.
So for the update query, if you are confident that only unique documents exist with those filters then here’s the updated code that I have recreated using in my environment.
functions/index.ts :
exports.updateTotals = functions.runWith({timeoutSeconds: 540})
.firestore.document("user/{userId}/CompletedTasks/{messageId}")
.onCreate(async (snap, context) => {
const mycompleted = snap.data();
const myuserid = context.params.userID;
console.log("USER: "+myuserid);
const mygroup = mycompleted.groupRef;
const myuser = mycompleted.userRef;
const newPoints = mycompleted.pointsEarned;
console.log("POINTS: "+newPoints);
const data = {
groupRef: mygroup,
userRef: myuser,
pointsTotal: newPoints,
};
const mytotalsref = admin.firestore()
.collection("TaskPointsTotals")
.where("groupRef", "==", mygroup)
.where("userRef", "==", myuser);
await mytotalsref.get().then(async function(thisDoc) {
if (!thisDoc.empty) { // check if the snapshot is empty or not
const doc = thisDoc.docs[0];
if(doc.exists){
const newTotalPoints = doc.data()?.pointsTotal + newPoints;
const id = doc.id;
await db.collection("TaskPointsTotals").doc(id).update({pointsTotal: newTotalPoints});
}
} else {
await db.collection("TaskPointsTotals").doc().set(data);
}
});
});
For more information about QuerySnapshot methods check this docs

API Request passing ID fetched from mongodb

I´m a Java Dev so I need help from NodeJS guys!
Task: create a script that retrieves '_id', 'document', and 'corporateName' from MongoDB, then take the retrieved '_id', and pass it as a parameter to an API request. The last part should be taking 'document', 'corporateName' + 'client_id', 'client_secret' and export it into a single csv file.
It might be a very simple script! Therefore I´ve done this till now:
const {MongoClient} = require('mongodb');
const uri = "mongodb+srv://<privateInfo>/";
const client = new MongoClient(uri);
async function run() {
try {
const database = client.db("merchant-profile");
const ecs = database.collection("merchant_wallet");
const api = `https://<prodAPI>/v1/merchant/wallet/${id}/oauth2`;
const ecOpt = {_id: 1, document: 1, corporateName: 1};
const credOpt = {client_id: 1, client_secret: 1};
const ec = ecs.find({}).project(ecOpt);
let id = ec.forEach(id => cred._id);
const cred = api.find({}).project(credOpt);
await cred.forEach(console.dir);
} finally {
await client.close();
}
}
run().catch(console.dir);
I´m trying to understand how can I take '_id' fetched in 'ec' and pass it as a param to the 'cred' call.
This would already be awesome!
If you could help me out with the CSV issue as well it would be perfect.
So I don´t want just the answer, but understand how to do this.
Thank you all in advance!
This is the way I found to do it:
const { default: axios } = require("axios");
const { MongoClient } = require("mongodb");
const uri = "mongodb+srv://admin:sLKJdsdRp4LrsVtLsnkR#pp-core-prd.fy3aq.mongodb.net/";
const client = new MongoClient(uri);
async function run() {
try {
const database = client.db("merchant-profile");
const ecs = database.collection("merchant_wallet");
const data = [];
await ecs.find({}).forEach(async function teste(response) {
const id = response._id;
const api = `https://api.pedepronto.com.br/v1/merchant/wallet/${id}/oauth2`;
try{
const res = await axios.get(api);
data.push({client_secret: res.data[0].client_secret, client_id: res.data[0].client_id})
}catch(e){
console.log(e);
}
})
} finally {
await client.close();
}
}
run().catch(console.dir);
It iterates over the find method and appends the fetched id to the uri.

firestore cloud function update another document

I want to increment the value of the field "votes" in a document (item_id) in the collection items. I want a cloud function to do this for me every time a new document is added to the collection votes. The new document contains the item_id. does anyone know how I can do this? This is what I have now:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const vote = functions.firestore.document("/Votes/{vote}")
.onCreate((snapshot, context) => {
const item = context.params.item_id;
const itemdoc = admin.firestore().collection("items").doc(item);
itemdoc.get().then((doc) => {
if (doc.exists) {
itemdoc.update({
"votes": admin.firestore.FieldValue.increment(1)})
.catch((err) => {
console.log("Error updating item vote", err);
});
}
});
});
In the firebase console logs that the path must be a non-empty string. Does anyone know what I do wrong? Since the path should not be empty.
The following should do the trick:
export const vote = functions.firestore.document("/Votes/{vote}")
.onCreate((snapshot, context) => {
const item = snapshot.data().item_id;
const itemDocRef = admin.firestore().collection("items").doc(item);
return itemDocRef.update({
"votes": admin.firestore.FieldValue.increment(1)
});
});
You need to use the data() method on snapshot, in order to get the JavaScript representation of the new document. Then you take the item_id property.
Another possibility is to use the get() method, as follows:
const item = snapshot.get("item_id");
I would suggest to rename the itemdoc variable to itemDocRef, since it is a DocumentReference.
Update following your comments:
If you want to read the item Doc after having updated it you should do as follows:
export const vote = functions.firestore.document("/Votes/{vote}")
.onCreate(async (snapshot, context) => {
const item = snapshot.data().item_id;
const itemDocRef = admin.firestore().collection("items").doc(item);
await itemDocRef.update({"votes": admin.firestore.FieldValue.increment(1)});
const itemDocSnapshot = await itemDocRef.get();
//Do whatever you want with the Snapshot
console.log(itemDocSnapshot.get("user_id"));
// For example update another doc
const anotherDocRef = admin.firestore().collection("....").doc("....");
await anotherDocRef.update({"user_id": itemDocSnapshot.get("user_id")});
return null;
});
Note the use of the async and await keywords.
const item = context.params.item_id;
By accessing context.params, you are trying to find a value in wildcard present in .document("/Votes/{vote}") which is undefined for sure. To read a field from document try this:
const {item_id} = snapshot.data()
// Getting item_id using Object destructuring
if (!item_id) {
// item_id is missing in document
return null
}
const itemdoc = admin.firestore().collection("items").doc(item_id);
// Pass item_id in doc ^^^^^^^
You can read more about onCreate in the documentation. The first parameter snapshot is the QueryDocumentSnapshot which contains your doc data and the second parameter context is EventContext.

My onCreate funciton in Functions of firebase is not creating my desired collection in the cloud database

I just typed a code in my index.js file of functions (firebase CLI).According to my code there must be a timeline collection created in cloud database of firebase.Function is healthy and there are no errors it gets deployed and even in the logs everything works fine. But still timeline collection is not created in the cloud databaese when I follow a user in my app.
this is my code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.onCreateFollower = functions.firestore
.document("/followers/{userId}/userFollowers/{followerId}")
.onCreate(async (snapshot, context) => {
console.log("Follower Created", snapshot.id);
const userId = context.params.userId;
const followerId = context.params.followerId;
// 1) Create followed users posts ref
const followedUserPostsRef = admin
.firestore()
.collection("posts")
.doc(userId)
.collection("userPosts");
// 2) Create following user's timeline ref
const timelinePostsRef = admin
.firestore()
.collection("timeline")
.doc(followerId)
.collection("timelinePosts");
// 3) Get followed users posts
const querySnapshot = await followedUserPostsRef.get();
// 4) Add each user post to following user's timeline
querySnapshot.forEach(doc => {
if (doc.exists) {
const postId = doc.id;
const postData = doc.data();
return timelinePostsRef.doc(postId).set(postData);
}
});
});
Since you want to execute a variable number of asynchronous calls in parallel, you should use Promise.all(), in order to wait that all these different asynchronous calls are completed before indicating to the CF platform that it can cleanup the CF. See https://firebase.google.com/docs/functions/terminate-functions for more details.
exports.onCreateFollower = functions.firestore
.document("/followers/{userId}/userFollowers/{followerId}")
.onCreate(async (snapshot, context) => {
const userId = context.params.userId;
const followerId = context.params.followerId;
// ...
// 3) Get followed users posts
const querySnapshot = await followedUserPostsRef.get();
// 4) Add each user post to following user's timeline
const promises = [];
querySnapshot.forEach(doc => {
//query results contain only existing documents, the exists property will always be true and data() will never return 'undefined'.
const postId = doc.id;
const postData = doc.data();
promises.push(timelinePostsRef.doc(postId).set(postData));
});
return Promise.all(promises);
});

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