Sometimes firebase cloud functions didn't execute - firebase

Hi I'm using a cloud function to aggregate data created on a sub-collection to the parent collection, when I test it it works like a charm, but when I deploy it to my production environment that sometimes (is not common but occurs) the data is no aggregated to the parent document.
I think that the function is not executed because I don't get any error on the logs.
Here is my function code
exports.aggregateTranlationsToSong = functions.firestore
.document("songs2/{songId}/translations/{langId}")
.onCreate((event, context) => {
const { songId, langId } = context.params;
console.log({ songId, langId });
let songRef = admin
.firestore()
.collection("songs2")
.doc(songId);
return admin
.firestore()
.runTransaction(transaction => {
return transaction.get(songRef).then(songSnap => {
let actualSongData = songSnap.data();
let translations = actualSongData.lyric_translations;
if (translations === undefined || translations === null)
translations = {};
translations[langId] = true;
console.log({ translations });
return transaction.update(songRef, {
lyric_translations: translations
});
});
})
.catch(e => {
console.error(e);
});
});

Related

Send auth context to firebase callable function in unittest

I have been working on a firebase project in which I created a cloud function that creates documents in firestore. This is the function -
export const createExpenseCategory = functions
.region("europe-west1")
.https.onCall(async (data, context) => { // data is a string
if (!context.auth?.uid) { // check that requesting user is authenticated
throw new functions.https.HttpsError(
"unauthenticated",
"Not Authenticated"
);
}
const res = await admin
.firestore()
.collection("/categories/")
.where("uid", "==", context.auth.uid)
.get();
const categoryExists = res.docs.find((doc) => doc.data().name === data); // check that there are not duplicates.
// doc looks like this -
// {
// "name": "Food",
// "uid": "some_long_uid"
// }
if (categoryExists) {
throw new functions.https.HttpsError(
"already-exists",
`Category ${data} already exists`
);
}
return admin
.firestore()
.collection("/categories/")
.add({ name: data, uid: context.auth.uid });
});
As you can see, at the beginning of the function I check whether the user that sent the request is authenticated with the context parameter. Everything works fine when I play around with it in my web app, but I have been trying to figure out a way to create a unittest for this function. My problem is that I can't really figure out how to create an authenticated request to make sure that my function doesn't fail every time. I tried to look online for any documentation but couldn't seem to find any.
Thanks in advance!
You can unit test your functions using the firebase-functions-test SDK. The guide mentions you can mock the data within the eventContext or context parameter passed to your function. This works for mocking the uid field of the auth object:
// Left out authType as it's only for RTDB
wrapped(data, {
auth: {
uid: 'jckS2Q0'
}
});
The guide uses mocha for testing, but you can use other testing frameworks. I made a simple test to see if it would work and I could send the mock uid to the function, which worked as expected:
index.js
exports.authTest = functions.https.onCall( async (data, context) => {
if(!context.auth.uid){
throw new functions.https.HttpsError('unauthenticated', 'Missing Authentication');
}
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
return admin.firestore().collection('users').doc(userDoc.id).update({name: data.name});
});
index.test.js
const test = require('firebase-functions-test')({
projectId: PROJECT_ID
}, SERVICE_ACCTKEY); //Path to service account file
const admin = require('firebase-admin');
describe('Cloud Functions Test', () => {
let myFunction;
before(() => {
myFunction = require('../index.js');
});
describe('AuthTest', () => {
it('Should update user name in UID document', () => {
const wrapped = test.wrap(myFunction.authTest);
const data = {
name: 'FooBar'
}
const context = {
auth: {
uid: "jckS2Q0" //Mocked uid value
}
}
return wrapped(data, context).then(async () => {
//Asserts that the document is updated with expected value, fetches it after update
const q = await admin.firestore().collection('users').where('uid', '==', context.auth.uid).get();
const userDoc = q.docs.find(doc => doc.data().uid == context.auth.uid);
assert.equal(userDoc.data().name, 'FooBar');
});
});
});
});
Let me know if this was useful.

