top of page

Cloud Firestore Development (With Typescript)



The purpose of this article is to show you how I added the calls between the React application and Cloud Firestore, and also how I added unit tests to verify the logic.


First, let’s define our Server interface. We are doing this so that when we are using a Server instance in the app, we can hide any methods that should not be used outside of the Server class.

import{DocumentData,DocumentReference}from'@firebase/firestore-types'
exportinterfaceServer
{
    save: (data: NapChartData,title: string,description: 
            string)=>Promise<string>
    loadChart: (chartid: string)=>Promise<ChartData>
    sendFeedback: (feedback: 
            string)=>Promise<DocumentReference<DocumentData>>
    addEmailToFeedback: (email: any,feedbackId: any)=>Promise<any>
    loadChartsForUser: (userId: number)=>Promise<any>
 }

Now, here we have our implementation of the Server class.


I know I could use React’s Context API, but I found it simpler to just make a Singleton object. I want it to be a Singleton because I want the Firebase object to be initialized only once, which will happen as soon as we access this class for the first time.

import{Server} from'./Server'
import{DocumentData,DocumentReference} from '@firebase/firestore-types'
import * asfirebase from 'firebase/app'
import{NapChart} from'../components/Editor/napchart'
importApp from'../components/Editor/Editor'
import{FireObject} from '@testing-library/react'
import{FirebaseFirestore,QuerySnapshot} from'@firebase/firestore-types'
import{NapchartData} from 'napchart'
import{AuthProvider} from '../auth/auth_provider'
import{firebaseAuthProvider} from '../auth/firebase_auth_provider'
import{ChartData} from './ChartData'
require('firebase/firestore')

/*This class contains all functionality for interacting with Firebase Firestore.
*/

export interface FirebaseServerProps
{
    testApp?: App | any
    authProvider: AuthProvider
    }

export class FirebaseServer implements Server{
    private static instance: FirebaseServer
    private db!: FirebaseFirestore 
    static getInstance(): Server{
        if(!FirebaseServer.instance){
            console.error('Fatal error. Firebase app not initialized.')
            }
            return FirebaseServer.instance
         }
     
