Welcome to Firebase Fundamentals

NoSQL database which follow hierarchy way collection/documents/collection/documents

Press Space for next page

Table of contents

  1. Welcome to Firebase Fundamentals

  2. Table of contents

  3. Installation

  4. Firebase Configuration

  5. Initializing Firebase

  6. Enable offline cache

  7. Local development

  8. Firestore

  9. Firestore Structure

  10. Reference Collections and Documents

  11. Reference Subcollections and Subdocuments

  12. Queries

  13. Get data

  14. Add data

  15. Update data

  16. Add or Update data

  17. Delete data

  18. Get realtime doc data

  19. Get realtime collection data

  20. Atomic operations

  21. Transactions

  22. Firebase Auth

  23. Sign up

  24. Sign in

  25. Check user state

  26. Sign out

  27. Link anonymous user

  28. Security Rules

  29. Firebase Storage

  30. List files

  31. Upload file

  32. Download file

  33. Delete file

  34. Admin SDK

  35. Cloud Functions

  36. Change deploy path

  37. Also …

1 / 37

Welcome to Firebase Fundamentals

NoSQL database which follow hierarchy way collection/documents/collection/documents

Press Space for next page
1

Table of contents

  1. Welcome to Firebase Fundamentals

  2. Table of contents

  3. Installation

  4. Firebase Configuration

  5. Initializing Firebase

  6. Enable offline cache

  7. Local development

  8. Firestore

  9. Firestore Structure

  10. Reference Collections and Documents

  11. Reference Subcollections and Subdocuments

  12. Queries

  13. Get data

  14. Add data

  15. Update data

  16. Add or Update data

  17. Delete data

  18. Get realtime doc data

  19. Get realtime collection data

  20. Atomic operations

  21. Transactions

  22. Firebase Auth

  23. Sign up

  24. Sign in

  25. Check user state

  26. Sign out

  27. Link anonymous user

  28. Security Rules

  29. Firebase Storage

  30. List files

  31. Upload file

  32. Download file

  33. Delete file

  34. Admin SDK

  35. Cloud Functions

  36. Change deploy path

  37. Also …

2

Installation

add client SDK and firebase-tools

# add client SDK to project
pnpm i firebase # or use `firebase-lite` which does'nt have real-time updates coz it is REST wrapper

# add firebase-tools then login
pnpm install -g firebase-tools
firebase login
3

Firebase Configuration

create project and copy them

they are not dangerous, it specify how client app should connect to firebase

const firebaseConfig = {
    apiKey: "AIzaSyBEDYLiNmyEg41MQ6aSKZl-jKgnHeU4xEk",
    authDomain: "fir-9-rush.firebaseapp.com",
    projectId: "fir-9-rush",
    storageBucket: "fir-9-rush.appspot.com",
    messagingSenderId: "322306232885",
    appId: "1:322306232885:web:04f99272f4dfa124ec177c"
};
4

Initializing Firebase

create instances of firebase services

import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
import { getAuth } from 'firebase/auth'
import { getStorage } from 'firebase/storage'

function initialize() {
	// init firebase app
	const firebaseApp = initializeApp(firebaseConfig)

	// init services
	// pass `firebaseApp` instance is optional, we can call following functions without it and from any place
	const firestore = getFirestore(firebaseApp)
	const auth = getAuth(firebaseApp)
	const storage = getStorage(firebaseApp)

	return { firebaseApp, firestore, auth, storage }
}
  
export const { firebaseApp, firestore: db, auth, storage } = initialize()
5

Enable offline cache

offline-first applications

enable single tab offline cache

export function initialize() {
	...
	enableIndexedDbPersistence(firestore)
		.catch((err) => {
			if (err.code == 'failed-precondition') alert("التطبيق مفتوح في اكثر من صفحة، فلن يعمل بدون اتصال انترنت");
			else if (err.code == 'unimplemented') console.log('indexedDB is not supported');
		});
	...
}

enable multi tab offline cache

export function initialize() {
	...
	enableMultiTabIndexedDbPersistence(firestore)
	...
}
6

