├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── transform.js └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/ 9 | node_modules/ 10 | .cargo-ok 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Max Goodhart 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sheet2json-worker 2 | 3 | A Cloudflare worker which transforms a Google Sheet into JSON. 4 | 5 | 6 | ## Usage: 7 | 8 | ### Edit the vars in wrangler.toml: 9 | 10 | Follow the [Cloudflare workers quickstart](https://developers.cloudflare.com/workers/quickstart#setup) to fill in `account_id`, `route`, and `zone_id` in your `wrangler.toml` file. 11 | 12 | Set the following vars to point the worker at your spreadsheet: 13 | 14 | ``` 15 | vars = { 16 | # From your spreadsheet URL: docs.google.com/spreadsheets/d/DOC_ID/... 17 | DOC_ID = "", 18 | # Name of spreadsheet tab to get data from 19 | SHEET_NAME = "", 20 | # Seconds to cache data 21 | TTL = "5", 22 | } 23 | ``` 24 | 25 | 26 | ### Add an optional transform function 27 | 28 | If you'd like to transform the data before returning it, edit `transform.js`. 29 | 30 | Example: 31 | 32 | ``` 33 | // Only emit rows containing an id 34 | module.exports = (rows) => rows.filter(row => row.id) 35 | ``` 36 | 37 | 38 | ### Publish! 39 | 40 | Run `npm run publish` to publish your new worker. 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Papa = require('papaparse') 2 | const camelCase = require('lodash.camelcase') 3 | const mapKeys = require('lodash.mapkeys') 4 | 5 | const transformRows = require('./transform') 6 | 7 | addEventListener('fetch', event => { 8 | event.respondWith(generateJSON()) 9 | }) 10 | 11 | function parseCSV(csvString, config) { 12 | return new Promise((resolve, reject) => { 13 | Papa.parse(csvString, { 14 | ...config, 15 | complete: ({ data }) => resolve(data), 16 | error: reject, 17 | }) 18 | }) 19 | }; 20 | 21 | async function generateJSON() { 22 | const url = `https://docs.google.com/spreadsheets/d/${DOC_ID}/gviz/tq?tqx=out:csv&headers=0&sheet=${encodeURIComponent(SHEET_NAME)}` 23 | 24 | const resp = await fetch(url, { 25 | cf: { 26 | cacheTtl: Number(TTL), // seconds 27 | }, 28 | }) 29 | const csv = await resp.text() 30 | 31 | const rows = await parseCSV(csv, { 32 | header: true, 33 | transformHeader: camelCase, 34 | }) 35 | const transformedRows = transformRows(rows) 36 | 37 | response = new Response(JSON.stringify(transformedRows), { 38 | headers: { 39 | 'Cache-Control': `max-age=${TTL}`, 40 | 'Content-Type': 'application/json', 41 | }, 42 | }) 43 | return response 44 | } 45 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sheet2json-worker", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@cloudflare/wrangler": { 8 | "version": "1.10.2", 9 | "resolved": "https://registry.npmjs.org/@cloudflare/wrangler/-/wrangler-1.10.2.tgz", 10 | "integrity": "sha512-9bumgVaS7vZhbhdAdHex+Iutkr3IZoA3DxG55LaNrXUHNV6uJwzN78uUJFwZQypxBzroZ1LwE1CFJxG1Z9KFrQ==", 11 | "dev": true, 12 | "requires": { 13 | "binary-install": "0.0.1" 14 | }, 15 | "dependencies": { 16 | "axios": { 17 | "version": "0.19.2", 18 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 19 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 20 | "dev": true, 21 | "requires": { 22 | "follow-redirects": "1.5.10" 23 | } 24 | }, 25 | "balanced-match": { 26 | "version": "1.0.0", 27 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 28 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 29 | "dev": true 30 | }, 31 | "binary-install": { 32 | "version": "0.0.1", 33 | "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-0.0.1.tgz", 34 | "integrity": "sha512-axr6lqB4ec/pkEOb/JMnZpfcroWv1zT48pVz1oQHG7XmGkS77vmdxmP1btuH79lWQdy9e2MVw/uW0D8siopkRg==", 35 | "dev": true, 36 | "requires": { 37 | "axios": "^0.19.0", 38 | "env-paths": "^2.2.0", 39 | "mkdirp": "^0.5.1", 40 | "rimraf": "^3.0.0", 41 | "tar": "^5.0.5", 42 | "universal-url": "^2.0.0" 43 | } 44 | }, 45 | "brace-expansion": { 46 | "version": "1.1.11", 47 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 48 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 49 | "dev": true, 50 | "requires": { 51 | "balanced-match": "^1.0.0", 52 | "concat-map": "0.0.1" 53 | } 54 | }, 55 | "chownr": { 56 | "version": "1.1.3", 57 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", 58 | "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", 59 | "dev": true 60 | }, 61 | "concat-map": { 62 | "version": "0.0.1", 63 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 64 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 65 | "dev": true 66 | }, 67 | "debug": { 68 | "version": "3.1.0", 69 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 70 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 71 | "dev": true, 72 | "requires": { 73 | "ms": "2.0.0" 74 | } 75 | }, 76 | "env-paths": { 77 | "version": "2.2.0", 78 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", 79 | "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", 80 | "dev": true 81 | }, 82 | "follow-redirects": { 83 | "version": "1.5.10", 84 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 85 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 86 | "dev": true, 87 | "requires": { 88 | "debug": "=3.1.0" 89 | } 90 | }, 91 | "fs-minipass": { 92 | "version": "2.1.0", 93 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 94 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 95 | "dev": true, 96 | "requires": { 97 | "minipass": "^3.0.0" 98 | } 99 | }, 100 | "fs.realpath": { 101 | "version": "1.0.0", 102 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 103 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 104 | "dev": true 105 | }, 106 | "glob": { 107 | "version": "7.1.6", 108 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 109 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 110 | "dev": true, 111 | "requires": { 112 | "fs.realpath": "^1.0.0", 113 | "inflight": "^1.0.4", 114 | "inherits": "2", 115 | "minimatch": "^3.0.4", 116 | "once": "^1.3.0", 117 | "path-is-absolute": "^1.0.0" 118 | } 119 | }, 120 | "hasurl": { 121 | "version": "1.0.0", 122 | "resolved": "https://registry.npmjs.org/hasurl/-/hasurl-1.0.0.tgz", 123 | "integrity": "sha512-43ypUd3DbwyCT01UYpA99AEZxZ4aKtRxWGBHEIbjcOsUghd9YUON0C+JF6isNjaiwC/UF5neaUudy6JS9jZPZQ==", 124 | "dev": true 125 | }, 126 | "inflight": { 127 | "version": "1.0.6", 128 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 129 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 130 | "dev": true, 131 | "requires": { 132 | "once": "^1.3.0", 133 | "wrappy": "1" 134 | } 135 | }, 136 | "inherits": { 137 | "version": "2.0.4", 138 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 139 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 140 | "dev": true 141 | }, 142 | "lodash.sortby": { 143 | "version": "4.7.0", 144 | "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", 145 | "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", 146 | "dev": true 147 | }, 148 | "minimatch": { 149 | "version": "3.0.4", 150 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 151 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 152 | "dev": true, 153 | "requires": { 154 | "brace-expansion": "^1.1.7" 155 | } 156 | }, 157 | "minimist": { 158 | "version": "1.2.5", 159 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 160 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 161 | "dev": true 162 | }, 163 | "minipass": { 164 | "version": "3.1.1", 165 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", 166 | "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", 167 | "dev": true, 168 | "requires": { 169 | "yallist": "^4.0.0" 170 | } 171 | }, 172 | "minizlib": { 173 | "version": "2.1.0", 174 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", 175 | "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", 176 | "dev": true, 177 | "requires": { 178 | "minipass": "^3.0.0", 179 | "yallist": "^4.0.0" 180 | } 181 | }, 182 | "mkdirp": { 183 | "version": "0.5.5", 184 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 185 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 186 | "dev": true, 187 | "requires": { 188 | "minimist": "^1.2.5" 189 | } 190 | }, 191 | "ms": { 192 | "version": "2.0.0", 193 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 194 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 195 | "dev": true 196 | }, 197 | "once": { 198 | "version": "1.4.0", 199 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 200 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 201 | "dev": true, 202 | "requires": { 203 | "wrappy": "1" 204 | } 205 | }, 206 | "path-is-absolute": { 207 | "version": "1.0.1", 208 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 209 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 210 | "dev": true 211 | }, 212 | "punycode": { 213 | "version": "2.1.1", 214 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 215 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 216 | "dev": true 217 | }, 218 | "rimraf": { 219 | "version": "3.0.1", 220 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.1.tgz", 221 | "integrity": "sha512-IQ4ikL8SjBiEDZfk+DFVwqRK8md24RWMEJkdSlgNLkyyAImcjf8SWvU1qFMDOb4igBClbTQ/ugPqXcRwdFTxZw==", 222 | "dev": true, 223 | "requires": { 224 | "glob": "^7.1.3" 225 | } 226 | }, 227 | "tar": { 228 | "version": "5.0.5", 229 | "resolved": "https://registry.npmjs.org/tar/-/tar-5.0.5.tgz", 230 | "integrity": "sha512-MNIgJddrV2TkuwChwcSNds/5E9VijOiw7kAc1y5hTNJoLDSuIyid2QtLYiCYNnICebpuvjhPQZsXwUL0O3l7OQ==", 231 | "dev": true, 232 | "requires": { 233 | "chownr": "^1.1.3", 234 | "fs-minipass": "^2.0.0", 235 | "minipass": "^3.0.0", 236 | "minizlib": "^2.1.0", 237 | "mkdirp": "^0.5.0", 238 | "yallist": "^4.0.0" 239 | } 240 | }, 241 | "tr46": { 242 | "version": "1.0.1", 243 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", 244 | "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", 245 | "dev": true, 246 | "requires": { 247 | "punycode": "^2.1.0" 248 | } 249 | }, 250 | "universal-url": { 251 | "version": "2.0.0", 252 | "resolved": "https://registry.npmjs.org/universal-url/-/universal-url-2.0.0.tgz", 253 | "integrity": "sha512-3DLtXdm/G1LQMCnPj+Aw7uDoleQttNHp2g5FnNQKR6cP6taNWS1b/Ehjjx4PVyvejKi3TJyu8iBraKM4q3JQPg==", 254 | "dev": true, 255 | "requires": { 256 | "hasurl": "^1.0.0", 257 | "whatwg-url": "^7.0.0" 258 | } 259 | }, 260 | "webidl-conversions": { 261 | "version": "4.0.2", 262 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", 263 | "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", 264 | "dev": true 265 | }, 266 | "whatwg-url": { 267 | "version": "7.1.0", 268 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", 269 | "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", 270 | "dev": true, 271 | "requires": { 272 | "lodash.sortby": "^4.7.0", 273 | "tr46": "^1.0.1", 274 | "webidl-conversions": "^4.0.2" 275 | } 276 | }, 277 | "wrappy": { 278 | "version": "1.0.2", 279 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 280 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 281 | "dev": true 282 | }, 283 | "yallist": { 284 | "version": "4.0.0", 285 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 286 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 287 | "dev": true 288 | } 289 | } 290 | }, 291 | "lodash.camelcase": { 292 | "version": "4.3.0", 293 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 294 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" 295 | }, 296 | "lodash.mapkeys": { 297 | "version": "4.6.0", 298 | "resolved": "https://registry.npmjs.org/lodash.mapkeys/-/lodash.mapkeys-4.6.0.tgz", 299 | "integrity": "sha1-3yz6Ix18V8eorQA6va1dc9PqUZU=" 300 | }, 301 | "papaparse": { 302 | "version": "5.2.0", 303 | "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz", 304 | "integrity": "sha512-ylq1wgUSnagU+MKQtNeVqrPhZuMYBvOSL00DHycFTCxownF95gpLAk1HiHdUW77N8yxRq1qHXLdlIPyBSG9NSA==" 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sheet2json-worker", 3 | "version": "1.0.0", 4 | "description": "A Cloudflare worker that turns a Google Spreadsheet into a JSON API endpoint", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "wrangler build", 8 | "publish": "wrangler publish" 9 | }, 10 | "author": "Max Goodhart ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@cloudflare/wrangler": "^1.10.2" 14 | }, 15 | "dependencies": { 16 | "lodash.camelcase": "^4.3.0", 17 | "lodash.mapkeys": "^4.6.0", 18 | "papaparse": "^5.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | module.exports = (rows) => rows 2 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "sheet2json-worker" 2 | type = "webpack" 3 | account_id = "" 4 | route = "" 5 | zone_id = "" 6 | vars = { 7 | # From your spreadsheet URL: docs.google.com/spreadsheets/d/DOC_ID/... 8 | DOC_ID = "", 9 | # Name of spreadsheet tab to get data from 10 | SHEET_NAME = "", 11 | # Seconds to cache data 12 | TTL = "5", 13 | } 14 | --------------------------------------------------------------------------------