     static init(props: FirebaseServerProps)
     {
     if(!FirebaseServer.instance){
         // If this is not a unit test, initialize Firebase normally.
         FirebaseServer.instance=new FirebaseServer()
         if(props.testApp==undefined||props.testApp==null){
         // This object is generated by the Firestore console.
             const firebaseConfig={
                 apiKey: 'HIDDEN_KEY',
                 authDomain: 'HIDDEN_AUTH_DOMAIN',
                 databaseURL: 'HIDDEN_DATABASE_URL',
                 projectId: 'HIDDEN_PROJECT_ID',
                 storageBucket: 'HIDDEN_STORAGE_BUCKET',
                 messagingSenderId: 'HIDDEN_MESSAGING_SENDER_ID',
                 appId: 'HIDDEN_APP_ID',
                 measurementId: 'HIDDEN_MEASUREMENT_ID',
                 }                            
             const firebaseApp=firebase.initializeApp(firebaseConfig)        
             FirebaseServer.instance.db=firebase.firestore(firebaseApp)
                 // If we are testing locally, use the emulator.
             if(window.location.hostname=='localhost'){
                 FirebaseServer.instance.db.settings(
                 {
                     ssl: false,
                     host: 'localhost:8080',
                  }
                 )}
               }   else    {
           FirebaseServer.instance.db=firebase.firestore(props.testApp)
            }
            }    else    {
                console.error('FATAL ERROR: You should only call init() 
                once, because this is a singleton.')}}

        save(data: NapchartData,title: string,description: string): Promise<string>{
            if(firebaseAuthProvider.isUserSignedIn()){
                const userId=firebaseAuthProvider.getUserId()
                if(userId !== undefined){
                // TODO: Fix this once login is implemented.
                return this.db
                    .collection('charts')
                    .doc(userId)
                    .set({ data })
                    .then(()=>'chartid')
                  }
            }
                 
                 return this.getUniqueChartId().then((chartid)=>{
                     console.error('generated unique id')
                     console.error(chartid)
                     return this.db
                     .collection('charts')
                     .doc(chartid)
                     .set({          
                         title,          
                         description,          
                         data,})
                      .then((docRef)=>{
                          return chartid
                          })
                      })
                  }
                  
        private generateRandomId(): string{
            const alphabet='abcdefghijklmnopqrstuwxyz0123456789'
            let id=''
            for(vari=0;i<5;i++){
            id+=alphabet.charAt(Math.floor(Math.random() * 
                    alphabet.length))
            }
            return id
            }
         
         async getUniqueChartId(): Promise<string>{
         let id=this.generateRandomId()
         while(await this.isIdAlreadyTaken(id)){
             id=this.generateRandomId()
          }
             return id
         }
         
         private isIdAlreadyTaken(id: string): Promise<boolean>{
             return FirebaseServer.instance.db
                 .collection('charts')
                 .doc(id)
                 .get()
                 .then((doc)=>{
                 return doc.exists}
                 )
           }
                 
         loadChart(chartid: string): Promise<ChartData>{
             return this.db
               .collection('charts')
               .doc(chartid)
               .get()
               .then((snapshot)=>{
                  const result: any=snapshot.data()
                  if(result===undefined){
                     return Promise.reject('Chart with ID '+chartid+' 
                         not found.')}
                   const chartData: ChartData=new ChartData(chartid,result.title,result.description,result.data)
                   return Promise.resolve(chartData)
                   })
                   }
         
       sendFeedback(feedback: string): Promise<DocumentReference<DocumentData>>{
         if(feedback.length==0){
             return Promise.reject('Feedback is empty.')}
         
         return this.db
             .collection('feedback')
             .add({        
                 feedback,}
              )
             .then((ref)=>ref)}
       
       addEmailToFeedback(email: string,feedbackDocRef: DocumentReference<DocumentData>): Promise<any>{
           return feedbackDocRef.get().then((snapshot)=>{
               const result: any=snapshot.data()
                   if(result===undefined){
                   return Promise.reject('Feedback document not 
                           found.')}
                   result.email=email
                   return feedbackDocRef.set(result)
                 })
          }
  }

And, as promised, here are the unit tests.


Note that for these to pass, you need to:

  1. Install the Firebase CLI.

  2. In the terminal, use `firebase emulators:start — only firestore` to start the local DB.

  3. Finally, I used yarn test to run my tests. I’m planning on setting up a CI/CD pipeline and run these tests in that pipeline when a pull request is made.

import * asf irebase from '@firebase/testing'
import {FirebaseServer} from '../FirebaseServer'
import {Server} from'../Server'
import {assert} from'console'
import {FirebaseFirestore,DocumentData,DocumentReference}from'@firebase/firestore-types'
import {firebaseAuthProvider,FirebaseAuthProvider}from'../../auth/firebase_auth_provider'
import {AuthProvider} from'../../auth/auth_provider'
import {napChartMock} from'../../components/Editor/__mocks__/napchart.mock'
import {ChartData} from'../ChartData'

const mockAuthProvider: AuthProvider={
    isUserSignedIn: ()=>false,
    getUserId: ()=>undefined,
}

const testApp: any=firebase.initializeTestApp({
    projectId: 'HIDDEN_PROJECT_ID',
    auth: {uid: 'MY_UID',email: 'alice@example.com'},
 })

FirebaseServer.init({testApp: testApp,authProvider: mockAuthProvider})
const server=FirebaseServer.getInstance()

afterEach(()=>{
    firebase.clearFirestoreData({projectId: 'HIDDEN_PROJECT_ID'})
 })
    
test('Save should return chart ID. Load chart should load chart successfully.',async()=>{
    const chartid=await     
        server.save(napChartMock.data,'testTitle','testDescription')
    const chart: ChartData=await server.loadChart(chartid)
    expect(chart.chartid).toBe(chartid)
    expect(chart.title).toBe('testTitle')
})
    
test('If chart ID is not found, promise should be rejected.',async()=>{
    const error=await server.loadChart('some fake 
        id').catch((err)=>err)
    expect(error).toBe('Chart with ID some fake id not found.')
})
    
test('Send feedback',async()=>{
    const feedbackString='This is some feedback.'
    const docRef: DocumentReference<DocumentData>=await 
            server.sendFeedback(feedbackString)
    const loadedDoc=await 
             docRef.get().then((snapshot)=>snapshot.data())
    expect({feedback: feedbackString}).toEqual(loadedDoc)
})
    
test('Attach email to feedback', async()=>{
    const feedbackString='This is some feedback.'
    const email: String='someEmail@gmail.com'
    const docRef: DocumentReference<DocumentData>=await 
            server.sendFeedback(feedbackString)
    await server.addEmailToFeedback(email,docRef)
    const loadedDoc=await 
            docRef.get().then((snapshot)=>snapshot.data())
    expect({feedback: feedbackString, email }).toEqual(loadedDoc)
 })

Finally, here’s an example of how we would use the class in a React component.

Notice that because `FirebaseServer.getInstance()` returns a Server object and not a FirebaseServer object, we only have access to the methods defined in the Server interface.


The project is still a work in progress, but if you want to see the rest of the app, here is the GitHub link.

this.setState({loading: true})
    FirebaseServer.getInstance()
        .loadChart(this.state.chartid)
        .then((chartData)=>{
            this.setState({
                initialData: chartData.data,
                loading: false,
        })
  })

Hopefully this helps you if you want to use Cloud Firestore in your project and you’re not sure how to set up the code or write unit tests. Feel free to ask any questions in the comments!


Source: Medium


The Tech Platform

0 comments

Comentarios


bottom of page