├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin ├── quiverFunctions.js └── server.js ├── database.rules.json ├── firebase.json ├── functions ├── config.json.dist ├── graphql-example.js ├── index.js ├── onCreate │ ├── updateUser.onCreate.js │ └── updateUser.onCreate.spec.js ├── onRequest │ ├── environment.onRequest.js │ ├── environment.onRequest.spec.js │ ├── graphqlServer.onRequest.js │ └── graphqlServer.onRequest.spec.js ├── onWrite │ ├── login.onWrite.js │ └── login.onWrite.spec.js ├── package.json ├── schemas │ ├── async.schema.js │ ├── items.schema.js │ └── types │ │ ├── async.type.js │ │ ├── item.type.js │ │ └── itemsQuery.type.js ├── services │ ├── environment.service.js │ ├── environment.service.spec.js │ ├── event.service.js │ ├── event.service.spec.js │ ├── index.js │ ├── localData.service.js │ └── localData.service.spec.js └── yarn.lock ├── index.js ├── jest.config.json ├── mocks ├── index.js ├── mock-auth-event.js ├── mock-db-event.js └── mock-user.json ├── modules ├── environment.js ├── graphqlServer.js ├── index.js ├── index.spec.js ├── login.js └── updateUser.js ├── package.json ├── public ├── README.md ├── bower.json ├── index.html ├── manifest.json ├── src │ └── quiver-functions-app │ │ └── quiver-functions-app.html └── test │ └── quiver-functions-app │ └── quiver-functions-app_test.html ├── services ├── index.js └── index.spec.js ├── utilities ├── environment.utility.js ├── environment.utility.spec.js ├── generator.utility.js ├── generator.utility.spec.js └── index.js └── 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 | .firebaserc 40 | .vscode 41 | bower_components 42 | env.client.json 43 | config.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | public 3 | spec 4 | *.spec.js 5 | config.json 6 | firebase.json 7 | database.rules.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chris Esplin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quiver Functions 2 | 3 | A collection of little helpers for your Firebase Functions pleasure. 4 | 5 | You'll find yourself implenting a lot of the same things over, and over and over. For instance, you'll need to test your functions locally, so you'll be implementing mock events over and over again. And setting functions environment variables programatically is a pain in the neck... so you'll want that automated too. 6 | 7 | ### Installation 8 | 9 | - ```npm install --save quiver-functions``` 10 | 11 | ### Testing 12 | 13 | - ```git clone https://github.com/deltaepsilon/quiver-functions.git && cd quiver-functions``` 14 | - Install ```firebase-tools``` globally with ```npm install -g firebase-tools```. 15 | - Run ```firebase init``` and connect the local project to one of your Firebase projects. No need to install database, functions or hosting if you're just running tests. 16 | - Get your CI token with ```firebase login:ci```. Follow the instructions and copy your new CI token from the CLI. 17 | - Edit ```./functions/config.json``` with your test project's details including the CI token. Let ```./functions/config.json.dist``` be your guide. 18 | - ```cd quiver-functions``` 19 | - ```npm install``` 20 | - ```cd ..``` 21 | - ```npm test``` 22 | 23 | ### Test bed web page 24 | 25 | - ```npm run install-public``` 26 | - ```npm run serve``` 27 | - Edit ```./public/env.client.json``` with your project's details. Let ```./public/env.client.json.dist``` be your guide. 28 | - Open [http://localhost:8080/](http://localhost:8080/) 29 | - ```firebase deploy --only database```: Deploys database rules 30 | - ```firebase deploy --only functions```: Deploys functions 31 | - ```firebase deploy --only hosting```: Deploys hosting; use if you want to host the test bed web page. 32 | - Profit 33 | 34 | ## How to use the module 35 | 36 | QuiverFunctions export statement looks like this: 37 | 38 | ``` 39 | module.exports = { 40 | UpdateUser: require('./functions/onCreate/updateUser.onCreate'), 41 | Login: require('./functions/onWrite/login.onWrite'), 42 | Environment: require('./functions/onRequest/environment.onRequest'), 43 | mocks: require('./mocks/mocks'), 44 | utilities: require('./utilities/utilities') 45 | } 46 | ``` 47 | 48 | ### UpdateUser 49 | 50 | ***UpdateUser*** is a simple handler for user-creation events. It simply copies the new user's JWT data to whatever collection you specify with the ```usersPath``` parameter. 51 | 52 | ### Login 53 | 54 | ***Login*** is designed so that every time your user logs in, he'll add his token from ```firebase.auth().currentUser.getToken(token => console.log(token))``` queue, and ***Login*** will process that queue item, verifying the ID token, checking to see if the user's email is in the ```adminUsers``` array and adding the ```user.isAdmin``` flag as appropriate. The user details are then updated to ```usersPath/{uid}``` based on the specified ```usersPath``` param. 55 | 56 | ***Login*** requires ```{uid}``` to be in the ref path. It's a generic solution, I know, but every app I've written in the last year has used this functionality, written exactly like this. 57 | 58 | ### EnvironmentService 59 | 60 | ***EnvironmentService*** takes environment config and has a utility method called ```getPublicEnvironment(host)``` that will return just the ```.public``` node of your environment as well as complete any overrides based on the ```host``` that you pass in. Note that keys can't have periods in them so we're using ```_``` instead of dots. Here's a quick example: 61 | 62 | ``` 63 | "public": { 64 | "a": "defaults: a", 65 | "models": { 66 | "queues": { 67 | "current-user": "quiver-functions/queues/current-user" 68 | } 69 | }, 70 | "localhost": { 71 | "a": "localhost: a", 72 | "b": "localhost: b" 73 | }, 74 | "subdomain_domain_tld": { 75 | "a": "subdomain_domain_tld: a", 76 | "b": "subdomain_domain_tld: b" 77 | } 78 | } 79 | ``` 80 | 81 | ***public*** and ***domain-specific overrides*** 82 | 83 | I always find myself wanting some of my environment variables to be public so that I can quickly pass them to my client apps. I also want to be able to override these public variables based on domain. I use different domains for dev, test and prod, so I should be able to easily override my public environment variables. 84 | 85 | 86 | Check out ```./functions/config.json.dist``` for a complete example. 87 | 88 | ### Environment Variable Rules 89 | 90 | Functions has some rules for environment variables/config. 91 | 92 | - No root-level key can have the word "firebase" in it. There is a ```firebase: {}``` attribute that will be filled in automatically by Functions, so you won't have to look far for your core environment variables. Also, it doesn't hurt to keep your own ```firebase: {}``` attributes in ```config.json```; just know that this attribute will be deleted upon upload. 93 | - All config must be nested. You can't do ```{ someValue: 'true' }```. It needs to be ```{some: { value: 'true' }}```. 94 | - Dashes and special characters in attribute names can wreck havoc. So can capitalization. Attribute names should be all lowercase and have no special characters. Underscores are preferred. So don't use ```{'test-user': {...}}```. Use ```{test_user: {...}}``` instead. 95 | 96 | 97 | ### Environment onRequest Handler 98 | 99 | The ***Environment*** onRequest handler is a bit fancy. It takes advantage of the fact that ```firebase-functions``` looks for environment variables in ```./functions/config.json```. 100 | 101 | First, place all of your environment variables in that .json file, and use ```./utilities/environment.utility.js``` and it's ```setAll()```, ```unsetAll()``` and ```getAll()``` functions to push those environment variables up to Functions config. Now your local environment and your Functions environment will be equivalent. 102 | 103 | Second, export an ```environment``` function in your ```./functions/index.js```. Here's an example: 104 | 105 | ``` 106 | const Environment = require('quiver-functions').Environment; 107 | const environment = new Environment(); 108 | exports.environment = functions.https.onRequest(environment.getFunction()); 109 | ``` 110 | 111 | Third, create a rewrite rule in your ```./firebase.json```. Here's what I'm using for ```firebase.json```. Notice that the ***source*** is ```/environment```, but there's no ***destination***... only a ***function***. That ***function*** value matches up to the ```exports.environment = ...``` line in the example above. 112 | 113 | ``` 114 | { 115 | "database": { 116 | "rules": "database.rules.json" 117 | }, 118 | "hosting": { 119 | "public": "public", 120 | "rewrites": [ 121 | { 122 | "source": "/environment.js", 123 | "function": "environment" 124 | }, 125 | { 126 | "source": "**", 127 | "destination": "/index.html" 128 | } 129 | ] 130 | } 131 | } 132 | 133 | ``` 134 | 135 | Now that you've completed the redirect, run ```firebase deploy``` in your terminal and hit [https://your-firebase.firebaseapp.com/environment](https://your-firebase.firebaseapp.com/environment) to see the magic. It's a single script tag that adds your public environment to ```window.firebaseEnv```. This is perfect for importing your client-side environment variables into your dev, test and prod clients. 136 | 137 | #### Full Example 138 | 139 | ``` 140 | const functions = require('firebase-functions'); 141 | const admin = require('firebase-admin'); 142 | const config = functions.config(); 143 | 144 | admin.initializeApp(config.firebase); 145 | 146 | const UpdateUser = require('quiver-functions').UpdateUser; 147 | const updateUser = new UpdateUser({ 148 | usersPath: 'quiver-functions/{environment}/users', 149 | database: admin.database() 150 | }); 151 | exports.updateUser = functions.auth.user().onCreate(updateUser.getFunction()); 152 | 153 | const Login = require('quiver-functions').Login; 154 | const login = new Login({ 155 | usersPath: 'quiver-functions/users', 156 | adminUsers: ['chris@chrisesplin.com'], 157 | auth: admin.auth() 158 | }); 159 | exports.login = functions.database.ref('quiver-functions/queues/current-user/{uid}').onWrite(login.getFunction()); 160 | 161 | const config = functions.config(); 162 | const headers = { 163 | 'Cache-Control': 'public, max-age=600, s-maxage=600' 164 | }; 165 | const Environment = require('quiver-functions').Environment; 166 | const environment = new Environment({ config, headers }); 167 | exports.environment = functions.https.onRequest(environment.getFunction()); 168 | 169 | ``` 170 | 171 | ### Utilities 172 | 173 | ***QuiverFunctions.utilities*** is a collection of useful little helpers. So far it's just ```utililities.EnvironmentUtility```, which manipulates Firebase Functions config as found in ```./functions/config.json```. There are three functions on ```EnvironmentUtility```: 174 | 175 | 1. ***EnvironmentUtility.getAll()*** 176 | 2. ***EnvironmentUtility.unsetAll()*** 177 | 3. ***EnvironmentUtility.setAll()*** 178 | 179 | #### Example 180 | 181 | ``` 182 | const config = require('../functions/config.json'); 183 | const quiverFunctions = require('quiver-functions'); 184 | const environmentUtility = new quiverFunctions.utilities.EnvironmentUtility('quiver-two', 'my-ci-token-from-firebase-tools-cli', config); 185 | 186 | environmentUtility.unsetAll() 187 | .then(() => { 188 | return environmentUtility.setAll(); 189 | }) 190 | .then(() => { 191 | return environmentUtility.getAll(); 192 | }) 193 | .then(newConfig => { 194 | console.log('New Firebase Functions config: ', newConfig); 195 | }); 196 | 197 | ``` 198 | 199 | ### Mocks 200 | 201 | ***QuiverFunctions.mocks*** is a collection of useful mocks for testing Firebase Functions locally. See the code in ```/mocks``` to see how the objects are built. Check out ```/functions/lib/login.spec.js``` and ```/functions/lib/on-create.spec.js``` for example implementation. 202 | 203 | Use ```new functions.database.DeltaSnapshot(...)``` to create mock DeltaSnapshots as documented [here](https://firebase.google.com/docs/functions/unit-testing). 204 | 205 | Developing Firebase Functions without a local testing environment is... a mistake. A huge mistake. It's basically impossible to develop effectively with a guess-and-check technique. Every call to ```firebase deploy --only functions``` takes at least a minute, and then the functions can take a while to warm up, so solving bugs by uploading new "fixed" functions can be a huge waste of time. Just write the tests. You'll be happier. Promise. 206 | 207 | ### QVF 208 | 209 | I found myself wanting to fire off the ***EnvironmentUtility*** functions directly from the command line, so I wrote a tiny CLI that's available with the command ```qvf``` (for quiver-functions). It has three commands and one flag. Here's the full example: 210 | 211 | ``` 212 | $ qvf unset 213 | $ qvf get 214 | $ qvf set 215 | $ qvf set --environment some/other/environment.json 216 | ``` 217 | 218 | Note that ```qvf set``` defaults to using ```$PWD/functions/config.json```. 219 | 220 | 221 | -------------------------------------------------------------------------------- /bin/quiverFunctions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const argv = require('yargs').argv; 4 | const cwd = path.resolve(process.cwd()); 5 | const env = require(`${cwd}/${argv.environment || 'functions/config.json'}`); 6 | const { EnvironmentUtility } = require('../utilities'); 7 | const environmentUtility = new EnvironmentUtility(env.config.project, env.config.token, env); 8 | 9 | const admin = require('firebase-admin'); 10 | admin.initializeApp({ 11 | databaseURL: env.firebase.databaseURL, 12 | credential: admin.credential.cert(env.firebase.serviceAccount), 13 | }); 14 | const GeneratorUtility = require('../utilities/generator.utility'); 15 | const generatorUtility = new GeneratorUtility({ admin }); 16 | 17 | if (argv._.includes('get')) { 18 | console.log('getting all'); 19 | handlePromise(Promise.resolve()); 20 | } else if (argv._.includes('set')) { 21 | console.log('setting all'); 22 | handlePromise(environmentUtility.setAll()); 23 | } else if (argv._.includes('unset')) { 24 | console.log('unsetting all'); 25 | handlePromise(environmentUtility.unsetAll()); 26 | } else if (argv._.includes('generate')) { 27 | console.log('generating'); 28 | const data = generatorUtility.generate(10, 3); 29 | admin 30 | .database() 31 | .ref('generated/connections') 32 | .set(data) 33 | .then(() => { 34 | console.log('done'); 35 | process.exit(); 36 | }); 37 | } 38 | 39 | function handlePromise(promise) { 40 | return promise 41 | .catch(err => { 42 | console.log(err); 43 | return true; 44 | }) 45 | .then(() => environmentUtility.getAll()) 46 | .then(config => { 47 | console.log(config); 48 | process.exit(); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | const GraphQLServer = require('../functions/onRequest/graphqlServer.onRequest'); 2 | const admin = require('firebase-admin'); 3 | const config = require('../functions/config.json'); 4 | const graphql = require('graphql'); 5 | const makeExecutableSchema = require('graphql-tools').makeExecutableSchema; 6 | 7 | admin.initializeApp({ 8 | databaseURL: config.firebase.databaseURL, 9 | credential: admin.credential.cert(config.firebase.serviceAccount), 10 | }); 11 | 12 | const schema = require('../functions/schemas/items.schema'); 13 | 14 | const ref = admin.database().ref('test/graphqlServer'); 15 | const server = new GraphQLServer({ ref }); 16 | 17 | const observable = server.start(schema); 18 | 19 | observable.filter(x => x.event == 'ready').subscribe(() => { 20 | console.log('ready!'); 21 | }); 22 | 23 | observable.filter(x => !!x.log).subscribe(x => console.log(x)); 24 | 25 | const port = 3333; 26 | 27 | server.app.listen(port, () => console.log(`Listening on port ${port}`)); 28 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "quiver-functions": { 4 | "queues": { 5 | "current-user": { 6 | "$uid": { 7 | ".write": "auth.uid == $uid" 8 | } 9 | } 10 | }, 11 | "users": { 12 | "$uid": { 13 | ".read": "auth.uid == $uid" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public", 7 | "rewrites": [ 8 | { 9 | "source": "/environment.js", 10 | "function": "environment" 11 | }, 12 | { 13 | "source": "**", 14 | "destination": "/index.html" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /functions/config.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": { 3 | "databaseURL": "https://quiver-two.firebaseio.com", 4 | "storageBucket": "quiver-two.appspot.com", 5 | "apiKey": "AIzaSyAgB7JVLSl-3TQGL-qvQ7mdSYjmpPNV7xQ", 6 | "authDomain": "quiver-two.firebaseapp.com", 7 | "serviceAccount": "/Users/quiver/.gcloud/quiver-two-service-account.json" 8 | }, 9 | "config": { 10 | "token": "1/oCzz-eMX9Sdqx0ImNIOhOajhvOjSKATeccBuU9WpMxc", 11 | "project": "quiver-two" 12 | }, 13 | "test_user": { 14 | "uid": "fake-uid", 15 | "email": "chris@quiver.is", 16 | "password": "123456", 17 | "display-name": "Chris Esplin", 18 | "photo-url": "https://lh4.googleusercontent.com/-ly98tZeA6F0/AAAAAAAAAAI/AAAAAAAAADk/G-1n2ID9bOw/photo.jpg?sz=64", 19 | "disabled": false, 20 | "email-verified": false 21 | }, 22 | "public": { 23 | "a": "defaults: a", 24 | "models": { 25 | "queues": { 26 | "current-user": "quiver-functions/queues/current-user" 27 | } 28 | }, 29 | "localhost": { 30 | "a": "localhost: a", 31 | "b": "localhost: b" 32 | }, 33 | "subdomain:domain:tld": { 34 | "a": "subdomain:domain:tld: a", 35 | "b": "subdomain:domain:tld: b" 36 | } 37 | }, 38 | "test_public": { 39 | "c": "test_public: c", 40 | "d": "test_public: d" 41 | }, 42 | "test_shared": { 43 | "e": "test_shared: e", 44 | "f": "test_shared: f" 45 | } 46 | } -------------------------------------------------------------------------------- /functions/graphql-example.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltaepsilon/quiver-functions/d9735a1e1fee53654417037567a9f372fa4418b2/functions/graphql-example.js -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | const config = functions.config(); 4 | 5 | admin.initializeApp(config.firebase); 6 | 7 | const UpdateUser = require('./onCreate/updateUser.onCreate'); 8 | const updateUser = new UpdateUser({ 9 | usersPath: 'quiver-functions/users', 10 | database: admin.database(), 11 | }); 12 | exports.updateUser = functions.auth.user().onCreate(updateUser.getFunction()); 13 | 14 | const Login = require('./onWrite/login.onWrite'); 15 | const login = new Login({ 16 | usersPath: 'quiver-functions/users', 17 | adminUsers: ['chris@chrisesplin.com'], 18 | auth: admin.auth(), 19 | }); 20 | exports.login = functions.database 21 | .ref('quiver-functions/queues/current-user/{uid}') 22 | .onWrite(login.getFunction()); 23 | 24 | const Environment = require('./onRequest/environment.onRequest'); 25 | const environment = new Environment({ config }); 26 | exports.environment = functions.https.onRequest(environment.getFunction()); 27 | 28 | const GraphQLServer = require('./onRequest/graphqlServer.onRequest'); 29 | const schema = require('./schemas/items.schema'); 30 | const connectionsRef = admin.database().ref('generated/connections'); 31 | const graphqlServer = new GraphQLServer({ ref: connectionsRef }); 32 | const graphqlObservable = graphqlServer.start(schema); 33 | 34 | graphqlObservable.filter(x => x.event == 'ready') 35 | .take(1) 36 | .subscribe(x => { 37 | console.log(`GraphQLServer is ready. Last key: ${x.key}`); 38 | }); 39 | graphqlObservable.filter(x => !!x.log).subscribe(x => console.log(x.log)); 40 | 41 | exports.graphql = functions.https.onRequest(graphqlServer.app); 42 | -------------------------------------------------------------------------------- /functions/onCreate/updateUser.onCreate.js: -------------------------------------------------------------------------------- 1 | module.exports = class UpdateUser { 2 | constructor(config) { 3 | if (!config.usersPath) { 4 | throw 'config.usersPath string missing. Looks like "/users"'; 5 | } 6 | if (!config.database) { 7 | throw 'config.database is missing. You must pass in a valid database using admin.database()'; 8 | } 9 | this.usersPath = config.usersPath; 10 | this.database = config.database; 11 | } 12 | 13 | getFunction() { 14 | return event => { 15 | const functions = require('firebase-functions'); 16 | 17 | const userRef = this.database.ref(this.usersPath).child(event.data.uid); 18 | const user = event.data; 19 | 20 | return userRef.update(user); 21 | }; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /functions/onCreate/updateUser.onCreate.spec.js: -------------------------------------------------------------------------------- 1 | describe('Login', function() { 2 | const admin = require('firebase-admin'); 3 | const config = require('../config.json'); 4 | 5 | admin.initializeApp({ 6 | credential: admin.credential.cert(config.firebase.serviceAccount), 7 | databaseURL: config.firebase.databaseURL 8 | }); 9 | 10 | const mocks = require('../../mocks'); 11 | const UpdateUser = require('./updateUser.onCreate'); 12 | 13 | const database = admin.database(); 14 | 15 | const usersPath = 'quiver-functions/users'; 16 | const usersRef = database.ref(usersPath); 17 | const userRef = usersRef.child(mocks.mockUser.uid); 18 | 19 | function cleanUp(done) { 20 | return Promise.all([usersRef.remove()]).then(() => done()); 21 | } 22 | 23 | beforeEach(cleanUp); 24 | 25 | let updateUser, event; 26 | beforeEach(() => { 27 | updateUser = new UpdateUser({ 28 | usersPath: usersPath, 29 | database 30 | }); 31 | event = new mocks.MockAuthEvent(mocks.mockUser); 32 | }); 33 | 34 | afterEach(cleanUp); 35 | 36 | it('should process auth updateUser', (done) => { 37 | const updateUserFunction = updateUser.getFunction(); 38 | 39 | updateUserFunction(event).then(() => userRef.once('value')).then(snap => { 40 | const user = snap.val(); 41 | 42 | expect(user.uid).toEqual(mocks.mockUser.uid); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /functions/onRequest/environment.onRequest.js: -------------------------------------------------------------------------------- 1 | const EnvironmentService = require('../services/environment.service'); 2 | const HOSTNAME_REGEXP = /\/\/([^:/]+)/; 3 | 4 | module.exports = class Environment { 5 | constructor(settings) { 6 | this.settings = settings; 7 | this.headers = settings && settings.headers || {}; 8 | } 9 | 10 | getFunction() { 11 | return (req, res) => { 12 | const environmentService = new EnvironmentService(this.settings); 13 | const hostnameMatch = req.headers.referer 14 | ? req.headers.referer.match(HOSTNAME_REGEXP) 15 | : false; 16 | const hostname = hostnameMatch ? hostnameMatch[1] : req.hostname; 17 | const publicEnvironment = environmentService.getPublicEnvironment(hostname); 18 | 19 | for (let header in this.headers) { 20 | res.set(header, this.headers[header]); 21 | } 22 | res.status(200).send(`window.firebaseEnv = ${JSON.stringify(publicEnvironment)}`); 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /functions/onRequest/environment.onRequest.spec.js: -------------------------------------------------------------------------------- 1 | const Environment = require('./environment.onRequest'); 2 | const config = require('../config.json'); 3 | const environment = new Environment({ config }); 4 | const httpMocks = require('node-mocks-http'); 5 | 6 | describe('Environment', () => { 7 | let func, req, res; 8 | beforeEach(() => { 9 | func = environment.getFunction(); 10 | req = httpMocks.createRequest(); 11 | res = httpMocks.createResponse(); 12 | }); 13 | 14 | it('should serve from functions.config()', () => { 15 | func(req, res); 16 | 17 | const env = extractEnv(res); 18 | expect(!!env.firebase.databaseURL).toEqual(true); 19 | }); 20 | 21 | it('should allow overriding config', () => { 22 | const config = { firebase: 1, public: { test: 2 } }; 23 | const testEnvironment = new Environment({ config }); 24 | func = testEnvironment.getFunction(); 25 | 26 | func(req, res); 27 | 28 | const env = extractEnv(res); 29 | expect(env).toEqual(config); 30 | }); 31 | 32 | describe('Public vs. Private', () => { 33 | it('serves only firebase, public and shared', () => { 34 | func(req, res); 35 | 36 | const env = extractEnv(res); 37 | expect(Object.keys(env)).toEqual(['firebase', 'public']); 38 | }); 39 | 40 | it('does not serve config.credentials', () => { 41 | func(req, res); 42 | 43 | const env = extractEnv(res); 44 | expect(!!env.firebase.credential).toEqual(false); 45 | }); 46 | 47 | it('does not serve config.serviceAccount', () => { 48 | func(req, res); 49 | 50 | const env = extractEnv(res); 51 | expect(!!env.firebase.serviceAccount).toEqual(false); 52 | }); 53 | 54 | describe('Alternate paths', () => { 55 | beforeEach(() => { 56 | const testEnvironment = new Environment({ 57 | config, 58 | publicPath: 'test_public', 59 | shared: 'test_shared', 60 | }); 61 | func = testEnvironment.getFunction(); 62 | }); 63 | 64 | it('public', () => { 65 | func(req, res); 66 | 67 | const env = extractEnv(res); 68 | expect(env['test_public'].c).toEqual('test_public: c'); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('handles defaults', () => { 74 | it('starts with defaults', () => { 75 | func(req, res); 76 | 77 | const env = extractEnv(res); 78 | expect(env.public.a).toEqual('defaults: a'); 79 | }); 80 | 81 | it('overwrites localhost', () => { 82 | req = httpMocks.createRequest({ headers: { referer: 'http://localhost:8081/login' } }); 83 | 84 | func(req, res); 85 | const env = extractEnv(res); 86 | expect(env.public.a).toEqual('localhost: a'); 87 | }); 88 | 89 | it('overwrites subdomain:domain:tld', () => { 90 | req = httpMocks.createRequest({ 91 | headers: { referer: 'http://subdomain.domain.tld/some-garbage' }, 92 | }); 93 | 94 | func(req, res); 95 | const env = extractEnv(res); 96 | expect(env.public.a).toEqual('subdomain.domain.tld: a'); 97 | }); 98 | }); 99 | 100 | describe('Headers', () => { 101 | it('sets headers', () => { 102 | const config = { firebase: 1, public: 2 }; 103 | const headers = { 104 | 'Cache-Control': 'public, max-age=123', 105 | }; 106 | const testEnvironment = new Environment({ config, headers }); 107 | func = testEnvironment.getFunction(); 108 | 109 | func(req, res); 110 | 111 | expect(res.header('Cache-Control')).toEqual(headers['Cache-Control']); 112 | }); 113 | }); 114 | 115 | function extractEnv(res) { 116 | const response = res._getData(); 117 | const json = response.match(/{.+}/)[0]; 118 | return JSON.parse(json); 119 | } 120 | }); 121 | -------------------------------------------------------------------------------- /functions/onRequest/graphqlServer.onRequest.js: -------------------------------------------------------------------------------- 1 | // Credit: https://codeburst.io/graphql-server-on-cloud-functions-for-firebase-ae97441399c0 2 | 3 | const bodyParser = require('body-parser'); 4 | const morgan = require('morgan'); 5 | const Rx = require('rxjs/Rx'); 6 | const LocalDataService = require('../services/localData.service'); 7 | const express = require('express'); 8 | const graphqlServerExpress = require('graphql-server-express'); 9 | const graphqlExpress = graphqlServerExpress.graphqlExpress; 10 | const graphiqlExpress = graphqlServerExpress.graphiqlExpress; 11 | const schemaPrinter = require('graphql/utilities/schemaPrinter'); 12 | const printSchema = schemaPrinter.printSchema; 13 | 14 | module.exports = class GraphQLServer { 15 | constructor({ ref, transform } = {}) { 16 | if (ref) { 17 | this.ref = ref; 18 | this.localDataService = new LocalDataService({ ref, transform }); 19 | } 20 | this.app = express(); 21 | } 22 | 23 | start(generateSchema) { 24 | const data = this.localDataService.data; 25 | const schema = generateSchema(data); 26 | const observable = this.listen(schema); 27 | 28 | return this.localDataService.listen().merge(observable); 29 | } 30 | 31 | listen(generatorOrSchema) { 32 | const subject = new Rx.Subject(); 33 | const isGenerator = typeof generatorOrSchema == 'function'; 34 | const schema = isGenerator ? generatorOrSchema({ subject }) : generatorOrSchema; 35 | 36 | const logger = this.getLogger(subject); 37 | 38 | this.app.use('/graphql', [logger, bodyParser.json()], graphqlExpress({ schema, context: {} })); 39 | this.app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); 40 | this.app.use('/schema', this.printSchema(schema)); 41 | return subject; 42 | } 43 | 44 | getLogger(subject) { 45 | return (req, res, next) => { 46 | const stream = { 47 | write: log => { 48 | subject.next({ log }); 49 | }, 50 | }; 51 | morgan('tiny', { stream })(req, res, next); 52 | }; 53 | } 54 | 55 | printSchema(schema) { 56 | return (req, res) => { 57 | res.set('Content-Type', 'text/plain'); 58 | res.send(printSchema(schema)); 59 | }; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /functions/onRequest/graphqlServer.onRequest.spec.js: -------------------------------------------------------------------------------- 1 | const GraphQLServer = require('./graphqlServer.onRequest'); 2 | const admin = require('firebase-admin'); 3 | const config = require('../config.json'); 4 | const httpMocks = require('node-mocks-http'); 5 | const request = require('supertest'); 6 | 7 | admin.initializeApp({ 8 | databaseURL: config.firebase.databaseURL, 9 | credential: admin.credential.cert(config.firebase.serviceAccount), 10 | }); 11 | 12 | describe('GraphQLServer', () => { 13 | const GeneratorUtility = require('../../utilities/generator.utility'); 14 | const ref = admin.database().ref('test/graphqlServer'); 15 | const itemsCount = 5; 16 | const connectionsCount = 3; 17 | beforeAll(done => { 18 | const generatorUtility = new GeneratorUtility({ ref }); 19 | generatorUtility.generateIfNeeded(itemsCount, connectionsCount).then(done, done.fail); 20 | }); 21 | 22 | let server, schema, req, res; 23 | beforeEach(() => { 24 | const generator = require('../schemas/items.schema'); 25 | 26 | schema = () => { 27 | const map = new Map([ 28 | ['one', { i: 1, connections: { two: true } }], 29 | ['two', { i: 2, connections: { one: true } }], 30 | ]); 31 | return generator(map); 32 | }; 33 | server = new GraphQLServer({ ref }); 34 | req = httpMocks.createRequest(); 35 | res = httpMocks.createResponse(); 36 | }); 37 | 38 | it('printSchema', done => { 39 | start().then(() => { 40 | request(server.app) 41 | .get('/schema') 42 | .end((err, res) => { 43 | expect(res.text).toMatch('type'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | it('graphqlMiddleware', done => { 50 | start().then(() => { 51 | request(server.app) 52 | .get('/graphql?query={items{i,key,connections{i}}}') 53 | .end((err, res) => { 54 | expect(res.text).toEqual( 55 | '{"data":{"items":[{"i":1,"key":"one","connections":[{"i":2}]},{"i":2,"key":"two","connections":[{"i":1}]}]}}' 56 | ); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | 62 | it('logging', done => { 63 | start().then(observable => { 64 | observable 65 | .filter(x => !!x.log) 66 | .take(1) 67 | .subscribe(log => { 68 | expect(!!log.log).toEqual(true); 69 | done(); 70 | }); 71 | request(server.app) 72 | .get('/graphql?query={items{i,key,connections{i}}}') 73 | .end((err, res) => {}); 74 | }); 75 | }); 76 | 77 | describe('async middleware with a subject', () => { 78 | const generator = require('../schemas/async.schema'); 79 | let observable; 80 | beforeEach(() => { 81 | observable = server.listen(generator); 82 | }); 83 | 84 | it('should enable observables to fire', done => { 85 | observable 86 | .filter(x => x == 'async finished') 87 | .take(1) 88 | .subscribe(x => done()); 89 | 90 | request(server.app) 91 | .get('/graphql?query={async}') 92 | .end((err, res) => {}); 93 | }); 94 | }); 95 | 96 | function start() { 97 | return new Promise(resolve => { 98 | const observable = server.start(schema); 99 | observable 100 | .filter(x => x.event == 'ready') 101 | .take(1) 102 | .subscribe(() => { 103 | resolve(observable); 104 | }); 105 | }); 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /functions/onWrite/login.onWrite.js: -------------------------------------------------------------------------------- 1 | const EventService = require('../services/event.service'); 2 | 3 | module.exports = class Login { 4 | constructor({ usersPath, adminUsers, auth }) { 5 | if (!usersPath) { 6 | throw 'config.usersPath string missing. Looks something like "/{anyWildcard}/users"'; 7 | } 8 | if (!adminUsers) { 9 | throw 'config.adminUsers array missing. Looks like ["chris@chrisesplin.com", "anotherAdmin@chrisesplin.com"]'; 10 | } 11 | if (!auth) { 12 | throw 'config.auth missing.'; 13 | } 14 | this.usersPath = usersPath; 15 | this.adminUsers = adminUsers; 16 | this.auth = auth; 17 | 18 | this.eventService = new EventService(); 19 | } 20 | 21 | getFunction() { 22 | return event => { 23 | const payload = event.data.val(); 24 | if (!payload) return; 25 | 26 | const token = payload.token; 27 | const user = { 28 | lastLogin: Date.now(), 29 | isAdmin: null 30 | }; 31 | 32 | return Promise.resolve() 33 | .then(() => { 34 | return this.auth.verifyIdToken(token); 35 | }) 36 | .then(token => { 37 | const userRef = this.getUserRef({ ref: event.data.adminRef, uid: token.uid, params: event.params }); 38 | 39 | user.token = token; 40 | if (this.adminUsers.includes(token.email)) { 41 | user.isAdmin = true; 42 | } 43 | 44 | return userRef.update(user); 45 | }) 46 | .then(() => event.data.ref.remove()) 47 | .then(() => user) 48 | .catch(error => { 49 | return event.data.ref.update({ error }).then(() => Promise.reject(error)); 50 | }); 51 | }; 52 | } 53 | 54 | getUserRef({ ref, uid, params }) { 55 | let usersPath = this.eventService.getAbsolutePath(this.usersPath, params); 56 | return ref.root.child(usersPath).child(uid); 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /functions/onWrite/login.onWrite.spec.js: -------------------------------------------------------------------------------- 1 | describe('Login', function() { 2 | const functions = require('firebase-functions'); 3 | const admin = require('firebase-admin'); 4 | const config = require('../config.json'); 5 | 6 | admin.initializeApp({ 7 | credential: admin.credential.cert(config.firebase.serviceAccount), 8 | databaseURL: config.firebase.databaseURL, 9 | }); 10 | 11 | const mocks = require('../../mocks'); 12 | const Login = require('./login.onWrite'); 13 | 14 | const db = admin.database(); 15 | 16 | const usersPath = 'quiver-functions/test/users'; 17 | const usersRef = db.ref(usersPath); 18 | const uid = 'fake-uid'; 19 | const userRef = usersRef.child(uid); 20 | 21 | const loginQueuePath = 'quiver-functions/test/queues/login'; 22 | const loginQueueRef = db.ref(loginQueuePath); 23 | 24 | function cleanUp(done) { 25 | return Promise.all([usersRef.remove(), loginQueueRef.remove()]).then(() => done(), done.fail); 26 | } 27 | 28 | beforeEach(cleanUp); 29 | 30 | let login, func, app; 31 | beforeEach(() => { 32 | login = new Login({ 33 | usersPath: usersPath, 34 | adminUsers: [mocks.mockUser.email], 35 | auth: admin.auth(), 36 | }); 37 | 38 | func = login.getFunction(); 39 | app = admin.app(); 40 | }); 41 | 42 | afterEach(cleanUp); 43 | 44 | describe('test', () => { 45 | it('should work', () => { 46 | expect(true).toEqual(true); 47 | }); 48 | }); 49 | 50 | describe('getFunction', () => { 51 | it('should handle a verification failure', done => { 52 | const event = new mocks.MockDBEvent({app, adminApp: app, delta: {token: 123}}); 53 | spyOn(event.data.ref, 'update').and.returnValue(Promise.resolve()); 54 | spyOn(login.auth, 'verifyIdToken').and.returnValue(Promise.reject(1)); 55 | 56 | func(event).then(() => userRef.once('value')).then(done.fail).catch(err => { 57 | expect(err).toEqual(1); 58 | expect(event.data.ref.update.calls.allArgs()).toEqual([[{ error: 1 }]]); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should update the user', done => { 64 | const event = new mocks.MockDBEvent({app, adminApp: app, delta: {token: 123}}); 65 | const spy = jasmine.createSpy('spy'); 66 | const token = { uid: 1, email: mocks.mockUser.email }; 67 | spyOn(login, 'getUserRef').and.returnValue({ update: spy }); 68 | spyOn(login.auth, 'verifyIdToken').and.returnValue(Promise.resolve(token)); 69 | 70 | func(event) 71 | .then(payload => { 72 | const args = spy.calls.argsFor(0); 73 | const updatedUser = args[0]; 74 | expect(typeof updatedUser.lastLogin).toEqual('number'); 75 | expect(payload.isAdmin).toEqual(true); 76 | expect(typeof payload.lastLogin).toEqual('number'); 77 | expect(payload.token).toEqual(token); 78 | done(); 79 | }) 80 | .catch(done.fail); 81 | }); 82 | }); 83 | 84 | describe('getUserRef', () => { 85 | it('should return ref with this.usersPath and uid', () => { 86 | const ref = db.ref('some/ridiculous/path'); 87 | const uid = 123; 88 | const params = { environment: 'test' }; 89 | expect(login.getUserRef({ ref, uid, params }).toString()).toMatch( 90 | new RegExp(`quiver-functions/test/users/${uid}`) 91 | ); 92 | }); 93 | }); 94 | 95 | describe('admin', () => { 96 | let event, spy, token; 97 | beforeEach(() => { 98 | event = new mocks.MockDBEvent({app, adminApp: app, delta: {token: 123}}); 99 | spy = jasmine.createSpy('spy'); 100 | token = { uid: 1, email: mocks.mockUser.email }; 101 | spyOn(login, 'getUserRef').and.returnValue({ update: spy }); 102 | spyOn(login.auth, 'verifyIdToken').and.returnValue(Promise.resolve(token)); 103 | }); 104 | 105 | it('should set isAdmin: true when email matches', done => { 106 | func(event) 107 | .then(payload => { 108 | expect(payload.isAdmin).toEqual(true); 109 | done(); 110 | }) 111 | .catch(done.fail); 112 | }); 113 | 114 | it('should set isAdmin: false when email does not match', done => { 115 | token.email = 'another-email'; 116 | func(event) 117 | .then(payload => { 118 | expect(payload.isAdmin).toEqual(null); 119 | done(); 120 | }) 121 | .catch(done.fail); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "body-parser": "^1.18.1", 6 | "express": "^4.15.4", 7 | "firebase-admin": "^4.1.3", 8 | "firebase-functions": "^0.5", 9 | "graphql": "^0.11.3", 10 | "graphql-server-express": "^1.1.2", 11 | "graphql-tools": "^1.2.2", 12 | "morgan": "^1.8.2", 13 | "rxjs": "^5.4.3" 14 | }, 15 | "private": true 16 | } 17 | -------------------------------------------------------------------------------- /functions/schemas/async.schema.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLSchema} = graphql; 3 | 4 | module.exports = args => { 5 | const query = require('./types/async.type')(args); 6 | return new GraphQLSchema({ query }); 7 | }; 8 | -------------------------------------------------------------------------------- /functions/schemas/items.schema.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLSchema} = graphql; 3 | 4 | module.exports = data => { 5 | const query = require('./types/itemsQuery.type')(data); 6 | return new GraphQLSchema({ query }); 7 | }; 8 | -------------------------------------------------------------------------------- /functions/schemas/types/async.type.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLList, GraphQLObjectType, GraphQLString } = graphql; 3 | 4 | module.exports = ({ subject }) => { 5 | return new GraphQLObjectType({ 6 | name: 'Query', 7 | description: 'The root of all... queries', 8 | fields: () => ({ 9 | async: { 10 | type: new GraphQLList(GraphQLString), 11 | resolve: root => { 12 | const text = 'async finished'; 13 | subject.next(text); 14 | return text; 15 | }, 16 | } 17 | }), 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /functions/schemas/types/item.type.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLList, GraphQLObjectType, GraphQLString, GraphQLInt } = graphql; 3 | 4 | module.exports = data => { 5 | const ItemType = new GraphQLObjectType({ 6 | name: 'Item', 7 | description: 'An item', 8 | fields: () => ({ 9 | i: { type: GraphQLInt }, 10 | key: { type: GraphQLString }, 11 | connections: { 12 | type: new GraphQLList(ItemType), 13 | resolve: item => { 14 | return Object.keys(item.connections).reduce((items, key) => { 15 | const item = data.get(key); 16 | item.key = key; 17 | items.push(item); 18 | return items; 19 | }, []); 20 | }, 21 | }, 22 | }), 23 | }); 24 | 25 | return ItemType; 26 | }; 27 | -------------------------------------------------------------------------------- /functions/schemas/types/itemsQuery.type.js: -------------------------------------------------------------------------------- 1 | const graphql = require('graphql'); 2 | const { GraphQLList, GraphQLObjectType, GraphQLString } = graphql; 3 | 4 | module.exports = data => { 5 | const ItemType = require('./item.type')(data); 6 | return new GraphQLObjectType({ 7 | name: 'Query', 8 | description: 'The root of all... queries', 9 | fields: () => ({ 10 | items: { 11 | type: new GraphQLList(ItemType), 12 | resolve: root => { 13 | const result = []; 14 | data.forEach((item, key) => { 15 | item.key = key; 16 | result.push(item); 17 | }); 18 | return result; 19 | }, 20 | }, 21 | item: { 22 | type: ItemType, 23 | args: { 24 | key: { type: GraphQLString }, 25 | }, 26 | resolve: (root, args) => { 27 | return data.get(args.key); 28 | }, 29 | }, 30 | }), 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /functions/services/environment.service.js: -------------------------------------------------------------------------------- 1 | module.exports = class EnvironmentService { 2 | constructor({config, publicPath = 'public', ignorePort}) { 3 | this.config = config; 4 | this.publicPath = publicPath; 5 | this.ignorePort = ignorePort; 6 | } 7 | 8 | get environment() { 9 | return require('../config.json'); 10 | } 11 | 12 | getPublicEnvironment(dirtyHost) { 13 | const host = this.getHost(dirtyHost); 14 | const config = this.config; 15 | const computedConfig = { firebase: config.firebase }; 16 | 17 | delete config.firebase.credential; 18 | delete config.firebase.serviceAccount; 19 | 20 | const publicEnvironment = { 21 | firebase: config.firebase, 22 | [this.publicPath]: Object.assign({}, config[this.publicPath]), 23 | }; 24 | 25 | if (publicEnvironment[this.publicPath] && publicEnvironment[this.publicPath][host]) { 26 | let overrides = publicEnvironment[this.publicPath][host]; 27 | for (let key in overrides) { 28 | publicEnvironment[this.publicPath][key] = publicEnvironment[this.publicPath][host][key]; 29 | } 30 | } 31 | return publicEnvironment; 32 | } 33 | 34 | getHost(host) { 35 | if (host) { 36 | host = host.replace(/\./g, '_'); 37 | 38 | if (this.ignorePort) { 39 | host = host.replace(/:\d+/, ''); 40 | } 41 | } 42 | return host; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /functions/services/environment.service.spec.js: -------------------------------------------------------------------------------- 1 | const Service = require('./environment.service'); 2 | const config = require('../config.json'); 3 | 4 | describe('EnvironmentService', () => { 5 | let service; 6 | beforeEach(() => { 7 | service = new Service({ config }); 8 | }); 9 | 10 | describe('getPublicEnvironment', () => { 11 | it('should resolve localhost', () => { 12 | const expected = { 13 | firebase: { 14 | apiKey: 'AIzaSyAgB7JVLSl-3TQGL-qvQ7mdSYjmpPNV7xQ', 15 | authDomain: 'quiver-two.firebaseapp.com', 16 | databaseURL: 'https://quiver-two.firebaseio.com', 17 | storageBucket: 'quiver-two.appspot.com', 18 | }, 19 | public: { 20 | a: 'localhost: a', 21 | b: 'localhost: b', 22 | localhost: { a: 'localhost: a', b: 'localhost: b' }, 23 | models: { queues: { 'current-user': 'quiver-functions/queues/current-user' } }, 24 | subdomain_domain_tld: { 25 | a: 'subdomain.domain.tld: a', 26 | b: 'subdomain.domain.tld: b', 27 | }, 28 | }, 29 | }; 30 | expect(service.getPublicEnvironment('localhost')).toEqual(expected); 31 | }); 32 | 33 | it('should resolve not localhost:8080 without ignorePort', () => { 34 | const environment = service.getPublicEnvironment('localhost:8080'); 35 | expect(environment.public.a).toEqual('defaults: a'); 36 | }); 37 | 38 | it('should resolve localhost:8080 when ignorePort is set', () => { 39 | service.ignorePort = true; 40 | const environment = service.getPublicEnvironment('localhost:8080'); 41 | expect(environment.public.a).toEqual('localhost: a'); 42 | }); 43 | 44 | it('should resolve subdomain_domain_tld', () => { 45 | const expected = { 46 | firebase: { 47 | apiKey: 'AIzaSyAgB7JVLSl-3TQGL-qvQ7mdSYjmpPNV7xQ', 48 | authDomain: 'quiver-two.firebaseapp.com', 49 | databaseURL: 'https://quiver-two.firebaseio.com', 50 | storageBucket: 'quiver-two.appspot.com', 51 | }, 52 | public: { 53 | a: 'subdomain.domain.tld: a', 54 | b: 'subdomain.domain.tld: b', 55 | localhost: { a: 'localhost: a', b: 'localhost: b' }, 56 | models: { queues: { 'current-user': 'quiver-functions/queues/current-user' } }, 57 | subdomain_domain_tld: { a: 'subdomain.domain.tld: a', b: 'subdomain.domain.tld: b' }, 58 | }, 59 | }; 60 | expect(service.getPublicEnvironment('subdomain_domain_tld')).toEqual(expected); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /functions/services/event.service.js: -------------------------------------------------------------------------------- 1 | module.exports = class EventService { 2 | getAbsolutePath(path, params = {}) { 3 | let result = path; 4 | for (let key in params) { 5 | const value = params[key]; 6 | const regexp = new RegExp(`{${key}}`); 7 | result = result.replace(regexp, value); 8 | } 9 | 10 | if (result.match(/(\{|\})/)) { 11 | throw `Params insufficient to replace all wildcards: ${result}`; 12 | } 13 | return result; 14 | } 15 | } -------------------------------------------------------------------------------- /functions/services/event.service.spec.js: -------------------------------------------------------------------------------- 1 | const EventService = require('./event.service'); 2 | 3 | describe('EventService', () => { 4 | let service; 5 | beforeEach(() => { 6 | service = new EventService(); 7 | }); 8 | 9 | describe('getAbsolutePath', () => { 10 | let path; 11 | beforeEach(() => { 12 | path = 'one/{two}/three/{four}/five/{six}'; 13 | }); 14 | 15 | it('should replace multiple params', () => { 16 | expect(service.getAbsolutePath(path, { two: 2, four: 4, six: 6 })).toEqual('one/2/three/4/five/6'); 17 | }); 18 | 19 | it('should throw for missing params', () => { 20 | expect(() => service.getAbsolutePath(path)).toThrow( 21 | 'Params insufficient to replace all wildcards: one/{two}/three/{four}/five/{six}' 22 | ); 23 | }); 24 | }); 25 | }); -------------------------------------------------------------------------------- /functions/services/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | EnvironmentService: require('./environment.service'), 3 | EventService: require('./event.service'), 4 | LocalDataService: require('./localData.service'), 5 | } -------------------------------------------------------------------------------- /functions/services/localData.service.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rxjs/Rx'); 2 | 3 | module.exports = class LocalDataService { 4 | constructor({ ref, transform }) { 5 | this.ref = ref; 6 | this.data = new Map(); 7 | this.handlers = {}; 8 | this.transform = transform; 9 | } 10 | 11 | listen() { 12 | return Rx.Observable.create(observer => { 13 | this.observer = observer; 14 | this.getLastKey().then(lastKey => { 15 | this.handlers.childAdded = this.listenToChildAdded(observer, lastKey); 16 | this.handlers.childChanged = this.listenToChildChanged(observer); 17 | this.handlers.childRemoved = this.listenToChildRemoved(observer); 18 | }); 19 | }); 20 | } 21 | 22 | unlisten() { 23 | this.ref.off('child_added', this.handlers.childAdded); 24 | this.ref.off('child_changed', this.handlers.childChanged); 25 | this.ref.off('child_removed', this.handlers.childRemoved); 26 | } 27 | 28 | getLastKey() { 29 | return this.ref 30 | .limitToLast(1) 31 | .once('child_added') 32 | .then(snap => snap.key); 33 | } 34 | 35 | listenToChildAdded(observer, lastKey) { 36 | let ready = false; 37 | 38 | return this.ref.on('child_added', snap => { 39 | const key = snap.key; 40 | const value = snap.val(); 41 | 42 | this.set(key, value); 43 | 44 | observer.next({ 45 | event: 'child_added', 46 | key, 47 | value, 48 | }); 49 | 50 | if (!ready && key == lastKey) { 51 | this.ready = true; 52 | observer.next({ event: 'ready', key }); 53 | } 54 | }); 55 | } 56 | 57 | listenToChildChanged(observer) { 58 | return this.ref.on('child_changed', snap => { 59 | const key = snap.key; 60 | const value = snap.val(); 61 | 62 | this.set(key, value); 63 | 64 | observer.next({ 65 | event: 'child_changed', 66 | key, 67 | value, 68 | }); 69 | }); 70 | } 71 | 72 | listenToChildRemoved(observer) { 73 | return this.ref.on('child_removed', snap => { 74 | const key = snap.key; 75 | const value = snap.val(); 76 | 77 | this.data.delete(key); 78 | 79 | observer.next({ 80 | event: 'child_removed', 81 | key, 82 | value, 83 | }); 84 | }); 85 | } 86 | 87 | set(key, value) { 88 | if (typeof this.transform == 'function') { 89 | value = this.transform({ key, value, data: this.data }); 90 | } 91 | this.data.set(key, value); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /functions/services/localData.service.spec.js: -------------------------------------------------------------------------------- 1 | const LocalDataService = require('./localData.service'); 2 | const admin = require('firebase-admin'); 3 | const config = require('../config.json'); 4 | 5 | admin.initializeApp({ 6 | databaseURL: config.firebase.databaseURL, 7 | credential: admin.credential.cert(config.firebase.serviceAccount), 8 | }); 9 | 10 | describe('GraphQLServer', () => { 11 | const GeneratorUtility = require('../../utilities/generator.utility'); 12 | const ref = admin.database().ref('test/localData'); 13 | const itemsCount = 5; 14 | const connectionsCount = 3; 15 | beforeAll(done => { 16 | const generatorUtility = new GeneratorUtility({ ref }); 17 | generatorUtility.generateIfNeeded(itemsCount, connectionsCount).then(done, done.fail); 18 | }); 19 | 20 | let service; 21 | beforeEach(() => { 22 | service = new LocalDataService({ ref }); 23 | }); 24 | 25 | it('listen', done => { 26 | service 27 | .listen() 28 | .filter(x => x.event == 'ready') 29 | .take(1) 30 | .subscribe(e => { 31 | let totalConnections = 0; 32 | 33 | service.data.forEach(item => { 34 | totalConnections += Object.keys(item.connections).length; 35 | }); 36 | 37 | expect(service.data.size).toEqual(itemsCount); 38 | expect(totalConnections).toEqual(itemsCount * connectionsCount); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('child_changed', done => { 44 | const observable = service.listen(); 45 | 46 | observable.filter(x => x.event == 'child_changed').take(1).subscribe(e => { 47 | expect(e.value.changed).toEqual(true); 48 | done(); 49 | }); 50 | 51 | observable.filter(x => x.event == 'ready').take(1).subscribe(e => { 52 | ref.child(e.key).update({ changed: true }); 53 | }); 54 | }); 55 | 56 | it('child_removed', done => { 57 | const observable = service.listen(); 58 | let lastKey; 59 | 60 | observable.filter(x => x.event == 'child_removed').take(1).subscribe(e => { 61 | expect(e.key).toEqual(lastKey); 62 | done(); 63 | }); 64 | 65 | observable.filter(x => x.event == 'ready').take(1).subscribe(e => { 66 | lastKey = e.key; 67 | ref.child(e.key).remove(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /functions/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/express-serve-static-core@*": 6 | version "4.0.41" 7 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.41.tgz#05df354cbbe5069b4c089320065870033f41e670" 8 | dependencies: 9 | "@types/node" "*" 10 | 11 | "@types/express@^4.0.33": 12 | version "4.0.35" 13 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.35.tgz#6267c7b60a51fac473467b3c4a02cd1e441805fe" 14 | dependencies: 15 | "@types/express-serve-static-core" "*" 16 | "@types/serve-static" "*" 17 | 18 | "@types/graphql@^0.9.0": 19 | version "0.9.4" 20 | resolved "https://registry.npmjs.org/@types/graphql/-/graphql-0.9.4.tgz#cdeb6bcbef9b6c584374b81aa7f48ecf3da404fa" 21 | 22 | "@types/jsonwebtoken@^7.1.32", "@types/jsonwebtoken@^7.1.33": 23 | version "7.2.0" 24 | resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.0.tgz#0fed32c8501da80ac9839d2d403a65c83d776ffd" 25 | dependencies: 26 | "@types/node" "*" 27 | 28 | "@types/lodash@^4.14.34": 29 | version "4.14.55" 30 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.55.tgz#75d7d4eba020ee4103d4cbd0f2a3ef5db8f7534f" 31 | 32 | "@types/mime@*": 33 | version "0.0.29" 34 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" 35 | 36 | "@types/node@*": 37 | version "7.0.8" 38 | resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.8.tgz#25e4dd804b630c916ae671233e6d71f6ce18124a" 39 | 40 | "@types/serve-static@*": 41 | version "1.7.31" 42 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.7.31.tgz#15456de8d98d6b4cff31be6c6af7492ae63f521a" 43 | dependencies: 44 | "@types/express-serve-static-core" "*" 45 | "@types/mime" "*" 46 | 47 | "@types/sha1@^1.1.0": 48 | version "1.1.0" 49 | resolved "https://registry.yarnpkg.com/@types/sha1/-/sha1-1.1.0.tgz#461eb18906d25e8d07c4678a0ed4f9ca07e46dd9" 50 | dependencies: 51 | "@types/node" "*" 52 | 53 | accepts@~1.3.3: 54 | version "1.3.3" 55 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 56 | dependencies: 57 | mime-types "~2.1.11" 58 | negotiator "0.6.1" 59 | 60 | apollo-server-core@^1.1.0: 61 | version "1.1.0" 62 | resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.1.0.tgz#74c3bf4394e14eae7ab60b1d999a3c5b8aa94e9a" 63 | dependencies: 64 | apollo-tracing "^0.0.7" 65 | 66 | apollo-server-express@^1.1.2: 67 | version "1.1.2" 68 | resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.1.2.tgz#6933c77fe5dfb9a7f30dd393239ad9953a613cd9" 69 | dependencies: 70 | apollo-server-core "^1.1.0" 71 | apollo-server-module-graphiql "^1.1.2" 72 | 73 | apollo-server-module-graphiql@^1.1.2: 74 | version "1.1.2" 75 | resolved "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.1.2.tgz#49a154cf80e984acb082bd0096175b561e1bfbcc" 76 | 77 | apollo-tracing@^0.0.7: 78 | version "0.0.7" 79 | resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.0.7.tgz#78466cfefdb52a0802a57b488d26a1a67a25909f" 80 | dependencies: 81 | graphql-tools "^1.1.0" 82 | 83 | array-flatten@1.1.1: 84 | version "1.1.1" 85 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 86 | 87 | base64url@2.0.0, base64url@^2.0.0: 88 | version "2.0.0" 89 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 90 | 91 | basic-auth@~1.1.0: 92 | version "1.1.0" 93 | resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" 94 | 95 | body-parser@^1.18.1: 96 | version "1.18.1" 97 | resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.18.1.tgz#9c1629370bcfd42917f30641a2dcbe2ec50d4c26" 98 | dependencies: 99 | bytes "3.0.0" 100 | content-type "~1.0.4" 101 | debug "2.6.8" 102 | depd "~1.1.1" 103 | http-errors "~1.6.2" 104 | iconv-lite "0.4.19" 105 | on-finished "~2.3.0" 106 | qs "6.5.1" 107 | raw-body "2.3.2" 108 | type-is "~1.6.15" 109 | 110 | buffer-equal-constant-time@1.0.1: 111 | version "1.0.1" 112 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 113 | 114 | bytes@3.0.0: 115 | version "3.0.0" 116 | resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 117 | 118 | "charenc@>= 0.0.1": 119 | version "0.0.2" 120 | resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 121 | 122 | content-disposition@0.5.2: 123 | version "0.5.2" 124 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 125 | 126 | content-type@~1.0.2: 127 | version "1.0.2" 128 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 129 | 130 | content-type@~1.0.4: 131 | version "1.0.4" 132 | resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 133 | 134 | cookie-signature@1.0.6: 135 | version "1.0.6" 136 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 137 | 138 | cookie@0.3.1: 139 | version "0.3.1" 140 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 141 | 142 | "crypt@>= 0.0.1": 143 | version "0.0.2" 144 | resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 145 | 146 | debug@2.6.1: 147 | version "2.6.1" 148 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" 149 | dependencies: 150 | ms "0.7.2" 151 | 152 | debug@2.6.8: 153 | version "2.6.8" 154 | resolved "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" 155 | dependencies: 156 | ms "2.0.0" 157 | 158 | depd@1.1.0, depd@~1.1.0: 159 | version "1.1.0" 160 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 161 | 162 | depd@1.1.1, depd@~1.1.1: 163 | version "1.1.1" 164 | resolved "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 165 | 166 | deprecated-decorator@^0.1.6: 167 | version "0.1.6" 168 | resolved "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" 169 | 170 | destroy@~1.0.4: 171 | version "1.0.4" 172 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 173 | 174 | ecdsa-sig-formatter@1.0.9: 175 | version "1.0.9" 176 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 177 | dependencies: 178 | base64url "^2.0.0" 179 | safe-buffer "^5.0.1" 180 | 181 | ee-first@1.1.1: 182 | version "1.1.1" 183 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 184 | 185 | encodeurl@~1.0.1: 186 | version "1.0.1" 187 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 188 | 189 | escape-html@~1.0.3: 190 | version "1.0.3" 191 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 192 | 193 | etag@~1.8.0: 194 | version "1.8.0" 195 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 196 | 197 | express@^4.0.33: 198 | version "4.15.2" 199 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" 200 | dependencies: 201 | accepts "~1.3.3" 202 | array-flatten "1.1.1" 203 | content-disposition "0.5.2" 204 | content-type "~1.0.2" 205 | cookie "0.3.1" 206 | cookie-signature "1.0.6" 207 | debug "2.6.1" 208 | depd "~1.1.0" 209 | encodeurl "~1.0.1" 210 | escape-html "~1.0.3" 211 | etag "~1.8.0" 212 | finalhandler "~1.0.0" 213 | fresh "0.5.0" 214 | merge-descriptors "1.0.1" 215 | methods "~1.1.2" 216 | on-finished "~2.3.0" 217 | parseurl "~1.3.1" 218 | path-to-regexp "0.1.7" 219 | proxy-addr "~1.1.3" 220 | qs "6.4.0" 221 | range-parser "~1.2.0" 222 | send "0.15.1" 223 | serve-static "1.12.1" 224 | setprototypeof "1.0.3" 225 | statuses "~1.3.1" 226 | type-is "~1.6.14" 227 | utils-merge "1.0.0" 228 | vary "~1.1.0" 229 | 230 | express@^4.15.4: 231 | version "4.15.4" 232 | resolved "https://registry.npmjs.org/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" 233 | dependencies: 234 | accepts "~1.3.3" 235 | array-flatten "1.1.1" 236 | content-disposition "0.5.2" 237 | content-type "~1.0.2" 238 | cookie "0.3.1" 239 | cookie-signature "1.0.6" 240 | debug "2.6.8" 241 | depd "~1.1.1" 242 | encodeurl "~1.0.1" 243 | escape-html "~1.0.3" 244 | etag "~1.8.0" 245 | finalhandler "~1.0.4" 246 | fresh "0.5.0" 247 | merge-descriptors "1.0.1" 248 | methods "~1.1.2" 249 | on-finished "~2.3.0" 250 | parseurl "~1.3.1" 251 | path-to-regexp "0.1.7" 252 | proxy-addr "~1.1.5" 253 | qs "6.5.0" 254 | range-parser "~1.2.0" 255 | send "0.15.4" 256 | serve-static "1.12.4" 257 | setprototypeof "1.0.3" 258 | statuses "~1.3.1" 259 | type-is "~1.6.15" 260 | utils-merge "1.0.0" 261 | vary "~1.1.1" 262 | 263 | faye-websocket@0.9.3: 264 | version "0.9.3" 265 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.3.tgz#482a505b0df0ae626b969866d3bd740cdb962e83" 266 | dependencies: 267 | websocket-driver ">=0.5.1" 268 | 269 | finalhandler@~1.0.0: 270 | version "1.0.0" 271 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.0.tgz#b5691c2c0912092f18ac23e9416bde5cd7dc6755" 272 | dependencies: 273 | debug "2.6.1" 274 | encodeurl "~1.0.1" 275 | escape-html "~1.0.3" 276 | on-finished "~2.3.0" 277 | parseurl "~1.3.1" 278 | statuses "~1.3.1" 279 | unpipe "~1.0.0" 280 | 281 | finalhandler@~1.0.4: 282 | version "1.0.4" 283 | resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" 284 | dependencies: 285 | debug "2.6.8" 286 | encodeurl "~1.0.1" 287 | escape-html "~1.0.3" 288 | on-finished "~2.3.0" 289 | parseurl "~1.3.1" 290 | statuses "~1.3.1" 291 | unpipe "~1.0.0" 292 | 293 | firebase-admin@^4.1.3: 294 | version "4.1.3" 295 | resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-4.1.3.tgz#7203b0914172dde224b1b20fbcb8c7923d548778" 296 | dependencies: 297 | "@types/jsonwebtoken" "^7.1.33" 298 | faye-websocket "0.9.3" 299 | jsonwebtoken "7.1.9" 300 | 301 | firebase-functions@^0.5: 302 | version "0.5.2" 303 | resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-0.5.2.tgz#ee0708923d6f87e1c754e0ad367d1e07dc46346a" 304 | dependencies: 305 | "@types/express" "^4.0.33" 306 | "@types/jsonwebtoken" "^7.1.32" 307 | "@types/lodash" "^4.14.34" 308 | "@types/sha1" "^1.1.0" 309 | express "^4.0.33" 310 | jsonwebtoken "^7.1.9" 311 | lodash "^4.6.1" 312 | sha1 "^1.1.1" 313 | 314 | forwarded@~0.1.0: 315 | version "0.1.0" 316 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 317 | 318 | fresh@0.5.0: 319 | version "0.5.0" 320 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 321 | 322 | graphql-server-express@^1.1.2: 323 | version "1.1.2" 324 | resolved "https://registry.npmjs.org/graphql-server-express/-/graphql-server-express-1.1.2.tgz#6e8a306a6616ac72c7850efa9fda9cba29836335" 325 | dependencies: 326 | apollo-server-express "^1.1.2" 327 | 328 | graphql-tools@^1.1.0, graphql-tools@^1.2.2: 329 | version "1.2.2" 330 | resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-1.2.2.tgz#ff791e91b78e05eec18a32716a7732bc7bf5cb4d" 331 | dependencies: 332 | deprecated-decorator "^0.1.6" 333 | uuid "^3.0.1" 334 | optionalDependencies: 335 | "@types/graphql" "^0.9.0" 336 | 337 | graphql@^0.11.3: 338 | version "0.11.3" 339 | resolved "https://registry.npmjs.org/graphql/-/graphql-0.11.3.tgz#9934e2df28f17d397a85f83cb39d1d179bffef47" 340 | dependencies: 341 | iterall "^1.1.0" 342 | 343 | hoek@2.x.x: 344 | version "2.16.3" 345 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 346 | 347 | http-errors@1.6.2, http-errors@~1.6.2: 348 | version "1.6.2" 349 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 350 | dependencies: 351 | depd "1.1.1" 352 | inherits "2.0.3" 353 | setprototypeof "1.0.3" 354 | statuses ">= 1.3.1 < 2" 355 | 356 | http-errors@~1.6.1: 357 | version "1.6.1" 358 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 359 | dependencies: 360 | depd "1.1.0" 361 | inherits "2.0.3" 362 | setprototypeof "1.0.3" 363 | statuses ">= 1.3.1 < 2" 364 | 365 | iconv-lite@0.4.19: 366 | version "0.4.19" 367 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 368 | 369 | inherits@2.0.3: 370 | version "2.0.3" 371 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 372 | 373 | ipaddr.js@1.2.0: 374 | version "1.2.0" 375 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4" 376 | 377 | ipaddr.js@1.4.0: 378 | version "1.4.0" 379 | resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" 380 | 381 | isemail@1.x.x: 382 | version "1.2.0" 383 | resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" 384 | 385 | iterall@^1.1.0: 386 | version "1.1.1" 387 | resolved "https://registry.npmjs.org/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" 388 | 389 | joi@^6.10.1: 390 | version "6.10.1" 391 | resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" 392 | dependencies: 393 | hoek "2.x.x" 394 | isemail "1.x.x" 395 | moment "2.x.x" 396 | topo "1.x.x" 397 | 398 | jsonwebtoken@7.1.9: 399 | version "7.1.9" 400 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz#847804e5258bec5a9499a8dc4a5e7a3bae08d58a" 401 | dependencies: 402 | joi "^6.10.1" 403 | jws "^3.1.3" 404 | lodash.once "^4.0.0" 405 | ms "^0.7.1" 406 | xtend "^4.0.1" 407 | 408 | jsonwebtoken@^7.1.9: 409 | version "7.3.0" 410 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.3.0.tgz#85118d6a70e3fccdf14389f4e7a1c3f9c8a9fbba" 411 | dependencies: 412 | joi "^6.10.1" 413 | jws "^3.1.4" 414 | lodash.once "^4.0.0" 415 | ms "^0.7.1" 416 | xtend "^4.0.1" 417 | 418 | jwa@^1.1.4: 419 | version "1.1.5" 420 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 421 | dependencies: 422 | base64url "2.0.0" 423 | buffer-equal-constant-time "1.0.1" 424 | ecdsa-sig-formatter "1.0.9" 425 | safe-buffer "^5.0.1" 426 | 427 | jws@^3.1.3, jws@^3.1.4: 428 | version "3.1.4" 429 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 430 | dependencies: 431 | base64url "^2.0.0" 432 | jwa "^1.1.4" 433 | safe-buffer "^5.0.1" 434 | 435 | lodash.once@^4.0.0: 436 | version "4.1.1" 437 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 438 | 439 | lodash@^4.6.1: 440 | version "4.17.4" 441 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 442 | 443 | media-typer@0.3.0: 444 | version "0.3.0" 445 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 446 | 447 | merge-descriptors@1.0.1: 448 | version "1.0.1" 449 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 450 | 451 | methods@~1.1.2: 452 | version "1.1.2" 453 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 454 | 455 | mime-db@~1.26.0: 456 | version "1.26.0" 457 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" 458 | 459 | mime-db@~1.30.0: 460 | version "1.30.0" 461 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 462 | 463 | mime-types@~2.1.11, mime-types@~2.1.13: 464 | version "2.1.14" 465 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" 466 | dependencies: 467 | mime-db "~1.26.0" 468 | 469 | mime-types@~2.1.15: 470 | version "2.1.17" 471 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 472 | dependencies: 473 | mime-db "~1.30.0" 474 | 475 | mime@1.3.4: 476 | version "1.3.4" 477 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 478 | 479 | moment@2.x.x: 480 | version "2.17.1" 481 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" 482 | 483 | morgan@^1.8.2: 484 | version "1.8.2" 485 | resolved "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" 486 | dependencies: 487 | basic-auth "~1.1.0" 488 | debug "2.6.8" 489 | depd "~1.1.0" 490 | on-finished "~2.3.0" 491 | on-headers "~1.0.1" 492 | 493 | ms@0.7.2: 494 | version "0.7.2" 495 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 496 | 497 | ms@2.0.0: 498 | version "2.0.0" 499 | resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 500 | 501 | ms@^0.7.1: 502 | version "0.7.3" 503 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" 504 | 505 | negotiator@0.6.1: 506 | version "0.6.1" 507 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 508 | 509 | on-finished@~2.3.0: 510 | version "2.3.0" 511 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 512 | dependencies: 513 | ee-first "1.1.1" 514 | 515 | on-headers@~1.0.1: 516 | version "1.0.1" 517 | resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" 518 | 519 | parseurl@~1.3.1: 520 | version "1.3.1" 521 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 522 | 523 | path-to-regexp@0.1.7: 524 | version "0.1.7" 525 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 526 | 527 | proxy-addr@~1.1.3: 528 | version "1.1.3" 529 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" 530 | dependencies: 531 | forwarded "~0.1.0" 532 | ipaddr.js "1.2.0" 533 | 534 | proxy-addr@~1.1.5: 535 | version "1.1.5" 536 | resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" 537 | dependencies: 538 | forwarded "~0.1.0" 539 | ipaddr.js "1.4.0" 540 | 541 | qs@6.4.0: 542 | version "6.4.0" 543 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 544 | 545 | qs@6.5.0: 546 | version "6.5.0" 547 | resolved "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" 548 | 549 | qs@6.5.1: 550 | version "6.5.1" 551 | resolved "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 552 | 553 | range-parser@~1.2.0: 554 | version "1.2.0" 555 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 556 | 557 | raw-body@2.3.2: 558 | version "2.3.2" 559 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 560 | dependencies: 561 | bytes "3.0.0" 562 | http-errors "1.6.2" 563 | iconv-lite "0.4.19" 564 | unpipe "1.0.0" 565 | 566 | rxjs@^5.4.3: 567 | version "5.4.3" 568 | resolved "https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" 569 | dependencies: 570 | symbol-observable "^1.0.1" 571 | 572 | safe-buffer@^5.0.1: 573 | version "5.0.1" 574 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" 575 | 576 | send@0.15.1: 577 | version "0.15.1" 578 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" 579 | dependencies: 580 | debug "2.6.1" 581 | depd "~1.1.0" 582 | destroy "~1.0.4" 583 | encodeurl "~1.0.1" 584 | escape-html "~1.0.3" 585 | etag "~1.8.0" 586 | fresh "0.5.0" 587 | http-errors "~1.6.1" 588 | mime "1.3.4" 589 | ms "0.7.2" 590 | on-finished "~2.3.0" 591 | range-parser "~1.2.0" 592 | statuses "~1.3.1" 593 | 594 | send@0.15.4: 595 | version "0.15.4" 596 | resolved "https://registry.npmjs.org/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" 597 | dependencies: 598 | debug "2.6.8" 599 | depd "~1.1.1" 600 | destroy "~1.0.4" 601 | encodeurl "~1.0.1" 602 | escape-html "~1.0.3" 603 | etag "~1.8.0" 604 | fresh "0.5.0" 605 | http-errors "~1.6.2" 606 | mime "1.3.4" 607 | ms "2.0.0" 608 | on-finished "~2.3.0" 609 | range-parser "~1.2.0" 610 | statuses "~1.3.1" 611 | 612 | serve-static@1.12.1: 613 | version "1.12.1" 614 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039" 615 | dependencies: 616 | encodeurl "~1.0.1" 617 | escape-html "~1.0.3" 618 | parseurl "~1.3.1" 619 | send "0.15.1" 620 | 621 | serve-static@1.12.4: 622 | version "1.12.4" 623 | resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" 624 | dependencies: 625 | encodeurl "~1.0.1" 626 | escape-html "~1.0.3" 627 | parseurl "~1.3.1" 628 | send "0.15.4" 629 | 630 | setprototypeof@1.0.3: 631 | version "1.0.3" 632 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 633 | 634 | sha1@^1.1.1: 635 | version "1.1.1" 636 | resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" 637 | dependencies: 638 | charenc ">= 0.0.1" 639 | crypt ">= 0.0.1" 640 | 641 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 642 | version "1.3.1" 643 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 644 | 645 | symbol-observable@^1.0.1: 646 | version "1.0.4" 647 | resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" 648 | 649 | topo@1.x.x: 650 | version "1.1.0" 651 | resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" 652 | dependencies: 653 | hoek "2.x.x" 654 | 655 | type-is@~1.6.14: 656 | version "1.6.14" 657 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" 658 | dependencies: 659 | media-typer "0.3.0" 660 | mime-types "~2.1.13" 661 | 662 | type-is@~1.6.15: 663 | version "1.6.15" 664 | resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 665 | dependencies: 666 | media-typer "0.3.0" 667 | mime-types "~2.1.15" 668 | 669 | unpipe@1.0.0, unpipe@~1.0.0: 670 | version "1.0.0" 671 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 672 | 673 | utils-merge@1.0.0: 674 | version "1.0.0" 675 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 676 | 677 | uuid@^3.0.1: 678 | version "3.1.0" 679 | resolved "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" 680 | 681 | vary@~1.1.0: 682 | version "1.1.0" 683 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" 684 | 685 | vary@~1.1.1: 686 | version "1.1.1" 687 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 688 | 689 | websocket-driver@>=0.5.1: 690 | version "0.6.5" 691 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" 692 | dependencies: 693 | websocket-extensions ">=0.1.1" 694 | 695 | websocket-extensions@>=0.1.1: 696 | version "0.1.1" 697 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" 698 | 699 | xtend@^4.0.1: 700 | version "4.0.1" 701 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 702 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./modules'); 2 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": ["utilities", "functions", "services", "modules"] 3 | } 4 | -------------------------------------------------------------------------------- /mocks/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MockAuthEvent: require('./mock-auth-event'), 3 | MockDBEvent: require('./mock-db-event'), 4 | mockUser: require('./mock-user.json') 5 | }; -------------------------------------------------------------------------------- /mocks/mock-auth-event.js: -------------------------------------------------------------------------------- 1 | module.exports = class MockAuthEvent { 2 | constructor(user) { 3 | if (!user) { 4 | throw 'user required to initialize MockAuthEvent'; 5 | } else { 6 | this.data = user; 7 | } 8 | this.eventId = '123456-fake-id-string'; 9 | this.eventType = 'providers/firebase.auth/eventTypes/user.create'; 10 | this.resource = 'projects/fake-project-id'; 11 | this.notSupported = {}; 12 | this.params = {}; 13 | this.timestamp = new Date().toString(); 14 | } 15 | }; -------------------------------------------------------------------------------- /mocks/mock-db-event.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | 3 | module.exports = class MockDBEvent { 4 | constructor({ app, adminApp, data, delta, path, params }) { 5 | this.data = new functions.database.DeltaSnapshot(app, adminApp, data, delta, path); 6 | this.params = params; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /mocks/mock-user.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid": "fake-uid", 3 | "email": "chris@chrisesplin.com", 4 | "password": "123456", 5 | "displayName": "Chris Esplin", 6 | "photoURL": "https: //lh4.googleusercontent.com/-ly98tZeA6F0/AAAAAAAAAAI/AAAAAAAAADk/G-1n2ID9bOw/photo.jpg?sz=64", 7 | "disabled": false, 8 | "emailVerified": false 9 | } -------------------------------------------------------------------------------- /modules/environment.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../functions/onRequest/environment.onRequest'); -------------------------------------------------------------------------------- /modules/graphqlServer.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../functions/onRequest/graphqlServer.onRequest'); -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Environment: require('./environment'), 3 | GraphQLServer: require('./graphqlServer'), 4 | Login: require('./login'), 5 | UpdateUser: require('./updateUser'), 6 | }; 7 | -------------------------------------------------------------------------------- /modules/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('/modules', () => { 2 | it('should import', () => testImport('./')); 3 | 4 | 5 | it('check /index.js too', () => testImport('../')); 6 | 7 | function testImport(path) { 8 | const modules = require(path); 9 | expect( 10 | Object.keys(modules) 11 | .sort() 12 | .join() 13 | ).toEqual('Environment,GraphQLServer,Login,UpdateUser'); 14 | 15 | const environment = new modules.Environment(); 16 | expect(environment.constructor.name).toEqual('Environment'); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /modules/login.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../functions/onWrite/login.onWrite'); -------------------------------------------------------------------------------- /modules/updateUser.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../functions/onCreate/updateUser.onCreate'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quiver-functions", 3 | "version": "1.5.0", 4 | "description": "A monorepo of useful Firebase Cloud Functions", 5 | "scripts": { 6 | "test": "jest --config=jest.config.json --env=node", 7 | "test:watch": "jest --watch --config=jest.config.json --env=node", 8 | "test:server": "nodemon bin/server.js", 9 | "install-functions": "cd functions && yarn install", 10 | "install-public": "cd public && bower install", 11 | "serve": "cd public && polymer serve", 12 | "deploy": "qvf unset && qvf set && firebase deploy", 13 | "deploy:functions": "firebase deploy --only functions", 14 | "deploy:hosting": "firebase deploy --only hosting", 15 | "deploy:config": "qvf unset && qvf set" 16 | }, 17 | "main": "index.js", 18 | "repository": "https://github.com/deltaepsilon/quiver-functions.git", 19 | "author": "Chris Esplin ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "body-parser": "^1.18.1", 23 | "express": "^4.15.4", 24 | "express-graphql": "^0.6.11", 25 | "firebase-admin": "^5.2.1", 26 | "firebase-functions": "^0.6.2", 27 | "graphql-server-express": "^1.1.2", 28 | "graphql-tools": "^1.2.2", 29 | "morgan": "^1.8.2", 30 | "rxjs": "^5.4.3", 31 | "yargs": "^8.0.1" 32 | }, 33 | "peerDependencies": { 34 | "firebase-tools": "^3.11.0", 35 | "graphql": "^0.11.3" 36 | }, 37 | "devDependencies": { 38 | "express-mocks-http": "^0.0.11", 39 | "firebase-tools": "^3.11.0", 40 | "graphql": "^0.11.3", 41 | "jest": "^21.1.0", 42 | "node-mocks-http": "^1.6.2", 43 | "nodemon": "^1.12.1", 44 | "supertest": "^3.0.0" 45 | }, 46 | "bin": { 47 | "qvf": "./bin/quiverFunctions.js" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/README.md: -------------------------------------------------------------------------------- 1 | # \ 2 | 3 | A test client for Quiver Functions 4 | 5 | ## Install the Polymer-CLI 6 | 7 | First, make sure you have the [Polymer CLI](https://www.npmjs.com/package/polymer-cli) installed. Then run `polymer serve` to serve your application locally. 8 | 9 | ## Viewing Your Application 10 | 11 | ``` 12 | $ polymer serve 13 | ``` 14 | 15 | ## Building Your Application 16 | 17 | ``` 18 | $ polymer build 19 | ``` 20 | 21 | This will create a `build/` folder with `bundled/` and `unbundled/` sub-folders 22 | containing a bundled (Vulcanized) and unbundled builds, both run through HTML, 23 | CSS, and JS optimizers. 24 | 25 | You can serve the built versions by giving `polymer serve` a folder to serve 26 | from: 27 | 28 | ``` 29 | $ polymer serve build/bundled 30 | ``` 31 | 32 | ## Running Tests 33 | 34 | ``` 35 | $ polymer test 36 | ``` 37 | 38 | Your application is already set up to be tested via [web-component-tester](https://github.com/Polymer/web-component-tester). Run `polymer test` to run your application's test suite locally. 39 | -------------------------------------------------------------------------------- /public/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quiver-functions", 3 | "description": "A test client for Quiver Functions", 4 | "main": "index.html", 5 | "dependencies": { 6 | "polymer": "Polymer/polymer#^1.4.0", 7 | "iron-ajax": "PolymerElements/iron-ajax#^1.4.3", 8 | "paper-material": "PolymerElements/paper-material#^1.0.6", 9 | "paper-button": "PolymerElements/paper-button#^1.0.14", 10 | "paper-input": "PolymerElements/paper-input#^1.1.23", 11 | "iron-flex-layout": "PolymerElements/iron-flex-layout#^1.3.2", 12 | "paper-toast": "PolymerElements/paper-toast#^1.3.0" 13 | }, 14 | "devDependencies": { 15 | "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", 16 | "iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.0.0", 17 | "web-component-tester": "^4.0.0", 18 | "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | quiver-functions 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quiver-functions", 3 | "short_name": "quiver-functions", 4 | "description": "A test client for Quiver Functions", 5 | "start_url": "/", 6 | "display": "standalone" 7 | } 8 | -------------------------------------------------------------------------------- /public/src/quiver-functions-app/quiver-functions-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 73 | 74 | 211 | -------------------------------------------------------------------------------- /public/test/quiver-functions-app/quiver-functions-app_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | quiver-functions-app test 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /services/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../functions/services'); -------------------------------------------------------------------------------- /services/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('/services', () => { 2 | it('should import', () => { 3 | const services = require('./'); 4 | expect( 5 | Object.keys(services) 6 | .sort() 7 | .join() 8 | ).toEqual('EnvironmentService,EventService,LocalDataService'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /utilities/environment.utility.js: -------------------------------------------------------------------------------- 1 | const firebaseTools = require('firebase-tools'); 2 | 3 | module.exports = class EnvironmentUtility { 4 | constructor(project, token, env) { 5 | if (!project) throw 'project must be first argument in constructor'; 6 | if (!token) throw 'token must be second argument in constructor'; 7 | if (!env) throw 'token must be third argument in constructor'; 8 | 9 | this.firebaseToolsOptions = { 10 | project: project, 11 | token: token 12 | }; 13 | this.env = env; 14 | this.FIREBASE_REGEX = /firebase/gi; 15 | } 16 | 17 | getConfigCommands(env, path = []) { 18 | if (typeof env == 'undefined') { 19 | return [path.join('.')]; 20 | } else if (!env) { 21 | let pathString = path.join('.') + '=false'; 22 | return [pathString]; 23 | } 24 | var keys = Object.keys(env); 25 | var i = keys.length; 26 | var paths = []; 27 | 28 | while (i--) { 29 | let key = keys[i]; 30 | let value = env[key]; 31 | let keyPath = path.concat(key).join('.'); 32 | 33 | if (value && typeof value == 'object' && !Object.keys(value).length) { 34 | paths.push(`${keyPath}=undefined`); 35 | } else if (typeof value == 'boolean') { 36 | const stringified = `${keyPath}=${value ? '"true"' : '"false"'}`; 37 | console.log(`Boolean forced to string: ${stringified}`); 38 | paths.push(stringified); 39 | } else if (this.isStringNumber(value)) { 40 | paths.push(`${keyPath}="${String(value)}"`); 41 | } else if (typeof value == 'string') { 42 | paths.push(`${keyPath}=${String(value)}`); 43 | } else { 44 | paths = paths.concat(this.getConfigCommands(value, path.concat(key))); 45 | } 46 | } 47 | return paths; 48 | } 49 | 50 | isStringNumber(value) { 51 | return String(+value) == value; 52 | } 53 | 54 | getAll() { 55 | return firebaseTools.functions.config.get(undefined, this.firebaseToolsOptions); 56 | } 57 | 58 | unsetAll() { 59 | return this.getAll().then(config => { 60 | const paths = this.getConfigCommands(config).map(command => this.getFirstKey(command)); 61 | const uniquePaths = [...new Set(paths)]; 62 | const filteredPaths = this.filterExcludedKeys(uniquePaths); 63 | const pathsString = filteredPaths.join(','); 64 | if (!pathsString) { 65 | return true; 66 | } else { 67 | console.log('config', JSON.stringify(config)); 68 | // console.log('filteredPaths', filteredPaths); 69 | return Promise.all(filteredPaths.map(path => { 70 | return firebaseTools.functions.config.unset([path], this.firebaseToolsOptions); 71 | })).then(() => pathsString); 72 | } 73 | }); 74 | } 75 | 76 | 77 | 78 | setAll(incomingCommands) { 79 | const commands = incomingCommands || this.getConfigCommands(this.env); 80 | const cleanedCommands = this.getCleanCommands(commands); 81 | console.log('cleanedCommands', cleanedCommands); 82 | return firebaseTools.functions.config.set(cleanedCommands, this.firebaseToolsOptions).catch(err => Promise.reject({cleanedCommands, err})).then(() => cleanedCommands); 83 | } 84 | 85 | getCleanCommands(commands) { 86 | const filteredCommands = this.filterCommands(commands); 87 | return this.cleanCommands(filteredCommands); 88 | } 89 | 90 | cleanCommands(commands) { 91 | return commands.map(command => { 92 | let [path, value] = command.split('='); 93 | 94 | if (path != path.toLowerCase()) { 95 | console.log('All config forced to lowercase.', command); 96 | path = path.toLowerCase(); 97 | } 98 | 99 | if (path.split('.').length < 2) { 100 | console.log('All config have at a 2-part key (e.g. foo.bar).', path); 101 | path = 'config.' + path; 102 | } 103 | 104 | return `${path}=${value}`; 105 | }); 106 | } 107 | 108 | filterCommands(commands) { 109 | const firstKeys = commands.map(command => this.getFirstKey(command)); 110 | const filteredKeys = this.filterExcludedKeys(firstKeys); 111 | const filteredCommands = this.filterExcludedCommands(filteredKeys, commands); 112 | 113 | if (commands.length != filteredCommands.length) { 114 | console.log('All keys containing "firebase" have been removed per Firebase Functions requirements.'); 115 | } 116 | 117 | return filteredCommands; 118 | } 119 | 120 | filterExcludedCommands(filteredKeys, commands) { 121 | return commands.filter(command => filteredKeys.includes(this.getFirstKey(command))); 122 | } 123 | 124 | filterExcludedKeys(paths) { 125 | return paths.filter(path => !path.match(this.FIREBASE_REGEX)); 126 | } 127 | 128 | getFirstKey(path) { 129 | return this.getKeys(path)[0].toLowerCase(); 130 | } 131 | 132 | getKeys(path) { 133 | const {key} = this.getKeyValue(path); 134 | return key.split('.'); 135 | } 136 | 137 | getKeyValue(path) { 138 | var [key, value] = path.split('='); 139 | return {key, value}; 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /utilities/environment.utility.spec.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | describe('EnvironmentUtility', function() { 4 | const config = require('../functions/config.json'); 5 | 6 | const utilities = require('./'); 7 | const environmentUtility = new utilities.EnvironmentUtility(config.config.project, config.config.token, config); 8 | 9 | describe('getConfigCommands', () => { 10 | it('should handle nasty config', () => { 11 | const config = { 12 | firebase: { 13 | apiKey: 'asdfadsfadas' 14 | }, 15 | config: { 16 | token: '1/asdfadsfdasf', 17 | project: 'asdfds', 18 | }, 19 | public: { 20 | config: { 21 | project: 'asdfads', 22 | root: 'production', 23 | firestore_options: { 24 | persistence: true, 25 | }, 26 | firestoreoptions: { 27 | persistence: true, 28 | }, 29 | }, 30 | models: { 31 | queues: { 32 | login: 'queues/login', 33 | }, 34 | }, 35 | localhost: { 36 | config: { 37 | project: 'asfds', 38 | root: 'development', 39 | }, 40 | }, 41 | }, 42 | }; 43 | 44 | const result = environmentUtility.getConfigCommands(config); 45 | expect(result).toEqual([ 'public.localhost.config.root=development', 46 | 'public.localhost.config.project=asfds', 47 | 'public.models.queues.login=queues/login', 48 | 'public.config.firestoreoptions.persistence="true"', 49 | 'public.config.firestore_options.persistence="true"', 50 | 'public.config.root=production', 51 | 'public.config.project=asdfads', 52 | 'config.project=asdfds', 53 | 'config.token=1/asdfadsfdasf', 54 | 'firebase.apiKey=asdfadsfadas' ]); 55 | }); 56 | }); 57 | 58 | // function cleanUp(done) { 59 | // environmentUtility.unsetAll().then(done).catch(error => { 60 | // console.log(error) 61 | // }); 62 | // } 63 | 64 | // beforeEach(cleanUp); 65 | 66 | // it('should start with an empty config', done => { 67 | // testConfig({}).then(done); 68 | // }); 69 | 70 | // it( 71 | // 'should set config', 72 | // done => { 73 | // // afterEach(cleanUp); 74 | // // return done(); 75 | 76 | // environmentUtility 77 | // .setAll() 78 | // .then(() => { 79 | // const expectedConfig = _.omit(config, ['firebase']); 80 | // return testConfig(expectedConfig); 81 | // }) 82 | // .then(done); 83 | // }, 84 | // 60000 85 | // ); 86 | 87 | // function testConfig(expectedConfig) { 88 | // return environmentUtility.getAll().then(config => { 89 | // const isEqual = _.isEqual(config, expectedConfig); 90 | 91 | // if (!isEqual) { 92 | // console.log('testConfig failed'); 93 | // console.log(config); 94 | // console.log(expectedConfig); 95 | // // debugger; 96 | // } 97 | 98 | // expect(isEqual).toEqual(true); 99 | // return true; 100 | // }); 101 | // } 102 | }); 103 | -------------------------------------------------------------------------------- /utilities/generator.utility.js: -------------------------------------------------------------------------------- 1 | module.exports = class GeneratorUtility { 2 | constructor({ admin, ref }) { 3 | this.ref = ref || admin.database().ref(); 4 | } 5 | 6 | generateIfNeeded(x, n) { 7 | return this.ref.once('value').then(snap => { 8 | let promise = Promise.resolve(); 9 | if (snap.numChildren() < x) { 10 | const data = this.generate(x, n); 11 | promise = this.ref.set(data); 12 | } 13 | return promise; 14 | }); 15 | } 16 | 17 | generate(x, n) { 18 | return this.generateKeys(x).reduce((data, key, i, keys) => { 19 | const connections = this.getConnections(keys, i, n); 20 | 21 | return ( 22 | (data[key] = { 23 | i, 24 | connections, 25 | }), 26 | data 27 | ); 28 | }, {}); 29 | } 30 | 31 | getConnections(keys, i, n) { 32 | let connections = keys.slice(i + 1, i + n + 1); 33 | const missingCount = n - connections.length; 34 | 35 | if (missingCount) { 36 | const leftoverKeys = keys.slice(0, missingCount); 37 | connections = connections.concat(leftoverKeys); 38 | } 39 | 40 | return connections.reduce((obj, x) => { 41 | return (obj[x] = true), obj; 42 | }, {}); 43 | } 44 | 45 | generateKeys(i) { 46 | const keys = []; 47 | while (i--) { 48 | keys.push(this.getPushKey()); 49 | } 50 | return keys; 51 | } 52 | 53 | getPushKey() { 54 | return this.ref.push().key; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /utilities/generator.utility.spec.js: -------------------------------------------------------------------------------- 1 | const Utility = require('./generator.utility'); 2 | const admin = require('firebase-admin'); 3 | const config = require('../functions/config.json'); 4 | 5 | admin.initializeApp({ 6 | databaseURL: config.firebase.databaseURL, 7 | credential: admin.credential.cert(config.firebase.serviceAccount) 8 | }); 9 | 10 | describe('GeneratorUtility', () => { 11 | let utility; 12 | beforeEach(() => { 13 | utility = new Utility({admin}); 14 | }); 15 | 16 | describe('generate', () => { 17 | it('creates x records with n relationships', () => { 18 | const data = utility.generate(10, 5); 19 | const keys = Object.keys(data); 20 | const connections = keys.reduce((connections, key) => { 21 | const connectionKeys = Object.keys(data[key].connections); 22 | return connections.concat(connectionKeys) 23 | }, []); 24 | 25 | expect(keys.length).toEqual(10); 26 | expect(connections.length).toEqual(50); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /utilities/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | EnvironmentUtility: require('./environment.utility'), 3 | GeneratorUtility: require('./generator.utility') 4 | }; --------------------------------------------------------------------------------