Firestore pre-deployment script - firebase

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.

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.

Did anything major change that my Cloud functions stopped working with flutter web?

I used to have Firebase Cloud Functions running. But after refactoring my whole codebase to sound null safety, cloud functions stopped working (Sadly, I cannot reproduce at which point in the timeline)..
pubspec.yaml
dependencies:
flutter:
sdk: flutter
firebase_core: ^1.0.2
firebase_auth: ^1.0.1
cloud_firestore: ^1.0.4
cloud_functions: ^1.1.0
...
web/index.html
...
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-functions.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.19.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.19.1/firebase-firestore.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "<myApiKey>",
authDomain: "<my-project>.firebaseapp.com",
databaseURL: "https://<my-project>.firebaseio.com",
projectId: "<my-project>",
storageBucket: "<my-project>.appspot.com",
messagingSenderId: "<myMessageSenderId>",
appId: "<myAppId>"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.functions().useFunctionsEmulator("http://10.0.2.2:5001");
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const { UserPropertyValue } = require('firebase-functions/lib/providers/analytics');
admin.initializeApp();
exports.setRoles = functions.https.onCall((data, context) => {
let userId = null;
let userCustomClaimsAdmin = false;
let userCustomClaimsEditor = false;
// get user and update custom claim
return admin.auth().getUserByEmail(data.email).then(user => {
userId = user.uid;
const currentCustomClaims = (user.customClaims == undefined) ? {} : user.customClaims;
switch (data.role) {
case 'admin':
currentCustomClaims.admin = (data.permission == 'grant') ? true : false;
break;
case 'editor':
currentCustomClaims.editor = (data.permission == 'grant') ? true : false;
break;
default:
return;
}
userCustomClaimsAdmin = currentCustomClaims.admin;
userCustomClaimsEditor = currentCustomClaims.editor;
return admin.auth().setCustomUserClaims(userId,
currentCustomClaims
);
}).then(() => {
// Update User record in Firestore
return admin.firestore().collection("users").doc(userId).update({
isAdmin: userCustomClaimsAdmin,
isEditor: userCustomClaimsEditor,
});
}).then(() => {
return {
message: 'Success'
}
})
.catch(err => {
console.log(err.toString());
});
});
Finally I call the function with:
...
final HttpsCallable setRoleCallable = FirebaseFunctions.instance
.httpsCallable('setRoles',
options:
HttpsCallableOptions(timeout: const Duration(seconds: 10)));
...
try {
final HttpsCallableResult result = await setRoleCallable.call(
<String, dynamic>{
'email': "<emailOfUserToBeChanged>",
'role': "<selectedRole>",
'permission': "<givenAccess>"
},
);
print(result.data);
} on FirebaseFunctionsException catch (e) {
print('caught firebase functions exception');
print(e.code);
print(e.message);
print(e.details);
} catch (e) {
print('caught generic exception');
print(e);
}
That call (emulated functions at localhost resp. 10.0.2.2) ends in
caught firebase functions exception
internal
internal
null
Did anything change in the meantime that I have missed? I could not find anything regarding this topic within the Firebase documentation.
Perhaps it might be a little change at some point that I did not recognize yet..
Well, a major change with Cloud Functions is that you now have to have the paid Firebase plan to use cloud functions as they sadly removed Cloud Functions from the free tier.
In your Cloud Function you don't wait for the asynchronous operations to complete before sending back the response. See the doc for more details on this key aspect.
The tricky thing is that it generates some "erratic" behaviour (sometimes it works, sometimes not) that can be explained as follows:
In some cases, your Cloud Function is terminated before the asynchronous operations are completed, as explained in the doc referred to above.
But, in some other cases, it may be possible that the Cloud Functions platform does not immediately terminate your CF, giving enough time for the asynchronous operations to be completed.
So you have the impression that "Cloud functions stopped working with flutter web" while, actually, sometimes it works and some other times not...
In addition, note that the setCustomUserClaims() method returns a Promise<void> not a user, therefore you need to keep a set of global variables for the userId and the claims in order to pass it from one then() block to the other.
So the following should do the trick (untested):
exports.setRoles = functions.https.onCall((data, context) => {
console.log('user to change email: ' + data.email);
let userId = null;
let userCustomClaimsAdmin = false;
let userCustomClaimsEditor = false;
// get user and update custom claim
return admin.auth().getUserByEmail(data.email)
.then(user => {
userId = user.uid; // the setCustomUserClaims() method returns a Promise<void> not a user !!
const currentCustomClaims = (user.customClaims == undefined) ? {} : user.customClaims;
switch (data.role) {
case 'admin':
currentCustomClaims.admin = (data.permission == 'grant') ? true : false;
break;
case 'editor':
currentCustomClaims.editor = (data.permission == 'grant') ? true : false;
break;
default:
return;
break;
}
// Here you need to adapt the value of userCustomClaimsAdmin and userCustomClaimsEditor
userCustomClaimsAdmin = ...
userCustomClaimsEditor = ...
// See return below !!!!
return admin.auth().setCustomUserClaims(user.uid,
currentCustomClaims
);
})
.then(() => {
// See return below !!!!
return admin.firestore().collection("users").doc(userId).update({
isAdmin: (userCustomClaimsAdmin) ? user.customClaims.admin : false,
isEditor: (userCustomClaimsEditor) ? user.customClaims.editor : false,
});
})
.then(() => {
return {
message: 'Success'
}
})
.catch(err => {
console.log(err.toString());
// !!!! See the doc: https://firebase.google.com/docs/functions/callable#handle_errors
});
});
Well, I am developing a flutter-web project. I use cloud-functions, not cloud-functions-web.
In my main.dart the directive to use the functions emulator was missing:
...
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseFunctions.instance
.useFunctionsEmulator(origin: 'http://localhost:5001'); // this was missing
runApp(MyApp());
...
}
It used to work, as I already had that directive in my index.html
...
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.functions().useFunctionsEmulator("http://10.0.2.2:5001");
...
Nevertheless it works now.

