Skip to content

Instantly share code, notes, and snippets.

@jhahspu
Last active June 7, 2021 12:03
Show Gist options
  • Save jhahspu/667ded517b0ea672bfc6471d82fd77a3 to your computer and use it in GitHub Desktop.
Save jhahspu/667ded517b0ea672bfc6471d82fd77a3 to your computer and use it in GitHub Desktop.
Firebase
// Assume we have the following data in the Database:
{
  "name": {
    "first": "Ada",
    "last": "Lovelace"
  }
}

// Test for the existence of certain keys within a DataSnapshot
var ref = firebase.database().ref("users/ada");
ref.once("value")
  .then(function(snapshot) {
    var a = snapshot.exists();  // true
    var b = snapshot.child("name").exists(); // true
    var c = snapshot.child("name/first").exists(); // true
    var d = snapshot.child("name/middle").exists(); // false
  });

Create + Update Product

Cloud Firestore

// SERVICE
getById(id: string) {
  return this.db.collection(this.dbPath).doc(id);
}

create(product: Product): any {
  return this.products$.add({...product});
}

updateProduct(product: Product, id: string):any {
  return this.products$.doc(id).update(product);
}


// PRODUCT FORM -- CONSTRUCTOR
this.id = this.route.snapshot.paramMap.get('id');
  if (this.id) {
  //  this.productService.getById(this.id).valueChanges().subscribe(data => {
  //    this.form.setValue(data);
  //  });
  this.productService.getById(this.id).valueChanges().pipe(take(1)).subscribe(data => {
    this.form.setValue(data);
  })
  }

this.form = this.formBuilder.group(
  {
    title: new FormControl('', Validators.required),
    price: new FormControl('', Validators.compose([Validators.required, Validators.min(0)])),
    category: new FormControl('', Validators.required),
    imageUrl: new FormControl('', Validators.required)
  }
);


// PRODUCT FORM
save(product) {
  this.submitted = true;
  if (this.form.invalid) {
      return;
  }
  if (this.id) {
    this.productService.updateProduct(product, this.id);
    this.router.navigate(['admin/products']);
  } else {
    this.productService.create(product);
    this.router.navigate(['admin/products']);
  }
}

Metods examples

ADD Fire to project

ng add @angular/fire

Setup in app.module.ts

import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireModule } from '@angular/fire';

import: [
  ...
  AngularFireModule.initialize(environment.firebase),
  AngularFirestoreModule,
  ...
]

The Service

const getObservable = (collection: AngularFirestoreCollection<Task>) => {
  const subject = new BehaviorSubject([]);
  collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
    subject.next(val);
  });
  return subject;
};

Initialize a firestore collection

  • the valueChanges method return an Observable that will emit new value every time the data changes
  • also configure firestore to use 'id' as an idField
collectionA = getObservable(this.store.collection('collectionA'));
collectionB = getObservable(this.store.collection('collectionB'));

// OR
collectionB = this.store.collection('collectionB').valueChanges({idField: 'id'});

Inject an instance of the AngularFirestore provider

constructor(
  // inject an instance of the AngularFirestore provider
  private store: AngularFirestore
) {}

Move doc from collection A to B

this.store.firestore.runTransaction(() => {
  return Promise.all([
    this.store.collection(collectionA).doc(id).delete(),
    this.store.collection(collectionB).add(item) // item is an interface
  ]);
});

Create New Doc

this.store.collection('[collection_name]').add(item);

Update Doc

this.store.collection(collectionA).doc(id).update(item);

Delete Doc by ID

this.store.collection(collectionA).doc(id).delete();

Use '| async' with template variable

*ngIf="(collectionA | async)"
*ngFore="let c of collentionA | async"

Observable

import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/combineLatest';

let hello = Observable.of('hello');
let world = Observable.of('world');
hello.combineLatest(world)

import 'rxjs/add/observable/combineLatest';
Observable.combineLatest(hello, world);

