├── .gitignore ├── README.md ├── airtable-export.js ├── app.json ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # airtable-github-export 2 | 3 | > Export airtable tables to json/geojson in Github 4 | 5 | We created this so that we could manage data for an online map in [Airtable](https://airtable.com/). It will export one or more tables in Airtable to an object of [GeoJSON](http://geojson.org/) files on Github. It parses each table for either a text field named `geometry`, which should be a stringified valid [GeoJSON geometry](https://tools.ietf.org/html/rfc7946#section-3.1), or two number fields named `lon` and `lat` which should be valid longitude and latitude in WGS84. The exported file will be of the format: 6 | 7 | ``` 8 | { 9 | : {...} // GeoJSON FeatureCollection 10 | } 11 | ``` 12 | 13 | ## Usage 14 | 15 | The script depends on several environment variables which you can set a `.env` file if you run this locally: 16 | 17 | ``` 18 | TABLES=Table 1, Table 2 19 | GITHUB_TOKEN=xxxxxx 20 | GITHUB_REPO=github_repo_name 21 | GITHUB_OWNER=github_repo_owner_name 22 | AIRTABLE_API_KEY=airtable_api_key 23 | AIRTABLE_BASE_ID=airtable_base_id 24 | GITHUB_BRANCH=github_branch_for_export 25 | GITHUB_FILENAME=filename_for_github_export 26 | ``` 27 | 28 | Run the export with `node airtable-export.js` 29 | 30 | We run this as a scheduled task on Heroku, and you can do the same by using the deploy button below: 31 | 32 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 33 | 34 | Once the app is deployed visit the 'Resources' page for your app on the Heroku dashboard, make sure the dynos are turned off, and configure the scheduler to run the export command `node airtable-export.js` at the schedule of your preference. 35 | 36 | ## Contribute 37 | 38 | PRs accepted. 39 | 40 | ## License 41 | 42 | MIT © Digital Democracy 43 | -------------------------------------------------------------------------------- /airtable-export.js: -------------------------------------------------------------------------------- 1 | var Airtable = require('airtable') 2 | var parallel = require('run-parallel') 3 | var Hubfs = require('hubfs.js') 4 | var geojsonhint = require('@mapbox/geojsonhint') 5 | var isEqualWith = require('lodash/isEqualWith') 6 | var rewind = require('@mapbox/geojson-rewind') 7 | var debug = require('debug')('airtable-github-export') 8 | var stringify = require('json-stable-stringify') 9 | 10 | require('dotenv').config() 11 | 12 | var config = { 13 | tables: process.env.TABLES.split(','), 14 | githubToken: process.env.GITHUB_TOKEN, 15 | repo: process.env.GITHUB_REPO, 16 | owner: process.env.GITHUB_OWNER, 17 | airtableToken: process.env.AIRTABLE_API_KEY, 18 | base: process.env.AIRTABLE_BASE_ID, 19 | branches: process.env.GITHUB_BRANCH ? process.env.GITHUB_BRANCH.split(',') : ['master'], 20 | filename: process.env.GITHUB_FILENAME || 'data.json' 21 | } 22 | 23 | var CREATE_MESSAGE = '[AIRTABLE-GITHUB-EXPORT] create ' + config.filename 24 | var UPDATE_MESSAGE = '[AIRTABLE-GITHUB-EXPORT] update ' + config.filename 25 | 26 | var hubfsOptions = { 27 | owner: config.owner, 28 | repo: config.repo, 29 | auth: { 30 | token: config.githubToken 31 | } 32 | } 33 | 34 | var gh = Hubfs(hubfsOptions) 35 | 36 | var base = new Airtable({apiKey: config.airtableToken}).base(config.base) 37 | 38 | var output = {} 39 | 40 | var tasks = config.tables.map(function (tableName) { 41 | return function (cb) { 42 | var data = [] 43 | // Ensure properties of output are set in the same order 44 | // otherwise they are set async and may change order, which 45 | // results in unhelpful diffs in Github 46 | output[tableName] = null 47 | 48 | base(tableName).select().eachPage(page, done) 49 | 50 | function page (records, next) { 51 | // This function will get called for each page of records. 52 | records.forEach(function (record) { 53 | var feature = { 54 | type: 'Feature', 55 | id: record._rawJson.id, 56 | properties: record._rawJson.fields || {} 57 | } 58 | var geometry = parseGeometry(get(record, 'geometry')) 59 | var coords = parseCoords([get(record, 'lon'), get(record, 'lat')]) 60 | if (geometry) { 61 | feature.geometry = geometry 62 | delete feature.properties.geometry 63 | delete feature.properties.Geometry 64 | } else if (coords) { 65 | feature.geometry = { 66 | type: 'Point', 67 | coordinates: coords 68 | } 69 | } else { 70 | feature.geometry = null 71 | } 72 | data.push(feature) 73 | }) 74 | next() 75 | } 76 | 77 | function done (err) { 78 | if (err) return cb(err) 79 | var featureCollection = { 80 | type: 'FeatureCollection', 81 | features: data 82 | } 83 | output[tableName] = featureCollection 84 | cb() 85 | } 86 | } 87 | }) 88 | 89 | parallel(tasks, function (err, result) { 90 | if (err) return onError(err) 91 | gh.readFile(config.filename, {ref: config.branches[0]}, function (err, data) { 92 | if (err) { 93 | if (!(/not found/i.test(err) || err.notFound)) { 94 | return onError(err) 95 | } 96 | } else { 97 | data = JSON.parse(data) 98 | } 99 | if (data && isEqualWith(data, output, customComparison)) { 100 | return debug('No changes from Airtable, skipping update to Github') 101 | } 102 | var message = data ? UPDATE_MESSAGE : CREATE_MESSAGE 103 | ghWrite(config.filename, output, config.branches, message, function (err) { 104 | if (err) return onError(err) 105 | debug('Updated ' + config.owner + '/' + config.repo + '/' + config.filename + 106 | ' with latest changes from Airtable') 107 | }) 108 | }) 109 | }) 110 | 111 | function ghWrite (filename, data, branches, message, cb) { 112 | var pending = branches.length 113 | branches.forEach(function (branch) { 114 | var opts = { 115 | message: message, 116 | branch: branch 117 | } 118 | gh.writeFile(filename, stringify(data, { replacer: null, space: 2 }), opts, done) 119 | }) 120 | function done (err) { 121 | if (err) return cb(err) 122 | if (--pending > 0) return 123 | cb() 124 | } 125 | } 126 | 127 | function onError (err) { 128 | console.error(err) 129 | process.exit(1) 130 | } 131 | 132 | // Case insensitive record.get 133 | function get (record, fieldName) { 134 | if (typeof record.get(fieldName) !== 'undefined') { 135 | return record.get(fieldName) 136 | } else if (typeof record.get(fieldName.charAt(0).toUpperCase() + fieldName.slice(1)) !== 'undefined') { 137 | return record.get(fieldName.charAt(0).toUpperCase() + fieldName.slice(1)) 138 | } else if (typeof record.get(fieldName.toUpperCase()) !== 'undefined') { 139 | return record.get(fieldName.toUpperCase()) 140 | } 141 | } 142 | 143 | // Try to parse a geometry field if it is valid GeoJSON geometry 144 | function parseGeometry (geom) { 145 | if (!geom) return null 146 | try { 147 | geom = rewind(JSON.parse(geom)) 148 | } catch (e) { 149 | return null 150 | } 151 | var errors = geojsonhint.hint(geom) 152 | if (errors && errors.length) return null 153 | return geom 154 | } 155 | 156 | // Check whether coordinates are valid 157 | function parseCoords (coords) { 158 | if (typeof coords[0] !== 'number' || typeof coords[1] !== 'number') return null 159 | if (coords[0] < -180 || coords[0] > 180 || coords[1] < -90 || coords[1] > 90) return null 160 | return coords 161 | } 162 | 163 | function isUrl(value) { 164 | if (typeof value !== 'string') return false 165 | try { 166 | new URL(value) 167 | return true 168 | } catch (e) { 169 | return false 170 | } 171 | } 172 | 173 | /** For URLs, ignore the query string when comparing, because Airtable adds a 174 | * timestamp to the URLs returned from the API which changes every time */ 175 | function customComparison (objValue, othValue) { 176 | // if neither is a URL, return undefined to use default comparison 177 | if (!(isUrl(objValue) && isUrl(othValue))) return 178 | const objUrl = new URL(objValue) 179 | const othUrl = new URL(othValue) 180 | objUrl.search = '' 181 | othUrl.search = '' 182 | return objUrl.toString() === othUrl.toString() 183 | } 184 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Airtable Github Export", 3 | "description": "Export airtable tables to json/geojson in Github", 4 | "keywords": [ 5 | "airtable", 6 | "maps", 7 | "github", 8 | "export" 9 | ], 10 | "formation": { 11 | "web": { 12 | "quantity": 0 13 | } 14 | }, 15 | "addons": [ 16 | "scheduler:standard" 17 | ], 18 | "env": { 19 | "AIRTABLE_API_KEY": { 20 | "description": "Airtable API key https://airtable.com/account" 21 | }, 22 | "AIRTABLE_BASE_ID": { 23 | "description": "Airtable base ID, from the API page for your base https://airtable.com/api" 24 | }, 25 | "GITHUB_BRANCH": { 26 | "description": "Github branch to save your data (default master)", 27 | "value": "master" 28 | }, 29 | "GITHUB_FILENAME": { 30 | "description": "Filename and path for output file on Github", 31 | "value": "data.json" 32 | }, 33 | "GITHUB_OWNER": { 34 | "description": "Username of the owner of the repo for output file on Github" 35 | }, 36 | "GITHUB_REPO": { 37 | "description": "Name of repo for output file on Github" 38 | }, 39 | "GITHUB_TOKEN": { 40 | "description": "Github personal access token from https://github.com/settings/tokens" 41 | }, 42 | "TABLES": { 43 | "description": "Comma separated list of table names to include in the export" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airtable-github-export", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@mapbox/geojson-rewind": { 8 | "version": "0.5.1", 9 | "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.1.tgz", 10 | "integrity": "sha512-eL7fMmfTBKjrb+VFHXCGv9Ot0zc3C0U+CwXo1IrP+EPwDczLoXv34Tgq3y+2mPSFNVUXgU42ILWJTC7145KPTA==", 11 | "requires": { 12 | "get-stream": "^6.0.1", 13 | "minimist": "^1.2.5" 14 | }, 15 | "dependencies": { 16 | "minimist": { 17 | "version": "1.2.5", 18 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 19 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 20 | } 21 | } 22 | }, 23 | "@mapbox/geojsonhint": { 24 | "version": "3.0.1", 25 | "resolved": "https://registry.npmjs.org/@mapbox/geojsonhint/-/geojsonhint-3.0.1.tgz", 26 | "integrity": "sha512-8BhaDcFnTGP9Z8gLWQfeOMMO4xWmJWN45ZITfjiatI5aP9rmRXTaF2ugvCAJQH+llWTuC+/V+c783ApVoSbnsQ==", 27 | "requires": { 28 | "concat-stream": "^1.6.1", 29 | "jsonlint-lines": "1.7.1", 30 | "minimist": "^1.2.5", 31 | "vfile": "^4.0.0", 32 | "vfile-reporter": "^5.1.1" 33 | } 34 | }, 35 | "@types/node": { 36 | "version": "14.17.16", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.16.tgz", 38 | "integrity": "sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==" 39 | }, 40 | "@types/unist": { 41 | "version": "2.0.6", 42 | "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", 43 | "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" 44 | }, 45 | "JSV": { 46 | "version": "4.0.2", 47 | "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", 48 | "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=" 49 | }, 50 | "abort-controller": { 51 | "version": "3.0.0", 52 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 53 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 54 | "requires": { 55 | "event-target-shim": "^5.0.0" 56 | } 57 | }, 58 | "abortcontroller-polyfill": { 59 | "version": "1.7.3", 60 | "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", 61 | "integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==" 62 | }, 63 | "airtable": { 64 | "version": "0.11.1", 65 | "resolved": "https://registry.npmjs.org/airtable/-/airtable-0.11.1.tgz", 66 | "integrity": "sha512-33zBuUDhLl+FWWAFxFjS1a+vJr/b+UK//EV943nuiimChWph6YykQjYPmu/GucQ30g7mgaqq+98uPD4rfDHOgg==", 67 | "requires": { 68 | "@types/node": ">=8.0.0 <15", 69 | "abort-controller": "^3.0.0", 70 | "abortcontroller-polyfill": "^1.4.0", 71 | "lodash": "^4.17.21", 72 | "node-fetch": "^2.6.1" 73 | } 74 | }, 75 | "ansi-regex": { 76 | "version": "3.0.0", 77 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 78 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 79 | }, 80 | "ansi-styles": { 81 | "version": "1.0.0", 82 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", 83 | "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=" 84 | }, 85 | "async": { 86 | "version": "0.9.2", 87 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 88 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 89 | }, 90 | "buffer-from": { 91 | "version": "1.1.2", 92 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 93 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 94 | }, 95 | "chalk": { 96 | "version": "0.4.0", 97 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", 98 | "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", 99 | "requires": { 100 | "ansi-styles": "~1.0.0", 101 | "has-color": "~0.1.0", 102 | "strip-ansi": "~0.1.0" 103 | } 104 | }, 105 | "concat-stream": { 106 | "version": "1.6.2", 107 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 108 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 109 | "requires": { 110 | "buffer-from": "^1.0.0", 111 | "inherits": "^2.0.3", 112 | "readable-stream": "^2.2.2", 113 | "typedarray": "^0.0.6" 114 | } 115 | }, 116 | "core-util-is": { 117 | "version": "1.0.3", 118 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 119 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 120 | }, 121 | "debug": { 122 | "version": "4.3.2", 123 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 124 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 125 | "requires": { 126 | "ms": "2.1.2" 127 | } 128 | }, 129 | "dotenv": { 130 | "version": "10.0.0", 131 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 132 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" 133 | }, 134 | "es6-promise": { 135 | "version": "3.0.2", 136 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", 137 | "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=" 138 | }, 139 | "event-target-shim": { 140 | "version": "5.0.1", 141 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 142 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" 143 | }, 144 | "get-stream": { 145 | "version": "6.0.1", 146 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", 147 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" 148 | }, 149 | "has-color": { 150 | "version": "0.1.7", 151 | "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", 152 | "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=" 153 | }, 154 | "has-flag": { 155 | "version": "3.0.0", 156 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 157 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 158 | }, 159 | "hubfs.js": { 160 | "version": "1.0.0", 161 | "resolved": "https://registry.npmjs.org/hubfs.js/-/hubfs.js-1.0.0.tgz", 162 | "integrity": "sha1-i3UIaIQ0tQ2H1uoc0anJODwLJio=", 163 | "requires": { 164 | "async": "^0.9.0", 165 | "octokat": "^0.4.9", 166 | "xtend": "^4.0.0" 167 | } 168 | }, 169 | "inherits": { 170 | "version": "2.0.4", 171 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 172 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 173 | }, 174 | "is-buffer": { 175 | "version": "2.0.5", 176 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", 177 | "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" 178 | }, 179 | "is-fullwidth-code-point": { 180 | "version": "2.0.0", 181 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 182 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 183 | }, 184 | "isarray": { 185 | "version": "1.0.0", 186 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 187 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 188 | }, 189 | "json-stable-stringify": { 190 | "version": "1.0.1", 191 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 192 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 193 | "requires": { 194 | "jsonify": "~0.0.0" 195 | } 196 | }, 197 | "jsonify": { 198 | "version": "0.0.0", 199 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 200 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" 201 | }, 202 | "jsonlint-lines": { 203 | "version": "1.7.1", 204 | "resolved": "https://registry.npmjs.org/jsonlint-lines/-/jsonlint-lines-1.7.1.tgz", 205 | "integrity": "sha1-UH3mgNP7jEvhZBzFfW9nnynxeP8=", 206 | "requires": { 207 | "JSV": ">= 4.0.x", 208 | "nomnom": ">= 1.5.x" 209 | } 210 | }, 211 | "lodash": { 212 | "version": "4.17.21", 213 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 214 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 215 | }, 216 | "minimist": { 217 | "version": "1.2.5", 218 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 219 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 220 | }, 221 | "ms": { 222 | "version": "2.1.2", 223 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 224 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 225 | }, 226 | "node-fetch": { 227 | "version": "2.6.2", 228 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz", 229 | "integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==" 230 | }, 231 | "nomnom": { 232 | "version": "1.8.1", 233 | "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", 234 | "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", 235 | "requires": { 236 | "chalk": "~0.4.0", 237 | "underscore": "~1.6.0" 238 | } 239 | }, 240 | "octokat": { 241 | "version": "0.4.18", 242 | "resolved": "https://registry.npmjs.org/octokat/-/octokat-0.4.18.tgz", 243 | "integrity": "sha1-r6flJlS1Rkkj+AGRezoxz/emhI0=", 244 | "requires": { 245 | "es6-promise": "3.0.2", 246 | "xmlhttprequest": "~1.8.0" 247 | } 248 | }, 249 | "process-nextick-args": { 250 | "version": "2.0.1", 251 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 252 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 253 | }, 254 | "queue-microtask": { 255 | "version": "1.2.3", 256 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 257 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" 258 | }, 259 | "readable-stream": { 260 | "version": "2.3.7", 261 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 262 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 263 | "requires": { 264 | "core-util-is": "~1.0.0", 265 | "inherits": "~2.0.3", 266 | "isarray": "~1.0.0", 267 | "process-nextick-args": "~2.0.0", 268 | "safe-buffer": "~5.1.1", 269 | "string_decoder": "~1.1.1", 270 | "util-deprecate": "~1.0.1" 271 | } 272 | }, 273 | "repeat-string": { 274 | "version": "1.6.1", 275 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 276 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 277 | }, 278 | "run-parallel": { 279 | "version": "1.2.0", 280 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 281 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 282 | "requires": { 283 | "queue-microtask": "^1.2.2" 284 | } 285 | }, 286 | "safe-buffer": { 287 | "version": "5.1.2", 288 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 289 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 290 | }, 291 | "string-width": { 292 | "version": "2.1.1", 293 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 294 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 295 | "requires": { 296 | "is-fullwidth-code-point": "^2.0.0", 297 | "strip-ansi": "^4.0.0" 298 | }, 299 | "dependencies": { 300 | "strip-ansi": { 301 | "version": "4.0.0", 302 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 303 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 304 | "requires": { 305 | "ansi-regex": "^3.0.0" 306 | } 307 | } 308 | } 309 | }, 310 | "string_decoder": { 311 | "version": "1.1.1", 312 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 313 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 314 | "requires": { 315 | "safe-buffer": "~5.1.0" 316 | } 317 | }, 318 | "strip-ansi": { 319 | "version": "0.1.1", 320 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", 321 | "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" 322 | }, 323 | "supports-color": { 324 | "version": "5.5.0", 325 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 326 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 327 | "requires": { 328 | "has-flag": "^3.0.0" 329 | } 330 | }, 331 | "typedarray": { 332 | "version": "0.0.6", 333 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 334 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" 335 | }, 336 | "underscore": { 337 | "version": "1.6.0", 338 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", 339 | "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" 340 | }, 341 | "unist-util-stringify-position": { 342 | "version": "2.0.3", 343 | "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", 344 | "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", 345 | "requires": { 346 | "@types/unist": "^2.0.2" 347 | } 348 | }, 349 | "util-deprecate": { 350 | "version": "1.0.2", 351 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 352 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 353 | }, 354 | "vfile": { 355 | "version": "4.2.1", 356 | "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", 357 | "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", 358 | "requires": { 359 | "@types/unist": "^2.0.0", 360 | "is-buffer": "^2.0.0", 361 | "unist-util-stringify-position": "^2.0.0", 362 | "vfile-message": "^2.0.0" 363 | } 364 | }, 365 | "vfile-message": { 366 | "version": "2.0.4", 367 | "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", 368 | "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", 369 | "requires": { 370 | "@types/unist": "^2.0.0", 371 | "unist-util-stringify-position": "^2.0.0" 372 | } 373 | }, 374 | "vfile-reporter": { 375 | "version": "5.1.2", 376 | "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-5.1.2.tgz", 377 | "integrity": "sha512-b15sTuss1wOPWVlyWOvu+n6wGJ/eTYngz3uqMLimQvxZ+Q5oFQGYZZP1o3dR9sk58G5+wej0UPCZSwQBX/mzrQ==", 378 | "requires": { 379 | "repeat-string": "^1.5.0", 380 | "string-width": "^2.0.0", 381 | "supports-color": "^5.0.0", 382 | "unist-util-stringify-position": "^2.0.0", 383 | "vfile-sort": "^2.1.2", 384 | "vfile-statistics": "^1.1.0" 385 | } 386 | }, 387 | "vfile-sort": { 388 | "version": "2.2.2", 389 | "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-2.2.2.tgz", 390 | "integrity": "sha512-tAyUqD2R1l/7Rn7ixdGkhXLD3zsg+XLAeUDUhXearjfIcpL1Hcsj5hHpCoy/gvfK/Ws61+e972fm0F7up7hfYA==" 391 | }, 392 | "vfile-statistics": { 393 | "version": "1.1.4", 394 | "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-1.1.4.tgz", 395 | "integrity": "sha512-lXhElVO0Rq3frgPvFBwahmed3X03vjPF8OcjKMy8+F1xU/3Q3QU3tKEDp743SFtb74PdF0UWpxPvtOP0GCLheA==" 396 | }, 397 | "xmlhttprequest": { 398 | "version": "1.8.0", 399 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 400 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" 401 | }, 402 | "xtend": { 403 | "version": "4.0.2", 404 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 405 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 406 | } 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airtable-github-export", 3 | "version": "1.0.0", 4 | "description": "Export airtable tables to json/geojson in Github", 5 | "main": "airtable-export.js", 6 | "bin": "airtable-export.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Gregor MacLennan", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@mapbox/geojson-rewind": "^0.5.1", 14 | "@mapbox/geojsonhint": "^3.0.1", 15 | "airtable": "^0.11.1", 16 | "debug": "^4.3.2", 17 | "dotenv": "^10.0.0", 18 | "hubfs.js": "^1.0.0", 19 | "json-stable-stringify": "^1.0.1", 20 | "lodash": "^4.17.21", 21 | "run-parallel": "^1.1.6" 22 | }, 23 | "devDependencies": {}, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/digidem/airtable-github-export.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/digidem/airtable-github-export/issues" 30 | }, 31 | "homepage": "https://github.com/digidem/airtable-github-export#readme" 32 | } 33 | --------------------------------------------------------------------------------