├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── README.md ├── __tests__ ├── .eslintrc ├── config.js ├── middleware.js └── validation.js ├── package.json ├── src ├── CALL_FIR_API.js ├── errors.js ├── index.js ├── middleware.js ├── types.js ├── utils.js └── validation.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 Chrome versions"] 6 | } 7 | }], 8 | "flow", 9 | "stage-0" 10 | ], 11 | "plugins": [ 12 | "transform-object-assign" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:flowtype/recommended" 5 | ], 6 | parser: "babel-eslint", 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | plugins: [ 12 | "flowtype" 13 | ], 14 | rules: { 15 | "no-implicit-coercion": 0, 16 | "max-len": 0, 17 | "no-case-declarations": 0, 18 | "no-cond-assign": 0 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | ./lib 5 | 6 | [libs] 7 | 8 | [options] 9 | suppress_type=$FlowIssue 10 | suppress_type=$FlowFixMe 11 | suppress_type=$FixMe 12 | 13 | module.file_ext=.js 14 | 15 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-7]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 16 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-7]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 17 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 18 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 19 | 20 | unsafe.enable_getters_and_setters=true 21 | 22 | [version] 23 | ^0.66.0 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | lib 5 | test/config.js 6 | *.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "flow.useNPMPackagedFlow": true, 4 | "javascript.validate.enable": false 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-firebase-middleware [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] 2 | > Redux middleware for firebase, support native web API or react-native-firebase API. 3 | 4 | **NOTE: Only support for Firebase realtime database at this moment, welcome PRs for supporting Firestore** 5 | 6 | ## Why? 7 | 8 | Firebase SDK is hard to achieve strict unidirectional data flow in Redux. If you have a hard time manage your Redux states from Firebase realtime database to your Redux store. This middleware help you seamlessly integrate Firebase with Redux workflow. 9 | 10 | ## Installation 11 | 12 | ```sh 13 | $ npm install --save redux-firebase-middleware 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Store 19 | 20 | Setting up in your redux store 21 | 22 | ##### Web 23 | 24 | ```js 25 | /** firebase web api **/ 26 | const {applyMiddleware, createStore, compose} = require('redux'); 27 | const {firMiddleware} = require('redux-firebase-middleware'); 28 | 29 | const config = { 30 | apiKey: 'xxxxxxxxxxx', 31 | authDomain: 'xxxxxxxxxxx', 32 | databaseURL: 'xxxxxxxxxxx', 33 | projectId: 'xxxxxxxxxxx', 34 | storageBucket: 'xxxxxxxxxxx', 35 | messagingSenderId: 'xxxxxxxxxxx', 36 | }; 37 | 38 | firebase.initializeApp(config); 39 | 40 | const finalCreateStore = compose( 41 | applyMiddleware(thunk), 42 | applyMiddleware(firMiddleware(firebase)) // -----> apply fir middleware in redux store 43 | )(createStore); 44 | 45 | ``` 46 | 47 | ##### React-native 48 | 49 | ```js 50 | /** react-native-firebase native api **/ 51 | import RNFirebase from 'react-native-firebase'; 52 | 53 | const configOpts = { 54 | debug: true, 55 | persistence: true, 56 | }; 57 | 58 | RNFirebase.initializeApp(configOpts); 59 | 60 | const finalCreateStore = compose( 61 | applyMiddleware(thunk), 62 | applyMiddleware(firMiddleware(RNFirebase)) // -----> apply fir middleware in redux store 63 | )(createStore); 64 | 65 | ..... 66 | 67 | ``` 68 | 69 | ### Basic operations (Read, and write data) 70 | 71 | dispatching a firMiddleware action. 72 | 73 | - types **(Array)** : action constants types 74 | - ref **((firebase.database) => firebase.database.Reference)**: Instance of firebase reference 75 | - method: could be one of 76 | * `once_value`: https://firebase.google.com/docs/reference/js/firebase.database.Reference#once 77 | * `set`: https://firebase.google.com/docs/reference/js/firebase.database.Reference#set 78 | * `update`: https://firebase.google.com/docs/reference/js/firebase.database.Reference#update 79 | * `remove`: https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove 80 | 81 | ```js 82 | const {CALL_FIR_API} = require('redux-firebase-middleware'); 83 | 84 | export const GET_MY_REF = [ 85 | 'GET_MY_REF_REQUEST', // -------> first, must be request type 86 | 'GET_MY_REF_SUCCESS', // -------> second, must be success type 87 | 'GET_MY_REF_FAILURE', // -------> third, must be failure type 88 | ]; 89 | 90 | function callAnAction() { 91 | return dispatch({[CALL_FIR_API]: { 92 | types: GET_MY_REF, // -----> normally this should put in constants, see `constants`(next seciton) for more info 93 | ref: (db) => db.ref('test/path1'), // ----> your firebase reference path 94 | method: 'once_value', 95 | }}); 96 | } 97 | ``` 98 | 99 | ***Reducers*** 100 | 101 | ```js 102 | export default function reducer(state: calcState = initialState, action: FSA) { 103 | const {type, payload} = action; 104 | 105 | switch (type) { 106 | case 'GET_MY_REF_REQUEST': 107 | // update request state 108 | 109 | case 'GET_MY_REF_SUCCESS': 110 | // update success state 111 | // you can get data from payload. 112 | 113 | case 'GET_MY_REF_FAILURE': 114 | // update failure state 115 | } 116 | } 117 | ``` 118 | 119 | ### Listener events (Reading and writing lists) 120 | 121 | dispatching a firMiddleware listener actions. 122 | 123 | - types **(Array)** : action constants types 124 | - ref **((firebase.database) => firebase.database.Reference | firebase.database.Query)**: Instance of firebase reference or firebase query 125 | - method: could be one of, please reference to: https://firebase.google.com/docs/reference/js/firebase.database.Reference#on 126 | * `on_value` 127 | * `on_child_added` 128 | * `on_child_changed` 129 | * `on_child_removed` 130 | * `on_child_moved` 131 | 132 | ```js 133 | const {CALL_FIR_API} = require('redux-firebase-middleware'); 134 | 135 | export const GET_MY_REF = [ 136 | 'GET_MY_REF_REQUEST', // -------> first, must be request type 137 | 'GET_MY_REF_SUCCESS', // -------> second, must be success type 138 | 'GET_MY_REF_FAILURE', // -------> third, must be failure type 139 | ]; 140 | 141 | function callAnAction() { 142 | return dispatch({[CALL_FIR_API]: { 143 | types: GET_MY_REF, // -----> normally this should put in constants, see `constants`(next seciton) for more info 144 | ref: (db) => db.ref('test/path1'), // ----> your firebase reference path 145 | method: 'on_value', 146 | }}); 147 | } 148 | ``` 149 | 150 | To remove the listener, you'll get `off` method in actions' reducer. 151 | 152 | ***Reducers*** 153 | 154 | When the state is successful it'll received data as payload, payload's value is slightly different in different methods. 155 | 156 | Payload in methods: 157 | * `on_value`: dataSnapshot 158 | * `on_child_added`: `{childSnapshot, prevChildKey}` 159 | * `on_child_changed`: `{childSnapshot, prevChildKey}` 160 | * `on_child_removed`: oldChildSnapshot 161 | * `on_child_moved`: `{childSnapshot, prevChildKey}` 162 | 163 | ```js 164 | export default function reducer(state: calcState = initialState, action: FSA) { 165 | // or if you're using event listeners you'll get additional `off` method to remove the listening event by calling `off()` 166 | const {type, payload, off} = action 167 | 168 | switch (type) { 169 | case 'GET_MY_REF_REQUEST': 170 | // update request state 171 | 172 | case 'GET_MY_REF_SUCCESS': 173 | // update success state 174 | // you can get data from payload. 175 | 176 | case 'GET_MY_REF_FAILURE': 177 | // update failure state 178 | 179 | case 'REMOVE_LISTENER': 180 | // call off method to unlisten the event 181 | off(); 182 | } 183 | } 184 | ``` 185 | 186 | #### Customized payload 187 | 188 | ```js 189 | export const GET_CALC_CAR_CATEGORY = [ 190 | 'GET_MY_REF_REQUEST', // -------> first, must be request type 191 | { 192 | type: 'GET_MY_REF_SUCCESS', // ------> second, must be success type 193 | payload: (action: FirAPI, state: GetState, data: any) => { 194 | // you can do what ever you want, transforming data or manipulating data .... etc 195 | // get firebase data called `data.val()` 196 | return data.val(); 197 | }, 198 | }, 199 | 'GET_MY_REF_FAILURE', // -------> third, must be failure type 200 | ]; 201 | ``` 202 | 203 | ## Credits 204 | 205 | Inspired by `redux-api-middleware` 206 | 207 | https://github.com/agraboso/redux-api-middleware 208 | 209 | ## License 210 | 211 | MIT © [chilijung](https://github.com/chilijung) 212 | 213 | 214 | [npm-image]: https://badge.fury.io/js/redux-firebase-middleware.svg 215 | [npm-url]: https://npmjs.org/package/redux-firebase-middleware 216 | [travis-image]: https://travis-ci.org/Canner/redux-firebase-middleware.svg?branch=master 217 | [travis-url]: https://travis-ci.org/Canner/redux-firebase-middleware 218 | [daviddm-image]: https://david-dm.org/Canner/redux-firebase-middleware.svg?theme=shields.io 219 | [daviddm-url]: https://david-dm.org/Canner/redux-firebase-middleware 220 | -------------------------------------------------------------------------------- /__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | apiKey: "AIzaSyDxmSl_0K25pDo348ULutKWSY1oE3YpGGk", 3 | authDomain: "redux-firebase-middlewar-f33ca.firebaseapp.com", 4 | databaseURL: "https://redux-firebase-middlewar-f33ca.firebaseio.com", 5 | projectId: "redux-firebase-middlewar-f33ca", 6 | storageBucket: "", 7 | messagingSenderId: "717467757462" 8 | }; 9 | -------------------------------------------------------------------------------- /__tests__/middleware.js: -------------------------------------------------------------------------------- 1 | import firMiddleware from '../src/middleware'; 2 | import CALL_FIR_API from '../src/CALL_FIR_API'; 3 | import firConfig from './config'; 4 | import firebase from 'firebase'; 5 | 6 | jest.setTimeout(10000); 7 | firebase.initializeApp(firConfig) 8 | 9 | describe('firMiddleware must be a Redux middleware', () => { 10 | const doGetState = () => {}; 11 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 12 | const doNext = () => {}; 13 | const actionHandler = nextHandler(doNext); 14 | 15 | test('must take one argument', () => { 16 | expect(firMiddleware.length).toBe(1) 17 | }) 18 | 19 | describe('next handler', () => { 20 | test('must return a function to handle next', () => { 21 | expect(typeof nextHandler).toBe('function') 22 | }) 23 | 24 | test('must take one argument', () => { 25 | expect(nextHandler.length).toBe(1); 26 | }) 27 | 28 | }) 29 | 30 | describe('action handler', () => { 31 | test('must return a function to handle action', () => { 32 | expect(typeof actionHandler).toBe('function'); 33 | }) 34 | 35 | test('must take one argument', () => { 36 | expect(actionHandler.length).toBe(1); 37 | }) 38 | }) 39 | }) 40 | 41 | describe('firMiddleware must pass actions without an [CALL_API] symbol to the next handler', () => { 42 | const doGetState = () => {}; 43 | const anAction = {}; 44 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 45 | test('should pass action to next handler', done => { 46 | const doNext = action => { 47 | expect(anAction).toBe(action) 48 | done(); 49 | }; 50 | const actionHandler = nextHandler(doNext); 51 | 52 | actionHandler(anAction); 53 | }) 54 | 55 | test("mustn't return a promise on actions", () => { 56 | const doNext = action => action; 57 | const actionHandler = nextHandler(doNext); 58 | 59 | const noProm = actionHandler(anAction); 60 | expect(typeof noProm.then).toBe("undefined"); 61 | }); 62 | }) 63 | 64 | describe('firMiddleware must pass valid request `types`', () => { 65 | const doGetState = () => {}; 66 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 67 | 68 | test('must return a promise on actions with a [CALL_FIR] property', () => { 69 | const doNext = action => action; 70 | const actionHandler = nextHandler(doNext); 71 | 72 | const yesProm = actionHandler({[CALL_FIR_API]: {}}); 73 | expect(typeof yesProm.then).toBe('function'); 74 | }); 75 | 76 | test('Must dispatch an error request for an invalid FirAction with a string request type', done => { 77 | const anAction = { 78 | [CALL_FIR_API]: { 79 | types: ['REQUEST'] 80 | } 81 | }; 82 | const doGetState = () => {}; 83 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 84 | const doNext = action => { 85 | expect(action.type).toBe('REQUEST'); 86 | expect(action.payload.name).toBe('InvalidFirAction'); 87 | expect(action.error).toBe(true); 88 | done(); 89 | }; 90 | const actionHandler = nextHandler(doNext); 91 | 92 | actionHandler(anAction); 93 | }); 94 | 95 | test('Must dispatch an error request for an invalid FirAction with a descriptor request type', done => { 96 | const anAction = { 97 | [CALL_FIR_API]: { 98 | types: [ 99 | { 100 | type: 'REQUEST' 101 | } 102 | ] 103 | } 104 | }; 105 | const doGetState = () => {}; 106 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 107 | const doNext = action => { 108 | expect(action.type).toBe('REQUEST'); 109 | expect(action.payload.name).toBe('InvalidFirAction'); 110 | expect(action.error).toBe(true); 111 | done(); 112 | }; 113 | const actionHandler = nextHandler(doNext); 114 | 115 | actionHandler(anAction); 116 | }); 117 | 118 | test('Must do nothing for an invalid request without a request type', done => { 119 | const anAction = { 120 | [CALL_FIR_API]: {} 121 | }; 122 | const doGetState = () => {}; 123 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 124 | const doNext = () => { 125 | throw Error('next handler called'); 126 | }; 127 | const actionHandler = nextHandler(doNext); 128 | 129 | actionHandler(anAction); 130 | setTimeout(() => { 131 | done(); 132 | }, 200); 133 | }); 134 | 135 | test('method must defined', done => { 136 | const anAction = { 137 | [CALL_FIR_API]: { 138 | ref: (db) => db.ref() 139 | } 140 | }; 141 | const doGetState = () => {}; 142 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 143 | const doNext = () => { 144 | throw Error('next handler called'); 145 | }; 146 | const actionHandler = nextHandler(doNext); 147 | 148 | actionHandler(anAction); 149 | setTimeout(() => { 150 | done(); 151 | }, 200); 152 | }); 153 | }) 154 | 155 | describe('firMiddleware must dispatch an error request when FirAction have wrong ref type', () => { 156 | test('ref must defined', done => { 157 | const anAction = { 158 | [CALL_FIR_API]: { 159 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 160 | method: "once_value" 161 | } 162 | }; 163 | const doGetState = () => {}; 164 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 165 | const doNext = (action) => { 166 | expect(action.type).toBe('REQUEST'); 167 | expect(action.payload.name).toBe('InvalidFirAction'); 168 | expect(action.payload.validationErrors[0]).toBe('[CALL_API].ref property must have an ref property'); 169 | expect(action.error).toBe(true); 170 | done(); 171 | }; 172 | const actionHandler = nextHandler(doNext); 173 | 174 | actionHandler(anAction); 175 | }); 176 | 177 | test('ref type is a string which is invalid', done => { 178 | const anAction = { 179 | [CALL_FIR_API]: { 180 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 181 | ref: "/test", 182 | method: "once_value" 183 | } 184 | }; 185 | const doGetState = () => {}; 186 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 187 | const doNext = (action) => { 188 | expect(action.type).toBe('REQUEST'); 189 | expect(action.payload.name).toBe('InvalidFirAction'); 190 | expect(action.payload.validationErrors[0]).toBe('[CALL_API].ref property must be a function'); 191 | expect(action.error).toBe(true); 192 | done(); 193 | }; 194 | const actionHandler = nextHandler(doNext); 195 | 196 | actionHandler(anAction); 197 | }); 198 | 199 | test('ref type return type is not firebase.database.Reference', done => { 200 | // https://firebase.google.com/docs/reference/js/firebase.database.Reference 201 | const anAction = { 202 | [CALL_FIR_API]: { 203 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 204 | ref: () => 'test', 205 | method: "once_value" 206 | } 207 | }; 208 | const doGetState = () => {}; 209 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 210 | const doNext = (action) => { 211 | expect(action.type).toBe('REQUEST'); 212 | expect(action.payload.name).toBe('InvalidFirAction'); 213 | expect(action.payload.validationErrors[0]).toBe('[CALL_API].ref property must be an instance of firebase.database.Reference or firebase.database.Query'); 214 | expect(action.error).toBe(true); 215 | done(); 216 | }; 217 | const actionHandler = nextHandler(doNext); 218 | 219 | actionHandler(anAction); 220 | }); 221 | }) 222 | 223 | describe('firMiddleware must dispatch an error request when FirAction have wrong `method` type', () => { 224 | test('method must defined', done => { 225 | const anAction = { 226 | [CALL_FIR_API]: { 227 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 228 | ref: (db) => db.ref() 229 | } 230 | }; 231 | const doGetState = () => {}; 232 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 233 | const doNext = (action) => { 234 | expect(action.type).toBe('REQUEST'); 235 | expect(action.payload.name).toBe('InvalidFirAction'); 236 | expect(action.payload.validationErrors[0]).toBe('[CALL_API].method must have a method property'); 237 | expect(action.error).toBe(true); 238 | done(); 239 | }; 240 | const actionHandler = nextHandler(doNext); 241 | 242 | actionHandler(anAction); 243 | }); 244 | 245 | test('method type must be one of the method types', done => { 246 | const anAction = { 247 | [CALL_FIR_API]: { 248 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 249 | ref: (db) => db.ref(), 250 | method: "test" 251 | } 252 | }; 253 | const doGetState = () => {}; 254 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 255 | const doNext = (action) => { 256 | expect(action.type).toBe('REQUEST'); 257 | expect(action.payload.name).toBe('InvalidFirAction'); 258 | expect(action.payload.validationErrors[0]).toMatch('Invalid [CALL_API].method: test, must be one of'); 259 | expect(action.error).toBe(true); 260 | done(); 261 | }; 262 | const actionHandler = nextHandler(doNext); 263 | 264 | actionHandler(anAction); 265 | }); 266 | 267 | test('method must be a string', done => { 268 | const anAction = { 269 | [CALL_FIR_API]: { 270 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 271 | ref: (db) => db.ref(), 272 | method: false 273 | } 274 | }; 275 | const doGetState = () => {}; 276 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 277 | const doNext = (action) => { 278 | expect(action.type).toBe('REQUEST'); 279 | expect(action.payload.name).toBe('InvalidFirAction'); 280 | expect(action.payload.validationErrors[0]).toBe('[CALL_API].method property must be a string'); 281 | expect(action.error).toBe(true); 282 | done(); 283 | }; 284 | const actionHandler = nextHandler(doNext); 285 | 286 | actionHandler(anAction); 287 | }); 288 | }) 289 | 290 | 291 | describe('firMiddleware get value from firebase reference once', () => { 292 | test('get the `/test` value once', done => { 293 | firebase.database().ref('test').set({hello: 'world'}) 294 | const anAction = { 295 | [CALL_FIR_API]: { 296 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 297 | ref: (db) => db.ref('/test'), 298 | method: 'once_value' 299 | } 300 | }; 301 | const doGetState = () => {}; 302 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 303 | const doNext = (action) => { 304 | if (action.type === 'SUCCESS') { 305 | expect(action.payload.val()).toEqual({hello: 'world'}); 306 | done(); 307 | } 308 | }; 309 | const actionHandler = nextHandler(doNext); 310 | 311 | actionHandler(anAction); 312 | }); 313 | }) 314 | 315 | describe('firMiddleware `set` value', () => { 316 | test('set the `/test` value with {foo: "bar"}', done => { 317 | firebase.database().ref('test').set({hello: 'world'}) 318 | const anAction = { 319 | [CALL_FIR_API]: { 320 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 321 | ref: (db) => db.ref('/test'), 322 | method: 'set', 323 | content: {foo: "bar"} 324 | } 325 | }; 326 | const doGetState = () => {}; 327 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 328 | const doNext = (action) => { 329 | if (action.type === 'SUCCESS') { 330 | firebase.database().ref('test').once('value') 331 | .then(dataSnapshot => { 332 | expect(dataSnapshot.val()).toEqual({foo: 'bar'}); 333 | done(); 334 | }) 335 | } 336 | }; 337 | const actionHandler = nextHandler(doNext); 338 | 339 | actionHandler(anAction); 340 | }); 341 | }) 342 | 343 | describe('firMiddleware `update` value', () => { 344 | test('update the `/test` value with {foo: "bar"}', done => { 345 | firebase.database().ref('test').set({hello: 'world'}) 346 | const anAction = { 347 | [CALL_FIR_API]: { 348 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 349 | ref: (db) => db.ref('/test'), 350 | method: 'update', 351 | content: {foo: "bar"} 352 | } 353 | }; 354 | const doGetState = () => {}; 355 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 356 | const doNext = (action) => { 357 | if (action.type === 'SUCCESS') { 358 | firebase.database().ref('test').once('value') 359 | .then(dataSnapshot => { 360 | expect(dataSnapshot.val()).toEqual({ 361 | foo: 'bar', 362 | hello: 'world' 363 | }); 364 | done(); 365 | }) 366 | } 367 | }; 368 | const actionHandler = nextHandler(doNext); 369 | 370 | actionHandler(anAction); 371 | }); 372 | }) 373 | 374 | describe('firMiddleware `remove` value', () => { 375 | test('remove the `/test` value with {foo: "bar"}', done => { 376 | firebase.database().ref('test').set({hello: 'world'}) 377 | const anAction = { 378 | [CALL_FIR_API]: { 379 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 380 | ref: (db) => db.ref('/test'), 381 | method: 'remove' 382 | } 383 | }; 384 | const doGetState = () => {}; 385 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 386 | const doNext = (action) => { 387 | if (action.type === 'SUCCESS') { 388 | firebase.database().ref('test').once('value') 389 | .then(dataSnapshot => { 390 | expect(dataSnapshot.val()).toEqual(null); 391 | done(); 392 | }) 393 | } 394 | }; 395 | const actionHandler = nextHandler(doNext); 396 | 397 | actionHandler(anAction); 398 | }); 399 | }) 400 | 401 | describe('firMiddleware `on_value` listen new values', () => { 402 | test('listen ref `/test` for update value with {foo: "bar"}', done => { 403 | firebase.database().ref('test').set({hello: 'world'}) 404 | let updateValueTest = false 405 | const anAction = { 406 | [CALL_FIR_API]: { 407 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 408 | ref: (db) => db.ref('/test'), 409 | method: 'on_value' 410 | } 411 | }; 412 | const doGetState = () => {}; 413 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 414 | const doNext = (action) => { 415 | if (action.type === 'SUCCESS' && !updateValueTest) { 416 | updateValueTest = true 417 | firebase.database().ref('test').update({foo: 'bar'}) 418 | } else if (action.type === 'SUCCESS' && updateValueTest) { 419 | expect(action.payload.val()).toEqual({ foo: 'bar', hello: 'world' }); 420 | action.off(); 421 | done(); 422 | } 423 | }; 424 | const actionHandler = nextHandler(doNext); 425 | 426 | actionHandler(anAction); 427 | }); 428 | 429 | test('`/test` listen value and unsubscribe', done => { 430 | firebase.database().ref('test').set({hello: 'world'}) 431 | const anAction = { 432 | [CALL_FIR_API]: { 433 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 434 | ref: (db) => db.ref('/test'), 435 | method: 'on_value' 436 | } 437 | }; 438 | const doGetState = () => {}; 439 | const testCall = jest.fn(); 440 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 441 | const doNext = (action) => { 442 | if (action.type === 'SUCCESS') { 443 | testCall(); 444 | action.off(); 445 | firebase.database().ref('test').update({foo: 'bar'}) 446 | .then(() => { 447 | expect(testCall).toHaveBeenCalledTimes(1); 448 | done(); 449 | }) 450 | } 451 | }; 452 | const actionHandler = nextHandler(doNext); 453 | 454 | actionHandler(anAction); 455 | }); 456 | 457 | test('`/test` listen value with query ', done => { 458 | firebase.database().ref('test').set({ 459 | howard: {score: 140}, 460 | bob: {score: 99}, 461 | william: {score: 120} 462 | }) 463 | const anAction = { 464 | [CALL_FIR_API]: { 465 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 466 | ref: (db) => db.ref('/test').orderByChild("score").equalTo(120), 467 | method: 'on_value' 468 | } 469 | }; 470 | const doGetState = () => {}; 471 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 472 | const doNext = (action) => { 473 | if (action.type === 'SUCCESS') { 474 | expect(action.payload.val()).toEqual({"william": {"score": 120}}); 475 | action.off(); 476 | done(); 477 | } 478 | }; 479 | const actionHandler = nextHandler(doNext); 480 | 481 | actionHandler(anAction); 482 | }); 483 | }) 484 | 485 | describe('firMiddleware `on_child_added` listen new values', () => { 486 | test('listen ref `/test` for update value with {foo: "bar"}', done => { 487 | firebase.database().ref('test').set({hello: 'world'}) 488 | let updateValueTest = false 489 | const anAction = { 490 | [CALL_FIR_API]: { 491 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 492 | ref: (db) => db.ref('/test'), 493 | method: 'on_child_added' 494 | } 495 | }; 496 | const doGetState = () => {}; 497 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 498 | const doNext = (action) => { 499 | if (action.type === 'SUCCESS' && !updateValueTest) { 500 | updateValueTest = true 501 | firebase.database().ref('test').update({hello2: 'bar'}) 502 | } else if (action.type === 'SUCCESS' && updateValueTest) { 503 | expect(action.payload.childSnapshot.val()).toEqual("bar"); 504 | expect(action.payload.prevChildKey).toEqual("hello"); 505 | action.off(); 506 | done(); 507 | } 508 | }; 509 | const actionHandler = nextHandler(doNext); 510 | 511 | actionHandler(anAction); 512 | }); 513 | 514 | test('`/test` listen value and unsubscribe', done => { 515 | firebase.database().ref('test').set({hello: 'world'}) 516 | const anAction = { 517 | [CALL_FIR_API]: { 518 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 519 | ref: (db) => db.ref('/test'), 520 | method: 'on_child_added' 521 | } 522 | }; 523 | const doGetState = () => {}; 524 | const testCall = jest.fn(); 525 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 526 | const doNext = (action) => { 527 | if (action.type === 'SUCCESS') { 528 | testCall(); 529 | action.off(); 530 | firebase.database().ref('test').update({foo: 'bar'}) 531 | .then(() => firebase.database().ref('test').once('value')) 532 | .then((snapShot) => { 533 | expect(snapShot.val()).toEqual({foo: 'bar', hello: 'world'}) 534 | expect(testCall).toHaveBeenCalledTimes(1); 535 | done(); 536 | }) 537 | } 538 | }; 539 | const actionHandler = nextHandler(doNext); 540 | 541 | actionHandler(anAction); 542 | }); 543 | }) 544 | 545 | describe('firMiddleware `on_child_added` listen new values', () => { 546 | test('listen ref `/test` for update value with {foo: "bar"}', done => { 547 | firebase.database().ref('test').set({hello: 'world'}) 548 | let updateValueTest = false 549 | const anAction = { 550 | [CALL_FIR_API]: { 551 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 552 | ref: (db) => db.ref('/test'), 553 | method: 'on_child_added' 554 | } 555 | }; 556 | const doGetState = () => {}; 557 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 558 | const doNext = (action) => { 559 | if (action.type === 'SUCCESS' && !updateValueTest) { 560 | updateValueTest = true 561 | firebase.database().ref('test').update({hello2: 'bar'}) 562 | } else if (action.type === 'SUCCESS' && updateValueTest) { 563 | expect(action.payload.childSnapshot.val()).toEqual("bar"); 564 | expect(action.payload.prevChildKey).toEqual("hello"); 565 | action.off(); 566 | done(); 567 | } 568 | }; 569 | const actionHandler = nextHandler(doNext); 570 | 571 | actionHandler(anAction); 572 | }); 573 | 574 | test('`/test` listen value and unsubscribe', done => { 575 | firebase.database().ref('test').set({hello: 'world'}) 576 | const anAction = { 577 | [CALL_FIR_API]: { 578 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 579 | ref: (db) => db.ref('/test'), 580 | method: 'on_child_added' 581 | } 582 | }; 583 | const doGetState = () => {}; 584 | const testCall = jest.fn(); 585 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 586 | const doNext = (action) => { 587 | if (action.type === 'SUCCESS') { 588 | testCall(); 589 | action.off(); 590 | firebase.database().ref('test').update({foo: 'bar'}) 591 | .then(() => firebase.database().ref('test').once('value')) 592 | .then((snapShot) => { 593 | expect(snapShot.val()).toEqual({foo: 'bar', hello: 'world'}) 594 | expect(testCall).toHaveBeenCalledTimes(1); 595 | done(); 596 | }) 597 | } 598 | }; 599 | const actionHandler = nextHandler(doNext); 600 | 601 | actionHandler(anAction); 602 | }); 603 | }) 604 | 605 | describe('firMiddleware `on_child_changed` listen new values', () => { 606 | test('listen ref `/test` for update value with {foo: "bar"}', done => { 607 | firebase.database().ref('test').set({hello: 'world'}) 608 | const anAction = { 609 | [CALL_FIR_API]: { 610 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 611 | ref: (db) => db.ref('/test'), 612 | method: 'on_child_changed' 613 | } 614 | }; 615 | const doGetState = () => {}; 616 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 617 | const doNext = (action) => { 618 | if (action.type === 'SUCCESS') { 619 | expect(action.payload.childSnapshot.val()).toEqual({foo: "bar"}); 620 | expect(action.payload.prevChildKey).toEqual(null); 621 | action.off(); 622 | done(); 623 | } 624 | }; 625 | const actionHandler = nextHandler(doNext); 626 | 627 | actionHandler(anAction) 628 | .then(() => { 629 | firebase.database().ref('test/hello').update({foo: 'bar'}) 630 | }); 631 | }); 632 | 633 | test('`/test` listen value and unsubscribe', done => { 634 | firebase.database().ref('test').set({hello: 'world'}) 635 | const anAction = { 636 | [CALL_FIR_API]: { 637 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 638 | ref: (db) => db.ref('/test'), 639 | method: 'on_child_changed' 640 | } 641 | }; 642 | const doGetState = () => {}; 643 | const testCall = jest.fn(); 644 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 645 | const doNext = (action) => { 646 | if (action.type === 'SUCCESS') { 647 | testCall(); 648 | action.off(); 649 | firebase.database().ref('test/hello').update({foo: 'bar2'}) 650 | .then(() => { 651 | expect(testCall).toHaveBeenCalledTimes(1); 652 | done(); 653 | }) 654 | } 655 | }; 656 | const actionHandler = nextHandler(doNext); 657 | 658 | actionHandler(anAction) 659 | .then(() => { 660 | firebase.database().ref('test/hello').update({foo: 'bar'}) 661 | }); 662 | }); 663 | }) 664 | 665 | describe('firMiddleware `on_child_removed` listen new values', () => { 666 | test('listen ref `/test` for removed value with {foo: "bar"}', done => { 667 | firebase.database().ref('test').set({ 668 | hello: 'world', 669 | foo: 'bar' 670 | }) 671 | const anAction = { 672 | [CALL_FIR_API]: { 673 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 674 | ref: (db) => db.ref('/test'), 675 | method: 'on_child_removed' 676 | } 677 | }; 678 | const doGetState = () => {}; 679 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 680 | const doNext = (action) => { 681 | if (action.type === 'SUCCESS') { 682 | expect(action.payload.val()).toEqual("bar"); 683 | action.off(); 684 | done(); 685 | } 686 | }; 687 | const actionHandler = nextHandler(doNext); 688 | 689 | actionHandler(anAction) 690 | .then(() => { 691 | firebase.database().ref('test/foo').remove() 692 | }); 693 | }); 694 | 695 | test('`/test` listen child_removed and unsubscribe', done => { 696 | firebase.database().ref('test').set({ 697 | hello: 'world', 698 | foo: "bar" 699 | }) 700 | const anAction = { 701 | [CALL_FIR_API]: { 702 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 703 | ref: (db) => db.ref('/test'), 704 | method: 'on_child_removed' 705 | } 706 | }; 707 | const doGetState = () => {}; 708 | const testCall = jest.fn(); 709 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 710 | const doNext = (action) => { 711 | if (action.type === 'SUCCESS') { 712 | testCall(); 713 | action.off(); 714 | firebase.database().ref('test/foo').remove() 715 | .then(() => { 716 | expect(testCall).toHaveBeenCalledTimes(1); 717 | done(); 718 | }) 719 | } 720 | }; 721 | const actionHandler = nextHandler(doNext); 722 | 723 | actionHandler(anAction) 724 | .then(() => { 725 | firebase.database().ref('test/hello').remove() 726 | }); 727 | }); 728 | }) 729 | 730 | describe('firMiddleware `on_child_moved` listen new values', () => { 731 | test('listen ref `/test` for moved child sort by height', done => { 732 | firebase.database().ref('test').set({ 733 | howard: {height: 100}, 734 | bob: {height: 200} 735 | }) 736 | const anAction = { 737 | [CALL_FIR_API]: { 738 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 739 | ref: (db) => db.ref('/test').orderByChild("height"), 740 | method: 'on_child_moved' 741 | } 742 | }; 743 | const doGetState = () => {}; 744 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 745 | const doNext = (action) => { 746 | if (action.type === 'SUCCESS') { 747 | expect(action.payload.childSnapshot.val()).toEqual({height: 50}); 748 | action.off(); 749 | done(); 750 | } 751 | }; 752 | const actionHandler = nextHandler(doNext); 753 | 754 | actionHandler(anAction) 755 | .then(() => { 756 | firebase.database().ref('/test/bob/height').set(50) 757 | }); 758 | }); 759 | 760 | test('`/test` listen child_moved and unsubscribe', done => { 761 | firebase.database().ref('test').set({ 762 | howard: {height: 100}, 763 | bob: {height: 200} 764 | }) 765 | const anAction = { 766 | [CALL_FIR_API]: { 767 | types: ['REQUEST', 'SUCCESS', 'FAILURE'], 768 | ref: (db) => db.ref('/test').orderByChild("height"), 769 | method: 'on_child_moved' 770 | } 771 | }; 772 | const doGetState = () => {}; 773 | const testCall = jest.fn(); 774 | const nextHandler = firMiddleware(firebase)({ getState: doGetState }); 775 | const doNext = (action) => { 776 | if (action.type === 'SUCCESS') { 777 | testCall(); 778 | action.off(); 779 | firebase.database().ref('/test/bob/height').set(200) 780 | .then(() => { 781 | expect(testCall).toHaveBeenCalledTimes(1); 782 | done(); 783 | }) 784 | } 785 | }; 786 | const actionHandler = nextHandler(doNext); 787 | 788 | actionHandler(anAction) 789 | .then(() => { 790 | firebase.database().ref('/test/bob/height').set(50) 791 | }); 792 | }); 793 | }) 794 | -------------------------------------------------------------------------------- /__tests__/validation.js: -------------------------------------------------------------------------------- 1 | import Symbol from 'es6-symbol'; 2 | import { 3 | isFirAction, 4 | isValidTypeDescriptor, 5 | validateFirAction, 6 | isValidateFirAction, 7 | } from '../src/validation'; 8 | 9 | import CALL_FIR_API from '../src/CALL_FIR_API'; 10 | 11 | describe('validate FIR actions', () => { 12 | it('should fail passing none object value', () => { 13 | expect(isFirAction('')).toBeFalsy(); 14 | }); 15 | 16 | it('must have [CALL_FIR_API]', () => { 17 | expect(isFirAction({})).toBeFalsy(); 18 | }); 19 | 20 | it('must return true, if have [CALL_FIR_API]', () => { 21 | expect(isFirAction({[CALL_FIR_API]: {}})).toBeDefined(); 22 | }) 23 | }); 24 | 25 | describe('validate type descriptor', () => { 26 | it('should fail passing none object value', () => { 27 | expect(isValidTypeDescriptor('')).toBeFalsy(); 28 | }); 29 | 30 | it('should fail passing invalid key', () => { 31 | expect(isValidTypeDescriptor({type: 'test', inValidKey: ''})).toBeFalsy(); 32 | }); 33 | 34 | it('should fail passing without type key', () => { 35 | expect(isValidTypeDescriptor({})).toBeFalsy(); 36 | }); 37 | 38 | it('type property must be a string or symbol', () => { 39 | expect(isValidTypeDescriptor({type: {}})).toBeFalsy(); 40 | }); 41 | 42 | it('type may be a string', () => { 43 | expect(isValidTypeDescriptor({type: 'test'})).toBeDefined(); 44 | }); 45 | 46 | it('type may be a symbol', () => { 47 | expect(isValidTypeDescriptor({type: Symbol()})).toBeDefined(); 48 | }); 49 | }) 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-firebase-middleware", 3 | "version": "1.2.0", 4 | "description": "redux middleware for firebase", 5 | "homepage": "", 6 | "author": { 7 | "name": "howardchi", 8 | "email": "chilijung@gmail.com", 9 | "url": "" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "main": "lib/index.js", 15 | "keywords": [ 16 | "redux", 17 | "firebase", 18 | "middleware" 19 | ], 20 | "jest": { 21 | "testPathIgnorePatterns": [ 22 | "/__tests__/config.js" 23 | ] 24 | }, 25 | "devDependencies": { 26 | "babel-cli": "^6.26.0", 27 | "babel-core": "^6.26.0", 28 | "babel-eslint": "^8.2.2", 29 | "babel-jest": "^22.4.1", 30 | "babel-plugin-import": "^1.1.0", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-plugin-transform-object-assign": "^6.22.0", 33 | "babel-preset-env": "^1.6.1", 34 | "babel-preset-flow": "^6.23.0", 35 | "babel-preset-stage-0": "^6.24.1", 36 | "babel-runtime": "^6.11.6", 37 | "del": "^2.0.2", 38 | "eslint": "^4.18.2", 39 | "eslint-config-prettier": "^2.9.0", 40 | "eslint-plugin-flowtype": "^2.46.1", 41 | "eslint-plugin-prettier": "^2.6.0", 42 | "firebase": "^4.13.0", 43 | "flow-bin": "^0.68.0", 44 | "flow-copy-source": "^1.3.0", 45 | "jest": "22.4.2", 46 | "prettier": "^1.12.1" 47 | }, 48 | "repository": "Canner/redux-firebase-middleware", 49 | "scripts": { 50 | "lint": "prettier --write ./src/**/*.js && eslint src test", 51 | "clean": "rimraf lib dist", 52 | "build:flow": "flow-copy-source -v -i '**/test/**' src lib", 53 | "build:commonjs": "babel src --out-dir lib", 54 | "build": "npm run build:commonjs && npm run build:flow", 55 | "prepublish": "npm run clean && npm run check:src && npm run build", 56 | "check:src": "npm run lint", 57 | "test": "jest", 58 | "test:watch": "jest --watch" 59 | }, 60 | "license": "MIT", 61 | "dependencies": { 62 | "es6-symbol": "^3.1.1", 63 | "lodash.isplainobject": "^4.0.6" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CALL_FIR_API.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import Symbol from "es6-symbol"; 6 | const CALL_API = Symbol("Call Firebase API"); 7 | 8 | export default CALL_API; 9 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error class for an FirAction that does not conform to the FirAction definition 3 | * 4 | * @class InvalidFirAction 5 | * @access public 6 | * @param {array} validationErrors - an array of validation errors 7 | */ 8 | export class InvalidFirAction extends Error { 9 | constructor(validationErrors) { 10 | super(); 11 | this.name = "InvalidFirAction"; 12 | this.message = "Invalid FirAction"; 13 | this.validationErrors = validationErrors; 14 | } 15 | } 16 | 17 | /** 18 | * Error class for a custom `payload` or `meta` function throwing 19 | * 20 | * @class InternalError 21 | * @access public 22 | * @param {string} message - the error message 23 | */ 24 | export class InternalError extends Error { 25 | constructor(message) { 26 | super(); 27 | this.name = "InternalError"; 28 | this.message = message; 29 | } 30 | } 31 | 32 | /** 33 | * Error class for an error raised trying to make an API call 34 | * 35 | * @class RequestError 36 | * @access public 37 | * @param {string} message - the error message 38 | */ 39 | export class RequestError extends Error { 40 | constructor(message) { 41 | super(); 42 | this.name = "RequestError"; 43 | this.message = message; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import CALL_FIR_API from "./CALL_FIR_API"; 6 | import { isFirAction } from "./validation"; 7 | import { InternalError, RequestError } from "./errors"; 8 | import firMiddleware from "./middleware"; 9 | 10 | export { 11 | CALL_FIR_API, 12 | isFirAction, 13 | InternalError, 14 | RequestError, 15 | firMiddleware 16 | }; 17 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import type { FirAPI } from "./types"; 6 | import CALL_FIR_API from "./CALL_FIR_API"; 7 | import { isFirAction, validateFirAction } from "./validation"; 8 | import { RequestError, InvalidFirAction } from "./errors"; 9 | import { actionWith, normalizeTypeDescriptors } from "./utils"; 10 | 11 | export default (firebase: any) => { 12 | return ({ getState }: any) => (next: Function) => (action: FirAPI) => { 13 | const db = firebase.database(); 14 | if (!isFirAction(action)) { 15 | // if it is not a FirAction go to the next middleware 16 | return next(action); 17 | } 18 | 19 | return (async () => { 20 | // try to dispatch an error request for invalid actions. 21 | const validationErrors = validateFirAction(action); 22 | if (validationErrors.length > 0) { 23 | const callAPI = action[CALL_FIR_API]; 24 | if (callAPI.types && Array.isArray(callAPI.types)) { 25 | let requestType = callAPI.types[0]; 26 | if (requestType && requestType.type) { 27 | requestType = requestType.type; 28 | } 29 | next({ 30 | type: requestType, 31 | payload: new InvalidFirAction(validationErrors), 32 | error: true 33 | }); 34 | } 35 | return; 36 | } 37 | 38 | const callFIR = action[CALL_FIR_API]; 39 | const { types, ref, method, content = {} } = callFIR; 40 | const [requestType, successType, failureType] = normalizeTypeDescriptors( 41 | types 42 | ); 43 | 44 | next(await actionWith(requestType, [action, getState()])); 45 | 46 | // listener method 47 | function listenerMethods(type) { 48 | const cb = async (dataSnapshot, prevChildKey) => { 49 | const newSuccessType = Object.assign({}, successType); 50 | if ( 51 | type === "child_added" || 52 | type === "child_changed" || 53 | type === "child_moved" 54 | ) { 55 | dataSnapshot = { 56 | childSnapshot: dataSnapshot, 57 | prevChildKey 58 | }; 59 | } 60 | 61 | next( 62 | await actionWith( 63 | newSuccessType, 64 | [action, getState(), dataSnapshot], 65 | () => ref(db).off(type, cb) 66 | ) 67 | ); 68 | }; 69 | 70 | ref(db).on(type, cb); 71 | } 72 | 73 | let dataSnapshot; 74 | try { 75 | switch (method) { 76 | // trigger firebase methods, get, set ...etc operations. 77 | case "once_value": 78 | dataSnapshot = await ref(db).once("value"); 79 | return next( 80 | await actionWith(successType, [action, getState(), dataSnapshot]) 81 | ); 82 | case "set": 83 | dataSnapshot = await ref(db).set(content); 84 | return next( 85 | await actionWith(successType, [action, getState(), dataSnapshot]) 86 | ); 87 | case "update": 88 | dataSnapshot = await ref(db).update(content); 89 | return next( 90 | await actionWith(successType, [action, getState(), dataSnapshot]) 91 | ); 92 | case "remove": 93 | dataSnapshot = await ref(db).remove(); 94 | return next( 95 | await actionWith(successType, [action, getState(), dataSnapshot]) 96 | ); 97 | case "on_value": 98 | listenerMethods("value"); 99 | return; 100 | case "on_child_added": 101 | listenerMethods("child_added"); 102 | return; 103 | case "on_child_changed": 104 | listenerMethods("child_changed"); 105 | return; 106 | case "on_child_removed": 107 | listenerMethods("child_removed"); 108 | return; 109 | case "on_child_moved": 110 | listenerMethods("child_moved"); 111 | return; 112 | default: 113 | throw new Error("Invalid method: ", method); 114 | } 115 | } catch (e) { 116 | // The request was malformed, or there was a network error 117 | return next( 118 | await actionWith( 119 | { 120 | ...failureType, 121 | payload: new RequestError(e.message), 122 | error: true 123 | }, 124 | [action, getState(), dataSnapshot] 125 | ) 126 | ); 127 | } 128 | })(); 129 | }; 130 | }; 131 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | export type FSA = { 6 | type: string, 7 | payload?: ?any, 8 | error?: ?boolean 9 | }; 10 | 11 | export type TypeDescriptor = { 12 | type: string, 13 | payload?: ?any 14 | }; 15 | 16 | export type FirAPI = { 17 | [symbolApi: any]: { 18 | types: [string, string, string], 19 | ref: string, 20 | method?: string 21 | } 22 | }; 23 | 24 | export type GetState = () => Object; 25 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import { InternalError } from "./errors"; 6 | import type { GetState, FSA } from "./types"; 7 | 8 | export function normalizeTypeDescriptors(reqTypes: any) { 9 | let [requestType, successType, failureType] = reqTypes; 10 | 11 | // $FlowFixMe symbol 12 | if (typeof requestType === "string" || typeof requestType === "symbol") { 13 | requestType = { type: requestType }; 14 | } 15 | 16 | // $FlowFixMe symbol 17 | if (typeof successType === "string" || typeof successType === "symbol") { 18 | successType = { type: successType }; 19 | } 20 | 21 | successType = { 22 | payload: (action: FSA, state: GetState, res: any) => res, 23 | ...successType 24 | }; 25 | 26 | // $FlowFixMe symbol 27 | if (typeof failureType === "string" || typeof failureType === "symbol") { 28 | failureType = { type: failureType }; 29 | } 30 | 31 | failureType = { 32 | payload: (action: FSA, state: GetState, res: any) => res, 33 | ...failureType 34 | }; 35 | 36 | return [requestType, successType, failureType]; 37 | } 38 | 39 | export async function actionWith(descriptor: FSA, args: Array, off) { 40 | try { 41 | descriptor.payload = await (typeof descriptor.payload === "function" 42 | ? descriptor.payload(...args) 43 | : descriptor.payload); 44 | } catch (e) { 45 | descriptor.payload = new InternalError(e.message); 46 | descriptor.error = true; 47 | } 48 | 49 | if (off) { 50 | descriptor.off = off; 51 | } 52 | 53 | return descriptor; 54 | } 55 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import CALL_FIR_API from "./CALL_FIR_API"; 6 | import isPlainObject from "lodash.isplainobject"; 7 | import firebase from "firebase"; 8 | import type { FirAPI, TypeDescriptor } from "./types"; 9 | 10 | export function isFirAction(action: FirAPI) { 11 | return isPlainObject(action) && action.hasOwnProperty(CALL_FIR_API); 12 | } 13 | 14 | export function isValidTypeDescriptor(obj: TypeDescriptor) { 15 | const validKeys = ["type", "payload"]; 16 | 17 | if (!isPlainObject(obj)) { 18 | return false; 19 | } 20 | for (let key in obj) { 21 | if (!~validKeys.indexOf(key)) { 22 | return false; 23 | } 24 | } 25 | if (!("type" in obj)) { 26 | return false; 27 | 28 | // $FlowFixMe symbol 29 | } else if (typeof obj.type !== "string" && typeof obj.type !== "symbol") { 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | export function validateFirAction(action: FirAPI) { 37 | var validationErrors = []; 38 | const db = firebase.database(); 39 | const validCallAPIKeys = ["ref", "method", "types", "content"]; 40 | 41 | const validMethods = [ 42 | "once_value", 43 | "set", 44 | "update", 45 | "remove", 46 | "on_value", 47 | "on_child_added", 48 | "on_child_changed", 49 | "on_child_removed", 50 | "on_child_moved" 51 | ]; 52 | 53 | if (!isFirAction(action)) { 54 | validationErrors.push( 55 | "FirAction must be plain JavaScript objects with a [CALL_FIR_API] property" 56 | ); 57 | return validationErrors; 58 | } 59 | 60 | for (let key in action) { 61 | if (key !== [CALL_FIR_API]) { 62 | validationErrors.push(`Invalid root key: ${key}`); 63 | } 64 | } 65 | 66 | const callAPI = action[CALL_FIR_API]; 67 | if (!isPlainObject(callAPI)) { 68 | validationErrors.push( 69 | "[CALL_FIR_API] property must be a plain JavaScript object" 70 | ); 71 | } 72 | for (let key in callAPI) { 73 | if (!~validCallAPIKeys.indexOf(key)) { 74 | validationErrors.push(`Invalid [CALL_FIR_API] key: ${key}`); 75 | } 76 | } 77 | 78 | const { ref, method, types } = callAPI; 79 | 80 | // check if `ref` property is valid 81 | if (typeof ref === "undefined") { 82 | validationErrors.push("[CALL_API].ref property must have an ref property"); 83 | } else if (typeof ref !== "string" && typeof ref !== "function") { 84 | validationErrors.push( 85 | "[CALL_API].ref property must be a string or a function" 86 | ); 87 | } else if (typeof ref !== "function") { 88 | validationErrors.push("[CALL_API].ref property must be a function"); 89 | } else if ( 90 | typeof ref === "function" && 91 | // check if is a Firebase Reference 92 | !(ref(db) instanceof firebase.database.Reference) && 93 | // check if is a Firebase Query 94 | !(ref(db) instanceof firebase.database.Query) 95 | ) { 96 | validationErrors.push( 97 | "[CALL_API].ref property must be an instance of firebase.database.Reference or firebase.database.Query" 98 | ); 99 | } 100 | 101 | // check if `method` property is valid 102 | if (typeof method === "undefined") { 103 | validationErrors.push("[CALL_API].method must have a method property"); 104 | } else if (typeof method !== "string") { 105 | validationErrors.push("[CALL_API].method property must be a string"); 106 | } else if (!~validMethods.indexOf(method)) { 107 | validationErrors.push( 108 | `Invalid [CALL_API].method: ${method}, must be one of ${validMethods.join( 109 | ", " 110 | )}` 111 | ); 112 | } 113 | 114 | // check if `types` property is valid 115 | if (typeof types === "undefined") { 116 | validationErrors.push("[CALL_API].types must have a types property"); 117 | } else if (!Array.isArray(types) || types.length !== 3) { 118 | validationErrors.push( 119 | "[CALL_API].types property must be an array of length 3" 120 | ); 121 | } else { 122 | const [requestType, successType, failureType] = types; 123 | 124 | // $FlowFixMe symbol 125 | if ( 126 | typeof requestType !== "string" && 127 | typeof requestType !== "symbol" && 128 | !isValidTypeDescriptor(requestType) 129 | ) { 130 | validationErrors.push("Invalid request type"); 131 | } 132 | // $FlowFixMe symbol 133 | if ( 134 | typeof successType !== "string" && 135 | typeof successType !== "symbol" && 136 | !isValidTypeDescriptor(successType) 137 | ) { 138 | validationErrors.push("Invalid success type"); 139 | } 140 | // $FlowFixMe symbol 141 | if ( 142 | typeof failureType !== "string" && 143 | typeof failureType !== "symbol" && 144 | !isValidTypeDescriptor(failureType) 145 | ) { 146 | validationErrors.push("Invalid failure type"); 147 | } 148 | } 149 | 150 | return validationErrors; 151 | } 152 | 153 | export function isValidateFirAction(action: FirAPI) { 154 | return !validateFirAction(action).length; 155 | } 156 | --------------------------------------------------------------------------------