├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .nvmrc ├── README.md ├── build └── index.js ├── index.js ├── index.test.js ├── package-lock.json ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "browsers": ["last 2 versions"] 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | ["@babel/plugin-proposal-object-rest-spread"] 11 | ] 12 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8.9.0 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: 38 | name: Setup Code Climate Test Reporter 39 | command: | 40 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 41 | chmod +x ./cc-test-reporter 42 | - run: 43 | name: Unit Tests With Coverage 44 | command: | 45 | ./cc-test-reporter before-build 46 | yarn coverage 47 | ./cc-test-reporter after-build --exit-code $? 48 | environment: 49 | JEST_JUNIT_OUTPUT: "coverage/junit/js-test-results.xml" 50 | - store_test_results: 51 | path: coverage/junit 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .c9 2 | /node_modules 3 | *.log 4 | /coverage 5 | *.xml -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.9.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firestore-parser · [![Maintainability](https://api.codeclimate.com/v1/badges/c021344dfe81edfce992/maintainability)](https://codeclimate.com/github/jdbence/firestore-parser/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/c021344dfe81edfce992/test_coverage)](https://codeclimate.com/github/jdbence/firestore-parser/test_coverage) [![npm version](https://img.shields.io/npm/v/firestore-parser.svg?style=flat)](https://www.npmjs.com/package/firestore-parser) [![npm license](https://img.shields.io/npm/l/firestore-parser.svg?style=flat)](https://www.npmjs.com/package/firestore-parser) 2 | 3 | Parse the [Firestore REST API](https://firebase.google.com/docs/firestore/reference/rest/) JSON into a useable JS object 4 | 5 | ## Installation 6 | 7 | ``` 8 | // Install with NPM 9 | npm install firestore-parser --save 10 | // or Install with Yarn 11 | yarn add firestore-parser 12 | ``` 13 | 14 | ## Examples 15 | [Live Example](https://repl.it/@jdbence/firestore-parser-example-01) 16 | ```JS 17 | // Node/AMD: 18 | // const FireStoreParser = require('firestore-parser'); 19 | // Node with TypeScript: 20 | // import * as FireStoreParser from 'firestore-parser'; 21 | import FireStoreParser from 'firestore-parser' 22 | const projectID = 'PROJECT_ID' 23 | const key = 'API_KEY' 24 | const doc = 'DOCUMENT' 25 | const url = `https://firestore.googleapis.com/v1beta1/projects/${projectID}/databases/(default)/documents/${doc}?key=${key}` 26 | 27 | fetch(url) 28 | .then(response => response.json()) 29 | .then(json => FireStoreParser(json)) 30 | .then(json => console.log(json)); 31 | ``` 32 | 33 | ### Data Structure 34 | 35 | The Firestore JSON returned in the REST API, uses value type as keys. This can be difficult to work with since you have to know the data type prior getting the value. The `firestore-parser` removes this barrier for you. 36 | #### JSON response from Firestore 37 | ``` 38 | { 39 | "player": { 40 | "mapValue": { 41 | "fields": { 42 | "name": { 43 | "stringValue": "steve" 44 | }, 45 | "health": { 46 | "integerValue": "100" 47 | }, 48 | "alive": { 49 | "booleanValue": true 50 | } 51 | } 52 | } 53 | }, 54 | "level": { 55 | "integerValue": "7" 56 | } 57 | } 58 | ``` 59 | #### JSON parsed with firestore-parser 60 | ``` 61 | { 62 | "player": { 63 | "name": "steve", 64 | "health": 100, 65 | "alive": true 66 | }, 67 | "level": 7 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | module.exports=function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}r.r(t),r.d(t,"FireStoreParser",function(){return u});var u=function e(t){var r=function(e){var t={arrayValue:1,bytesValue:1,booleanValue:1,doubleValue:1,geoPointValue:1,integerValue:1,mapValue:1,nullValue:1,referenceValue:1,stringValue:1,timestampValue:1};return Object.keys(e).find(function(e){return 1===t[e]})}(t);return"doubleValue"===r||"integerValue"===r?t=Number(t[r]):"arrayValue"===r?t=(t[r]&&t[r].values||[]).map(function(t){return e(t)}):"mapValue"===r?t=e(t[r]&&t[r].fields||{}):"geoPointValue"===r?t=function(e){for(var t=1;t { 2 | const props = { 'arrayValue': 1, 'bytesValue': 1, 'booleanValue': 1, 'doubleValue': 1, 'geoPointValue': 1, 'integerValue': 1, 'mapValue': 1, 'nullValue': 1, 'referenceValue': 1, 'stringValue': 1, 'timestampValue': 1 } 3 | return Object.keys(value).find(k => props[k] === 1) 4 | } 5 | 6 | export const FireStoreParser = value => { 7 | const prop = getFireStoreProp(value) 8 | if (prop === 'doubleValue' || prop === 'integerValue') { 9 | value = Number(value[prop]) 10 | } 11 | else if (prop === 'arrayValue') { 12 | value = (value[prop] && value[prop].values || []).map(v => FireStoreParser(v)) 13 | } 14 | else if (prop === 'mapValue') { 15 | value = FireStoreParser(value[prop] && value[prop].fields || {}) 16 | } 17 | else if (prop === 'geoPointValue') { 18 | value = { latitude: 0, longitude: 0, ...value[prop] } 19 | } 20 | else if (prop) { 21 | value = value[prop] 22 | } 23 | else if (typeof value === 'object') { 24 | Object.keys(value).forEach(k => value[k] = FireStoreParser(value[k])) 25 | } 26 | return value; 27 | } 28 | export default FireStoreParser 29 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | /*global expect*/ 2 | import FireStoreParser from './index' 3 | 4 | const testData = { 5 | "name": "some/large/long/value", 6 | "fields": { 7 | "double": { 8 | "doubleValue": "456" 9 | }, 10 | "number": { 11 | "integerValue": "123" 12 | }, 13 | "array": { 14 | "arrayValue": { 15 | "values": [{ 16 | "stringValue": "cat" 17 | }, 18 | { 19 | "stringValue": "dog" 20 | } 21 | ] 22 | } 23 | }, 24 | "array2": { 25 | "arrayValue": {} 26 | }, 27 | "timestamp": { 28 | "timestampValue": "2018-03-11T08:00:00Z" 29 | }, 30 | "obj": { 31 | "mapValue": { 32 | "fields": { 33 | "string": { 34 | "stringValue": "def" 35 | } 36 | } 37 | } 38 | }, 39 | "bool": { 40 | "booleanValue": true 41 | }, 42 | "bytes": { 43 | "bytesValue": "bWFkZSB0aGUgS2Vzc2VsIFJ1biBpbiBsZXNzIHRoYW4gdHdlbHZlIHBhcnNlY3M=" 44 | }, 45 | "string": { 46 | "stringValue": "abc" 47 | }, 48 | "geo": { 49 | "geoPointValue": { 50 | "latitude": 10, 51 | "longitude": 30 52 | } 53 | }, 54 | "ref": { 55 | "referenceValue": "some/large/long/value" 56 | }, 57 | "isNull": { 58 | "nullValue": null 59 | } 60 | }, 61 | "createTime": "2018-03-11T14:10:11.083793Z", 62 | "updateTime": "2018-03-11T14:10:11.083793Z" 63 | }; 64 | 65 | test('Simple JS object match', () => { 66 | expect(FireStoreParser({ data: "" })).toEqual({ data: "" }); 67 | }); 68 | 69 | test('Complex JS object match', () => { 70 | expect(FireStoreParser(testData)).toEqual({ 71 | "createTime": "2018-03-11T14:10:11.083793Z", 72 | "fields": { 73 | "array": [ 74 | "cat", 75 | "dog" 76 | ], 77 | "array2": [], 78 | "bool": true, 79 | "geo": { 80 | "latitude": 10, 81 | "longitude": 30 82 | }, 83 | "isNull": null, 84 | "double": 456, 85 | "number": 123, 86 | "obj": { 87 | "string": "def" 88 | }, 89 | "ref": "some/large/long/value", 90 | "bytes": "bWFkZSB0aGUgS2Vzc2VsIFJ1biBpbiBsZXNzIHRoYW4gdHdlbHZlIHBhcnNlY3M=", 91 | "string": "abc", 92 | "timestamp": "2018-03-11T08:00:00Z" 93 | }, 94 | "name": "some/large/long/value", 95 | "updateTime": "2018-03-11T14:10:11.083793Z" 96 | }); 97 | }); 98 | 99 | test('Bytes match', () => { 100 | expect(FireStoreParser({ 101 | encoded: { 102 | bytesValue: "bHVrZWlhbXlvdXJmYXRoZXI=" 103 | } 104 | })).toEqual({ 105 | encoded: "bHVrZWlhbXlvdXJmYXRoZXI=" 106 | }); 107 | }); 108 | 109 | test('Strings match', () => { 110 | expect(FireStoreParser({ 111 | createTime: "2018-03-11T14:10:11.083793Z" 112 | })).toEqual({ 113 | createTime: "2018-03-11T14:10:11.083793Z" 114 | }); 115 | }); 116 | 117 | test('Null match', () => { 118 | expect(FireStoreParser({ 119 | "isNull": { 120 | "nullValue": null 121 | } 122 | })).toEqual({ "isNull": null }); 123 | }); 124 | 125 | test('Reference match', () => { 126 | expect(FireStoreParser({ 127 | "ref": { 128 | "referenceValue": "some/longe/string/that/has/values" 129 | } 130 | })).toEqual({ 131 | "ref": "some/longe/string/that/has/values" 132 | }); 133 | }); 134 | 135 | test('Geo match', () => { 136 | expect(FireStoreParser({ 137 | "geo": { 138 | "geoPointValue": { 139 | "latitude": 10, 140 | "longitude": 30 141 | } 142 | } 143 | })).toEqual({ 144 | "geo": { 145 | "latitude": 10, 146 | "longitude": 30 147 | } 148 | }); 149 | }); 150 | 151 | test('Geo match zeros', () => { 152 | expect(FireStoreParser({ 153 | "geo": { 154 | "geoPointValue": {} 155 | } 156 | })).toEqual({ 157 | "geo": { 158 | "latitude": 0, 159 | "longitude": 0 160 | } 161 | }); 162 | }); 163 | 164 | test('boolean match', () => { 165 | expect(FireStoreParser({ 166 | "bool": { 167 | "booleanValue": true 168 | } 169 | })).toEqual({ "bool": true }); 170 | }); 171 | 172 | test('double match', () => { 173 | expect(FireStoreParser({ 174 | "double": { 175 | "doubleValue": "456" 176 | } 177 | })).toEqual({ "double": 456 }); 178 | }); 179 | 180 | test('integer match', () => { 181 | expect(FireStoreParser({ 182 | "number": { 183 | "integerValue": "123" 184 | } 185 | })).toEqual({ "number": 123 }); 186 | }); 187 | 188 | test('Object match', () => { 189 | expect(FireStoreParser({ 190 | "obj": { 191 | "mapValue": { 192 | "fields": { 193 | "string": { 194 | "stringValue": "def" 195 | } 196 | } 197 | } 198 | } 199 | })).toEqual({ "obj": { "string": "def" } }); 200 | }); 201 | 202 | test('Object match with no values', () => { 203 | expect(FireStoreParser({ 204 | "obj": { 205 | "mapValue": {} 206 | } 207 | })).toEqual({ "obj": {} }); 208 | }); 209 | 210 | test('Object match with undefined value', () => { 211 | expect(FireStoreParser({ 212 | "obj": { 213 | "mapValue": undefined 214 | } 215 | })).toEqual({ "obj": {} }); 216 | }); 217 | 218 | test('Array match', () => { 219 | expect(FireStoreParser({ 220 | "array": { 221 | "arrayValue": { 222 | "values": [{ 223 | "stringValue": "cat" 224 | }, 225 | { 226 | "stringValue": "dog" 227 | } 228 | ] 229 | } 230 | } 231 | })).toEqual({ "array": ["cat", "dog"] }); 232 | }); 233 | 234 | test('Array match with no values', () => { 235 | expect(FireStoreParser({ 236 | "array": { 237 | "arrayValue": {} 238 | } 239 | })).toEqual({ "array": [] }); 240 | }); 241 | 242 | test('Array match with undefined value', () => { 243 | expect(FireStoreParser({ 244 | "array": { 245 | "arrayValue": undefined 246 | } 247 | })).toEqual({ "array": [] }); 248 | }); 249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firestore-parser", 3 | "version": "0.9.0", 4 | "description": "Parse the Firestore REST API endpoint JSON into a useable JS object", 5 | "main": "./build/index.js", 6 | "scripts": { 7 | "test": "jest --coverage", 8 | "coverage": "jest --env=jsdom --ci --coverage --reporters=default --reporters=jest-junit ./node_modules/jest-junit", 9 | "start": "webpack --watch --mode development", 10 | "build": "webpack --mode production" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jdbence/firestore-parser.git" 15 | }, 16 | "keywords": [ 17 | "JSON", 18 | "Firebase", 19 | "Firestore", 20 | "Rest" 21 | ], 22 | "author": "Joshua Bence", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jdbence/firestore-parser/issues" 26 | }, 27 | "homepage": "https://github.com/jdbence/firestore-parser#readme", 28 | "devDependencies": { 29 | "@babel/cli": "^7.7.7", 30 | "@babel/core": "^7.7.7", 31 | "@babel/plugin-proposal-object-rest-spread": "^7.7.7", 32 | "@babel/preset-env": "^7.7.7", 33 | "babel-jest": "^24.9.0", 34 | "babel-loader": "^8.0.6", 35 | "jest": "^24.9.0", 36 | "jest-junit": "^10.0.0", 37 | "regenerator-runtime": "^0.13.3", 38 | "webpack": "^4.41.5", 39 | "webpack-cli": "^3.3.10" 40 | }, 41 | "jest": { 42 | "testPathIgnorePatterns": [ 43 | "/node_modules/", 44 | ".c9" 45 | ], 46 | "testEnvironment": "node" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = { 3 | entry: './index.js', 4 | output: { 5 | path: path.resolve(__dirname, 'build'), 6 | filename: 'index.js', 7 | libraryTarget: 'commonjs2', 8 | libraryExport: 'default' 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.js$/, 13 | exclude: /(node_modules|build)/, 14 | use: { 15 | loader: 'babel-loader' 16 | } 17 | }] 18 | } 19 | }; 20 | --------------------------------------------------------------------------------