Subscriptions

cats: FirebaseListObservable<any[]>

dogs: Array<any[]>;
subscription: Subscription;

ngOnInit() {
  this.cats = this.db.list('/cats');

  this.subscription = this.db.list('/dogs').subscribe(dogs => {
    this.dogs = dogs;
  });
}

ngOnDestroy() {
  this.subscription.unsubscribe();
}
<div *ngFor="let cat of cats | async">{{cat.name}}</div>

<div *ngFor="let dog of dogs">{{dog.name}}</div>

Map Observables

catCount: Observable<number>;
dogName: Observable<string>;

ngOnInit() {
  this.catCount = this.db.list('/cats')
                  .map(cats => {
                    return cats.length
                  })

  this.dogName = this.db.object('/dogs/dRSsdfSDFSwS')
                  .map(dog => {
                    return dog.name
                  })
}
there are {{catCount | async}} cats in this list

the dog's name is {{dogName | async}}

switchMap

human: FirebaseObjectObservable<any>;
dogs: Observable<any[]>;

ngOnInit() {
  this.human = this.db.object('/humans/jeff')
  this.dogs = this.human.switchMap(human => {
    return this.db.list('/dogs', {
      query: {
        orderByChild: 'owner',
        equalTo: human.name
      }
    })
  })
}
<div *ngFor="let dog of dogs | async">{{dog.name}}</div>

Combine Observables

dog: FirebaseObjectObservable<any>;
cat: FirebaseObjectObservable<any>;

animals: Observable<any[]>

ngOnInit() {
  this.cat = this.db.object('/cats/asdddSHGHfghfghf');
  this.dog = this.db.object('/dogs/TsdfsSHGHfghfghf');

  this.animals = Observable.combineLatest(this.cat, this.dog);
}
<div *ngFor="let animal of animals | async">{{animal.name}}</div>

Behavior Subject

dog: FirebaseListObservable<any>;

currentDog = new BehaviorSubjet(null);

ngOnInit() {
  this.dogs = this.db.list('/dogs');
}

changeDog(dog) {
  this.currentDog.next(dog)
}
<div *ngFor="let dog of dogs | async" (click)="changeDog(dog)">{{dog.name}}</div>


<h2>Current Dog</h2>
<div>{{ (curentDog | async)?.name }}</div>

Cloud Firestore Rules

service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userId} {
      allow write, read: if isOwner(userId);
    }
    
    match /categories/{category} {
      allow read: if true;
    }
    
    match /products/{products} {
      allow read: if true;
      allow write, create, update, delete: if isAdmin();
    }

    // Reusable function to determine document ownership
    function isOwner(userId) {
      return request.auth.uid == userId
    }
    
    // If User is Admin
    function isAdmin() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
    }
  }

Realtime Database Rules

.read

  • Describes if and when data is allowed to be read by users. .write
  • Describes if and when data is allowed to be written. .validate
  • Defines what a correctly formatted value will look like, whether it has child attributes, and the data type. .indexOn
  • Specifies a child to index to support ordering and querying.

Firebase Security

No security

// No Security
{
  β€œrules”: {
    β€œ.read”: true,
    β€œ.write”: true
  }
}

Full Security

// No Security
{
  β€œrules”: {
    β€œ.read”: false,
    β€œ.write”: false
  }
}

Only authenticated

// Only authenticated users can access/write data
{
  β€œrules”: {
    β€œ.read”: β€œauth != null”,
    β€œ.write”: β€œauth != null”
  }
}

User Authentication from a particular domain

// Only authenticated users from a particular domain (example.com) can access/write data
{
  β€œrules”: {
    β€œ.read”: β€œauth.token.email.endsWith(β€˜@example.com’)”,
    β€œ.write”: β€œauth.token.email.endsWith(β€˜@example.com’)”
  }
}