Local development

create emulators
export FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099"
export FIREBASE_EMULATOR_HOST="localhost:9099"

firebase init emulators
# init firestore rules locally
firebase init firestore # don't forget disable opened access in access config

# run emulators with
firebase emulators:start [firestore,functions,auth,hosting]
# to keep emulator data
firebase emulators:start [firestore,functions,auth,hosting] [--import=./local] [--export-on-exist]
use them
export function initialize() {
	// init services
	const firestore = ...
	const auth = ...
	const storage = ...

	if (location.hostname === 'localhost') {
        // options are optional to hide warning message injected to page
        connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true })
        connectFirestoreEmulator(firestore, 'localhost', 8080)
    }
	
	return { firebaseApp, firestore, auth, storage }
}
7

Firestore

Flexible, scalable datastore

8

Firestore Structure

RDBMS organize data in tables and columns, firestore in collections and rows

What are Collections?

  • Collections are containers for documents.
  • Collections cannot directly contain other collections, but they can contain nested collections within documents.

What are Documents?

  • Documents are individual units of data stored in Firestore.
  • Each document contains fields, which map to values, and can contain subcollections.
9

Reference Collections and Documents

make references to work with collections and documents

reference collection

const booksRef = collection(db, 'books')

reference document

use doc without pass id will create new doc with generated id in client sdk

