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.
I am currently working on an App. The workflow I currently have is fairly simple.
A user creates an account, and then is taken to a page to populate their profile information. Name, description, and a few images.
I use expo's ImagePicker to get the image:
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.1,
allowsEditing: true,
aspect: [2, 3],
base64: true
});
Originally, I was using this to upload the images:
// Why are we using XMLHttpRequest? See:
// https://github.com/expo/expo/issues/2402#issuecomment-443726662
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onerror = function(e) {
reject(new TypeError("Network request failed"));
};
xhr.responseType = "blob";
xhr.open("GET", uri, true);
xhr.send(null);
});
const ref = firebase
.storage()
.ref()
.child(uuid.v4());
const snapshot = await ref.put(blob);
// We're done with the blob, close and release it
blob.close();
let url = await snapshot.ref.getDownloadURL();
return url;
The problem here is I looped through that function about 6 times, and I kept getting some obscure error.
Currently, I am attempting to upload the images using this:
const ref = firebase
.storage()
.ref()
.child(uuid.v4());
const snapshot = await ref.putString(b64Url, "data_url");
This works well on web, but in the native app I get the error:
FirebaseStorageError {
"code_": "storage/invalid-format",
"message_": "Firebase Storage: String does not match format 'base64': Invalid character found",
"name_": "FirebaseError",
"serverResponse_": null,
}
The last comment on this issue outlines the problem. To break it down: atob doesn't exist. This is the sole problem behind the error. To fix, I polyfilled it like this:
import { decode, encode } from "base-64";
if (!global.btoa) {
global.btoa = encode;
}
if (!global.atob) {
global.atob = decode;
}
However, the second problem is that:
Firebase also tries to use the native Blob class (implemented by react-native), but the react-native version of Blob incorrectly converts the Uint8Array data to a string, corrupting the upload.
I tried his solution of deleteing global.Blob and restoring it after the upload. Firebase must have become dependent upon blob though, because now it errors out since Blob doesn't exist. Edit: Blob is actually being called somewhere in AppEntry.bundle, the uploading works correctly.
I would like to keep my app in a managed workflow, so I would very much prefer not to eject.
My questions are as follows:
Where specifically in react-native is the broken Blob code that:
incorrectly converts the Uint8Array data to a string
Is there a way that I can, while avoiding errors or ejecting, upload 6 images at once to firebase storage? If so, how?
The solution I ended up following was this:
async function uploadImageAsync(uri) {
const ref = firebase
.storage()
.ref()
.child(uuid.v4());
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onerror = function() {
reject(new TypeError("Network request failed"));
};
xhr.responseType = "blob";
xhr.open("GET", uri, true);
xhr.send(null);
});
var mimeString = uri
.split(",")[0]
.split(":")[1]
.split(";")[0];
const snapshot = await ref.put(blob, { contentType: mimeString });
let url = await snapshot.ref.getDownloadURL();
return url;
}
I found that I could not seem to get firebase's putString function to work, but I could create a blob out of the string using XMLHttpRequest. Then I just upload the blob to firebase
I'm trying to build an Android application. In my Firestore database I have Users collection and Counters collection. In Counters collection I have userCounter. What I want to do is whenerever a new user logs in and I push it to firestore, userCounter to increase.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addNewUser =
functions.firestore.document('Users/{userID}').onCreate((event) => {
var db = admin.firestore();
var counterRef = db.collection("Counters");
var temp = counterRef.doc("0").data().userCounter++;
counterRef.doc("0").update(
{
userCounter: temp
});
});
In this state, this function doesn't work, and I'm a newbie so I'd appreciate any help.
Thx beforehand
EDIT
After implementing Firebaser and Pablo AlmĂ©cija RodrĂguez's answers, my code looks like this.
const Firestore = require('#google-cloud/firestore');
const firestore = new Firestore({
projectId: process.env.GCP_PROJECT,
});
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNewUser =
functions.firestore.document('Users/{userId}').onCreate((snapShot) => {
const userCounterRef = db.collection('Counters').doc('Users');
return db.runTransaction(transaction => {
const doc = transaction.get(userCounterRef);
console.log("1");
const count = doc.data();
console.log(`5`);
const updatedCount = count + 1;
console.log(`6`);
return transaction.update(userCounterRef, {counter: updatedCount })
})
});
And this is the firebase console log. The problem is
const count = doc.data();
TypeError: doc.data is not a function
Firebase Console Log
I suggest you to create a collection named counters and inside it a document named users to handle counter for users. Here is the structure:
- counters (collection)
- users (document)
count: 0 (field)
Then, you should use a transaction to perform an update on this counter document to make sure you are working with up-to-date data to deal with concurrency (multiple accounts created at the same time)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNewUser =
functions.firestore.document('users/{userId}').onCreate((snapShot) => {
const userCounterRef = db.doc('counters/users');
return db.runTransaction(async transaction => {
const doc = await transaction.get(userCounterRef)
const { count } = doc.data()
const updatedCount = count + 1
return transaction.update(userCounterRef, {count: updatedCount })
})
});
https://firebase.google.com/docs/firestore/manage-data/transactions
EDIT: If you don't want to deal with async/await syntax, update your transaction like that :
return db.runTransaction(transaction => {
return transaction.get(userCounterRef)
.then(doc => {
const count = doc.data().count
const updatedCount = count + 1
transaction.update(userCounterRef, {count: updatedCount })
})
})
I have reproduced it in Cloud Functions, and this simple solution worked. Edited answer to fit Firebase, and it also uses the Firestore dependency for nodejs.
const Firestore = require('#google-cloud/firestore');
const firestore = new Firestore({
projectId: process.env.GCP_PROJECT,
});
const functions = require('firebase-functions');
//I am not using next two lines right now though
const admin = require('firebase-admin');
admin.initializeApp();
exports.helloFirestore =
functions.firestore.document('users/{userID}').onCreate((event) => {
const doc = firestore.doc('Counters/UserCounter/');
doc.get().then(docSnap => {
//Get the specific field you want to modify
return docSnap.get('userCount');
}).then(field => {
field++;
//Log entry to see the change happened
console.log(`Retrieved field value after +1: ${field}`);
//Update field of doc with the new value
doc.update({
userCount: field,
});
});
});
The wildcard you used should be fine, just be careful with the full path for the collection, mind the upper/lower case. For this case, this is how my package.json looks like:
{
"name": "sample-firestore",
"version": "0.0.1",
"dependencies": {
"#google-cloud/firestore": "^1.1.0",
"firebase-functions": "2.2.0",
"firebase-admin": "7.0.0"
}
}
I build app in react native with firebase/firestore.
I'm looking a way to check the count of users online in app, so I found a way to do with this tutorial
var userRef = new Firebase('https://<demo>.firebaseio.com/presence/' + userid);
userRef.on('value', function(snapshot) {
if (snapshot.val() === true) {
// User is online, update UI.
} else {
// User logged off at snapshot.val() - seconds since epoch.
}
});
I'm looking a way to do with firestore and react native. is there any implementation i can see how do that?
I found this way to do with firestore
import { Platform } from 'react-native';
import firebase from 'react-native-firebase';
function rtdb_and_local_fs_presence() {
// [START rtdb_and_local_fs_presence]
// [START_EXCLUDE]
var uid = firebase.auth().currentUser.uid;
console.log('uid',uid)
var userStatusDatabaseRef = firebase.database().ref('status/' + uid);
var isOfflineForDatabase = {
state: 'offline',
last_changed: firebase.database.ServerValue.TIMESTAMP,
};
var isOnlineForDatabase = {
state: 'online',
last_changed: firebase.database.ServerValue.TIMESTAMP,
};
// [END_EXCLUDE]
var userStatusFirestoreRef = firebase.firestore().doc('status/' + uid);
// Firestore uses a different server timestamp value, so we'll
// create two more constants for Firestore state.
var isOfflineForFirestore = {
state: 'offline',
last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};
var isOnlineForFirestore = {
state: 'online',
last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};
firebase.database().ref('.info/connected').on('value', function(snapshot) {
if (snapshot.val() == false) {
// Instead of simply returning, we'll also set Firestore's state
// to 'offline'. This ensures that our Firestore cache is aware
// of the switch to 'offline.'
userStatusFirestoreRef.set(isOfflineForFirestore);
return;
};
userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
userStatusDatabaseRef.set(isOnlineForDatabase);
// We'll also add Firestore set here for when we come online.
userStatusFirestoreRef.set(isOnlineForFirestore);
});
});
// [END rtdb_and_local_fs_presence]
}
function fs_listen() {
var uid = firebase.auth().currentUser.uid;
var userStatusFirestoreRef = firebase.firestore().doc('status/' + uid);
// [START fs_onsnapshot]
userStatusFirestoreRef.onSnapshot(function(doc) {
var isOnline = doc.data().state == 'online';
// ... use isOnline
});
}
firebase.auth().signInAnonymouslyAndRetrieveData().then((user) => {
rtdb_and_local_fs_presence();
fs_listen();
});
it really update the status collection with the right uid when I'm online, but when I disconnect from app, it not update to offline. how can I do that?
It will be working when user closes the app completly.
onStatusOffline(user){
firebase.database().ref(`users/${user.uid}`)
.onDisconnect()
.update({
online: false,
});
}
Is there a way to import CSV or JSON to firebase cloud firestore like in firebase realtime database?
General Solution
I've found many takes on a script allowing to upload a JSON but none of them allowed sub-collections. My script above handles any level of nesting and sub-collections. It also handles the case where a document has its own data and sub-collections. This is based on the assumption that collection is array/object of objects (including an empty object or array).
To run the script make sure you have npm and node installed. Then run your code as node <name of the file>. Note, there is no need to deploy it as a cloud funciton.
const admin = require('../functions/node_modules/firebase-admin');
const serviceAccount = require("./service-key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://<your-database-name>.firebaseio.com"
});
const data = require("./fakedb.json");
/**
* Data is a collection if
* - it has a odd depth
* - contains only objects or contains no objects.
*/
function isCollection(data, path, depth) {
if (
typeof data != 'object' ||
data == null ||
data.length === 0 ||
isEmpty(data)
) {
return false;
}
for (const key in data) {
if (typeof data[key] != 'object' || data[key] == null) {
// If there is at least one non-object item in the data then it cannot be collection.
return false;
}
}
return true;
}
// Checks if object is empty.
function isEmpty(obj) {
for(const key in obj) {
if(obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
async function upload(data, path) {
return await admin.firestore()
.doc(path.join('/'))
.set(data)
.then(() => console.log(`Document ${path.join('/')} uploaded.`))
.catch(() => console.error(`Could not write document ${path.join('/')}.`));
}
/**
*
*/
async function resolve(data, path = []) {
if (path.length > 0 && path.length % 2 == 0) {
// Document's length of path is always even, however, one of keys can actually be a collection.
// Copy an object.
const documentData = Object.assign({}, data);
for (const key in data) {
// Resolve each collection and remove it from document data.
if (isCollection(data[key], [...path, key])) {
// Remove a collection from the document data.
delete documentData[key];
// Resolve a colleciton.
resolve(data[key], [...path, key]);
}
}
// If document is empty then it means it only consisted of collections.
if (!isEmpty(documentData)) {
// Upload a document free of collections.
await upload(documentData, path);
}
} else {
// Collection's length of is always odd.
for (const key in data) {
// Resolve each collection.
await resolve(data[key], [...path, key]);
}
}
}
resolve(data);
You need a custom script to do that.
I wrote one based on the Firebase admin SDK, as long as firebase library does not allow you to import nested arrays of data.
const admin = require('./node_modules/firebase-admin');
const serviceAccount = require("./service-key.json");
const data = require("./data.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://YOUR_DB.firebaseio.com"
});
data && Object.keys(data).forEach(key => {
const nestedContent = data[key];
if (typeof nestedContent === "object") {
Object.keys(nestedContent).forEach(docTitle => {
admin.firestore()
.collection(key)
.doc(docTitle)
.set(nestedContent[docTitle])
.then((res) => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
});
}
});
Update: I wrote an article on this topic - Filling Firestore with data
There is not, you'll need to write your own script at this time.
I used the General Solution provided by Maciej Caputa. Thank you (:
Here are a few hints. Assuming that you have an Ionic Firebase application installed with the required Firebase node modules in the functions folder inside that solution. This is a standard Ionic Firebase install. I created an import folder to hold the script and data at the same level.
Folder Hierarchy
myIonicApp
functions
node_modules
firebase-admin
ImportFolder
script.js
FirebaseIonicTest-a1b2c3d4e5.json
fileToImport.json
Script Parameters
const admin = require('../myIonicApp/functions/node_modules/firebase-admin'); //path to firebase-admin module
const serviceAccount = require("./FirebaseTest-xxxxxxxxxx.json"); //service account key file
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://fir-test-xxxxxx.firebaseio.com" //Your domain from the hosting tab
});
Creating the Service Account Key File
In the Firebase console for your project, next to the Project
Overwiew item, click on the gear icon and select Users and
permissions
At the bottom of the screen, click Advanced permission
settings
This opens another tab for the Google Cloud Platform Console
On the left select the Service Accounts item
Create a Service Account for an existing Service Account
I simply added a key to the App Engine default service account
The Create key function will offer to download the key to a JSON file
JSON Data Structure
To use the script provided, the data structure must be as follows:
{
"myCollection" : {
"UniqueKey1" : {
"field1" : "foo",
"field2" : "bar"
},{
"UniqueKey2" : {
"field1" : "fog",
"field2" : "buzz"
}...
}
For reference. I wrote a function that helps to import and export data in Firestore.
https://github.com/dalenguyen/firestore-import-export (archived)
Updated 2023:
Use this package instead. Thanks, #Webber
https://github.com/dalenguyen/firestore-backup-restore
https://gist.github.com/JoeRoddy/1c706b77ca676bfc0983880f6e9aa8c8
This should work for an object of objects (generally how old firebase json is set up). You can add that code to an app that's already configured with Firestore.
Just make sure you have it pointing to the correct JSON file.
Good luck!
This workaround in Python may help some people. First convert json or csv to dataframe using Pandas, then convert dataframe to dictionary and upload dictionary to firestore.
import firebase_admin as fb
from firebase_admin import firestore
import pandas as pd
cred = fb.credentials.Certificate('my_credentials_certificate.json')
default_app = fb.initialize_app(cred)
db = firestore.client()
df = pd.read_csv('data.csv')
dict = df.to_dict(orient='records')
my_doc_ref = db.collection('my_collection').document('my_document')
my_doc_ref.set(dict)
There could be similar workarounds in javascript and other languages using libraries similar to Pandas.
No as of now, you can't.. firestore structures data into a different format that is, using collections and each collection has a series of documents which then are stored in JSON format.. in future they might make a tool to convert JSON in to firestore.. for reference check this out
:https://cloud.google.com/firestore/docs/concepts/structure-data
****EDIT :****
You can automate the process up to some extent, that is, write a mock software which only pushes the fields of your CSV or JSON data into your Cloud Firestore DB. I migrated my whole database my making just a simple app which retrieved my DB and pushed it on Firestore
var admin = require('firebase-admin');
var serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://csvread-d149c.firebaseio.com'
});
const csv = require('csv-parser');
const fs = require('fs');
const firestore = admin.firestore();
// CSV FILE data.csv
// Name,Surname,Age,Gender
// John,Snow,26,M
// Clair,White,33,F
// Fancy,Brown,78,F
fs.createReadStream('data.csv')
.pipe(csv())
.on('data', (row) => {
console.log(row);
if(row) {
firestore.collection('csv').add({
name: row.Name,
surname: row.Surname,
age: row.Age,
sex: row.Gender
});
}
else {
console.log('No data')
}
})
.on('end', () => {
console.log('CSV file successfully processed');
});
There is now a paid GUI tool that does this, Firefoo. I haven't used it, but it seems quite powerful for managing Firestore - including importing csv files:
This is a small modification that copies the 'id' of the document, if exists, to its path. Otherwise it will use the "for"'s index.
...
...
} else {
// Collection's length of is always odd.
for (const key in data) {
// Resolve each collection.
if (data[key]['id']!==undefined)
await resolve(data[key], [...path,data[key]['id']])
else
await resolve(data[key], [...path, key]);
}
}
}
resolve(data);
1 - Importing only collections
If the names of your collections are not only composed of numbers, then you can define the name of the document as follows.
...
...
} else {
// Collection's length of is always odd.
for (const key in data) {
// // Resolve each collection.
// If key is a number it means that it is not a collection
// The variable id in this case will be the name of the document field or you
// can generate randomly
let id = !isNaN(key) ? data[key].id : key;
await resolve(data[key], [...path, id]);
}
}
}
2 - Import collections and sub-collections
In the same way as in the example above, the name of the sub-collection can not contain only numbers.
...
...
for (const key in data) {
// Resolve each collection and remove it from document data.
if (isCollection(data[key], [...path, key])) {
// Remove a collection from the document data.
delete documentData[key];
// If key is a number it means that it is not a collection
// The variable id in this case will be the name of the document field or you
// can generate randomly
let id = !isNaN(key) ? data[key].id : key;
// Resolve a colleciton.
resolve(data[key], [...path, id]);
}
}
...
...
Note.: Changes in #Maciej Caputa's code
this library you can use https://www.npmjs.com/package/csv-to-firestore
install this library with this command npm i -g csv-to-firestore
put this script inside your ./scripts
module.exports = {
path: "./scripts/palabrasOutput.csv",
firebase: {
credential: "./scripts/serviceAccount.json",
collection: "collectionInFirestore",
},
mapper: (dataFromCSV) => {
return dataFromCSV;
},
};
and run:
csv-to-firestore --config ./scripts/csvToFirestore.js
btw: Generate new private key (it's a file in Service account section in firestore)
put that file in /scripts folder and rename serviceAccount.json