The Angular Firebase Survival Guide Angularfirebase By Jeff Delaney
User Manual:
Open the PDF directly: View PDF .
Page Count: 87
Download | |
Open PDF In Browser | View PDF |
The Angular Firebase Survival Guide Build Angular Apps on a Solid Foundation with Firebase Jeff Delaney This book is for sale at http://leanpub.com/angularfirebase This version was published on 2018-05-23 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2017 - 2018 Jeff Delaney To my loving wife, you inspire me daily. Contents Introduction . . . . . . . . . . . . . . . . Why Angular? . . . . . . . . . . . . . Why Firebase? . . . . . . . . . . . . . Why Angular and Firebase Together? This Book is for Developers Who… . . Angular Firebase Starter App . . . . . Package Versions . . . . . . . . . . . . Watch the Videos . . . . . . . . . . . Join the Angular Firebase Slack Team . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 2 3 3 3 4 The Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 Top Ten Best Practices . . . . . . . . . . . . . . . . . . 1.2 Start a New App from Scratch . . . . . . . . . . . . . . 1.3 Separating Development and Production Environments 1.4 Importing Firebase Modules . . . . . . . . . . . . . . . 1.5 Deployment to Firebase Hosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 8 9 10 Cloud Firestore . . . . . . . . . . . . . . . . . . 2.0 Cloud Firestore versus Realtime Database 2.1 Data Structuring . . . . . . . . . . . . . . 2.2 Collection Retrieval . . . . . . . . . . . . 2.3 Document Retrieval . . . . . . . . . . . . 2.4 Include Document Ids with a Collection . 2.5 Add a Document to Collections . . . . . . 2.6 Set, Update, and Delete a Document . . . 2.7 Create References between Documents . . 2.8 Set a Consistent Timestamp . . . . . . . . 2.9 Use the GeoPoint Datatype . . . . . . . . 2.10 Atomic Writes . . . . . . . . . . . . . . . 2.11 Order Collections . . . . . . . . . . . . . 2.12 Limit and Offset Collections . . . . . . . 2.13 Querying Collections with Where . . . . 2.14 Creating Indices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 13 17 20 21 21 22 23 23 24 24 25 26 27 28 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS 2.15 Backend Firestore Security Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Realtime Database . . . . . . . . . . . . . . . . . . . . . . . . . 3.0 Migrating from AngularFire Version 4 to Version 5 . . . 3.1 Data Modeling . . . . . . . . . . . . . . . . . . . . . . . 3.2 Database Retrieval as an Object . . . . . . . . . . . . . . 3.3 Show Object Data in HTML . . . . . . . . . . . . . . . . 3.4 Subscribe without the Async Pipe . . . . . . . . . . . . . 3.5 Map Object Observables to New Values . . . . . . . . . 3.6 Create, Update, Delete a FirebaseObjectObservable data 3.7 Database Retrieval as a Collection . . . . . . . . . . . . 3.8 Viewing List Data in the Component HTML . . . . . . . 3.9 Limiting Lists . . . . . . . . . . . . . . . . . . . . . . . . 3.10 Filter Lists by Value . . . . . . . . . . . . . . . . . . . . 3.11 Create, Update, Delete Lists . . . . . . . . . . . . . . . 3.12 Catch Errors with Firebase Operations . . . . . . . . . 3.13 Atomic Database Writes . . . . . . . . . . . . . . . . . 3.14 Backend Database Rules . . . . . . . . . . . . . . . . . 3.15 Backend Data Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 35 36 38 39 40 41 42 43 44 45 45 46 47 47 49 User Authentication . . . . . . . . . . . . . . . . 4.1 Getting Current User Data . . . . . . . . . 4.2 OAuth Authentication . . . . . . . . . . . 4.3 Anonymous Authentication . . . . . . . . 4.4 Email Password Authentication . . . . . . 4.5 Handle Password Reset . . . . . . . . . . 4.6 Catch Errors during Login . . . . . . . . . 4.7 Log Users Out . . . . . . . . . . . . . . . 4.8 Save Auth Data to the Realtime Database 4.9 Creating a User Profile . . . . . . . . . . . 4.10 Auth Guards to Protect Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 53 54 55 56 57 57 58 59 60 Firebase Cloud Storage . . . . . . . . . . . . . . . . . . 5.1 Creating an Upload Task . . . . . . . . . . . . . . 5.2 Handling the Upload Task . . . . . . . . . . . . . 5.3 Saving Data about a file to the Realtime Database 5.4 Uploading a Single File . . . . . . . . . . . . . . 5.5 Delete Files . . . . . . . . . . . . . . . . . . . . . 5.6 Validate Files on the Frontend . . . . . . . . . . . 5.7 Upload Images in Base64 Format . . . . . . . . . 5.8 Validating Files on the Backend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 62 63 64 65 66 67 68 68 Firebase Cloud Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 CONTENTS 6.1 Initialize Cloud Functions in an Angular Project 6.2 Deploy Cloud Cloud Functions . . . . . . . . . 6.3 Setup an HTTP Cloud Function . . . . . . . . . 6.4 Setup an Auth Cloud Function . . . . . . . . . 6.5 Setup a Database Cloud Function . . . . . . . . 6.6 Setup a Firestore Cloud Function . . . . . . . . 6.7 Setup a Storage Cloud Function . . . . . . . . . Real World Combined Examples . . . . . . . 7.1 Auth with Firestore Custom User Data 7.2 Role-based Access Control . . . . . . . 7.3 Drag and Drop File Uploads . . . . . . 7.4 Firestore NoSQL Data Modeling . . . 7.5 Server Side Rendering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 71 72 74 74 75 76 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 80 80 80 81 Introduction The Angular Firebase Survival Guide is about getting stuff done. No effort is made to explicitly cover high level programming theories or low level Angular architecture concepts – there are plenty of other books for that purpose. The focus of this book is building useful app features. Each section starts with a problem statement, then solves it with code. Even for experienced JavaScript developers, the learning curve for Angular is quite steep. Mastering this framework is only possible by putting forth the effort to build your own features from scratch. Your journey will inevitably lead to moments of frustration - you may even dream about switching to VueJS or React - but this is just part of the learning process. Once you have Angular down, you will arrive among a rare class of developers who can build enterprise-grade realtime apps for web, mobile, and desktop. The mission of this book is to provide a diverse collection of snippets (recipes) that demonstrate the combined power of Angular and Firebase. The format is non-linear, so when a client asks you to build a “Custom Username” feature, you can jump to section 6.1 and start coding. By the end of the book, you will know how to authenticate users, handle realtime data streams, upload files, trigger background tasks with cloud functions, process payments, and much more. I am not sponsored by any of the brands or commercial services mentioned in this book. I recommend these tools because I am confident in their efficacy through my experience as a web development consultant. Why Angular? Angular can produce maintainable cross-platform JavaScript apps that deliver an awesome user experience. It’s open source, backed by Google, has excellent developer tooling via TypeScript, a large community of developers, and is being adopted by large enterprises. I see more and more Angular2+ job openings every week. Introduction 2 Why Firebase? Firebase eliminates the need for managed servers, scales automatically, dramatically reduces development time, is built on Google Cloud Platform, and is free for small apps. Firebase is a Backend-as-a-Service (BaaS) that also offers Functions-as-a-Service (FaaS). The Firebase backend will handle your database, file storage, and authentication – features that would normally take weeks to develop from scratch. Cloud functions will run background tasks and microservices in a completely isolated NodeJS environment. On top of that, Firebase provides hosting with free SSL, analytics, and cloud messaging. Furthermore, Firebase is evolving with the latest trends in web development. In March 2017, the platform introduced Cloud Functions for Firebase. Then in October 2017, the platform introduced the Firestore Document Database. I have been blown away at the sheer pace and quality of new feature roll-outs for the platform. Needless to say, I stay very busy keeping this book updated. Why Angular and Firebase Together? When you’re a consultant or startup, it doesn’t really matter what languages or frameworks you know. What does matter is what you can produce, how fast you can make it, and how much it will cost. Optimizing these variables forces you to choose a technology stack that won’t disappoint. Angular does take time to learn (I almost quit), but when you master the core patterns, development time will improve rapidly. Adding Firebase to the mix virtually eliminates your backend maintenance worries and abstracts difficult aspects of app development - including user authentication, file storage, push notifications, and a realtime pub/sub database. The bottom line is that with Angular and Firebase you can roll out amazing apps quickly for your employer, your clients, or your own startup. This Book is for Developers Who… • Want to build real world apps • Dislike programming books the size of War & Peace 3 Introduction • • • • Have basic JavaScript (TypeScript), HTML, and SCSS skills Have some Angular experience – such as the demo on Angular.io Have a Firebase or GCP account Enjoy quick problem-solution style tutorials Note for Native Mobile Developers I am not going to cover the specifics of mobile or desktop frameworks, such as Ionic, Electron, NativeScript. However, most of the core principles and patterns covered in this book can be applied to native development. Angular Firebase Starter App To keep the recipes consistent, most of the code examples are centered around a book sharing app where users can post information about books and their authors. The Firestarter App¹ provides an open-source live demo that shares much of its codebase with this book. Package Versions Change happens fast in the web development world. The package versions used in this book are as follows: • • • • • • Angular v6.0 Angular CLI v6.0 TypeScript v2.8 Firebase JS SDK v5.0 Firebase Functions v1.0 Cloud Firestore vBeta Everything else we build from the ground up. Watch the Videos The book is accompanied by an active YouTube channel that produces quick tutorials on relevant Angular solutions that you can start using right away. I will reference these videos throughout the book. https://www.youtube.com/c/AngularFirebase ¹https://github.com/codediodeio/angular-firestarter/blob/master/package.json Introduction 4 Join the Angular Firebase Slack Team My goal is to help you ship your app as quickly as possible. To facilitate this goal, I would like to invite you to join our Slack room dedicated to Angular Firebase development. We discuss ideas, best practices, share code, and help each other get our apps production ready. Get the your Slack invite link here². ²https://angularfirebase.com The Basics The goal of the first chapter is discuss best practices and get your first app configured with Angular 4 and Firebase. By the end of the chapter you will have solid skeleton app from which we can start building more complex features. 1.1 Top Ten Best Practices Problem You want a few guidelines and best practices for building Angular apps with Firebase. Solution Painless development is grounded in a few core principles. Here are my personal top ten ten tips for Angular Firebase development. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Learn and use the Angular CLI. Use AngularFire when working with Firebase. Create generic services to handle data logic. Create components/directives to handle data presentation. Unwrap Observables in the template with the async pipe when practical. Deploy your production app with Ahead-of-Time compilation to Firebase hosting. Always define backend database and storage rules on Firebase. Take advantage of TypeScript static typing features. Setup separate Firebase projects for development and production. Don’t be afraid to use Lodash to simplify JavaScript. 1.2 Start a New App from Scratch Problem You want start a new Angular project, using Firebase for the backend. Solution Let’s start with the bare essentials. (You may need to prefix commands with sudo). The Basics 1 2 3 6 npm install -g @angular/cli@latest npm install -g typescript npm install -g firebase-tools Then head over to https://firebase.com and create a new project. Setting up an Angular app with Firebase is easy. We are going to build the app with the Angular CLI, specifying the routing module and SCSS for styling. Let’s name the app fire. 1 2 ng new fire --routing --style scss cd fire Next, we need to get AngularFire2, which includes Firebase as a dependency. npm install angularfire2 firebase --save In the environments/environment.ts, add your credentials. Make sure to keep this file private by adding it to .gitignore. You don’t want it exposed in a public git repo. 7 The Basics 1 2 3 4 5 6 7 8 9 10 11 export const environment = { production: false, firebaseConfig: { apiKey: '', authDomain: ' ', databaseURL: ' ', projectId: ' ', storageBucket: ' ', messagingSenderId: ' ' } }; In the app.module.ts, add AngularFire2 to the imports. You only need to import the modules you plan on using. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import import import import import { { { { { AngularFireModule } from 'angularfire2'; AngularFireDatabaseModule } from 'angularfire2/database'; AngularFireAuthModule } from 'angularfire2/auth'; AngularFirestoreModule } from 'angularfire2/firestore'; AngularFireStorageModule } from 'angularfire2/storage'; import { environment } from '../environments/environment'; export const firebaseConfig = environment.firebaseConfig; // ...omitted @NgModule({ imports: [ BrowserModule, AppRoutingModule, AngularFireModule.initializeApp(environment.firebaseConfig), AngularFireDatabaseModule, AngularFireAuthModule, AngularFirestoreModule ], // ...omitted }) That’s it. You now have a skeleton app ready for development. 1 ng serve The Basics 8 1.3 Separating Development and Production Environments Problem You want maintain separate backend environments for develop and production. Solution It’s a good practice to perform development on an isolated backend. You don’t want to accidentally pollute or delete your user data while experimenting with a new feature. The first step is to create a second Firebase project. You should have two projects named something like MyAppDevelopment and MyAppProduction. Next, grab the API credentials and update the environment.prod.ts file. The Basics 1 2 3 4 5 6 7 8 9 9 export const environment = { production: true, firebaseConfig: { apiKey: "PROD_API_KEY", authDomain: "PROD.firebaseapp.com", databaseURL: "https://PROD.firebaseio.com", storageBucket: "PROD.appspot.com" } }; Now, in your app.module.ts, your app will use different backend variables based on the environment. 1 2 3 4 5 6 import { environment } from '../environments/environment'; export const firebaseConfig = environment.firebaseConfig; // ... omitted imports: [ AngularFireModule.initializeApp(firebaseConfig) ] Test it by running ng serve for development and ng serve --prod for production. 1.4 Importing Firebase Modules Problem You want to import the AngularFire2 or the Firebase SDK into a service or component. Solution Take advantage of tree shaking with AngularFire2 to only import the modules you need. In many cases, you will only need the database or authentication, but not both. Here’s how to import them into a service or component. The Basics 1 2 3 4 5 6 7 8 9 10 11 10 import { AngularFirestore } from 'angularfire2/firestore'; import { AngularFireDatabase } from 'angularfire2/database'; import { AngularFireAuth } from 'angularfire2/auth'; ///... component or service constructor( private afs: AngularFirestore, private db: AngularFireDatabase, private afAuth: AngularFireAuth ) {} You can also import the firebase SDK directly when you need functionality not offered by AngularFire2. Firebase is not a NgModule, so no need to include it in the constructor. 1 import * as firebase from 'firebase/app'; 1.5 Deployment to Firebase Hosting Problem You want to deploy your production app to Firebase Hosting. Solution It is a good practice to build your production app frequently. It is common to find bugs and compilation errors when specifically when running an Ahead-of-Time (AOT) build in Angular. During development, Angular is running with Just-In-Time (JIT) compilation, which is more forgiving with type safety errors. 1 ng build --prod Make sure you are logged into firebase-tools. 1 2 npm install -g firebase-tools firebase login The Basics Then initialize the project. 1 firebase init 1. 2. 3. 4. 1 Choose hosting. Change public folder to dist/ when asked (it defaults to public). Configure as single page app? Yes. Overwrite your index.html file? No. firebase deploy If all went well, your app should be live on the firebase project URL. 11 Cloud Firestore Firestore was introduced into the Firebase platform on October 3rd, 2017. It is a superior alternative (in most situations) to the Realtime Database that is covered in Chapter 3. What is Firestore? Firestore is a NoSQL document-oriented database, similar to MongoDB, CouchDB, and AWS DynamoDB. It works by storing JSON-like data into documents, then organizes them into collections that can be queried. All data is contained on the document, while a collection just serves as a container. Documents can contain their own nested subcollections of documents, leading to a hierarchical structure. The end result is a database that can model complex relationships and make multi-property compound queries. Unlike a table in a SQL database, a Firestore document does not adhere to a data schema. In other words, document-ABC can look completely different from document-XYZ in the same collection. However, it is a good practice to keep data structures as consistent as possible across collections. Firestore automatically indexes documents by their properties, so your ability to query a collection is optimized by a consistent document structure. The goal of this chapter is to introduce data modeling best practices and teach you how perform common tasks with Firestore in Angular. 2.0 Cloud Firestore versus Realtime Database Problem You’re not sure if you should use Firestore or the Realtime Database. Cloud Firestore 13 Solution I follow a simple rule - use Firestore, unless you have a good reason not to. However, if you can answer TRUE to ALL statements below, the Realtime Database might worth exploring. 1. You make frequent queries to a small dataset. 2. You do not require complex querying, filtering, sorting. 3. You do not need to model data relationships. If you responded FALSE to any of these statements, use Firestore. Realtime Database billing is weighted heavily on data storage, while Cloud Firestore is weighted on bandwidth. Cost savings could make Realtime Database a compelling option when you have high-bandwidth demands on a lightweight dataset. Why are there two databases in Firebase? Firebase won’t tell you this outright, but the Realtime Database has its share of frustrating caveats. Exhibit A: querying/filtering data is very limited. Exhibit B: nesting data is impossible on large datasets, requiring you to denormalize at the global level. Lucky for you, Firestore addresses these issues head on, which means you’re in great shape if you’re just starting a new app. Realtime Database is still around because it would be risky/impossible to migrate the gazillions of bytes of data from Realtime Database to Firestore. So Google decided to add a second database to the platform and not deal with the data migration problem. 2.1 Data Structuring Firestore Quick Start Video Lesson https://youtu.be/-GjF9pSeFTs Problem You want to know how to structure your data in Firestore. Cloud Firestore 14 Solution You already know JavaScript, so think of a collection as an Array and a document as an Object. What’s Inside a Document? A document contains JSON-like data that includes all of the expected primitive datatypes like strings, numbers, dates, booleans, and null - as well as objects and arrays. Documents also have several custom datatypes. A GeoPoint will automatically validate latitude and longitude coordinates, while a DocumentReference can point to another document in your database. We will see these special datatypes in action later in the chapter. Best Practices Firestore pushes you to form a hierarchy of data relationships. You start with (1) a collection in the root of the database, then (2) add a document inside of it, then (3) add another collection inside that document, then (4) repeat steps 2 and 3 as many times as you need. 1. Always think about HOW the data will be queried. Your goal is to make data retrieval fast and efficient. 2. Collections can be large, but documents should be small. 3. If a document becomes too large, consider nesting data in a deeper collection. Let’s take a look at some common examples. Example: Blog Posts and Comments In this example, we have a collection of posts with some basic content data, but posts can also receive comments from users. We could save new comments directly on the document, but would that scale well if we had 10,000 comments? No, the memory in the app would blow up trying to retrieve this data. In fact, Firestore will throw an error for violating the 1 Mb document size limit well before Cloud Firestore 15 reaching this point. A better approach is to nest a comments subcollection under each document and query it separately from the post data. Document retrieval is shallow - only the top level data is returned, while nested collections can be retrieved separately. 1 2 3 4 5 6 7 8 9 10 ++postsCollection postDoc - author - title - content ++commentsCollection commentDocFoo - text commentDocBar - text Example: Group Chat For group chat, we can use two root level collections called users and chats. The user document is simple - just a place to keep basic user data like email, username, etc. A chat document stores basic data about a chat room, such as the participating users. Each room has a nested collection of messages (just like the previous example). However, the message makes a reference to the associated user document, allowing us to query additional data about the user if we so choose. A document reference is very similar to a foreign key in a SQL database. It is just a pointer to a document that exists at some other location in the database. Cloud Firestore 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 ++usersCollection userDoc - username - email ++chatsCollection chatDoc - users[] ++messagesCollection messageDocFoo - text - userDocReference messageDocBar - userDocReference Example: Stars, Hearts, Likes, Votes, Etc. In the graphic above, we can see how the movies collection and users collection have a two-way connection through the middle-man stars collection. All data about a relationship is kept in the star document - data never needs to change on the connected user/movie documents directly. Having a root collection structure allows us to query both “Movie reviews” and “User reviews” independently. This would not be possible if stars were nested as a sub collection. This is similar to a many-to-many-through relationship in a SQL database. Cloud Firestore 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 17 ++usersCollection userDoc - username - email ++starsCollection starDoc - userId - movieId - value ++moviesCollection movieDoc - title - plot 2.2 Collection Retrieval Problem You want to retrieve a collection of documents. Solution A collection of documents in Firestore is like a table of rows in a SQL database, or a list of objects in the Realtime Database. When we retrieve a collection in Angular, the endgame is to generate an Observable array of objects [{...data}, {...data}, {...data}] that we can show the end user. The examples in this chapter will use the TypeScript Book interface below. AngularFire requires a type to be specified, but you can opt out with the any type, for example AngularFirestoreCollection . What is a TypeScript interface? An interface is simply a blueprint for how a data structure should look - it does not contain or create any actual values. Using your own interfaces will help with debugging, provide better developer tooling, and make your code readable/maintainable. Cloud Firestore 1 2 3 4 5 18 export interface Book { author: string; title: string: content: string; } I am setting up the code in an Angular component, but you can also extract this logic into a service to make it available (injectable) to multiple components. Reading data in AngularFire is accomplished by (1) making a reference to its location in Firestore, (2) requesting an Observable with valueChanges(), and (3) subscribing to the Observable. Steps 1 and 2: book-info.component.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore'; @Component({ selector: 'book-info', templateUrl: './book-info.component.html', styleUrls: ['./book-info.component.scss'] }) export class BookInfoComponent implements OnInit { constructor(private afs: AngularFirestore) {} booksCollection: AngularFireCollection ; booksObservable: Observable ; ngOnInit() { // Step 1: Make a reference this.booksCollection = this.afs.collection('books'); // Step 2: Get an observable of the data this.booksObservable = this.booksCollection.valueChanges(); } } Cloud Firestore 19 Step 3: book-info.component.html The ideal way to handle an Observable subscription is with the async pipe in the HTML. Angular will subscribe (and unsubscribe) automatically, making your code concise and maintainable. 1 2 3 4 5 6 Step 3 (alternative): book-info.component.ts It is also possible to subscribe directly in the Typescript. You just need to remember to unsubscribe to avoid memory leaks. Modify the component code with the following changes to handle the subscription manually. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Subscription } from 'rxjs'; /// ...omitted sub: Subscription; ngOnInit() { /// ...omitted // Step 3: Subscribe this.sub = this.booksObservable.subscribe(books => console.log(books)) } ngOnDestroy() { this.sub.unsubscribe() } } Cloud Firestore 20 2.3 Document Retrieval Inferring Documents vs. Collections The path segment to a collection is ODD, while the path to a document is EVEN. For example, root(0)/collection(1)/document(2)/collection(3)/document(4). This rule always holds true in Firestore. Problem You want to retrieve a single document. Solution Every document is created with a auto-generated unique ID. If you know the unique ID, you can retrieve the document with the same basic process as a collection, but using the afs.doc('collection/docId') method. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export class BookInfoComponent implements OnInit { constructor(private afs: AngularFirestore) {} bookDocument: AngularFireDocument
- {{ book.title }} by {{ book.author }}
; bookObservable: Observable ; ngOnInit() { // Step 1: Make a reference this.bookDocument = this.afs.doc('books/bookID'); // Step 2: Get an observable of the data this.bookObservable = this.bookDocument.valueChanges(); } } book-info.component.html When working with an individual document, it is useful to set the unwrapped Observable as a template variable in Angular. This little trick allows you to use the async pipe once, then call any property on the object - much cleaner than an async pipe on every property. Cloud Firestore 1 2 3 4 21 {{ book.title }} by {{ book.author }}2.4 Include Document Ids with a Collection Problem You want the document IDs included with a collection. Solution By default, valueChanges() does not map the document ID to the document objects in the array. In many cases, you will need the document ID to make queries for individual documents. We can satisfy this requirement by pulling the entire snapshot from Firestore and mapping it’s metadata to a new object. 1 2 3 4 5 6 7 8 9 this.booksObservable = booksCollection.snapshotChanges().map(arr => { return arr.map(snap => { const data = snap.payload.doc.data(); const id = snap.payload.doc.id; return { id, ...data }; }); }); // Unwrapped data: [{ id: 'xyz', author: 'Jeff Delaney', ...}] This is not the most beautiful code in the world, but it’s the best we can do at this point. If you perform this operation frequently, I recommend building a generic Angular service that can apply the code to any collection. 2.5 Add a Document to Collections Problem You want to add a new document to a collection. Cloud Firestore 22 Solution Collections have an add() method that takes a plain JavaScript object and creates a new document in the collection. The method will return a Promise that resolves when the operation is successful, giving you the option to execute additional code after the operation succeeds or fails. 1 2 3 4 5 6 7 8 9 10 11 12 const collection = this.afs.collection('books'); new data = { author: 'Jeff Delaney' title: 'The Angular Firebase Survival Guide', year: 2017 } collection.add(data) /// optional Promise methods .then(() => console.log('success') ) .catch(err => console.log(err) ) 2.6 Set, Update, and Delete a Document Problem You want to set, update, and delete individual documents. Solution Write operations are easy to perform in Firestore. You have the following three methods at your disposal. • set() will destroy all existing data and replace it with new data. • update() will only modify existing properties. • delete() will destroy the document. Cloud Firestore 1 2 3 4 5 6 7 8 9 10 11 23 const doc = this.afs.doc('books/bookID'); const data = { author: 'Jeff Delaney' title: 'The Angular Firebase Survival Guide', year: 2017 }; doc.set(data); // reset all properties with new data doc.update({ publisher: 'LeanPub' }); // update individual properties doc.delete(); // update individual properties All operations return a Promise that resolves when the operation is successful, giving you the option to execute additional code after the operation succeeds or fails. 1 2 3 doc.update(data) .then(() => console.log('success') ) .catch(err => console.log(err) ) 2.7 Create References between Documents Problem You want to create a reference between two related documents. Solution Document references provide a convenient way to model data relationships, similar to the way foreign keys work in a SQL database. We can set them by sending a DocumentReference object to firestore. In AngularFire, this is as simple as calling the ref property on the document reference. Here’s how we can host a reference to a user document on a book document. 1 2 3 4 const bookDoc = this.afs.doc('books/bookID'); const userDoc = this.afs.doc('users/userID'); bookDoc.update({ author: userDoc.ref }); 2.8 Set a Consistent Timestamp Problem You want to maintain a consistent server timestamp on database records. Cloud Firestore 24 Solution Setting timestamps with the JavaScript Date class does not provide consistent results on the server. Fortunately, we can tell Firestore to set a server timestamp when running write operations. I recommend setting up a TypeScript getter to make the timestamp call less verbose. Simply pass the object returned from FieldValue.serverTimestamp() as the value to any property that requires a timestamp. 1 2 3 4 5 6 const bookDoc = this.afs.doc('books/bookID'); bookDoc.update({ updatedAt: this.timestamp }); get timestamp() { return firebase.firestore.FieldValue.serverTimestamp(); } 2.9 Use the GeoPoint Datatype Problem You want to save geolocation data in Firestore. Solution We need to send geolocation data to Firestore as an instance of the GeoPoint class. I recommend setting up a helper method to return the instance from the Firebase SDK. From there, you can use the GeoPoint as the value to any property that requires latitude/longitude coordinates. 1 2 3 4 5 6 7 8 9 const bookDoc = this.afs.doc('books/bookID'); const geopoint = this.geopoint(38.23, -119.77); bookDoc.update({ location: geopoint }); geopoint(lat: number, lng: number) { return new firebase.firestore.GeoPoint(lat, lng); } 2.10 Atomic Writes Problem You want to perform multiple database writes in a batch that will succeed or fail together. Cloud Firestore 25 Solution Using the firebase SDK directly, we can create batch writes that will update multiple documents simultaneously. If any single operation fails, none of the changes will be applied. It works setting all operations on the batch instance, then runs them with batch.commit(). If any operation in the batch fails, the database rolls back to the previous state. 1 2 3 4 5 6 7 8 9 10 11 12 13 const batch = firebase.firestore().batch(); /// add your operations here const itemDoc = firebase.firestore().doc('items/itemID'); const userDoc = firebase.firestore().doc('users/userID'); const currentTime = this.timestamp batch.update(itemDoc, { timestamp: currentTime }); batch.update(userDoc, { timestamp: currentTime }); /// commit operations batch.commit(); 2.11 Order Collections Problem You want a collection ordered by a specific document property. Solution Let’s assume we have the following documents in the books collection. Keep in mind, Firestore does not order by ID, so it is important to set documents with an property that makes sense for ordering, such as a timestamp. 1 2 afs.doc('books/atlas-shrugged').set({ author: 'Ayn Rand', year: 1957 }) afs.doc('books/war-and-peace').set({ author: 'Leo Tolstoy', year: 1865 }) To order by year in ascending order (oldest to newest). Cloud Firestore 1 2 3 4 26 const books = afs.collection('books', ref => ref.orderBy('year') ) // { author: 'Leo Tolstoy', year: 1865 } // { author: 'Ayn Rand', year: 1957 } To order by year in descending order (newest to oldest). 1 2 3 4 const books = afs.collection('books', ref => ref.orderBy('year', 'desc') ) // { author: 'Ayn Rand', year: 1957 } // { author: 'Leo Tolstoy', year: 1865 } Ordering is not just limited to numeric values - we can also order documents alphabetically. 1 const books = afs.collection('books', ref => ref.orderBy('name' ) 2.12 Limit and Offset Collections Problem You want a specific number of documents returned in a collection. Solution As your collections grow larger, you will need to limit collections to a manageable size. For the sake of this example, let’s assume we have millions of books in our collection. The limit() method will return the first N documents from the collection. In general, it will always be used in conjunction with orderBy() because documents have no order by default. 1 afs.collection('books', ref => ref.orderBy('year').limit(100) ) When it comes to offsetting data, you have four methods at your disposal. I find it easier write them out in a sentence. • • • • startAt - Give me everything after this document, including this document startAfter - Give me everything after this document, excluding this document. endAt - Give me everything before this document, including this document. endBefore - Give me everything before this document, excluding this document. If we want all books written after a certain year, we run the query like so: Cloud Firestore 1 2 3 27 afs.collection('books', ref => ref.orderBy('year').startAt(1969) ) /// Like saying books where year >= 1969 If we change it to startAfter(), books from 1969 will be excluded from the query. 1 2 3 afs.collection('books', ref => ref.orderBy('year').startAfter(1969) ) /// Like saying books where year > 1969 These methods are very useful when it comes to building pagination and infinite scroll features in apps. 2.13 Querying Collections with Where Problem You want query documents with equality and/or range operators. Solution The where() method provides an expressive way to filter data in a collection. The beauty of the method is that it works just like it reads. It requires three arguments ref.where(field, operator, value). • field is any property on your document, i.e. author or year • operator is any of the following logical operators: ==, <, <=, >, or >=. (notice != is not included) • value is the value you’re comparing. i.e. ‘George Orwell’ or 1984 Let’s look at some examples and read them like sentences. First, we can filter by equality. 1 2 3 afs.collection('books', ref => ref.where('author', '==', 'James Joyce') ) // Give me all books where the author is James Joyce Our we can use logical range operators Cloud Firestore 1 2 3 afs.collection('books', ref => ref.where('year', '>=', 2001) ) 1 2 3 afs.collection('books', ref => ref.where('year', '<', 2001) ) 28 // Books where the year published is greater-than or equal-to 2001. // Books where the year published is less-than 2001. We can also chain the where method to make multi-property queries. 1 2 3 4 afs.collection('books', ref => ref.where('author', '==', 'James Joyce').where('y\ ear', '>=', 1920) ) // Books where author is James Joyce AND year is greater-than 1920. But there is one major exception! You cannot combine range operators on multiple properties. 1 2 3 4 afs.collection('books', ref => ref.where('year', '>=', 2003).where('author', '>'\ , 'B') ) // ERROR 2.14 Creating Indices Problem You want to order a collection by multiple properties, which requires a custom index. Solution Firestore will automatically create an index for every individual property in your collection. However, it would result in an enormous amount of indices if Firestore indexed every combination of properties. A document with just 7 properties has 120 possible index combinations if you follow the rule of Eularian numbers. The best way to create an index is to simply wait for Firestore to tell you when one is necessary. If we try to order by two different properties, we should get an error in the browser console. Cloud Firestore 1 2 3 29 afs.collection('books', ref => ref.orderBy('year').orderBy('author') ) // Error, you need to create an index for the query The error message will provide a URL link to the Firestore console to create the index. Create the index, and the error will have disappeared the next time you run the query. 2.15 Backend Firestore Security Rules Problem You want to secure your backend data to authorized users with firestore security rules. Solution All Firestore rules must evaluate to a boolean value (true or false). Writing a rule is like saying “If some condition is true, I grant you permission to read/write this data”. There are thousands of possible security rule scenarios and I can’t cover them all, but I can show you what I consider the most useful configurations. Firestore Rules do NOT Cascade If you’ve used rules in the Realtime Database you might be used to cascading rules, where higher level rules apply to nested data. It does not work like this in Firestore unless you explicitly use the =** wildcard operator. Applying Rules to Documents Before we write any rules, let’s look at how we target rules to specific documents. There are three different options, as outlined below. 1. Apply to exact document: Cloud Firestore 1 30 match /itemsCollection/itemXYZ 1. Apply to all documents at this level: 1 match /itemsCollection/{itemID} 1. Apply to all documents at this level AND its nested subcollections: 1 match /itemsCollection/{itemID=**} No Security: Everybody can read and write To make your database completely accessible to anyone. 1 2 3 4 5 6 7 8 service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read; allow write; } } } Note: From here on out, I am going to omit the code surrounding the database to avoid repeating myself. Full Security: Nobody can read or write If you need to lock down your database completely, add this rule. 1 2 3 4 match /{document=**} { allow read: if false; allow write: if false; } Authenticated Security: Logged in users can read or write This allows logged-in users full access to the database. Keep in mind, it does not secure data at the user level - for example, userA can still read/write data that belongs to userB. You can also combine actions on a single line to avoid duplicating identical rules. Cloud Firestore 1 2 3 31 match /{document=**} { allow read, write: if request.auth != null; } User Security: Users can only write data they own This is perhaps the most common and useful security pattern for apps. It locks down anything nested under a userID to that specific user. 1 2 3 match /users/{userId} { allow read, write: if request.auth.uid == userID; } Role Based Security: Only Moderators can Write Sata Many apps give certain users special moderator/admin privileges. These types of rules can get quite verbose, but Firestore allows you to define your own custom reusable functions. This rule will only allow users who have the isModerator == true attribute on their user account to delete posts in the forum. 1 2 3 4 5 6 7 8 function isModerator(userId) { get(/databases/$(database)/documents/users/$(userId)).data.isModerator =\ = true; } match /forum/{postID} { allow delete: if isModerator(request.auth.uid); } Regex Security You can perform a a regular expression match to ensure data adheres to a certain format. For example, this rule will only allow writes of the email address ends in @angularfirebase.com 1 2 3 match /{document} { allow write: if document.matches('.*@angularfirebase\.com') } Time Security You can also get the exact timestamp in UTC format from the request to compare to an existing timestamp in the database. Cloud Firestore 1 2 3 4 match /{document} { allow write: if request.time < resource.data.timestamp + duration.value(\ 1, 'm'); } 32 Realtime Database Firebase provides a realtime NoSQL database. This means all clients subscribe to one database instance and listen for changes. As a developer, it allows you to handle database as an asynchronous data stream. Firebase has abstracted away the pub/sub process you would normally need to build from scratch using something like Redis. Here are the main things you should know when designing an app with Firebase. • • • • • It is a NoSQL JSON-style database When changes occur they are published to all subscribers. Operations must be executed quickly (SQL-style joins on thousands of records are not allowed) Data is retrieved in the form of an RxJS Observable Data is unwrapped asynchronously by subscribing to Observables Injecting the AngularFire Database ALL code examples in this chapter assume you have injected the AngularFireDatabase into your component or service. Example 3.2 is the only snippet that shows this process completely. Would you rather use the Firestore database? In most cases, the Firestore (section 2) document database is superior to the realtime database. It provides better querying methods and data structuring flexibility. You should have good reason to use Realtime Database over Firestore. 3.0 Migrating from AngularFire Version 4 to Version 5 Problem You want to migrate an existing app from AngularFire <= v4 to v5. (If you’re brand new to AngularFire, skip this snippet). Realtime Database 34 Solution AngularFire v5.0.0 was released in October 2017 and was a complete rewrite of the realtime database API. It introduced significant breaking changes to previous versions, so I want to provide a quick migration guide for developers in the middle of existing projects. Quick Fix After you upgrade to v5, your database code will break catastrophically. Fortunately, the AngularFire core team realized this issue and kept the old API available under a different namespace of databasedeprecated. You can make your code work by simply updating your imports. Do a project search for “angularfire2/database” and replace all instances with “angularfire2/databasedeprecated”. You code should now look like this: 1 2 3 4 5 import { AngularFireDatabase, FirebaseObjectObservable, FirebaseListObservable } from 'angularfire2/database-deprecated'; Full Fix Fully migrating to the new API is going to be a little more tedious. The main difference in v5 is the decoupling of the Observable from its reference to firebase. Let’s compare the APIs. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /// *** Version 4 *** const item: FirebaseObjectObservable- = db.object('items/someKey') item.update(data) item.remove() item.subscribe(data => console.log(data) ) /// *** Version 5 *** const item: AngularFireObject
- = db.object('items/someKey') item.update(data) item.remove() // Notice how the Observable is separate from write options const itemObservable: Observable
- = object.valueChanges() itemObservable.subscribe(data => console.log(data) ) Realtime Database 35 Here is the basic process you will need to follow to update from v4 to v5: 1. For database write operations (push, update, set, remove), you will need to convert every Firebase(List | Object)Observable into the new AngularFire(List | Object) reference. 2. To read data as an Observable you will need to call valueChanges() or snapshotChanges() on the reference created in the previous step. 3.1 Data Modeling Firebase NoSQL Data Modeling https://youtu.be/2ciHixbc4HE Problem You want to know how to model data for Firebase NoSQL. Solution In NoSQL, you should always ask “How am I going to be querying this data?”, because operations must be executed quickly. Usually, that means designing a database that is shallow or that avoids large nested documents. You might even need to duplicate data and that’s OK - I realize that might freak you out if you come from a SQL background. Consider this fat and wide design: 1 2 3 4 5 6 7 -|users -|userID -|books -|bookID -|comments -|commentID -|likes Now imagine you wanted to loop over the users just to display their usernames. You would also need load their books, the book comments, and the likes – all that data just for some usernames. We can do better with a tall and skinny design - a denormalized design. Realtime Database 1 2 3 4 5 6 7 8 9 10 11 12 36 -|users -|userID -|books -|userId -|bookID -|comments -|bookID -|likes -|commentID 3.2 Database Retrieval as an Object Build a Firebase CRUD App https://youtu.be/6N_1vUPlhvk Problem You want to retrieve and subscribe to data from Firebase as a single object. Solution You should retrieve data as an object when you do not plan iterating over it. For example, let’s imagine we have a single book in our database. The AngularFireObject
requires a TypeScript type to be specified. If you want to opt out, you can use AngularFireObject , but it’s a good idea to statically type your own interfaces: What is a TypeScript interface? An interface is simply a blueprint for how a data structure should look - it does not contain or create any actual values. Using your own interfaces will help with debugging, provide better developer tooling, and mke your code readable/maintainable. Let’s create a custom type for your Book data. Realtime Database 1 2 3 4 5 export interface Book { author: string; title: string: content: string; } AngularFireList<> We can observe this data in an Angular Component. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { AngularFireDatabase, AngularFireObject, AngularFireList } from 'angularfire2/database'; @Component({ selector: 'book-info', templateUrl: './book-info.component.html', styleUrls: ['./book-info.component.scss'] }) export class BookInfoComponent implements OnInit { constructor(private db: AngularFireDatabase) {} bookRef: AngularFireList ; bookObservable: Observable ; ngOnInit() { 37 Realtime Database 23 24 25 26 27 28 29 30 38 // Step 1: Make a reference this.bookRef = this.db.object('books/atlas-shrugged'); // Step 2: Get an observable of the data this.bookObservable = this.bookRef.valueChanges() } } 3.3 Show Object Data in HTML Problem You want to show the Observable data in the component HTML template. Solution We have a Observable . How do we actually get data from it? The answer is we subscribe to it. Angular has a built async pipe³ that will subscribe (and unsubscribe) to the Observable from the template. 1 2 3 4 5 {{ bookObservable | async | json }} {{ (bookObservable | async)?.content }} We unwrap the Observable in parenthesis before trying to call its attributes. Calling bookObservable.author would not work because that attribute does not exist on the Observable itself, but rather its emitted value. The result should look like this: ³https://angular.io/api/common/AsyncPipe Realtime Database 39 If you have an object with many properties, consider setting the unwrapped Observable as a template variable in Angular. This little trick allows you to use the async pipe once, then call any property on the object - much cleaner than an async pipe on every property. 1 2 3 4 5{{ book.author }} {{ book.title }} {{ book.content }} 3.4 Subscribe without the Async Pipe Problem You want to extract Observable data in the component TypeScript before it reaches the template. Solution Sometimes you need to play with the data before it reaches to the template. We can replicate the async pipe in the component’s TypeScript, but it takes some extra code because we must create the subscription, then unsubscribe when the component is destroyed to avoid memory leaks. Realtime Database 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 40 //// book-info.component.ts import { Subscription } from 'rxjs'; subscription: Subscription; bookRef: AngularFireList; bookData: Book; ngOnInit() { this.bookRef = this.db.object('books/atlas-shrugged'); this.subscription = this.bookRef.valueChanges() .subscribe(book => { this.bookData = book }) } ngOnDestroy() { this.subscription.unsubscribe() } In the HTML, the async pipe is no longer needed because we unwrapped the raw data in the TypeScript with subscribe. 1 2 3 {{ bookData | json }} {{ bookData?.content }} 3.5 Map Object Observables to New Values RxJS Quick Start Video Lesson https://youtu.be/2LCo926NFLI Problem Problem you want to alter Observable values before they are emitted in a subscription. Realtime Database 41 Solution RxJS ships with all sorts to helpful operators to change the behavior of Observables. For now, I will demonstrate map because it is the most frequently used in Angular. Let’s get the object Observable, then map its author property to an uppercase string. 1 2 this.bookObserbable = this.bookRef .map(book => book.author.toUpperCase() ) The HTML remains the same. 1 {{ bookObservable | async }} But the result will be a string of AYN RAND, instead of the JS object displayed in section 3.3. 3.6 Create, Update, Delete a FirebaseObjectObservable data Problem You know how to retrieve data, but now you want to perform operations on it. Solution You have three available operators to manipulate objects. 1. Set - Destructive update. Deletes all data, replacing it with new data. 2. Update - Only updates specified properties, leaving others unchanged. 3. Remove - Deletes all data. Here are three methods showing you how to perform these operations on an AngularfireObject. Realtime Database 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 42 createBook() { const book = { title: 'War and Peace' } return this.db.object('/books/war-and-peace') .set(book) } updateBook(newTitle) { const book = { title: newTitle } return this.db.object('/books/war-and-peace') .update(book) } deleteBook() { return this.db.object('/books/twilight-new-moon') .remove() } 3.7 Database Retrieval as a Collection Problem You want to retrieve data from Firebase as a list or array. Solution The AngularFireList is ideal when you plan on iterating over objects, such as a collection of books. The process is exactly the same as an object, but we expect an Array of objects. Realtime Database RxJS Observable Naming Preferences It is common for Observable streams to be named with an ending $, such as book$. Some love it, some hate it. I will not be doing it here, but you may see this come up occasionally in Angular tutorials. 1 2 3 4 5 6 7 8 9 10 11 12 //// books-list.component.ts booksRef: AngularFireList ; booksObservable: Observable ; // <-- notice the [] here ngOnInit() { // Step 1: Make a reference this.booksRef = this.db.list('books'); // Step 2: Get an observable of the data this.bookObservable = this.booksRef.valueChanges(); } 3.8 Viewing List Data in the Component HTML Problem You want to iterate over an Observable list in the HTML template. 43 Realtime Database 44 Solution Again, you should take advantage of Angular’s async pipe to unwrap the Observable in the template. This will handle the subscribe and unsubscribe process automagically. 1 2 3 4 5 The result should look like this: 3.9 Limiting Lists Problem You want to limit the number of results in a collection. Solution You can pass a second callback argument to db.list(path, queryFn) to access Firebase realtime database query methods. In this example, we limit the results to the first 10 books in the database. Realtime Database 1 2 3 45 queryBooks() { return this.db.list('/books' ref => ref.limitToFirst(10) ) } 3.10 Filter Lists by Value Never use orderByPrority Firebase has an option to orderByPrority, but it only exists for legacy support. Use other ordering options instead. Problem You want to return list items that have a specific property value. Solution This time, let’s filter the collection to all books with an author property of Jack London. 1 2 3 4 5 queryBooks() { return this.db.list('/books', ref => { return ref.orderByChild('author').equalTo('Jack London') }) } 3.11 Create, Update, Delete Lists Problem You want create, update, or remove values in a list Observable. Solution When creating new books, we push them to the list. This will create a push key automatically, which is an encoded timestamp that looks like “-Xozdf2i23sfdf73”. You can think of this the unique ID for an item in a list. Update and delete operations are similar to objects, but require the key of the item as an argument. The key is not returned with valueChanges(), so I included a helper method booksWithKeys that will return an Observable array with the pushKeys included. Realtime Database 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 46 /// Helper method to retrieve the keys as an Observable booksWithKeys(booksRef) { return this.booksRef.snapshotChanges().map(changes => { return changes.map(c => ({ key: c.payload.key, ...c.payload.val() })); }); } pushBook() { const book = { title: 'Call of the Wild' } return this.db.list('/books').push(book) } updateBook(pushKey) { const data = { title: 'White Fang' } return this.db.list('/books').update(pushKey, data) } deleteBook(pushKey) { return this.db.list('/books').remove(pushKey) } Obtain the Push Key on New Items When pushing to a list, you might want the $key from new item. You can obtain it with this.db.list('/books').push(book).key 3.12 Catch Errors with Firebase Operations Problem You want to handle errors gracefully when a Firebase operation fails. Solution Data manipulation (set, update, push, remove) functions return a Promise, so we can determine success or error by calling then and/or catch. In this example, a separate error handler is defined that can be reused as needed. You might want to add some logging, analytics, or messaging logic to the handleError function. Realtime Database 1 2 3 4 5 6 7 8 9 10 11 47 this.createBook() .then( () => console.log('book added successfully')) .catch(err => handleError(err) ); this.updateBook() .then( () => console.log('book updated!')) .catch(err => handleError(err) ); private handleError(err) { console.log("Something went horribly wrong...", err) } 3.13 Atomic Database Writes Problem You want to update multiple database locations atomically, to prevent data anomalies. Solution You will often find situations where you need to keep multiple collections or documents in sync during a single operation. In database theory, this is known as an atomic operation. For example, when a user comments on a book, you want to update the user’s comment collection as well as the book’s comment collection simultaneously. If one operation succeeded, but the other failed, it would lead to a data mismatch or anomaly. In this basic example, we will update the tag attribute on two different books in a single operation. But be careful - this example will perform at destructive set, even though it calls update. 1 2 3 4 5 6 7 atomicSet() { let updates = {}; updates['books/atlas-shrugged/tags/epic'] = true; updates['tags/epic/atlas-shrugged'] = true this.db.object('/').update(updates) } 3.14 Backend Database Rules Database Rules Video Lesson https://youtu.be/qLrDWBKTUZo Realtime Database 48 Problem You want to secure read/write access to your data on the backend. Solution Firebase allows you to define database security logic in JSON format that mirrors to the structure of your database. You just write logical statements that evaluate to true or false, giving users access to read or write data at a given location. First, let’s go over a few special built-in variables you should know about. auth – The current user’s auth state. root – The root of the database and can be traversed with .child(‘name’). data – Data state before an operation (the old data) newData – Data after an operation (the new data) now – Unix epoch timestamp ${wildcard} – Wildcard, used to compare keys. Common Pitfall - Cascading Rules You cannot grant access to data, then revoke it later. However, you can do the opposite – revoke access, then grant it back later. That being said, it is usually best to deny access by default, then grant access when the ideal conditions have been satisfied deeper in the tree. Let’s start by locking down the database at the root. Nothing goes in, nothing comes out. 1 2 3 4 "rules": { ".read": false, ".write": false } Now, let’s give logged in users read access 1 2 3 4 "rules": { ".read": "auth != null", ".write": false } Now let’s allow users to write to the books collection, but only if the data is under their own UID. Realtime Database 1 2 3 4 5 6 7 8 49 "rules": { ".read": "auth != null", "books": { "$uid": { ".write": "$uid === auth.uid" } } } Now, let’s assume we have moderator users, who have access to write to any user’s book. Notice the use of the OR || operator in the rule to chain an extra condition. You can also use AND && when multiple conditions must be met. 1 2 3 4 5 6 7 8 9 "rules": { ".read": "auth != null", "books": { "$uid": { ".write": "$uid === auth.uid || root.child('moderators').child(auth.uid).val() === true" } } } 3.15 Backend Data Validation Problem You want to validate data before it’s written to the database. Solution Firebase has a third rule, .validate, which allows you to put constraints on the type of data that can be saved on the backend. The incoming data will be in the newData Firebase variable. Difference between Write and Validate (1) Validation rules only apply to non-null values. (2) They do not cascade (they only apply to the level at which they are defined.) Realtime Database 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 50 "rules": { "books": { "$bookId": { "title": { ".validate": "newData.isString()" } } } } You will likely want to chain multiple validations together. ```json { ".validate": "newData.isString() && newData.val().matches('regex-expression')" } You might have a list of allowed values in your database, let’s image categories. You can validate against them by traversing the database. 1 2 3 { ".validate": "root.child('categories/' + newData.val()).exists()" } When creating an object, you might want to validate it has all the required attributes. 1 2 3 4 5 6 7 8 9 10 { "$bookId": { ".validate": "newData.hasChildren(['title', 'body', 'author'])", "title": { ".validate": "newData.isString()" }, "body": {}, "author": {} } } User Authentication Firebase provides a flexible authentication system that integrates nicely with Angular and RxJS. In this chapter, I will show you how use three different paradigms, including: • OAuth with Google, Facebook, Twitter, and Github • Email/Password • Anonymous Injecting AngularFire Auth and Database Most code examples in this chapter assume you have injected the AngularFireDatabase and AngularFireAuth into your component or service. If you do not know how to inject these dependencies, revisit section 1.4. 4.1 Getting Current User Data Problem You want to obtain the current user data from Firebase. User Authentication 52 Solution AngularFire2 returns an authState Observable that contains the important user information, such as the UID, display name, email address, etc. You can obtain the current user as an Observable like so. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import import import import { { { { Component, OnInit } from '@angular/core'; AngularFireAuth } from 'angularfire2/auth'; auth } from 'firebase/app'; Observable } from 'rxjs'; @Component({ selector: 'app-user', templateUrl: './user.component.html', styleUrls: ['./user.component.scss'] }) export class UserComponent implements OnInit { currentUser: Observable
- {{ book.title }} by {{ book.author }}
; constructor(private afAuth: AngularFireAuth) { } ngOnInit() { this.currentUser = this.afAuth.authState; } } Alternatively, you can unwrap the auth observable by by subscribing to it. This may be necessary if you need the UID to load other data from the database 1 2 3 4 5 6 7 8 currentUser = null; // or ngOnInit for components constructor(afAuth: AngularFireAuth) { afAuth.authState.subscribe(userData => { this.currentUser = userData }); } At this point, the authState will be null. In the following sections, it will be populated with different login methods. User Authentication 53 4.2 OAuth Authentication OAuth Video https://youtu.be/-3rkY8X2EWc Problem You want to authenticate users via Google, Facebook, Github, or Twitter. Solution Firebase makes OAuth a breeze. In the past, this was the most difficult form of authentication for developers to implement. From the Firebase console, you need to manually activate the providers you want to use. Google is ready to go without any configuration, but other providers like Facebook or Github, require you to get your own developer API keys. Here’s how to handle the login process in a service. 1 2 3 4 5 6 7 8 9 10 11 12 13 googleLogin() { const provider = new auth.GoogleAuthProvider() return this.socialSignIn(provider); } facebookLogin() { const provider = new auth.FacebookAuthProvider() return this.socialSignIn(provider); } private socialSignIn(provider) { return this.afAuth.auth.signInWithPopup(provider) } Now you can create login buttons in your component HTML that trigger the login functions on the click event and Firebase will handle the rest. 1 2 User Authentication 54 4.3 Anonymous Authentication Anonymous Auth Video https://youtu.be/dyQDAaDq2ag Problem You want lazily register users with anonymous authentication. Solution Anonymous auth simply means creating a user session without collecting credentials to reauthenticate, such as an email address and password. This approach is beneficial when you want a guest user to try out the app, then register later. 1 2 3 anonymousLogin() { return this.afAuth.auth.signInAnonymously() } That was easy, but the trick is upgrading their account. Firebase supports account upgrading, but it’s not supported by AngularFire2, so let’s tap into the Firebase SDK. You can link or upgrade any account by calling linkWithPopup. 1 2 3 4 5 6 7 8 9 10 11 12 import { AngularFireAuth } from 'angularfire2/auth'; import { auth } from 'firebase/app'; linkGoogle() { const provider = new auth.GoogleAuthProvider() auth().currentUser.linkWithPopup(provider) } linkFacebook() { const provider = new auth.FacebookAuthProvider() auth().currentUser.linkWithPopup(provider) } User Authentication 55 4.4 Email Password Authentication Problem You want a user to sign up with their email and password. Solution Email/password auth is the most difficult to setup because we need to run some form validation and generate different views for new user sign up and returning user sign in. Here’s how you might handle the process in a component. Full Code Example The code below is a minimal implementation for the book. Checkout the full example in the demo app at https://github.com/codediodeio/angular-firestarter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 userForm: FormGroup; constructor(private fb: FormBuilder, private afAuth: AngularFireAuth) {} ngOnInit() { this.userForm = this.fb.group({ 'email': ['', [ Validators.required, Validators.email ] ], 'password': ['', [ Validators.pattern('^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$'), Validators.minLength(6), Validators.maxLength(25) ] ] }); } emailSignUp() { let email = this.userForm.value['email'] let password = this.userForm.value['password'] User Authentication 24 25 26 27 28 29 30 31 56 return this.afAuth.auth.createUserWithEmailAndPassword(email, password) } emailLogin() { let email = this.userForm.value['email'] let password = this.userForm.value['password'] return this.afAuth.auth.signInWithEmailAndPassword(email, password) } Then create the form in the HTML 1 2 3 4 5 6 7 8 9 10 11 4.5 Handle Password Reset Problem You need a way for users to reset their password. Solution Firebase has a built-in flow for resetting passwords. It works by sending the user an email with a tokenized link to update the password - you just need a way to trigger the process directly via the Firebase SDK. User Authentication 1 2 3 4 5 6 57 userEmail: string; resetPassword() { const fbAuth = auth(); fbAuth.sendPasswordResetEmail(userEmail) } Use ngModel in the HTML template to collect the user’s email address. Then send the reset password email on the button click. 1 2 3 4.6 Catch Errors during Login Problem You want to catch errors when login fails. Solution The login process can fail⁴ for a variety of reasons, so let’s refactor the social sign in function from section 4.2. It is a good idea to create an error handler, especially if you use multiple login methods. 1 2 3 4 5 6 7 8 9 10 private socialSignIn(provider) { return this.afAuth.auth.signInWithPopup(provider) .then(() => console.log('success') ) .catch(error => handleError(error) ); } private handleError(error) { console.log(error) // alert user via toast message } 4.7 Log Users Out Problem You want to end a user session. ⁴https://firebase.google.com/docs/reference/js/firebase.auth.Error User Authentication 58 Solution As you can imagine, logging out is a piece of cake. Calling signOut() will destroy the session and reset the current authState to null. 1 2 3 logout() { this.afAuth.auth.signOut(); } 4.8 Save Auth Data to the Realtime Database Problem You want to save a user’s auth information to the realtime database. Solution The Firebase login function returns a Promise. We can catch a successful response by calling then and running some extra code to update the database. Let’s refactor the our sign in function from section 4.2 to save the user’s email address to the realtime database after sign in. A good database structure for this problem has data nested under each user’s UID. 1 2 3 4 5 -| users -| $uid email: string moderator: boolean birthday: number In the component, we call the desired signin function, which returns a Promise. When resolved, the Promise provides a credential object with the user data that can be saved to the database. User Authentication 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 59 private socialSignIn(provider) { return this.afAuth.auth.signInWithPopup(provider) .then(credential => { const user = credential.user this.saveEmail(user) }) } private saveEmail(user) { if (!user) { return; } const path = `users/${user.uid}`; const data = { email: user.email } this.db.object(path).update(data) } 4.9 Creating a User Profile Problem You want to display user data in profile page. Solution The Firebase auth object has some useful information we can use to build a basic user profile, especially when used with OAuth. This snippet is designed to show you the default properties available. Let’s assume we have subscribed to the currentUser from section 4.1. You can simply call its properties in the template. 1 2 3 4 Here are the Firebase default properties you can use to build user profile data. • uid • displayName User Authentication • • • • • 60 photoUrl email emailVerified phoneNumber isAnonymous You can add additional custom user details to the realtime database using the technique described in section 4.9. 4.10 Auth Guards to Protect Routes Problem You want to prevent unauthenticated users from navigating to certain pages. Solution Guards provide a way to lock down routes until its logic resolves to true. This may look complex (most of it is boilerplate), but it’s actually very simple. We take the first emission from the AuthState Observable, map it to a boolean, and if false, the user is redirected to a login page. You can generate the guard with the CLI via ng generate guard auth; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from\ '@angular/router'; import { Observable } from 'rxjs'; import { tap, map, take } from 'rxjs/operators'; import { AngularFireAuth } from 'angularfire2/auth'; @Injectable() export class AuthGuard implements CanActivate { constructor(private afAuth: AngularFireAuth, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { return this.afAuth.authState .pipe( take(1) User Authentication 19 20 21 22 23 24 25 26 27 28 map(user => !!user) tap(loggedIn => { if (!loggedIn) { console.log("access denied") this.router.navigate(['/login']); } }) ) } } In the routing module, you can activate the guard by adding it to the canActivate property. 1 { path: 'private-page', component: SomeComponent, canActivate: [AuthGuard] } 61 Firebase Cloud Storage File storage used to be a major development hassle. It could take weeks of development fine tuning and optimizing a web app’s file uploading process. With Firebase, you have a GCP Storage Bucket integrated into every project, along with security, admin console management, and a robust API. First, let’s start with this shell of a component to handle the file uploading process. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { Component, OnInit } from '@angular/core'; import { AngularFireStorage, AngularFireUploadTask } from 'angularfire2/storage'; @Component({ selector: 'app-upload', templateUrl: './upload.component.html', styleUrls: ['./upload.component.scss'] }) export class UploadComponent implements OnInit { selectedFiles: FileList; uploadTask: AngularFireUploadTask; constructor(private storage: AngularFireStorage) { } ngOnInit() { } } 5.1 Creating an Upload Task File Storage DropZone https://youtu.be/wRWZQwiNFnM Problem You want to initiate an Upload task. Firebase Cloud Storage 63 Solution Important Caveat The path to a file in a storage bucket must be unique. If two users upload a file to /images/my_pet_pug.jpg, only the first file will be persisted. If this could be a problem with your file structure, you may want to add a unique token or timestamp to every file name. An AngularFireUploadTask is an async object (that allows us to get progress data as an Observable) used to store a file in Firebase Storage. You create the task like so: 1. Get a JavaScript File object via a form input (See Section 5.4) 2. Make a reference to the location you want to save it in Firebase 3. Call the upload to immediately start the upload process to your storage bucket 1 2 3 4 5 upload(file: File): AngularFireUploadTask { const path = 'awesome/image.jpg'; this.uploadTask = this.storage.upload(path, file); } 5.2 Handling the Upload Task Problem You want to handle the progress, success, and failure of the upload task. Solution Let’s modify the example in 5.1. AngularFireUploadTask provides a few observables that we can use to obtain more information. Firebase Cloud Storage 1 2 3 4 5 6 7 8 64 upload(file: File): AngularFireUploadTask { const path = 'awesome/image.jpg'; this.uploadTask = this.storage.upload(path, file); // Number ranging from 0 to 100 this.percentage = this.task.percentageChanges(); } In the HTML, we can unwrap the percentage with the async pipe to display and animate a progress bar. 1 5.3 Saving Data about a file to the Realtime Database Problem You want to save properties from an uploaded file to the Firestore database. Solution Saving upload information to the database is very often required, as you will want to probably reference the download URL at a later time. Here’s what we can get from a file snapshot. https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot • downloadURL • totalBytes • metadata (contentType, contentLanguage, etc) When the upload task completes, we can use the snapshot to save information to the database. Again, we are building on the upload function in examples 5.1 and 5.2. Firebase Cloud Storage 1 2 3 4 5 6 7 8 9 10 11 12 13 65 this.snapshot = this.task.snapshotChanges() .pipe( tap(snap => { if (snap.bytesTransferred === snap.totalBytes) { // Update firestore on completion this.db.collection('photos').add( { path, size: snap.totalBytes }) } }), finalize(() => { this.downloadURL = this.storage.ref(path).getDownloadURL() }) ) .subscribe() The downloadURL is also an Observable, so we can simply unwrap it into the image src. 1 5.4 Uploading a Single File Problem You want to enable users to upload a single file from Angular. Solution Now that you know how to upload files on the backend, how do you actually receive the necessary File object from a user? Here we have an input element for a file, that triggers a detectFiles function when it changes (when they select a file on their device). Then the user can start the upload process by clicking the button attached to uploadSingle. 1 2 3
Source Exif Data:
File Type : PDF File Type Extension : pdf MIME Type : application/pdf PDF Version : 1.6 Linearized : No Author : Jeff Delaney Create Date : 2018:05:23 13:51:36Z Modify Date : 2019:03:12 16:57:57-05:00 Has XFA : No XMP Toolkit : Adobe XMP Core 5.6-c015 84.159810, 2016/09/10-02:41:30 Format : application/pdf Creator : Jeff Delaney Title : The Angular Firebase Survival Guide Creator Tool : LaTeX with hyperref package Metadata Date : 2019:03:12 16:57:57-05:00 Producer : XeTeX 0.99998 Document ID : uuid:5ee46e06-02d7-4bbd-a13e-84106e29d375 Instance ID : uuid:cafd2e28-3e68-4043-9dd8-64ae740de5c1 Page Mode : UseOutlines Page Count : 87EXIF Metadata provided by EXIF.tools