├── .gitignore ├── .node-version ├── articles └── firebase-in-2019.md ├── cloud-firestore ├── README.md ├── bin │ └── upload-data.js ├── complete.html ├── data │ └── people.json ├── index.html ├── package.json └── yarn.lock ├── firebase-authentication ├── 01-intro.md ├── 02-configuration.md ├── 03-implementation.md ├── 04-outro.md ├── 05-notes.md └── screenshots │ ├── 1000-auth-methods.png │ ├── 1100-sign-in-method-enabled.png │ ├── 1200-auth-domain-enabled.png │ ├── 2000-facebook-read-docs.png │ ├── 2050-facebook-create-app.png │ ├── 2100-facebook-add-login.png │ ├── 2200-facebook-copy-app-id-and-secret.png │ ├── 2300-facebook-paste-id-and-secret.png │ ├── 2400-facebook-paste-redirect-url.png │ ├── 3000-go-do-dashboard.png │ ├── 3100-copy-config.png │ ├── 3300-auth-domain-initialized.png │ ├── 3400-init-js.png │ ├── 3500-folder-structure.png │ ├── 4000-auth-service.png │ ├── 4100-auth-service-return.png │ ├── 4200-method-signature.png │ ├── 4300-onAuthStateChanged.png │ ├── 4350-sign-out-delete.png │ ├── 4400-email-password.png │ ├── 4500-password-reset.png │ ├── 4600-phone.png │ ├── 4700-recaptcha-verifier.png │ ├── 4800-oauth-sign-in.png │ ├── 5000-login-options.png │ ├── 5100-input-email.png │ ├── 5200-input-password.png │ ├── 5300-logged-in.png │ ├── 5400-bad-password.png │ ├── 5500-input-email-two.png │ ├── 5600-input-password-two.png │ ├── 5700-register-email.png │ ├── 5800-input-phone.png │ ├── 5900-confirm-phone.png │ └── 5950-oauth-google.png ├── firebase-cloud-functions ├── .firebaserc ├── README.md ├── context.json ├── firebase.json ├── functions │ ├── config.json.dist │ ├── index.js │ ├── index.spec.js │ ├── package.json │ └── yarn.lock ├── snap.val.json └── spec │ └── support │ └── jasmine.json ├── starter-project ├── .babelrc ├── .firebaserc ├── database.rules.json ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions │ ├── index.js │ ├── package.json │ └── yarn.lock ├── index.html ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── index.html │ └── index.js ├── storage.rules └── yarn.lock └── write-your-first-query ├── README.md ├── bin └── upload-data.js ├── complete.html ├── data └── people.json ├── index.html ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | config.json 40 | *.log 41 | 42 | .vscode 43 | 44 | dist 45 | .cache 46 | 47 | service-account.json -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | lts 2 | -------------------------------------------------------------------------------- /articles/firebase-in-2019.md: -------------------------------------------------------------------------------- 1 | # Firebase: 2019's Dominant Web App Platform 2 | 3 | ## tldr; 4 | 5 | Google has invested heavily to make Firebase a full-featured application platform for web. It's dirt cheap to use. It's reliable. It saves up to 50% of your dev time. 6 | 7 | Firebase is all you need... with a few exceptions. Firebase is still missing search functionality, but we can plug that hole with Algolia. Google Cloud Platform provides a few more ancillary services such as DNS, Cloud SQL for reporting data and Bigtable for graph data. 8 | 9 | ## A Quick History 10 | 11 | Firebase was [birthed from a Y Combinator company](https://hackernoon.com/how-to-build-a-product-loved-by-millions-and-get-acquired-by-google-the-firebase-story-82dab4e3e80c) that was acquired by Google in 2014. 12 | 13 | Firebase is a classic [disruptive company](http://claytonchristensen.com/key-concepts/). It started as a quick prototyping tool for developers who wanted to move quickly but didn't necessarily need much scale or sophistication. 14 | 15 | They've spent the last eight years building capabilities from that original base of passionate freelancers and indie devs, and as a cornerstone of Google Cloud Platform's strategy, Firebase is fighting its way into the larger corporate market. 16 | 17 | ## What is Firebase? 18 | 19 | Firebase is a suite of managed services that are focused on developer experience over raw power. 20 | 21 | Firebase handles your authentication, data storage, document storage and serverless functions. 22 | 23 | AWS and GCP appeal to large, corporate projects that can take years to ship. 24 | 25 | Use Firebase when you need to ship yesterday. 26 | 27 | You may have heard that Firebase is just for startups or prototypes, or that it fails at scale or gets super expensive. Well... things have changed. A lot. 28 | 29 | ## Firebase teaches front-end devs to use a distributed architecture 30 | 31 | Firebase enables a distributed architecture. 32 | 33 | New developers have traditionally learned to build monolithic apps. 34 | 35 | A monolith is a single application server that holds your database and server. Everything lives on a single server that you control, so it's very easy to teach and understand. 36 | 37 | The first managed service that I used was Amazon S3. It was an endpoint where I could send files. S3 would save the files and give me a link. I'd save the link in my local SQL database and S3 would serve the file to my users. 38 | 39 | Managed services are used to create distributed systems. Each part of the system is optimized for a small part of the puzzle. Developers compose these systems together. 40 | 41 | AWS and GCP provide extremely powerful, complicated managed services aimed at large, enterprise users. They're not built for indie devs. And that's where Firebase comes in. 42 | 43 | Firebase lets you quickly deploy managed services for small- to medium-sized apps. And you can roll in heavier-duty Google Cloud Platform (GCP) services as your app grows. 44 | 45 | ## An alternative to SQL 46 | 47 | Most developers are raised on SQL. 48 | 49 | SQL is great for modeling relational data, but guess what! Most data is not as relational as you may think. 50 | 51 | SQL is ideal when you don't know how you're going to query your data. You simply store your data in [third normal form](https://en.wikipedia.org/wiki/Third_normal_form) and SQL lets you query it into almost any structure for your front end. 52 | 53 | Firebase and other NoSQL datastores optimize for reads, which are much more common than writes. So instead of writing once and reading in many different ways like with SQL... NoSQL/Firebase asks you to write the data to match your reads. 54 | 55 | ## Firebase crushes other databases (for many use cases) 56 | 57 | Let's say that I have new user sign up for a multi-player RPG. I want to show that user's email address on their profile, next to any messages that they send to other users. Finally, I'll need a list of all user emails for my admin dashboard. 58 | 59 | Firebase asks you to write that email address in three different places, one for each type of read operation. "But Chris!" you ask. What if I need to change the email address??? Isn't that inefficient??? 60 | 61 | Yep. It's inefficient. You'll have to update that email address in three places, and you'll have to be careful to track everywhere that you saved it. 62 | 63 | But how often do you actually need to **write** an email address? What about the reads??? 64 | 65 | The reality, and the reason that Firebase works so well, is that most apps **read data significantly more** than they write it. 66 | 67 | So get over your SQL-induced anxiety and duplicate data throughout your Firebase data structure. Your database reads will dramatically outperform SQL reads at scale. 68 | 69 | ## The Evil Twin Databases 70 | 71 | Firebase now has two databases, 72 | 73 | 1. the [Realtime Database](https://firebase.google.com/docs/database/), also known as classic Firebase or the RTDB, and 74 | 2. [Cloud Firestore](https://firebase.google.com/docs/firestore/). 75 | 76 | The Realtime Database is ideal for fast, high-volume operations. I use the RTDB for tracking how many users I have online at any one time. I use it for job queues that are consumed with [Cloud Functions](https://firebase.google.com/docs/functions/). 77 | 78 | Firestore is better for longer-lived, more structured data. I use Firestore for 90% of my data storage. You pay by the operation, so it's not ideal for extremely frequent reads and writes... but that's why we have the RTDB :) 79 | 80 | I use Firestore for user profiles, chat logs, purchase records... anything that I need to stick around for a while. 81 | 82 | ## Firebase is cheap and much, much faster 83 | 84 | The [Iron Triangle of Project Management](https://en.wikipedia.org/wiki/Project_management_triangle) states that all projects are compromises between **cost**, **scope** and **time**. 85 | 86 | Firebase is the lowest-cost and fastest development experience of which I'm aware. 87 | 88 | I bootstrap projects on a regular basis. Most of my projects are solo projects. I'm the developer, designer and marketer. I'm incredibly time-constrained, so I use Firebase to enable me to achieve greater scope while keeping my time costs low. 89 | 90 | I've built the following projects entirely on my own with Firebase: 91 | 92 | - [calligraphy.org](https://www.calligraphy.org/): Online teaching platform integrated with Shopify 93 | - [HiiTClock.com](https://www.hiitclock.com/): PWA workout timer 94 | - [pixels.chrisesplin.com](https://pixels.chrisesplin.com/): Chrome extension for UI development 95 | - [bunches.chrisesplin.com](https://bunches.chrisesplin.com/): Multi-player card game 96 | 97 | Calligraphy.org powers my wife's business. It took me four months to write it in the mornings before my full-time job. It would have taken twice as long without Firebase. 98 | 99 | The other projects took between 75 and 200 hours each. Again, I finished them in my spare time while relying heavily on Firebase. 100 | 101 | Firebase has spoiled me. I've used Firebase instead of SQL since 2013. And these aren't just rapid prototypes. They're fully-functional, scalable and used by thousands of customers. 102 | 103 | ## Firebase includes _nearly_ everything I need to build small- to medium-sized apps 104 | 105 | The pillars of Firebase's web offering are 106 | 107 | - the Realtime Database (json database), 108 | - Cloud Firestore (document/collection database), 109 | - Cloud Functions for Firebase (serverless functions), 110 | - Firebase Storage (file/blob storage), and 111 | - Firebase Hosting (static file hosting). 112 | 113 | These five pillars can support an enormous range of apps. 114 | 115 | Realistically, you'll need to "cheat" on Firebase a bit for larger apps. I use GCP for a few things that don't fit neatly within the Firebase offering. I also use [Algolia](https://www.algolia.com/) to power my search. 116 | 117 | The funny thing is that Algolia is the most expensive part of my stack at $35/month. The Firebase databases are optimized for everything **except search**. This is an important caveat to recognize early. Searching Firestore collections or anything in the Realtime Database is extremely limited, and for strong architectural reasons. I don't expect to ever see Firebase support search. 118 | 119 | I primarily use GCP to manage the DNS for my domains. And I sometimes run Cloud Compute instances for small, custom tasks. 120 | 121 | I also use [GitLab.com](https://gitlab.com/deltaepsilon) for CI/CD purposes... so I guess I step out on Firebase a couple of times on each project :) 122 | 123 | ## Firebase scales effortlessly 124 | 125 | Firebase does not allow slow operations. The database does not execute joins. It doesn't search. 126 | 127 | I haven't personally built a Firebase app up to massive scale. I've heard that the Realtime Database can run into limits and require manual sharding; however, Firestore is architected much differently, and I wouldn't be surprised if it scales more like [Cloud Spanner](https://cloud.google.com/spanner/). 128 | 129 | Firestore has some awesome performance characteristics. 130 | 131 | From the [Firestore marketing page](https://firebase.google.com/products/firestore/): 132 | 133 | > All queries scale with the size of your result set (note: not your data set), so your app is ready to scale from day one. 134 | 135 | Firestore also offers strong consistency, meaning that you don't have to worry about the eventual-consistency data models that bedevil most NoSQL implementations. 136 | 137 | ## GCP is the escape hatch 138 | 139 | Firebase services are built on Google Cloud Platform (aka GCP) infrastructure, and some of them are closely integrated. 140 | 141 | For instance, Firebase Cloud Storage uses GCP Storage buckets that you can access through the GCP SDKs. 142 | 143 | GCP is an enterprise-focused suite of services. It's massive. 144 | 145 | Each Firebase project comes with its own GCP account, granting you access to the full power of GCP. So don't worry if Firebase doesn't fulfill your every need. GCP has it covered. 146 | 147 | For example, If you don't want to pay for Algolia, you can spin up an Elasticsearch cluster on GCP and roll your own search. It'll be just as expensive as Algolia... so I can't recommend it for small projects; however, those capabilities are all there. 148 | 149 | ## Common concerns with Firebase 150 | 151 | Firebase has it's downsides! 152 | 153 | - Firebase cannot be hosted locally. 154 | - Firebase is tough to mock for local unit tests. 155 | - There's far less tooling available than for SQL databases. 156 | - Firebase can get expensive if you implement it poorly. 157 | 158 | None of these problems bothers me. It may be Stockholm Syndrome... but I'm at peace with all of these "problems" with Firebase. 159 | 160 | Engineering is all about tradeoffs. Firebase trades a bunch of control and autonomy for speed and simplicity of development. 161 | 162 | ## When to NOT use Firebase 163 | 164 | **DO NOT** use Firebase for highly relational data. 165 | 166 | **DO NOT** use Firebase for complex graph data. 167 | 168 | **DO NOT** use Firebase for complex server needs. 169 | 170 | I sometimes recommend hybrid architectures. Store your relational data in GCP's Cloud SQL or Cloud Spanner databases. Store your graph data in JanusGraph on top of Cloud Bigtable. 171 | 172 | But these are all advanced use cases! Don't worry about them until you need them. 173 | 174 | ## The competition 175 | 176 | Firebase's direct competition is dead. [Facebook killed off Parse](https://blog.parseplatform.org/announcements/a-parse-shutdown-reminder/) and [RethinkDB](https://rethinkdb.com/blog/rethinkdb-shutdown/) suffered a similar fate. 177 | 178 | The most direct alternative to Firebase at this point is AWS or GCP. They're much more complicated and harder to use... but you could achieve similar architectures with other services. 179 | 180 | Or just use Postgres. Postgres is killer... as long as you have budget to write your own API on top of it. 181 | 182 | That's about it. Use AWS, GCP or Postgres. Your dev velocity will suffer, but at least with Postgres you can run everything locally! 183 | 184 | I've worked in a corporate setting with Postgres running in a local Docker container, and it's slick. I didn't write that API myself. It was expensive to maintain, and I could have duplicated it in Firebase at a much lower cost. But the business I was working with wasn't a fan of managed services. What could I do :) 185 | 186 | ## Conclusion: Firebase is preferred for most front-end-focused apps 187 | 188 | Firebase is a slam dunk for small- to medium-sized projects, especially if they're front-end focused. 189 | 190 | You may need to re-architect if you hit massive scale... but you **always** have to re-architect for massive scale. Don't prematurely optimize for scale. 191 | 192 | Choose Firebase because it gets you to market faster. It helps you validate your ideas and get feedback. 193 | 194 | Firebase is a cornerstone of Google's cloud strategy. It's not going anywhere. 195 | -------------------------------------------------------------------------------- /cloud-firestore/README.md: -------------------------------------------------------------------------------- 1 | # Write Your First Query 2 | 3 | ## Open in Glitch 4 | 5 | We'll be working in Google Chrome. 6 | 7 | Open the Glitch project with Google Chrome: [Write Your First Query](https://glitch.com/edit/#!/coordinated-freighter?path=index.html) 8 | 9 | ## DevTools 10 | 11 | Click the green Show (Live) button in the header of the Glitch project to open a new window. 12 | 13 | The new window has a live view of your project, so you can see what happens as you edit it. 14 | 15 | Right-click on the page and select "Inspect Element". 16 | 17 | The new panel that has opened up is called Dev Tools. 18 | 19 | Click the "three dots" button on the top-right of Dev Tools and select Dock Side > Dock to Right. 20 | 21 | Now expand drag DevTools to be nice and big and select the Console tab. 22 | 23 | 24 | ## Write the code 25 | 26 | Return to Glitch and start writing. 27 | 28 | Switch to the live-preview tab to see your progress! 29 | 30 | ## Files 31 | 32 | The files are in this repo. 33 | 34 | They're also available at the following links: 35 | 36 | - [index.html](https://raw.githubusercontent.com/how-to-firebase/tutorials/master/write-your-first-query/index.html) 37 | - [complete.html](https://raw.githubusercontent.com/how-to-firebase/tutorials/master/write-your-first-query/complete.html) 38 | 39 | -------------------------------------------------------------------------------- /cloud-firestore/bin/upload-data.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const serviceAccount = require('../../service-account.json'); 3 | 4 | admin.initializeApp({ 5 | credential: admin.credential.cert(serviceAccount), 6 | databaseURL: 'https://how-to-firebase-tutorials.firebaseio.com', 7 | }); 8 | 9 | const people = require('../data/people.json'); 10 | 11 | const records = people.map(({ name, height, hair_color, url, mass, eye_color, gender }) => { 12 | const urlParts = url.split('/'); 13 | let id = String(urlParts[urlParts.length - 2]); 14 | let charactersToPad = 3 - id.length; 15 | while (charactersToPad--) { 16 | id = '0' + id; 17 | } 18 | 19 | return { 20 | id, 21 | name, 22 | height: +height, 23 | hair_color, 24 | mass: +mass, 25 | eye_color, 26 | gender, 27 | }; 28 | }); 29 | 30 | const db = admin.firestore(); 31 | const collection = db 32 | .collection('public') 33 | .doc('cloud-firestore') 34 | .collection('star-wars-people'); 35 | const batch = db.batch(); 36 | 37 | records.forEach(({ id, name, height, hair_color, mass, eye_color, gender }) => { 38 | batch.set(collection.doc(id), { name, height, hair_color, mass, eye_color, gender }); 39 | }); 40 | 41 | batch.commit().then(() => { 42 | console.log('people records saved'); 43 | }); 44 | -------------------------------------------------------------------------------- /cloud-firestore/complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | First Firestore Query 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 38 | 39 |

Page intentionally left blank