Firestore pre-deployment script

I'm searching for a way to add pre-deployment scripts to my Firebase project.
I'm using Firestore and my security rules are set up in a way that only cloud functions can write to Firestore.
I've added a user role field to my user table which automatically gets populated on userCreate. This works fine but my prod env still has users without this field.
A logical solution would be to run a pre-deploy command which add this field to all existing users but I have no clue how to do this.
My current best solution is to create a cloud function specifically for this one-time use and trigger it.
This doesn't feel like the right way to handle such things.
How do I run a one time update statement on Firestore?
You can write a temporary script using Firebase Admin SDK and execute it once. The flow would look something like:
Fetching all documents without the userRole field.
Add update statements in an array and execute all the promises at once.
Here's a demo:
const admin = require("firebase-admin");
const serviceAccount = require("/path/to/serviceAccountKet.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://fate-bot-discord.firebaseio.com"
});
async function addRoles() {
try {
const userColRef = admin.firestore().collection("users")
const users = await userColRef.where("userRole", "==", "").get()
const updates = []
users.docs.forEach((user) => {
updates.push(userColRef.doc(user.id).update({ userRole: "theNewRole" }))
})
await Promise.all(updates)
console.log("Roles added successfully")
return "Roles Added"
} catch (error) {
console.log(error);
return error
}
}
//Call the function
addRoles().then((response) => {
console.log(response)
}).catch((e) => {
console.log(e)
})
Please let me know if you need further assistance!
I've updated #Dharmaraj answer with some extra features in case someone ever needs this.
const admin = require('firebase-admin');
// DEV
const serviceAccount = require('./x-dev-firebase-adminsdk-1234.json');
// PROD
// const serviceAccount = require('./x-firebase-adminsdk-1234.json');
const newRoles = [0];
const emails = ['admin1#gmail.com', 'admin2#gmail.com'];
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const addRoles = async () => {
try {
let userColRef = admin.firestore().collection('users');
if (emails.length) {
userColRef = userColRef.where('email', 'in', emails);
}
const users = await userColRef.get();
const updates = [];
users.docs.forEach((doc) => {
const user = doc.data();
let existingRoles = [];
if (user.roles) {
existingRoles = user.roles;
if (newRoles.every((role) => existingRoles.includes(role))) {
return;
}
}
const roles = Array.from(new Set(existingRoles.concat(newRoles)));
updates.push(doc.ref.set({ roles }, { merge: true }));
});
await Promise.all(updates);
console.log(
`Role${newRoles.length > 1 ? 's' : ''} added to ${updates.length} user${
updates.length !== 1 ? 's' : ''
}.`
);
return true;
} catch (error) {
console.log(error);
return error;
}
};
addRoles().catch((e) => {
console.log(e);
});
Here's where you create the service account btw.

How to ensure that a cloud function is running every time a new document gets created?