Sometimes firebase cloud functions didn't execute

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);
});
});

Firebase Cloud Function not executing

I'm working on the group functionality of my app where members of the group can add task to a group that they are currently working on. So, when a task is added I want to notify all members using FCM that a task had been added to the group.
EDIT:
The code to add the task to a group is run on the client and works successfully. The purpose of the cloud function is just to send cloud messages to all the members of the group that a task has been added by a particular member.
This is my logic for the cloud function :
1st. As a task can be added to multiple groups at a time I'm using a forEach().
2nd. I'm fetching the uids of the Members in the groups and pushing them into an array(uids) so that later I can retrieve their fcmTokens.
3rd. Running a forEach on the uids to retrieve the fcmTokens.
4th.Sending Cloud message to devices.
But the cloud function doesn't execute as expected.
This my cloud function:
exports.newTaskAdded = functions.https.onCall(async (data, context) => {
const groups = data.groups; //Can be multiple groups hence an array.
const uid = data.uid;
const author = data.author;
const taskId = data.taskId;
const taskTitle = data.taskTitle;
try {
groups.forEach(async group => {
const groupName = group.groupName;
console.log('groupName: ', groupName);
const groupId = groups.groupId;
const membersPromises = [];
membersPromises.push(
admin
.firestore()
.collection('Groups')
.doc(`${groupId}`)
.collection('Members') //Members collection has document for each user with their uid as the document name.
.get(),
);
console.log('memberPromises: ', membersPromises);//Function stops after this.
const membersSnapshot = await Promise.all(membersPromises);
console.log('membersSnapshots', membersSnapshot);
const uids = [];
membersSnapshot.forEach(doc => {
doc.forEach(snap => {
console.log(snap.id);
uids.push(snap.id);
});
});
console.log(uids);
const uidPromises = [];
uids.forEach(uid => {
uidPromises.push(
admin
.firestore()
.collection('Users')
.doc(`${uid}`)
.get(),
);
});
console.log('uidPromises: ', uidPromises);
const tokensSnapshots = await Promise.all(uidPromises);
const notifPromises = [];
tokensSnapshots.forEach(snap => {
console.log(snap.data());
const token = Object.keys(snap.data().fcmTokens);
const payload = {
notification: {
title: `${author} has added a new task to ${groupName}`,
body: `Task Added: ${taskTitle}`,
sound: 'default',
},
};
notifPromises.push(admin.messaging().sendToDevice(token, payload));
});
await Promise.all(notifPromises);
});
} catch (err) {
console.log(err);
}
return {result: 'OK'};
});
This is my log:
As you can see there is no error shown.
Help would be very much appreciated. Thank you

Firestore - Get document collections

I would automate the backup process of a firestore database.
The idea is to loop over the root document to build a JSON tree, but I didn't find a way to get all collections available for a document. I guess it's possible as in firestore console we can see the tree.
Any ideas?
ref doc: https://firebase.google.com/docs/reference/js/firebase.firestore
Its possible on web (client side js)
db.collection('FirstCollection/' + id + '/DocSubCollectionName').get().then((subCollectionSnapshot) => {
subCollectionSnapshot.forEach((subDoc) => {
console.log(subDoc.data());
});
});
Thanks to #marcogramy comment
firebase.initializeApp(config);
const db = firebase.firestore();
db.settings({timestampsInSnapshots: true});
const collection = db.collection('user_dat');
collection.get().then(snapshot => {
snapshot.forEach(doc => {
console.log( doc.data().name );
console.log( doc.data().mail );
});
});
Update
API has been updated, now function is .listCollections()
https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#listCollections
getCollections() method is available for NodeJS.
Sample code:
db.collection("Collection").doc("Document").getCollections().then((querySnapshot) => {
querySnapshot.forEach((collection) => {
console.log("collection: " + collection.id);
});
});
If you are using the Node.js server SDK you can use the getCollections() method on DocumentReference:
https://cloud.google.com/nodejs/docs/reference/firestore/0.8.x/DocumentReference#getCollections
This method will return a promise for an array of CollectionReference objects which you can use to access the documents within the collections.
As mentioned by others, on the server side you can use getCollections(). To get all the root-level collections, use it on the db like so:
const serviceAccount = require('service-accout.json');
const databaseURL = 'https://your-firebase-url-here';
const admin = require("firebase-admin");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: databaseURL
});
const db = admin.firestore();
db.settings({ timestampsInSnapshots: true });
db.getCollections().then((snap) => {
snap.forEach((collection) => {
console.log(`paths for colletions: ${collection._referencePath.segments}`);
});
});

Resources