40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /cloud-firestore/data/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Luke Skywalker", 4 | "height": "172", 5 | "mass": "77", 6 | "hair_color": "blond", 7 | "skin_color": "fair", 8 | "eye_color": "blue", 9 | "birth_year": "19BBY", 10 | "gender": "male", 11 | "homeworld": "https://swapi.co/api/planets/1/", 12 | "films": [ 13 | "https://swapi.co/api/films/2/", 14 | "https://swapi.co/api/films/6/", 15 | "https://swapi.co/api/films/3/", 16 | "https://swapi.co/api/films/1/", 17 | "https://swapi.co/api/films/7/" 18 | ], 19 | "species": ["https://swapi.co/api/species/1/"], 20 | "vehicles": ["https://swapi.co/api/vehicles/14/", "https://swapi.co/api/vehicles/30/"], 21 | "starships": ["https://swapi.co/api/starships/12/", "https://swapi.co/api/starships/22/"], 22 | "created": "2014-12-09T13:50:51.644000Z", 23 | "edited": "2014-12-20T21:17:56.891000Z", 24 | "url": "https://swapi.co/api/people/1/" 25 | }, 26 | { 27 | "name": "C-3PO", 28 | "height": "167", 29 | "mass": "75", 30 | "hair_color": "n/a", 31 | "skin_color": "gold", 32 | "eye_color": "yellow", 33 | "birth_year": "112BBY", 34 | "gender": "n/a", 35 | "homeworld": "https://swapi.co/api/planets/1/", 36 | "films": [ 37 | "https://swapi.co/api/films/2/", 38 | "https://swapi.co/api/films/5/", 39 | "https://swapi.co/api/films/4/", 40 | "https://swapi.co/api/films/6/", 41 | "https://swapi.co/api/films/3/", 42 | "https://swapi.co/api/films/1/" 43 | ], 44 | "species": ["https://swapi.co/api/species/2/"], 45 | "vehicles": [], 46 | "starships": [], 47 | "created": "2014-12-10T15:10:51.357000Z", 48 | "edited": "2014-12-20T21:17:50.309000Z", 49 | "url": "https://swapi.co/api/people/2/" 50 | }, 51 | { 52 | "name": "R2-D2", 53 | "height": "96", 54 | "mass": "32", 55 | "hair_color": "n/a", 56 | "skin_color": "white, blue", 57 | "eye_color": "red", 58 | "birth_year": "33BBY", 59 | "gender": "n/a", 60 | "homeworld": "https://swapi.co/api/planets/8/", 61 | "films": [ 62 | "https://swapi.co/api/films/2/", 63 | "https://swapi.co/api/films/5/", 64 | "https://swapi.co/api/films/4/", 65 | "https://swapi.co/api/films/6/", 66 | "https://swapi.co/api/films/3/", 67 | "https://swapi.co/api/films/1/", 68 | "https://swapi.co/api/films/7/" 69 | ], 70 | "species": ["https://swapi.co/api/species/2/"], 71 | "vehicles": [], 72 | "starships": [], 73 | "created": "2014-12-10T15:11:50.376000Z", 74 | "edited": "2014-12-20T21:17:50.311000Z", 75 | "url": "https://swapi.co/api/people/3/" 76 | }, 77 | { 78 | "name": "Darth Vader", 79 | "height": "202", 80 | "mass": "136", 81 | "hair_color": "none", 82 | "skin_color": "white", 83 | "eye_color": "yellow", 84 | "birth_year": "41.9BBY", 85 | "gender": "male", 86 | "homeworld": "https://swapi.co/api/planets/1/", 87 | "films": [ 88 | "https://swapi.co/api/films/2/", 89 | "https://swapi.co/api/films/6/", 90 | "https://swapi.co/api/films/3/", 91 | "https://swapi.co/api/films/1/" 92 | ], 93 | "species": ["https://swapi.co/api/species/1/"], 94 | "vehicles": [], 95 | "starships": ["https://swapi.co/api/starships/13/"], 96 | "created": "2014-12-10T15:18:20.704000Z", 97 | "edited": "2014-12-20T21:17:50.313000Z", 98 | "url": "https://swapi.co/api/people/4/" 99 | }, 100 | { 101 | "name": "Leia Organa", 102 | "height": "150", 103 | "mass": "49", 104 | "hair_color": "brown", 105 | "skin_color": "light", 106 | "eye_color": "brown", 107 | "birth_year": "19BBY", 108 | "gender": "female", 109 | "homeworld": "https://swapi.co/api/planets/2/", 110 | "films": [ 111 | "https://swapi.co/api/films/2/", 112 | "https://swapi.co/api/films/6/", 113 | "https://swapi.co/api/films/3/", 114 | "https://swapi.co/api/films/1/", 115 | "https://swapi.co/api/films/7/" 116 | ], 117 | "species": ["https://swapi.co/api/species/1/"], 118 | "vehicles": ["https://swapi.co/api/vehicles/30/"], 119 | "starships": [], 120 | "created": "2014-12-10T15:20:09.791000Z", 121 | "edited": "2014-12-20T21:17:50.315000Z", 122 | "url": "https://swapi.co/api/people/5/" 123 | }, 124 | { 125 | "name": "Owen Lars", 126 | "height": "178", 127 | "mass": "120", 128 | "hair_color": "brown, grey", 129 | "skin_color": "light", 130 | "eye_color": "blue", 131 | "birth_year": "52BBY", 132 | "gender": "male", 133 | "homeworld": "https://swapi.co/api/planets/1/", 134 | "films": [ 135 | "https://swapi.co/api/films/5/", 136 | "https://swapi.co/api/films/6/", 137 | "https://swapi.co/api/films/1/" 138 | ], 139 | "species": ["https://swapi.co/api/species/1/"], 140 | "vehicles": [], 141 | "starships": [], 142 | "created": "2014-12-10T15:52:14.024000Z", 143 | "edited": "2014-12-20T21:17:50.317000Z", 144 | "url": "https://swapi.co/api/people/6/" 145 | }, 146 | { 147 | "name": "Beru Whitesun lars", 148 | "height": "165", 149 | "mass": "75", 150 | "hair_color": "brown", 151 | "skin_color": "light", 152 | "eye_color": "blue", 153 | "birth_year": "47BBY", 154 | "gender": "female", 155 | "homeworld": "https://swapi.co/api/planets/1/", 156 | "films": [ 157 | "https://swapi.co/api/films/5/", 158 | "https://swapi.co/api/films/6/", 159 | "https://swapi.co/api/films/1/" 160 | ], 161 | "species": ["https://swapi.co/api/species/1/"], 162 | "vehicles": [], 163 | "starships": [], 164 | "created": "2014-12-10T15:53:41.121000Z", 165 | "edited": "2014-12-20T21:17:50.319000Z", 166 | "url": "https://swapi.co/api/people/7/" 167 | }, 168 | { 169 | "name": "R5-D4", 170 | "height": "97", 171 | "mass": "32", 172 | "hair_color": "n/a", 173 | "skin_color": "white, red", 174 | "eye_color": "red", 175 | "birth_year": "unknown", 176 | "gender": "n/a", 177 | "homeworld": "https://swapi.co/api/planets/1/", 178 | "films": ["https://swapi.co/api/films/1/"], 179 | "species": ["https://swapi.co/api/species/2/"], 180 | "vehicles": [], 181 | "starships": [], 182 | "created": "2014-12-10T15:57:50.959000Z", 183 | "edited": "2014-12-20T21:17:50.321000Z", 184 | "url": "https://swapi.co/api/people/8/" 185 | }, 186 | { 187 | "name": "Biggs Darklighter", 188 | "height": "183", 189 | "mass": "84", 190 | "hair_color": "black", 191 | "skin_color": "light", 192 | "eye_color": "brown", 193 | "birth_year": "24BBY", 194 | "gender": "male", 195 | "homeworld": "https://swapi.co/api/planets/1/", 196 | "films": ["https://swapi.co/api/films/1/"], 197 | "species": ["https://swapi.co/api/species/1/"], 198 | "vehicles": [], 199 | "starships": ["https://swapi.co/api/starships/12/"], 200 | "created": "2014-12-10T15:59:50.509000Z", 201 | "edited": "2014-12-20T21:17:50.323000Z", 202 | "url": "https://swapi.co/api/people/9/" 203 | }, 204 | { 205 | "name": "Obi-Wan Kenobi", 206 | "height": "182", 207 | "mass": "77", 208 | "hair_color": "auburn, white", 209 | "skin_color": "fair", 210 | "eye_color": "blue-gray", 211 | "birth_year": "57BBY", 212 | "gender": "male", 213 | "homeworld": "https://swapi.co/api/planets/20/", 214 | "films": [ 215 | "https://swapi.co/api/films/2/", 216 | "https://swapi.co/api/films/5/", 217 | "https://swapi.co/api/films/4/", 218 | "https://swapi.co/api/films/6/", 219 | "https://swapi.co/api/films/3/", 220 | "https://swapi.co/api/films/1/" 221 | ], 222 | "species": ["https://swapi.co/api/species/1/"], 223 | "vehicles": ["https://swapi.co/api/vehicles/38/"], 224 | "starships": [ 225 | "https://swapi.co/api/starships/48/", 226 | "https://swapi.co/api/starships/59/", 227 | "https://swapi.co/api/starships/64/", 228 | "https://swapi.co/api/starships/65/", 229 | "https://swapi.co/api/starships/74/" 230 | ], 231 | "created": "2014-12-10T16:16:29.192000Z", 232 | "edited": "2014-12-20T21:17:50.325000Z", 233 | "url": "https://swapi.co/api/people/10/" 234 | } 235 | ] 236 | -------------------------------------------------------------------------------- /cloud-firestore/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | First Firestore Query 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 |

Page intentionally left blank