I am uploading my questions and answers to my quiz to Firestore. For that I am using following function:
const firestore = admin.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
if (data && (typeof data === "object")) {
Object.keys(data).forEach(docKey => {
var data_to_push = data[docKey];
data_to_push['category'] = "Wirtschaft";
firestore.collection(collectionKey).add(data_to_push).then((res) => {
console.log("Document " + docKey + " successfully written!");
}).catch((error) => {
console.error("Error writing document: ", error);
});
});
This function works fine, all the documents I need are created but whenever a document get created I have another function that is running:
// This function adds the doc ids of newly created questions to an arrayList
exports.AddKeyToArray = functions.region('europe-west1').firestore.document('Questions/{nameId}').onCreate(async (snp, context) => {
console.log(snp.id);
console.log(context.params);
await db.collection("Questions_keys").doc(snp.data().category).update({ "questions": admin.firestore.FieldValue.arrayUnion(snp.id) }).then(() => {
return console.log("Key added");
}).catch(async (e) => {
console.log(e);
if (e.code === 5) {
await db.collection("Questions_keys").doc(snp.data().category).set({ "questions": admin.firestore.FieldValue.arrayUnion(snp.id) }).then(() => {
return console.log("First time key added");
}).catch(e => {
return console.log(e);
})
}
})
return "okay";
})
This function basically gets the document id of the previously added question/answer and creates an array with all the document ids of that quiz category (so I then later can get a random question without much reading operations). The problem is that not all document ids are added to the array so I wanted to know if there is a better way to ensure that all the document ids are added into the array.
I upload sometimes 500 documents at once, would be a solution to reduce the documents I upload at once to ensure a better performance of the second function?
Any help is much appreciated!
I suggest that rather than using cloud functions here is to create another collection in your database. This way you can add more questions to that collection easily. This design will increase performance as what you will need is only query the new collection directly and this way you will avoid all the complication needed to manage and work with Cloud Functions.
With help I found a solution: The following function uploads data to firestore and gets the ids of the documents and sets it to an array:
...
const firestore = admin.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
if (data && (typeof data === "object")) {
Object.keys(data).forEach(async docKey => {
var data_to_push = data[docKey];
data_to_push['category'] = "Deutschland";
await firestore.collection(collectionKey).add(data_to_push).then(async (res) => {
var key = (res['_path']['segments'][1]);
await firestore.collection("Questions_keys").doc(data_to_push['category']).update({ "questions": admin.firestore.FieldValue.arrayUnion(key) }).then(() => {
console.log("Key added: " + key);
}).catch(async (e) => {
if (e.code === 5) {
await firestore.collection("Questions_keys").doc(data_to_push['category']).set({ "questions": admin.firestore.FieldValue.arrayUnion(key) }).then(() => {
return console.log("First time key added");
}).catch(e => {
return console.log(e);
})
}
console.log(e);
})
}).catch((error) => {
console.error("Error writing document: ", error);
});
});
}

Can't access data base from a Firebase function

I tried everything , I have this cloud function (that otherwise works) :
exports.contentServer = functions.https.onRequest((request, response) => {
admin.database().ref('/list/' + "abc").once('value').then(function(snapshot) {
console.log(snapshot.val() );
return null;
}).catch(function(error) {
console.log("Error getting document:", error);
return response.send(error);
});
});
or also this :
admin.database().ref('/list').once('value').then(function(snapshot) {
var event = snapshot.val();
app.tell('Result: '+event);
});
and this :
exports.contentServer = functions.https.onRequest((request, response) => {
var db = admin.database();
db.ref("list/abc").once("value").then(snap => {
var store = snap.val().description;
return store;
}).then(() => {
var store = snap.val().description;
return store;
}).then(snap => {
var store = snap.val().description;
return store;
}).catch(err => {
console.log(err);
response.send("error occurred");
});
});
and always get back the error :
"Could not handle the request"
Or I get error on deploy that :
Each then() should return a value or throw
I have a collection called list, inside I have a document named "abc".
Is there something I have to include ? something I have to setup in Firebase to make it work ? anything basic nobody write on the docs ?
Modified following the comments above explaining the OP uses Firestore and not the Realtime Database
You should do as follows. You have to wait that the promise returned by the get() method resolves before sending back the response. For this you need to use the then() method, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
exports.contentServer = functions.https.onRequest((request, response) => {
admin.firestore().collection('list').doc('abc').get()
.then(docSnapshot => {
console.log(docSnapshot.data());
return response.send(docSnapshot.data()); // or any other value, like return response.send( {result: "success"} );
})
.catch(error => {
console.log("Error getting document:", error);
return response.status(500).send(error);
});
});
As written in the comments above, I would suggest that you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/
Try this
Updated. Return the response inside then() as what #Renaud Tarnec pointed out.
Using realtime database
exports.contentServer = functions.https.onRequest((request, response) => {
var database = admin.database();
database.ref('list').child('abc').once("value", snapshot => {
const data = snapshot.val();
return response.send(data);
}).catch(error => {
return response.status(500).send(error);
});
});
If you are using firestore.
exports.contentServer = functions.https.onRequest((request, response) => {
const firestore = admin.firestore();
firestore.collection("list").doc('abc').get().then(doc => {
console.log(doc.data());
return response.send(doc.data());
}).catch(error => {
return response.status(500).send(error);
});
});
Important: Don't forget to terminate the request by calling response.redirect(), response.send(), or responses.end() so you can avoid excessive charges from functions that run for too long

How to update document in firebase cloud function

In my cloud function I want to update my document from 'dashboard' collection when a new student added to 'students' collection.
const getActiveStudents = () => {
return db.collection('/students/').where('status', '==', true).get().then(
snapshot => {
let studentsCount = snapshot.docs.length;
db.collection('/dashboard/').where('type', '==', 'students').get().then(
result => {
if (result.docs.length === 0) {
db.collection('dashboard').add({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}else {
result.docs[0].ref.update({
count: studentsCount,
type: 'students',
label: 'Active students'
});
}
return result;
}
).catch(error => {
console.log(error);
});
return snapshot;
}
).catch(error => {
console.log(error);
})
}
exports.onChangesInStudents = functions.firestore.document('/students/{studentId}').onWrite(event => {
getActiveStudents();
return;
});
When I add a new student, instead of updating document it adds a new document to my 'dashboard' collection.
How should I organize my code in order to properly update the quantity of students.
as #Doug mentioned, iterating over the entire collection is too heavy. instead you can stream the query results and iterate over keys, using query.stream().
to access and update a single field in a document, first retrieve the document by its ID with doc(), then use update() while specifying the field.
here's an example of implementation based on your scenario.
package.json
{
"dependencies": {
"firebase-admin": "^6.5.1",
"firebase-functions": "^2.1.0"
}
}
index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const studentsRef = admin.firestore().collection('students');
const dashboardRef = admin.firestore().collection('dashboard');
exports.addStudent = functions.firestore
.document('students/{studentId}')
.onCreate((snap, context) => {
var newStudent = snap.data();
console.log('New student in collection: ', newStudent);
var activeCount = 0;
studentsRef.where('status', '==', true).select().stream()
.on('data', () => {
++activeCount;
}).on('end', () => {
dashboardRef.where('type', '==', 'students').get()
.then(querySnap => {
if (querySnap.docs[0].data().count == activeCount){
console.log('No new active student: ', querySnap.docs[0].data());
} else {
console.log('New active count: ', activeCount);
console.log('Student Dashboard before update: ', querySnap.docs[0].id, '=>', querySnap.docs[0].data());
dashboardRef.doc(querySnap.docs[0].id).update({
count: activeCount
});
console.log('Active student count updated: ', querySnap.docs[0].data().count, '=>', activeCount);
};
});
});
return null
});
gcloud
gcloud functions deploy addStudent \
--runtime nodejs8 \
--trigger-event providers/cloud.firestore/eventTypes/document.create \
--trigger-resource "projects/[PROJECT_ID]/databases/(default)/documents/students/{studentId}"
When a function is triggered, you might want to get data from a document that was updated, or get the data prior to update.
You can get the prior data by using change.before.data(), which contains the document snapshot before the update.
Similarly, change.after.data() contains the document snapshot state after the update.
Node.js
exports.updateUser = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
// Get an object representing the current document
const newValue = change.after.data();
// ...or the previous value before this update
const previousValue = change.before.data();
//...therefore update the document as.
admin.firestore().collection('user').doc(docId).update(snapshot.after.data());
});
Reference:-
https://firebase.google.com/docs/functions/firestore-events

Resources