User Data Only

  • gives each authenticated user a personal node at /post/$user_id where $user_id is the ID of the user obtained through authentication
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    }
  }
}

Validates user is moderator from different database location

// Validates user is moderator from different database location
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.write”: β€œroot.child(β€˜users’).child(β€˜moderator’).val() === true”
      }
    }
  }
}

Validates string datatype and length range

// Validates string datatype and length range
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.validate”: β€œnewData.isString() 
          && newData.val().length > 0
          && newData.val().length <= 140”
      }
    }
  }
}

Checks presence of child attributes

// Checks presence of child attributes
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.validate”: β€œnewData.hasChildren([β€˜username’, β€˜timestamp’])”
      }
    }
  }
}

Validates timestamp

// Validates timestamp is not a future value
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œtimestamp”: { 
          β€œ.validate”: β€œnewData.val() <= now” 
        }
      }
    }
  }
}

Prevents Delete or Update

// Prevents Delete or Update
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.write”: β€œ!data.exists()”
      }
    }
  }
}

Prevents only Delete

// Prevents only Delete
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.write”: β€œnewData.exists()”
      }
    }
  }
}

Prevents only Delete

// Prevents only Update
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.write”: β€œ!data.exists() || !newData.exists()”
      }
    }
  }
}

Prevents Create and Delete

// Prevents Create and Delete
{
  β€œrules”: {
    β€œposts”: {
      β€œ$uid”: {
        β€œ.write”: β€œdata.exists() && newData.exists()”
      }
    }
  }
}

Setup

ng new [app_name]

npm i --save firebase

ng add @angular/fire

npm i --save bootstrap

Configure Firebase & copy firebaseConfig to environments

app.module.ts

// Fire
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireStorageModule } from '@angular/fire/storage';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { environment } from 'src/environments/environment';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    AngularFireModule.initializeApp(environment.firebaseConfig),
    AngularFirestoreModule, // firestore
    AngularFireAuthModule, // auth
    AngularFireStorageModule, // storage
    AngularFireDatabaseModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})

Deploying app to firebase

npm i -g firebase-tools

firebase --version

firebase login

firebase init
  -> select "hosting"
  -> select [app_name] from list
  -> public = dist/[app_name]

firebase.json

{
  "hosting": {
    "public": "dist/organicshop",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

Firestore Rules

service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userId} {
        allow write, read: if isOwner(userId);
    }

    // Reusable function to determine document ownership
    function isOwner(userId) {
        return request.auth.uid == userId
    }
  }
}

Build prod && Deploy

ng build --prod

firebase deploy

auth.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { User } from 'src/app/interfaces/user';

import firebase from 'firebase/app';
import { AngularFireAuth } from '@angular/fire/auth';
import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore';

import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';

@Injectable({  providedIn: 'root' })

export class AuthService {

  user$: Observable<User>;

  constructor(
    private afAuth: AngularFireAuth,
    private afs: AngularFirestore,
    private router: Router
  ) {

    this.user$ = this.afAuth.authState.pipe(
      switchMap(user => {
        if (user) {
          return this.afs.doc<User>(`users/${user.uid}`).valueChanges();
        } else {
          return of(null);
        }
      })
    )

  }

  async googleSignin() {
    const provider = new firebase.auth.GoogleAuthProvider();
    const credential = await this.afAuth.signInWithPopup(provider);
    return this.updateUserData(credential.user);
  }

  private updateUserData({ uid, email, displayName, photoURL }: User) {
    // sets user data to firestore on login
    const userRef: AngularFirestoreDocument<User> = this.afs.doc(`users/${uid}`);

    const data = {
      uid,
      email,
      displayName,
      photoURL
    }

    return userRef.set(data, { merge: true });
  }

  async signOut() {
    await this.afAuth.signOut();
    return this.router.navigate(['/']);
  }
}

login component

import { Component } from '@angular/core';
import { AuthService } from 'src/app/services/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {

  constructor(public auth: AuthService) { }

}

