Skip to content

Instantly share code, notes, and snippets.

@Herve07h22
Last active February 21, 2024 09:27
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Herve07h22/ff340eaa80aee70b67490a3331a4a2d2 to your computer and use it in GitHub Desktop.
Save Herve07h22/ff340eaa80aee70b67490a3331a4a2d2 to your computer and use it in GitHub Desktop.
React-admin data & auth provider for Firebase and Firestore
// Implement a firestore data provider for react-admin
import {
GET_LIST,
GET_ONE,
CREATE,
UPDATE,
UPDATE_MANY,
DELETE,
DELETE_MANY,
GET_MANY,
GET_MANY_REFERENCE,
} from 'react-admin';
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_GET_PERMISSIONS } from 'react-admin';
// Firebase settings
const firebase = require("firebase");
// Required for side-effects
require("firebase/firestore");
firebase.initializeApp({
apiKey: 'YOUR_FIREBASE_API_KEY_THERE',
authDomain: 'your_project.firebaseapp.com',
databaseURL: 'https://your_project.firebaseio.com',
projectId: 'your_project',
storageBucket: 'gs://your_project.appspot.com',
messagingSenderId: 'YOUR_MESSAGING_ID'
});
// Initialize Cloud Firestore through Firebase
var db = firebase.firestore();
var storage = firebase.storage();
var storageRoot = storage.ref();
// Disable deprecated features
db.settings({
timestampsInSnapshots: true
});
/**
* Utility function to flatten firestore objects, since 'id' is not a field in FireStore
*
* @param {DocumentSnapshot} DocumentSnapshot Firestore document snapshot
* @returns {Object} the DocumentSnapshot.data() with an additionnal "Id" attribute
*/
function getDataWithId(DocumentSnapshot) {
var dataWithId = {}
// console.log('getDataWithId Id=', DocumentSnapshot.id)
if (DocumentSnapshot) {
dataWithId = {
id : DocumentSnapshot.id,
...DocumentSnapshot.data()
}
}
// console.log(dataWithId);
return dataWithId
}
/**
* Utility function to upload a file in a Firebase storage bucket
*
* @param {File} rawFile the file to upload
* @param {File} storageRef the storage reference
* @returns {Promise} the promise of the URL where the file can be download from the bucket
*/
async function uploadFileToBucket(rawFile, storageRef) {
console.log('Beginning upload');
return storageRef.put(rawFile)
.then( snapshot => {
console.log('Uploaded file !');
// Add url
return storageRef.getDownloadURL();
})
.catch( (error) => {
console.log(error);
throw new Error( { message: error.message_ , status:401} )
});
}
/**
* Utility function to create or update a file in Firestore
*
* @param {String} resource resource name, will be used as a directory to prevent an awful mess in the bucket
* @param {File} rawFile the file to upload if it is not already there
* @param {Function} uploadFile the storage reference
* @returns {Promise} the promise of the URL where the file can be download from the bucket
*/
async function createOrUpdateFile(resource, rawFile, uploadFile) {
console.log("Beginning upload file to storage bucket for file :", rawFile.name);
var storageRef = storageRoot.child(resource + '/' + rawFile.name);
// Check if the file already exist (same name, same size)
// In this case, no need to upload
return storageRef.getMetadata()
.then( metadata => {
console.log(metadata)
if ( metadata && metadata.size === rawFile.size) {
console.log("file already exists");
return storageRef.getDownloadURL();
} else {
return uploadFile(rawFile, storageRef)
}
})
.catch(
() => { console.log('File does not exist'); return uploadFile(rawFile, storageRef) }
);
}
/**
* Maps react-admin queries to Firebase
*
* @param {string} type Request type, e.g GET_LIST
* @param {string} resource Resource name, e.g. "posts"
* @param {Object} payload Request parameters. Depends on the request type
* @returns {Promise} the Promise for a data response
*/
export const firestoreProvider = (type, resource, params) => {
switch (type) {
case GET_LIST: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const filter = params.filter;
// query all the docs from the first to page*perPage
var query = field === 'id' ? db.collection(resource).limit(page*perPage) : db.collection(resource).limit(page*perPage).orderBy(field, order.toLowerCase());
if (filter) {
query = Object.keys(filter).reduce( (q, k) => q.where(k,"==",filter[k]) , query)
}
return query.get()
.then( QuerySnapshot => {
// slice the results
var totalCount = QuerySnapshot.docs.length;
var firstDocToDisplayCount = page === 1 ? 1 : Math.min( (page-1)*perPage , totalCount )
var firstDocToDisplay = QuerySnapshot.docs.slice(firstDocToDisplayCount-1);
return {
data: firstDocToDisplay.map( doc => getDataWithId(doc) ),
total: totalCount
}
})
}
case GET_ONE: {
return db.collection(resource)
.doc(params.id)
.get()
.then( doc => {
if (doc.exists) {
return { data : getDataWithId(doc) };
} else {
throw new Error({ message:'No such doc', status: 404});
}
})
.catch( error => {
throw new Error({ message:error, status:404});
});
}
case UPDATE:
case CREATE: {
// Check if there is a file to upload
var listOfFiles = Object.keys(params.data).filter( key => params.data[key].rawFile)
return Promise
.all(
listOfFiles.map( key => {
// Upload file to the Storage bucket
return createOrUpdateFile(resource, params.data[key].rawFile, uploadFileToBucket)
.then( downloadURL => {
return { key : key, downloadURL : downloadURL }
})
}))
.then(arrayOfResults => {
arrayOfResults.map( (keyAndUrl) => {
// Remove rawFile attr as it will raise an error when setting the data
delete params.data[keyAndUrl.key].rawFile;
// Set the url to get the file
params.data[keyAndUrl.key].downloadURL = keyAndUrl.downloadURL;
return params.data
});
if (type===CREATE) {
console.log("Creating the data");
return db.collection(resource)
.add(params.data)
.then( DocumentReference =>
DocumentReference
.get()
.then( DocumentSnapshot => { return { data : getDataWithId(DocumentSnapshot)} })
)
}
if (type===UPDATE) {
console.log("Updating the data");
return db.collection(resource)
.doc(params.id)
.set(params.data)
.then( () => { return { data : params.data } } )
}
});
}
case UPDATE_MANY: {
// Will crash if there is a File Input in the params
// TODO
return params.ids.map( id =>
db.collection(resource)
.doc(id)
.set(params.data)
.then( () => id )
)
}
case DELETE: {
console.log('Delete record id', params.id)
return db.collection(resource)
.doc(params.id)
.delete()
.then( () => { return { data : params.previousData } } )
}
case DELETE_MANY: {
return {
data : params.ids.map( id =>
db
.collection(resource)
.doc(id)
.delete()
.then( () => id )
)
}
}
case GET_MANY: {
// Do not use FireStore Ref because react-admin will not be able to create or update
// Use a String field containing the ID instead
return Promise
.all(params.ids.map( id => db.collection(resource).doc(id).get() ))
.then(arrayOfResults => {
return {
data : arrayOfResults.map( documentSnapshot => getDataWithId(documentSnapshot) )
}
});
}
case GET_MANY_REFERENCE: {
const { target, id } = params;
const { field, order } = params.sort;
return db.collection(resource)
.where(target, "==", id)
.orderBy(field, order.toLowerCase())
.get()
.then( QuerySnapshot =>
{
return {
data : QuerySnapshot.docs.map( DocumentSnapshot => getDataWithId(DocumentSnapshot) ),
total : QuerySnapshot.docs.length
}
}
);
}
default: {
throw new Error(`Unsupported Data Provider request type ${type}`);
}
}
};
/**
* Ultra simple authentication provider
*
* @param {string} type Request type, e.g AUTH_LOGI
* @param {Object} params Request parameters. Includes login and pwd
* @returns {Promise} the Promise for a data response
*/
export const firebaseAuthProvider = (type, params) => {
if (type === AUTH_LOGIN) {
const { username, password } = params;
return firebase.auth()
.signInWithEmailAndPassword(username, password)
.catch( (error) => { throw new Error({ message:error.message, status: 401}) } )
}
if (type === AUTH_LOGOUT) {
return firebase.auth().signOut()
.catch( (error) => { throw new Error({ message:error.message, status: 500}) } );
}
if (type === AUTH_CHECK) {
return firebase.auth().currentUser ? Promise.resolve() : Promise.reject();
}
if (type === AUTH_GET_PERMISSIONS) {
// Try to find a "user" collection and return the role attribute
return db.collection("user")
.doc(firebase.auth().currentUser)
.then( doc => {
if (doc.exists) {
return doc.data().role;
} else {
return 'user'
}
})
.catch( error => {
return 'user'
});
}
return Promise.resolve();
}
@dallashuggins
Copy link

Hello! Using this provider, does refreshing the app cause you to have to login again?

@Herve07h22
Copy link
Author

Herve07h22 commented Oct 8, 2019 via email

@zhouhao27
Copy link

Any example about how to use storage to save to storage bucket? Thanks.

@Herve07h22
Copy link
Author

Herve07h22 commented Oct 25, 2019 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment