├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin └── index.js ├── index.js ├── index.test.js ├── package.json └── src ├── config.js ├── config.test.js ├── handler.js ├── parse-env.js ├── parse-env.test.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore npm/yarn scraps 2 | node_modules/ 3 | npm-debug.log 4 | yarn.lock 5 | yarn-debug.log 6 | yarn-error.log 7 | package-lock.json 8 | 9 | # Ignore environment files 10 | .env 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | notifications: 5 | email: false 6 | script: yarn test --ci && yarn lint 7 | cache: yarn 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.1.0] - 2018-09-14 11 | 12 | ### Added 13 | 14 | - Allow specifying permissions per-table ([#18](https://github.com/rosszurowski/micro-airtable-api/pull/18)) 15 | 16 | ## [1.0.0] - 2018-09-11 17 | 18 | ### Added 19 | 20 | - npm package. 21 | 22 | ### Changed 23 | 24 | - Server script to uses package export (`src/handler.js`) internally. 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Local Setup 4 | 5 | 1. Clone the repository with `git clone https://github.com/rosszurowski/micro-airtable-api.git` 6 | 2. Install dependencies using `npm install` (or `yarn install` if you use yarn) 7 | 3. Run the server in development mode with `npm run dev`, which restarts every time you edit the code 8 | 4. Run the test suite with `npm test` 9 | 5. Run Prettier (to fix style inconsistencies) with `npm run prettier` 10 | 11 | ### Testing 12 | 13 | Automated tests are written using [Jest](https://jestjs.io/en/). Check out [the Jest documentation](https://jestjs.io/docs/en/getting-started) for more details. 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Ross Zurowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-airtable-api 2 | 3 | [![Build Status](https://badgen.net/travis/rosszurowski/micro-airtable-api)](https://travis-ci.com/rosszurowski/micro-airtable-api) 4 | 5 | Quickly make an API from an [Airtable](https://airtable.com/). Use it as a database or CMS without any hassle. 6 | 7 | Airtable offers [a great API](https://airtable.com/api), but using it on the client-side exposes your API key, giving anyone read-write permissions to your data. `micro-airtable-api` proxies an Airtable API, hiding your API key and letting you control access (eg. marking an API as read-only). 8 | 9 | Use Airtable as a cheap-and-easy CMS for simple blogs and sites :tada: 10 | 11 | > :construction: This project has not been thoroughly tested. Use at your own risk! 12 | 13 | ## Usage 14 | 15 | The simplest way to get started with your own Airtable proxy is via [`now`](https://now.sh/). Setup and deploy with a single command: 16 | 17 | ```bash 18 | $ now rosszurowski/micro-airtable-api -e AIRTABLE_BASE_ID=asdf123 -e AIRTABLE_API_KEY=xyz123 19 | 20 | > Deployment complete! https://micro-airtable-api-asfdasdf.now.sh 21 | ``` 22 | 23 | Once deployed, you can read or edit your data at: 24 | 25 | ``` 26 | https://micro-airtable-api-asdasd.now.sh/v0/TableName 27 | ``` 28 | 29 | To update to a new version with potential bugfixes, all you have to do is run the `now` command again and change the URL you call in your app! 30 | 31 | ### CLI 32 | 33 | If you'd like to run a proxy on a different service, you can use the `micro-airtable-api` command-line. Install the package globally and run it: 34 | 35 | ```bash 36 | $ npm i -g micro-airtable-api 37 | $ AIRTABLE_BASE_ID=asdf123 AIRTABLE_API_KEY=xyz123 micro-airtable-api 38 | 39 | > micro-airtable-api listening on http://localhost:3000 40 | ``` 41 | 42 | ### JS API 43 | 44 | For more advanced configuration or to integrate with an existing http or express server, you can also install the package locally and pass the handler into your webserver: 45 | 46 | ```bash 47 | $ npm i micro-airtable-api 48 | ``` 49 | 50 | ```js 51 | const http = require('http'); 52 | const createAirtableProxy = require('micro-airtable-api'); 53 | 54 | const config = { 55 | airtableApiKey: 'YourApiKey', 56 | airtableBaseId: 'YourBaseId', 57 | }; 58 | 59 | const server = http.createServer(createAirtableProxy(config)); 60 | ``` 61 | 62 | ### Setup Notes 63 | 64 | You can find your _Base ID_ in the [Airtable API docs](https://airtable.com/api) and _API key_ in [your Airtable account settings](https://airtable.com/account). 65 | 66 | Read below for [all configurable options](#configuration). 67 | 68 | ## Configuration 69 | 70 | `micro-airtable-api` is configurable both through the JS API and the CLI. 71 | 72 | ```jsx 73 | const http = require('http'); 74 | const createAirtableProxy = require('micro-airtable-api'); 75 | 76 | const config = {}; 77 | 78 | http.createServer(createAirtableProxy(config)); 79 | ``` 80 | 81 | #### `config.airtableBaseId` **(required)** 82 | 83 | The _Base ID_ of the Airtable you want to connect to. You can find this in your [Airtable API docs](https://airtable.com/api). 84 | 85 | #### `config.airtableApiKey` **(required)** 86 | 87 | Your personal account API key. You can find this in [your account settings](https://airtable.com/account). 88 | 89 | #### `config.allowedMethods` 90 | 91 | An array of HTTP methods supported by the API. Use this to restrict how users can interact with your API. Defaults to all methods. 92 | 93 | This maps directly to operations on Airtable: 94 | 95 | - `GET` allows reading lists of records or individual records 96 | - `POST` allows creating new records 97 | - `PATCH` allows updating specific fields existing records 98 | - `PUT` allows updating an entire existing record 99 | - `DELETE` allows removing records. 100 | 101 | You can read [Airtable's API documentation](https://airtable.com/api) for more details about how to use these methods. 102 | 103 | To create a read-only API, to use as a CMS: 104 | 105 | ```jsx 106 | createAirtableProxy({ 107 | airtableBaseId: '...', 108 | airtableApiKeyId: '...', 109 | allowedMethods: ['GET'], 110 | }); 111 | ``` 112 | 113 | To create a write-only API, to use for collecting survey responses: 114 | 115 | ```jsx 116 | // A write-only API (eg. surveys) 117 | createAirtableProxy({ 118 | airtableBaseId: '...', 119 | airtableApiKeyId: '...', 120 | allowedMethods: ['POST'], 121 | }); 122 | ``` 123 | 124 | You can set table-specific permissions by passing in an object with table names as the keys. 125 | 126 | If you were setting up a blog through Airtable, you could do the following: 127 | 128 | ```jsx 129 | createAirtableProxy({ 130 | airtableBaseId: '...', 131 | airtableApiKeyId: '...', 132 | allowedMethods: { 133 | 'Blog Posts': ['GET'], 134 | 'Blog Comments': ['POST', 'PATCH', 'PUT', 'DELETE'], 135 | }, 136 | }); 137 | ``` 138 | 139 | Note, the `OPTIONS` method is always allowed for [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) purposes. 140 | 141 | ### CLI Options 142 | 143 | The CLI exposes the above configuration options through environment variables for easy deployment. 144 | 145 | ```bash 146 | $ AIRTABLE_BASE_ID=asdf123 AIRTABLE_API_KEY=xyz123 micro-airtable-api 147 | ``` 148 | 149 | - **`AIRTABLE_BASE_ID` (required)** Same as `config.airtableBaseId` above 150 | - **`AIRTABLE_API_KEY` (required)** Same as `config.airtableApiKey` above 151 | - `ALLOWED_METHODS` Similar to `config.allowedMethods` above, except a comma-separated list instead of an array. For example, allow creating new records but not deleting by passing in a string without the delete method: `ALLOWED_METHODS=GET,POST,PATCH,PUT`. The CLI does not support table-specific permissions. Use the JS API if this is something you need. 152 | - `READ_ONLY` A shortcut variable to restrict the API to only `GET` requests. Equivalent to `ALLOWED_METHODS=GET`. Users of the API will be able to list all records and individual records, but not create, update, or delete. 153 | - `PORT` Sets the port for the local server. Defaults to `3000`. 154 | 155 | ## Contributing 156 | 157 | Issues and PRs are welcome! If you'd like to contribute code, check out our [guide on how to contribute](https://github.com/rosszurowski/micro-airtable-api/blob/master/CONTRIBUTING.md). 158 | 159 | ## License 160 | 161 | [MIT](https://github.com/rosszurowski/micro-airtable-api/blob/master/LICENSE.md) 162 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const http = require('http'); 4 | const handler = require('../src/handler'); 5 | const parseEnv = require('../src/parse-env'); 6 | 7 | process.on('uncaughtException', err => { 8 | exit(err); 9 | }); 10 | 11 | const config = parseEnv(process.env); 12 | const server = http.createServer(handler(config)); 13 | 14 | server.listen(config.port, err => { 15 | if (err) { 16 | exit(err); 17 | return; 18 | } 19 | 20 | console.log( 21 | `> micro-airtable-api listening on http://localhost:${config.port}` 22 | ); 23 | }); 24 | 25 | function exit(err) { 26 | console.error('Error:', err.message); 27 | process.exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/handler'); 2 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const got = require('got'); 3 | const listen = require('test-listen'); 4 | const handler = require('./index'); 5 | 6 | const describeIfEnv = 7 | process.env.AIRTABLE_API_KEY && process.env.AIRTABLE_BASE_ID 8 | ? describe 9 | : xdescribe; 10 | const baseConfig = { 11 | airtableBaseId: process.env.AIRTABLE_BASE_ID, 12 | airtableApiKey: process.env.AIRTABLE_API_KEY, 13 | }; 14 | 15 | const getExistingPosts = async client => { 16 | return (await client.get('/v0/Posts')).body.records; 17 | }; 18 | 19 | describeIfEnv('micro-airtable-api', () => { 20 | describe('when `allowedMethods="*"`', () => { 21 | const config = { ...baseConfig, allowedMethods: '*' }; 22 | let server; 23 | let client; 24 | 25 | beforeAll(async () => { 26 | server = http.createServer(handler(config)); 27 | const baseUrl = await listen(server); 28 | client = got.extend({ baseUrl, json: true }); 29 | }); 30 | 31 | afterAll(() => { 32 | server.close(); 33 | }); 34 | 35 | describe('GET /v0/Posts', () => { 36 | it('returns successful response', async () => { 37 | await client.get('/v0/Posts'); 38 | }); 39 | }); 40 | 41 | describe('POST /v0/Posts', () => { 42 | it('returns successful response', async () => { 43 | await client.post('/v0/Posts', { 44 | body: { fields: { title: 'Test POST request' } }, 45 | }); 46 | }); 47 | }); 48 | 49 | describe('PUT /v0/Posts/:id', () => { 50 | it('returns successful response', async () => { 51 | const post = (await getExistingPosts(client))[0]; 52 | 53 | await client.put(`/v0/Posts/${post.id}`, { 54 | body: { fields: { title: 'Test PUT request' } }, 55 | }); 56 | }); 57 | }); 58 | 59 | describe('PATCH /v0/Posts/:id', () => { 60 | it('returns successful response', async () => { 61 | const post = (await getExistingPosts(client))[0]; 62 | 63 | await client.patch(`/v0/Posts/${post.id}`, { 64 | body: { fields: { title: 'Test PATCH request' } }, 65 | }); 66 | }); 67 | }); 68 | 69 | describe('DELETE /v0/Posts/:id', () => { 70 | it('returns successful response', async () => { 71 | const post = (await getExistingPosts(client))[0]; 72 | 73 | await client.delete(`/v0/Posts/${post.id}`); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-airtable-api", 3 | "version": "1.1.0", 4 | "main": "index.js", 5 | "bin": "./bin/index.js", 6 | "scripts": { 7 | "start": "node bin/index.js", 8 | "dev": "nodemon -r dotenv/config --ignore '*.test.js' bin/index.js", 9 | "prettier": "prettier --write 'index.js' 'index.test.js' 'src/*.js'", 10 | "lint": "prettier --list-different 'index.js' 'src/*.js'", 11 | "test": "npm run lint --silent && jest" 12 | }, 13 | "dependencies": { 14 | "http-proxy": "^1.16.2", 15 | "path-match": "^1.2.4" 16 | }, 17 | "devDependencies": { 18 | "dotenv": "^4.0.0", 19 | "got": "^9.2.2", 20 | "jest": "^23.5.0", 21 | "nodemon": "^1.18.3", 22 | "prettier": "1.14.2", 23 | "test-listen": "^1.1.0" 24 | }, 25 | "prettier": { 26 | "singleQuote": true, 27 | "trailingComma": "es5" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const { isObject } = require('./utils'); 2 | 3 | const invariant = (condition, err) => { 4 | if (condition) { 5 | return; 6 | } 7 | 8 | throw err; 9 | }; 10 | 11 | const defaultConfig = { 12 | airtableApiKey: null, 13 | airtableBaseId: null, 14 | allowedMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], 15 | }; 16 | 17 | module.exports = inputConfig => { 18 | const config = Object.assign({}, defaultConfig, inputConfig); 19 | 20 | invariant( 21 | typeof config.airtableApiKey === 'string', 22 | new TypeError('config.airtableApiKey must be a string') 23 | ); 24 | invariant( 25 | typeof config.airtableBaseId === 'string', 26 | new TypeError('config.airtableBaseId must be a string') 27 | ); 28 | invariant( 29 | Array.isArray(config.allowedMethods) || 30 | isObject(config.allowedMethods) || 31 | config.allowedMethods === '*', 32 | new TypeError('config.allowedMethods must be an array or object') 33 | ); 34 | 35 | return config; 36 | }; 37 | -------------------------------------------------------------------------------- /src/config.test.js: -------------------------------------------------------------------------------- 1 | const getConfig = require('./config'); 2 | 3 | describe('getConfig', () => { 4 | it('throws without airtableApiKey or airtableBaseId', () => { 5 | expect(() => { 6 | getConfig({ 7 | airtableApiKey: undefined, 8 | airtableBaseId: 'YourBaseId', 9 | }); 10 | }).toThrow(/airtableApiKey must be/i); 11 | 12 | expect(() => { 13 | getConfig({ 14 | airtableApiKey: 'YourApiKey', 15 | airtableBaseId: undefined, 16 | }); 17 | }).toThrow(/airtableBaseId must be/i); 18 | 19 | expect(() => { 20 | getConfig({ 21 | airtableApiKey: 'YourApiKey', 22 | airtableBaseId: 'YourBaseId', 23 | }); 24 | }).not.toThrow(); 25 | }); 26 | 27 | it('throws on invalid allowedMethods', () => { 28 | expect(() => { 29 | getConfig({ 30 | airtableApiKey: 'YourApiKey', 31 | airtableBaseId: 'YourBaseId', 32 | allowedMethods: 'GET,POST', 33 | }); 34 | }).toThrow(/allowedMethods must be an array or object/i); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | const parse = require('url').parse; 2 | const httpProxy = require('http-proxy'); 3 | const route = require('path-match')(); 4 | const getConfig = require('./config'); 5 | const { isObject, compact } = require('./utils'); 6 | 7 | const allowedHttpHeaders = [ 8 | 'Authorization', 9 | 'Content-Type', 10 | 'Content-Length', 11 | 'User-Agent', 12 | 'X-Airtable-Application-ID', 13 | 'X-Airtable-User-Agent', 14 | 'X-API-Version', 15 | 'X-Requested-With', 16 | ]; 17 | 18 | const writeError = (res, status, code, message) => { 19 | res.writeHead(status, { 'Content-Type': 'application/json' }); 20 | res.end(JSON.stringify({ code, message })); 21 | }; 22 | 23 | const createProxy = apiKey => { 24 | const proxy = httpProxy.createProxyServer({ 25 | changeOrigin: true, 26 | headers: { 27 | Accept: 'application/json', 28 | Authorization: `Bearer ${apiKey}`, 29 | }, 30 | target: 'https://api.airtable.com', 31 | secure: false, 32 | ssl: { 33 | rejectUnauthorized: false, 34 | }, 35 | }); 36 | 37 | proxy.on('error', (err, req, res) => { 38 | writeError( 39 | res, 40 | 500, 41 | 'Internal Server Error', 42 | `An unknown error occurred: ${err.message}` 43 | ); 44 | }); 45 | 46 | return proxy; 47 | }; 48 | 49 | const match = route('/:version/:tableName/:recordId?'); 50 | const parseUrl = (originalUrl, airtableBaseId) => { 51 | const components = parse(originalUrl); 52 | const params = match(components.pathname); 53 | 54 | if (params === false) { 55 | const originalPath = components.path; 56 | return { proxyUrl: originalPath, tableName: false }; 57 | } 58 | 59 | const proxyUrl = 60 | '/' + 61 | compact([ 62 | params.version, 63 | airtableBaseId, 64 | params.tableName, 65 | params.recordId, 66 | components.search, 67 | ]).join('/'); 68 | 69 | return { 70 | proxyUrl, 71 | tableName: params.tableName, 72 | }; 73 | }; 74 | 75 | const allMethods = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE']; 76 | const getAllowedMethods = (config, tableName) => { 77 | let allowedMethods = []; 78 | 79 | if (!tableName) { 80 | allowedMethods = allMethods; 81 | } else { 82 | const hasRouteSpecificConfig = isObject(config.allowedMethods); 83 | const configAllowedMethods = hasRouteSpecificConfig 84 | ? config.allowedMethods[tableName] || [] 85 | : config.allowedMethods; 86 | 87 | allowedMethods = 88 | configAllowedMethods === '*' ? allMethods : configAllowedMethods; 89 | } 90 | 91 | return ['OPTIONS', ...allowedMethods]; 92 | }; 93 | 94 | const isAllowed = (allowedMethods, method) => { 95 | if (Array.isArray(allowedMethods) && allowedMethods.includes(method)) { 96 | return true; 97 | } 98 | 99 | return false; 100 | }; 101 | 102 | module.exports = options => { 103 | const config = getConfig(options); 104 | const proxy = createProxy(config.airtableApiKey); 105 | 106 | return (req, res) => { 107 | const method = 108 | req.method && req.method.toUpperCase && req.method.toUpperCase(); 109 | 110 | const { proxyUrl, tableName } = parseUrl(req.url, config.airtableBaseId); 111 | const allowedMethods = getAllowedMethods(config, tableName); 112 | 113 | req.url = proxyUrl; 114 | 115 | res.setHeader('Access-Control-Allow-Origin', '*'); 116 | res.setHeader('Access-Control-Request-Method', '*'); 117 | res.setHeader('Access-Control-Allow-Methods', allowedMethods.join(',')); 118 | res.setHeader('Access-Control-Allow-Headers', allowedHttpHeaders.join(',')); 119 | 120 | if (method === 'OPTIONS') { 121 | res.setHeader('Content-Length', '0'); 122 | res.writeHead(204); 123 | res.end(); 124 | return; 125 | } 126 | 127 | if (!isAllowed(allowedMethods, method)) { 128 | writeError( 129 | res, 130 | 405, 131 | 'Method Not Allowed', 132 | `This API does not allow '${method}' requests for '${tableName}'` 133 | ); 134 | return; 135 | } 136 | 137 | proxy.web(req, res); 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /src/parse-env.js: -------------------------------------------------------------------------------- 1 | module.exports = env => { 2 | const { 3 | AIRTABLE_BASE_ID, 4 | AIRTABLE_API_KEY, 5 | ALLOWED_METHODS, 6 | PORT = 3000, 7 | READ_ONLY, 8 | } = env; 9 | 10 | if ( 11 | typeof AIRTABLE_BASE_ID === 'undefined' || 12 | typeof AIRTABLE_API_KEY === 'undefined' 13 | ) { 14 | throw new TypeError( 15 | 'Please provide AIRTABLE_BASE_ID and AIRTABLE_API_KEY as environment variables.' 16 | ); 17 | } 18 | 19 | const config = { 20 | airtableApiKey: AIRTABLE_API_KEY, 21 | airtableBaseId: AIRTABLE_BASE_ID, 22 | port: PORT, 23 | }; 24 | 25 | if (READ_ONLY === 'true') { 26 | config.allowedMethods = ['GET']; 27 | } else if (ALLOWED_METHODS) { 28 | config.allowedMethods = ALLOWED_METHODS.split(','); 29 | } 30 | 31 | return config; 32 | }; 33 | -------------------------------------------------------------------------------- /src/parse-env.test.js: -------------------------------------------------------------------------------- 1 | const parseEnv = require('./parse-env'); 2 | 3 | describe('parseEnv', () => { 4 | const defaultConfig = { 5 | airtableApiKey: 'YourApiKey', 6 | airtableBaseId: 'YourBaseId', 7 | port: 3000, 8 | }; 9 | 10 | it('returns the default config when only API Key and Base ID are set', () => { 11 | const config = parseEnv({ 12 | AIRTABLE_API_KEY: 'YourApiKey', 13 | AIRTABLE_BASE_ID: 'YourBaseId', 14 | }); 15 | 16 | expect(config).not.toBeNull(); 17 | expect(config).toEqual(defaultConfig); 18 | }); 19 | 20 | it('throws an error when no env variables set', () => { 21 | expect(() => { 22 | parseEnv({}); 23 | }).toThrow(/Please provide AIRTABLE_BASE_ID and AIRTABLE_API_KEY/i); 24 | }); 25 | 26 | it('returns the correct config when env variables are set', () => { 27 | const expectedConfig = { 28 | allowedMethods: ['GET', 'POST', 'DELETE'], 29 | airtableApiKey: 'YourApiKey', 30 | airtableBaseId: 'YourBaseId', 31 | port: 3001, 32 | }; 33 | 34 | const config = parseEnv({ 35 | ALLOWED_METHODS: 'GET,POST,DELETE', 36 | AIRTABLE_API_KEY: 'YourApiKey', 37 | AIRTABLE_BASE_ID: 'YourBaseId', 38 | PORT: 3001, 39 | }); 40 | 41 | expect(config).not.toBeNull(); 42 | expect(config).toEqual(expectedConfig); 43 | }); 44 | 45 | it('returns a correct config when READ_ONLY is true', () => { 46 | const expectedConfig = { 47 | allowedMethods: ['GET'], 48 | airtableApiKey: 'YourApiKey', 49 | airtableBaseId: 'YourBaseId', 50 | port: 3000, 51 | }; 52 | 53 | const config = parseEnv({ 54 | AIRTABLE_API_KEY: 'YourApiKey', 55 | AIRTABLE_BASE_ID: 'YourBaseId', 56 | READ_ONLY: 'true', 57 | }); 58 | 59 | expect(config).not.toBeNull(); 60 | expect(config).toEqual(expectedConfig); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | exports.isObject = input => 2 | Object.prototype.toString.call(input) === '[object Object]'; 3 | 4 | exports.compact = arr => arr.filter(Boolean); 5 | --------------------------------------------------------------------------------