31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /cloud-firestore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "write-your-first-query", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "firebase-admin": "^5.8.2" 8 | }, 9 | "scripts": { 10 | "upload": "node bin/upload-data.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cloud-firestore/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@firebase/app-types@0.1.1": 6 | version "0.1.1" 7 | resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.1.1.tgz#1b794e101c779310763b1bfce8c24e7728fb9a91" 8 | 9 | "@firebase/app@^0.1.1": 10 | version "0.1.8" 11 | resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.1.8.tgz#0952d0c0cb2926aaa9b39459c7d9df653fa54330" 12 | dependencies: 13 | "@firebase/app-types" "0.1.1" 14 | "@firebase/util" "0.1.8" 15 | 16 | "@firebase/database-types@0.1.1": 17 | version "0.1.1" 18 | resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.1.1.tgz#601b8040191766777b785c1675eac34ce57c669c" 19 | 20 | "@firebase/database@^0.1.3": 21 | version "0.1.9" 22 | resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.1.9.tgz#ea5f376d1a59d16f909dfb46c597b55e35d9834b" 23 | dependencies: 24 | "@firebase/database-types" "0.1.1" 25 | "@firebase/util" "0.1.8" 26 | faye-websocket "0.11.1" 27 | 28 | "@firebase/util@0.1.8": 29 | version "0.1.8" 30 | resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.1.8.tgz#7a7eb9d5fc56ba9e9b854bb2357d51f83b07df31" 31 | 32 | "@google-cloud/common-grpc@^0.5.3": 33 | version "0.5.4" 34 | resolved "https://registry.yarnpkg.com/@google-cloud/common-grpc/-/common-grpc-0.5.4.tgz#bbc36de0c01cf101dcb2660d76c8b833db6f7a23" 35 | dependencies: 36 | "@google-cloud/common" "^0.15.1" 37 | dot-prop "^4.2.0" 38 | duplexify "^3.5.1" 39 | extend "^3.0.1" 40 | grpc "~1.7.2" 41 | is "^3.2.0" 42 | modelo "^4.2.0" 43 | retry-request "^3.3.1" 44 | through2 "^2.0.3" 45 | 46 | "@google-cloud/common@^0.15.1": 47 | version "0.15.1" 48 | resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-0.15.1.tgz#a6f79535de626cc0e05b0ccda9db0c36099e47a3" 49 | dependencies: 50 | array-uniq "^1.0.3" 51 | arrify "^1.0.1" 52 | concat-stream "^1.6.0" 53 | create-error-class "^3.0.2" 54 | duplexify "^3.5.0" 55 | ent "^2.2.0" 56 | extend "^3.0.1" 57 | google-auto-auth "^0.8.0" 58 | is "^3.2.0" 59 | log-driver "^1.2.5" 60 | methmeth "^1.1.0" 61 | modelo "^4.2.0" 62 | request "^2.79.0" 63 | retry-request "^3.0.0" 64 | split-array-stream "^1.0.0" 65 | stream-events "^1.0.1" 66 | string-format-obj "^1.1.0" 67 | through2 "^2.0.3" 68 | 69 | "@google-cloud/firestore@^0.11.2": 70 | version "0.11.2" 71 | resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-0.11.2.tgz#b511a1ec7a3b9df5cc453034b24391b0ccff9b1d" 72 | dependencies: 73 | "@google-cloud/common" "^0.15.1" 74 | "@google-cloud/common-grpc" "^0.5.3" 75 | bun "^0.0.12" 76 | extend "^3.0.1" 77 | functional-red-black-tree "^1.0.1" 78 | google-gax "^0.14.3" 79 | is "^3.2.1" 80 | safe-buffer "^5.1.1" 81 | through2 "^2.0.3" 82 | 83 | "@google-cloud/storage@^1.2.1": 84 | version "1.5.2" 85 | resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-1.5.2.tgz#5e206603570f5d1cee4681b23919d8f3655e4e60" 86 | dependencies: 87 | "@google-cloud/common" "^0.15.1" 88 | arrify "^1.0.0" 89 | async "^2.0.1" 90 | concat-stream "^1.5.0" 91 | create-error-class "^3.0.2" 92 | duplexify "^3.5.0" 93 | extend "^3.0.0" 94 | gcs-resumable-upload "^0.8.2" 95 | hash-stream-validation "^0.2.1" 96 | is "^3.0.1" 97 | mime-types "^2.0.8" 98 | once "^1.3.1" 99 | pumpify "^1.3.3" 100 | request "^2.83.0" 101 | safe-buffer "^5.1.1" 102 | snakeize "^0.1.0" 103 | stream-events "^1.0.1" 104 | string-format-obj "^1.0.0" 105 | through2 "^2.0.0" 106 | 107 | "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": 108 | version "1.1.2" 109 | resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" 110 | 111 | "@protobufjs/base64@^1.1.2": 112 | version "1.1.2" 113 | resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" 114 | 115 | "@protobufjs/codegen@^2.0.4": 116 | version "2.0.4" 117 | resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" 118 | 119 | "@protobufjs/eventemitter@^1.1.0": 120 | version "1.1.0" 121 | resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" 122 | 123 | "@protobufjs/fetch@^1.1.0": 124 | version "1.1.0" 125 | resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" 126 | dependencies: 127 | "@protobufjs/aspromise" "^1.1.1" 128 | "@protobufjs/inquire" "^1.1.0" 129 | 130 | "@protobufjs/float@^1.0.2": 131 | version "1.0.2" 132 | resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" 133 | 134 | "@protobufjs/inquire@^1.1.0": 135 | version "1.1.0" 136 | resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" 137 | 138 | "@protobufjs/path@^1.1.2": 139 | version "1.1.2" 140 | resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" 141 | 142 | "@protobufjs/pool@^1.1.0": 143 | version "1.1.0" 144 | resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" 145 | 146 | "@protobufjs/utf8@^1.1.0": 147 | version "1.1.0" 148 | resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" 149 | 150 | "@types/google-cloud__storage@^1.1.1": 151 | version "1.1.7" 152 | resolved "https://registry.yarnpkg.com/@types/google-cloud__storage/-/google-cloud__storage-1.1.7.tgz#f4b568b163cce16314f32f954f5b7d5c9001fa86" 153 | dependencies: 154 | "@types/node" "*" 155 | 156 | "@types/long@^3.0.32": 157 | version "3.0.32" 158 | resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69" 159 | 160 | "@types/node@*": 161 | version "9.4.2" 162 | resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.2.tgz#b109a6c4f64147ccf9476d9e1a6fbf69a10faeb8" 163 | 164 | "@types/node@^8.0.53", "@types/node@^8.5.5": 165 | version "8.9.0" 166 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.0.tgz#99449266e9f023cc3ad5de304d759de787d18ea4" 167 | 168 | abbrev@1: 169 | version "1.1.1" 170 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 171 | 172 | acorn-es7-plugin@^1.0.12: 173 | version "1.1.7" 174 | resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b" 175 | 176 | acorn@^4.0.0: 177 | version "4.0.13" 178 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" 179 | 180 | ajv@^4.9.1: 181 | version "4.11.8" 182 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" 183 | dependencies: 184 | co "^4.6.0" 185 | json-stable-stringify "^1.0.1" 186 | 187 | ajv@^5.1.0: 188 | version "5.5.2" 189 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" 190 | dependencies: 191 | co "^4.6.0" 192 | fast-deep-equal "^1.0.0" 193 | fast-json-stable-stringify "^2.0.0" 194 | json-schema-traverse "^0.3.0" 195 | 196 | ansi-regex@^2.0.0: 197 | version "2.1.1" 198 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 199 | 200 | aproba@^1.0.3: 201 | version "1.2.0" 202 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" 203 | 204 | are-we-there-yet@~1.1.2: 205 | version "1.1.4" 206 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" 207 | dependencies: 208 | delegates "^1.0.0" 209 | readable-stream "^2.0.6" 210 | 211 | arguejs@^0.2.3: 212 | version "0.2.3" 213 | resolved "https://registry.yarnpkg.com/arguejs/-/arguejs-0.2.3.tgz#b6f939f5fe0e3cd1f3f93e2aa9262424bf312af7" 214 | 215 | array-filter@^1.0.0: 216 | version "1.0.0" 217 | resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" 218 | 219 | array-union@^1.0.1: 220 | version "1.0.2" 221 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" 222 | dependencies: 223 | array-uniq "^1.0.1" 224 | 225 | array-uniq@^1.0.1, array-uniq@^1.0.3: 226 | version "1.0.3" 227 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 228 | 229 | arrify@^1.0.0, arrify@^1.0.1: 230 | version "1.0.1" 231 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 232 | 233 | ascli@~1: 234 | version "1.0.1" 235 | resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" 236 | dependencies: 237 | colour "~0.7.1" 238 | optjs "~3.2.2" 239 | 240 | asn1@~0.2.3: 241 | version "0.2.3" 242 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 243 | 244 | assert-plus@1.0.0, assert-plus@^1.0.0: 245 | version "1.0.0" 246 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 247 | 248 | assert-plus@^0.2.0: 249 | version "0.2.0" 250 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" 251 | 252 | async@^2.0.1, async@^2.3.0, async@^2.4.0: 253 | version "2.6.0" 254 | resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" 255 | dependencies: 256 | lodash "^4.14.0" 257 | 258 | asynckit@^0.4.0: 259 | version "0.4.0" 260 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 261 | 262 | aws-sign2@~0.6.0: 263 | version "0.6.0" 264 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" 265 | 266 | aws-sign2@~0.7.0: 267 | version "0.7.0" 268 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 269 | 270 | aws4@^1.2.1, aws4@^1.6.0: 271 | version "1.6.0" 272 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 273 | 274 | balanced-match@^1.0.0: 275 | version "1.0.0" 276 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 277 | 278 | base64url@2.0.0, base64url@^2.0.0: 279 | version "2.0.0" 280 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 281 | 282 | bcrypt-pbkdf@^1.0.0: 283 | version "1.0.1" 284 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 285 | dependencies: 286 | tweetnacl "^0.14.3" 287 | 288 | block-stream@*: 289 | version "0.0.9" 290 | resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 291 | dependencies: 292 | inherits "~2.0.0" 293 | 294 | boom@2.x.x: 295 | version "2.10.1" 296 | resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" 297 | dependencies: 298 | hoek "2.x.x" 299 | 300 | boom@4.x.x: 301 | version "4.3.1" 302 | resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" 303 | dependencies: 304 | hoek "4.x.x" 305 | 306 | boom@5.x.x: 307 | version "5.2.0" 308 | resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" 309 | dependencies: 310 | hoek "4.x.x" 311 | 312 | brace-expansion@^1.1.7: 313 | version "1.1.8" 314 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 315 | dependencies: 316 | balanced-match "^1.0.0" 317 | concat-map "0.0.1" 318 | 319 | buffer-equal-constant-time@1.0.1: 320 | version "1.0.1" 321 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 322 | 323 | buffer-equal@^1.0.0: 324 | version "1.0.0" 325 | resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" 326 | 327 | bun@^0.0.12: 328 | version "0.0.12" 329 | resolved "https://registry.yarnpkg.com/bun/-/bun-0.0.12.tgz#d54fae69f895557f275423bc14b404030b20a5fc" 330 | dependencies: 331 | readable-stream "~1.0.32" 332 | 333 | bytebuffer@~5: 334 | version "5.0.1" 335 | resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" 336 | dependencies: 337 | long "~3" 338 | 339 | call-signature@0.0.2: 340 | version "0.0.2" 341 | resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996" 342 | 343 | camelcase@^2.0.1: 344 | version "2.1.1" 345 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 346 | 347 | capture-stack-trace@^1.0.0: 348 | version "1.0.0" 349 | resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" 350 | 351 | caseless@~0.12.0: 352 | version "0.12.0" 353 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 354 | 355 | cliui@^3.0.3: 356 | version "3.2.0" 357 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" 358 | dependencies: 359 | string-width "^1.0.1" 360 | strip-ansi "^3.0.1" 361 | wrap-ansi "^2.0.0" 362 | 363 | co@^4.6.0: 364 | version "4.6.0" 365 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 366 | 367 | code-point-at@^1.0.0: 368 | version "1.1.0" 369 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 370 | 371 | colour@~0.7.1: 372 | version "0.7.1" 373 | resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" 374 | 375 | combined-stream@^1.0.5, combined-stream@~1.0.5: 376 | version "1.0.5" 377 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 378 | dependencies: 379 | delayed-stream "~1.0.0" 380 | 381 | concat-map@0.0.1: 382 | version "0.0.1" 383 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 384 | 385 | concat-stream@^1.5.0, concat-stream@^1.6.0: 386 | version "1.6.0" 387 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 388 | dependencies: 389 | inherits "^2.0.3" 390 | readable-stream "^2.2.2" 391 | typedarray "^0.0.6" 392 | 393 | configstore@^3.0.0: 394 | version "3.1.1" 395 | resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" 396 | dependencies: 397 | dot-prop "^4.1.0" 398 | graceful-fs "^4.1.2" 399 | make-dir "^1.0.0" 400 | unique-string "^1.0.0" 401 | write-file-atomic "^2.0.0" 402 | xdg-basedir "^3.0.0" 403 | 404 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 405 | version "1.1.0" 406 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 407 | 408 | core-js@^2.0.0: 409 | version "2.5.3" 410 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" 411 | 412 | core-util-is@1.0.2, core-util-is@~1.0.0: 413 | version "1.0.2" 414 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 415 | 416 | create-error-class@^3.0.2: 417 | version "3.0.2" 418 | resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" 419 | dependencies: 420 | capture-stack-trace "^1.0.0" 421 | 422 | cryptiles@2.x.x: 423 | version "2.0.5" 424 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" 425 | dependencies: 426 | boom "2.x.x" 427 | 428 | cryptiles@3.x.x: 429 | version "3.1.2" 430 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" 431 | dependencies: 432 | boom "5.x.x" 433 | 434 | crypto-random-string@^1.0.0: 435 | version "1.0.0" 436 | resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" 437 | 438 | dashdash@^1.12.0: 439 | version "1.14.1" 440 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 441 | dependencies: 442 | assert-plus "^1.0.0" 443 | 444 | debug@^2.2.0: 445 | version "2.6.9" 446 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 447 | dependencies: 448 | ms "2.0.0" 449 | 450 | decamelize@^1.1.1: 451 | version "1.2.0" 452 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 453 | 454 | deep-extend@~0.4.0: 455 | version "0.4.2" 456 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" 457 | 458 | define-properties@^1.1.2: 459 | version "1.1.2" 460 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" 461 | dependencies: 462 | foreach "^2.0.5" 463 | object-keys "^1.0.8" 464 | 465 | delayed-stream@~1.0.0: 466 | version "1.0.0" 467 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 468 | 469 | delegates@^1.0.0: 470 | version "1.0.0" 471 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 472 | 473 | detect-libc@^1.0.2: 474 | version "1.0.3" 475 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" 476 | 477 | diff-match-patch@^1.0.0: 478 | version "1.0.0" 479 | resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.0.tgz#1cc3c83a490d67f95d91e39f6ad1f2e086b63048" 480 | 481 | dir-glob@^2.0.0: 482 | version "2.0.0" 483 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" 484 | dependencies: 485 | arrify "^1.0.1" 486 | path-type "^3.0.0" 487 | 488 | dot-prop@^4.1.0, dot-prop@^4.2.0: 489 | version "4.2.0" 490 | resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" 491 | dependencies: 492 | is-obj "^1.0.0" 493 | 494 | duplexify@^3.5.0, duplexify@^3.5.1, duplexify@^3.5.3: 495 | version "3.5.3" 496 | resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" 497 | dependencies: 498 | end-of-stream "^1.0.0" 499 | inherits "^2.0.1" 500 | readable-stream "^2.0.0" 501 | stream-shift "^1.0.0" 502 | 503 | eastasianwidth@^0.1.1: 504 | version "0.1.1" 505 | resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.1.1.tgz#44d656de9da415694467335365fb3147b8572b7c" 506 | 507 | ecc-jsbn@~0.1.1: 508 | version "0.1.1" 509 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 510 | dependencies: 511 | jsbn "~0.1.0" 512 | 513 | ecdsa-sig-formatter@1.0.9: 514 | version "1.0.9" 515 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 516 | dependencies: 517 | base64url "^2.0.0" 518 | safe-buffer "^5.0.1" 519 | 520 | empower-core@^0.6.2: 521 | version "0.6.2" 522 | resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-0.6.2.tgz#5adef566088e31fba80ba0a36df47d7094169144" 523 | dependencies: 524 | call-signature "0.0.2" 525 | core-js "^2.0.0" 526 | 527 | empower@^1.2.3: 528 | version "1.2.3" 529 | resolved "https://registry.yarnpkg.com/empower/-/empower-1.2.3.tgz#6f0da73447f4edd838fec5c60313a88ba5cb852b" 530 | dependencies: 531 | core-js "^2.0.0" 532 | empower-core "^0.6.2" 533 | 534 | end-of-stream@^1.0.0, end-of-stream@^1.1.0: 535 | version "1.4.1" 536 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" 537 | dependencies: 538 | once "^1.4.0" 539 | 540 | ent@^2.2.0: 541 | version "2.2.0" 542 | resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" 543 | 544 | espurify@^1.6.0: 545 | version "1.7.0" 546 | resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.7.0.tgz#1c5cf6cbccc32e6f639380bd4f991fab9ba9d226" 547 | dependencies: 548 | core-js "^2.0.0" 549 | 550 | estraverse@^4.1.0, estraverse@^4.2.0: 551 | version "4.2.0" 552 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" 553 | 554 | extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1: 555 | version "3.0.1" 556 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 557 | 558 | extsprintf@1.3.0: 559 | version "1.3.0" 560 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 561 | 562 | extsprintf@^1.2.0: 563 | version "1.4.0" 564 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 565 | 566 | fast-deep-equal@^1.0.0: 567 | version "1.0.0" 568 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" 569 | 570 | fast-json-stable-stringify@^2.0.0: 571 | version "2.0.0" 572 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 573 | 574 | faye-websocket@0.11.1: 575 | version "0.11.1" 576 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" 577 | dependencies: 578 | websocket-driver ">=0.5.1" 579 | 580 | faye-websocket@0.9.3: 581 | version "0.9.3" 582 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.3.tgz#482a505b0df0ae626b969866d3bd740cdb962e83" 583 | dependencies: 584 | websocket-driver ">=0.5.1" 585 | 586 | firebase-admin@^5.8.2: 587 | version "5.8.2" 588 | resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-5.8.2.tgz#4c390df3b6fd32567d8d4558ac04974bebd15749" 589 | dependencies: 590 | "@firebase/app" "^0.1.1" 591 | "@firebase/database" "^0.1.3" 592 | "@google-cloud/firestore" "^0.11.2" 593 | "@google-cloud/storage" "^1.2.1" 594 | "@types/google-cloud__storage" "^1.1.1" 595 | "@types/node" "^8.0.53" 596 | faye-websocket "0.9.3" 597 | jsonwebtoken "8.1.0" 598 | node-forge "0.7.1" 599 | 600 | foreach@^2.0.5: 601 | version "2.0.5" 602 | resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 603 | 604 | forever-agent@~0.6.1: 605 | version "0.6.1" 606 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 607 | 608 | form-data@~2.1.1: 609 | version "2.1.4" 610 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" 611 | dependencies: 612 | asynckit "^0.4.0" 613 | combined-stream "^1.0.5" 614 | mime-types "^2.1.12" 615 | 616 | form-data@~2.3.1: 617 | version "2.3.1" 618 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" 619 | dependencies: 620 | asynckit "^0.4.0" 621 | combined-stream "^1.0.5" 622 | mime-types "^2.1.12" 623 | 624 | fs.realpath@^1.0.0: 625 | version "1.0.0" 626 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 627 | 628 | fstream-ignore@^1.0.5: 629 | version "1.0.5" 630 | resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" 631 | dependencies: 632 | fstream "^1.0.0" 633 | inherits "2" 634 | minimatch "^3.0.0" 635 | 636 | fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: 637 | version "1.0.11" 638 | resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" 639 | dependencies: 640 | graceful-fs "^4.1.2" 641 | inherits "~2.0.0" 642 | mkdirp ">=0.5 0" 643 | rimraf "2" 644 | 645 | functional-red-black-tree@^1.0.1: 646 | version "1.0.1" 647 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 648 | 649 | gauge@~2.7.3: 650 | version "2.7.4" 651 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 652 | dependencies: 653 | aproba "^1.0.3" 654 | console-control-strings "^1.0.0" 655 | has-unicode "^2.0.0" 656 | object-assign "^4.1.0" 657 | signal-exit "^3.0.0" 658 | string-width "^1.0.1" 659 | strip-ansi "^3.0.1" 660 | wide-align "^1.1.0" 661 | 662 | gcp-metadata@^0.3.0: 663 | version "0.3.1" 664 | resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.3.1.tgz#313814456e7c3d0eeb8f8b084b33579e886f829a" 665 | dependencies: 666 | extend "^3.0.0" 667 | retry-request "^3.0.0" 668 | 669 | gcp-metadata@^0.4.1: 670 | version "0.4.1" 671 | resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.4.1.tgz#64623b84175357cc119ad7a6aec759392a90a58b" 672 | dependencies: 673 | extend "^3.0.0" 674 | retry-request "^3.1.0" 675 | 676 | gcs-resumable-upload@^0.8.2: 677 | version "0.8.2" 678 | resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-0.8.2.tgz#37df02470430395a789a637e72cabc80677ae964" 679 | dependencies: 680 | buffer-equal "^1.0.0" 681 | configstore "^3.0.0" 682 | google-auto-auth "^0.7.1" 683 | pumpify "^1.3.3" 684 | request "^2.81.0" 685 | stream-events "^1.0.1" 686 | through2 "^2.0.0" 687 | 688 | getpass@^0.1.1: 689 | version "0.1.7" 690 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 691 | dependencies: 692 | assert-plus "^1.0.0" 693 | 694 | glob@^7.0.5, glob@^7.1.2: 695 | version "7.1.2" 696 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 697 | dependencies: 698 | fs.realpath "^1.0.0" 699 | inflight "^1.0.4" 700 | inherits "2" 701 | minimatch "^3.0.4" 702 | once "^1.3.0" 703 | path-is-absolute "^1.0.0" 704 | 705 | globby@^7.1.1: 706 | version "7.1.1" 707 | resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" 708 | dependencies: 709 | array-union "^1.0.1" 710 | dir-glob "^2.0.0" 711 | glob "^7.1.2" 712 | ignore "^3.3.5" 713 | pify "^3.0.0" 714 | slash "^1.0.0" 715 | 716 | google-auth-library@^0.10.0: 717 | version "0.10.0" 718 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" 719 | dependencies: 720 | gtoken "^1.2.1" 721 | jws "^3.1.4" 722 | lodash.noop "^3.0.1" 723 | request "^2.74.0" 724 | 725 | google-auth-library@^0.12.0: 726 | version "0.12.0" 727 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.12.0.tgz#a3fc6c296d00bb54e4d877ef581a05947330d07f" 728 | dependencies: 729 | gtoken "^1.2.3" 730 | jws "^3.1.4" 731 | lodash.isstring "^4.0.1" 732 | lodash.merge "^4.6.0" 733 | request "^2.81.0" 734 | 735 | google-auto-auth@^0.7.1: 736 | version "0.7.2" 737 | resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.7.2.tgz#bf9352d5c4a0897bf31fd9c491028b765fbea71e" 738 | dependencies: 739 | async "^2.3.0" 740 | gcp-metadata "^0.3.0" 741 | google-auth-library "^0.10.0" 742 | request "^2.79.0" 743 | 744 | google-auto-auth@^0.8.0: 745 | version "0.8.2" 746 | resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.8.2.tgz#928ee8954514a2ea179de8dd4e97f04d40d13d0a" 747 | dependencies: 748 | async "^2.3.0" 749 | gcp-metadata "^0.3.0" 750 | google-auth-library "^0.12.0" 751 | request "^2.79.0" 752 | 753 | google-auto-auth@^0.9.0: 754 | version "0.9.3" 755 | resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.9.3.tgz#544c36da639890cf56040b0246859060f140715a" 756 | dependencies: 757 | async "^2.3.0" 758 | gcp-metadata "^0.4.1" 759 | google-auth-library "^0.12.0" 760 | request "^2.79.0" 761 | 762 | google-gax@^0.14.3: 763 | version "0.14.5" 764 | resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-0.14.5.tgz#b2c73c61df6cead94f90421d17f44ff161f623c7" 765 | dependencies: 766 | extend "^3.0.0" 767 | globby "^7.1.1" 768 | google-auto-auth "^0.9.0" 769 | google-proto-files "^0.14.1" 770 | grpc "~1.7.2" 771 | is-stream-ended "^0.1.0" 772 | lodash "^4.17.2" 773 | protobufjs "^6.8.0" 774 | readable-stream "^2.2.2" 775 | through2 "^2.0.3" 776 | 777 | google-p12-pem@^0.1.0: 778 | version "0.1.2" 779 | resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" 780 | dependencies: 781 | node-forge "^0.7.1" 782 | 783 | google-proto-files@^0.14.1: 784 | version "0.14.2" 785 | resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-0.14.2.tgz#958cffea7e8888e00b9a6c55ed1362c06b426f4c" 786 | dependencies: 787 | globby "^7.1.1" 788 | power-assert "^1.4.4" 789 | prettier "^1.10.2" 790 | protobufjs "^6.8.0" 791 | 792 | graceful-fs@^4.1.11, graceful-fs@^4.1.2: 793 | version "4.1.11" 794 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 795 | 796 | grpc@~1.7.2: 797 | version "1.7.3" 798 | resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.7.3.tgz#c9d034324e2ec8a06cfaa577a044a116f96c8c90" 799 | dependencies: 800 | arguejs "^0.2.3" 801 | lodash "^4.15.0" 802 | nan "^2.0.0" 803 | node-pre-gyp "^0.6.39" 804 | protobufjs "^5.0.0" 805 | 806 | gtoken@^1.2.1, gtoken@^1.2.3: 807 | version "1.2.3" 808 | resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" 809 | dependencies: 810 | google-p12-pem "^0.1.0" 811 | jws "^3.0.0" 812 | mime "^1.4.1" 813 | request "^2.72.0" 814 | 815 | har-schema@^1.0.5: 816 | version "1.0.5" 817 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" 818 | 819 | har-schema@^2.0.0: 820 | version "2.0.0" 821 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 822 | 823 | har-validator@~4.2.1: 824 | version "4.2.1" 825 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" 826 | dependencies: 827 | ajv "^4.9.1" 828 | har-schema "^1.0.5" 829 | 830 | har-validator@~5.0.3: 831 | version "5.0.3" 832 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" 833 | dependencies: 834 | ajv "^5.1.0" 835 | har-schema "^2.0.0" 836 | 837 | has-unicode@^2.0.0: 838 | version "2.0.1" 839 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 840 | 841 | hash-stream-validation@^0.2.1: 842 | version "0.2.1" 843 | resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz#ecc9b997b218be5bb31298628bb807869b73dcd1" 844 | dependencies: 845 | through2 "^2.0.0" 846 | 847 | hawk@3.1.3, hawk@~3.1.3: 848 | version "3.1.3" 849 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" 850 | dependencies: 851 | boom "2.x.x" 852 | cryptiles "2.x.x" 853 | hoek "2.x.x" 854 | sntp "1.x.x" 855 | 856 | hawk@~6.0.2: 857 | version "6.0.2" 858 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" 859 | dependencies: 860 | boom "4.x.x" 861 | cryptiles "3.x.x" 862 | hoek "4.x.x" 863 | sntp "2.x.x" 864 | 865 | hoek@2.x.x: 866 | version "2.16.3" 867 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 868 | 869 | hoek@4.x.x: 870 | version "4.2.0" 871 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" 872 | 873 | http-parser-js@>=0.4.0: 874 | version "0.4.10" 875 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" 876 | 877 | http-signature@~1.1.0: 878 | version "1.1.1" 879 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" 880 | dependencies: 881 | assert-plus "^0.2.0" 882 | jsprim "^1.2.2" 883 | sshpk "^1.7.0" 884 | 885 | http-signature@~1.2.0: 886 | version "1.2.0" 887 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 888 | dependencies: 889 | assert-plus "^1.0.0" 890 | jsprim "^1.2.2" 891 | sshpk "^1.7.0" 892 | 893 | ignore@^3.3.5: 894 | version "3.3.7" 895 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" 896 | 897 | imurmurhash@^0.1.4: 898 | version "0.1.4" 899 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 900 | 901 | indexof@0.0.1: 902 | version "0.0.1" 903 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 904 | 905 | inflight@^1.0.4: 906 | version "1.0.6" 907 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 908 | dependencies: 909 | once "^1.3.0" 910 | wrappy "1" 911 | 912 | inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: 913 | version "2.0.3" 914 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 915 | 916 | ini@~1.3.0: 917 | version "1.3.5" 918 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 919 | 920 | invert-kv@^1.0.0: 921 | version "1.0.0" 922 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" 923 | 924 | is-fullwidth-code-point@^1.0.0: 925 | version "1.0.0" 926 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 927 | dependencies: 928 | number-is-nan "^1.0.0" 929 | 930 | is-obj@^1.0.0: 931 | version "1.0.1" 932 | resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" 933 | 934 | is-stream-ended@^0.1.0: 935 | version "0.1.3" 936 | resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.3.tgz#a0473b267c756635486beedc7e3344e549d152ac" 937 | 938 | is-typedarray@~1.0.0: 939 | version "1.0.0" 940 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 941 | 942 | is@^3.0.1, is@^3.2.0, is@^3.2.1: 943 | version "3.2.1" 944 | resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" 945 | 946 | isarray@0.0.1: 947 | version "0.0.1" 948 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 949 | 950 | isarray@~1.0.0: 951 | version "1.0.0" 952 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 953 | 954 | isstream@~0.1.2: 955 | version "0.1.2" 956 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 957 | 958 | jsbn@~0.1.0: 959 | version "0.1.1" 960 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 961 | 962 | json-schema-traverse@^0.3.0: 963 | version "0.3.1" 964 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" 965 | 966 | json-schema@0.2.3: 967 | version "0.2.3" 968 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 969 | 970 | json-stable-stringify@^1.0.1: 971 | version "1.0.1" 972 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 973 | dependencies: 974 | jsonify "~0.0.0" 975 | 976 | json-stringify-safe@~5.0.1: 977 | version "5.0.1" 978 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 979 | 980 | jsonify@~0.0.0: 981 | version "0.0.0" 982 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 983 | 984 | jsonwebtoken@8.1.0: 985 | version "8.1.0" 986 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" 987 | dependencies: 988 | jws "^3.1.4" 989 | lodash.includes "^4.3.0" 990 | lodash.isboolean "^3.0.3" 991 | lodash.isinteger "^4.0.4" 992 | lodash.isnumber "^3.0.3" 993 | lodash.isplainobject "^4.0.6" 994 | lodash.isstring "^4.0.1" 995 | lodash.once "^4.0.0" 996 | ms "^2.0.0" 997 | xtend "^4.0.1" 998 | 999 | jsprim@^1.2.2: 1000 | version "1.4.1" 1001 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 1002 | dependencies: 1003 | assert-plus "1.0.0" 1004 | extsprintf "1.3.0" 1005 | json-schema "0.2.3" 1006 | verror "1.10.0" 1007 | 1008 | jwa@^1.1.4: 1009 | version "1.1.5" 1010 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 1011 | dependencies: 1012 | base64url "2.0.0" 1013 | buffer-equal-constant-time "1.0.1" 1014 | ecdsa-sig-formatter "1.0.9" 1015 | safe-buffer "^5.0.1" 1016 | 1017 | jws@^3.0.0, jws@^3.1.4: 1018 | version "3.1.4" 1019 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 1020 | dependencies: 1021 | base64url "^2.0.0" 1022 | jwa "^1.1.4" 1023 | safe-buffer "^5.0.1" 1024 | 1025 | lcid@^1.0.0: 1026 | version "1.0.0" 1027 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" 1028 | dependencies: 1029 | invert-kv "^1.0.0" 1030 | 1031 | lodash.includes@^4.3.0: 1032 | version "4.3.0" 1033 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 1034 | 1035 | lodash.isboolean@^3.0.3: 1036 | version "3.0.3" 1037 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 1038 | 1039 | lodash.isinteger@^4.0.4: 1040 | version "4.0.4" 1041 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 1042 | 1043 | lodash.isnumber@^3.0.3: 1044 | version "3.0.3" 1045 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 1046 | 1047 | lodash.isplainobject@^4.0.6: 1048 | version "4.0.6" 1049 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 1050 | 1051 | lodash.isstring@^4.0.1: 1052 | version "4.0.1" 1053 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 1054 | 1055 | lodash.merge@^4.6.0: 1056 | version "4.6.1" 1057 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" 1058 | 1059 | lodash.noop@^3.0.1: 1060 | version "3.0.1" 1061 | resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" 1062 | 1063 | lodash.once@^4.0.0: 1064 | version "4.1.1" 1065 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 1066 | 1067 | lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2: 1068 | version "4.17.5" 1069 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" 1070 | 1071 | log-driver@^1.2.5: 1072 | version "1.2.5" 1073 | resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" 1074 | 1075 | long@^3.2.0, long@~3: 1076 | version "3.2.0" 1077 | resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" 1078 | 1079 | make-dir@^1.0.0: 1080 | version "1.1.0" 1081 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" 1082 | dependencies: 1083 | pify "^3.0.0" 1084 | 1085 | methmeth@^1.1.0: 1086 | version "1.1.0" 1087 | resolved "https://registry.yarnpkg.com/methmeth/-/methmeth-1.1.0.tgz#e80a26618e52f5c4222861bb748510bd10e29089" 1088 | 1089 | mime-db@~1.30.0: 1090 | version "1.30.0" 1091 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 1092 | 1093 | mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7: 1094 | version "2.1.17" 1095 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 1096 | dependencies: 1097 | mime-db "~1.30.0" 1098 | 1099 | mime@^1.4.1: 1100 | version "1.6.0" 1101 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 1102 | 1103 | minimatch@^3.0.0, minimatch@^3.0.4: 1104 | version "3.0.4" 1105 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 1106 | dependencies: 1107 | brace-expansion "^1.1.7" 1108 | 1109 | minimist@0.0.8: 1110 | version "0.0.8" 1111 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 1112 | 1113 | minimist@^1.2.0: 1114 | version "1.2.0" 1115 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 1116 | 1117 | "mkdirp@>=0.5 0", mkdirp@^0.5.1: 1118 | version "0.5.1" 1119 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 1120 | dependencies: 1121 | minimist "0.0.8" 1122 | 1123 | modelo@^4.2.0: 1124 | version "4.2.3" 1125 | resolved "https://registry.yarnpkg.com/modelo/-/modelo-4.2.3.tgz#b278588a4db87fc1e5107ae3a277c0876f38d894" 1126 | 1127 | ms@2.0.0: 1128 | version "2.0.0" 1129 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1130 | 1131 | ms@^2.0.0: 1132 | version "2.1.1" 1133 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 1134 | 1135 | nan@^2.0.0: 1136 | version "2.8.0" 1137 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" 1138 | 1139 | node-forge@0.7.1, node-forge@^0.7.1: 1140 | version "0.7.1" 1141 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" 1142 | 1143 | node-pre-gyp@^0.6.39: 1144 | version "0.6.39" 1145 | resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" 1146 | dependencies: 1147 | detect-libc "^1.0.2" 1148 | hawk "3.1.3" 1149 | mkdirp "^0.5.1" 1150 | nopt "^4.0.1" 1151 | npmlog "^4.0.2" 1152 | rc "^1.1.7" 1153 | request "2.81.0" 1154 | rimraf "^2.6.1" 1155 | semver "^5.3.0" 1156 | tar "^2.2.1" 1157 | tar-pack "^3.4.0" 1158 | 1159 | nopt@^4.0.1: 1160 | version "4.0.1" 1161 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 1162 | dependencies: 1163 | abbrev "1" 1164 | osenv "^0.1.4" 1165 | 1166 | npmlog@^4.0.2: 1167 | version "4.1.2" 1168 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" 1169 | dependencies: 1170 | are-we-there-yet "~1.1.2" 1171 | console-control-strings "~1.1.0" 1172 | gauge "~2.7.3" 1173 | set-blocking "~2.0.0" 1174 | 1175 | number-is-nan@^1.0.0: 1176 | version "1.0.1" 1177 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 1178 | 1179 | oauth-sign@~0.8.1, oauth-sign@~0.8.2: 1180 | version "0.8.2" 1181 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 1182 | 1183 | object-assign@^4.1.0: 1184 | version "4.1.1" 1185 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1186 | 1187 | object-keys@^1.0.0, object-keys@^1.0.8: 1188 | version "1.0.11" 1189 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" 1190 | 1191 | once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: 1192 | version "1.4.0" 1193 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1194 | dependencies: 1195 | wrappy "1" 1196 | 1197 | optjs@~3.2.2: 1198 | version "3.2.2" 1199 | resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" 1200 | 1201 | os-homedir@^1.0.0: 1202 | version "1.0.2" 1203 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 1204 | 1205 | os-locale@^1.4.0: 1206 | version "1.4.0" 1207 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" 1208 | dependencies: 1209 | lcid "^1.0.0" 1210 | 1211 | os-tmpdir@^1.0.0: 1212 | version "1.0.2" 1213 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 1214 | 1215 | osenv@^0.1.4: 1216 | version "0.1.4" 1217 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" 1218 | dependencies: 1219 | os-homedir "^1.0.0" 1220 | os-tmpdir "^1.0.0" 1221 | 1222 | path-is-absolute@^1.0.0: 1223 | version "1.0.1" 1224 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1225 | 1226 | path-type@^3.0.0: 1227 | version "3.0.0" 1228 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" 1229 | dependencies: 1230 | pify "^3.0.0" 1231 | 1232 | performance-now@^0.2.0: 1233 | version "0.2.0" 1234 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" 1235 | 1236 | performance-now@^2.1.0: 1237 | version "2.1.0" 1238 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 1239 | 1240 | pify@^3.0.0: 1241 | version "3.0.0" 1242 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" 1243 | 1244 | power-assert-context-formatter@^1.0.7: 1245 | version "1.1.1" 1246 | resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz#edba352d3ed8a603114d667265acce60d689ccdf" 1247 | dependencies: 1248 | core-js "^2.0.0" 1249 | power-assert-context-traversal "^1.1.1" 1250 | 1251 | power-assert-context-reducer-ast@^1.0.7: 1252 | version "1.1.2" 1253 | resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz#484a99e26f4973ff8832e5c5cc756702e6094174" 1254 | dependencies: 1255 | acorn "^4.0.0" 1256 | acorn-es7-plugin "^1.0.12" 1257 | core-js "^2.0.0" 1258 | espurify "^1.6.0" 1259 | estraverse "^4.2.0" 1260 | 1261 | power-assert-context-traversal@^1.1.1: 1262 | version "1.1.1" 1263 | resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz#88cabca0d13b6359f07d3d3e8afa699264577ed9" 1264 | dependencies: 1265 | core-js "^2.0.0" 1266 | estraverse "^4.1.0" 1267 | 1268 | power-assert-formatter@^1.3.1: 1269 | version "1.4.1" 1270 | resolved "https://registry.yarnpkg.com/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a" 1271 | dependencies: 1272 | core-js "^2.0.0" 1273 | power-assert-context-formatter "^1.0.7" 1274 | power-assert-context-reducer-ast "^1.0.7" 1275 | power-assert-renderer-assertion "^1.0.7" 1276 | power-assert-renderer-comparison "^1.0.7" 1277 | power-assert-renderer-diagram "^1.0.7" 1278 | power-assert-renderer-file "^1.0.7" 1279 | 1280 | power-assert-renderer-assertion@^1.0.7: 1281 | version "1.1.1" 1282 | resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz#cbfc0e77e0086a8f96af3f1d8e67b9ee7e28ce98" 1283 | dependencies: 1284 | power-assert-renderer-base "^1.1.1" 1285 | power-assert-util-string-width "^1.1.1" 1286 | 1287 | power-assert-renderer-base@^1.1.1: 1288 | version "1.1.1" 1289 | resolved "https://registry.yarnpkg.com/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb" 1290 | 1291 | power-assert-renderer-comparison@^1.0.7: 1292 | version "1.1.1" 1293 | resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz#d7439d97d85156be4e30a00f2fb5a72514ce3c08" 1294 | dependencies: 1295 | core-js "^2.0.0" 1296 | diff-match-patch "^1.0.0" 1297 | power-assert-renderer-base "^1.1.1" 1298 | stringifier "^1.3.0" 1299 | type-name "^2.0.1" 1300 | 1301 | power-assert-renderer-diagram@^1.0.7: 1302 | version "1.1.2" 1303 | resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz#655f8f711935a9b6d541b86327654717c637a986" 1304 | dependencies: 1305 | core-js "^2.0.0" 1306 | power-assert-renderer-base "^1.1.1" 1307 | power-assert-util-string-width "^1.1.1" 1308 | stringifier "^1.3.0" 1309 | 1310 | power-assert-renderer-file@^1.0.7: 1311 | version "1.1.1" 1312 | resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz#a37e2bbd178ccacd04e78dbb79c92fe34933c5e7" 1313 | dependencies: 1314 | power-assert-renderer-base "^1.1.1" 1315 | 1316 | power-assert-util-string-width@^1.1.1: 1317 | version "1.1.1" 1318 | resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz#be659eb7937fdd2e6c9a77268daaf64bd5b7c592" 1319 | dependencies: 1320 | eastasianwidth "^0.1.1" 1321 | 1322 | power-assert@^1.4.4: 1323 | version "1.4.4" 1324 | resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.4.4.tgz#9295ea7437196f5a601fde420f042631186d7517" 1325 | dependencies: 1326 | define-properties "^1.1.2" 1327 | empower "^1.2.3" 1328 | power-assert-formatter "^1.3.1" 1329 | universal-deep-strict-equal "^1.2.1" 1330 | xtend "^4.0.0" 1331 | 1332 | prettier@^1.10.2: 1333 | version "1.10.2" 1334 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" 1335 | 1336 | process-nextick-args@~1.0.6: 1337 | version "1.0.7" 1338 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 1339 | 1340 | protobufjs@^5.0.0: 1341 | version "5.0.2" 1342 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.2.tgz#59748d7dcf03d2db22c13da9feb024e16ab80c91" 1343 | dependencies: 1344 | ascli "~1" 1345 | bytebuffer "~5" 1346 | glob "^7.0.5" 1347 | yargs "^3.10.0" 1348 | 1349 | protobufjs@^6.8.0: 1350 | version "6.8.4" 1351 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.4.tgz#183f90d1c4aca5f6b34a79eaedd0d89ad21f603b" 1352 | dependencies: 1353 | "@protobufjs/aspromise" "^1.1.2" 1354 | "@protobufjs/base64" "^1.1.2" 1355 | "@protobufjs/codegen" "^2.0.4" 1356 | "@protobufjs/eventemitter" "^1.1.0" 1357 | "@protobufjs/fetch" "^1.1.0" 1358 | "@protobufjs/float" "^1.0.2" 1359 | "@protobufjs/inquire" "^1.1.0" 1360 | "@protobufjs/path" "^1.1.2" 1361 | "@protobufjs/pool" "^1.1.0" 1362 | "@protobufjs/utf8" "^1.1.0" 1363 | "@types/long" "^3.0.32" 1364 | "@types/node" "^8.5.5" 1365 | long "^3.2.0" 1366 | 1367 | pump@^2.0.0: 1368 | version "2.0.1" 1369 | resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" 1370 | dependencies: 1371 | end-of-stream "^1.1.0" 1372 | once "^1.3.1" 1373 | 1374 | pumpify@^1.3.3: 1375 | version "1.4.0" 1376 | resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" 1377 | dependencies: 1378 | duplexify "^3.5.3" 1379 | inherits "^2.0.3" 1380 | pump "^2.0.0" 1381 | 1382 | punycode@^1.4.1: 1383 | version "1.4.1" 1384 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 1385 | 1386 | qs@~6.4.0: 1387 | version "6.4.0" 1388 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 1389 | 1390 | qs@~6.5.1: 1391 | version "6.5.1" 1392 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 1393 | 1394 | rc@^1.1.7: 1395 | version "1.2.5" 1396 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" 1397 | dependencies: 1398 | deep-extend "~0.4.0" 1399 | ini "~1.3.0" 1400 | minimist "^1.2.0" 1401 | strip-json-comments "~2.0.1" 1402 | 1403 | readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2: 1404 | version "2.3.3" 1405 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 1406 | dependencies: 1407 | core-util-is "~1.0.0" 1408 | inherits "~2.0.3" 1409 | isarray "~1.0.0" 1410 | process-nextick-args "~1.0.6" 1411 | safe-buffer "~5.1.1" 1412 | string_decoder "~1.0.3" 1413 | util-deprecate "~1.0.1" 1414 | 1415 | readable-stream@~1.0.32: 1416 | version "1.0.34" 1417 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" 1418 | dependencies: 1419 | core-util-is "~1.0.0" 1420 | inherits "~2.0.1" 1421 | isarray "0.0.1" 1422 | string_decoder "~0.10.x" 1423 | 1424 | request@2.81.0: 1425 | version "2.81.0" 1426 | resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" 1427 | dependencies: 1428 | aws-sign2 "~0.6.0" 1429 | aws4 "^1.2.1" 1430 | caseless "~0.12.0" 1431 | combined-stream "~1.0.5" 1432 | extend "~3.0.0" 1433 | forever-agent "~0.6.1" 1434 | form-data "~2.1.1" 1435 | har-validator "~4.2.1" 1436 | hawk "~3.1.3" 1437 | http-signature "~1.1.0" 1438 | is-typedarray "~1.0.0" 1439 | isstream "~0.1.2" 1440 | json-stringify-safe "~5.0.1" 1441 | mime-types "~2.1.7" 1442 | oauth-sign "~0.8.1" 1443 | performance-now "^0.2.0" 1444 | qs "~6.4.0" 1445 | safe-buffer "^5.0.1" 1446 | stringstream "~0.0.4" 1447 | tough-cookie "~2.3.0" 1448 | tunnel-agent "^0.6.0" 1449 | uuid "^3.0.0" 1450 | 1451 | request@^2.72.0, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@^2.83.0: 1452 | version "2.83.0" 1453 | resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" 1454 | dependencies: 1455 | aws-sign2 "~0.7.0" 1456 | aws4 "^1.6.0" 1457 | caseless "~0.12.0" 1458 | combined-stream "~1.0.5" 1459 | extend "~3.0.1" 1460 | forever-agent "~0.6.1" 1461 | form-data "~2.3.1" 1462 | har-validator "~5.0.3" 1463 | hawk "~6.0.2" 1464 | http-signature "~1.2.0" 1465 | is-typedarray "~1.0.0" 1466 | isstream "~0.1.2" 1467 | json-stringify-safe "~5.0.1" 1468 | mime-types "~2.1.17" 1469 | oauth-sign "~0.8.2" 1470 | performance-now "^2.1.0" 1471 | qs "~6.5.1" 1472 | safe-buffer "^5.1.1" 1473 | stringstream "~0.0.5" 1474 | tough-cookie "~2.3.3" 1475 | tunnel-agent "^0.6.0" 1476 | uuid "^3.1.0" 1477 | 1478 | retry-request@^3.0.0, retry-request@^3.1.0, retry-request@^3.3.1: 1479 | version "3.3.1" 1480 | resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-3.3.1.tgz#fb71276235a617e97551e9be737ab5b91591fb9e" 1481 | dependencies: 1482 | request "^2.81.0" 1483 | through2 "^2.0.0" 1484 | 1485 | rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: 1486 | version "2.6.2" 1487 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 1488 | dependencies: 1489 | glob "^7.0.5" 1490 | 1491 | safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1492 | version "5.1.1" 1493 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 1494 | 1495 | semver@^5.3.0: 1496 | version "5.5.0" 1497 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" 1498 | 1499 | set-blocking@~2.0.0: 1500 | version "2.0.0" 1501 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 1502 | 1503 | signal-exit@^3.0.0, signal-exit@^3.0.2: 1504 | version "3.0.2" 1505 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1506 | 1507 | slash@^1.0.0: 1508 | version "1.0.0" 1509 | resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 1510 | 1511 | snakeize@^0.1.0: 1512 | version "0.1.0" 1513 | resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" 1514 | 1515 | sntp@1.x.x: 1516 | version "1.0.9" 1517 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" 1518 | dependencies: 1519 | hoek "2.x.x" 1520 | 1521 | sntp@2.x.x: 1522 | version "2.1.0" 1523 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" 1524 | dependencies: 1525 | hoek "4.x.x" 1526 | 1527 | split-array-stream@^1.0.0: 1528 | version "1.0.3" 1529 | resolved "https://registry.yarnpkg.com/split-array-stream/-/split-array-stream-1.0.3.tgz#d2b75a8e5e0d824d52fdec8b8225839dc2e35dfa" 1530 | dependencies: 1531 | async "^2.4.0" 1532 | is-stream-ended "^0.1.0" 1533 | 1534 | sshpk@^1.7.0: 1535 | version "1.13.1" 1536 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" 1537 | dependencies: 1538 | asn1 "~0.2.3" 1539 | assert-plus "^1.0.0" 1540 | dashdash "^1.12.0" 1541 | getpass "^0.1.1" 1542 | optionalDependencies: 1543 | bcrypt-pbkdf "^1.0.0" 1544 | ecc-jsbn "~0.1.1" 1545 | jsbn "~0.1.0" 1546 | tweetnacl "~0.14.0" 1547 | 1548 | stream-events@^1.0.1: 1549 | version "1.0.2" 1550 | resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.2.tgz#abf39f66c0890a4eb795bc8d5e859b2615b590b2" 1551 | dependencies: 1552 | stubs "^3.0.0" 1553 | 1554 | stream-shift@^1.0.0: 1555 | version "1.0.0" 1556 | resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" 1557 | 1558 | string-format-obj@^1.0.0, string-format-obj@^1.1.0: 1559 | version "1.1.1" 1560 | resolved "https://registry.yarnpkg.com/string-format-obj/-/string-format-obj-1.1.1.tgz#c7612ca4e2ad923812a81db192dc291850aa1f65" 1561 | 1562 | string-width@^1.0.1, string-width@^1.0.2: 1563 | version "1.0.2" 1564 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 1565 | dependencies: 1566 | code-point-at "^1.0.0" 1567 | is-fullwidth-code-point "^1.0.0" 1568 | strip-ansi "^3.0.0" 1569 | 1570 | string_decoder@~0.10.x: 1571 | version "0.10.31" 1572 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 1573 | 1574 | string_decoder@~1.0.3: 1575 | version "1.0.3" 1576 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 1577 | dependencies: 1578 | safe-buffer "~5.1.0" 1579 | 1580 | stringifier@^1.3.0: 1581 | version "1.3.0" 1582 | resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.3.0.tgz#def18342f6933db0f2dbfc9aa02175b448c17959" 1583 | dependencies: 1584 | core-js "^2.0.0" 1585 | traverse "^0.6.6" 1586 | type-name "^2.0.1" 1587 | 1588 | stringstream@~0.0.4, stringstream@~0.0.5: 1589 | version "0.0.5" 1590 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 1591 | 1592 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 1593 | version "3.0.1" 1594 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 1595 | dependencies: 1596 | ansi-regex "^2.0.0" 1597 | 1598 | strip-json-comments@~2.0.1: 1599 | version "2.0.1" 1600 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1601 | 1602 | stubs@^3.0.0: 1603 | version "3.0.0" 1604 | resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" 1605 | 1606 | tar-pack@^3.4.0: 1607 | version "3.4.1" 1608 | resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" 1609 | dependencies: 1610 | debug "^2.2.0" 1611 | fstream "^1.0.10" 1612 | fstream-ignore "^1.0.5" 1613 | once "^1.3.3" 1614 | readable-stream "^2.1.4" 1615 | rimraf "^2.5.1" 1616 | tar "^2.2.1" 1617 | uid-number "^0.0.6" 1618 | 1619 | tar@^2.2.1: 1620 | version "2.2.1" 1621 | resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" 1622 | dependencies: 1623 | block-stream "*" 1624 | fstream "^1.0.2" 1625 | inherits "2" 1626 | 1627 | through2@^2.0.0, through2@^2.0.3: 1628 | version "2.0.3" 1629 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" 1630 | dependencies: 1631 | readable-stream "^2.1.5" 1632 | xtend "~4.0.1" 1633 | 1634 | tough-cookie@~2.3.0, tough-cookie@~2.3.3: 1635 | version "2.3.3" 1636 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" 1637 | dependencies: 1638 | punycode "^1.4.1" 1639 | 1640 | traverse@^0.6.6: 1641 | version "0.6.6" 1642 | resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" 1643 | 1644 | tunnel-agent@^0.6.0: 1645 | version "0.6.0" 1646 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1647 | dependencies: 1648 | safe-buffer "^5.0.1" 1649 | 1650 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1651 | version "0.14.5" 1652 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1653 | 1654 | type-name@^2.0.1: 1655 | version "2.0.2" 1656 | resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4" 1657 | 1658 | typedarray@^0.0.6: 1659 | version "0.0.6" 1660 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 1661 | 1662 | uid-number@^0.0.6: 1663 | version "0.0.6" 1664 | resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" 1665 | 1666 | unique-string@^1.0.0: 1667 | version "1.0.0" 1668 | resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" 1669 | dependencies: 1670 | crypto-random-string "^1.0.0" 1671 | 1672 | universal-deep-strict-equal@^1.2.1: 1673 | version "1.2.2" 1674 | resolved "https://registry.yarnpkg.com/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7" 1675 | dependencies: 1676 | array-filter "^1.0.0" 1677 | indexof "0.0.1" 1678 | object-keys "^1.0.0" 1679 | 1680 | util-deprecate@~1.0.1: 1681 | version "1.0.2" 1682 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1683 | 1684 | uuid@^3.0.0, uuid@^3.1.0: 1685 | version "3.2.1" 1686 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" 1687 | 1688 | verror@1.10.0: 1689 | version "1.10.0" 1690 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 1691 | dependencies: 1692 | assert-plus "^1.0.0" 1693 | core-util-is "1.0.2" 1694 | extsprintf "^1.2.0" 1695 | 1696 | websocket-driver@>=0.5.1: 1697 | version "0.7.0" 1698 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" 1699 | dependencies: 1700 | http-parser-js ">=0.4.0" 1701 | websocket-extensions ">=0.1.1" 1702 | 1703 | websocket-extensions@>=0.1.1: 1704 | version "0.1.3" 1705 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" 1706 | 1707 | wide-align@^1.1.0: 1708 | version "1.1.2" 1709 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" 1710 | dependencies: 1711 | string-width "^1.0.2" 1712 | 1713 | window-size@^0.1.4: 1714 | version "0.1.4" 1715 | resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" 1716 | 1717 | wrap-ansi@^2.0.0: 1718 | version "2.1.0" 1719 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 1720 | dependencies: 1721 | string-width "^1.0.1" 1722 | strip-ansi "^3.0.1" 1723 | 1724 | wrappy@1: 1725 | version "1.0.2" 1726 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1727 | 1728 | write-file-atomic@^2.0.0: 1729 | version "2.3.0" 1730 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" 1731 | dependencies: 1732 | graceful-fs "^4.1.11" 1733 | imurmurhash "^0.1.4" 1734 | signal-exit "^3.0.2" 1735 | 1736 | xdg-basedir@^3.0.0: 1737 | version "3.0.0" 1738 | resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" 1739 | 1740 | xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: 1741 | version "4.0.1" 1742 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 1743 | 1744 | y18n@^3.2.0: 1745 | version "3.2.1" 1746 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" 1747 | 1748 | yargs@^3.10.0: 1749 | version "3.32.0" 1750 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" 1751 | dependencies: 1752 | camelcase "^2.0.1" 1753 | cliui "^3.0.3" 1754 | decamelize "^1.1.1" 1755 | os-locale "^1.4.0" 1756 | string-width "^1.0.1" 1757 | window-size "^0.1.4" 1758 | y18n "^3.2.0" 1759 | -------------------------------------------------------------------------------- /firebase-authentication/01-intro.md: -------------------------------------------------------------------------------- 1 | # Introduction: Why Firebase Authentication? 2 | 3 | You need Firebase Authentication for two reasons: 4 | 5 | 1. Firebase Security Rules rely on Firebase Authentication 6 | 2. Firebase Authentication is ridiculously easy to use 7 | 8 | ## Firebase Auth + Security Rules 9 | 10 | Firebase needs a security system. In a traditional database you provide your own security using your 11 | API server. Since Firebase **Is** the API server, it needs a programmable way to control read and 12 | write access to your data. When users use your client apps to authenticate with Firebase 13 | Authentication they receive a [JSON Web Tokens](https://jwt.io/) that will identify them to 14 | Firebase's Security Rules system. We'll cover this more later. 15 | 16 | ## Ease Of Use 17 | 18 | Have you ever implemented your own auth system? Yes? Then you know how challenging it can be. If 19 | not... then take my word for it and use an off-the-shelf system. Firebase Authentication provides 20 | the following auth methods: 21 | 22 | * Email/password 23 | * Phone 24 | * OAuth 2 25 | * Google 26 | * Facebook 27 | * Twitter 28 | * Github 29 | * Anonymous 30 | * Custom auth 31 | 32 | ![Imgur](https://i.imgur.com/5K9DW4z.png) 33 | 34 | All of these methods use [JSON Web Tokens](https://jwt.io/), also known as JWTs. 35 | 36 | > JWT is pronounced the same as the English word "jot". 37 | 38 | We'll cover JWTs later. Don't worry. They're relatively simple. 39 | 40 | ### Email/Password 41 | 42 | Email/password auth is exactly what it sounds like. You register an email address and a password 43 | with Firebase and it keeps track of your user account. 44 | 45 | ### Phone 46 | 47 | Google [acquired Twitter Digits](https://firebase.googleblog.com/2017/01/FabricJoinsGoogle17.html) 48 | in early 2017 and rolled their phone authentication into Firebase Authentication. This means that 49 | with minimum fuss you can implement a full SMS-based phone auth flow. This is particularly great for 50 | mobile web apps. Phone authentication is the preferred auth method for many users, especially those 51 | outside of the United States. 52 | 53 | ### OAuth 2 (Google, Facebook, Twitter, Github) 54 | 55 | OAuth is the fastest auth method, because it relies on accounts that your users already have. Most 56 | everyone has either Google, Facebook or Twitter, and developers love Github. OAuth 2 is also the 57 | easiest auth flow to implement. 58 | 59 | And all of the OAuth providers support 60 | [multi-factor authentication](https://en.wikipedia.org/wiki/Multi-factor_authentication), which we 61 | should all be using. 62 | 63 | ### Anonymous Auth 64 | 65 | You may want to interact with a client that hasn't authenticated. If you're developing a shopping 66 | cart feature for your app, you may want users to add items to their carts before they've created an 67 | account. This is where anonymous auth comes in. 68 | 69 | Without some sort of authentication, there's no way for your server to know who it's talking to. 70 | Firebase's data is either entirely open or requires authentication. Any private transaction between 71 | the Firebase database and a client app requires authentication or it will be public and could be 72 | intercepted by a malicious third party. 73 | 74 | There are situations in which you might make your data publicly accessible; however, user 75 | transactions with your database are unlikely to fit this model... and that's where anonymous auth 76 | comes in. 77 | 78 | We won't cover anonymous auth any further except to show how dead simple it is to execute: 79 | 80 | ```javascript 81 | firebase 82 | .auth() 83 | .signInAnonymously() 84 | .then(successHandler, errorHandler); 85 | ``` 86 | 87 | ### Custom Tokens 88 | 89 | Would you like to use a different authentication method with Firebase? That's not a problem. We 90 | won't cover custom tokens here except to say that you can use your auth server to mint Firebase auth 91 | tokens that you can give to your client applications. Those tokens will allow your client apps to 92 | authenticate with Firebase using whatever JWT you chose to create on your server. 93 | 94 | ## Link Multiple Auth Providers 95 | 96 | This is another topic we won't cover here. Just be aware that you can easily 97 | [link multiple auth providers to a single account](https://firebase.google.com/docs/auth/web/account-linking). 98 | 99 | For example, your user might register an email/password account and you could prompt her to link her 100 | account to Google and Facebook. Your client app can make a couple of easy calls to Firebase 101 | Authentication to initiate the linking procedure, and now your user can sign in with Google, 102 | Facebook, Twitter or Github. 103 | 104 | Alternatively, if your user signed up with an OAuth account, you can prompt her to register a linked 105 | email/password combination. 106 | -------------------------------------------------------------------------------- /firebase-authentication/02-configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration: Let's get started! 2 | 3 | Your first step is to visit your Firebase Dashboard and select Develop > Authentication from the 4 | left nav bar. 5 | 6 | Now select the **SIGN-IN METHOD** tab and start enabling sign-in methods. 7 | 8 | ![sign in method](https://i.imgur.com/dmAgaFh.png) 9 | 10 | Email/Password, Phone, Google and Anonymous auth can be enabled with a quick button click. The 11 | third-party providers--Facebook, Twitter and Github--will require a little more ceremony. You'll 12 | need to follow the instructions in the Firebase docs to register your app with the third-party 13 | provider. We'll cover the Facebook flow below. 14 | 15 | But quick! Before you get distracted, scroll down a bit and notice that Firebase requires you to 16 | approve the domains that you want to use with Firebase Authentication. Your Firebase Hosting domain 17 | will be there by default. So will 127.0.0.1 and localhost. Add any extra domains you hope to use. 18 | 19 | Also notice the **Advanced** options: 20 | 21 | * One account per email address 22 | * Manage sign-up quota 23 | 24 | I personally like to enable multiple auth methods for each email address. That enables my users to 25 | sign in with Google one day and Facebook the next. My app's business logic will notice the matching 26 | email addresses and provide them the same account data. 27 | 28 | Multiple accounts per email is harder to secure than requiring one account per email. If you require 29 | one account per email you can still prompt users to link accounts their other 30 | Google/Facebook/Twitter/Github accounts once they've logged in. 31 | 32 | I've just noticed that my users tend to forget how they logged in last time, so I like to 33 | automatically link accounts if possible. But yeah... that's extra work on my end, and it's possible 34 | that an attacker could steal a victim's email, add it to their Facebook account and use Facebook to 35 | log into my app :( 36 | 37 | Managing the sign-up quota is useful if you find that your app is getting spammed by fake sign-up 38 | attempts. This is rare :) 39 | 40 | ## Add a third-party OAuth provider 41 | 42 | We'll cover the Facebook flow below. Facebook may alter their developer site, so check the 43 | [Firebase docs](https://firebase.google.com/docs/auth/web/facebook-login) if you get confused. 44 | 45 | 1. [Create a developer account](https://developers.facebook.com/) with Facebook or log in to your 46 | existing account. 47 | 2. Create a new Facebook app. 48 | 3. Copy your App ID and App Secret 49 | 4. Paste your App ID and App Secret into the Firebase Dashboard 50 | 5. Copy the OAuth redirect URI from your Firebase Dashboard 51 | 6. Set up Facebook Login on Facebook's developer site 52 | 7. Paste your OAuth redirect URI from step 5 into Facebook's **Valid OAuth redirect URIs** field 53 | 54 | ### Screenshots: Add Facebook OAuth 55 | 56 | ![Imgur](https://i.imgur.com/I9VkiYK.png) 57 | 58 | ![Create a Facebook App](https://i.imgur.com/2vl7bAA.png) 59 | 60 | ![Copy your App ID and App Secret](https://i.imgur.com/uCkDHm6.png) 61 | 62 | ![Paste your App Id and App Secret](https://i.imgur.com/qaSYddj.png) 63 | 64 | ![Set up Facebook Login](https://i.imgur.com/Rcwulu0.png) 65 | 66 | ![Paste your OAuth redirect URI](https://i.imgur.com/jQjgGJH.png) 67 | 68 | # Add Firebase Auth to your Web App 69 | 70 | This is the easy part. 71 | 72 | The Firebase SDK is typically added to a web page using a ` 124 | 125 | 126 | 127 | ``` 128 | 129 | ![Use the __/firebase folder](https://i.imgur.com/qVYhFmg.png) 130 | 131 | ![Local folder structure](https://i.imgur.com/vqgfyCj.png) 132 | -------------------------------------------------------------------------------- /firebase-authentication/03-implementation.md: -------------------------------------------------------------------------------- 1 | # Implementation: The easy part? 2 | 3 | All example code can be found in my 4 | [Quiver repo](https://github.com/deltaepsilon/quiver/tree/master/packages/firebase-authentication) 5 | on Github. I've called the project `firebase-authentication`. The relevant Firebase Authentication 6 | code can be found in 7 | [auth-service.js](https://github.com/deltaepsilon/quiver/blob/master/packages/firebase-authentication/src/services/auth.service.js). 8 | 9 | > These examples use 10 | > [JavaScript ES2015 destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) 11 | > and single-parameter (monad) functions. I like to pass in a single object with named attributes, 12 | > then destructure those attributes in the method signature. This is all personal preference, so 13 | > don't let it distract you. 14 | 15 | ## Overview 16 | 17 | The example code uses [Preact](https://github.com/developit/preact) for client-side templating, but 18 | don't get distracted by that. The relevant code for Firebase is all standard ES2015 JavaScript. 19 | 20 | ### Auth Service 21 | 22 | We'll start by reviewing 23 | [auth-service.js](https://github.com/deltaepsilon/quiver/blob/master/packages/firebase-authentication/src/services/auth.service.js). 24 | 25 | I'm using [Visual Studio Code](https://code.visualstudio.com/) as my editor/IDE, and you'll notice a 26 | lot of code folding in my example images. Code folding is a way to visually collapse blocks of code 27 | in the IDE so that they don't distract us. 28 | 29 | ### Function Signatures 30 | 31 | **AuthService** has 10 methods that use `firebase.auth()`. 32 | 33 | * `onAuthStateChanged` 34 | * `signOut` 35 | * `currentUserDelete` 36 | * `signInWithEmailAndPassword` 37 | * `createUserWithEmailAndPassword` 38 | * `sendPasswordResetEmail` 39 | * `signInWithPhoneNumber` 40 | * `signInWithPopup` 41 | * `signInWithRedirect` 42 | * `getRecaptchaVerifier` 43 | 44 | ![AuthService](https://i.imgur.com/ZWrk6uA.png) 45 | 46 | ![AuthService return](https://i.imgur.com/B7uqUSS.png) 47 | 48 | **AuthService** takes 4 functions as external dependencies 49 | 50 | * fire 51 | * handleError 52 | * changeView 53 | * clearInputs 54 | 55 | These external dependencies encapsulate business logic that's irrelevant to our examples, so don't 56 | worry about them. 57 | 58 | **AuthService** also uses `window.firebase` and `firebase.auth()` to bootstrap itself. This assumes 59 | that Firebase has already been attached to the `window` object via script tags on the page. 60 | 61 | Finally, **AuthService** creates a `providersMap` using the `firebase.auth` prototype. These 62 | providers will be used for OAuth login in the `signInWithPopup` and `signInWithRedirect` methods. 63 | 64 | ![method signature](https://i.imgur.com/CJ8Ohet.png) 65 | 66 | ### onAuthStateChanged 67 | 68 | The `onAuthStateChanged` method is the crux of Firebase Authentication. It's used to register a 69 | callback that gets called whenever the page's authentication state changes. 70 | 71 | > Note: Firebase Authentication uses `localStorage` to save authentication tokens across user 72 | > sessions. 73 | 74 | If the page loads and there are no authentication tokens in `localStorage`, then the callback is 75 | called with a `null` argument. Otherwise, if Firebase Authentication has previously authenticated on 76 | the page and saved a token to `localStorage`, then `onAuthStateChanged` is called with Firebase 77 | Authentication's `currentUser`. 78 | 79 | The `currentUser` is the JWT that Firebase Authentication is using for the session. 80 | 81 | The example below shows that `AuthService`'s exported `onAuthStateChanged` function is merely a 82 | wrapper for `window.firebase.auth().onAuthStateChanged`. You don't need to wrap `onAuthStateChanged` 83 | in your own code, but it was helpful with this service architecture to isolate `AuthService` from 84 | the component that's using it. 85 | 86 | The example below has two files, `auth.service.js` and `index.js`. `index.js` is the code for the 87 | component that's using `AuthService`. Notice in the example below that `initAuthService` creates a 88 | new `AuthService` and passes in the relevant dependencies. Next it passes a callback into 89 | `this.authService.onAuthStateChanged`. This callback contains application-specific business logic 90 | which we can ignore. Just note that `currentUser` will be ``null` if authentication fails and will 91 | contain the JWT if authentication succeeds. 92 | 93 | ![onAuthStateChanged](https://i.imgur.com/UC8l10r.png) 94 | 95 | ### signOut & delete 96 | 97 | Signing out is easy enough. We called `firebase.auth()` at the top of the file and assigned the auth 98 | object to the variable `auth`, so all we need to do is call `auth.signOut()` and our session will be 99 | closed and our `onAuthStateChanged` callback will be called with a `null` currentUser. 100 | 101 | Deleting our currentUser is a bit trickier, because we need to get the currentUser first. 102 | currentUser is an attribute on our `auth` object. It can be accessed directly directly from 103 | `window.firebase.auth().currentUser`. The `currentUser` object has a `delete` method that returns a 104 | promise. 105 | 106 | ![Imgur](https://i.imgur.com/uKhdDRW.png) 107 | 108 | ### Email/Password Auth 109 | 110 | There are two email/password-specific functions with the same method signature: 111 | 112 | * `auth.signInWithEmailAndPassword(email, password)` 113 | * `auth.createUserWithEmailAndPassword(email, password)` 114 | 115 | Both methods return promises, and you should definitely use `.catch` blocks to handle errors from 116 | both methods. The error objects have a `code` attribute that you'll use to handle the errors. 117 | 118 | `signInWithEmailAndPassword` throws two codes worth handling: 119 | 120 | * `auth/user-not-found` 121 | * `auth/wrong-password` 122 | 123 | There's one **gotcha** here, and it's with `auth/wrong-password`. If you offer login with an OAuth 124 | provider--Google/Facebook/Twitter/Github--you'll likely have users that log in with the OAuth 125 | provider, sign out, and then try to log in with the same email address. 126 | 127 | In this case there will be an account under that email address, but it won't have a password 128 | associated with it. This login attempt will throw an `auth/wrong-password` error, so you'll need to 129 | decide on your own business logic in this case. Do you want to suggest that they reset their 130 | password, register a new email/password account, or remind them to try signing in with an OAuth 131 | provider? It's up to you! 132 | 133 | `createUserWithEmailAndPassword` throws one error: 134 | 135 | * `auth/email-already-in-use` 136 | 137 | Note that `auth/email-already-in-use` will only be thrown if a user already has an OAuth account 138 | **AND** you've limited users to one account per email address. You can change this setting in your 139 | Firebase Dashboard under **Authentication > Sign-In Method > Advanced > One account per email 140 | address > CHANGE** 141 | 142 | ![sign in user with email/password](https://i.imgur.com/kWo2Zlb.png) 143 | 144 | There's one more email/password method that we've missed: `sendPasswordResetEmail(email)` 145 | 146 | It's simple enough. It sends an email with a password reset link. You can modify the password reset 147 | email on your Dashboard under **Authentication > Templates > Password reset**. 148 | 149 | ![send password reset email](https://i.imgur.com/c1HPgZT.png) 150 | 151 | ### Phone Auth 152 | 153 | Phone auth is a little trickier to implement due to the need for a recaptcha element on the page. 154 | This can get a little complicated, so you'll want to review the 155 | [Firebase docs](https://firebase.google.com/docs/auth/web/phone-auth) before creating your own 156 | implementation. 157 | 158 | The gist is that you create an element on your page to hold the recaptcha element and give Firebase 159 | the `id` for that element. Firebase Authentication will automatically inject a recaptcha box into 160 | the element and will request that the user complete a recaptcha test before signing in with a phone 161 | number. 162 | 163 | The trick is that you can specify that the recaptcha should be `invisible` and use the `id` of the 164 | button that you use to trigger the phone auth. Firebase will then use Google's recaptcha system to 165 | do mouse tracking on the button to determine if the user is human. If the recaptcha mouse-tracking 166 | test is inconclusive, Firebase will open a recaptcha modal in the window. 167 | 168 | You'll need to register your `RecaptchaVerifier` when you render your phone login form. You'll also 169 | need to pass the resulting `RecaptchaVerifier` object into `signInwithPhoneNumber(phoneNumber, 170 | recaptchaVerifier)`. 171 | 172 | ![create recaptcha verifier](https://i.imgur.com/L0IqQEF.png) 173 | 174 | There's another **gotcha** with `signInWithPhoneNumber`: be careful how you format the phone number. 175 | The format that's successful for me is `+1 1234567890`, where `+1` is a country code, followed by 176 | a space and then the phone number. The following example uses the variable name `callingCode` for 177 | the country code. 178 | 179 | Notice in the example how I handle a successful call to `signInWithPhoneNumber` in the `.then(...)` 180 | block. The `.then(...)` block gets called with a `confirmationResult` object that has a `.confirm` 181 | method on it. `.confirm` returns a promise, and you'll need to catch any errors with a `.catch(...)` 182 | block. You'll call `confirmationResult.confirm(code)` with the confirmation code that will be sent 183 | to your user's phone via SMS. 184 | 185 | If `.confirm` is passed a valid code, then the callback we registered earlier with 186 | `auth.onAuthStateChanged(cb)` will be called with the new currentUser JWT. If the code is wrong, the 187 | `.confirm` call will throw an error with the code `auth/invalid-verification-code`. 188 | 189 | ![sign in with phone number](https://i.imgur.com/V7hBqys.png) 190 | 191 | ## Phone Auth Review 192 | 193 | Phone auth is tricky for two reasons: 194 | 195 | 1. Recaptcha 196 | 2. Saving the `confirmationResult` and calling it later 197 | 198 | You'll likely need to fuss with the recaptcha a bit to get it working smoothly with your user 199 | interface. Experiment with different size options and use the 200 | [Firebase docs](https://firebase.google.com/docs/auth/web/phone-auth) as your guide. I personally 201 | like to attach an invisible recaptcha to my **SEND SMS** button, but you may find the visible 202 | captcha to be more reliable. 203 | 204 | I also found that I needed to pass in a callback when registering a new `RecaptchaVerifier`, even if 205 | that callback was an empty function. Experiment with it and be patient :) 206 | 207 | Second, calling `auth.signInwithPhoneNumber` can be tricky because you'll need to save the resulting 208 | `confirmationResult` using `.then(confirmationResult => ...)`. In my example I used an external 209 | function called `setConfirm` and passed it a function that takes a code and passes it into 210 | `confirmationResult.confirm`. This is a common JavaScript pattern, but it may be hard to read if 211 | you're less familiar with JavaScript. Pay close attention to lines 64 and 65 of the example. 212 | 213 | `setConfirm` is an external function that I passed into `AuthService` from a UI component. 214 | `setConfirm` takes a callback function and saves it to a variable for later use. When the user has 215 | entered a confirmation code into that component's form, the component calls the callback function 216 | with the code. The callback function has wrapped a call to 217 | `confirmationResult.confirm(code).catch(...)`. 218 | 219 | ### OAuth with Popup or Redirect 220 | 221 | OAuth is the easy part. We can call one of two methods, and the callback we registered earlier with 222 | `auth.onAuthStateChanged` will handle the result. And that's it. 223 | 224 | * `auth.signInWithPopup(provider)` 225 | * `auth.signInWithRedirect(provider)` 226 | 227 | Just make sure to pass in a valid provider. Our example created a map of all possible providers at 228 | the top of the file and assigned it to `providersMap`, but you can create the provider object inline 229 | if you prefer. 230 | 231 | ![sign in with OAuth](https://i.imgur.com/8EBSuBo.png) 232 | -------------------------------------------------------------------------------- /firebase-authentication/04-outro.md: -------------------------------------------------------------------------------- 1 | # Outro: Maybe it wasn't that easy :) 2 | 3 | We covered email/password, phone and OAuth authentication methods. 4 | 5 | We did not cover anonymous authentication or custom authentication. 6 | 7 | ## Anonymous & Custom Authentication 8 | 9 | Most apps will not need anonymous or custom authentication, and I don't recommend using them unless 10 | you absolutely need them. 11 | 12 | You'll know that you need anonymous auth if you need secure database transactions with an 13 | otherwise-unauthenticated user, or if you need to maintain a user session without authenticating 14 | your user. 15 | 16 | You'll only use custom auth if you need to integrate a Firebase client with a non-Firebase 17 | authentication system. There's no problem with doing this... just don't waste your time on it if 18 | it's not necessary. The entire point of using Firebase is that it's an app platform. Yes, you can 19 | use each part of the platform separately, but that kinda defeats the purpose. 20 | 21 | ## OAuth is the best 22 | 23 | Seriously. Use OAuth providers, especially Google. OAuth providers let your users benefit from 24 | multi-factor authentication, and they de-risk your own code. Sure, holding onto an email and 25 | password for a few seconds before you pass them to Firebase and delete them forever isn't 26 | particularly risky... but why waste the effort when you can use users' existing OAuth provider 27 | accounts? 28 | 29 | ## Know your users 30 | 31 | 32 | I just urged you to use OAuth. Well, maybe don't use OAuth. Make sure you know your users. Talk to 33 | them. Are they social-media obsessed power users who prefer Twitter OAuth, or are they phone-first 34 | users who may not have any other accounts and would prefer a quick SMS auth experience? 35 | 36 | Yeah, it's easy to overload your users with **ALL OF THE AUTHS**, but sometimes a simple 37 | email/password form is more effective. Find out. Ask your users. And if you're too lazy to ask, 38 | maybe start with OAuth and track page abandonment analytics? 39 | 40 | Auth sign-in flows can be make-or-break for an app. Don't fail because of your sign-in flow. 41 | 42 | 43 | -------------------------------------------------------------------------------- /firebase-authentication/05-notes.md: -------------------------------------------------------------------------------- 1 | ### Phone Auth 2 | 3 | * `signInWithPhoneNumber('+1 1234567890', new 4 | window.firebase.auth.RecaptchaVerifier('recaptcha-id-from-element', {size: 'invisible', callback: 5 | () => {}}))` 6 | * `window.firebase.auth().signInWithPhoneNumber(phoneNumber, 7 | recaptchaVerifier).then(confirmationResult => { myConfirmFunction = code => 8 | confirmationResult.confirm(code) })` 9 | -------------------------------------------------------------------------------- /firebase-authentication/screenshots/1000-auth-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/1000-auth-methods.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/1100-sign-in-method-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/1100-sign-in-method-enabled.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/1200-auth-domain-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/1200-auth-domain-enabled.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2000-facebook-read-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2000-facebook-read-docs.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2050-facebook-create-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2050-facebook-create-app.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2100-facebook-add-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2100-facebook-add-login.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2200-facebook-copy-app-id-and-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2200-facebook-copy-app-id-and-secret.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2300-facebook-paste-id-and-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2300-facebook-paste-id-and-secret.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/2400-facebook-paste-redirect-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/2400-facebook-paste-redirect-url.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/3000-go-do-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/3000-go-do-dashboard.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/3100-copy-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/3100-copy-config.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/3300-auth-domain-initialized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/3300-auth-domain-initialized.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/3400-init-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/3400-init-js.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/3500-folder-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/3500-folder-structure.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4000-auth-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4000-auth-service.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4100-auth-service-return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4100-auth-service-return.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4200-method-signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4200-method-signature.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4300-onAuthStateChanged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4300-onAuthStateChanged.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4350-sign-out-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4350-sign-out-delete.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4400-email-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4400-email-password.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4500-password-reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4500-password-reset.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4600-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4600-phone.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4700-recaptcha-verifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4700-recaptcha-verifier.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/4800-oauth-sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/4800-oauth-sign-in.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5000-login-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5000-login-options.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5100-input-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5100-input-email.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5200-input-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5200-input-password.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5300-logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5300-logged-in.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5400-bad-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5400-bad-password.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5500-input-email-two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5500-input-email-two.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5600-input-password-two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5600-input-password-two.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5700-register-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5700-register-email.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5800-input-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5800-input-phone.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5900-confirm-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5900-confirm-phone.png -------------------------------------------------------------------------------- /firebase-authentication/screenshots/5950-oauth-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/tutorials/6dc00133e85118c64fbd7553efb587e5e0764cc2/firebase-authentication/screenshots/5950-oauth-google.png -------------------------------------------------------------------------------- /firebase-cloud-functions/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "quiver-two" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /firebase-cloud-functions/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [Firebase Cloud Functions](https://firebase.googleblog.com/2017/03/introducing-cloud-functions-for-firebase.html) are Google's serverless solution for Firebase apps. They can be used as the (R)eactor functions for [FIRE Stack](https://howtofirebase.com/fire-stack-4195a13daf96) app architecture. If you've developed with firebase-queue, AWS Lambda or some other Functions-as-a-Service architecture, Firebase Cloud Functions should feel natural... just a lot slicker and easier to use :) 4 | 5 | If you're wondering where to start... well, read on my friend. 6 | 7 | ### FIRE Stack 8 | 9 | [FIRE Stack architecture](https://howtofirebase.com/fire-stack-4195a13daf96) replaces the typical REST API with its endpoint and HTTP calls with standalone functions——written by you and running on Firebase's infrastructure—-that react to changes in your app and do anything that you can do in Node.js or Java. 10 | 11 | As of this writing, there are six types of triggers: 12 | 13 | 1. [Firebase Realtime Database](https://firebase.google.com/docs/functions/database-events) triggers 14 | 2. [Firebase Authentication](https://firebase.google.com/docs/functions/auth-events) triggers 15 | 3. [Firebase Analytics](https://firebase.google.com/docs/functions/analytics-events) triggers 16 | 4. [Cloud Storage](https://firebase.google.com/docs/functions/gcp-storage-events) triggers 17 | 5. [HTTP](https://firebase.google.com/docs/functions/http-events) triggers 18 | 6. [Cloud Pub/Sub](https://firebase.google.com/docs/functions/pubsub-events) triggers 19 | 20 | You can read the docs on each of those triggers for the full rundown. They're not hard to use, although they can be tricky to test. I'm going to start with Authentication and Firebase Realtime Database triggers. If you can get those working, you shouldn't have trouble with the other event types. 21 | 22 | ### Node Environment 23 | 24 | Cloud Functions supports Node.js LTS releases. The current release is v6.9.1, but [check the docs](https://cloud.google.com/functions/docs/writing/) to make sure that you're developing against the freshest-possible version of Node.js. 25 | 26 | If you need help jumping between Node.js versions, check out [n](https://github.com/tj/n) for fast version switching. 27 | 28 | To get started, I'm running ```n 6.9.1``` to switch to v6.9.1. 29 | 30 | ### Authentication Triggers 31 | 32 | Authentication triggers track creation and deletion of Firebase Authentication users. That's about it. Here are the examples from [the docs](https://firebase.google.com/docs/functions/auth-events). 33 | 34 | ```javascript 35 | exports.sendWelcomeEmail = functions.auth.user().onCreate(event => { 36 | const user = event.data; 37 | const email = user.email; 38 | // ... 39 | }); 40 | 41 | exports.sendByeEmail = functions.auth.user().onDelete(event => { 42 | const user = event.data; 43 | const email = user.email; 44 | // ... 45 | }); 46 | ``` 47 | 48 | That's really all there is to it. ```event.data``` is the user data from your newly minted or deleted (currentUser JWT (JavaScript Web Token))[https://firebase.google.com/docs/auth/users]. 49 | 50 | If you want to access your Realtime Database, you can't get it from the event :( Fortunately, ```functions.config().firebase``` contains your initialization details, so you can use ```firebase-admin``` to create whatever refs you need. 51 | 52 | ```javascript 53 | functions.auth.user().onCreate(event => { 54 | const functions = require('firebase-functions'); 55 | const admin = require('firebase-admin'); 56 | const config = functions.config(); 57 | 58 | admin.initializeApp(config.firebase); 59 | 60 | const user = event.data; 61 | const userRef = admin.database().ref('/users').child(user.uid); 62 | 63 | return userRef.update(user); 64 | }); 65 | ``` 66 | 67 | ### Realtime Database Triggers 68 | 69 | This is likely the trigger that you need for [FIRE Stack architecture](https://howtofirebase.com/fire-stack-4195a13daf96). 70 | 71 | It's also the most complicated trigger, because of all of the attributes on the event itself. You need to read up on these attributes. I'll only be summarizing a few here. 72 | 73 | First, [scan the docs](https://firebase.google.com/docs/reference/functions/functions.Event#properties) about event properties. Focus on the following: 74 | 75 | - ```event.data``` 76 | - ```event.params``` 77 | 78 | Next, read a bit more carefully through the docs on [DeltaSnapshot](https://firebase.google.com/docs/reference/functions/functions.database.DeltaSnapshot), a.k.a. ```event.data```. This is the crux of Realtime Database events. Pay attention to everything, but read the following word-for-word. 79 | 80 | - ```DeltaSnapshot.adminRef``` 81 | - ```DeltaSnapshot.current``` 82 | - ```DeltaSnapshot.key``` 83 | - ```DeltaSnapshot.previous``` 84 | - ```DeltaSnapshot.ref``` 85 | - ```DeltaSnapshot.val()``` 86 | - ```DeltaSnapshot.toJSON()``` 87 | - ```DeltaSnapshot.numChildren()``` 88 | 89 | You're back already? Have you understood the docs? If so, GREAT! If not, for shame! Go back and read 'em :) 90 | 91 | Once you understand the ```event``` object, database triggers aren't tough to figure out. I have an architectural pattern where I like to track user logins. It's kind of an important metric, and it's a good opportunity to update the user's account. I have my client app push data to ```/queues/current-user/{uid}```, something like this... 92 | 93 | ```javascript 94 | // Client-side code, in my case a web browser 95 | firebase.auth().onAuthStateChanged(function (user) { 96 | this.user = user; // Set my local copy of the 'user' object 97 | if (user) { 98 | this.displayName = user.displayName; 99 | this.photoURL = user.photoURL; 100 | var userRef = firebase.database().ref('queues/login').child(user.uid); 101 | 102 | userRef.remove() 103 | .then(function () { 104 | return userRef.set({ 105 | email: user.email, 106 | emailVerified: user.emailVerified, 107 | photoURL: user.photoURL, 108 | displayName: user.displayName, 109 | timestamp: new Date().toString() 110 | }); 111 | }); 112 | } 113 | }.bind(this)); 114 | ``` 115 | 116 | Then I register a Firebase Function to listen to ```queues/login/{uid}``` and do whatever I need. 117 | 118 | ```javascript 119 | const function = require('firebase-functions'); 120 | 121 | functions.database.ref('queues/login/{uid}').onWrite(event => { 122 | const config = functions.config(); 123 | // functions.config() returns your environment variables. 124 | // In this case I have an array of my admin users' email addresses in accessControlLists.adminUsers 125 | // It looks like ['chris@chrisesplin.com', 'some-other-admin@chrisesplin.com'] 126 | const adminUsersString = config.['access-control-lists']['admin-users']; 127 | const adminUsers = adminUsersString.split(','); 128 | const user = event.data.val(); 129 | const userRef = event.data.adminRef.root.child('users').child(event.params.uid); 130 | 131 | if (!user) return Promise.resolve(); 132 | 133 | user.lastLogin = Date.now(); 134 | 135 | if (adminUsers.includes(user.email)) { 136 | user.isAdmin = true; 137 | } 138 | 139 | return userRef.update(user).then(() => { 140 | return event.data.ref.remove(); 141 | }); 142 | }; 143 | }); 144 | ``` 145 | 146 | ### Use event.data.adminRef and event.data.ref 147 | 148 | ```event.data.adminRef``` is a ref with full admin privileges over your entire database. ```event.data.ref``` is a ref as well, but is has the same authentication permissions as the user who triggered the event. This is ***insanely useful***. It enables you to impersonate a user from within your function, security-rules restrictions and all! 149 | 150 | Also, if you try to do something "admin-like" from ```event.data.ref```, don't be surprised when you get weird ***permission denied*** errors in your Cloud Functions logs. 151 | 152 | ### Use event.params 153 | 154 | In the earlier example I registered my ***onWrite*** event like so: 155 | 156 | ```javascript 157 | functions.database.ref('queues/login/{uid}').onWrite(event => {...}); 158 | ``` 159 | Notice the wildcard ```{uid}```. That wildcard ends up as ```event.params.uid```. So if write something to ```/queues/login/fake-uid-123```, then ```event.params.uid``` will be ```'fake-uid-123'```. Pretty simple. 160 | 161 | But the plot thickens, because you can do crazy stuff like 162 | 163 | ```javascript 164 | functions.database.ref('/queues/{queueType}/{uid}').onWrite(event => {...}); 165 | ``` 166 | See that! You can use multiple wildcards in a path! I don't know how ***many*** wildcards you can use, and I've never used more than two... but go hog-wild and let me know if you find the limits! 167 | 168 | ### Use environment variables! 169 | 170 | The docs on [environment variables](https://firebase.google.com/docs/functions/config-env) show how you can use ```firebase-tools``` to set your functions environment variables from the command line. Basically, you install ```firebase-tools``` globally with ```yarn global add firebase-tools``` (or ```npm install -g firebase-tools```). Then you use the CLI to do the work. 171 | 172 | ```bash 173 | firebase functions:config:set access-control-lists.admin-users="chris@chrisesplin.com,someone-else@chrisesplin.com" 174 | ``` 175 | 176 | Notice that I used kebab case instead of camel case. Functions config does not allow uppercase characters in attribute keys. So ```accessControlLists.adminUsers```, which is what I'd like to type here, ***will not work!!!*** 👹 177 | 178 | Also notice that I passed in a comma-delimited string. Stick to strings. You can [coerce](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch4.md) these strings to other data types once you're in the function... but don't try to get cute with booleans, numbers or arrays. They'll all get wrapped in double-quotes. 179 | 180 | ### Require your dependencies within each function 181 | 182 | You'll want to define most of your require statements, i.e., ```const quiverFunctions = require('quiver-functions')```, within the function itself. This will run the ```require``` function every time the function is run. 183 | 184 | This won't bite you often, but I've run into a few situations where my functions wouldn't deploy until I moved a ```require``` call into the function. I suspect it has something to do with how the functions are triggered on Google's end. If you see error messages when attempting to deploy functions, this tip might help. 185 | 186 | ***Good*** 187 | ```javascript 188 | exports.sendEmail = functions.auth.user().onCreate(event => { 189 | const mailgun = require('mailgun-js'); 190 | // ... 191 | }); 192 | ``` 193 | 194 | ***Not As Awesome*** 195 | ```javascript 196 | const mailgun = require('mailgun-js'); 197 | exports.sendEmail = functions.auth.user().onCreate(event => { 198 | // ... 199 | }); 200 | ``` 201 | 202 | ### Testing 203 | 204 | I'm using [Jasmine](https://jasmine.github.io/2.0/node.html) for my Node.js tests. It's easy to get started on your own projects. Just do the following: 205 | 206 | > I'm using Yarn instead of NPM these days. NPM works almost the same, but let's be honest, Yarn's better :) 207 | > Here are [the docs on installing Yarn](https://yarnpkg.com/lang/en/docs/install/). 208 | > If you're on OS X, you should be using [Homebrew](https://brew.sh/) for your own sanity. 209 | > If you're serious about dev on OS X, and you're not using Homebrew, take a few minutes to get it set up. Thank me later. 210 | > Installing Yarn with Homebrew is as easy as ```brew update && brew install yarn```. 211 | 212 | 1. Install Jasmine globally: ```yarn global add jasmine``` 213 | 2. Install Jasmine in your project: ```yarn add jasmine``` 214 | 3. Initialize Jasmin: ```jasmine init``` 215 | 4. Edit ```./spec/support/jasmine.json``` to make sure that the ```spec_dir``` and ```spec_files``` will pick up your tests. 216 | 217 | You can run the test in this repo by simply installing and running ```jasmine``` at the top-level of the repo. Otherwise, you can ```cd functions && yarn test``` to get the same result. Note that you'll need to copy ```/functions/config.json.dist``` to ```/functions/config.json``` and edit the file so that it contains your Firebase details. I'm not checking my ```service-account.json``` file into source control :) 218 | 219 | ### NEXT STEPS 220 | 221 | 1. Long-running processes 222 | 2. Cron jobs 223 | 3. All of the new event types 224 | 225 | -------------------------------------------------------------------------------- /firebase-cloud-functions/context.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventId": "eb6H4zSgQABVKKifYOV24u0+Z0k=", 3 | "timestamp": "2018-03-20T11:27:57.893Z", 4 | "eventType": "google.firebase.database.ref.create", 5 | "resource": { 6 | "service": "firebaseio.com", 7 | "name": 8 | "projects/_/instances/how-to-firebase-tutorials/refs/authenticated/react-chat/Default Chat Room/-L829b5QxEoINb1ft39v" 9 | }, 10 | "authType": "USER", 11 | "auth": { 12 | "uid": "DNhXq7T4igfchsZDIhxBWbRquF03", 13 | "token": { 14 | "name": "Chris Esplin", 15 | "email_verified": true, 16 | "email": "chris@quiver.is", 17 | "exp": 1521548871, 18 | "user_id": "DNhXq7T4igfchsZDIhxBWbRquF03", 19 | "picture": 20 | "https://lh4.googleusercontent.com/-ly98tZeA6F0/AAAAAAAAAAI/AAAAAAAAADk/G-1n2ID9bOw/photo.jpg", 21 | "iat": 1521545271, 22 | "sub": "DNhXq7T4igfchsZDIhxBWbRquF03", 23 | "aud": "how-to-firebase-tutorials", 24 | "auth_time": 1521545271, 25 | "iss": "https://securetoken.google.com/how-to-firebase-tutorials", 26 | "firebase": { 27 | "identities": { "google.com": ["116279478330828791198"], "email": ["chris@quiver.is"] }, 28 | "sign_in_provider": "google.com" 29 | } 30 | } 31 | }, 32 | "params": { "room": "Default Chat Room", "key": "-L829b5QxEoINb1ft39v" } 33 | } 34 | -------------------------------------------------------------------------------- /firebase-cloud-functions/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /firebase-cloud-functions/functions/config.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": { 3 | "databaseURL": "https://quiver-two.firebaseio.com", 4 | "storageBucket": "quiver-two.appspot.com", 5 | "apiKey": "123123123", 6 | "authDomain": "quiver-two.firebaseapp.com", 7 | "serviceAccount": "/Users/quiver/.gcloud/quiver-two-service-account.json" 8 | }, 9 | "config": { 10 | "token": "1/ABCDEFG", 11 | "project": "quiver-two" 12 | }, 13 | "models": { 14 | "queues": { 15 | "current-user": "quiver-functions/queues/current-user" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /firebase-cloud-functions/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | 4 | const quiverFunctions = require('quiver-functions'); 5 | const OnCreate = quiverFunctions.OnCreate; 6 | const Login = quiverFunctions.Login; 7 | 8 | const config = functions.config(); 9 | admin.initializeApp(config.firebase); 10 | 11 | const onCreate = new OnCreate({ 12 | usersPath: 'quiver-functions/users', 13 | database: admin.database() 14 | }); 15 | exports.onCreate = functions.auth.user().onCreate(onCreate.getFunction()); 16 | 17 | const login = new Login({ 18 | usersPath: 'quiver-functions/users', 19 | adminUsers: ['chris@chrisesplin.com'] 20 | }); 21 | exports.login = functions.database.ref('quiver-functions/queues/current-user/{uid}').onWrite(login.getFunction()); 22 | -------------------------------------------------------------------------------- /firebase-cloud-functions/functions/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const admin = require('firebase-admin'); 3 | const config = require('./config.json'); 4 | const quiverFunctions = require('quiver-functions'); 5 | const Login = quiverFunctions.Login; 6 | const OnCreate = quiverFunctions.OnCreate; 7 | const mocks = quiverFunctions.mocks; 8 | const _ = require('lodash'); 9 | 10 | admin.initializeApp({ 11 | credential: admin.credential.cert(config.firebase.serviceAccount), 12 | databaseURL: config.firebase.databaseURL 13 | }); 14 | 15 | const database = admin.database(); 16 | const rootRef = database.ref('quiver-functions/test'); 17 | 18 | const mockUser = { 19 | uid: 'fake-uid', 20 | email: 'chris@chrisesplin.com', 21 | password: '123456', 22 | displayName: 'Chris Esplin', 23 | photoURL: 'https://lh4.googleusercontent.com/-ly98tZeA6F0/AAAAAAAAAAI/AAAAAAAAADk/G-1n2ID9bOw/photo.jpg?sz=64', 24 | disabled: false, 25 | emailVerified: false 26 | }; 27 | 28 | function cleanUp(done) { 29 | return rootRef.remove().then(done); 30 | }; 31 | 32 | beforeEach(done => cleanUp(done)); 33 | afterAll(done => cleanUp(done)); 34 | 35 | describe('Login', () => { 36 | let fakeUser, userRef, loginEvent, loginFunction; 37 | beforeEach(() => { 38 | const login = new Login({ 39 | usersPath: '/quiver-functions/test/users', 40 | adminUsers: ['chris@chrisesplin.com'] 41 | }); 42 | const loginQueueRef = rootRef.child('/queues/login'); 43 | 44 | fakeUser = _.cloneDeep(mockUser); 45 | userRef = rootRef.child('users').child(fakeUser.uid); 46 | loginEvent = new mocks.MockDBEvent(loginQueueRef, { uid: fakeUser.uid }, fakeUser); 47 | loginFunction = login.getFunction(); 48 | }); 49 | 50 | it('should process a user login queue item', done => { 51 | loginFunction(loginEvent).then(() => userRef.once('value')).then(snap => { 52 | const user = snap.val(); 53 | 54 | expect(snap.key).toEqual(fakeUser.uid); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('onCreate', () => { 61 | let fakeUser, userRef, onCreateEvent, onCreateFunction; 62 | beforeEach(() => { 63 | const onCreate = new OnCreate({ 64 | usersPath: '/quiver-functions/test/users', 65 | database: database 66 | }); 67 | 68 | fakeUser = _.cloneDeep(mockUser); 69 | userRef = rootRef.child('users').child(fakeUser.uid); 70 | onCreateEvent = new mocks.MockAuthEvent(fakeUser); 71 | onCreateFunction = onCreate.getFunction(); 72 | }); 73 | 74 | it('should process auth onCreate', done => { 75 | 76 | onCreateFunction(onCreateEvent).then(() => userRef.once('value')).then(snap => { 77 | const user = snap.val(); 78 | 79 | expect(user.uid).toEqual(fakeUser.uid); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /firebase-cloud-functions/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "^4.1.2", 6 | "firebase-functions": "^0.5", 7 | "quiver-functions": "^1.0.11" 8 | }, 9 | "private": true, 10 | "scripts": { 11 | "test": "cd .. && jasmine", 12 | "deploy": "cd .. && firebase deploy --only functions" 13 | }, 14 | "version": "1.0.0", 15 | "main": "index.js", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "lodash": "^4.17.4", 19 | "jasmine": "^2.5.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /firebase-cloud-functions/snap.val.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Chris Esplin", 3 | "email": "chris@quiver.is", 4 | "message": "Let's try this again", 5 | "photoURL": 6 | "https://lh4.googleusercontent.com/-ly98tZeA6F0/AAAAAAAAAAI/AAAAAAAAADk/G-1n2ID9bOw/photo.jpg", 7 | "uid": "DNhXq7T4igfchsZDIhxBWbRquF03" 8 | } 9 | -------------------------------------------------------------------------------- /firebase-cloud-functions/spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "", 3 | "spec_files": [ 4 | "functions/index.spec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /starter-project/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "preact" 5 | ] 6 | } -------------------------------------------------------------------------------- /starter-project/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "how-to-firebase-tutorials" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /starter-project/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "users": { 4 | "$uid": { 5 | ".read": "auth.uid === $uid || auth.token.admin === true", 6 | ".write": "auth.uid === $uid || auth.token.admin === true" 7 | } 8 | }, 9 | "userOwned": { 10 | "$objectType": { 11 | "$uid": { 12 | ".read": "auth.uid === $uid || auth.token.admin === true", 13 | ".write": "auth.uid === $uid || auth.token.admin === true" 14 | } 15 | } 16 | }, 17 | "userReadable": { 18 | "$objectType": { 19 | "$uid": { 20 | ".read": "auth.uid === $uid || auth.token.admin === true", 21 | ".write": "auth.token.admin === true" 22 | } 23 | } 24 | }, 25 | "userWriteable": { 26 | "$objectType": { 27 | "$uid": { 28 | ".read": "auth.token.admin === true", 29 | ".write": "auth.uid === $uid || auth.token.admin === true" 30 | } 31 | } 32 | }, 33 | "adminOwned": { 34 | "$objectType": { 35 | "$uid": { 36 | ".read": "auth.token.admin === true", 37 | ".write": "auth.token.admin === true" 38 | } 39 | } 40 | }, 41 | "public": { 42 | "$objectType": { 43 | "$uid": { 44 | ".read": true, 45 | ".write": "auth.token.admin === true" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /starter-project/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | }, 9 | "hosting": { 10 | "public": "public", 11 | "ignore": [ 12 | "firebase.json", 13 | "**/.*", 14 | "**/node_modules/**" 15 | ], 16 | "rewrites": [ 17 | { 18 | "source": "**", 19 | "destination": "/index.html" 20 | } 21 | ] 22 | }, 23 | "storage": { 24 | "rules": "storage.rules" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /starter-project/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | // Example: 3 | // 4 | // "indexes": [ 5 | // { 6 | // "collectionId": "widgets", 7 | // "fields": [ 8 | // { "fieldPath": "foo", "mode": "ASCENDING" }, 9 | // { "fieldPath": "bar", "mode": "DESCENDING" } 10 | // ] 11 | // } 12 | // ] 13 | "indexes": [] 14 | } -------------------------------------------------------------------------------- /starter-project/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /starter-project/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /starter-project/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase experimental:functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "dependencies": { 12 | "firebase-admin": "~5.8.1", 13 | "firebase-functions": "^0.8.1" 14 | }, 15 | "private": true 16 | } 17 | -------------------------------------------------------------------------------- /starter-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Firebase Authentication 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /starter-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-project", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@babel/core": "^7.0.0-beta.39", 8 | "@babel/preset-env": "^7.0.0-beta.39", 9 | "babel-preset-preact": "^1.1.0", 10 | "firebase-tools": "^3.17.4" 11 | }, 12 | "scripts": { 13 | "deploy": "firebase deploy", 14 | "deploy:hosting": "firebase deploy --only hosting", 15 | "deploy:database": "firebase deploy --only database", 16 | "deploy:firestore": "firebase deploy --only firestore", 17 | "deploy:storage": "firebase deploy --only storage", 18 | "deploy:functions": "firebase deploy --only functions", 19 | "start": "parcel src/index.html" 20 | }, 21 | "dependencies": { 22 | "@quiver/firebase-authentication": "^0.1.3", 23 | "preact-custom-element": "^2.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /starter-project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 34 |
35 |

Welcome

36 |

Firebase Hosting Setup Complete

37 |

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

38 | Open Hosting Documentation 39 |
40 |

Firebase SDK Loading…

41 | 42 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /starter-project/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Firebase Starter Project 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /starter-project/src/index.js: -------------------------------------------------------------------------------- 1 | import registerCustomElement from 'preact-custom-element'; 2 | import FirebaseAuthentication from '@quiver/firebase-authentication'; 3 | registerCustomElement(FirebaseAuthentication, 'firebase-authentication'); 4 | -------------------------------------------------------------------------------- /starter-project/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /write-your-first-query/README.md: -------------------------------------------------------------------------------- 1 | # Write Your First Query 2 | 3 | ## Open in Glitch 4 | 5 | We'll be working in Google Chrome. 6 | 7 | Open the Glitch project with Google Chrome: [Write Your First Query](https://glitch.com/edit/#!/coordinated-freighter?path=index.html) 8 | 9 | ## DevTools 10 | 11 | Click the green Show (Live) button in the header of the Glitch project to open a new window. 12 | 13 | The new window has a live view of your project, so you can see what happens as you edit it. 14 | 15 | Right-click on the page and select "Inspect Element". 16 | 17 | The new panel that has opened up is called Dev Tools. 18 | 19 | Click the "three dots" button on the top-right of Dev Tools and select Dock Side > Dock to Right. 20 | 21 | Now expand drag DevTools to be nice and big and select the Console tab. 22 | 23 | 24 | ## Write the code 25 | 26 | Return to Glitch and start writing. 27 | 28 | Switch to the live-preview tab to see your progress! 29 | 30 | ## Files 31 | 32 | The files are in this repo. 33 | 34 | They're also available at the following links: 35 | 36 | - [index.html](https://raw.githubusercontent.com/how-to-firebase/tutorials/master/write-your-first-query/index.html) 37 | - [complete.html](https://raw.githubusercontent.com/how-to-firebase/tutorials/master/write-your-first-query/complete.html) 38 | 39 | -------------------------------------------------------------------------------- /write-your-first-query/bin/upload-data.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const serviceAccount = require('../../service-account.json'); 3 | 4 | admin.initializeApp({ 5 | credential: admin.credential.cert(serviceAccount), 6 | databaseURL: 'https://how-to-firebase-tutorials.firebaseio.com', 7 | }); 8 | 9 | const people = require('../data/people.json'); 10 | 11 | const records = people.map(({ name, height, hair_color, url }) => { 12 | const urlParts = url.split('/'); 13 | let id = String(urlParts[urlParts.length - 2]); 14 | let charactersToPad = 3 - id.length; 15 | while (charactersToPad--) { 16 | id = '0' + id; 17 | } 18 | 19 | return { 20 | id, 21 | name, 22 | height, 23 | hair_color, 24 | }; 25 | }); 26 | 27 | const db = admin.firestore(); 28 | const collection = db 29 | .collection('public') 30 | .doc('write-your-first-query') 31 | .collection('star-wars-people'); 32 | const batch = db.batch(); 33 | 34 | records.forEach(({ id, name, height, hair_color }) => { 35 | batch.set(collection.doc(id), { name, height, hair_color }); 36 | }); 37 | 38 | batch.commit().then(() => { 39 | console.log('people records saved'); 40 | }); 41 | -------------------------------------------------------------------------------- /write-your-first-query/complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | First Firestore Query 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 52 | 53 |

Page intentionally left blank

54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /write-your-first-query/data/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Luke Skywalker", 4 | "height": "172", 5 | "mass": "77", 6 | "hair_color": "blond", 7 | "skin_color": "fair", 8 | "eye_color": "blue", 9 | "birth_year": "19BBY", 10 | "gender": "male", 11 | "homeworld": "https://swapi.co/api/planets/1/", 12 | "films": [ 13 | "https://swapi.co/api/films/2/", 14 | "https://swapi.co/api/films/6/", 15 | "https://swapi.co/api/films/3/", 16 | "https://swapi.co/api/films/1/", 17 | "https://swapi.co/api/films/7/" 18 | ], 19 | "species": ["https://swapi.co/api/species/1/"], 20 | "vehicles": ["https://swapi.co/api/vehicles/14/", "https://swapi.co/api/vehicles/30/"], 21 | "starships": ["https://swapi.co/api/starships/12/", "https://swapi.co/api/starships/22/"], 22 | "created": "2014-12-09T13:50:51.644000Z", 23 | "edited": "2014-12-20T21:17:56.891000Z", 24 | "url": "https://swapi.co/api/people/1/" 25 | }, 26 | { 27 | "name": "C-3PO", 28 | "height": "167", 29 | "mass": "75", 30 | "hair_color": "n/a", 31 | "skin_color": "gold", 32 | "eye_color": "yellow", 33 | "birth_year": "112BBY", 34 | "gender": "n/a", 35 | "homeworld": "https://swapi.co/api/planets/1/", 36 | "films": [ 37 | "https://swapi.co/api/films/2/", 38 | "https://swapi.co/api/films/5/", 39 | "https://swapi.co/api/films/4/", 40 | "https://swapi.co/api/films/6/", 41 | "https://swapi.co/api/films/3/", 42 | "https://swapi.co/api/films/1/" 43 | ], 44 | "species": ["https://swapi.co/api/species/2/"], 45 | "vehicles": [], 46 | "starships": [], 47 | "created": "2014-12-10T15:10:51.357000Z", 48 | "edited": "2014-12-20T21:17:50.309000Z", 49 | "url": "https://swapi.co/api/people/2/" 50 | }, 51 | { 52 | "name": "R2-D2", 53 | "height": "96", 54 | "mass": "32", 55 | "hair_color": "n/a", 56 | "skin_color": "white, blue", 57 | "eye_color": "red", 58 | "birth_year": "33BBY", 59 | "gender": "n/a", 60 | "homeworld": "https://swapi.co/api/planets/8/", 61 | "films": [ 62 | "https://swapi.co/api/films/2/", 63 | "https://swapi.co/api/films/5/", 64 | "https://swapi.co/api/films/4/", 65 | "https://swapi.co/api/films/6/", 66 | "https://swapi.co/api/films/3/", 67 | "https://swapi.co/api/films/1/", 68 | "https://swapi.co/api/films/7/" 69 | ], 70 | "species": ["https://swapi.co/api/species/2/"], 71 | "vehicles": [], 72 | "starships": [], 73 | "created": "2014-12-10T15:11:50.376000Z", 74 | "edited": "2014-12-20T21:17:50.311000Z", 75 | "url": "https://swapi.co/api/people/3/" 76 | }, 77 | { 78 | "name": "Darth Vader", 79 | "height": "202", 80 | "mass": "136", 81 | "hair_color": "none", 82 | "skin_color": "white", 83 | "eye_color": "yellow", 84 | "birth_year": "41.9BBY", 85 | "gender": "male", 86 | "homeworld": "https://swapi.co/api/planets/1/", 87 | "films": [ 88 | "https://swapi.co/api/films/2/", 89 | "https://swapi.co/api/films/6/", 90 | "https://swapi.co/api/films/3/", 91 | "https://swapi.co/api/films/1/" 92 | ], 93 | "species": ["https://swapi.co/api/species/1/"], 94 | "vehicles": [], 95 | "starships": ["https://swapi.co/api/starships/13/"], 96 | "created": "2014-12-10T15:18:20.704000Z", 97 | "edited": "2014-12-20T21:17:50.313000Z", 98 | "url": "https://swapi.co/api/people/4/" 99 | }, 100 | { 101 | "name": "Leia Organa", 102 | "height": "150", 103 | "mass": "49", 104 | "hair_color": "brown", 105 | "skin_color": "light", 106 | "eye_color": "brown", 107 | "birth_year": "19BBY", 108 | "gender": "female", 109 | "homeworld": "https://swapi.co/api/planets/2/", 110 | "films": [ 111 | "https://swapi.co/api/films/2/", 112 | "https://swapi.co/api/films/6/", 113 | "https://swapi.co/api/films/3/", 114 | "https://swapi.co/api/films/1/", 115 | "https://swapi.co/api/films/7/" 116 | ], 117 | "species": ["https://swapi.co/api/species/1/"], 118 | "vehicles": ["https://swapi.co/api/vehicles/30/"], 119 | "starships": [], 120 | "created": "2014-12-10T15:20:09.791000Z", 121 | "edited": "2014-12-20T21:17:50.315000Z", 122 | "url": "https://swapi.co/api/people/5/" 123 | }, 124 | { 125 | "name": "Owen Lars", 126 | "height": "178", 127 | "mass": "120", 128 | "hair_color": "brown, grey", 129 | "skin_color": "light", 130 | "eye_color": "blue", 131 | "birth_year": "52BBY", 132 | "gender": "male", 133 | "homeworld": "https://swapi.co/api/planets/1/", 134 | "films": [ 135 | "https://swapi.co/api/films/5/", 136 | "https://swapi.co/api/films/6/", 137 | "https://swapi.co/api/films/1/" 138 | ], 139 | "species": ["https://swapi.co/api/species/1/"], 140 | "vehicles": [], 141 | "starships": [], 142 | "created": "2014-12-10T15:52:14.024000Z", 143 | "edited": "2014-12-20T21:17:50.317000Z", 144 | "url": "https://swapi.co/api/people/6/" 145 | }, 146 | { 147 | "name": "Beru Whitesun lars", 148 | "height": "165", 149 | "mass": "75", 150 | "hair_color": "brown", 151 | "skin_color": "light", 152 | "eye_color": "blue", 153 | "birth_year": "47BBY", 154 | "gender": "female", 155 | "homeworld": "https://swapi.co/api/planets/1/", 156 | "films": [ 157 | "https://swapi.co/api/films/5/", 158 | "https://swapi.co/api/films/6/", 159 | "https://swapi.co/api/films/1/" 160 | ], 161 | "species": ["https://swapi.co/api/species/1/"], 162 | "vehicles": [], 163 | "starships": [], 164 | "created": "2014-12-10T15:53:41.121000Z", 165 | "edited": "2014-12-20T21:17:50.319000Z", 166 | "url": "https://swapi.co/api/people/7/" 167 | }, 168 | { 169 | "name": "R5-D4", 170 | "height": "97", 171 | "mass": "32", 172 | "hair_color": "n/a", 173 | "skin_color": "white, red", 174 | "eye_color": "red", 175 | "birth_year": "unknown", 176 | "gender": "n/a", 177 | "homeworld": "https://swapi.co/api/planets/1/", 178 | "films": ["https://swapi.co/api/films/1/"], 179 | "species": ["https://swapi.co/api/species/2/"], 180 | "vehicles": [], 181 | "starships": [], 182 | "created": "2014-12-10T15:57:50.959000Z", 183 | "edited": "2014-12-20T21:17:50.321000Z", 184 | "url": "https://swapi.co/api/people/8/" 185 | }, 186 | { 187 | "name": "Biggs Darklighter", 188 | "height": "183", 189 | "mass": "84", 190 | "hair_color": "black", 191 | "skin_color": "light", 192 | "eye_color": "brown", 193 | "birth_year": "24BBY", 194 | "gender": "male", 195 | "homeworld": "https://swapi.co/api/planets/1/", 196 | "films": ["https://swapi.co/api/films/1/"], 197 | "species": ["https://swapi.co/api/species/1/"], 198 | "vehicles": [], 199 | "starships": ["https://swapi.co/api/starships/12/"], 200 | "created": "2014-12-10T15:59:50.509000Z", 201 | "edited": "2014-12-20T21:17:50.323000Z", 202 | "url": "https://swapi.co/api/people/9/" 203 | }, 204 | { 205 | "name": "Obi-Wan Kenobi", 206 | "height": "182", 207 | "mass": "77", 208 | "hair_color": "auburn, white", 209 | "skin_color": "fair", 210 | "eye_color": "blue-gray", 211 | "birth_year": "57BBY", 212 | "gender": "male", 213 | "homeworld": "https://swapi.co/api/planets/20/", 214 | "films": [ 215 | "https://swapi.co/api/films/2/", 216 | "https://swapi.co/api/films/5/", 217 | "https://swapi.co/api/films/4/", 218 | "https://swapi.co/api/films/6/", 219 | "https://swapi.co/api/films/3/", 220 | "https://swapi.co/api/films/1/" 221 | ], 222 | "species": ["https://swapi.co/api/species/1/"], 223 | "vehicles": ["https://swapi.co/api/vehicles/38/"], 224 | "starships": [ 225 | "https://swapi.co/api/starships/48/", 226 | "https://swapi.co/api/starships/59/", 227 | "https://swapi.co/api/starships/64/", 228 | "https://swapi.co/api/starships/65/", 229 | "https://swapi.co/api/starships/74/" 230 | ], 231 | "created": "2014-12-10T16:16:29.192000Z", 232 | "edited": "2014-12-20T21:17:50.325000Z", 233 | "url": "https://swapi.co/api/people/10/" 234 | } 235 | ] 236 | -------------------------------------------------------------------------------- /write-your-first-query/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firestore: where and orderBy 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 52 | 53 |

This page intentionally left blank

54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /write-your-first-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "write-your-first-query", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "firebase-admin": "^5.8.2" 8 | }, 9 | "scripts": { 10 | "upload": "node bin/upload-data.js" 11 | } 12 | } 13 | --------------------------------------------------------------------------------