I'm using Firebase scheduled function to periodically check data in Firestore if users' detail are added to SendGrid contact list. After they're successfully added to SendGrid, I want to update the value in firestore addToContact from false to true.
exports.addContact = functions.region(region).pubsub.schedule('every 4 minutes').onRun(async(context) => {
// Query data from firestore
const querySnapshot = await admin.firestore().collection('users')
.where('metadata.addToContact', '==', false)
.get();
// Call SendGrid API to add contact
// If success, change metadata.addToContact to true
if (response) {
const docRef = admin.firestore().collection('users').doc(""+context.params.id);
await docRef.update( {'metadata.sendToSg': true }, { merge: true } );
}
}
I want to access context.params.id but I realise that context that passed in .onRun isn't the same with the context passed in a callable function.
NOTE: If I pass context.params.id to .doc without ""+, I got an error Value for argument "documentPath" is not a valid resource path. Path must be a non-empty string. After google the answer, I tried using ""+context.params.id then I can console.log the context value. I only got
context {
eventId: '<eventID>',
timestamp: '2020-11-21T09:05:38.861Z',
eventType: 'google.pubsub.topic.publish',
params: {}
}
I thought I'd get document ID from params object here but it's empty.
Is there a way to get Document ID from firestore in a scheduled function?
The scheduled Cloud Function itself has no notion of a Firestore document ID since it is triggered by the Google Cloud Scheduler and not by an event occurring on a Firestore document. This is why the corresponding context object is different from the context object of a Firestore triggered Cloud Function.
I understand that you want to update the unique document corresponding to your first query (admin.firestore().collection('users').where('metadata.addToContact', '==', false).get();).
If this understanding is correct, do as follows:
exports.addContact = functions.region(region).pubsub.schedule('every 4 minutes').onRun(async (context) => {
// Query data from firestore
const querySnapshot = await admin.firestore().collection('users')
.where('metadata.addToContact', '==', false)
.get();
// Call SendGrid API to add contact
// If success, change metadata.addToContact to true
if (response) {
const docRef = querySnapshot.docs[0].ref;
await docRef.update({ 'metadata.sendToSg': true }, { merge: true });
} else {
console.log('No response');
}
return null;
});
Update following your comment:
You should not use async/await with forEach(), see here for more details. Use Promise.all() instead, as follows:
exports.addContact = functions.region(region).pubsub.schedule('every 4 minutes').onRun(async (context) => {
// Query data from firestore
const querySnapshot = await admin.firestore().collection('users')
.where('metadata.addToContact', '==', false)
.get();
// Call SendGrid API to add contact
// If success, change metadata.addToContact to true
if (response) {
const promises = [];
querySnapshot.forEach((doc) => {
const { metadata } = doc.data();
if (metadata.sendToSg == false) {
promises.push(doc.ref.update({ 'metadata.sendToSg': true }, { merge: true }));
}
})
await Promise.all(promises);
} else {
console.log('No response');
}
return null;
});
Related
I'm trying to get a doc in a collection and if it doesn't exist I create a new one, I would like to know if there is any way to get userData even if it doesn't exist (so to assign it the new one I have created) or I need to take it with
firebase
.firestore()
.collection("users")
.doc(ID);
again
const userRef = firebase
.firestore()
.collection("users")
.doc(ID);
const user = await userRef.get();
if (!user.exists) {
userRef.set(userSchema);
}
const userData = user.data();
I'm not sure where the user data is coming from but let's have a test user as follows:
const userData = {
name: "TestUser",
uid: "1234",
verified: true
}
Then here goes the function you need:
async function addUserToDatabase(userData) {
//Document Reference
const userDocRef = admin.firestore().collection("users").doc(userData.uid)
//Checking if document exists
if ((await userDocRef.get()).exists) {
return "User document already exists!"
}
//Document does not exists so create one
await userDocRef.set(userData)
return
}
New to Cloud Functions and trying to understand my error here from the log. It says cannot read property 'uid' of undefined. I am trying to match users together. onCreate will call matching function to check if a user exists under live-Channels and if so will set channel value under both users in live-Users to uid+uid2. Does the log also say which line the error is from? Confused where it shows that.
const functions = require('firebase-functions');
//every time user added to liveLooking node
exports.command = functions.database
.ref('/liveLooking/{uid}')
.onCreate(event => {
const uid = event.params.uid
console.log(`${uid} this is the uid`)
const root = event.data.adminRef.root
//match with another user
let pr_cmd = match(root, uid)
const pr_remove = event.data.adminRef.remove()
return Promise.all([pr_cmd, pr_remove])
})
function match(root, uid) {
let m1uid, m2uid
return root.child('liveChannels').transaction((data) => {
//if no existing channels then add user to liveChannels
if (data === null) {
console.log(`${uid} waiting for match`)
return { uid: uid }
}
else {
m1uid = data.uid
m2uid = uid
if (m1uid === m2uid) {
console.log(`$m1uid} tried to match with self!`)
return
}
//match user with liveChannel user
else {
console.log(`matched ${m1uid} with ${m2uid}`)
return {}
}
}
},
(error, committed, snapshot) => {
if (error) {
throw error
}
else {
return {
committed: committed,
snapshot: snapshot
}
}
},
false)
.then(result => {
// Add channels for each user matched
const channel_id = m1uid+m2uid
console.log(`starting channel ${channel_id} with m1uid: ${m1uid}, m2uid: ${m2uid}`)
const m_state1 = root.child(`liveUsers/${m1uid}`).set({
channel: channel_id
})
const m_state2 = root.child(`liveUsers/${m2uid}`).set({
channel: channel_id
})
return Promise.all([m_state1, m_state2])
})
}
You are referring to a very old version of the Cloud Functions API. Whatever site or tutorial you might be looking it, it's showing examples that are no longer relevant.
In modern Cloud Functions for Firebase, Realtime Database onCreate triggers receive two parameters, a DataSnapshot, and a Context. It no longer receives an "event" as the only parameter. You're going to have to port the code you're using now to the new way of doing things. I strongly suggest reviewing the product documentation for modern examples.
If you want to get the wildcard parameters as you are trying with the code const uid = event.params.uid, you will have to use the second context parameter as illustrated in the docs. To access the data from snapshot, use the first parameter.
I am currently trying to sync my firestore documents with algolia upon a new document creation or the update of a document. The path to the collection in firestore is videos/video. The function seems to be triggering fine, however after triggering, the firebase function does not seem to relay any of the information to algolia (no records are being created). I am not getting any errors in the log. (I also double checked the rules and made sure the node could be read by default, and yes I am on the blaze plan). Does anyone know how to sync a firestore node and algolia? Thanks for all your help!
"use strict";
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const algoliasearch_1 = require("algoliasearch");
// Set up Firestore.
admin.initializeApp();
const env = functions.config();
// Set up Algolia.
// The app id and API key are coming from the cloud functions environment, as we set up in Part 1,
const algoliaClient = algoliasearch_1.default(env.algolia.appid, env.algolia.apikey);
// Since I'm using develop and production environments, I'm automatically defining
// the index name according to which environment is running. functions.config().projectId is a default property set by Cloud Functions.
const collectionindexvideo = algoliaClient.initIndex('videos');
exports.collectionvideoOnCreate = functions.firestore.document('videos/{uid}').onCreate(async(snapshot, context) => {
await savevideo(snapshot);
});
exports.collectionvideoOnUpdate = functions.firestore.document('videos/{uid}').onUpdate(async(change, context) => {
await updatevideo(change);
});
exports.collectionvideoOnDelete = functions.firestore.document('videos/{uid}').onDelete(async(snapshot, context) => {
await deletevideo(snapshot);
});
async function savevideo(snapshot) {
if (snapshot.exists) {
const document = snapshot.data();
// Essentially, you want your records to contain any information that facilitates search,
// display, filtering, or relevance. Otherwise, you can leave it out.
const record = {
objectID: snapshot.id,
uid: document.uid,
title: document.title,
thumbnailurl: document.thumbnailurl,
date: document.date,
description: document.description,
genre: document.genre,
recipe: document.recipe
};
if (record) { // Removes the possibility of snapshot.data() being undefined.
if (document.isIncomplete === false) {
// In this example, we are including all properties of the Firestore document
// in the Algolia record, but do remember to evaluate if they are all necessary.
// More on that in Part 2, Step 2 above.
await collectionindexvideo.saveObject(record); // Adds or replaces a specific object.
}
}
}
}
async function updatevideo(change) {
const docBeforeChange = change.before.data();
const docAfterChange = change.after.data();
if (docBeforeChange && docAfterChange) {
if (docAfterChange.isIncomplete && !docBeforeChange.isIncomplete) {
// If the doc was COMPLETE and is now INCOMPLETE, it was
// previously indexed in algolia and must now be removed.
await deletevideo(change.after);
} else if (docAfterChange.isIncomplete === false) {
await savevideo(change.after);
}
}
}
async function deletevideo(snapshot) {
if (snapshot.exists) {
const objectID = snapshot.id;
await collectionindexvideo.deleteObject(objectID);
}
}
Still don't know what I did wrong, however if anyone else is stuck in this situation, this repository is a great resource: https://github.com/nayfin/algolia-firestore-sync. I used it and was able to properly sync firebase and algolia. Cheers!
// Takes an the Algolia index and key of document to be deleted
const removeObject = (index, key) => {
// then it deletes the document
return index.deleteObject(key, (err) => {
if (err) throw err
console.log('Key Removed from Algolia Index', key)
})
}
// Takes an the Algolia index and data to be added or updated to
const upsertObject = (index, data) => {
// then it adds or updates it
return index.saveObject(data, (err, content) => {
if (err) throw err
console.log(`Document ${data.objectID} Updated in Algolia Index `)
})
}
exports.syncAlgoliaWithFirestore = (index, change, context) => {
const data = change.after.exists ? change.after.data() : null;
const key = context.params.id; // gets the id of the document changed
// If no data then it was a delete event
if (!data) {
// so delete the document from Algolia index
return removeObject(index, key);
}
data['objectID'] = key;
// upsert the data to the Algolia index
return upsertObject(index, data);
};
So i have 2 collections
1 collection is 'users'. There i have documents (objects) with property 'profile', that contains string. It's id of profile, that is stored in other collection 'roles' as document.
So i'm trying to get this data, but without success. Is there exist join method or something like that? Or i must use promise for getting data from collection roles, and then assign it with agent?
async componentDidMount() {
firebase
.firestore()
.collection('users')
.orderBy('lastName')
.onSnapshot(async snapshot => {
let changes = snapshot.docChanges()
const agents = this.state.agents
for (const change of changes) {
if (change.type === 'added') {
const agent = {
id: change.doc.id,
...change.doc.data()
}
await firebase
.firestore()
.collection('roles')
.doc(change.doc.data().profile).get().then( response => {
//when i get response i want to set for my object from above this val
agent['profile'] = response.data().profile
//after that i want to push my 'agent' object to array of 'agents'
agents.push(agent)
console.log(agent)
}
)
}
}
this.setState({
isLoading: false,
agents: agents
})
})
}
To do async operation on array of objects you can use promise.all, i have given a example below that is similar to your use case where multiple async operation has to be done
const all_past_deals = await Promise.all(past_deals.map(async (item, index) => {
const user = await Users.get_user_info(item.uid);
const dealDetails = await Deals.get_deal_by_ids(user.accepted_requests || []);
const test = await Users.get_user_info(dealDetails[0].uid);
return test
}))
}
This way you can get data from once api call and make other api call with the obtained data
I'm having some trouble that I'm not understanding very well playing with next.js and Firebase's Cloud Firestore, but basically this works:
export async function fetchBroadcasts() {
const db = await loadDB();
const firestore = db.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
return await firestore.collection('broadcasts').doc('message').get().then(doc => ({ broadcast: doc.data() }));
}
and this doesn't:
export async function fetchBroadcasts() {
const db = await loadDB();
const firestore = db.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
return await firestore.collection('broadcasts').doc('message').onSnapshot(doc => ({ broadcast: doc.data() }));
}
I can't figure out why the second option doesn't work since I'm basically following the documentation.
On my index.js page I have this:
static async getInitialProps() {
return fetchBroadcasts();
}
onSnapshot doesn't return a promise, so you can't await it. As you can see from the linked API docs, it returns a function that you call when you want to stop the listener that you just added.
You use onSnapshot when you want to set up a persistent listener on a document that constantly receives changes to that document. You use get when you want a single snapshot of that document.
Firestore's onSnapshot() always return documents array regardless of query you use:
firestore.collection('broadcasts').doc('message')
.onSnapshot(docs => {
return docs[0].data();
});
While in case of get you can get single document as well on the base of query:
firestore.collection('broadcasts').doc('message')
.get( doc => ({ broadcast: doc.data() }) );