const bookRef = doc(db, 'books', 'fE60goLDBwLtQLPyvDUH')
const bookRefVariant1 = doc(db, 'books/fE60goLDBwLtQLPyvDUH'
const bookRefVariant2 = doc(booksRef, 'fE60goLDBwLtQLPyvDUH')
10

Reference Subcollections and Subdocuments

document contain properties (columns) and can contain subcollections (one-to-many relation)

reference subcollection

const chaptersRef = collection(db, 'books', 'fE60goLDBwLtQLPyvDUH', 'chapters')
const chaptersRefVariant1 = collection(db, 'books/fE60goLDBwLtQLPyvDUH/chapters')
const chaptersRefVariant2 = collection(booksRef, 'fE60goLDBwLtQLPyvDUH', 'chapters')

reference subcollection groups

to access all subcollections one time use collectionGroup

const expensesRef = collectionGroup(db, 'expenses')

reference subdocument

const chapterRef = doc(db, 'books', 'fE60goLDBwLtQLPyvDUH', 'chapters', 'fE60goLDBwLtQLPyvDUH')
const chapterRefVariant1 = doc(db, 'books/fE60goLDBwLtQLPyvDUH/chapters/fE60goLDBwLtQLPyvDUH')
const chapterRefVariant2 = doc(booksRef, 'fE60goLDBwLtQLPyvDUH', 'chapters/fE60goLDBwLtQLPyvDUH')
11

Queries

query is a subdocuments of collection

const expensesCol = collection(firestore, 'expenses')
const expensesQuery = query(
	expensesCol,
	// >, >=, <, <=, ==, !=
	where('release', '>', new Date('6/10/2023')),
	// in, not-in
	where('city', 'in', ['sanaa', 'aden']),
	// we can nest objects to 20 nest
	where('product.cost', '>=', 200),
	where('product.cost', '<=', 100),
	// category is an array: [...]
	where('category', '==', ['food', 'candy']),
	// if category contain 
	where('category', 'array-contains', 'food'),
	// if category contain any of
	where('category', 'array-contains-any', ['food', 'candy']),
	// for range functions, all of them depend on what orderBy
	orderBy('date', 'desc')
	// if use both it will get range of values
	startAt(new Date('1/1/2022'))
	endAt(new Date('/1/2022'))
	limit(100)
)
12

Get data

get data from firebase collection, query or document

get collection or query data

getDocs(booksRef)
    .then((snapshot) => {
		// console.log(snapshot.docs)
		let books = []
		snapshot.docs.forEach((doc) => {
			books.push({ ...doc.data(), id: doc.id })
		})
		console.log(books)
	})
	.catch(err => {
		console.log(err.message)
	})

get doc data

getDoc(bookRef)
	.then( doc => {
		console.log(doc.data(), doc.id)
	})
13

Add data

add doc with generated key locally

firebase document can hold up to 1MB

const addBookForm = document.querySelector(".add")
addBookForm.addEventListener('submit', e => {
	e.preventDefault();

	// `booksRef` is query or collection
	addDoc(booksRef, {
		title: addBookForm.title.value,
		author: addBookForm.author.value,
		createdAt: serverTimestamp() // don't trust local dates
	})
	.then(() => {
		addBookForm.reset()
	})
	.catch((err) => {
		console.error(err.message)
	})
})
14

Update data

see differences and apply delta, doc must exist

firebase can only reliably handle 1 write per second on a document

const updateBookForm = document.querySelector(".update")
updateBookForm.addEventListener('submit', e => {
	e.preventDefault()

	const docRef = doc(db, 'books', updateBookForm.id.value)
	updateDoc(docRef, {
		title: "updated title",
		pages: increment(10) // inc/dec without effort
		authors: ["updated author1", "updated author2"] // replace old authors by new array
		prints: arrayUnion("updated print1", "updated print2") // add new prints to original array
		chapters: arrayRemove("updated chapter1", "updated chapter2") // remove these chapters from original array
	})
	.then(() => {
		updateBookForm.reset()
	})
	.catch((err) => {
		console.error(err.message)
	})
})
15

Add or Update data

remove all doc data and add new, if doc is not exist create it

// if id is for non-exist doc, client will generate new id
const cityRef = doc(db, 'cities', 'BJ')

setDoc(cityRef, { 
	capital: true,
	long: false,
	title: true
},
// see differences and apply delta, if doc is not exist create it
{ merge: true }
);
16

Delete data

delete doc

const deleteBookForm = document.querySelector(".delete")
deleteBookForm.addEventListener('submit', e => {
	e.preventDefault()

	const docRef = doc(db, 'books', deleteBookForm.id.value)
	deleteDoc(docRef)
	.then(() => {
		deleteBookForm.reset()
	})
	.catch((err) => {
		console.error(err.message)
	})
})
17

Get realtime doc data

observe changes on docs

// call unsubBook() to stop observing
const unsubBook = onSnapshot(bookRef, 
	// snapshot callback re-run with every change
	(snapshot) => {
		// this is one doc
		console.log(snapshot)

		// this is doc data and id
		console.log(snapshot.data(), snapshot.id)

		// this use the metadata to detect from where doc came
		// { fromCach: true, hasPendingWrite: true }
		console.log(snapshot.metadata)
	},
	// optional error handler
	(errr) => console.error(errr.message)
)
18

Get realtime collection data

observe changes on collections

const unsubBooks = onSnapshot(booksRef,
	(snapshot) => {
		// this is an array of docs
		console.log(snapshot.docs)

		// you can itrate throgh and map waht you need
		console.log(snapshot.docs.map(d => d.data()))
    
		// document change types
		console.log(snapshot.docChanges[0])
		// { type: 'added', doc: {}, oldIndex: -1, newIndex: 1 }

		// create object indexed by changes
		const change = snapshot.docChanges.reduce((acc, curr) => { 
			acc[curr.type] = { ...curr.doc.data(), curr.doc.id }
		}, { added: [], modified: [], removed: [] })
		// { added: [{}, {}, ...], modified: [], removed: [] }
	},
	(err) => console.error(err.message)
)
19

Atomic operations

do operations as bulk

import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore'

let batch = writebatch(firestore)
let expensesCol = collection(firestore, 'users/rush/expenses')
batch.set(doc(expesnsesCol), {
	categories: ['food'],
	cost: 123.23,
	fate: serverTimestamp()
})
batch.update(doc(expesnsesCol, 'i-know-this-id'), {
	categories: ['transportation', 'fun'],
})
batch.delete(doc(expesnsesCol, 'i-know-this-id'))

try {
	await batch.commit()
} catch(eeror) {
	// was there a problem? if so, roll it all back
}
  • register all operations to batch, after register all of them commit the batch, if it faiel do all of them you can rollback
  • batch can contain up to 500 documernt per btach
  • batches rewrite on docs even if thier data updated while transaction work
20

Transactions

  • transactions care to data state, if data changed through transaction process it will re-run transaction
  • for example in a game user take award points, we should not update user points directly, I should register update in transaction, if I update directly points may change until updated points arrive, so it will override data, but with transaction it will lock this row until transaction complete
  • in firebase when transaction lock row it allow other operations update row data, but the operation which lock row will fail if row data changed
import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore'

let batch = writebatch(firestore)
let expensesCol = collection(firestore, 'users/rush/expenses')
batch.set(doc(expesnsesCol), {
	categories: ['food'],
	cost: 123.23,
	fate: serverTimestamp()
})
batch.update(doc(expesnsesCol, 'i-know-this-id'), {
	categories: ['transportation', 'fun'],
})
batch.delete(doc(expesnsesCol, 'i-know-this-id'))

try {
	await batch.commit()
} catch(eeror) {
	// was there a problem? if so, roll it all back
}
21

Firebase Auth

Authentication made easy

22

Sign up

signing users up

const signupForm = document.querySelector('.signup')
signupForm.addEventListener('submit', e => {
	e.preventDefault()

	const email = signupForm.email.value
	const password = signupForm.password.value

	createUserWithEmailAndPassword(auth, email, password)
		.then( cred => {
			// or maybe need to store cred.user.email
			console.log('user created: ', cred.user)
			signupForm.reset()
		})
		.catch(error => {
			console.log(error.message)
		})
})
23

Sign in

signing users in

const signinForm = document.querySelector('.signin')
signinForm.addEventListener('submit', e => {
	e.preventDefault()

	const email = signinForm.email.value
	const password = signinForm.password.value

	signInWithEmailAndPassword(auth, email, password)
        .then(cred => {
			console.log('user logged in: ', cred.user)
			signinForm.reset()
		})
		.catch(error => {
			console.log(error.message)
		})
})

there is more than method to auth, one of them signInAnonymously(auth) in this method it will give user access for some sessions only, we can later make user sign in and link it’s data with

24

Check user state

// check if user is authorized
console.log(auth.currentUser)

// check if user logged in or created
const unsubAuth = onAuthStateChanged(auth, user => { // if logged out, user will be null
    console.log('user status changed', user)
})
25

Sign out

signout users

const signoutButton = document.querySelector('.signout')
signoutButton.addEventListener('click', () => {
	signOut(auth)
		.then(() => {
			console.log('the user signed out')
			unsubBooks()
			unsubBook()
			unsubAuth()
		})
		.catch(error => {
			console.log(error.message)
		})
})
26

Link anonymous user

link current user to provider / signIn from provider

// it does'nt return result coz it redirect
linkWithRedirect(auth.currentUser, new GoogleAuthProvider())
signInWithRedirect(auth, new GoogleAuthProvider())

// receive result after redirect done (in onMount or useEffect function)
try {
    await getRedirectResult(auth)
} catche (error) {
    console.log(error)
}
27

Security Rules

firebase security rules has it’s own language coz this part work with every request to enable as fast experience as possible, security rules similar to express router.

// express
app.get('/users/rush', (req, res) {
    // code
	// if (some_expression) return allow()
})

app.get('/users/:id', (req, res) {
    const { id } = req.params
    // code
})
// `firestore.rules` (should added to `firebase.json`)
// match should be on document level not collection level
match /users/rush {
    // code
    // (allow|deny) permission: if some_expression
}

match /users/{uid} {
    // code
}

permissions can be

`read`: in read we can specify permissions precisely:
  • get: only allow get on this document
  • list: allow listing on the collection level
`write`:
  • create: first time creation document
  • update
  • delete
firebase deploy --only firebase:rules # deploy security rules only
28

Firebase Storage

Securely store and serve user-generated content

29

List files

const listRef = ref(storage, 'files');
listAll(listRef)
	.then((res) => {
		// All the prefixes under listRef, You may call listAll() recursively on them.
		res.prefixes.forEach((folderRef) => {
			console.log('folder: ' + folderRef.fullPath)
		});

		// All the items under listRef.
		res.items.forEach((itemRef) => {
			console.log('file: ' + itemRef.fullPath)
		});
	}).catch((error) => {
		// Uh-oh, an error occurred!
	});
30

Upload file

const addFileForm = document.querySelector(".addFile")
addFileForm.addEventListener('submit', e => {
    e.preventDefault()

    const file = addFileForm.file.files[0]
    const metadata = { contentType: file.type }; // 'image/png'
      
    // Upload file and metadata to the object 'images/mountains.jpg'
    const photoRef = ref(storage, `/files/${file.name}`) //const filesRef = photoRef.parent ref by another ref
    const uploadTask = uploadBytesResumable(photoRef, file, metadata);
      
    // Start upload task and listen for state changes, errors, and completion of the upload (try/catch/finally).
    uploadTask.on('state_changed',progresshandler, errorhandler, completeHandler);
})
const progressHandler =(snapshot) => {
	// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
	const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
	console.log('Upload is ' + progress + '% done');
	switch (snapshot.state) {
		case 'paused':
			console.log('Upload is paused'); // call uploadTask.pause()
			break;
		case 'running':
			console.log('Upload is running'); // call uploadTask.resume()
			break;
    	}
	}
const errorHandler = (error) => {
	// A full list of error codes is available at
	// https://firebase.google.com/docs/storage/web/handle-errors
	switch (error.code) {
		case 'storage/unauthorized':
        	// User doesn't have permission to access the object
		break;
		case 'storage/canceled':
        	// User canceled the upload by call uploadTask.caancel()
		break;
      
		// ...
      
		case 'storage/unknown':
        	// Unknown error occurred, inspect error.serverResponse
		break;
	}
}
31

Download file

get a download URL of a file

// or photoRef
getDownloadURL(uploadTask.snapshot.ref) 
	.then((url) => {
		const img = document.querySelector('.file');
		img.setAttribute('src', url);
	})
	.catch((error) => {
		switch (error.code) {
			case 'storage/object-not-found':
				// File doesn't exist
				break;
			case 'storage/unauthorized':
				// User doesn't have permission to access the object
				break;
			case 'storage/canceled':
				// User canceled the upload
				break;

			case 'storage/unknown':
				// Unknown error occurred, inspect the server response
				break;
		}
	});
32

Delete file

const deleteFileForm = document.querySelector(".deleteFile")
deleteFileForm.addEventListener('submit', e => {
	e.preventDefault()

	const fileRef = ref(storage, deleteFileForm.file.value)
	deleteObject(fileRef)
		.then(() => {
			deleteBookForm.reset()
			console.log("delete file sucessfully")
		})
		.catch((err) => {
			console.error(err.message)
		})
})
33

Admin SDK

all what we done with firestore and auth done with client SDK, client SDK restricted by rules, to ignore rules use admin SDK on server, I will explain use it with cloud functions section

34

Cloud Functions

more samples about cloud functions

firebase init functions
# write a cloud function
firebase deploy --only functions
// functions/index.js
const functions = require('firebase-functions')
exports.helloWorld = functions.https.onRequest((req, res) => {})
  • run as respones for an event
  • we need Admin SDK to build them which
  • simliar to client SDK but it have ultimate privilages
  • cloud functions have a cold start which mean function take time to boot up
  • cold start take between 500ms or hiegher depend on multiple factors
  • one of them node modules size
  • reserve amount of instances for cloud functions to be warmed always
35

Change deploy path

  • when deploy cloud function we get url path for this function
  • we can change it’s path from hosting configs like following with rewrites
// firebase config file
{
    "hosting": {
        "public": "...",
        "source": "....",
        "rewrites": [{
            "source": "**", // or spesfic path
            "function": "ssr"
        }]
    }
}
36

Also …

37