login html

<!-- template will replace this div -->
<div *ngIf="auth.user$ | async; then authenticated else guest">
  
</div>

<!-- User NOT logged in -->
<ng-template #guest>
<h3>Howdy, GUEST</h3>
<p>Login to get started...</p>

<button
  (click)="auth.googleSignin()"
  class="btn btn-primary">
  Login with Google
</button>

</ng-template>


<!-- User logged in -->
<ng-template #authenticated>
<div *ngIf="auth.user$ | async as user">
  <h3>Howdy, {{ user.displayName }}</h3>
  <img  [src]="user.photoURL">
  <p>UID: {{ user.uid }}</p>
  <button (click)="auth.signOut()">Logout</button>
</div>
</ng-template>

Tips & Tricks


One to many

async function oneToMany() {
  const { uid } = await auth.currentUser;
  const ref = db.collection('accounts').doc(uid).collection('orders');
  return ref.add({ someData });
}

// Group subcollections for unified query
db.collectionGroup('orders').orderBy('date').where('data', '==', [something])

Many to many

async function manyToMany(){
  const { uid, displayName } = await auth.currentUser;
  const ref = db.collection('chats');
  const members = {
    [uid]: displayName
  };
  ref.set({members}, {merge: true});
}

const query = db.collection('chats').orderBy('members.[memeberid]')

Realtime data

// single read
query.get()

// realtime
query.onSnapshot(q=> q.docChanges().map(change => change.doc/newIndex/oldIndex/type))

Offline Firestore settings

db.enablePersistence({synchronizeTabs: true})

Emojis - utf-8 characters

const ref = db.collection('foods').doc('broccoli')
ref.set({name: 'πŸ₯¦'})
const query = ref.where('name', '==', 'πŸ₯¦')

Query Similar

const start = 'The Fast and The Furious';
const end = start + '~';

movies
  .orderBy('title')
  .startAt(start)
  .endAt(end)

Auto-index compound queries

query
  .where('game', '==', 'completed')
  .where('score', '==', 123)
  .orderBy('time')

Write to lists with array union or remove

const ref = db.collection('recipes')
ref.add({
  ingredients: [
    'πŸ¦‘',
    'πŸ₯‘',
    'πŸ‹'
  ]
});
ref.update({
  ingredients: firebase.firestore.FieldValue.arrayUnion('🍍')
  // or
  ingredients: firebase.firestore.FieldValue.arrayRemove('πŸ¦‘')
})

ref.where('ingredients', 'array-contains', 'πŸ₯‘')

Pipeline multiple reads into a concurrent request

const ids = [
  'a',
  'b',
  'c'
];
const readIds = (ids) => {
  const reads = ids.map(id => 
    db.collection('foo').doc(id).get()
  );
  return Promise.all(reads);
}

Timestamps => serverTimestamp();

const { serverTimestamp, increment } = firebase.firestore.FieldValue;

ref.update({
  timestamp: serverTimestamp(),
  counter: increment(1)
})

Batch for atomic writes

const batch = db.batch();

batch.set(game, {score});
batch.set(user, {lifetimeScore});

batch.commit();

File storage

  • use coldline buckets to save $ on storage
  • storage location + download url
const storageRef = storage.refFromURL('gs://awesome-dev.appspot.com/giphy.gif');
const storageRef = storage.refFromURL('');

List all files from bucket

async function allFiles() {
  const dir = storage.ref('images/sample123')
  const allFiles = await storageRef.listAll();
}

upload files

const storageRef = storage.reg('image.png');
const task = storageRef.put(someBlob);
task.on(firebase.storage.TaskEvent.STATE_CHANGED, e => {
  const progress = e.bytesTransferred / e.totalBytes;
})

upload concurrently

const files = [...Array(20).keys()];
for (const f of files) {
  storage.ref(f).put(someFile, {customMetadata: {userId, platform}})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment