├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .sample.env ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── arrayFunctions.ts ├── asyncAirtable.ts ├── baseHandlers.ts ├── buildExpression.ts ├── buildOpts.ts ├── checkArg.ts ├── checkError.ts ├── dateFunctions.ts ├── fetch.ts ├── http.ts ├── logicalFunctions.ts ├── logicalOperators.ts ├── numericFunctions.ts ├── queryBuilder.ts ├── rateLimitHandler.ts ├── regexFunctions.ts ├── tests │ ├── bulkCreate.test.ts │ ├── bulkDelete.test.ts │ ├── bulkUpdate.test.ts │ ├── createRecord.test.ts │ ├── deleteRecord.test.ts │ ├── find.test.ts │ ├── globalSetup.ts │ ├── globalTeardown.ts │ ├── helpers.test.ts │ ├── main.test.ts │ ├── queryBuilder.test.ts │ ├── select.test.ts │ ├── testData.json │ ├── updateRecord.test.ts │ └── upsert.test.ts ├── textFunctions.ts ├── typeCheckers.ts └── types │ ├── airtable.ts │ ├── index.ts │ ├── queryBuilder.ts │ └── queryBuilder │ ├── array.ts │ ├── comparison.ts │ ├── date.ts │ ├── logical.ts │ ├── numeric.ts │ ├── regex.ts │ └── text.ts ├── tsconfig.json ├── typedoc.json ├── webpack.config.js └── webpack.config.min.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "rules": { 5 | "no-console": [ 6 | "error", 7 | { 8 | "allow": ["error"] 9 | } 10 | ] 11 | }, 12 | "plugins": ["jest", "@typescript-eslint/eslint-plugin"], 13 | "extends": [ 14 | "eslint:recommended", 15 | "prettier", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "ignorePatterns": [ 19 | "/lib", 20 | "src/tests", 21 | "jest.config.js", 22 | "/docs", 23 | "/coverage", 24 | "/dist", 25 | "webpack.*.js" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: develop 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | - next 9 | pull_request: 10 | branches: 11 | - develop 12 | - main 13 | - next 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Setup PNPM 26 | uses: pnpm/action-setup@v2.1.0 27 | with: 28 | version: 6.27.1 29 | - name: Setup Node 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: 14 33 | cache: 'pnpm' 34 | - name: Install deps 35 | run: pnpm install 36 | - name: Run tests 37 | run: pnpm test 38 | env: 39 | AIRTABLE_KEY: ${{secrets.AIRTABLE_KEY}} 40 | AIRTABLE_BASE: ${{secrets.AIRTABLE_BASE}} 41 | AIRTABLE_TABLE: ${{ matrix.os }} 42 | TEST_FILTER: "{email} = 'same@test.com'" 43 | NEW_RECORD: '{"title": "test-create", "value": 23, "email": "new@test.com"}' 44 | UPDATE_RECORD: '{"title": "test-UPDATED"}' 45 | BULK_UPDATE: '{"title": "test-BULK"}' 46 | DESTRUCTIVE_UPDATE_RECORD: '{"title": "test-UPDATED-destructive", "value": 23}' 47 | RETRY_TIMEOUT: 60000 48 | REQ_COUNT: 100 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | coverage/ 4 | .eslintcache 5 | docs/ 6 | lib/ 7 | dist/ 8 | .DS_STORE 9 | # Local Netlify folder 10 | .netlify 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | docs/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": true, 4 | "useTabs": false, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "singleQuote": true, 8 | "tabWidth": 2 9 | } 10 | -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | AIRTABLE_KEY='YOUR AIRTABLE API KEY' 2 | AIRTABLE_BASE='YOUR AIRTABLE BASE ID' 3 | AIRTABLE_TABLE='tests' 4 | TEST_FILTER="{email} = 'same@test.com'" 5 | NEW_RECORD='{"title": "test-create", "value": 23, "email": "new@test.com"}' 6 | UPDATE_RECORD='{"title": "test-UPDATED"}' 7 | DESTRUCTIVE_UPDATE_RECORD='{"title": "test-UPDATED-destructive", "value": 23}' 8 | RETRY_TIMEOUT=60000 9 | REQ_COUNT=120 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Async Airtable Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at: 63 | 64 | - Email: 65 | - Twitter: [@\_\_\_datboi\_](https://twitter.com/___datboi_) 66 | 67 | All complaints will be reviewed and investigated promptly and fairly. 68 | 69 | All community leaders are obligated to respect the privacy and security of the 70 | reporter of any incident. 71 | 72 | ## Enforcement Guidelines 73 | 74 | Community leaders will follow these Community Impact Guidelines in determining 75 | the consequences for any action they deem in violation of this Code of Conduct: 76 | 77 | ### 1. Correction 78 | 79 | **Community Impact**: Use of inappropriate language or other behavior deemed 80 | unprofessional or unwelcome in the community. 81 | 82 | **Consequence**: A private, written warning from community leaders, providing 83 | clarity around the nature of the violation and an explanation of why the 84 | behavior was inappropriate. A public apology may be requested. 85 | 86 | ### 2. Warning 87 | 88 | **Community Impact**: A violation through a single incident or series 89 | of actions. 90 | 91 | **Consequence**: A warning with consequences for continued behavior. No 92 | interaction with the people involved, including unsolicited interaction with 93 | those enforcing the Code of Conduct, for a specified period of time. This 94 | includes avoiding interactions in community spaces as well as external channels 95 | like social media. Violating these terms may lead to a temporary or 96 | permanent ban. 97 | 98 | ### 3. Temporary Ban 99 | 100 | **Community Impact**: A serious violation of community standards, including 101 | sustained inappropriate behavior. 102 | 103 | **Consequence**: A temporary ban from any sort of interaction or public 104 | communication with the community for a specified period of time. No public or 105 | private interaction with the people involved, including unsolicited interaction 106 | with those enforcing the Code of Conduct, is allowed during this period. 107 | Violating these terms may lead to a permanent ban. 108 | 109 | ### 4. Permanent Ban 110 | 111 | **Community Impact**: Demonstrating a pattern of violation of community 112 | standards, including sustained inappropriate behavior, harassment of an 113 | individual, or aggression toward or disparagement of classes of individuals. 114 | 115 | **Consequence**: A permanent ban from any sort of public interaction within 116 | the community. 117 | 118 | ## Attribution 119 | 120 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 121 | version 2.0, available at 122 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 123 | 124 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 125 | enforcement ladder](https://github.com/mozilla/diversity). 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | 129 | For answers to common questions about this code of conduct, see the FAQ at 130 | https://www.contributor-covenant.org/faq. Translations are available at 131 | https://www.contributor-covenant.org/translations. 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Async Airtable 2 | 3 | 💖 **Hi!** Thanks so much for thinking about contributing. We're so glad you're here! 💖 4 | 5 | **Ok... Let's get down to business!** 6 | 7 | ## A Word About Git Flow 8 | 9 | We have recently transitioned to using Git Flow for managing features, hotfixes, and releases. If you're not familiar with Git Flow, we suggest you head over to [this page](https://datasift.github.io/gitflow/TheHubFlowTools.html) and give it a quick read. 10 | 11 | ## Setting Up Your Environment 12 | 13 | 1. Add the [testing base](https://airtable.com/addBaseFromShare/shrU70u93JxzNqBhe?utm_source=airtable_shared_application) to a new or existing base on your Airtable account. 14 | 2. Get the base ID of the newly create base. You can do this by heading over to [Airtable's API page](https://airtable.com/api) and selecting that base from the list, you should see: 15 | > _The ID of this base is BASE_ID_ 16 | 3. Create an .env file in the repository root, using the supplied [.sample.env](.sample.env) file as a template. 17 | 4. Make sure to copy the **base ID** and your **[API key](https://support.airtable.com/hc/en-us/articles/219046777-How-do-I-get-my-API-key-)** into the newly create .env file. 18 | 5. Install all dependancies: 19 | `npm install` 20 | 6. Verify everything is working with a quick run of the tests. 21 | `npm test` 22 | 7. You should be good to get to work!👍 23 | 24 | ## The Developer's Code 25 | 26 | ### Code of Conduct 27 | 28 | In addition to the guidelines below, check out our [Code of Conduct](CODE_OF_CONDUCT.md). 29 | 30 | ### General Guidelines 31 | 32 | - Keep your Pull Requests Short (when possible) 33 | - Document your code, please. We know it was hard to code... but it doesn't have to be hard to read. 34 | - Test your code! Write some tests, please. They don't need to be fancy, we just want to know it works. 35 | - All commits need to be verified with a GPG signature. 36 | - We run ESLint & Prettier during every commit... so be mindful that our coding style guide may irritate you if you like 4 space tabs, and hate semicolons. 37 | 38 | ### New Features 39 | 40 | Members of the community, please feel welcome to fork a copy of the code and tackle any outstanding issues. We will make sure to tag some 'Good first issues' for anyone looking to get some practical coding experience (and we'll vouch for you if you throw us on your CV). That in mind, please follow the guidelines below for new feature submission. 41 | 42 | - All new features should be branched off the latest `develop` branch commit. 43 | - Feature branches should be prefixed with `feature/` and contain a brief description of the feature in the branch name 44 | - _Example: `feature/add-response-routes`_ 45 | - Once finished with a feature, create a PR on the develop branch. 46 | - Once the PR is submitted, we will take care of reviewing and merging to the appropriate branch(es). 47 | - If not already there... we will add you the list of contributors to the project. 48 | 49 | ### New Releases 50 | 51 | Only members of the core team will usually be creating release branches, but the following guidelines should help if the need arises for community assistance. 52 | 53 | - All new release should be branched off the latest `develop` branch commit. 54 | - Release branches should be prefixed with `release/` and contain the upcoming version number. 55 | - _Example: `release/1.5`_ 56 | - _Please don't use patch level version numbers, as those will be create from the respective hotfix branch._ 57 | - Any outstanding feature branches that will be included should be merged in to develop prior to creating the release branch. 58 | - As additional features and bugfixes are merged in to their respective branchs, make sure they find their way to the upcoming release branch as well. 59 | - When the time is ready. The release branch will be merged in to master with the appropriate release & tag. 60 | 61 | ### New Hotfixes (Bugfixes) 62 | 63 | There may be some issues tagged that are mission critical hotfixes. We will most likely tackle them rather quickly, but if you see one that you'd like to fix... go for it. Just follow these guidelines. 64 | 65 | - All new hotfixes should be branched off the latest `master` release/tag. **NOT the develop branch!** 66 | - Hotfix/Bugfix branches should be prefixed with `hotfix/` and contain a brief summary of the fix. 67 | - _Example: `hotfix/fix-create-survey-status-code`_ 68 | - These PRs should be kept small, ideally touching only a few lines of code. 69 | - We will take care of the rest from there! 😄 70 | 71 |

Thanks so much! 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Graham Vasquez. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async Airtable 2 | 3 | ## ARCHIVED 4 | 5 | I strongly suggest using the current official [Airtable SDK](https://github.com/Airtable/airtable.js) as it now supports promises and I don't really have time to maintain this anymore. 6 | 7 | [![Build: Tests](https://img.shields.io/github/workflow/status/GV14982/async-airtable/next?label=Next&logo=jest&logoColor=white&style=flat)](https://github.com/gv14982/async-airtable/actions) 8 | [![Build: Tests](https://img.shields.io/github/workflow/status/GV14982/async-airtable/main?label=Main&logo=jest&logoColor=white&style=flat)](https://github.com/gv14982/async-airtable/actions) 9 | [![npm](https://img.shields.io/npm/v/asyncairtable)](https://www.npmjs.com/package/asyncairtable) 10 | [![npm (tag)](https://img.shields.io/npm/v/asyncairtable/next)](https://www.npmjs.com/package/asyncairtable) 11 | [![MIT License](https://img.shields.io/github/license/GV14982/async-airtable?style=flat)](LICENSE.md) 12 | 13 | AsyncAirtable is a lightweight npm package to handle working with the [Airtable API](https://airtable.com/api). 14 | 15 | They have an existing library, but it is callback based and can get a little klunky at times, so I wrote this one that is promise based to make your life easier 😊. 16 | 17 | I also wrote a query builder so, instead of having to write those really annyoying [filter formula strings](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference#array_functions) you can just use an object like: 18 | 19 | ``` 20 | { 21 | where: { 22 | name: 'AsyncAirtable', 23 | $gte: {stars: 13} 24 | } 25 | } 26 | ``` 27 | 28 | which will generate the following filterFormula string for you: `AND({name} = 'AsyncAirtable', {stars} >= 13)` 29 | 30 | ## Requirements 31 | 32 | - NodeJS 33 | - npm 34 | - [Airtable account](https://airtable.com/signup) 35 | 36 | ## Installation 37 | 38 | - Be sure get your [API key](https://support.airtable.com/hc/en-us/articles/219046777-How-do-I-get-my-API-key-) 39 | 40 | - To get the base ID of your new base. You can do this by heading over to [Airtable's API page](https://airtable.com/api) and selecting that base from the list, you should see: 41 | 42 | > The ID of this base is BASE_ID 43 | 44 | - Install all dependancies: 45 | 46 | ``` 47 | npm install asyncairtable 48 | ``` 49 | 50 | Then you should be good to go!👍 51 | 52 | ## Browser 53 | 54 | If you want to use AsyncAirtable in a browser, please use the files in the `./dist` folder. There is a regular and a minified version. 55 | 56 | They are also available via [unpkg.com](https://unpkg.com/): 57 | 58 | - [Regular](https://unpkg.com/asyncairtable/dist/asyncAirtable.js) 59 | - [Minified](https://unpkg.com/asyncairtable/dist/asyncAirtable.min.js) 60 | 61 | ## Usage 62 | 63 | ```javascript 64 | const AsyncAirtable = require('async-airtable'); // or import { AsyncAirtable } from 'asyncairtable'; 65 | const asyncAirtable = new AsyncAirtable(API_KEY, BASE_ID, { ...CONFIG }); 66 | ``` 67 | 68 | ## Documentation 69 | 70 | To setup documentation run: 71 | `npm run doc` 72 | 73 | This will generate a _docs_ folder. Just open or serve _index.html_ and you will have the docs! 74 | 75 | You can also view them [online](https://asyncairtable.com). 76 | 77 | ## License 78 | 79 | [MIT](https://choosealicense.com/licenses/mit/) 80 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | globalSetup: './src/tests/globalSetup.ts', 7 | globalTeardown: './src/tests/globalTeardown.ts', 8 | verbose: true, 9 | testTimeout: parseInt(process.env.RETRY_TIMEOUT), 10 | preset: 'ts-jest', 11 | testEnvironment: 'node', 12 | collectCoverage: true, 13 | coverageDirectory: 'coverage', 14 | collectCoverageFrom: [ 15 | 'src/**/*.{js,ts}', 16 | '!/node_modules/', 17 | '!src/types/**/*', 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeAcquisition": { 3 | "enable": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncairtable", 3 | "version": "2.3.1", 4 | "description": "A lightweight, promisified airtable client", 5 | "main": "./lib/asyncAirtable.js", 6 | "types": "./lib/types/index.d.ts", 7 | "files": [ 8 | "dist/**/*", 9 | "lib/**/*", 10 | "LICENSE.md", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "test": "jest -i --no-cache --coverage", 15 | "docs": "typedoc", 16 | "check": "tsc --noEmit", 17 | "build": "npm run build:js && npm run build:browser && npm run build:browser-min", 18 | "build:js": "tsc", 19 | "build:browser": "webpack -c ./webpack.config.js", 20 | "build:browser-min": "webpack -c ./webpack.config.min.js", 21 | "lint": "eslint ./ --fix --cache", 22 | "format": "prettier ./ --write", 23 | "prepublishOnly": "npm run build && npm run lint" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/GV14982/async-airtable.git" 28 | }, 29 | "keywords": [ 30 | "async", 31 | "airtable", 32 | "promise", 33 | "asynchronus", 34 | "air", 35 | "table" 36 | ], 37 | "author": { 38 | "name": "Graham Vasquez", 39 | "url": "https://github.com/GV14982" 40 | }, 41 | "contributors": [ 42 | { 43 | "name": "Cas Ibrahim", 44 | "url": "https://github.com/mamacas" 45 | }, 46 | { 47 | "name": "Sean Metzgar", 48 | "url": "https://github.com/seanmetzgar" 49 | } 50 | ], 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/GV14982/async-airtable/issues" 54 | }, 55 | "homepage": "https://github.com/GV14982/async-airtable#readme", 56 | "dependencies": { 57 | "@types/node": "^14.18.18", 58 | "node-fetch": "^2.6.5" 59 | }, 60 | "devDependencies": { 61 | "@types/jest": "^27.0.0", 62 | "@types/node-fetch": "^2.5.12", 63 | "@typescript-eslint/eslint-plugin": "^4.33.0", 64 | "@typescript-eslint/parser": "^4.33.0", 65 | "dotenv": "^8.6.0", 66 | "eslint": "^7.32.0", 67 | "eslint-config-prettier": "^6.12.0", 68 | "eslint-plugin-jest": "^24.7.0", 69 | "husky": "^4.3.8", 70 | "jest": "^27.2.5", 71 | "lint-staged": "^10.5.4", 72 | "prettier": "^2.4.1", 73 | "ts-jest": "^27.0.0", 74 | "ts-loader": "^8.3.0", 75 | "ts-node": "^9.0.0", 76 | "typedoc": "^0.22.5", 77 | "typescript": "^4.6.4", 78 | "webpack": "^5.58.2", 79 | "webpack-cli": "^4.9.0" 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "pre-commit": "npx lint-staged" 84 | } 85 | }, 86 | "lint-staged": { 87 | "*.{ts,js}": "eslint --cache --fix", 88 | "*.{ts,js,md,json}": "prettier --write" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/arrayFunctions.ts: -------------------------------------------------------------------------------- 1 | import { queryBuilder } from './queryBuilder'; 2 | import { ArrayFunctions, QueryField } from './types'; 3 | 4 | export const arrayFunctions: ArrayFunctions = { 5 | $arrayCompact: (val: QueryField): string => { 6 | return `ARRAYCOMPACT(${queryBuilder(val)})`; 7 | }, 8 | $arrayFlatten: (val: QueryField): string => { 9 | return `ARRAYFLATTEN(${queryBuilder(val)})`; 10 | }, 11 | $arrayUnique: (val: QueryField): string => { 12 | return `ARRAYUNIQUE(${queryBuilder(val)})`; 13 | }, 14 | $arrayJoin: (val: QueryField, seperator = ','): string => { 15 | return `ARRAYJOIN(${queryBuilder(val)}, ${queryBuilder(seperator)})`; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/asyncAirtable.ts: -------------------------------------------------------------------------------- 1 | import buildOpts from './buildOpts'; 2 | import checkArg from './checkArg'; 3 | import { 4 | SelectOptions, 5 | AirtableDeletedResponse, 6 | AirtableRecord, 7 | AirtableRecordResponse, 8 | AirtableUpdateRecord, 9 | DeleteResponse, 10 | Fields, 11 | Config, 12 | queryBody, 13 | Typecast, 14 | updateOpts, 15 | bulkQueryBody, 16 | } from './types'; 17 | import { request } from './http'; 18 | 19 | /** @ignore */ 20 | declare global { 21 | interface Window { 22 | AsyncAirtable: typeof AsyncAirtable; 23 | } 24 | } 25 | 26 | const validOptions: string[] = [ 27 | 'fields', 28 | 'filterByFormula', 29 | 'maxRecords', 30 | 'pageSize', 31 | 'sort', 32 | 'view', 33 | 'where', 34 | ]; 35 | 36 | /** 37 | * The main AsyncAirtable class. 38 | */ 39 | export class AsyncAirtable { 40 | /** 41 | * @default=true 42 | * This decides whether or not the library will 43 | * handle retrying a request when rate limited 44 | */ 45 | retryOnRateLimit: boolean; 46 | /** 47 | * @default=60000 48 | * The maxmium amount of time before the 49 | * library will stop retrying and timeout when rate limited 50 | */ 51 | maxRetry: number; 52 | /** 53 | * @default=5000 54 | * The starting timeout for the retry. This will get 50% 55 | * larger with each try until you hit the maxRetry amount 56 | */ 57 | retryTimeout: number; 58 | /** The API Key from AirTable */ 59 | apiKey: string; 60 | /** The base id from AirTable */ 61 | base: string; 62 | /** the base URL to use when making API requests */ 63 | baseURL: string; 64 | 65 | /** 66 | * Creates a new instance of the AsyncAirtable library. 67 | * @param apiKey The API Key from AirTable 68 | * @param base The base id from AirTable 69 | * @param config The config to use for this instance of AsyncAirtable 70 | */ 71 | constructor(apiKey: string, base: string, config?: Config) { 72 | if (!apiKey) throw new Error('API Key is required.'); 73 | if (!base) throw new Error('Base ID is required.'); 74 | this.apiKey = apiKey; 75 | this.base = base; 76 | this.retryOnRateLimit = config?.retryOnRateLimit || true; 77 | this.retryTimeout = config?.retryTimeout || 5000; 78 | this.maxRetry = config?.maxRetry || 60000; 79 | this.baseURL = config?.baseURL || 'https://api.airtable.com/v0'; 80 | } 81 | 82 | /** 83 | * Select record(s) from the specified table. 84 | * @param table Table name 85 | * @param options Options object, used to filter records 86 | * @param page Used to get a specific page of records 87 | * @returns 88 | * @async 89 | */ 90 | select = async ( 91 | table: string, 92 | options?: SelectOptions, 93 | page?: number, 94 | ): Promise => { 95 | try { 96 | checkArg(table, 'table', 'string'); 97 | checkArg(options, 'options', 'object', false); 98 | checkArg(page, 'page', 'number', false); 99 | const url = `${this.baseURL}/${this.base}/${table}`; 100 | const opts: SelectOptions = options ? { ...options } : {}; 101 | Object.keys(opts).forEach((option) => { 102 | if (!validOptions.includes(option)) { 103 | throw new Error(`Invalid option: ${option}`); 104 | } 105 | }); 106 | let offset: string | undefined = ''; 107 | let data: AirtableRecord[] = []; 108 | if (page) { 109 | for (let i = 0; i < page; i++) { 110 | if (offset) { 111 | opts.offset = offset; 112 | } 113 | try { 114 | const body = await request({ 115 | endpoint: `${url}?${buildOpts(opts)}`, 116 | options: { headers: { Authorization: `Bearer ${this.apiKey}` } }, 117 | instance: this, 118 | pageHandler: { 119 | index: i, 120 | page: page, 121 | }, 122 | }); 123 | if (i + 1 === page) { 124 | return body.records; 125 | } 126 | offset = body.offset; 127 | } catch (err) { 128 | throw new Error(err); 129 | } 130 | } 131 | } else { 132 | let done = false; 133 | while (!done) { 134 | if (offset) { 135 | opts.offset = offset; 136 | } 137 | try { 138 | const body: AirtableRecordResponse = await request({ 139 | endpoint: `${url}?${buildOpts(opts)}`, 140 | options: { 141 | headers: { Authorization: `Bearer ${this.apiKey}` }, 142 | }, 143 | instance: this, 144 | }); 145 | data = data.concat(body.records); 146 | offset = body.offset; 147 | if (!body.offset) { 148 | done = true; 149 | } 150 | } catch (err) { 151 | throw new Error(err); 152 | } 153 | } 154 | } 155 | return data; 156 | } catch (err) { 157 | throw new Error(err); 158 | } 159 | }; 160 | 161 | /** 162 | * Finds a record on the specified table. 163 | * @param table Table name 164 | * @param id Airtable record ID 165 | * @returns 166 | * @async 167 | */ 168 | find = async (table: string, id: string): Promise => { 169 | try { 170 | checkArg(table, 'table', 'string'); 171 | checkArg(id, 'id', 'string'); 172 | const url = `${this.baseURL}/${this.base}/${table}/${id}`; 173 | const data: AirtableRecord = await request({ 174 | endpoint: url, 175 | options: { 176 | headers: { Authorization: `Bearer ${this.apiKey}` }, 177 | }, 178 | instance: this, 179 | }); 180 | return data; 181 | } catch (err) { 182 | throw new Error(err); 183 | } 184 | }; 185 | 186 | /** 187 | * Creates a new record on the specified table. 188 | * @param table - Table name 189 | * @param record - Record object, used to structure data for insert 190 | * @param typecast - Used for allowing the ability to add new selections for Select and Multiselect fields. 191 | * @returns 192 | * @async 193 | */ 194 | createRecord = async ( 195 | table: string, 196 | record: Fields, 197 | typecast?: Typecast, 198 | ): Promise => { 199 | try { 200 | checkArg(table, 'table', 'string'); 201 | checkArg(record, 'record', 'object'); 202 | checkArg(typecast, 'typecast', 'boolean', false); 203 | const url = `${this.baseURL}/${this.base}/${table}`; 204 | const body: queryBody = { fields: record }; 205 | if (typecast !== undefined) { 206 | body.typecast = typecast; 207 | } 208 | const data: AirtableRecord = await request({ 209 | endpoint: url, 210 | instance: this, 211 | options: { 212 | method: 'post', 213 | body: JSON.stringify(body), 214 | headers: { 215 | Authorization: `Bearer ${this.apiKey}`, 216 | 'Content-Type': 'application/json', 217 | }, 218 | }, 219 | }); 220 | return data; 221 | } catch (err) { 222 | throw new Error(err); 223 | } 224 | }; 225 | 226 | /** 227 | * Updates a record on the specified table. 228 | * @param table - Table name 229 | * @param record - Record object, used to update data within a specific record 230 | * @param opts - An object with options for your update statement 231 | * @returns 232 | * @async 233 | */ 234 | updateRecord = async ( 235 | table: string, 236 | record: AirtableUpdateRecord, 237 | opts?: updateOpts, 238 | ): Promise => { 239 | try { 240 | checkArg(table, 'table', 'string'); 241 | checkArg(record, 'record', 'object'); 242 | if (opts) { 243 | checkArg(opts.destructive, 'opts.desctructive', 'boolean'); 244 | checkArg(opts.typecast, 'opts.typecast', 'boolean', false); 245 | } 246 | const url = `${this.baseURL}/${this.base}/${table}/${record.id}`; 247 | const body: queryBody = { fields: record.fields }; 248 | if (opts?.typecast !== undefined) { 249 | body.typecast = opts?.typecast; 250 | } 251 | const data: AirtableRecord = await request({ 252 | endpoint: url, 253 | instance: this, 254 | options: { 255 | method: opts?.destructive ? 'put' : 'patch', 256 | body: JSON.stringify(body), 257 | headers: { 258 | Authorization: `Bearer ${this.apiKey}`, 259 | 'Content-Type': 'application/json', 260 | }, 261 | }, 262 | }); 263 | return data; 264 | } catch (err) { 265 | throw new Error(err); 266 | } 267 | }; 268 | 269 | /** 270 | * Deletes a record from the specified table. 271 | * @param table - Table name 272 | * @param id - Airtable record ID 273 | * @returns 274 | * @async 275 | */ 276 | deleteRecord = async (table: string, id: string): Promise => { 277 | try { 278 | checkArg(table, 'table', 'string'); 279 | checkArg(id, 'id', 'string'); 280 | const url = `${this.baseURL}/${this.base}/${table}/${id}`; 281 | const data: DeleteResponse = await request({ 282 | endpoint: url, 283 | instance: this, 284 | options: { 285 | method: 'delete', 286 | headers: { 287 | Authorization: `Bearer ${this.apiKey}`, 288 | }, 289 | }, 290 | }); 291 | return data; 292 | } catch (err) { 293 | throw new Error(err); 294 | } 295 | }; 296 | 297 | /** 298 | * Creates multiple new records on the specified table. 299 | * @param table - Table name 300 | * @param records - An array of Record objects 301 | * @param typecast - Used for allowing the ability to add new selections for Select and Multiselect fields. 302 | * @returns 303 | * @async 304 | */ 305 | bulkCreate = async ( 306 | table: string, 307 | records: Fields[], 308 | typecast?: Typecast, 309 | ): Promise => { 310 | try { 311 | checkArg(table, 'table', 'string'); 312 | checkArg(records, 'records', 'array'); 313 | checkArg(typecast, 'typecast', 'boolean', false); 314 | const url = `${this.baseURL}/${this.base}/${table}`; 315 | const body: bulkQueryBody = { 316 | records: records.map((record) => ({ 317 | fields: record, 318 | })), 319 | }; 320 | if (typecast !== undefined) { 321 | body.typecast = typecast; 322 | } 323 | const data: AirtableRecordResponse = await request({ 324 | endpoint: url, 325 | options: { 326 | method: 'post', 327 | body: JSON.stringify(body), 328 | headers: { 329 | Authorization: `Bearer ${this.apiKey}`, 330 | 'Content-Type': 'application/json', 331 | }, 332 | }, 333 | instance: this, 334 | }); 335 | return data.records; 336 | } catch (err) { 337 | throw new Error(err); 338 | } 339 | }; 340 | 341 | /** 342 | * Updates multiple records on the specified table 343 | * @param table - Table name 344 | * @param records - An array of Record objects 345 | * @param opts - An object with options for your update statement 346 | * @returns 347 | * @async 348 | */ 349 | bulkUpdate = async ( 350 | table: string, 351 | records: AirtableUpdateRecord[], 352 | opts?: updateOpts, 353 | ): Promise => { 354 | try { 355 | checkArg(table, 'table', 'string'); 356 | checkArg(records, 'records', 'array'); 357 | if (opts) { 358 | checkArg(opts.destructive, 'opts.desctructive', 'boolean', false); 359 | checkArg(opts.typecast, 'opts.typecast', 'boolean', false); 360 | } 361 | const url = `${this.baseURL}/${this.base}/${table}`; 362 | const body: bulkQueryBody = { records }; 363 | if (opts?.typecast !== undefined) { 364 | body.typecast = opts?.typecast; 365 | } 366 | const data: AirtableRecordResponse = await request({ 367 | endpoint: url, 368 | options: { 369 | method: opts?.destructive ? 'put' : 'patch', 370 | body: JSON.stringify(body), 371 | headers: { 372 | Authorization: `Bearer ${this.apiKey}`, 373 | 'Content-Type': 'application/json', 374 | }, 375 | }, 376 | instance: this, 377 | }); 378 | return data.records; 379 | } catch (err) { 380 | throw new Error(err); 381 | } 382 | }; 383 | 384 | /** 385 | * Deletes multiple records from the specified table 386 | * @param table - Table name 387 | * @param ids - Array of Airtable record IDs 388 | * @returns 389 | * @async 390 | */ 391 | bulkDelete = async ( 392 | table: string, 393 | ids: string[], 394 | ): Promise => { 395 | try { 396 | checkArg(table, 'table', 'string'); 397 | checkArg(ids, 'ids', 'array'); 398 | let query = ''; 399 | ids.forEach((id, i) => { 400 | if (i !== 0) { 401 | query = `${query}&records[]=${id}`; 402 | } else { 403 | query = `records[]=${id}`; 404 | } 405 | }); 406 | const url = `${this.baseURL}/${this.base}/${table}?${encodeURI(query)}`; 407 | const data: AirtableDeletedResponse = await request({ 408 | endpoint: url, 409 | options: { 410 | method: 'delete', 411 | headers: { 412 | Authorization: `Bearer ${this.apiKey}`, 413 | }, 414 | }, 415 | instance: this, 416 | }); 417 | return data.records; 418 | } catch (err) { 419 | throw new Error(err); 420 | } 421 | }; 422 | 423 | /** 424 | * Checks if a record exists, and if it does updates it, if not creates a new record. 425 | * @param table - Table name 426 | * @param filterString - The filter formula string used to check for a record 427 | * @param record - Record object used to either update or create a record 428 | * @param opts - An object with options for your update statement 429 | * @returns 430 | * @async 431 | */ 432 | upsertRecord = async ( 433 | table: string, 434 | filterString: string, 435 | record: Fields, 436 | opts?: updateOpts, 437 | ): Promise => { 438 | checkArg(table, 'table', 'string'); 439 | checkArg(filterString, 'filterString', 'string'); 440 | checkArg(record, 'record', 'object'); 441 | if (opts) { 442 | checkArg(opts.destructive, 'opts.desctructive', 'boolean', false); 443 | checkArg(opts.typecast, 'opts.typecast', 'boolean', false); 444 | } 445 | const exists = await this.select(table, { filterByFormula: filterString }); 446 | if (!exists[0]) { 447 | return await this.createRecord(table, record, opts?.typecast); 448 | } else { 449 | return await this.updateRecord( 450 | table, 451 | { 452 | id: exists[0].id, 453 | fields: record, 454 | }, 455 | opts, 456 | ); 457 | } 458 | }; 459 | } 460 | 461 | if (typeof window !== 'undefined') { 462 | window.AsyncAirtable = AsyncAirtable; 463 | } 464 | 465 | export const CREATED_TIME = (): string => 'CREATED_TIME()'; 466 | export const NOW = (): string => 'NOW()'; 467 | export const TODAY = (): string => 'TODAY()'; 468 | export const ERROR = (): string => 'ERROR()'; 469 | export const LAST_MODIFIED_TIME = (): string => 'LAST_MODIFIED_TIME()'; 470 | export const RECORD_ID = (): string => 'RECORD_ID()'; 471 | -------------------------------------------------------------------------------- /src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { BaseFieldType } from './types'; 2 | 3 | export const booleanHandler = (bool: boolean): string => { 4 | return bool ? 'TRUE()' : 'FALSE()'; 5 | }; 6 | 7 | export const baseHandler = (val: BaseFieldType): string => { 8 | if (val === null) { 9 | return 'BLANK()'; 10 | } 11 | switch (typeof val) { 12 | case 'number': 13 | return `${val}`; 14 | case 'string': 15 | return `'${val}'`; 16 | case 'boolean': 17 | return booleanHandler(val); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/buildExpression.ts: -------------------------------------------------------------------------------- 1 | import { ComparisonObject } from './types'; 2 | import { queryBuilder } from './queryBuilder'; 3 | 4 | export const operators = [ 5 | { $eq: '=' }, 6 | { $neq: '!=' }, 7 | { $gt: '>' }, 8 | { $gte: '>=' }, 9 | { $lt: '<' }, 10 | { $lte: '<=' }, 11 | { $add: '+' }, 12 | { $sub: '-' }, 13 | { $multi: '*' }, 14 | { $div: '/' }, 15 | ]; 16 | 17 | export const buildExpression = (obj: ComparisonObject, op: string): string => { 18 | if (typeof obj !== 'object' || Array.isArray(obj)) 19 | throw new Error('Missing or Invalid Comparison Object'); 20 | if ( 21 | typeof op !== 'string' && 22 | !operators.map((o) => Object.values(o)[0]).includes(op) 23 | ) 24 | throw new Error('Missing or Invalid Comparison Operator'); 25 | const keys = Object.keys(obj); 26 | const expressionMapper = (k: string, i: number) => { 27 | const val = queryBuilder(obj[k]); 28 | return `{${k}} ${op} ${val}${i < keys.length - 1 ? ', ' : ''}`; 29 | }; 30 | const exp = `${keys.map(expressionMapper).join('')}`; 31 | return keys.length > 1 ? `AND(${exp})` : exp; 32 | }; 33 | -------------------------------------------------------------------------------- /src/buildOpts.ts: -------------------------------------------------------------------------------- 1 | import { SelectOptions } from './types'; 2 | import { queryBuilder } from './queryBuilder'; 3 | 4 | export default (opts: SelectOptions): string => { 5 | if ( 6 | Object.prototype.hasOwnProperty.call(opts, 'filterByFormula') && 7 | Object.prototype.hasOwnProperty.call(opts, 'where') 8 | ) 9 | throw new Error( 10 | 'Cannot use both where and filterByFormula as they accomplish the same thing', 11 | ); 12 | 13 | const params = Object.keys(opts) 14 | .map((key: string, i) => { 15 | /** @todo Find a better type than any for this */ 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | const opt: any = opts[key as keyof SelectOptions]; 18 | let formatted; 19 | if (Array.isArray(opt)) { 20 | formatted = opt 21 | .map((item, j) => { 22 | switch (typeof item) { 23 | case 'object': 24 | return Object.keys(item) 25 | .map((subKey) => { 26 | return `${encodeURIComponent( 27 | `${key}[${j}][${subKey}]`, 28 | )}=${encodeURIComponent(item[subKey])}`; 29 | }) 30 | .join('&'); 31 | case 'string': 32 | return `${encodeURIComponent(key + '[]')}=${encodeURIComponent( 33 | item, 34 | )}`; 35 | } 36 | }) 37 | .join('&'); 38 | } else { 39 | if (key === 'where') { 40 | formatted = `filterByFormula=${encodeURIComponent( 41 | queryBuilder(opt), 42 | )}`; 43 | } else { 44 | formatted = `${key}=${encodeURIComponent(opt)}`; 45 | } 46 | } 47 | return i !== 0 ? `&${formatted}` : formatted; 48 | }) 49 | .join(''); 50 | return params; 51 | }; 52 | -------------------------------------------------------------------------------- /src/checkArg.ts: -------------------------------------------------------------------------------- 1 | import { Arg } from './types'; 2 | 3 | export default ( 4 | arg: Arg, 5 | name: string, 6 | type: string, 7 | required = true, 8 | ): void => { 9 | if (arg === undefined && required) 10 | throw new Error(`Argument "${name}" is required.`); 11 | if (arg === undefined && !required) return; 12 | if (typeof arg !== type) { 13 | if (type === 'array' && Array.isArray(arg)) return; 14 | throw new Error( 15 | `Incorrect data type on argument "${name}". Received "${typeof arg}": expected "${type}"`, 16 | ); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/checkError.ts: -------------------------------------------------------------------------------- 1 | export default (status: number): boolean => status >= 300; 2 | -------------------------------------------------------------------------------- /src/dateFunctions.ts: -------------------------------------------------------------------------------- 1 | import { handleError, queryBuilder } from './queryBuilder'; 2 | import { 3 | hasDoubleDateArg, 4 | isDateAddArg, 5 | isDateDiffArg, 6 | isDateFormatArg, 7 | isDateParseArg, 8 | isDateSameArg, 9 | isDateWeekArg, 10 | isDateWorkDayArg, 11 | isDateWorkDayDiffArg, 12 | isStringArray, 13 | isTextArg, 14 | } from './typeCheckers'; 15 | import { 16 | QueryField, 17 | DateAddFunc, 18 | DateDiffFunc, 19 | DateSameFunc, 20 | DateFormatFunc, 21 | DateLastModifiedFunc, 22 | DateParseFunc, 23 | DatePastFutureFuncs, 24 | DateWeekFuncs, 25 | DateWorkDayDiffFunc, 26 | DateWorkDayFunc, 27 | SingleArgDateFuncs, 28 | } from './types'; 29 | 30 | export const singleArgDateFuncs: SingleArgDateFuncs = { 31 | $dateStr: (date) => `DATESTR(${queryBuilder(date)})`, 32 | $day: (date) => `DAY(${queryBuilder(date)})`, 33 | $hour: (date) => `HOUR(${queryBuilder(date)})`, 34 | $minute: (date) => `MINUTE(${queryBuilder(date)})`, 35 | $month: (date) => `MONTH(${queryBuilder(date)})`, 36 | $second: (date) => `SECOND(${queryBuilder(date)})`, 37 | $timeStr: (date) => `TIMESTR(${queryBuilder(date)})`, 38 | $toNow: (date) => `TONOW(${queryBuilder(date)})`, 39 | $fromNow: (date) => `FROMNOW(${queryBuilder(date)})`, 40 | $year: (date) => `YEAR(${queryBuilder(date)})`, 41 | }; 42 | 43 | export const dateAddFunc: DateAddFunc = { 44 | $dateAdd: ({ date, count, units }) => 45 | `DATEADD(${queryBuilder(date)}, ${queryBuilder(count)}, '${units}')`, 46 | }; 47 | 48 | export const dateDiffFunc: DateDiffFunc = { 49 | $dateDiff: ({ date1, date2, units }) => 50 | `DATETIME_DIFF(${queryBuilder(date1)}, ${queryBuilder(date2)}, '${units}')`, 51 | }; 52 | 53 | export const dateSameFunc: DateSameFunc = { 54 | $dateSame: ({ date1, date2, units }) => 55 | `IS_SAME(${queryBuilder(date1)}, ${queryBuilder(date2)}${ 56 | units ? ", '" + units + "'" : '' 57 | })`, 58 | }; 59 | 60 | export const datePastFutureFuncs: DatePastFutureFuncs = { 61 | $dateBefore: ({ date1, date2 }) => 62 | `IS_BEFORE(${queryBuilder(date1)}, ${queryBuilder(date2)})`, 63 | $dateAfter: ({ date1, date2 }) => 64 | `IS_AFTER(${queryBuilder(date1)}, ${queryBuilder(date2)})`, 65 | }; 66 | 67 | export const dateFormatFunc: DateFormatFunc = { 68 | $dateFormat: ({ date, format }) => 69 | `DATETIME_FORMAT(${queryBuilder(date)}${ 70 | format ? ", '" + format + "'" : '' 71 | })`, 72 | }; 73 | 74 | export const dateParseFunc: DateParseFunc = { 75 | $dateParse: ({ date, format, locale }) => 76 | `DATETIME_PARSE(${queryBuilder(date)}${format ? ", '" + format + "'" : ''}${ 77 | locale ? ", '" + locale + "'" : '' 78 | })`, 79 | }; 80 | 81 | export const dateWeekFuncs: DateWeekFuncs = { 82 | $weekDay: ({ date, start }) => 83 | `WEEKDAY(${queryBuilder(date)}${start ? ", '" + start + "'" : ''})`, 84 | $weekNum: ({ date, start }) => 85 | `WEEKNUM(${queryBuilder(date)}${start ? ", '" + start + "'" : ''})`, 86 | }; 87 | 88 | export const dateWorkDayFunc: DateWorkDayFunc = { 89 | $workDay: ({ date, numDays, holidays }) => 90 | `WORKDAY(${queryBuilder(date)}, ${queryBuilder(numDays)}${ 91 | holidays ? ", '" + holidays.join("', '") + "'" : '' 92 | })`, 93 | }; 94 | 95 | export const dateWorkDayDiffFunc: DateWorkDayDiffFunc = { 96 | $workDayDiff: ({ date1, date2, holidays }) => 97 | `WORKDAY_DIFF(${queryBuilder(date1)}, ${queryBuilder(date2)}${ 98 | holidays ? ", '" + holidays.join("', '") + "'" : '' 99 | })`, 100 | }; 101 | 102 | export const lastModifiedFunc: DateLastModifiedFunc = { 103 | $lastModified: (arr) => 104 | `LAST_MODIFIED_TIME(${arr.map((field) => '{' + field + '}').join(', ')})`, 105 | }; 106 | 107 | export const dateFuncs = { 108 | ...singleArgDateFuncs, 109 | ...dateAddFunc, 110 | ...dateDiffFunc, 111 | ...dateSameFunc, 112 | ...dateFormatFunc, 113 | ...dateParseFunc, 114 | ...datePastFutureFuncs, 115 | ...dateWeekFuncs, 116 | ...dateWorkDayDiffFunc, 117 | ...dateWorkDayFunc, 118 | ...lastModifiedFunc, 119 | }; 120 | 121 | export const handleDateFunc = (key: string, val: QueryField): string => { 122 | if (key in singleArgDateFuncs && isTextArg(val)) { 123 | return singleArgDateFuncs[key](val); 124 | } else if (key in dateAddFunc && isDateAddArg(val)) { 125 | return dateAddFunc[key](val); 126 | } else if (key in dateDiffFunc && isDateDiffArg(val)) { 127 | return dateDiffFunc[key](val); 128 | } else if (key in dateSameFunc && isDateSameArg(val)) { 129 | return dateSameFunc[key](val); 130 | } else if (key in dateFormatFunc && isDateFormatArg(val)) { 131 | return dateFormatFunc[key](val); 132 | } else if (key in dateParseFunc && isDateParseArg(val)) { 133 | return dateParseFunc[key](val); 134 | } else if (key in datePastFutureFuncs && hasDoubleDateArg(val)) { 135 | return datePastFutureFuncs[key](val); 136 | } else if (key in dateWeekFuncs && isDateWeekArg(val)) { 137 | return dateWeekFuncs[key](val); 138 | } else if (key in dateWorkDayDiffFunc && isDateWorkDayDiffArg(val)) { 139 | return dateWorkDayDiffFunc[key](val); 140 | } else if (key in dateWorkDayFunc && isDateWorkDayArg(val)) { 141 | return dateWorkDayFunc[key](val); 142 | } else if (key in lastModifiedFunc && isStringArray(val)) { 143 | return lastModifiedFunc[key](val); 144 | } 145 | throw handleError({ key, val }); 146 | }; 147 | -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | import * as nodeFetch from 'node-fetch'; 2 | export default typeof window !== 'undefined' 3 | ? window.fetch.bind(window) 4 | : ((nodeFetch as unknown) as typeof fetch); 5 | -------------------------------------------------------------------------------- /src/http.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from './asyncAirtable'; 2 | import checkError from './checkError'; 3 | import fetch from './fetch'; 4 | import rateLimitHandler from './rateLimitHandler'; 5 | 6 | type Args = { 7 | endpoint: string; 8 | options?: RequestInit; 9 | instance: AsyncAirtable; 10 | pageHandler?: { 11 | index: number; 12 | page: number; 13 | }; 14 | }; 15 | 16 | export const request = async ({ 17 | endpoint, 18 | options, 19 | instance, 20 | pageHandler, 21 | }: Args): Promise => { 22 | try { 23 | const res: Response = await fetch(endpoint, options); 24 | const body = await res.json(); 25 | if (checkError(res.status)) { 26 | if (res.status !== 429) { 27 | throw new Error(JSON.stringify(body)); 28 | } 29 | 30 | if (instance.retryOnRateLimit) { 31 | if (!pageHandler || pageHandler.index + 1 === pageHandler.page) { 32 | const limit = await rateLimitHandler( 33 | endpoint, 34 | options ?? {}, 35 | instance.retryTimeout, 36 | instance.maxRetry, 37 | ); 38 | return limit; 39 | } 40 | } 41 | } 42 | return body; 43 | } catch (err) { 44 | throw new Error(err); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/logicalFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayExpressionFuncs, 3 | ExpressionFuncs, 4 | IfArgs, 5 | IfFunction, 6 | QueryField, 7 | SwitchFunction, 8 | } from './types'; 9 | import { handleError, queryBuilder } from './queryBuilder'; 10 | import { 11 | isQueryObjectArray, 12 | isQueryObject, 13 | isIfArgs, 14 | isSwitchArgs, 15 | } from './typeCheckers'; 16 | 17 | export const arrayArgFuncs: ArrayExpressionFuncs = { 18 | $and: (args: QueryField[]): string => `AND(${queryBuilder(args)})`, 19 | $or: (args: QueryField[]): string => `OR(${queryBuilder(args)})`, 20 | $xor: (args: QueryField[]): string => `XOR(${queryBuilder(args)})`, 21 | }; 22 | 23 | export const expressionFuncs: ExpressionFuncs = { 24 | $not: (expression: QueryField): string => `NOT(${queryBuilder(expression)})`, 25 | $isError: (expression: QueryField): string => 26 | `ISERROR(${queryBuilder(expression)})`, 27 | }; 28 | 29 | export const ifFunc: IfFunction = { 30 | $if: ({ expression, ifTrue, ifFalse }: IfArgs): string => 31 | `IF(${queryBuilder(expression)}, ${queryBuilder(ifTrue)}, ${queryBuilder( 32 | ifFalse, 33 | )})`, 34 | }; 35 | 36 | export const switchFunc: SwitchFunction = { 37 | $switch: ({ expression, cases, defaultVal }): string => 38 | `SWITCH(${queryBuilder(expression)}, ${cases 39 | .slice(0) 40 | .map( 41 | ({ switchCase, val }) => 42 | `${queryBuilder(switchCase)}, ${queryBuilder(val)}`, 43 | ) 44 | .join(', ')}, ${queryBuilder(defaultVal)})`, 45 | }; 46 | 47 | export const logicalFunctions = { 48 | ...arrayArgFuncs, 49 | ...expressionFuncs, 50 | ...ifFunc, 51 | ...switchFunc, 52 | }; 53 | 54 | export const handleLogicalFunc = (key: string, val: QueryField): string => { 55 | if (key in arrayArgFuncs && isQueryObjectArray(val)) { 56 | return arrayArgFuncs[key](val); 57 | } else if (key in expressionFuncs && isQueryObject(val)) { 58 | return expressionFuncs[key](val); 59 | } else if (key in ifFunc && isIfArgs(val)) { 60 | return ifFunc[key](val); 61 | } else if (key in switchFunc && isSwitchArgs(val)) { 62 | return switchFunc[key](val); 63 | } 64 | throw handleError({ key, val }); 65 | }; 66 | -------------------------------------------------------------------------------- /src/logicalOperators.ts: -------------------------------------------------------------------------------- 1 | import { LogicalOperators } from './types'; 2 | import { buildExpression } from './buildExpression'; 3 | 4 | export const logicalOperators: LogicalOperators = { 5 | $gt: (val) => { 6 | return buildExpression(val, '>'); 7 | }, 8 | $lt: (val) => { 9 | return buildExpression(val, '<'); 10 | }, 11 | $gte: (val) => { 12 | return buildExpression(val, '>='); 13 | }, 14 | $lte: (val) => { 15 | return buildExpression(val, '<='); 16 | }, 17 | $eq: (val) => { 18 | return buildExpression(val, '='); 19 | }, 20 | $neq: (val) => { 21 | return buildExpression(val, '!='); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/numericFunctions.ts: -------------------------------------------------------------------------------- 1 | import { buildExpression } from './buildExpression'; 2 | import { handleError, queryBuilder } from './queryBuilder'; 3 | import { 4 | isNumArgArray, 5 | isNumArg, 6 | isCeilFloorArg, 7 | isLogArg, 8 | isModArg, 9 | isPowerArg, 10 | isRoundArg, 11 | } from './typeCheckers'; 12 | import { QueryField } from './types'; 13 | import { 14 | ArrayArgNumFunctions, 15 | CeilFloorNumFunctions, 16 | LogNumFunction, 17 | ModNumFunction, 18 | NumericOperators, 19 | PowerNumFunction, 20 | RoundNumFunctions, 21 | SingleArgNumFunctions, 22 | } from './types/queryBuilder/numeric'; 23 | 24 | export const singleArgNumFunctions: SingleArgNumFunctions = { 25 | $abs: (arg) => `ABS(${queryBuilder(arg)})`, 26 | $even: (arg) => `EVEN(${queryBuilder(arg)})`, 27 | $exp: (arg) => `EXP(${queryBuilder(arg)})`, 28 | $int: (arg) => `INT(${queryBuilder(arg)})`, 29 | $odd: (arg) => `ODD(${queryBuilder(arg)})`, 30 | $sqrt: (arg) => `SQRT(${queryBuilder(arg)})`, 31 | }; 32 | 33 | export const arrayArgNumFunctions: ArrayArgNumFunctions = { 34 | $avg: (arg) => `AVERAGE(${arg.map((a) => queryBuilder(a)).join(', ')})`, 35 | $count: (arg) => `COUNT(${arg.map((a) => queryBuilder(a)).join(', ')})`, 36 | $counta: (arg) => `COUNTA(${arg.map((a) => queryBuilder(a)).join(', ')})`, 37 | $countAll: (arg) => `COUNTALL(${arg.map((a) => queryBuilder(a)).join(', ')})`, 38 | $max: (arg) => `MAX(${arg.map((a) => queryBuilder(a)).join(', ')})`, 39 | $min: (arg) => `MIN(${arg.map((a) => queryBuilder(a)).join(', ')})`, 40 | $sum: (arg) => `SUM(${arg.map((a) => queryBuilder(a)).join(', ')})`, 41 | }; 42 | 43 | export const ceilFloorNumFunctions: CeilFloorNumFunctions = { 44 | $ceil: ({ val, significance }) => 45 | `CEILING(${queryBuilder(val)}, ${queryBuilder(significance ?? 1)})`, 46 | $floor: ({ val, significance }) => 47 | `FLOOR(${queryBuilder(val)}, ${queryBuilder(significance ?? 1)})`, 48 | }; 49 | 50 | export const logNumFunction: LogNumFunction = { 51 | $log: ({ num, base }) => 52 | `LOG(${queryBuilder(num)}, ${queryBuilder(base ?? 10)})`, 53 | }; 54 | 55 | export const modNumFunction: ModNumFunction = { 56 | $mod: ({ val, divisor }) => 57 | `MOD(${queryBuilder(val)}, ${queryBuilder(divisor)})`, 58 | }; 59 | 60 | export const powerNumFunction: PowerNumFunction = { 61 | $pow: ({ base, power }) => 62 | `POWER(${queryBuilder(base)}, ${queryBuilder(power)})`, 63 | }; 64 | 65 | export const roundNumFunctions: RoundNumFunctions = { 66 | $round: ({ val, precision }) => 67 | `ROUND(${queryBuilder(val)}, ${queryBuilder(precision)})`, 68 | $roundDown: ({ val, precision }) => 69 | `ROUNDDOWN(${queryBuilder(val)}, ${queryBuilder(precision)})`, 70 | $roundUp: ({ val, precision }) => 71 | `ROUNDUP(${queryBuilder(val)}, ${queryBuilder(precision)})`, 72 | }; 73 | 74 | export const numericalFunctions = { 75 | ...singleArgNumFunctions, 76 | ...arrayArgNumFunctions, 77 | ...ceilFloorNumFunctions, 78 | ...logNumFunction, 79 | ...modNumFunction, 80 | ...powerNumFunction, 81 | ...roundNumFunctions, 82 | }; 83 | 84 | export const numericOperators: NumericOperators = { 85 | $add: (arg) => buildExpression(arg, '+'), 86 | $sub: (arg) => buildExpression(arg, '-'), 87 | $multi: (arg) => buildExpression(arg, '*'), 88 | $div: (arg) => buildExpression(arg, '/'), 89 | }; 90 | 91 | export const handleNumericalFunc = (key: string, val: QueryField): string => { 92 | if (key in arrayArgNumFunctions && isNumArgArray(val)) { 93 | return arrayArgNumFunctions[key](val); 94 | } else if (key in singleArgNumFunctions && isNumArg(val)) { 95 | return singleArgNumFunctions[key](val); 96 | } else if (key in ceilFloorNumFunctions && isCeilFloorArg(val)) { 97 | return ceilFloorNumFunctions[key](val); 98 | } else if (key in logNumFunction && isLogArg(val)) { 99 | return logNumFunction[key](val); 100 | } else if (key in modNumFunction && isModArg(val)) { 101 | return modNumFunction[key](val); 102 | } else if (key in powerNumFunction && isPowerArg(val)) { 103 | return powerNumFunction[key](val); 104 | } else if (key in roundNumFunctions && isRoundArg(val)) { 105 | return roundNumFunctions[key](val); 106 | } 107 | throw handleError({ key, val }); 108 | }; 109 | -------------------------------------------------------------------------------- /src/queryBuilder.ts: -------------------------------------------------------------------------------- 1 | import { QueryField, QueryObject } from './types'; 2 | import { arrayFunctions } from './arrayFunctions'; 3 | import { baseHandler } from './baseHandlers'; 4 | import { handleLogicalFunc, logicalFunctions } from './logicalFunctions'; 5 | import { logicalOperators } from './logicalOperators'; 6 | import { handleTextFunc, textFunctions } from './textFunctions'; 7 | import { 8 | allIndexesValid, 9 | isBaseField, 10 | isFunc, 11 | isJoinArgs, 12 | isQueryObject, 13 | isQueryObjectArray, 14 | isRegexArgs, 15 | isRegexReplaceArgs, 16 | isString, 17 | } from './typeCheckers'; 18 | import { regexFunctions, regexReplaceFunction } from './regexFunctions'; 19 | import { 20 | handleNumericalFunc, 21 | numericalFunctions, 22 | numericOperators, 23 | } from './numericFunctions'; 24 | 25 | export const operatorFunctions = { 26 | ...logicalOperators, 27 | ...numericOperators, 28 | }; 29 | 30 | export const handleError = (arg: QueryField): Error => 31 | new Error(`Invalid Query Object, ${JSON.stringify(arg)}`); 32 | 33 | export const queryBuilder = (arg: QueryField): string => { 34 | if (arg !== undefined) { 35 | if (isFunc(arg) && !isBaseField(arg) && !isQueryObject(arg)) { 36 | return arg(); 37 | } 38 | if (isBaseField(arg)) { 39 | return baseHandler(arg); 40 | } 41 | 42 | if (arg instanceof Array) { 43 | const str = arg.map((a: QueryField) => queryBuilder(a)).join(', '); 44 | return str.trim(); 45 | } 46 | 47 | const keys = Object.keys(arg); 48 | const vals = Object.values(arg); 49 | 50 | if ( 51 | keys.length > 1 && 52 | allIndexesValid(vals) && 53 | isQueryObjectArray(keys.map((k, i) => ({ [k]: vals[i] }))) 54 | ) { 55 | return logicalFunctions.$and( 56 | keys.map((k, i) => ({ [k]: vals[i] })) as QueryObject & QueryObject[], 57 | ); 58 | } 59 | 60 | const key = keys[0]; 61 | if (arg[key] === undefined) { 62 | throw new Error('Invalid query'); 63 | } 64 | 65 | const val = arg[key]; 66 | if (val !== undefined) { 67 | if (key === '$fieldName' && isString(val)) { 68 | return `{${val}}`; 69 | } 70 | 71 | if (key === '$insert' && isString(val)) { 72 | return val; 73 | } 74 | if (key in logicalFunctions) { 75 | return handleLogicalFunc(key, val); 76 | } else if (key in textFunctions) { 77 | return handleTextFunc(key, val); 78 | } else if (key in numericalFunctions) { 79 | return handleNumericalFunc(key, val); 80 | } else if (key in regexFunctions && isRegexArgs(val)) { 81 | return regexFunctions[key](val); 82 | } else if (key in regexReplaceFunction && isRegexReplaceArgs(val)) { 83 | return regexFunctions[key](val); 84 | } else if (key in arrayFunctions) { 85 | if (isJoinArgs(val)) { 86 | return arrayFunctions.$arrayJoin(val.val, val.separator); 87 | } else if (typeof val === 'string') { 88 | return arrayFunctions[key](val); 89 | } 90 | } else if (isQueryObject(val)) { 91 | const valKeys = Object.keys(val); 92 | const subVals = Object.values(val); 93 | 94 | if ( 95 | valKeys.length > 1 && 96 | allIndexesValid(subVals) && 97 | isQueryObjectArray(valKeys.map((k, i) => ({ [k]: subVals[i] }))) && 98 | subVals.every((v) => isQueryObject(v) || isBaseField(v)) 99 | ) { 100 | if (valKeys.every((k) => k in logicalOperators)) { 101 | return logicalFunctions.$and( 102 | valKeys.map((k, i) => ({ 103 | [key]: { [k]: subVals[i] }, 104 | })), 105 | ); 106 | } 107 | if (valKeys.every((k) => k in numericOperators)) { 108 | return ''; 109 | } 110 | } 111 | 112 | const valKey = valKeys[0]; 113 | const subVal = subVals[0]; 114 | if ( 115 | valKey in operatorFunctions && 116 | (isQueryObject(subVal) || isBaseField(subVal)) 117 | ) { 118 | return operatorFunctions[valKey]({ 119 | [key]: subVal, 120 | }); 121 | } 122 | } else if (isQueryObject(val) || isBaseField(val)) { 123 | return operatorFunctions.$eq({ [key]: val }); 124 | } 125 | } 126 | } 127 | throw handleError(arg); 128 | }; 129 | -------------------------------------------------------------------------------- /src/rateLimitHandler.ts: -------------------------------------------------------------------------------- 1 | import fetch from './fetch'; 2 | type RateLimitHandler = ( 3 | url: string, 4 | opts: RequestInit, 5 | retryTimeout: number, 6 | maxRetry: number, 7 | key?: string, 8 | ) => Promise; 9 | const rateLimitHandler: RateLimitHandler = async ( 10 | url: string, 11 | opts: RequestInit, 12 | retryTimeout: number, 13 | maxRetry: number, 14 | key?: string, 15 | /** @todo Find a better type than any for this */ 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | ) => { 18 | return new Promise((resolve, reject) => { 19 | const retryRateLimit = ( 20 | url: string, 21 | opts: RequestInit, 22 | retryTimeout: number, 23 | maxRetry: number, 24 | key?: string, 25 | ): void => { 26 | if (maxRetry && maxRetry < 1) { 27 | reject('Max timeout exceeded'); 28 | } 29 | setTimeout(async () => { 30 | try { 31 | const res = await fetch(url, opts); 32 | const data = await res.json(); 33 | if (res.status === 429) { 34 | retryRateLimit( 35 | url, 36 | opts, 37 | retryTimeout * 1.5, 38 | maxRetry - retryTimeout, 39 | key, 40 | ); 41 | } else { 42 | resolve(data); 43 | } 44 | } catch (err) { 45 | reject(err); 46 | } 47 | }, retryTimeout); 48 | }; 49 | 50 | retryRateLimit(url, opts, retryTimeout, maxRetry, key); 51 | }); 52 | }; 53 | export default rateLimitHandler; 54 | -------------------------------------------------------------------------------- /src/regexFunctions.ts: -------------------------------------------------------------------------------- 1 | import { queryBuilder } from './queryBuilder'; 2 | import { RegexFunctions, RegexReplaceFunction } from './types'; 3 | 4 | export const regexFunctions: RegexFunctions = { 5 | $regexMatch: ({ text, regex }) => 6 | `REGEX_MATCH(${queryBuilder(text)}, ${queryBuilder(regex)})`, 7 | $regexExtract: ({ text, regex }) => 8 | `REGEX_EXTRACT(${queryBuilder(text)}, ${queryBuilder(regex)})`, 9 | }; 10 | 11 | export const regexReplaceFunction: RegexReplaceFunction = { 12 | $regexReplace: ({ text, regex, replacement }) => 13 | `REGEX_REPLACE(${queryBuilder(text)}, ${queryBuilder( 14 | regex, 15 | )}, ${queryBuilder(replacement)})`, 16 | }; 17 | -------------------------------------------------------------------------------- /src/tests/bulkCreate.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let created: AirtableRecord; 10 | let deleteGroup: string[] = []; 11 | describe('.bulkCreate', () => { 12 | test('should create a new entry in the table with the given fields', async () => { 13 | const results = await asyncAirtable.bulkCreate( 14 | process.env.AIRTABLE_TABLE || '', 15 | [ 16 | JSON.parse(process.env.NEW_RECORD || ''), 17 | JSON.parse(process.env.NEW_RECORD || ''), 18 | ], 19 | ); 20 | expect(results).toBeDefined(); 21 | expect(Array.isArray(results)).toBe(true); 22 | expect(results.length).toBeGreaterThan(0); 23 | results.forEach((result) => { 24 | expect(result.id).toBeDefined(); 25 | expect(result.fields).toBeDefined(); 26 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 27 | expect(result.createdTime).toBeDefined(); 28 | }); 29 | created = results[0]; 30 | results.map((result) => { 31 | deleteGroup.push(result.id); 32 | }); 33 | }); 34 | 35 | test('should be able to find the record by the id after creation', async () => { 36 | const result = await asyncAirtable.find( 37 | process.env.AIRTABLE_TABLE || '', 38 | created.id, 39 | ); 40 | expect(result).toBeDefined(); 41 | expect(typeof result).toBe('object'); 42 | expect(Object.keys(result).length).toBeGreaterThan(0); 43 | expect(result.id).toBeDefined(); 44 | expect(result.fields).toBeDefined(); 45 | expect(result.createdTime).toBeDefined(); 46 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 47 | expect(JSON.stringify(result)).toEqual(JSON.stringify(created)); 48 | }); 49 | 50 | test('should throw an error if you do not pass a table', async () => { 51 | // @ts-ignore 52 | await expect(asyncAirtable.bulkCreate()).rejects.toThrowError( 53 | 'Argument "table" is required', 54 | ); 55 | }); 56 | 57 | test('should throw an error if you do not pass a record', async () => { 58 | await expect( 59 | // @ts-ignore 60 | asyncAirtable.bulkCreate(process.env.AIRTABLE_TABLE), 61 | ).rejects.toThrowError('Argument "records" is required'); 62 | }); 63 | 64 | test('should throw an error if pass a field that does not exist', async () => { 65 | await expect( 66 | asyncAirtable.bulkCreate(process.env.AIRTABLE_TABLE || '', [ 67 | { 68 | gringle: 'grangle', 69 | }, 70 | ]), 71 | ).rejects.toThrowError(/UNKNOWN_FIELD_NAME/g); 72 | }); 73 | 74 | test('should throw an error if pass a field with the incorrect data type', async () => { 75 | await expect( 76 | asyncAirtable.bulkCreate(process.env.AIRTABLE_TABLE || '', [ 77 | { ...JSON.parse(process.env.NEW_RECORD || ''), value: 'nope' }, 78 | ]), 79 | ).rejects.toThrowError(/INVALID_VALUE_FOR_COLUMN/g); 80 | }); 81 | 82 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 83 | await expect( 84 | // @ts-ignore 85 | asyncAirtable.bulkCreate(10, JSON.parse(process.env.NEW_RECORD)), 86 | ).rejects.toThrowError(/Incorrect data type/g); 87 | }); 88 | 89 | test('should throw an error if pass the record argument with an incorrect data type', async () => { 90 | await expect( 91 | // @ts-ignore 92 | asyncAirtable.bulkCreate(process.env.AIRTABLE_TABLE, 10), 93 | ).rejects.toThrowError(/Incorrect data type/g); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/tests/bulkDelete.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let deleteGroup: string[]; 10 | let deleteTest: AirtableRecord[] = []; 11 | describe('.bulkDelete', () => { 12 | beforeAll(async () => { 13 | const results = await asyncAirtable.select( 14 | process.env.AIRTABLE_TABLE || '', 15 | { view: 'Grid view' }, 16 | ); 17 | 18 | deleteGroup = results 19 | .slice(results.length - 4, results.length) 20 | .map((result) => result.id); 21 | 22 | const records = []; 23 | for (let i = 0; i < 10; i++) { 24 | records.push(JSON.parse(process.env.NEW_RECORD || '')); 25 | } 26 | for (let j = 0; j < parseInt(process.env.REQ_COUNT || '') / 10; j++) { 27 | const values = await asyncAirtable.bulkCreate( 28 | process.env.AIRTABLE_TABLE || '', 29 | records, 30 | ); 31 | deleteTest = [...deleteTest, ...values]; 32 | } 33 | }); 34 | 35 | test('should delete a record with the given id', async () => { 36 | const deleted = await asyncAirtable.bulkDelete( 37 | process.env.AIRTABLE_TABLE || '', 38 | deleteGroup, 39 | ); 40 | expect(deleted).toBeDefined(); 41 | expect(Array.isArray(deleted)).toBe(true); 42 | expect(deleted.length).toBeGreaterThan(0); 43 | deleted.forEach((del) => { 44 | expect(Object.keys(del).length).toBeGreaterThan(0); 45 | expect(del.deleted).toBeDefined(); 46 | expect(del.deleted).toBe(true); 47 | expect(del.id).toBeDefined(); 48 | expect(deleteGroup).toContain(del.id); 49 | }); 50 | }); 51 | 52 | test('should throw an error if you do not pass a table', async () => { 53 | // @ts-ignore 54 | await expect(asyncAirtable.bulkDelete()).rejects.toThrowError( 55 | 'Argument "table" is required', 56 | ); 57 | }); 58 | 59 | test('should throw an error if you do not pass an id', async () => { 60 | await expect( 61 | // @ts-ignore 62 | asyncAirtable.bulkDelete(process.env.AIRTABLE_TABLE || ''), 63 | ).rejects.toThrowError('Argument "ids" is required'); 64 | }); 65 | 66 | test('should throw an error if the id does not exist', async () => { 67 | await expect( 68 | asyncAirtable.bulkDelete(process.env.AIRTABLE_TABLE || '', [ 69 | 'doesnotexist', 70 | ]), 71 | ).rejects.toThrowError(/"INVALID_RECORDS"/g); 72 | }); 73 | 74 | test('should throw an error if the id has already been deleted', async () => { 75 | await expect( 76 | asyncAirtable.bulkDelete(process.env.AIRTABLE_TABLE || '', deleteGroup), 77 | ).rejects.toThrowError(/"NOT_FOUND"/g); 78 | }); 79 | 80 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 81 | await expect( 82 | // @ts-ignore 83 | asyncAirtable.bulkDelete(10, deleteGroup), 84 | ).rejects.toThrowError(/Incorrect data type/g); 85 | }); 86 | 87 | test('should throw an error if pass the id argument with an incorrect data type', async () => { 88 | await expect( 89 | // @ts-ignore 90 | asyncAirtable.bulkDelete(process.env.AIRTABLE_TABLE || '', 10), 91 | ).rejects.toThrowError(/Incorrect data type/g); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/tests/bulkUpdate.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let initResult: AirtableRecord[]; 10 | describe('.bulkUpdate', () => { 11 | beforeAll(async () => { 12 | initResult = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 13 | sort: [{ field: 'value', direction: 'asc' }], 14 | view: 'Grid view', 15 | }); 16 | initResult = initResult.slice(initResult.length - 7, initResult.length - 4); 17 | }); 18 | 19 | test('should update a record with provided data', async () => { 20 | const results = await asyncAirtable.bulkUpdate( 21 | process.env.AIRTABLE_TABLE || '', 22 | [ 23 | { 24 | id: initResult[0].id, 25 | fields: JSON.parse(process.env.BULK_UPDATE || ''), 26 | }, 27 | { 28 | id: initResult[1].id, 29 | fields: JSON.parse(process.env.BULK_UPDATE || ''), 30 | }, 31 | { 32 | id: initResult[2].id, 33 | fields: JSON.parse(process.env.BULK_UPDATE || ''), 34 | }, 35 | ], 36 | {}, 37 | ); 38 | expect(results).toBeDefined(); 39 | expect(Array.isArray(results)).toBe(true); 40 | expect(results.length).toBeGreaterThan(0); 41 | results.forEach((result) => { 42 | expect(result.id).toBeDefined(); 43 | expect(result.fields).toBeDefined(); 44 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 45 | expect(result.createdTime).toBeDefined(); 46 | }); 47 | results.forEach((result, i) => { 48 | expect(JSON.stringify(result)).not.toEqual(JSON.stringify(initResult[i])); 49 | }); 50 | }); 51 | 52 | test('should update a record with provided data and the destructive flag', async () => { 53 | const results = await asyncAirtable.bulkUpdate( 54 | process.env.AIRTABLE_TABLE || '', 55 | [ 56 | { 57 | id: initResult[0].id, 58 | fields: JSON.parse(process.env.DESTRUCTIVE_UPDATE_RECORD || ''), 59 | }, 60 | { 61 | id: initResult[1].id, 62 | fields: JSON.parse(process.env.DESTRUCTIVE_UPDATE_RECORD || ''), 63 | }, 64 | { 65 | id: initResult[2].id, 66 | fields: JSON.parse(process.env.DESTRUCTIVE_UPDATE_RECORD || ''), 67 | }, 68 | ], 69 | { 70 | destructive: true, 71 | }, 72 | ); 73 | expect(results).toBeDefined(); 74 | expect(Array.isArray(results)).toBe(true); 75 | expect(results.length).toBeGreaterThan(0); 76 | results.forEach((result) => { 77 | expect(result.id).toBeDefined(); 78 | expect(result.fields).toBeDefined(); 79 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 80 | expect(result.createdTime).toBeDefined(); 81 | }); 82 | results.forEach((result, i) => { 83 | expect(result).not.toHaveProperty('email'); 84 | }); 85 | }); 86 | 87 | test('should throw an error if you do not pass a table', async () => { 88 | // @ts-ignore 89 | await expect(asyncAirtable.bulkUpdate()).rejects.toThrowError( 90 | 'Argument "table" is required', 91 | ); 92 | }); 93 | 94 | test('should throw an error if you do not pass a record', async () => { 95 | await expect( 96 | // @ts-ignore 97 | asyncAirtable.bulkUpdate(process.env.AIRTABLE_TABLE || ''), 98 | ).rejects.toThrowError('Argument "records" is required'); 99 | }); 100 | 101 | test('should throw an error if pass a field that does not exist', async () => { 102 | await expect( 103 | asyncAirtable.bulkUpdate(process.env.AIRTABLE_TABLE || '', [ 104 | { 105 | id: initResult[0].id, 106 | fields: { gringle: 'grangle' }, 107 | }, 108 | ]), 109 | ).rejects.toThrowError(/UNKNOWN_FIELD_NAME/g); 110 | }); 111 | 112 | test('should throw an error if you send an incorrect id', async () => { 113 | await expect( 114 | asyncAirtable.bulkUpdate(process.env.AIRTABLE_TABLE || '', [ 115 | { id: 'doesnotexist', ...JSON.parse(process.env.BULK_UPDATE || '') }, 116 | ]), 117 | ).rejects.toThrowError(/INVALID_RECORDS/g); 118 | }); 119 | 120 | test('should throw an error if pass a field with the incorrect data type', async () => { 121 | await expect( 122 | asyncAirtable.bulkUpdate(process.env.AIRTABLE_TABLE || '', [ 123 | { 124 | id: initResult[0].id, 125 | fields: { 126 | ...JSON.parse(process.env.BULK_UPDATE || ''), 127 | value: 'nope', 128 | }, 129 | }, 130 | ]), 131 | ).rejects.toThrowError(/INVALID_VALUE_FOR_COLUMN/g); 132 | }); 133 | 134 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 135 | await expect( 136 | // @ts-ignore 137 | asyncAirtable.bulkUpdate(10, [ 138 | { 139 | id: initResult[0].id, 140 | fields: JSON.parse(process.env.BULK_UPDATE || ''), 141 | }, 142 | ]), 143 | ).rejects.toThrowError(/Incorrect data type/g); 144 | }); 145 | 146 | test('should throw an error if pass the record argument with an incorrect data type', async () => { 147 | await expect( 148 | // @ts-ignore 149 | asyncAirtable.bulkUpdate(process.env.AIRTABLE_TABLE || '', 10), 150 | ).rejects.toThrowError(/Incorrect data type/g); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/tests/createRecord.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let created: AirtableRecord; 10 | describe('.createRecord', () => { 11 | test('should create a new entry in the table with the given fields', async () => { 12 | const result = await asyncAirtable.createRecord( 13 | process.env.AIRTABLE_TABLE || '', 14 | JSON.parse(process.env.NEW_RECORD || ''), 15 | ); 16 | expect(result).toBeDefined(); 17 | expect(typeof result).toBe('object'); 18 | expect(Object.keys(result).length).toBeGreaterThan(0); 19 | expect(result.id).toBeDefined(); 20 | expect(result.fields).toBeDefined(); 21 | expect(result.createdTime).toBeDefined(); 22 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 23 | created = result; 24 | }); 25 | 26 | test('should be able to find the record by the id after creation', async () => { 27 | const result = await asyncAirtable.find( 28 | process.env.AIRTABLE_TABLE || '', 29 | created.id, 30 | ); 31 | expect(result).toBeDefined(); 32 | expect(typeof result).toBe('object'); 33 | expect(Object.keys(result).length).toBeGreaterThan(0); 34 | expect(result.id).toBeDefined(); 35 | expect(result.fields).toBeDefined(); 36 | expect(result.createdTime).toBeDefined(); 37 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 38 | expect(JSON.stringify(result)).toEqual(JSON.stringify(created)); 39 | }); 40 | 41 | test('should throw an error if you do not pass a table', async () => { 42 | // @ts-ignore 43 | await expect(asyncAirtable.createRecord()).rejects.toThrowError( 44 | 'Argument "table" is required', 45 | ); 46 | }); 47 | 48 | test('should throw an error if you do not pass a record', async () => { 49 | await expect( 50 | // @ts-ignore 51 | asyncAirtable.createRecord(process.env.AIRTABLE_TABLE || ''), 52 | ).rejects.toThrowError('Argument "record" is required'); 53 | }); 54 | 55 | test('should throw an error if pass a field that does not exist', async () => { 56 | await expect( 57 | asyncAirtable.createRecord(process.env.AIRTABLE_TABLE || '', { 58 | gringle: 'grangle', 59 | }), 60 | ).rejects.toThrowError(/UNKNOWN_FIELD_NAME/g); 61 | }); 62 | 63 | test('should throw an error if pass a field with the incorrect data type', async () => { 64 | await expect( 65 | asyncAirtable.createRecord(process.env.AIRTABLE_TABLE || '', { 66 | ...JSON.parse(process.env.NEW_RECORD || ''), 67 | value: 'nope', 68 | }), 69 | ).rejects.toThrowError(/INVALID_VALUE_FOR_COLUMN/g); 70 | }); 71 | 72 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 73 | await expect( 74 | asyncAirtable.createRecord( 75 | // @ts-ignore 76 | 10, 77 | JSON.parse(process.env.NEW_RECORD || ''), 78 | ), 79 | ).rejects.toThrowError(/Incorrect data type/g); 80 | }); 81 | 82 | test('should throw an error if pass the record argument with an incorrect data type', async () => { 83 | await expect( 84 | // @ts-ignore 85 | asyncAirtable.createRecord(process.env.AIRTABLE_TABLE || '', 10), 86 | ).rejects.toThrowError(/Incorrect data type/g); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/tests/deleteRecord.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let deleteMe: string; 10 | let deleteTest: AirtableRecord[] = []; 11 | describe('.deleteRecord', () => { 12 | beforeAll(async () => { 13 | const result = await asyncAirtable.select( 14 | process.env.AIRTABLE_TABLE || '', 15 | { maxRecords: 2, view: 'Grid view' }, 16 | ); 17 | deleteMe = result[1].id; 18 | const records = []; 19 | for (let i = 0; i < 10; i++) { 20 | records.push(JSON.parse(process.env.NEW_RECORD || '')); 21 | } 22 | for (let j = 0; j < parseInt(process.env.REQ_COUNT || '') / 10; j++) { 23 | const values = await asyncAirtable.bulkCreate( 24 | process.env.AIRTABLE_TABLE || '', 25 | records, 26 | ); 27 | deleteTest = [...deleteTest, ...values]; 28 | } 29 | }); 30 | 31 | test('should delete a record with the given id', async () => { 32 | const deleted = await asyncAirtable.deleteRecord( 33 | process.env.AIRTABLE_TABLE || '', 34 | deleteMe, 35 | ); 36 | expect(deleted).toBeDefined(); 37 | expect(typeof deleted).toBe('object'); 38 | expect(Object.keys(deleted).length).toBeGreaterThan(0); 39 | expect(deleted.deleted).toBeDefined(); 40 | expect(deleted.deleted).toBe(true); 41 | expect(deleted.id).toBeDefined(); 42 | expect(deleted.id).toBe(deleteMe); 43 | }); 44 | 45 | test('should throw an error if you do not pass a table', async () => { 46 | // @ts-ignore 47 | await expect(asyncAirtable.deleteRecord()).rejects.toThrowError( 48 | 'Argument "table" is required', 49 | ); 50 | }); 51 | 52 | test('should throw an error if you do not pass an id', async () => { 53 | await expect( 54 | // @ts-ignore 55 | asyncAirtable.deleteRecord(process.env.AIRTABLE_TABLE || ''), 56 | ).rejects.toThrowError('Argument "id" is required'); 57 | }); 58 | 59 | test('should throw an error if the id does not exist', async () => { 60 | await expect( 61 | //@ts-ignore 62 | asyncAirtable.deleteRecord( 63 | process.env.AIRTABLE_TABLE || '', 64 | 'doesnotexist', 65 | ), 66 | ).rejects.toThrowError(/"NOT_FOUND"/g); 67 | }); 68 | 69 | test('should throw an error if the id has already been deleted', async () => { 70 | await expect( 71 | //@ts-ignore 72 | asyncAirtable.deleteRecord(process.env.AIRTABLE_TABLE || '', deleteMe), 73 | ).rejects.toThrowError(/"Record not found"/g); 74 | }); 75 | 76 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 77 | await expect( 78 | // @ts-ignore 79 | asyncAirtable.deleteRecord(10, deleteMe), 80 | ).rejects.toThrowError(/Incorrect data type/g); 81 | }); 82 | 83 | test('should throw an error if pass the id argument with an incorrect data type', async () => { 84 | await expect( 85 | // @ts-ignore 86 | asyncAirtable.deleteRecord(process.env.AIRTABLE_TABLE || '', 10), 87 | ).rejects.toThrowError(/Incorrect data type/g); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/tests/find.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let firstResult: AirtableRecord; 10 | let secondResult: AirtableRecord; 11 | let compare: AirtableRecord; 12 | describe('.find', () => { 13 | beforeAll(async () => { 14 | const testResult = await asyncAirtable.select( 15 | process.env.AIRTABLE_TABLE || '', 16 | { 17 | maxRecords: 2, 18 | }, 19 | ); 20 | firstResult = testResult[0]; 21 | secondResult = testResult[1]; 22 | }); 23 | 24 | test('should find a specific record by Airtable ID', async () => { 25 | const result = await asyncAirtable.find( 26 | process.env.AIRTABLE_TABLE || '', 27 | firstResult.id, 28 | ); 29 | expect(result).toBeDefined(); 30 | expect(typeof result).toBe('object'); 31 | expect(Object.keys(result).length).toBeGreaterThan(0); 32 | expect(result.id).toBeDefined(); 33 | expect(result.fields).toBeDefined(); 34 | expect(result.createdTime).toBeDefined(); 35 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 36 | compare = result; 37 | }); 38 | 39 | test('should find a different specific record by Airtable ID', async () => { 40 | const result = await asyncAirtable.find( 41 | process.env.AIRTABLE_TABLE || '', 42 | secondResult.id, 43 | ); 44 | expect(result).toBeDefined(); 45 | expect(typeof result).toBe('object'); 46 | expect(Object.keys(result).length).toBeGreaterThan(0); 47 | expect(result.id).toBeDefined(); 48 | expect(result.fields).toBeDefined(); 49 | expect(result.createdTime).toBeDefined(); 50 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 51 | expect(JSON.stringify(result)).not.toEqual(JSON.stringify(compare)); 52 | }); 53 | 54 | test('should throw an error if you do not pass a table', async () => { 55 | // @ts-ignore 56 | await expect(asyncAirtable.find()).rejects.toThrowError( 57 | 'Argument "table" is required', 58 | ); 59 | }); 60 | 61 | test('should throw an error if the table does not exist', async () => { 62 | await expect( 63 | asyncAirtable.find('doesnotexist', firstResult.id), 64 | ).rejects.toThrowError(/"TABLE_NOT_FOUND"/g); 65 | }); 66 | 67 | test('should throw an error if you pass an incorrect data type for table', async () => { 68 | // @ts-ignore 69 | await expect(asyncAirtable.find(10)).rejects.toThrowError( 70 | /Incorrect data type/g, 71 | ); 72 | }); 73 | 74 | test('should throw an error if you do not pass an id', async () => { 75 | await expect( 76 | // @ts-ignore 77 | asyncAirtable.find(process.env.AIRTABLE_TABLE || ''), 78 | ).rejects.toThrowError('Argument "id" is required'); 79 | }); 80 | 81 | test('should throw an error if the id does not exist', async () => { 82 | await expect( 83 | asyncAirtable.find(process.env.AIRTABLE_TABLE || '', 'doesnotexist'), 84 | ).rejects.toThrowError(/"NOT_FOUND"/g); 85 | }); 86 | 87 | test('should throw an error if you pass an incorrect data type for table', async () => { 88 | await expect( 89 | // @ts-ignore 90 | asyncAirtable.find(process.env.AIRTABLE_TABLE || '', 10), 91 | ).rejects.toThrowError(/Incorrect data type/g); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/tests/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | config(); 3 | import * as d from './testData.json'; 4 | import { AsyncAirtable } from '../asyncAirtable'; 5 | import { AirtableRecord } from '../types'; 6 | const asyncAirtable = new AsyncAirtable( 7 | process.env.AIRTABLE_KEY || '', 8 | process.env.AIRTABLE_BASE || '', 9 | ); 10 | declare global { 11 | namespace NodeJS { 12 | interface Global { 13 | document: Document; 14 | window: Window; 15 | navigator: Navigator; 16 | AsyncAirtable: any; 17 | asyncAirtable: AsyncAirtable; 18 | } 19 | } 20 | } 21 | module.exports = async () => { 22 | let created: AirtableRecord[] = []; 23 | // @ts-ignore 24 | const data: AirtableRecord[] = d.default; 25 | for (let j = 0; j < 2; j++) { 26 | for (let i = 0; i < data.length; i += 10) { 27 | const records = data.slice(i, i + 10).map((record) => record.fields); 28 | const values = await asyncAirtable.bulkCreate( 29 | process.env.AIRTABLE_TABLE || '', 30 | records, 31 | ); 32 | created = created.concat(values); 33 | } 34 | } 35 | if (created.length == data.length * 2) { 36 | //eslint-disable-next-line 37 | console.log('\nTable seeded.🌱'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/tests/globalTeardown.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | config(); 3 | import { AsyncAirtable } from '../asyncAirtable'; 4 | import { AirtableRecord, DeleteResponse } from '../types'; 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | module.exports = async () => { 10 | const data: AirtableRecord[] = await asyncAirtable.select( 11 | process.env.AIRTABLE_TABLE || '', 12 | ); 13 | let deleted: DeleteResponse[] = []; 14 | for (let i = 0; i < data.length; i += 10) { 15 | const records = data.slice(i, i + 10).map((record) => record.id); 16 | const values = await asyncAirtable.bulkDelete( 17 | process.env.AIRTABLE_TABLE || '', 18 | records, 19 | ); 20 | deleted = deleted.concat(values); 21 | } 22 | if (deleted.length === data.length) { 23 | //eslint-disable-next-line 24 | console.log('Table cleared. 🧹'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/tests/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { baseHandler } from '../baseHandlers'; 2 | import buildOpts from '../buildOpts'; 3 | import checkArg from '../checkArg'; 4 | import checkError from '../checkError'; 5 | import { 6 | allIndexesValid, 7 | isBaseField, 8 | isIfArgs, 9 | isJoinArgs, 10 | isQueryObject, 11 | isQueryObjectArray, 12 | isRegexArgs, 13 | isRegexReplaceArgs, 14 | isStringOrFieldNameObject, 15 | isSwitchArgs, 16 | isTextArgArray, 17 | isTextDoubleArg, 18 | isTextMidArgs, 19 | isTextReplaceArgs, 20 | isTextSearchArgs, 21 | isTextSubArgs, 22 | } from '../typeCheckers'; 23 | 24 | describe('Helper Functions', () => { 25 | describe('buildOpts', () => { 26 | test('should return a URI encoded string from an object of select options', () => { 27 | expect( 28 | buildOpts({ 29 | fields: ['name', 'email', 'date'], 30 | filterByFormula: "{name} = 'Paul'", 31 | maxRecords: 50, 32 | pageSize: 10, 33 | sort: [ 34 | { 35 | field: 'name', 36 | direction: 'desc', 37 | }, 38 | { 39 | field: 'date', 40 | direction: 'asc', 41 | }, 42 | ], 43 | view: 'Grid view', 44 | }), 45 | ).toBe( 46 | "fields%5B%5D=name&fields%5B%5D=email&fields%5B%5D=date&filterByFormula=%7Bname%7D%20%3D%20'Paul'&maxRecords=50&pageSize=10&sort%5B0%5D%5Bfield%5D=name&sort%5B0%5D%5Bdirection%5D=desc&sort%5B1%5D%5Bfield%5D=date&sort%5B1%5D%5Bdirection%5D=asc&view=Grid%20view", 47 | ); 48 | 49 | expect( 50 | buildOpts({ 51 | fields: ['name', 'email', 'date'], 52 | where: { name: 'Paul' }, 53 | maxRecords: 50, 54 | pageSize: 10, 55 | sort: [ 56 | { 57 | field: 'name', 58 | direction: 'desc', 59 | }, 60 | { 61 | field: 'date', 62 | direction: 'asc', 63 | }, 64 | ], 65 | view: 'Grid view', 66 | }), 67 | ).toBe( 68 | "fields%5B%5D=name&fields%5B%5D=email&fields%5B%5D=date&filterByFormula=%7Bname%7D%20%3D%20'Paul'&maxRecords=50&pageSize=10&sort%5B0%5D%5Bfield%5D=name&sort%5B0%5D%5Bdirection%5D=desc&sort%5B1%5D%5Bfield%5D=date&sort%5B1%5D%5Bdirection%5D=asc&view=Grid%20view", 69 | ); 70 | }); 71 | 72 | test('should throw an error if both filterByFormula and where are used', () => { 73 | expect(() => { 74 | buildOpts({ 75 | where: { field: 'value' }, 76 | filterByFormula: '', 77 | }); 78 | }).toThrow( 79 | 'Cannot use both where and filterByFormula as they accomplish the same thing', 80 | ); 81 | }); 82 | }); 83 | 84 | describe('checkArgs', () => { 85 | test('should not throw an error if the argument is passed and matches the type', () => { 86 | expect(checkArg('test', 'test', 'string')).toBeUndefined(); 87 | expect(checkArg(10, 'test', 'number')).toBeUndefined(); 88 | expect(checkArg(['test'], 'test', 'array')).toBeUndefined(); 89 | expect(checkArg({ pageSize: 10 }, 'test', 'object')).toBeUndefined(); 90 | expect(checkArg(undefined, 'test', 'object', false)).toBeUndefined(); 91 | }); 92 | 93 | test('should throw an error if the argument is undefined and required or the wrong type', () => { 94 | expect(() => { 95 | //@ts-ignore 96 | checkArg(undefined, 'test', 'string'); 97 | }).toThrow(/Argument .+ is required/); 98 | 99 | expect(() => { 100 | //@ts-ignore 101 | checkArg(10, 'test', 'string'); 102 | }).toThrow( 103 | /Incorrect data type on argument .+\. Received .+: expected .+/, 104 | ); 105 | }); 106 | }); 107 | 108 | describe('Base Handlers', () => { 109 | expect(baseHandler('test')).toBe("'test'"); 110 | expect(baseHandler(9)).toBe('9'); 111 | expect(baseHandler(null)).toBe('BLANK()'); 112 | expect(baseHandler(true)).toBe('TRUE()'); 113 | expect(baseHandler(false)).toBe('FALSE()'); 114 | }); 115 | 116 | describe('checkErrors', () => { 117 | test('should return a boolean denoting if the status is in the 200 range', () => { 118 | expect(checkError(200)).toBe(false); 119 | expect(checkError(300)).toBe(true); 120 | }); 121 | }); 122 | 123 | describe('Type Checkers', () => { 124 | test('should return true if the val is a given type', () => { 125 | expect(isQueryObject({ test: true })).toBe(true); 126 | expect(isQueryObjectArray([{ test: true }])).toBe(true); 127 | expect(isTextArgArray(['test'])).toBe(true); 128 | expect(isStringOrFieldNameObject('test')).toBe(true); 129 | expect(isStringOrFieldNameObject({ $fieldName: 'test' })).toBe(true); 130 | expect(isJoinArgs({ val: 'test', separator: ':' })).toBe(true); 131 | expect( 132 | isTextSearchArgs({ stringToFind: 'test', whereToSearch: 'test' }), 133 | ).toBe(true); 134 | expect( 135 | isTextSearchArgs({ 136 | stringToFind: 'test', 137 | whereToSearch: 'test', 138 | index: 2, 139 | }), 140 | ).toBe(true); 141 | expect( 142 | isTextSubArgs({ text: 'test', oldText: 'test', newText: 'text' }), 143 | ).toBe(true); 144 | expect( 145 | isTextSubArgs({ 146 | text: 'test', 147 | oldText: 'test', 148 | newText: 'text', 149 | index: 2, 150 | }), 151 | ).toBe(true); 152 | expect( 153 | isTextReplaceArgs({ 154 | text: 'test', 155 | startChar: 0, 156 | numChars: 1, 157 | replacement: 'r', 158 | }), 159 | ).toBe(true); 160 | expect( 161 | isTextMidArgs({ 162 | text: 'test', 163 | whereToStart: 0, 164 | num: 1, 165 | }), 166 | ).toBe(true); 167 | expect( 168 | isTextDoubleArg({ 169 | text: 'test', 170 | num: 1, 171 | }), 172 | ).toBe(true); 173 | expect(allIndexesValid([{ test: 'test' }])).toBe(true); 174 | expect(isBaseField('test')).toBe(true); 175 | expect( 176 | isIfArgs({ 177 | expression: { field: { $lt: 10 } }, 178 | ifTrue: true, 179 | ifFalse: false, 180 | }), 181 | ).toBe(true); 182 | expect( 183 | isSwitchArgs({ 184 | expression: { $fieldName: 'test' }, 185 | cases: [ 186 | { 187 | switchCase: 'test', 188 | val: 'test', 189 | }, 190 | ], 191 | defaultVal: false, 192 | }), 193 | ).toBe(true); 194 | expect(isRegexArgs({ text: 'test', regex: 'test' })).toBe(true); 195 | expect( 196 | isRegexReplaceArgs({ 197 | text: 'test', 198 | regex: 'test', 199 | replacement: 'text', 200 | }), 201 | ).toBe(true); 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /src/tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { AirtableRecord } from '../types'; 3 | config(); 4 | import { AsyncAirtable } from '../asyncAirtable'; 5 | const requiredMethods = [ 6 | 'select', 7 | 'find', 8 | 'createRecord', 9 | 'updateRecord', 10 | 'deleteRecord', 11 | 'bulkCreate', 12 | 'bulkUpdate', 13 | 'bulkDelete', 14 | 'upsertRecord', 15 | ]; 16 | 17 | describe('asyncAirtable', () => { 18 | test('should instantiate a new AsyncAirtable instance with all required methods', () => { 19 | const asyncAirtable = new AsyncAirtable( 20 | process.env.AIRTABLE_KEY || '', 21 | process.env.AIRTABLE_BASE || '', 22 | { retryOnRateLimit: true }, 23 | ); 24 | 25 | const methods = Object.keys(asyncAirtable); 26 | requiredMethods.forEach((method) => { 27 | expect(methods.includes(method)).toBe(true); 28 | }); 29 | }); 30 | 31 | test('should throw an error if you instatiate without an API key', () => { 32 | expect(() => { 33 | // @ts-ignore 34 | new AsyncAirtable(); 35 | }).toThrowError('API Key is required.'); 36 | }); 37 | 38 | test('should throw an error if you instatiate without a base ID', () => { 39 | expect(() => { 40 | // @ts-ignore 41 | new AsyncAirtable(process.env.AIRTABLE_KEY || ''); 42 | }).toThrowError('Base ID is required.'); 43 | }); 44 | 45 | test('should retry if rate limited', async () => { 46 | const asyncAirtable = new AsyncAirtable( 47 | process.env.AIRTABLE_KEY || '', 48 | process.env.AIRTABLE_BASE || '', 49 | { retryOnRateLimit: true }, 50 | ); 51 | let results = []; 52 | for (let i = 0; i < parseInt(process.env.REQ_COUNT || '0'); i++) { 53 | results.push( 54 | asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 55 | maxRecords: 1, 56 | }), 57 | ); 58 | } 59 | const data: Array = await Promise.all(results); 60 | data.forEach((result) => { 61 | expect(result).toBeDefined(); 62 | expect(Array.isArray(result)).toBe(true); 63 | expect(result).toHaveLength(1); 64 | result.forEach((record) => { 65 | expect(record).toHaveProperty('id'); 66 | expect(record).toHaveProperty('fields'); 67 | expect(record).toHaveProperty('createdTime'); 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/tests/queryBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR, 3 | CREATED_TIME, 4 | RECORD_ID, 5 | NOW, 6 | TODAY, 7 | LAST_MODIFIED_TIME, 8 | } from '../asyncAirtable'; 9 | import { operatorFunctions, queryBuilder } from '../queryBuilder'; 10 | import { arrayFunctions } from '../arrayFunctions'; 11 | import { buildExpression, operators } from '../buildExpression'; 12 | import { 13 | arrayArgFuncs, 14 | expressionFuncs, 15 | ifFunc, 16 | switchFunc, 17 | } from '../logicalFunctions'; 18 | import { 19 | textSearchFunctions, 20 | textConcatFunction, 21 | textMidFunction, 22 | textReplacementFunction, 23 | textSubstituteFunction, 24 | textDoubleArgumentFunctions, 25 | textSingleArgumentFunctions, 26 | } from '../textFunctions'; 27 | import { isQueryObject } from '../typeCheckers'; 28 | import { regexFunctions, regexReplaceFunction } from '../regexFunctions'; 29 | import { 30 | arrayArgNumFunctions, 31 | ceilFloorNumFunctions, 32 | logNumFunction, 33 | modNumFunction, 34 | powerNumFunction, 35 | roundNumFunctions, 36 | singleArgNumFunctions, 37 | } from '../numericFunctions'; 38 | import { 39 | dateAddFunc, 40 | dateDiffFunc, 41 | dateFormatFunc, 42 | dateParseFunc, 43 | dateSameFunc, 44 | dateWeekFuncs, 45 | dateWorkDayDiffFunc, 46 | dateWorkDayFunc, 47 | lastModifiedFunc, 48 | singleArgDateFuncs, 49 | } from '../dateFunctions'; 50 | 51 | describe('Query Builder', () => { 52 | describe('isQueryObject', () => { 53 | test('should return true if the passed object is of type QueryObject', () => { 54 | expect( 55 | isQueryObject({ 56 | $or: [{ name: 'fred' }, { lt$: { coins: 10 } }], 57 | }), 58 | ).toBe(true); 59 | 60 | expect( 61 | isQueryObject({ 62 | email: null, 63 | }), 64 | ).toBe(true); 65 | }); 66 | 67 | test('should return false if the passed object is of type a string, number, boolean, or null', () => { 68 | expect(isQueryObject('NotQueryObject')).toBe(false); 69 | expect(isQueryObject(10)).toBe(false); 70 | expect(isQueryObject(true)).toBe(false); 71 | expect(isQueryObject(null)).toBe(false); 72 | }); 73 | 74 | test('should throw an error if passed an undefined value', () => { 75 | expect(() => { 76 | //@ts-ignore 77 | isQueryObject(undefined); 78 | }).toThrow('Missing Query Object'); 79 | }); 80 | }); 81 | 82 | describe('buildExpression', () => { 83 | test('should return a string when passed in a logical expression', () => { 84 | expect(buildExpression({ email: 'test@test.com' }, '=')).toBe( 85 | "{email} = 'test@test.com'", 86 | ); 87 | }); 88 | 89 | test('should return an AND string when passed in multiple logical expressions', () => { 90 | expect(buildExpression({ coins: 10, test: 20 }, '>=')).toBe( 91 | 'AND({coins} >= 10, {test} >= 20)', 92 | ); 93 | }); 94 | 95 | test('should throw an error if you pass an incorrect value for the comparison object', () => { 96 | expect(() => { 97 | //@ts-ignore 98 | buildExpression(['false'], '='); 99 | }).toThrow('Missing or Invalid Comparison Object'); 100 | }); 101 | 102 | test('should throw an error if you pass an incorrect value for the comparison operator', () => { 103 | expect(() => { 104 | //@ts-ignore 105 | buildExpression({ email: 'test@test.com' }, false); 106 | }).toThrow('Missing or Invalid Comparison Operator'); 107 | }); 108 | }); 109 | 110 | describe('Operators', () => { 111 | operators.forEach((op) => { 112 | test(`should return a string with the correct operator: ${ 113 | Object.values(op)[0] 114 | } for ${Object.keys(op)[0]}`, () => { 115 | expect(operatorFunctions[Object.keys(op)[0]]({ field: 10 })).toBe( 116 | `{field} ${Object.values(op)[0]} 10`, 117 | ); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('Logical Functions', () => { 123 | test('Should return a string wrapped in a NOT function', () => { 124 | expect( 125 | //@ts-ignore 126 | expressionFuncs.$not({ 127 | field: 'value', 128 | otherField: 10, 129 | }), 130 | ).toBe("NOT(AND({field} = 'value', {otherField} = 10))"); 131 | }); 132 | 133 | test('Should return a string wrapped in an AND function', () => { 134 | expect( 135 | //@ts-ignore 136 | arrayArgFuncs.$and([{ coins: { $lt: 10 } }, { name: 'fred' }]), 137 | ).toBe("AND({coins} < 10, {name} = 'fred')"); 138 | }); 139 | 140 | test('Should return a string wrapped in an OR function', () => { 141 | expect( 142 | //@ts-ignore 143 | arrayArgFuncs.$or([{ coins: { $lt: 10 } }, { name: 'fred' }]), 144 | ).toBe("OR({coins} < 10, {name} = 'fred')"); 145 | }); 146 | 147 | test('Should return a string wrapped in an XOR function', () => { 148 | expect( 149 | //@ts-ignore 150 | arrayArgFuncs.$xor([{ coins: { $lt: 10 } }, { name: 'fred' }]), 151 | ).toBe("XOR({coins} < 10, {name} = 'fred')"); 152 | }); 153 | 154 | test('Should return an isError function', () => { 155 | expect( 156 | //@ts-ignore 157 | expressionFuncs.$isError({ coins: { $lt: 10 } }), 158 | ).toBe('ISERROR({coins} < 10)'); 159 | }); 160 | 161 | test('Should return a string wrapped in an IF function', () => { 162 | expect( 163 | ifFunc.$if({ 164 | expression: { coins: { $lt: 10 } }, 165 | ifTrue: 'poor', 166 | ifFalse: 'rich', 167 | }), 168 | ).toBe("IF({coins} < 10, 'poor', 'rich')"); 169 | }); 170 | 171 | test('should return a string wrapped in a SWITCH function', () => { 172 | expect( 173 | switchFunc.$switch({ 174 | expression: { $fieldName: 'coins' }, 175 | cases: [ 176 | { 177 | switchCase: 9, 178 | val: 'nine', 179 | }, 180 | { 181 | switchCase: 10, 182 | val: 'ten', 183 | }, 184 | { 185 | switchCase: 11, 186 | val: 'eleven', 187 | }, 188 | ], 189 | defaultVal: null, 190 | }), 191 | ).toBe("SWITCH({coins}, 9, 'nine', 10, 'ten', 11, 'eleven', BLANK())"); 192 | }); 193 | }); 194 | 195 | describe('Array Functions', () => { 196 | test('should return the string with the specified method', () => { 197 | expect(arrayFunctions.$arrayCompact({ $fieldName: 'test' })).toBe( 198 | 'ARRAYCOMPACT({test})', 199 | ); 200 | expect(arrayFunctions.$arrayFlatten({ $fieldName: 'test' })).toBe( 201 | 'ARRAYFLATTEN({test})', 202 | ); 203 | expect(arrayFunctions.$arrayUnique({ $fieldName: 'test' })).toBe( 204 | 'ARRAYUNIQUE({test})', 205 | ); 206 | expect(arrayFunctions.$arrayJoin({ $fieldName: 'test' })).toBe( 207 | "ARRAYJOIN({test}, ',')", 208 | ); 209 | }); 210 | }); 211 | 212 | describe('Text Functions', () => { 213 | test('should return a string with the specified method', () => { 214 | expect( 215 | textSearchFunctions.$find({ 216 | stringToFind: { $fieldName: 'test' }, 217 | whereToSearch: 'test', 218 | }), 219 | ).toBe("FIND({test}, 'test', 0)"); 220 | expect( 221 | textSearchFunctions.$search({ 222 | stringToFind: { $fieldName: 'test' }, 223 | whereToSearch: 'test', 224 | }), 225 | ).toBe("SEARCH({test}, 'test', 0)"); 226 | expect( 227 | textSearchFunctions.$find({ 228 | stringToFind: 'test', 229 | whereToSearch: { $fieldName: 'test' }, 230 | }), 231 | ).toBe("FIND('test', {test}, 0)"); 232 | expect( 233 | textSearchFunctions.$search({ 234 | stringToFind: 'test', 235 | whereToSearch: { $fieldName: 'test' }, 236 | }), 237 | ).toBe("SEARCH('test', {test}, 0)"); 238 | expect( 239 | textSearchFunctions.$find({ 240 | stringToFind: 'test', 241 | whereToSearch: 'test', 242 | index: 2, 243 | }), 244 | ).toBe("FIND('test', 'test', 2)"); 245 | expect( 246 | textSearchFunctions.$search({ 247 | stringToFind: 'test', 248 | whereToSearch: 'test', 249 | index: 2, 250 | }), 251 | ).toBe("SEARCH('test', 'test', 2)"); 252 | expect(textConcatFunction.$concatenate(['test', 'test'])).toBe( 253 | "CONCATENATE('test', 'test')", 254 | ); 255 | expect( 256 | textConcatFunction.$concatenate([{ $fieldName: 'test' }, 'test']), 257 | ).toBe("CONCATENATE({test}, 'test')"); 258 | expect( 259 | textMidFunction.$mid({ text: 'test', whereToStart: 1, num: 2 }), 260 | ).toBe("MID('test', 1, 2)"); 261 | expect( 262 | textMidFunction.$mid({ 263 | text: { $fieldName: 'test' }, 264 | whereToStart: 1, 265 | num: 2, 266 | }), 267 | ).toBe('MID({test}, 1, 2)'); 268 | expect( 269 | textReplacementFunction.$replace({ 270 | text: 'test', 271 | startChar: 2, 272 | numChars: 1, 273 | replacement: 'x', 274 | }), 275 | ).toBe("REPLACE('test', 2, 1, 'x')"); 276 | expect( 277 | textSubstituteFunction.$substitute({ 278 | text: 'test', 279 | oldText: 's', 280 | newText: 'x', 281 | }), 282 | ).toBe("SUBSTITUTE('test', 's', 'x', 0)"); 283 | expect( 284 | textSubstituteFunction.$substitute({ 285 | text: 'test', 286 | oldText: 's', 287 | newText: 'x', 288 | index: 2, 289 | }), 290 | ).toBe("SUBSTITUTE('test', 's', 'x', 2)"); 291 | expect(textDoubleArgumentFunctions.$left({ text: 'test', num: 2 })).toBe( 292 | "LEFT('test', 2)", 293 | ); 294 | expect(textDoubleArgumentFunctions.$right({ text: 'test', num: 2 })).toBe( 295 | "RIGHT('test', 2)", 296 | ); 297 | expect(textDoubleArgumentFunctions.$rept({ text: 'test', num: 2 })).toBe( 298 | "REPT('test', 2)", 299 | ); 300 | expect(textSingleArgumentFunctions.$encodeUrlComponent('test')).toBe( 301 | "ENCODE_URL_COMPONENT('test')", 302 | ); 303 | expect(textSingleArgumentFunctions.$len('test')).toBe("LEN('test')"); 304 | expect(textSingleArgumentFunctions.$lower('test')).toBe("LOWER('test')"); 305 | expect(textSingleArgumentFunctions.$trim('test')).toBe("TRIM('test')"); 306 | expect(textSingleArgumentFunctions.$upper('test')).toBe("UPPER('test')"); 307 | }); 308 | }); 309 | 310 | describe('Regex Functions', () => { 311 | expect(regexFunctions.$regexExtract({ text: 'test', regex: 't.*t' })).toBe( 312 | "REGEX_EXTRACT('test', 't.*t')", 313 | ); 314 | expect(regexFunctions.$regexMatch({ text: 'test', regex: 't.*t' })).toBe( 315 | "REGEX_MATCH('test', 't.*t')", 316 | ); 317 | expect( 318 | regexReplaceFunction.$regexReplace({ 319 | text: 'test', 320 | regex: '.s', 321 | replacement: 'ex', 322 | }), 323 | ).toBe("REGEX_REPLACE('test', '.s', 'ex')"); 324 | }); 325 | 326 | describe('Numeric functions', () => { 327 | test('should return the correct string for the numeric function', () => { 328 | expect(singleArgNumFunctions.$abs(10)).toBe('ABS(10)'); 329 | expect(singleArgNumFunctions.$even(10)).toBe('EVEN(10)'); 330 | expect(singleArgNumFunctions.$exp(10)).toBe('EXP(10)'); 331 | expect(singleArgNumFunctions.$int(10)).toBe('INT(10)'); 332 | expect(singleArgNumFunctions.$odd(10)).toBe('ODD(10)'); 333 | expect(singleArgNumFunctions.$sqrt(10)).toBe('SQRT(10)'); 334 | 335 | expect(arrayArgNumFunctions.$avg([10, 11])).toBe('AVERAGE(10, 11)'); 336 | expect(arrayArgNumFunctions.$count([10, 11])).toBe('COUNT(10, 11)'); 337 | expect(arrayArgNumFunctions.$counta([10, 11])).toBe('COUNTA(10, 11)'); 338 | expect(arrayArgNumFunctions.$countAll([10, 11])).toBe('COUNTALL(10, 11)'); 339 | expect(arrayArgNumFunctions.$max([10, 11])).toBe('MAX(10, 11)'); 340 | expect(arrayArgNumFunctions.$min([10, 11])).toBe('MIN(10, 11)'); 341 | expect(arrayArgNumFunctions.$sum([10, 11])).toBe('SUM(10, 11)'); 342 | 343 | expect(ceilFloorNumFunctions.$ceil({ val: 10 })).toBe('CEILING(10, 1)'); 344 | expect(ceilFloorNumFunctions.$ceil({ val: 10, significance: 2 })).toBe( 345 | 'CEILING(10, 2)', 346 | ); 347 | expect(ceilFloorNumFunctions.$floor({ val: 10 })).toBe('FLOOR(10, 1)'); 348 | expect(ceilFloorNumFunctions.$floor({ val: 10, significance: 2 })).toBe( 349 | 'FLOOR(10, 2)', 350 | ); 351 | 352 | expect(logNumFunction.$log({ num: 10 })).toBe('LOG(10, 10)'); 353 | expect(logNumFunction.$log({ num: 10, base: 2 })).toBe('LOG(10, 2)'); 354 | 355 | expect(modNumFunction.$mod({ val: 10, divisor: 2 })).toBe('MOD(10, 2)'); 356 | expect(powerNumFunction.$pow({ base: 10, power: 2 })).toBe( 357 | 'POWER(10, 2)', 358 | ); 359 | 360 | expect(roundNumFunctions.$round({ val: 10, precision: 1 })).toBe( 361 | 'ROUND(10, 1)', 362 | ); 363 | expect(roundNumFunctions.$roundDown({ val: 10, precision: 1 })).toBe( 364 | 'ROUNDDOWN(10, 1)', 365 | ); 366 | expect(roundNumFunctions.$roundUp({ val: 10, precision: 1 })).toBe( 367 | 'ROUNDUP(10, 1)', 368 | ); 369 | }); 370 | }); 371 | 372 | describe('Date Functions', () => { 373 | test('should return a correct string for date function', () => { 374 | expect(singleArgDateFuncs.$dateStr({ $fieldName: 'date' })).toBe( 375 | 'DATESTR({date})', 376 | ); 377 | expect(singleArgDateFuncs.$day({ $fieldName: 'date' })).toBe( 378 | 'DAY({date})', 379 | ); 380 | expect(singleArgDateFuncs.$fromNow({ $fieldName: 'date' })).toBe( 381 | 'FROMNOW({date})', 382 | ); 383 | expect(singleArgDateFuncs.$hour({ $fieldName: 'date' })).toBe( 384 | 'HOUR({date})', 385 | ); 386 | expect(singleArgDateFuncs.$minute({ $fieldName: 'date' })).toBe( 387 | 'MINUTE({date})', 388 | ); 389 | expect(singleArgDateFuncs.$month({ $fieldName: 'date' })).toBe( 390 | 'MONTH({date})', 391 | ); 392 | expect(singleArgDateFuncs.$second({ $fieldName: 'date' })).toBe( 393 | 'SECOND({date})', 394 | ); 395 | expect(singleArgDateFuncs.$timeStr({ $fieldName: 'date' })).toBe( 396 | 'TIMESTR({date})', 397 | ); 398 | expect(singleArgDateFuncs.$toNow({ $fieldName: 'date' })).toBe( 399 | 'TONOW({date})', 400 | ); 401 | expect(singleArgDateFuncs.$year({ $fieldName: 'date' })).toBe( 402 | 'YEAR({date})', 403 | ); 404 | expect( 405 | dateAddFunc.$dateAdd({ 406 | date: { $fieldName: 'date' }, 407 | count: 10, 408 | units: 'days', 409 | }), 410 | ).toBe("DATEADD({date}, 10, 'days')"); 411 | expect( 412 | dateDiffFunc.$dateDiff({ 413 | date1: { $fieldName: 'date1' }, 414 | date2: { $fieldName: 'date2' }, 415 | units: 'days', 416 | }), 417 | ).toBe("DATETIME_DIFF({date1}, {date2}, 'days')"); 418 | expect( 419 | dateSameFunc.$dateSame({ 420 | date1: { $fieldName: 'date1' }, 421 | date2: { $fieldName: 'date2' }, 422 | units: 'days', 423 | }), 424 | ).toBe("IS_SAME({date1}, {date2}, 'days')"); 425 | expect( 426 | dateSameFunc.$dateSame({ 427 | date1: { $fieldName: 'date1' }, 428 | date2: { $fieldName: 'date2' }, 429 | }), 430 | ).toBe('IS_SAME({date1}, {date2})'); 431 | expect( 432 | dateFormatFunc.$dateFormat({ 433 | date: { $fieldName: 'date' }, 434 | format: 'YYYY-MM-DD', 435 | }), 436 | ).toBe("DATETIME_FORMAT({date}, 'YYYY-MM-DD')"); 437 | expect(dateFormatFunc.$dateFormat({ date: { $fieldName: 'date' } })).toBe( 438 | 'DATETIME_FORMAT({date})', 439 | ); 440 | expect( 441 | dateParseFunc.$dateParse({ 442 | date: { $fieldName: 'date' }, 443 | format: 'YYYY-MM-DD', 444 | locale: 'es', 445 | }), 446 | ).toBe("DATETIME_PARSE({date}, 'YYYY-MM-DD', 'es')"); 447 | expect( 448 | dateParseFunc.$dateParse({ 449 | date: { $fieldName: 'date' }, 450 | format: 'YYYY-MM-DD', 451 | }), 452 | ).toBe("DATETIME_PARSE({date}, 'YYYY-MM-DD')"); 453 | expect( 454 | dateParseFunc.$dateParse({ 455 | date: { $fieldName: 'date' }, 456 | }), 457 | ).toBe('DATETIME_PARSE({date})'); 458 | expect( 459 | dateWeekFuncs.$weekDay({ 460 | date: { $fieldName: 'date' }, 461 | start: 'Monday', 462 | }), 463 | ).toBe("WEEKDAY({date}, 'Monday')"); 464 | expect(dateWeekFuncs.$weekDay({ date: { $fieldName: 'date' } })).toBe( 465 | 'WEEKDAY({date})', 466 | ); 467 | expect( 468 | dateWeekFuncs.$weekNum({ 469 | date: { $fieldName: 'date' }, 470 | start: 'Monday', 471 | }), 472 | ).toBe("WEEKNUM({date}, 'Monday')"); 473 | expect(dateWeekFuncs.$weekNum({ date: { $fieldName: 'date' } })).toBe( 474 | 'WEEKNUM({date})', 475 | ); 476 | expect( 477 | dateWorkDayFunc.$workDay({ 478 | date: { $fieldName: 'date' }, 479 | numDays: 10, 480 | holidays: ['10/30/21'], 481 | }), 482 | ).toBe("WORKDAY({date}, 10, '10/30/21')"); 483 | expect( 484 | dateWorkDayFunc.$workDay({ 485 | date: { $fieldName: 'date' }, 486 | numDays: 10, 487 | holidays: ['10/30/21', '11/19/21'], 488 | }), 489 | ).toBe("WORKDAY({date}, 10, '10/30/21', '11/19/21')"); 490 | expect( 491 | dateWorkDayFunc.$workDay({ 492 | date: { $fieldName: 'date' }, 493 | numDays: 10, 494 | }), 495 | ).toBe('WORKDAY({date}, 10)'); 496 | expect( 497 | dateWorkDayDiffFunc.$workDayDiff({ 498 | date1: { $fieldName: 'date1' }, 499 | date2: { $fieldName: 'date2' }, 500 | holidays: ['10/30/21'], 501 | }), 502 | ).toBe("WORKDAY_DIFF({date1}, {date2}, '10/30/21')"); 503 | expect( 504 | dateWorkDayDiffFunc.$workDayDiff({ 505 | date1: { $fieldName: 'date1' }, 506 | date2: { $fieldName: 'date2' }, 507 | holidays: ['10/30/21', '11/19/21'], 508 | }), 509 | ).toBe("WORKDAY_DIFF({date1}, {date2}, '10/30/21', '11/19/21')"); 510 | expect( 511 | dateWorkDayDiffFunc.$workDayDiff({ 512 | date1: { $fieldName: 'date1' }, 513 | date2: { $fieldName: 'date2' }, 514 | }), 515 | ).toBe('WORKDAY_DIFF({date1}, {date2})'); 516 | expect(lastModifiedFunc.$lastModified(['date1', 'date2'])).toBe( 517 | 'LAST_MODIFIED_TIME({date1}, {date2})', 518 | ); 519 | }); 520 | }); 521 | 522 | describe('queryBuilder', () => { 523 | test('should return a filter formula string from a query object', () => { 524 | expect( 525 | queryBuilder({ 526 | field: { 527 | $lt: 10, 528 | $lte: 9, 529 | $gt: 5, 530 | $gte: 6, 531 | $eq: 7, 532 | $neq: 8, 533 | }, 534 | $not: { 535 | $or: [ 536 | { $and: [{ field1: 'yes' }, { field2: true }] }, 537 | { field3: false }, 538 | ], 539 | }, 540 | $if: { 541 | expression: { 542 | $isError: { 543 | $fieldName: 'field4', 544 | }, 545 | }, 546 | ifTrue: 'err', 547 | ifFalse: 'no err', 548 | }, 549 | $switch: { 550 | expression: { 551 | $fieldName: 'field1', 552 | }, 553 | cases: [ 554 | { 555 | switchCase: 'yes', 556 | val: true, 557 | }, 558 | { 559 | switchCase: 'no', 560 | val: false, 561 | }, 562 | { 563 | switchCase: 'maybe', 564 | val: null, 565 | }, 566 | ], 567 | defaultVal: ERROR, 568 | }, 569 | $xor: [ 570 | { 571 | field5: { 572 | $lt: 50, 573 | }, 574 | }, 575 | { 576 | field5: { 577 | $gt: 40, 578 | }, 579 | }, 580 | { 581 | field5: { 582 | $neq: 45, 583 | }, 584 | }, 585 | ], 586 | }), 587 | ).toBe( 588 | `AND(AND({field} < 10, {field} <= 9, {field} > 5, {field} >= 6, {field} = 7, {field} != 8), NOT(OR(AND({field1} = 'yes', {field2} = TRUE()), {field3} = FALSE())), IF(ISERROR({field4}), 'err', 'no err'), SWITCH({field1}, 'yes', TRUE(), 'no', FALSE(), 'maybe', BLANK(), ERROR()), XOR({field5} < 50, {field5} > 40, {field5} != 45))`, 589 | ); 590 | // @ts-ignore 591 | expect(() => queryBuilder({ test: undefined })).toThrowError( 592 | 'Invalid query', 593 | ); 594 | // @ts-ignore 595 | expect(() => queryBuilder({ test: () => 10 })).toThrowError( 596 | 'Invalid Query Object', 597 | ); 598 | }); 599 | }); 600 | }); 601 | -------------------------------------------------------------------------------- /src/tests/select.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let firstPage: AirtableRecord[]; 10 | 11 | const checkResult = ( 12 | result: AirtableRecord[], 13 | length?: number, 14 | fields?: boolean, 15 | ) => { 16 | expect(result).toBeDefined(); 17 | expect(Array.isArray(result)).toBe(true); 18 | if (length) expect(result).toHaveLength(length); 19 | result.forEach((item) => { 20 | expect(typeof item).toBe('object'); 21 | if (fields) expect(Object.keys(item.fields)).toHaveLength(1); 22 | }); 23 | }; 24 | 25 | describe('.select', () => { 26 | test('should respond with all entries without any options', async () => { 27 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || ''); 28 | checkResult(items); 29 | }); 30 | 31 | test('should respond with the first page of results using the pageSize option and adding a page argument', async () => { 32 | const items = await asyncAirtable.select( 33 | process.env.AIRTABLE_TABLE || '', 34 | { pageSize: 20 }, 35 | 1, 36 | ); 37 | checkResult(items, 20); 38 | firstPage = items; 39 | }); 40 | 41 | test('should respond with the second page by incrementing the page argument', async () => { 42 | const items = await asyncAirtable.select( 43 | process.env.AIRTABLE_TABLE || '', 44 | { pageSize: 20 }, 45 | 2, 46 | ); 47 | checkResult(items, 20); 48 | expect(JSON.stringify(items)).not.toEqual(JSON.stringify(firstPage)); 49 | }); 50 | 51 | test('should respond with a subset of records using the maxRecords option', async () => { 52 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 53 | maxRecords: 30, 54 | }); 55 | checkResult(items, 30); 56 | }); 57 | 58 | test('should respond with a sorted array when using the sort option', async () => { 59 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 60 | sort: [{ field: 'title' }], 61 | }); 62 | checkResult(items); 63 | }); 64 | 65 | test('should respond with only specific fields when using the fields option', async () => { 66 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 67 | fields: ['title'], 68 | }); 69 | checkResult(items, parseInt(process.env.NUM_RECORDS || ''), true); 70 | }); 71 | 72 | test('should respond with only specific records when using the filterByFormula option', async () => { 73 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 74 | filterByFormula: process.env.TEST_FILTER || '', 75 | }); 76 | checkResult(items); 77 | }); 78 | 79 | test('should respond with only specific records when using the where option', async () => { 80 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 81 | where: { 82 | email: 'same@test.com', 83 | }, 84 | }); 85 | checkResult(items); 86 | }); 87 | 88 | test('should respond with records in the format of the view specified.', async () => { 89 | const items = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 90 | view: 'Kanban', 91 | }); 92 | checkResult(items); 93 | }); 94 | 95 | test('should throw an error if you do not pass a table', async () => { 96 | // @ts-ignore 97 | await expect(asyncAirtable.select()).rejects.toThrowError( 98 | 'Argument "table" is required', 99 | ); 100 | }); 101 | 102 | test('should throw an error if the table does not exist', async () => { 103 | await expect(asyncAirtable.select('doesnotexist')).rejects.toThrowError( 104 | /"TABLE_NOT_FOUND"/g, 105 | ); 106 | }); 107 | 108 | test('should throw an error if you pass an incorrect data type for table', async () => { 109 | // @ts-ignore 110 | await expect(asyncAirtable.select(10)).rejects.toThrowError( 111 | /Incorrect data type/g, 112 | ); 113 | }); 114 | 115 | test('should throw an error if you pass an incorrect data type for options', async () => { 116 | await expect( 117 | // @ts-ignore 118 | asyncAirtable.select(process.env.AIRTABLE_TABLE || '', 10), 119 | ).rejects.toThrowError(/Incorrect data type/g); 120 | }); 121 | 122 | test('should throw an error if you pass in an invalid option', async () => { 123 | await expect( 124 | asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 125 | // @ts-ignore 126 | test: 'test', 127 | }), 128 | ).rejects.toThrowError('Invalid option: test'); 129 | }); 130 | 131 | test('should throw an error if you pass an incorrect data type for page', async () => { 132 | await expect( 133 | // @ts-ignore 134 | asyncAirtable.select(process.env.AIRTABLE_TABLE || '', {}, [10]), 135 | ).rejects.toThrowError(/Incorrect data type/g); 136 | }); 137 | test('should throw an error if you pass a table name that does not exist with a page as well', async () => { 138 | await expect( 139 | asyncAirtable.select('doesnotexist' || '', {}, 1), 140 | ).rejects.toThrowError(/NOT_FOUND/g); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/tests/testData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "email": "same@test.com", 5 | "value": 4, 6 | "title": "test-same-email-4" 7 | } 8 | }, 9 | { 10 | "fields": { 11 | "email": "new@test.com", 12 | "value": 32, 13 | "title": "test-select" 14 | } 15 | }, 16 | { 17 | "fields": { 18 | "email": "new@test.com", 19 | "value": 9, 20 | "title": "test-select" 21 | } 22 | }, 23 | { 24 | "fields": { 25 | "email": "find@test.com", 26 | "value": 17, 27 | "title": "test-find-1" 28 | } 29 | }, 30 | { 31 | "fields": { 32 | "email": "new@test.com", 33 | "value": 31, 34 | "title": "test-select" 35 | } 36 | }, 37 | { 38 | "fields": { 39 | "email": "new@test.com", 40 | "value": 22, 41 | "title": "test-select" 42 | } 43 | }, 44 | { 45 | "fields": { 46 | "email": "new@test.com", 47 | "value": 17, 48 | "title": "test-select" 49 | } 50 | }, 51 | { 52 | "fields": { 53 | "email": "new@test.com", 54 | "value": 36, 55 | "title": "test-select" 56 | } 57 | }, 58 | { 59 | "fields": { 60 | "email": "new@test.com", 61 | "value": 18, 62 | "title": "test-select" 63 | } 64 | }, 65 | { 66 | "fields": { 67 | "email": "new@test.com", 68 | "value": 14, 69 | "title": "test-select" 70 | } 71 | }, 72 | { 73 | "fields": { 74 | "email": "new@test.com", 75 | "value": 39, 76 | "title": "test-select" 77 | } 78 | }, 79 | { 80 | "fields": { 81 | "email": "update@test.com", 82 | "value": 23, 83 | "title": "test-update" 84 | } 85 | }, 86 | { 87 | "fields": { 88 | "email": "new@test.com", 89 | "value": 37, 90 | "title": "test-select" 91 | } 92 | }, 93 | { 94 | "fields": { 95 | "email": "new@test.com", 96 | "value": 15, 97 | "title": "test-select" 98 | } 99 | }, 100 | { 101 | "fields": { 102 | "email": "new@test.com", 103 | "value": 33, 104 | "title": "test-select" 105 | } 106 | }, 107 | { 108 | "fields": { 109 | "email": "new@test.com", 110 | "value": 24, 111 | "title": "test-select" 112 | } 113 | }, 114 | { 115 | "fields": { 116 | "email": "new@test.com", 117 | "value": 19, 118 | "title": "test-select" 119 | } 120 | }, 121 | { 122 | "fields": { 123 | "email": "new@test.com", 124 | "value": 38, 125 | "title": "test-select" 126 | } 127 | }, 128 | { 129 | "fields": { 130 | "email": "new@test.com", 131 | "value": 29, 132 | "title": "test-select" 133 | } 134 | }, 135 | { 136 | "fields": { 137 | "email": "new@test.com", 138 | "value": 4, 139 | "title": "test-select" 140 | } 141 | }, 142 | { 143 | "fields": { 144 | "email": "new@test.com", 145 | "value": 26, 146 | "title": "test-select" 147 | } 148 | }, 149 | { 150 | "fields": { 151 | "email": "new@test.com", 152 | "value": 6, 153 | "title": "test-select" 154 | } 155 | }, 156 | { 157 | "fields": { 158 | "email": "new@test.com", 159 | "value": 5, 160 | "title": "test-select" 161 | } 162 | }, 163 | { 164 | "fields": { 165 | "email": "new@test.com", 166 | "value": 1, 167 | "title": "test-select" 168 | } 169 | }, 170 | { 171 | "fields": { 172 | "email": "new@test.com", 173 | "value": 23, 174 | "title": "test-select" 175 | } 176 | }, 177 | { 178 | "fields": { 179 | "email": "bulky@test.com", 180 | "value": 0, 181 | "title": "test-bulk-update-1" 182 | } 183 | }, 184 | { 185 | "fields": { 186 | "email": "new@test.com", 187 | "value": 11, 188 | "title": "test-select" 189 | } 190 | }, 191 | { 192 | "fields": { 193 | "email": "new@test.com", 194 | "value": 40, 195 | "title": "test-select" 196 | } 197 | }, 198 | { 199 | "fields": { 200 | "email": "bulky@test.com", 201 | "value": 0, 202 | "title": "test-bulk-update-3" 203 | } 204 | }, 205 | { 206 | "fields": { 207 | "email": "new@test.com", 208 | "value": 35, 209 | "title": "test-select" 210 | } 211 | }, 212 | { 213 | "fields": { 214 | "email": "new@test.com", 215 | "value": 8, 216 | "title": "test-select" 217 | } 218 | }, 219 | { 220 | "fields": { 221 | "email": "new@test.com", 222 | "value": 21, 223 | "title": "test-select" 224 | } 225 | }, 226 | { 227 | "fields": { 228 | "email": "new@test.com", 229 | "value": 10, 230 | "title": "test-select" 231 | } 232 | }, 233 | { 234 | "fields": { 235 | "email": "new@test.com", 236 | "value": 25, 237 | "title": "test-select" 238 | } 239 | }, 240 | { 241 | "fields": { 242 | "email": "new@test.com", 243 | "value": 20, 244 | "title": "test-select" 245 | } 246 | }, 247 | { 248 | "fields": { 249 | "email": "same@test.com", 250 | "value": 1, 251 | "title": "test-same-email-1" 252 | } 253 | }, 254 | { 255 | "fields": { 256 | "email": "new@test.com", 257 | "value": 13, 258 | "title": "test-select" 259 | } 260 | }, 261 | { 262 | "fields": { 263 | "email": "new@test.com", 264 | "value": 27, 265 | "title": "test-select" 266 | } 267 | }, 268 | { 269 | "fields": { 270 | "email": "new@test.com", 271 | "value": 3, 272 | "title": "test-select" 273 | } 274 | }, 275 | { 276 | "fields": { 277 | "email": "new@test.com", 278 | "value": 16, 279 | "title": "test-select" 280 | } 281 | }, 282 | { 283 | "fields": { 284 | "email": "new@test.com", 285 | "value": 34, 286 | "title": "test-select" 287 | } 288 | }, 289 | { 290 | "fields": { 291 | "email": "new@test.com", 292 | "value": 30, 293 | "title": "test-select" 294 | } 295 | }, 296 | { 297 | "fields": { 298 | "email": "new@test.com", 299 | "value": 12, 300 | "title": "test-select" 301 | } 302 | }, 303 | { 304 | "fields": { 305 | "email": "new@test.com", 306 | "value": 2, 307 | "title": "test-select" 308 | } 309 | }, 310 | { 311 | "fields": { 312 | "email": "same@test.com", 313 | "value": 3, 314 | "title": "test-same-email-3" 315 | } 316 | }, 317 | { 318 | "fields": { 319 | "email": "new@test.com", 320 | "value": 28, 321 | "title": "test-select" 322 | } 323 | }, 324 | { 325 | "fields": { 326 | "email": "bulky@test.com", 327 | "value": 0, 328 | "title": "test-bulk-update-4" 329 | } 330 | }, 331 | { 332 | "fields": { 333 | "value": 23, 334 | "title": "test-destructive", 335 | "email": "destructive@test.com" 336 | } 337 | }, 338 | { 339 | "fields": { 340 | "email": "bulky@test.com", 341 | "value": 0, 342 | "title": "test-bulk-update-2" 343 | } 344 | }, 345 | { 346 | "fields": { 347 | "email": "same@test.com", 348 | "value": 2, 349 | "title": "test-same-email-2" 350 | } 351 | }, 352 | { 353 | "fields": { 354 | "email": "new@test.com", 355 | "value": 7, 356 | "title": "test-select" 357 | } 358 | }, 359 | { 360 | "fields": { 361 | "email": "find@test.com", 362 | "value": 13, 363 | "title": "test-find-2" 364 | } 365 | }, 366 | { 367 | "fields": { 368 | "email": "upsert@test.com", 369 | "value": 13, 370 | "title": "test-upsert" 371 | } 372 | }, 373 | { 374 | "fields": { 375 | "email": "upsert@test.com", 376 | "value": 13, 377 | "title": "test-upsert-2" 378 | } 379 | } 380 | ] 381 | -------------------------------------------------------------------------------- /src/tests/updateRecord.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let initResult: AirtableRecord[]; 10 | describe('.updateRecord', () => { 11 | beforeAll(async () => { 12 | initResult = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 13 | maxRecords: 2, 14 | sort: [{ field: 'value', direction: 'desc' }], 15 | view: 'Grid view', 16 | }); 17 | }); 18 | 19 | test('should update a record with provided data', async () => { 20 | const result = await asyncAirtable.updateRecord( 21 | process.env.AIRTABLE_TABLE || '', 22 | { 23 | id: initResult[0].id, 24 | fields: JSON.parse(process.env.UPDATE_RECORD || ''), 25 | }, 26 | ); 27 | expect(result).toBeDefined(); 28 | expect(typeof result).toBe('object'); 29 | expect(Object.keys(result).length).toBeGreaterThan(0); 30 | expect(result.id).toBeDefined(); 31 | expect(result.fields).toBeDefined(); 32 | expect(result.createdTime).toBeDefined(); 33 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 34 | expect(JSON.stringify(result)).not.toEqual(JSON.stringify(initResult[0])); 35 | initResult[0] = result; 36 | }); 37 | 38 | test('should update a record and set unprovided field to null if you pass in the destructive arg', async () => { 39 | const result = await asyncAirtable.updateRecord( 40 | process.env.AIRTABLE_TABLE || '', 41 | { 42 | id: initResult[0].id, 43 | fields: JSON.parse(process.env.DESTRUCTIVE_UPDATE_RECORD || ''), 44 | }, 45 | { destructive: true }, 46 | ); 47 | expect(result).toBeDefined(); 48 | expect(typeof result).toBe('object'); 49 | expect(Object.keys(result).length).toBeGreaterThan(0); 50 | expect(result.id).toBeDefined(); 51 | expect(result.fields).toBeDefined(); 52 | expect(result.createdTime).toBeDefined(); 53 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 54 | expect(JSON.stringify(result)).not.toEqual(JSON.stringify(initResult[0])); 55 | expect(result).not.toHaveProperty('email'); 56 | }); 57 | 58 | test('should throw an error if you do not pass a table', async () => { 59 | // @ts-ignore 60 | await expect(asyncAirtable.updateRecord()).rejects.toThrowError( 61 | 'Argument "table" is required', 62 | ); 63 | }); 64 | 65 | test('should throw an error if you do not pass a record', async () => { 66 | await expect( 67 | // @ts-ignore 68 | asyncAirtable.updateRecord(process.env.AIRTABLE_TABLE || ''), 69 | ).rejects.toThrowError('Argument "record" is required'); 70 | }); 71 | 72 | test('should throw an error if you pass a field that does not exist', async () => { 73 | await expect( 74 | asyncAirtable.updateRecord(process.env.AIRTABLE_TABLE || '', { 75 | id: initResult[0].id, 76 | //@ts-ignore 77 | fields: { gringle: 'grangle' }, 78 | }), 79 | ).rejects.toThrowError(/UNKNOWN_FIELD_NAME/g); 80 | }); 81 | 82 | test('should throw an error if you send an incorrect id', async () => { 83 | await expect( 84 | asyncAirtable.updateRecord(process.env.AIRTABLE_TABLE || '', { 85 | id: 'doesnotexist', 86 | fields: JSON.parse(process.env.UPDATE_RECORD || ''), 87 | }), 88 | ).rejects.toThrowError(/NOT_FOUND/g); 89 | }); 90 | 91 | test('should throw an error if pass a field with the incorrect data type', async () => { 92 | await expect( 93 | asyncAirtable.updateRecord(process.env.AIRTABLE_TABLE || '', { 94 | id: initResult[0].id, 95 | fields: { 96 | ...JSON.parse(process.env.UPDATE_RECORD || ''), 97 | value: 'nope', 98 | }, 99 | }), 100 | ).rejects.toThrowError(/INVALID_VALUE_FOR_COLUMN/g); 101 | }); 102 | 103 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 104 | await expect( 105 | // @ts-ignore 106 | asyncAirtable.updateRecord(10, { 107 | id: initResult[0].id, 108 | fields: JSON.parse(process.env.UPDATE_RECORD || ''), 109 | }), 110 | ).rejects.toThrowError(/Incorrect data type/g); 111 | }); 112 | 113 | test('should throw an error if pass the record argument with an incorrect data type', async () => { 114 | await expect( 115 | //@ts-ignore 116 | asyncAirtable.updateRecord(process.env.AIRTABLE_TABLE || '', 10), 117 | ).rejects.toThrowError(/Incorrect data type/g); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/tests/upsert.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAirtable } from '../asyncAirtable'; 2 | import { AirtableRecord } from '../types'; 3 | import { config } from 'dotenv'; 4 | config(); 5 | const asyncAirtable = new AsyncAirtable( 6 | process.env.AIRTABLE_KEY || '', 7 | process.env.AIRTABLE_BASE || '', 8 | ); 9 | let initResult: AirtableRecord[]; 10 | describe('.upsertRecord', () => { 11 | beforeAll(async () => { 12 | initResult = await asyncAirtable.select(process.env.AIRTABLE_TABLE || '', { 13 | filterByFormula: "{title} = 'test-find'", 14 | maxRecords: 1, 15 | sort: [{ field: 'value', direction: 'asc' }], 16 | view: 'Grid view', 17 | }); 18 | }); 19 | 20 | test('should update a record with provided data if it exists', async () => { 21 | const result = await asyncAirtable.upsertRecord( 22 | process.env.AIRTABLE_TABLE || '', 23 | "{title} = 'test-upsert'", 24 | JSON.parse(process.env.UPDATE_RECORD || ''), 25 | ); 26 | expect(result).toBeDefined(); 27 | expect(typeof result).toBe('object'); 28 | expect(Object.keys(result).length).toBeGreaterThan(0); 29 | expect(result.id).toBeDefined(); 30 | expect(result.fields).toBeDefined(); 31 | expect(result.createdTime).toBeDefined(); 32 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 33 | expect(JSON.stringify(result)).not.toEqual(JSON.stringify(initResult[0])); 34 | initResult[0] = result; 35 | }); 36 | 37 | test('should update a record with provided data desctructively if it exists', async () => { 38 | const result = await asyncAirtable.upsertRecord( 39 | process.env.AIRTABLE_TABLE || '', 40 | "{title} = 'test-upsert'", 41 | JSON.parse(process.env.DESTRUCTIVE_UPDATE_RECORD || ''), 42 | { destructive: true }, 43 | ); 44 | expect(result).toBeDefined(); 45 | expect(typeof result).toBe('object'); 46 | expect(Object.keys(result).length).toBeGreaterThan(0); 47 | expect(result.id).toBeDefined(); 48 | expect(result.fields).toBeDefined(); 49 | expect(result.createdTime).toBeDefined(); 50 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 51 | expect(result.fields).not.toHaveProperty('email'); 52 | }); 53 | 54 | test('should create a new record with provided data if one does not exist', async () => { 55 | const result = await asyncAirtable.upsertRecord( 56 | process.env.AIRTABLE_TABLE || '', 57 | "{title} = 'test-test'", 58 | JSON.parse(process.env.UPDATE_RECORD || ''), 59 | ); 60 | expect(result).toBeDefined(); 61 | expect(typeof result).toBe('object'); 62 | expect(Object.keys(result).length).toBeGreaterThan(0); 63 | expect(result.id).toBeDefined(); 64 | expect(result.fields).toBeDefined(); 65 | expect(result.createdTime).toBeDefined(); 66 | expect(Object.keys(result.fields).length).toBeGreaterThan(0); 67 | expect(result.id).not.toEqual(initResult[0].id); 68 | }); 69 | 70 | test('should throw an error if you do not pass a table', async () => { 71 | // @ts-ignore 72 | await expect(asyncAirtable.upsertRecord()).rejects.toThrowError( 73 | 'Argument "table" is required', 74 | ); 75 | }); 76 | 77 | test('should throw an error if you do not pass a filterString', async () => { 78 | await expect( 79 | // @ts-ignore 80 | asyncAirtable.upsertRecord(process.env.AIRTABLE_TABLE || ''), 81 | ).rejects.toThrowError('Argument "filterString" is required'); 82 | }); 83 | 84 | test('should throw an error if you do not pass a record', async () => { 85 | await expect( 86 | // @ts-ignore 87 | asyncAirtable.upsertRecord( 88 | process.env.AIRTABLE_TABLE || '', 89 | "{title} = 'test-create'", 90 | ), 91 | ).rejects.toThrowError('Argument "record" is required'); 92 | }); 93 | 94 | test('should throw an error if you pass a field that does not exist', async () => { 95 | await expect( 96 | asyncAirtable.upsertRecord( 97 | process.env.AIRTABLE_TABLE || '', 98 | "{title} = 'test-create'", 99 | { 100 | gringle: 'grangle', 101 | }, 102 | ), 103 | ).rejects.toThrowError(/UNKNOWN_FIELD_NAME/g); 104 | }); 105 | 106 | test('should throw an error if pass a field with the incorrect data type', async () => { 107 | await expect( 108 | asyncAirtable.upsertRecord( 109 | process.env.AIRTABLE_TABLE || '', 110 | "{title} = 'test-create'", 111 | { 112 | ...JSON.parse(process.env.UPDATE_RECORD || ''), 113 | value: 'nope', 114 | }, 115 | ), 116 | ).rejects.toThrowError(/INVALID_VALUE_FOR_COLUMN/g); 117 | }); 118 | 119 | test('should throw an error if pass the table argument with an incorrect data type', async () => { 120 | await expect( 121 | asyncAirtable.upsertRecord( 122 | // @ts-ignore 123 | 10, 124 | "{title} = 'test-create'", 125 | JSON.parse(process.env.UPDATE_RECORD || ''), 126 | ), 127 | ).rejects.toThrowError(/Incorrect data type/g); 128 | }); 129 | 130 | test('should throw an error if pass the filterString argument with an incorrect data type', async () => { 131 | await expect( 132 | asyncAirtable.upsertRecord( 133 | process.env.AIRTABLE_TABLE || '', 134 | //@ts-ignore 135 | 10, 136 | JSON.parse(process.env.UPDATE_RECORD || ''), 137 | ), 138 | ).rejects.toThrowError(/Incorrect data type/g); 139 | }); 140 | 141 | test('should throw an error if pass the record argument with an incorrect data type', async () => { 142 | await expect( 143 | asyncAirtable.upsertRecord( 144 | process.env.AIRTABLE_TABLE || '', 145 | "{title} = 'test-create'", 146 | //@ts-ignore 147 | 10, 148 | ), 149 | ).rejects.toThrowError(/Incorrect data type/g); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /src/textFunctions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryField, 3 | TextConcatFunctions, 4 | TextDoubleArgumentFunctions, 5 | TextMidFunction, 6 | TextReplaceFunctions, 7 | TextSearchFunctions, 8 | TextSingleArgumentFunctions, 9 | TextSubFunctions, 10 | } from './types'; 11 | import { handleError, queryBuilder } from './queryBuilder'; 12 | import { 13 | isTextSearchArgs, 14 | isTextArgArray, 15 | isTextMidArgs, 16 | isTextReplaceArgs, 17 | isTextSubArgs, 18 | isTextDoubleArg, 19 | isStringOrFieldNameObject, 20 | isQueryObject, 21 | } from './typeCheckers'; 22 | 23 | export const textSearchFunctions: TextSearchFunctions = { 24 | $find: ({ stringToFind, whereToSearch, index }) => 25 | `FIND(${queryBuilder(stringToFind)}, ${queryBuilder(whereToSearch)}, ${ 26 | index ?? 0 27 | })`, 28 | $search: ({ stringToFind, whereToSearch, index }) => 29 | `SEARCH(${queryBuilder(stringToFind)}, ${queryBuilder(whereToSearch)}, ${ 30 | index ?? 0 31 | })`, 32 | }; 33 | 34 | export const textReplacementFunction: TextReplaceFunctions = { 35 | $replace: ({ text, startChar, numChars, replacement }) => 36 | `REPLACE(${queryBuilder(text)}, ${queryBuilder(startChar)}, ${queryBuilder( 37 | numChars, 38 | )}, ${queryBuilder(replacement)})`, 39 | }; 40 | 41 | export const textSubstituteFunction: TextSubFunctions = { 42 | $substitute: ({ text, oldText, newText, index }) => 43 | `SUBSTITUTE(${queryBuilder(text)}, ${queryBuilder(oldText)}, ${queryBuilder( 44 | newText, 45 | )}, ${index ?? 0})`, 46 | }; 47 | 48 | export const textConcatFunction: TextConcatFunctions = { 49 | $concatenate: (args) => 50 | `CONCATENATE(${args.map((a) => queryBuilder(a)).join(', ')})`, 51 | }; 52 | 53 | export const textDoubleArgumentFunctions: TextDoubleArgumentFunctions = { 54 | $left: ({ text, num }) => `LEFT(${queryBuilder(text)}, ${queryBuilder(num)})`, 55 | $right: ({ text, num }) => 56 | `RIGHT(${queryBuilder(text)}, ${queryBuilder(num)})`, 57 | $rept: ({ text, num }) => `REPT(${queryBuilder(text)}, ${queryBuilder(num)})`, 58 | }; 59 | 60 | export const textMidFunction: TextMidFunction = { 61 | $mid: ({ text, whereToStart, num }) => 62 | `MID(${queryBuilder(text)}, ${queryBuilder(whereToStart)}, ${queryBuilder( 63 | num, 64 | )})`, 65 | }; 66 | 67 | export const textSingleArgumentFunctions: TextSingleArgumentFunctions = { 68 | $encodeUrlComponent: (str) => `ENCODE_URL_COMPONENT(${queryBuilder(str)})`, 69 | $len: (str) => `LEN(${queryBuilder(str)})`, 70 | $lower: (str) => `LOWER(${queryBuilder(str)})`, 71 | $trim: (str) => `TRIM(${queryBuilder(str)})`, 72 | $upper: (str) => `UPPER(${queryBuilder(str)})`, 73 | }; 74 | 75 | export const textFunctions = { 76 | ...textSearchFunctions, 77 | ...textConcatFunction, 78 | ...textMidFunction, 79 | ...textReplacementFunction, 80 | ...textSubstituteFunction, 81 | ...textDoubleArgumentFunctions, 82 | ...textSingleArgumentFunctions, 83 | }; 84 | 85 | export const handleTextFunc = (key: string, val: QueryField): string => { 86 | if (key in textSearchFunctions && isTextSearchArgs(val)) { 87 | return textSearchFunctions[key](val); 88 | } else if (key in textConcatFunction && isTextArgArray(val)) { 89 | return textConcatFunction[key](val); 90 | } else if (key in textMidFunction && isTextMidArgs(val)) { 91 | return textMidFunction[key](val); 92 | } else if (key in textReplacementFunction && isTextReplaceArgs(val)) { 93 | return textReplacementFunction[key](val); 94 | } else if (key in textSubstituteFunction && isTextSubArgs(val)) { 95 | return textSubstituteFunction[key](val); 96 | } else if (key in textDoubleArgumentFunctions && isTextDoubleArg(val)) { 97 | return textDoubleArgumentFunctions[key](val); 98 | } else if ( 99 | key in textSingleArgumentFunctions && 100 | (isStringOrFieldNameObject(val) || isQueryObject(val)) 101 | ) { 102 | return textSingleArgumentFunctions[key](val); 103 | } 104 | throw handleError({ key, val }); 105 | }; 106 | -------------------------------------------------------------------------------- /src/typeCheckers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryObject, 3 | QueryField, 4 | JoinArgs, 5 | TextSearchArgs, 6 | FieldNameObject, 7 | UncheckedArray, 8 | BaseFieldType, 9 | IfArgs, 10 | TextReplaceArgs, 11 | TextSubArgs, 12 | TextMidArgs, 13 | TextDoubleArg, 14 | SwitchArgs, 15 | RegexArgs, 16 | RegexReplaceArgs, 17 | TextArg, 18 | NumArg, 19 | BoolArg, 20 | AddArg, 21 | DiffArg, 22 | FormatArg, 23 | ParseArg, 24 | WeekArg, 25 | WorkDayArg, 26 | WorkDayDiffArg, 27 | DoubleDateObj, 28 | } from './types'; 29 | import { 30 | CeilFloorArg, 31 | LogArg, 32 | ModArg, 33 | PowerArg, 34 | RoundArg, 35 | } from './types/queryBuilder/numeric'; 36 | 37 | const checkProperty = ( 38 | obj: QueryObject, 39 | prop: string, 40 | typeCheck?: (arg: QueryField | undefined) => boolean, 41 | ) => 42 | Object.prototype.hasOwnProperty.call(obj, prop) && 43 | (typeCheck !== undefined ? typeCheck(obj[prop]) : true); 44 | 45 | export const isQueryObject = (item: QueryField): item is QueryObject => { 46 | if (item === undefined) throw new Error('Missing Query Object'); 47 | return item !== null && typeof item === 'object' && !Array.isArray(item); 48 | }; 49 | 50 | export const isTextArg = (item: QueryField): item is TextArg => 51 | isStringOrFieldNameObject(item) || isQueryObject(item); 52 | 53 | export const isNumArg = (item: QueryField): item is NumArg => 54 | isNumOrFieldNameObject(item) || isQueryObject(item); 55 | 56 | export const isBoolArg = (item: QueryField): item is BoolArg => 57 | isBoolOrFieldNameObject(item) || isQueryObject(item); 58 | 59 | export const isQueryObjectArray = (arr: QueryField): arr is QueryObject[] => 60 | arr instanceof Array && arr.every((v: QueryField) => isQueryObject(v)); 61 | 62 | export const isTextArgArray = (arr: QueryField): arr is TextArg[] => 63 | arr instanceof Array && arr.every((v: QueryField) => isTextArg(v)); 64 | 65 | export const isNumArgArray = (arr: QueryField): arr is NumArg[] => 66 | arr instanceof Array && arr.every((v: QueryField) => isNumArg(v)); 67 | 68 | export const isJoinArgs = (arg: QueryField): arg is JoinArgs => 69 | !!(isQueryObject(arg) && arg.val); 70 | 71 | export const isFieldNameObject = (val: QueryField): val is FieldNameObject => 72 | isQueryObject(val) && typeof val.$fieldName === 'string'; 73 | 74 | export const isString = (arg: QueryField): arg is string => 75 | typeof arg === 'string'; 76 | 77 | export const isStringArray = (arg: QueryField): arg is string[] => 78 | Array.isArray(arg) && arg.every((s) => typeof s === 'string'); 79 | 80 | export const isStringOrFieldNameObject = ( 81 | val: QueryField, 82 | ): val is string | FieldNameObject => isString(val) || isFieldNameObject(val); 83 | 84 | export const isNumOrFieldNameObject = ( 85 | val: QueryField, 86 | ): val is number | FieldNameObject => 87 | typeof val === 'number' || isFieldNameObject(val); 88 | 89 | export const isBoolOrFieldNameObject = ( 90 | val: QueryField, 91 | ): val is number | FieldNameObject => 92 | typeof val === 'boolean' || isFieldNameObject(val); 93 | 94 | export const isTextSearchArgs = (arg: QueryField): arg is TextSearchArgs => 95 | !!( 96 | isQueryObject(arg) && 97 | checkProperty(arg, 'stringToFind', isTextArg) && 98 | checkProperty(arg, 'whereToSearch', isTextArg) && 99 | (checkProperty(arg, 'index') ? isNumArg(arg.index) : true) 100 | ); 101 | 102 | export const isTextReplaceArgs = (arg: QueryField): arg is TextReplaceArgs => 103 | !!( 104 | isQueryObject(arg) && 105 | checkProperty(arg, 'text', isTextArg) && 106 | checkProperty(arg, 'startChar', isNumArg) && 107 | checkProperty(arg, 'numChars', isNumArg) && 108 | checkProperty(arg, 'replacement', isTextArg) 109 | ); 110 | 111 | export const isTextSubArgs = (arg: QueryField): arg is TextSubArgs => 112 | !!( 113 | isQueryObject(arg) && 114 | checkProperty(arg, 'text', isTextArg) && 115 | checkProperty(arg, 'oldText', isTextArg) && 116 | checkProperty(arg, 'newText', isTextArg) && 117 | (checkProperty(arg, 'index') ? isNumArg(arg.index) : true) 118 | ); 119 | export const isTextMidArgs = (arg: QueryField): arg is TextMidArgs => 120 | !!( 121 | isQueryObject(arg) && 122 | checkProperty(arg, 'text', isTextArg) && 123 | checkProperty(arg, 'whereToStart', isNumArg) && 124 | checkProperty(arg, 'num', isNumArg) 125 | ); 126 | export const isTextDoubleArg = (arg: QueryField): arg is TextDoubleArg => 127 | !!( 128 | isQueryObject(arg) && 129 | checkProperty(arg, 'text', isTextArg) && 130 | checkProperty(arg, 'num', isNumArg) 131 | ); 132 | 133 | export const allIndexesValid = (arr: UncheckedArray): arr is QueryField[] => 134 | arr.every((e) => e !== undefined && e !== null); 135 | 136 | export const isBaseField = (item: QueryField): item is BaseFieldType => 137 | (typeof item !== 'object' && typeof item !== 'function') || item === null; 138 | 139 | export const isIfArgs = (arg: QueryField): arg is IfArgs => 140 | !!( 141 | isQueryObject(arg) && 142 | checkProperty(arg, 'expression', isBoolArg) && 143 | checkProperty(arg, 'ifTrue') && 144 | checkProperty(arg, 'ifFalse') 145 | ); 146 | 147 | export const isSwitchArgs = (arg: QueryField): arg is SwitchArgs => 148 | !!( 149 | isQueryObject(arg) && 150 | checkProperty(arg, 'expression', isBoolArg) && 151 | checkProperty(arg, 'cases') && 152 | arg.cases instanceof Array && 153 | arg.cases.every( 154 | (c) => 155 | isQueryObject(c) && 156 | checkProperty(c, 'switchCase') && 157 | checkProperty(c, 'val'), 158 | ) && 159 | checkProperty(arg, 'defaultVal') 160 | ); 161 | 162 | export const isRegexArgs = (arg: QueryField): arg is RegexArgs => 163 | !!( 164 | isQueryObject(arg) && 165 | checkProperty(arg, 'text', isTextArg) && 166 | checkProperty(arg, 'regex', isTextArg) 167 | ); 168 | 169 | export const isRegexReplaceArgs = (arg: QueryField): arg is RegexReplaceArgs => 170 | !!( 171 | isQueryObject(arg) && 172 | checkProperty(arg, 'text', isTextArg) && 173 | checkProperty(arg, 'regex', isTextArg) && 174 | checkProperty(arg, 'replacement', isTextArg) 175 | ); 176 | 177 | export const isCeilFloorArg = (arg: QueryField): arg is CeilFloorArg => 178 | !!(isQueryObject(arg) && checkProperty(arg, 'val', isNumArg)) && 179 | (checkProperty(arg, 'significance') ? isNumArg(arg.significance) : true); 180 | 181 | export const isModArg = (arg: QueryField): arg is ModArg => 182 | !!( 183 | isQueryObject(arg) && 184 | checkProperty(arg, 'val', isNumArg) && 185 | checkProperty(arg, 'divisor', isNumArg) 186 | ); 187 | 188 | export const isPowerArg = (arg: QueryField): arg is PowerArg => 189 | !!( 190 | isQueryObject(arg) && 191 | checkProperty(arg, 'base', isNumArg) && 192 | checkProperty(arg, 'power', isNumArg) 193 | ); 194 | 195 | export const isRoundArg = (arg: QueryField): arg is RoundArg => 196 | !!( 197 | isQueryObject(arg) && 198 | checkProperty(arg, 'val', isNumArg) && 199 | checkProperty(arg, 'precision', isNumArg) 200 | ); 201 | 202 | export const isLogArg = (arg: QueryField): arg is LogArg => 203 | !!(isQueryObject(arg) && 204 | checkProperty(arg, 'num') && 205 | checkProperty(arg, 'base') 206 | ? isNumArg(arg.base) 207 | : true); 208 | 209 | export const isFunc = (arg: QueryField | (() => string)): arg is () => string => 210 | !!(arg && typeof arg === 'function' && typeof arg() === 'string'); 211 | 212 | export const hasDateArg = (arg: QueryField): arg is QueryObject => 213 | !!(isQueryObject(arg) && checkProperty(arg, 'date', isTextArg)); 214 | 215 | export const hasDoubleDateArg = (arg: QueryField): arg is DoubleDateObj => 216 | !!( 217 | isQueryObject(arg) && 218 | checkProperty(arg, 'date1', isTextArg) && 219 | checkProperty(arg, 'date2', isTextArg) 220 | ); 221 | 222 | export const isDateAddArg = (arg: QueryField): arg is AddArg => 223 | !!( 224 | hasDateArg(arg) && 225 | checkProperty(arg, 'count', isNumArg) && 226 | checkProperty(arg, 'units', isString) 227 | ); 228 | 229 | export const isDateDiffArg = (arg: QueryField): arg is DiffArg => 230 | !!(hasDoubleDateArg(arg) && checkProperty(arg, 'units', isString)); 231 | 232 | export const isDateSameArg = (arg: QueryField): arg is DiffArg => 233 | !!(hasDoubleDateArg(arg) && checkProperty(arg, 'units') 234 | ? isString(arg) 235 | : true); 236 | 237 | export const isDateFormatArg = (arg: QueryField): arg is FormatArg => 238 | !!(hasDateArg(arg) && checkProperty(arg, 'format', isTextArg)); 239 | 240 | export const isDateParseArg = (arg: QueryField): arg is ParseArg => 241 | !!(hasDateArg(arg) && 242 | (checkProperty(arg, 'format') ? isTextArg(arg.format) : true) && 243 | checkProperty(arg, 'locale') 244 | ? isTextArg(arg.locale) 245 | : true); 246 | 247 | export const isDateWeekArg = (arg: QueryField): arg is WeekArg => 248 | !!(hasDateArg(arg) && checkProperty(arg, 'start') 249 | ? isTextArg(arg.format) 250 | : true); 251 | 252 | export const isDateWorkDayArg = (arg: QueryField): arg is WorkDayArg => 253 | !!(hasDateArg(arg) && checkProperty(arg, 'holidays') 254 | ? isTextArgArray(arg.format) 255 | : true); 256 | 257 | export const isDateWorkDayDiffArg = (arg: QueryField): arg is WorkDayDiffArg => 258 | !!(hasDateArg(arg) && 259 | checkProperty(arg, 'numDays', isNumArg) && 260 | checkProperty(arg, 'holidays') 261 | ? isTextArgArray(arg.format) 262 | : true); 263 | -------------------------------------------------------------------------------- /src/types/airtable.ts: -------------------------------------------------------------------------------- 1 | import { QueryObject } from '.'; 2 | 3 | /** 4 | * Optional object to pass to the #select method to tailor the response. 5 | * 6 | * @example 7 | * ``` 8 | * { 9 | * fields: ['name', 'email', 'date'], 10 | * where: {name: 'Paul'}, 11 | * maxRecords: 50, 12 | * pageSize: 10, 13 | * sort: [ 14 | * { 15 | * field: "name", 16 | * direction: "desc" 17 | * }, 18 | * { 19 | * field: "date", 20 | * direction: "asc" 21 | * } 22 | * ], 23 | * view: 'Grid view' 24 | * } 25 | * ``` 26 | */ 27 | 28 | export interface SelectOptions { 29 | /** 30 | * An array of specific field names to be returned. 31 | * Returns all fields if none are supplied. 32 | */ 33 | fields?: string[]; 34 | /** 35 | * A [formula used](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) 36 | * to filter the records to return. 37 | */ 38 | filterByFormula?: string; 39 | /** 40 | * A Query Object used to build a 41 | * [filter formula](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) 42 | * to filter the records to return. 43 | */ 44 | where?: QueryObject; 45 | /** 46 | * @default=100 47 | * The maximum total number of records that will be returned in your requests. 48 | * Should be smaller than or equal to `pageSize` 49 | */ 50 | maxRecords?: number; 51 | /** 52 | * @default=100 53 | * The number of records returned in each request. 54 | * Must be less than or equal to 100 55 | */ 56 | pageSize?: number; 57 | /** 58 | * A list of sort objects that specifies how the records will be ordered 59 | */ 60 | sort?: SortObject[]; 61 | /** 62 | * The name or id of a view on the specified table. 63 | * If set, only the records in that view will be returned. 64 | * The records will be sorted according to the order 65 | * of the view unless the sort parameter is included, 66 | * which overrides that order. Fields hidden in this view 67 | * will be returned in the results. To only return 68 | * a subset of fields, use the fields parameter. 69 | */ 70 | view?: string; 71 | /** 72 | * @ignore 73 | */ 74 | offset?: string; 75 | } 76 | 77 | /** 78 | * An optional object used to instatiate AsyncAirtable 79 | * 80 | * @example 81 | * ``` 82 | * { 83 | * "retryOnRateLimit": true, 84 | * "maxRetry": 60000, 85 | * "retryTimeout": 5000 86 | * } 87 | * ``` 88 | */ 89 | 90 | export interface Config { 91 | /** 92 | * @default=true 93 | * This decides whether or not the library will 94 | * handle retrying a request when rate limited 95 | * */ 96 | retryOnRateLimit?: boolean; 97 | /** 98 | * @default=60000 99 | * The maxmium amount of time before the 100 | * library will stop retrying and timeout when rate limited 101 | */ 102 | maxRetry?: number; 103 | /** 104 | * @default=5000 105 | * The starting timeout for the retry. This will get 50% 106 | * larger with each try until you hit the maxRetry amount 107 | */ 108 | retryTimeout?: number; 109 | /** 110 | * @default=https://api.airtable.com/v0 111 | * The endpoint to make API calls against. This is useful when setting up a custom caching server as succested in the Airtable API docs 112 | */ 113 | baseURL?: string; 114 | } 115 | 116 | /** @ignore */ 117 | export type ConfigKey = keyof Config; 118 | 119 | /** 120 | * Sort Option 121 | * @example 122 | * ``` 123 | * { 124 | * field: "name", 125 | * direction: "desc" 126 | * } 127 | * ``` 128 | */ 129 | 130 | export interface SortObject { 131 | /** The field name you want to sort by */ 132 | field: string; 133 | /** The direction of the sort */ 134 | direction?: 'asc' | 'desc'; 135 | } 136 | 137 | /** 138 | * The response from the #delete and #bulkDelete methods 139 | * 140 | * @example 141 | * ``` 142 | * { 143 | * id: "recABCDEFGHIJK", 144 | * deleted: true 145 | * } 146 | * ``` 147 | */ 148 | 149 | export interface DeleteResponse { 150 | /** ID of the deleted record */ 151 | id?: string; 152 | /** Status if a record was deleted */ 153 | deleted: boolean; 154 | } 155 | 156 | /** 157 | * The record returned by AsyncAirtable 158 | * 159 | * @example 160 | * ``` 161 | * { 162 | * id: "recABCDEFGHIJK", 163 | * fields: { 164 | * title: "hello", 165 | * description: "world" 166 | * }, 167 | * createdTime: 'timestamp' 168 | * } 169 | * ``` 170 | */ 171 | 172 | export interface AirtableRecord { 173 | /** Airtable Record ID */ 174 | id: string; 175 | /** Object of fields in the record */ 176 | fields: Fields; 177 | /** Created Timestamp */ 178 | createdTime?: string; 179 | } 180 | 181 | /** @ignore */ 182 | export interface Fields { 183 | [key: string]: unknown; 184 | } 185 | 186 | /** @ignore */ 187 | export interface AirtableRecordResponse { 188 | records: AirtableRecord[]; 189 | offset?: string; 190 | } 191 | 192 | /** @ignore */ 193 | export interface AirtableDeletedResponse { 194 | records: DeleteResponse[]; 195 | } 196 | /** 197 | * The record passed into the #updateRecord and #bulkUpdate methods 198 | * 199 | * @example 200 | * ``` 201 | * { 202 | * id: "recABCDEFGHIJK", 203 | * fields: { 204 | * title: "hello", 205 | * description: "world" 206 | * } 207 | * } 208 | * ``` 209 | */ 210 | export interface AirtableUpdateRecord { 211 | /** The Airtable Record ID of the record you want to update */ 212 | id: string; 213 | /** Object of fields you want to update in the record */ 214 | fields: Fields; 215 | } 216 | 217 | /** @ignore */ 218 | export interface queryBody { 219 | fields: Fields; 220 | typecast?: Typecast; 221 | } 222 | 223 | /** @ignore */ 224 | interface fieldsObject { 225 | fields: Fields; 226 | } 227 | 228 | /** @ignore */ 229 | export interface bulkQueryBody { 230 | records: fieldsObject[] | AirtableRecord[]; 231 | typecast?: Typecast; 232 | } 233 | 234 | /** 235 | * Used for allowing the option to add additional 236 | * select items when creating or updating a record. 237 | * Without, it will throw an INVALID_MULTIPLE_CHOICE error if you 238 | * try to pass an item that doesn't already exist. 239 | */ 240 | export type Typecast = boolean; 241 | 242 | /** Options for updating records */ 243 | export interface updateOpts { 244 | /** (Dis-)Allow a destructive update */ 245 | destructive?: boolean; 246 | /** 247 | * Used for allowing the ability to add new selections for Select and Multiselect fields. 248 | */ 249 | typecast?: Typecast; 250 | } 251 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './airtable'; 2 | export * from './queryBuilder'; 3 | export * from './queryBuilder/array'; 4 | export * from './queryBuilder/comparison'; 5 | export * from './queryBuilder/logical'; 6 | export * from './queryBuilder/text'; 7 | export * from './queryBuilder/regex'; 8 | export * from './queryBuilder/date'; 9 | -------------------------------------------------------------------------------- /src/types/queryBuilder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AirtableUpdateRecord, 3 | SelectOptions, 4 | TextSearchArgs, 5 | RegexArgs, 6 | } from './'; 7 | import { IfArgs, SwitchArgs } from './queryBuilder/logical'; 8 | import { 9 | CeilFloorArg, 10 | LogArg, 11 | ModArg, 12 | PowerArg, 13 | RoundArg, 14 | } from './queryBuilder/numeric'; 15 | import { RegexReplaceArgs } from './queryBuilder/regex'; 16 | import { TextMidArgs, TextReplaceArgs, TextSubArgs } from './queryBuilder/text'; 17 | 18 | /** 19 | * An object to handle filtering the records returned by the #select and #upsert methods. 20 | * 21 | * @example 22 | * ``` 23 | * { 24 | * id: 'Some ID', 25 | * count: { 26 | * $lt: 10, 27 | * $gt: 5 28 | * }, 29 | * $or: [ 30 | * active: true, 31 | * name: 'active' 32 | * ] 33 | * } 34 | * ``` 35 | */ 36 | 37 | interface AirtableFilters extends QueryObject { 38 | /** 39 | * Less than operator 40 | * 41 | * @example 42 | * ``` 43 | * {field: {$lt: value}} 44 | * ``` 45 | */ 46 | $lt?: QueryObject; 47 | /** 48 | * Greater than operator 49 | * 50 | * @example 51 | * ``` 52 | * {field: {$gt: value}} 53 | * ``` 54 | */ 55 | $gt?: QueryObject; 56 | /** 57 | * Less than or equal operator 58 | * 59 | * @example 60 | * ``` 61 | * {field: {$lte: value}} 62 | * ``` 63 | */ 64 | $lte?: QueryObject; 65 | /** 66 | * Greater than or equal operator 67 | * 68 | * @example 69 | * ``` 70 | * {field: {$gte: value}} 71 | * ``` 72 | */ 73 | $gte?: QueryObject; 74 | /** 75 | * Equal operator 76 | * 77 | * @example 78 | * ``` 79 | * {field: {$eq: value}} 80 | * ``` 81 | */ 82 | $eq?: QueryObject; 83 | /** 84 | * Not equal operator 85 | * 86 | * @example 87 | * ``` 88 | * {field: {$neq: value}} 89 | * ``` 90 | */ 91 | $neq?: QueryObject; 92 | /** 93 | * Addition operator 94 | */ 95 | $add?: QueryObject; 96 | /** 97 | * Subtraction operator 98 | */ 99 | $sub?: QueryObject; 100 | /** 101 | * Multiplication operator 102 | */ 103 | $multi?: QueryObject; 104 | /** 105 | * Division operator 106 | */ 107 | $div?: QueryObject; 108 | /** 109 | * NOT logical operator 110 | * 111 | * @example 112 | * ``` 113 | * {$not: expression} 114 | * ``` 115 | */ 116 | $not?: BoolArg; 117 | /** 118 | * AND logical operator 119 | * 120 | * @example 121 | * ``` 122 | * {$and: [{expression}, {expression}, ...{expression}]} 123 | * ``` 124 | */ 125 | $and?: BoolArg[]; 126 | /** 127 | * OR logical operator 128 | * 129 | * @example 130 | * ``` 131 | * {$or: [{expression}, {expression}, ...{expression}]} 132 | * ``` 133 | */ 134 | $or?: BoolArg[]; 135 | /** 136 | * Returns value1 if the logical argument is true, otherwise it returns value2. Can also be used to make nested IF statements. 137 | * Can also be used to check if a cell is blank/is empty. 138 | */ 139 | $if: IfArgs; 140 | /** 141 | * Takes an expression, a list of possible values for that expression, and for each one, a value that the expression should take in that case. It can also take a default value if the expression input doesn't match any of the defined patterns. In many cases, SWITCH() can be used instead of a nested IF formula. 142 | */ 143 | $switch: SwitchArgs; 144 | /** 145 | * Returns true if an odd number of arguments are true. 146 | */ 147 | $xor: BoolArg[]; 148 | /** 149 | * Returns true if the expression causes an error. 150 | */ 151 | $isError: QueryField; 152 | /** 153 | * Removes empty strings and null values from the array. Keeps "false" and strings that contain one or more blank characters. 154 | * 155 | * @example 156 | * ``` 157 | * {$arrayCompact: "field name"} 158 | * ``` 159 | */ 160 | $arrayCompact?: string; 161 | /** 162 | * Takes all subarrays and flattens the elements into a single array. 163 | * 164 | * @example 165 | * ``` 166 | * {$arrayFlatten: "field name"} 167 | * ``` 168 | */ 169 | $arrayFlatten?: string; 170 | /** 171 | * Filters out duplicate array elements. 172 | * 173 | * @example 174 | * ``` 175 | * {$arrayUnique: "field name"} 176 | * ``` 177 | */ 178 | $arrayUnique?: string; 179 | /** 180 | * Joins all array elements into a string with the given separator 181 | * 182 | * @example 183 | * ``` 184 | * {$arrayJoin: {fieldName: 'test', separator: '; '}} 185 | * ``` 186 | * @default separator "," 187 | */ 188 | $arrayJoin?: JoinArgs; 189 | /** 190 | * Finds an occurrence of stringToFind in whereToSearch string starting from an optional startFromPosition.(startFromPosition is 0 by default.) If no occurrence of stringToFind is found, the result will be 0. 191 | * 192 | * @example 193 | * ``` 194 | * {$textFind: {searchText: 'test', query: 'test'}} 195 | * ``` 196 | */ 197 | $find?: TextSearchArgs; 198 | /** 199 | * Searches for an occurrence of stringToFind in whereToSearch string starting from an optional startFromPosition. (startFromPosition is 0 by default.) If no occurrence of stringToFind is found, the result will be empty. 200 | * 201 | * @example 202 | * ``` 203 | * {$textSearch: {searchText: 'test', query: 'test'}} 204 | * ``` 205 | */ 206 | $search?: TextSearchArgs; 207 | /** 208 | * Joins together the text arguments into a single text value. To concatenate static text, surround it with double quotation marks. To concatenate double quotation marks, you need to use a backslash (\) as an escape character. 209 | */ 210 | $concat?: TextArg[]; 211 | /** 212 | * Replaces certain characters with encoded equivalents for use in constructing URLs or URIs. Does not encode the following characters: - _ . ~ 213 | */ 214 | $encodeUrlComponent?: TextArg; 215 | /** 216 | * Extract howMany characters from the beginning of the string. 217 | */ 218 | $left?: TextArg; 219 | /** 220 | * Returns the length of a string. 221 | */ 222 | $len?: TextArg; 223 | /** 224 | * Makes a string lowercase. 225 | */ 226 | $lower?: TextArg; 227 | /** 228 | * Extract a substring of count characters starting at whereToStart. 229 | */ 230 | $mid?: TextMidArgs; 231 | /** 232 | * Replaces the number of characters beginning with the start character with the replacement text. 233 | */ 234 | $replace?: TextReplaceArgs; 235 | /** 236 | * Repeats string by the specified number of times. 237 | */ 238 | $rept?: TextArg; 239 | /** 240 | * Extract howMany characters from the end of the string. 241 | */ 242 | $right?: TextArg; 243 | /** 244 | * Replaces occurrences of old_text with new_text. 245 | * 246 | * You can optionally specify an index number (starting from 1) to replace just a specific occurrence of old_text. If no index number is specified, then all occurrences of old_text will be replaced. 247 | */ 248 | $substitute?: TextSubArgs; 249 | /** 250 | * Removes whitespace at the beginning and end of string. 251 | */ 252 | $trim?: TextArg; 253 | /** 254 | * Makes string uppercase. 255 | */ 256 | $upper?: TextArg; 257 | /** 258 | * Returns the absolute value. 259 | */ 260 | $abs?: NumArg; 261 | /** 262 | * Returns the average of the numbers. 263 | */ 264 | $avg?: NumArg[]; 265 | /** 266 | * Returns the nearest integer multiple of significance that is greater than or equal to the value. If no significance is provided, a significance of 1 is assumed. 267 | */ 268 | $ceil?: CeilFloorArg; 269 | /** 270 | * Count the number of numeric items. 271 | */ 272 | $count?: NumArg[]; 273 | /** 274 | * Count the number of non-empty values. This function counts both numeric and text values. 275 | */ 276 | $counta?: NumArg[]; 277 | /** 278 | * Count the number of all elements including text and blanks. 279 | */ 280 | $countAll?: NumArg[]; 281 | /** 282 | * Returns the smallest even integer that is greater than or equal to the specified value. 283 | */ 284 | $even?: NumArg; 285 | /** 286 | * Computes Euler's number (e) to the specified power. 287 | */ 288 | $exp?: NumArg; 289 | /** 290 | * Returns the nearest integer multiple of significance that is less than or equal to the value. If no significance is provided, a significance of 1 is assumed. 291 | */ 292 | $floor?: CeilFloorArg; 293 | /** 294 | * Returns the greatest integer that is less than or equal to the specified value. 295 | */ 296 | $int?: NumArg; 297 | /** 298 | * Computes the logarithm of the value in provided base. The base defaults to 10 if not specified. 299 | */ 300 | $log?: LogArg; 301 | /** 302 | * Returns the largest of the given numbers. 303 | */ 304 | $max?: NumArg[]; 305 | /** 306 | * Returns the smallest of the given numbers. 307 | */ 308 | $min?: NumArg[]; 309 | /** 310 | * Returns the remainder after dividing the first argument by the second. 311 | */ 312 | $mod?: ModArg; 313 | /** 314 | * Rounds positive value up the the nearest odd number and negative value down to the nearest odd number. 315 | */ 316 | $odd?: NumArg; 317 | /** 318 | * Computes the specified base to the specified power. 319 | */ 320 | $pow?: PowerArg; 321 | /** 322 | * Rounds the value to the number of decimal places given by "precision." (Specifically, ROUND will round to the nearest integer at the specified precision, with ties broken by rounding half up toward positive infinity.) 323 | */ 324 | $round?: RoundArg; 325 | /** 326 | * Rounds the value to the number of decimal places given by "precision," always rounding down, i.e., toward zero. (You must give a value for the precision or the function will not work.) 327 | */ 328 | $roundDown?: RoundArg; 329 | /** 330 | * Rounds the value to the number of decimal places given by "precision," always rounding up, i.e., away from zero. (You must give a value for the precision or the function will not work.) 331 | */ 332 | $roundUp?: RoundArg; 333 | /** 334 | * Returns the square root of a nonnegative number. 335 | */ 336 | $sqrt?: NumArg; 337 | /** 338 | * Sum together the numbers. Equivalent to number1 + number2 + ... 339 | */ 340 | $sum?: NumArg[]; 341 | /** 342 | * Returns whether the input text matches a regular expression. 343 | */ 344 | $regexMatch?: RegexArgs; 345 | /** 346 | * Returns the first substring that matches a regular expression. 347 | */ 348 | $regexExtract?: RegexArgs; 349 | /** 350 | * Substitutes all matching substrings with a replacement string value. 351 | */ 352 | $regexReplace?: RegexReplaceArgs; 353 | /** 354 | * Used for handling fieldNames in text methods 355 | * 356 | * @example 357 | * ``` 358 | * {$textFind("text to find", {fieldName: "field to search"})} 359 | * ``` 360 | */ 361 | $fieldName?: string; 362 | $insert?: string; 363 | } 364 | export interface QueryObject extends Record { 365 | /** 366 | * Shortform equal 367 | * (equivalent to $eq) 368 | * 369 | * @example 370 | * ``` 371 | * {field: value} 372 | * ``` 373 | */ 374 | [key: string]: QueryField; 375 | } 376 | 377 | /** @ignore */ 378 | type ErrorFunc = () => string; 379 | /** @ignore */ 380 | export interface ErrorFuncs extends Record { 381 | $error: ErrorFunc; 382 | } 383 | /** @ignore */ 384 | export type QueryField = 385 | | QueryObject 386 | | QueryField[] 387 | | AirtableFilters 388 | | BaseFieldType 389 | | (() => string) 390 | | undefined; 391 | /** @ignore */ 392 | export type BaseFieldType = string | number | boolean | null; 393 | 394 | /**@ignore */ 395 | export type UncheckedArray = (QueryField | QueryField[] | undefined)[]; 396 | 397 | /** @ignore */ 398 | export type Arg = 399 | | string 400 | | number 401 | | boolean 402 | | SelectOptions 403 | | Record[] 404 | | string[] 405 | | AirtableUpdateRecord 406 | | AirtableUpdateRecord[] 407 | | undefined; 408 | 409 | /** For using a fieldname as a value in the query builder. */ 410 | export type FieldNameObject = { 411 | $fieldName: string; 412 | }; 413 | 414 | export interface JoinArgs extends QueryObject { 415 | val: QueryField; 416 | separator?: QueryField; 417 | } 418 | 419 | export type TextArg = string | FieldNameObject | QueryObject; 420 | export type NumArg = number | FieldNameObject | QueryObject; 421 | export type BoolArg = boolean | FieldNameObject | QueryObject; 422 | -------------------------------------------------------------------------------- /src/types/queryBuilder/array.ts: -------------------------------------------------------------------------------- 1 | import { QueryField } from '../queryBuilder'; 2 | 3 | /** @ignore */ 4 | type ArrayFunction = (arg: QueryField, separator?: QueryField) => string; 5 | 6 | /** @ignore */ 7 | export interface ArrayFunctions extends Record { 8 | $arrayCompact: ArrayFunction; 9 | $arrayFlatten: ArrayFunction; 10 | $arrayUnique: ArrayFunction; 11 | $arrayJoin: ArrayFunction; 12 | } 13 | -------------------------------------------------------------------------------- /src/types/queryBuilder/comparison.ts: -------------------------------------------------------------------------------- 1 | import { BaseFieldType, QueryObject } from '..'; 2 | 3 | /** @ignore */ 4 | export type ComparisonObject = Record; 5 | 6 | /** @ignore */ 7 | type ComparisonFunction = (vals: ComparisonObject) => string; 8 | 9 | /** @ignore */ 10 | export interface LogicalOperators extends Record { 11 | $lt: ComparisonFunction; 12 | $gt: ComparisonFunction; 13 | $lte: ComparisonFunction; 14 | $gte: ComparisonFunction; 15 | $eq: ComparisonFunction; 16 | $neq: ComparisonFunction; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/queryBuilder/date.ts: -------------------------------------------------------------------------------- 1 | import { NumArg, QueryField, QueryObject, TextArg } from '../queryBuilder'; 2 | 3 | type Unit = 4 | | 'milliseconds' 5 | | 'seconds' 6 | | 'minutes' 7 | | 'hours' 8 | | 'days' 9 | | 'weeks' 10 | | 'months' 11 | | 'quarters' 12 | | 'years'; 13 | 14 | type Locale = 15 | | 'af' 16 | | 'ar-ma' 17 | | 'ar-ma' 18 | | 'ar-sa' 19 | | 'ar-tn' 20 | | 'ar' 21 | | 'az' 22 | | 'be' 23 | | 'bg' 24 | | 'bn' 25 | | 'bo' 26 | | 'br' 27 | | 'bs' 28 | | 'ca' 29 | | 'cs' 30 | | 'cv' 31 | | 'cy' 32 | | 'da' 33 | | 'de-at' 34 | | 'de' 35 | | 'el' 36 | | 'en-au' 37 | | 'en-ca' 38 | | 'en-gb' 39 | | 'en-ie' 40 | | 'en-nz' 41 | | 'eo' 42 | | 'es' 43 | | 'et' 44 | | 'eu' 45 | | 'fa' 46 | | 'fi' 47 | | 'fo' 48 | | 'fr-ca' 49 | | 'fr-ch' 50 | | 'fr' 51 | | 'fy' 52 | | 'gl' 53 | | 'he' 54 | | 'hi' 55 | | 'hr' 56 | | 'hu' 57 | | 'hy-am' 58 | | 'id' 59 | | 'is' 60 | | 'it' 61 | | 'ja' 62 | | 'jv' 63 | | 'ka' 64 | | 'km' 65 | | 'ko' 66 | | 'lb' 67 | | 'lt' 68 | | 'lv' 69 | | 'me' 70 | | 'mk' 71 | | 'ml' 72 | | 'mr' 73 | | 'ms' 74 | | 'my' 75 | | 'nb' 76 | | 'ne' 77 | | 'nl' 78 | | 'nn' 79 | | 'pl' 80 | | 'pt-br' 81 | | 'pt' 82 | | 'ro' 83 | | 'ru' 84 | | 'si' 85 | | 'sk' 86 | | 'sl' 87 | | 'sq' 88 | | 'sr-cyrl' 89 | | 'sr' 90 | | 'sv' 91 | | 'ta' 92 | | 'th' 93 | | 'tl-ph' 94 | | 'tr' 95 | | 'tzl' 96 | | 'tzm-latn' 97 | | 'tzm' 98 | | 'uk' 99 | | 'uz' 100 | | 'vi' 101 | | 'zh-cn' 102 | | 'zh-tw'; 103 | 104 | type Timezone = 105 | | 'Africa/Abidjan' 106 | | 'Africa/Accra' 107 | | 'Africa/Algiers' 108 | | 'Africa/Bissau' 109 | | 'Africa/Cairo' 110 | | 'Africa/Casablanca' 111 | | 'Africa/Ceuta' 112 | | 'Africa/El_Aaiun' 113 | | 'Africa/Johannesburg' 114 | | 'Africa/Khartoum' 115 | | 'Africa/Lagos' 116 | | 'Africa/Maputo' 117 | | 'Africa/Monrovia' 118 | | 'Africa/Nairobi' 119 | | 'Africa/Ndjamena' 120 | | 'Africa/Tripoli' 121 | | 'Africa/Tunis' 122 | | 'Africa/Windhoek' 123 | | 'America/Adak' 124 | | 'America/Anchorage' 125 | | 'America/Araguaina' 126 | | 'America/Argentina/Buenos_Aires' 127 | | 'America/Argentina/Catamarca' 128 | | 'America/Argentina/Cordoba' 129 | | 'America/Argentina/Jujuy' 130 | | 'America/Argentina/La_Rioja' 131 | | 'America/Argentina/Mendoza' 132 | | 'America/Argentina/Rio_Gallegos' 133 | | 'America/Argentina/Salta' 134 | | 'America/Argentina/San_Juan' 135 | | 'America/Argentina/San_Luis' 136 | | 'America/Argentina/Tucuman' 137 | | 'America/Argentina/Ushuaia' 138 | | 'America/Asuncion' 139 | | 'America/Atikokan' 140 | | 'America/Bahia' 141 | | 'America/Bahia_Banderas' 142 | | 'America/Barbados' 143 | | 'America/Belem' 144 | | 'America/Belize' 145 | | 'America/Blanc-Sablon' 146 | | 'America/Boa_Vista' 147 | | 'America/Bogota' 148 | | 'America/Boise' 149 | | 'America/Cambridge_Bay' 150 | | 'America/Campo_Grande' 151 | | 'America/Cancun' 152 | | 'America/Caracas' 153 | | 'America/Cayenne' 154 | | 'America/Chicago' 155 | | 'America/Chihuahua' 156 | | 'America/Costa_Rica' 157 | | 'America/Creston' 158 | | 'America/Cuiaba' 159 | | 'America/Curacao' 160 | | 'America/Danmarkshavn' 161 | | 'America/Dawson' 162 | | 'America/Dawson_Creek' 163 | | 'America/Denver' 164 | | 'America/Detroit' 165 | | 'America/Edmonton' 166 | | 'America/Eirunepe' 167 | | 'America/El_Salvador' 168 | | 'America/Fort_Nelson' 169 | | 'America/Fortaleza' 170 | | 'America/Glace_Bay' 171 | | 'America/Godthab' 172 | | 'America/Goose_Bay' 173 | | 'America/Grand_Turk' 174 | | 'America/Guatemala' 175 | | 'America/Guayaquil' 176 | | 'America/Guyana' 177 | | 'America/Halifax' 178 | | 'America/Havana' 179 | | 'America/Hermosillo' 180 | | 'America/Indiana/Indianapolis' 181 | | 'America/Indiana/Knox' 182 | | 'America/Indiana/Marengo' 183 | | 'America/Indiana/Petersburg' 184 | | 'America/Indiana/Tell_City' 185 | | 'America/Indiana/Vevay' 186 | | 'America/Indiana/Vincennes' 187 | | 'America/Indiana/Winamac' 188 | | 'America/Inuvik' 189 | | 'America/Iqaluit' 190 | | 'America/Jamaica' 191 | | 'America/Juneau' 192 | | 'America/Kentucky/Louisville' 193 | | 'America/Kentucky/Monticello' 194 | | 'America/La_Paz' 195 | | 'America/Lima' 196 | | 'America/Los_Angeles' 197 | | 'America/Maceio' 198 | | 'America/Managua' 199 | | 'America/Manaus' 200 | | 'America/Martinique' 201 | | 'America/Matamoros' 202 | | 'America/Mazatlan' 203 | | 'America/Menominee' 204 | | 'America/Merida' 205 | | 'America/Metlakatla' 206 | | 'America/Mexico_City' 207 | | 'America/Miquelon' 208 | | 'America/Moncton' 209 | | 'America/Monterrey' 210 | | 'America/Montevideo' 211 | | 'America/Nassau' 212 | | 'America/New_York' 213 | | 'America/Nipigon' 214 | | 'America/Nome' 215 | | 'America/Noronha' 216 | | 'America/North_Dakota/Beulah' 217 | | 'America/North_Dakota/Center' 218 | | 'America/North_Dakota/New_Salem' 219 | | 'America/Ojinaga' 220 | | 'America/Panama' 221 | | 'America/Pangnirtung' 222 | | 'America/Paramaribo' 223 | | 'America/Phoenix' 224 | | 'America/Port-au-Prince' 225 | | 'America/Port_of_Spain' 226 | | 'America/Porto_Velho' 227 | | 'America/Puerto_Rico' 228 | | 'America/Rainy_River' 229 | | 'America/Rankin_Inlet' 230 | | 'America/Recife' 231 | | 'America/Regina' 232 | | 'America/Resolute' 233 | | 'America/Rio_Branco' 234 | | 'America/Santarem' 235 | | 'America/Santiago' 236 | | 'America/Santo_Domingo' 237 | | 'America/Sao_Paulo' 238 | | 'America/Scoresbysund' 239 | | 'America/Sitka' 240 | | 'America/St_Johns' 241 | | 'America/Swift_Current' 242 | | 'America/Tegucigalpa' 243 | | 'America/Thule' 244 | | 'America/Thunder_Bay' 245 | | 'America/Tijuana' 246 | | 'America/Toronto' 247 | | 'America/Vancouver' 248 | | 'America/Whitehorse' 249 | | 'America/Winnipeg' 250 | | 'America/Yakutat' 251 | | 'America/Yellowknife' 252 | | 'Antarctica/Casey' 253 | | 'Antarctica/Davis' 254 | | 'Antarctica/DumontDUrville' 255 | | 'Antarctica/Macquarie' 256 | | 'Antarctica/Mawson' 257 | | 'Antarctica/Palmer' 258 | | 'Antarctica/Rothera' 259 | | 'Antarctica/Syowa' 260 | | 'Antarctica/Troll' 261 | | 'Antarctica/Vostok' 262 | | 'Asia/Almaty' 263 | | 'Asia/Amman' 264 | | 'Asia/Anadyr' 265 | | 'Asia/Aqtau' 266 | | 'Asia/Aqtobe' 267 | | 'Asia/Ashgabat' 268 | | 'Asia/Baghdad' 269 | | 'Asia/Baku' 270 | | 'Asia/Bangkok' 271 | | 'Asia/Barnaul' 272 | | 'Asia/Beirut' 273 | | 'Asia/Bishkek' 274 | | 'Asia/Brunei' 275 | | 'Asia/Chita' 276 | | 'Asia/Choibalsan' 277 | | 'Asia/Colombo' 278 | | 'Asia/Damascus' 279 | | 'Asia/Dhaka' 280 | | 'Asia/Dili' 281 | | 'Asia/Dubai' 282 | | 'Asia/Dushanbe' 283 | | 'Asia/Gaza' 284 | | 'Asia/Hebron' 285 | | 'Asia/Ho_Chi_Minh' 286 | | 'Asia/Hong_Kong' 287 | | 'Asia/Hovd' 288 | | 'Asia/Irkutsk' 289 | | 'Asia/Jakarta' 290 | | 'Asia/Jayapura' 291 | | 'Asia/Jerusalem' 292 | | 'Asia/Kabul' 293 | | 'Asia/Kamchatka' 294 | | 'Asia/Karachi' 295 | | 'Asia/Kathmandu' 296 | | 'Asia/Khandyga' 297 | | 'Asia/Kolkata' 298 | | 'Asia/Krasnoyarsk' 299 | | 'Asia/Kuala_Lumpur' 300 | | 'Asia/Kuching' 301 | | 'Asia/Macau' 302 | | 'Asia/Magadan' 303 | | 'Asia/Makassar' 304 | | 'Asia/Manila' 305 | | 'Asia/Nicosia' 306 | | 'Asia/Novokuznetsk' 307 | | 'Asia/Novosibirsk' 308 | | 'Asia/Omsk' 309 | | 'Asia/Oral' 310 | | 'Asia/Pontianak' 311 | | 'Asia/Pyongyang' 312 | | 'Asia/Qatar' 313 | | 'Asia/Qyzylorda' 314 | | 'Asia/Rangoon' 315 | | 'Asia/Riyadh' 316 | | 'Asia/Sakhalin' 317 | | 'Asia/Samarkand' 318 | | 'Asia/Seoul' 319 | | 'Asia/Shanghai' 320 | | 'Asia/Singapore' 321 | | 'Asia/Srednekolymsk' 322 | | 'Asia/Taipei' 323 | | 'Asia/Tashkent' 324 | | 'Asia/Tbilisi' 325 | | 'Asia/Tehran' 326 | | 'Asia/Thimphu' 327 | | 'Asia/Tokyo' 328 | | 'Asia/Tomsk' 329 | | 'Asia/Ulaanbaatar' 330 | | 'Asia/Urumqi' 331 | | 'Asia/Ust-Nera' 332 | | 'Asia/Vladivostok' 333 | | 'Asia/Yakutsk' 334 | | 'Asia/Yekaterinburg' 335 | | 'Asia/Yerevan' 336 | | 'Atlantic/Azores' 337 | | 'Atlantic/Bermuda' 338 | | 'Atlantic/Canary' 339 | | 'Atlantic/Cape_Verde' 340 | | 'Atlantic/Faroe' 341 | | 'Atlantic/Madeira' 342 | | 'Atlantic/Reykjavik' 343 | | 'Atlantic/South_Georgia' 344 | | 'Atlantic/Stanley' 345 | | 'Australia/Adelaide' 346 | | 'Australia/Brisbane' 347 | | 'Australia/Broken_Hill' 348 | | 'Australia/Currie' 349 | | 'Australia/Darwin' 350 | | 'Australia/Eucla' 351 | | 'Australia/Hobart' 352 | | 'Australia/Lindeman' 353 | | 'Australia/Lord_Howe' 354 | | 'Australia/Melbourne' 355 | | 'Australia/Perth' 356 | | 'Australia/Sydney' 357 | | 'GMT' 358 | | 'Europe/Amsterdam' 359 | | 'Europe/Andorra' 360 | | 'Europe/Astrakhan' 361 | | 'Europe/Athens' 362 | | 'Europe/Belgrade' 363 | | 'Europe/Berlin' 364 | | 'Europe/Brussels' 365 | | 'Europe/Bucharest' 366 | | 'Europe/Budapest' 367 | | 'Europe/Chisinau' 368 | | 'Europe/Copenhagen' 369 | | 'Europe/Dublin' 370 | | 'Europe/Gibraltar' 371 | | 'Europe/Helsinki' 372 | | 'Europe/Istanbul' 373 | | 'Europe/Kaliningrad' 374 | | 'Europe/Kiev' 375 | | 'Europe/Kirov' 376 | | 'Europe/Lisbon' 377 | | 'Europe/London' 378 | | 'Europe/Luxembourg' 379 | | 'Europe/Madrid' 380 | | 'Europe/Malta' 381 | | 'Europe/Minsk' 382 | | 'Europe/Monaco' 383 | | 'Europe/Moscow' 384 | | 'Europe/Oslo' 385 | | 'Europe/Paris' 386 | | 'Europe/Prague' 387 | | 'Europe/Riga' 388 | | 'Europe/Rome' 389 | | 'Europe/Samara' 390 | | 'Europe/Simferopol' 391 | | 'Europe/Sofia' 392 | | 'Europe/Stockholm' 393 | | 'Europe/Tallinn' 394 | | 'Europe/Tirane' 395 | | 'Europe/Ulyanovsk' 396 | | 'Europe/Uzhgorod' 397 | | 'Europe/Vienna' 398 | | 'Europe/Vilnius' 399 | | 'Europe/Volgograd' 400 | | 'Europe/Warsaw' 401 | | 'Europe/Zaporozhye' 402 | | 'Europe/Zurich' 403 | | 'Indian/Chagos' 404 | | 'Indian/Christmas' 405 | | 'Indian/Cocos' 406 | | 'Indian/Kerguelen' 407 | | 'Indian/Mahe' 408 | | 'Indian/Maldives' 409 | | 'Indian/Mauritius' 410 | | 'Indian/Reunion' 411 | | 'Pacific/Apia' 412 | | 'Pacific/Auckland' 413 | | 'Pacific/Bougainville' 414 | | 'Pacific/Chatham' 415 | | 'Pacific/Chuuk' 416 | | 'Pacific/Easter' 417 | | 'Pacific/Efate' 418 | | 'Pacific/Enderbury' 419 | | 'Pacific/Fakaofo' 420 | | 'Pacific/Fiji' 421 | | 'Pacific/Funafuti' 422 | | 'Pacific/Galapagos' 423 | | 'Pacific/Gambier' 424 | | 'Pacific/Guadalcanal' 425 | | 'Pacific/Guam' 426 | | 'Pacific/Honolulu' 427 | | 'Pacific/Kiritimati' 428 | | 'Pacific/Kosrae' 429 | | 'Pacific/Kwajalein' 430 | | 'Pacific/Majuro' 431 | | 'Pacific/Marquesas' 432 | | 'Pacific/Nauru' 433 | | 'Pacific/Niue' 434 | | 'Pacific/Norfolk' 435 | | 'Pacific/Noumea' 436 | | 'Pacific/Pago_Pago' 437 | | 'Pacific/Palau' 438 | | 'Pacific/Pitcairn' 439 | | 'Pacific/Pohnpei' 440 | | 'Pacific/Port_Moresby' 441 | | 'Pacific/Rarotonga' 442 | | 'Pacific/Tahiti' 443 | | 'Pacific/Tarawa' 444 | | 'Pacific/Tongatapu' 445 | | 'Pacific/Wake' 446 | | 'Pacific/Wallis'; 447 | 448 | interface DateObj extends QueryObject { 449 | date: TextArg; 450 | } 451 | 452 | export interface DoubleDateObj extends QueryObject { 453 | date1: TextArg; 454 | date2: TextArg; 455 | } 456 | 457 | export interface AddArg extends DateObj { 458 | count: NumArg; 459 | units: Unit; 460 | } 461 | 462 | export interface DiffArg extends DoubleDateObj { 463 | units: Unit; 464 | } 465 | 466 | export interface SameArg extends DoubleDateObj { 467 | units?: Unit; 468 | } 469 | 470 | export interface FormatArg extends QueryObject { 471 | date: TextArg | SetArg; 472 | format?: TextArg; 473 | } 474 | 475 | export interface ParseArg extends FormatArg { 476 | locale?: TextArg; 477 | } 478 | 479 | export interface SetArg extends DateObj { 480 | identifier: Locale | Timezone; 481 | } 482 | 483 | export interface WeekArg extends DateObj { 484 | start?: 'Sunday' | 'Monday'; 485 | } 486 | 487 | export interface WorkDayArg extends DateObj { 488 | numDays: NumArg; 489 | holidays?: string[]; 490 | } 491 | 492 | export interface WorkDayDiffArg extends DoubleDateObj { 493 | holidays?: string[]; 494 | } 495 | 496 | type SingleArgFunc = (arg: QueryField) => string; 497 | type AddFunc = (arg: AddArg) => string; 498 | type DiffFunc = (arg: DiffArg) => string; 499 | type SameFunc = (arg: SameArg) => string; 500 | type FormatFunc = (arg: FormatArg) => string; 501 | type ParseFunc = (arg: ParseArg) => string; 502 | type PastFutureFunc = (arg: DoubleDateObj) => string; 503 | type WeekFunc = (arg: WeekArg) => string; 504 | type WorkDayFunc = (arg: WorkDayArg) => string; 505 | type WorkDayDiffFunc = (arg: WorkDayDiffArg) => string; 506 | type LastModifiedFunc = (arg: string[]) => string; 507 | 508 | export interface SingleArgDateFuncs extends Record { 509 | $dateStr: SingleArgFunc; 510 | $day: SingleArgFunc; 511 | $hour: SingleArgFunc; 512 | $minute: SingleArgFunc; 513 | $month: SingleArgFunc; 514 | $second: SingleArgFunc; 515 | $timeStr: SingleArgFunc; 516 | $toNow: SingleArgFunc; 517 | $fromNow: SingleArgFunc; 518 | $year: SingleArgFunc; 519 | } 520 | 521 | export interface DateAddFunc extends Record { 522 | $dateAdd: AddFunc; 523 | } 524 | 525 | export interface DateDiffFunc extends Record { 526 | $dateDiff: DiffFunc; 527 | } 528 | 529 | export interface DateSameFunc extends Record { 530 | $dateSame: SameFunc; 531 | } 532 | 533 | export interface DatePastFutureFuncs extends Record { 534 | $dateBefore: PastFutureFunc; 535 | $dateAfter: PastFutureFunc; 536 | } 537 | 538 | export interface DateFormatFunc extends Record { 539 | $dateFormat: FormatFunc; 540 | } 541 | 542 | export interface DateParseFunc extends Record { 543 | $dateParse: ParseFunc; 544 | } 545 | 546 | export interface DateWeekFuncs extends Record { 547 | $weekDay: WeekFunc; 548 | $weekNum: WeekFunc; 549 | } 550 | 551 | export interface DateWorkDayFunc extends Record { 552 | $workDay: WorkDayFunc; 553 | } 554 | 555 | export interface DateWorkDayDiffFunc extends Record { 556 | $workDayDiff: WorkDayDiffFunc; 557 | } 558 | 559 | export interface DateLastModifiedFunc extends Record { 560 | $lastModified: LastModifiedFunc; 561 | } 562 | -------------------------------------------------------------------------------- /src/types/queryBuilder/logical.ts: -------------------------------------------------------------------------------- 1 | import { QueryField } from '..'; 2 | 3 | export type IfArgs = { 4 | expression: QueryField; 5 | ifTrue: QueryField; 6 | ifFalse: QueryField; 7 | }; 8 | 9 | export type SwitchArgs = { 10 | expression: QueryField; 11 | cases: { 12 | switchCase: QueryField; 13 | val: QueryField; 14 | }[]; 15 | defaultVal: QueryField; 16 | }; 17 | 18 | /** @ignore */ 19 | type ExpressionFunc = (expression: QueryField) => string; 20 | /** @ignore */ 21 | type ArrayExpressionFunc = (args: QueryField[]) => string; 22 | /** @ignore */ 23 | type IfFunc = (arg: IfArgs) => string; 24 | /** @ignore */ 25 | type SwitchFunc = (args: SwitchArgs) => string; 26 | /** @ignore */ 27 | export interface ExpressionFuncs extends Record { 28 | $not: ExpressionFunc; 29 | $isError: ExpressionFunc; 30 | } 31 | /** @ignore */ 32 | export interface ArrayExpressionFuncs 33 | extends Record { 34 | $and: ArrayExpressionFunc; 35 | $or: ArrayExpressionFunc; 36 | $xor: ArrayExpressionFunc; 37 | } 38 | /** @ignore */ 39 | export interface IfFunction extends Record { 40 | $if: IfFunc; 41 | } 42 | /** @ignore */ 43 | export interface SwitchFunction extends Record { 44 | $switch: SwitchFunc; 45 | } 46 | 47 | export interface LogicalFunctions 48 | extends ExpressionFuncs, 49 | ArrayExpressionFuncs, 50 | IfFunction, 51 | SwitchFunction {} 52 | -------------------------------------------------------------------------------- /src/types/queryBuilder/numeric.ts: -------------------------------------------------------------------------------- 1 | import { BaseFieldType, NumArg, QueryObject } from '../queryBuilder'; 2 | 3 | /** @ignore */ 4 | export type NumericalOpObject = Record; 5 | 6 | /** @ignore */ 7 | type NumericalOpFunction = (vals: NumericalOpObject) => string; 8 | 9 | export type CeilFloorArg = { 10 | val: NumArg; 11 | significance?: NumArg; 12 | }; 13 | 14 | export type LogArg = { 15 | num: NumArg; 16 | base?: NumArg; 17 | }; 18 | 19 | export type ModArg = { 20 | val: NumArg; 21 | divisor: NumArg; 22 | }; 23 | 24 | export type PowerArg = { 25 | base: NumArg; 26 | power: NumArg; 27 | }; 28 | 29 | export type RoundArg = { 30 | val: NumArg; 31 | precision: NumArg; 32 | }; 33 | 34 | export type SingleArgNumFunc = (arg: NumArg) => string; 35 | export type ArrayArgNumFunc = (arg: NumArg[]) => string; 36 | export type CeilFloorNumFunc = (arg: CeilFloorArg) => string; 37 | export type LogNumFunc = (arg: LogArg) => string; 38 | export type ModNumFunc = (arg: ModArg) => string; 39 | export type PowerNumFunc = (arg: PowerArg) => string; 40 | export type RoundNumFunc = (arg: RoundArg) => string; 41 | 42 | export interface SingleArgNumFunctions 43 | extends Record { 44 | $abs: SingleArgNumFunc; 45 | $even: SingleArgNumFunc; 46 | $exp: SingleArgNumFunc; 47 | $int: SingleArgNumFunc; 48 | $odd: SingleArgNumFunc; 49 | $sqrt: SingleArgNumFunc; 50 | } 51 | 52 | export interface ArrayArgNumFunctions extends Record { 53 | $avg: ArrayArgNumFunc; 54 | $count: ArrayArgNumFunc; 55 | $counta: ArrayArgNumFunc; 56 | $countAll: ArrayArgNumFunc; 57 | $max: ArrayArgNumFunc; 58 | $min: ArrayArgNumFunc; 59 | $sum: ArrayArgNumFunc; 60 | } 61 | 62 | export interface CeilFloorNumFunctions 63 | extends Record { 64 | $ceil: CeilFloorNumFunc; 65 | $floor: CeilFloorNumFunc; 66 | } 67 | 68 | export interface LogNumFunction extends Record { 69 | $log: LogNumFunc; 70 | } 71 | 72 | export interface ModNumFunction extends Record { 73 | $mod: ModNumFunc; 74 | } 75 | 76 | export interface PowerNumFunction extends Record { 77 | $pow: PowerNumFunc; 78 | } 79 | 80 | export interface RoundNumFunctions extends Record { 81 | $round: RoundNumFunc; 82 | $roundDown: RoundNumFunc; 83 | $roundUp: RoundNumFunc; 84 | } 85 | 86 | export interface NumericOperators extends Record { 87 | $add: NumericalOpFunction; 88 | $sub: NumericalOpFunction; 89 | $multi: NumericalOpFunction; 90 | $div: NumericalOpFunction; 91 | } 92 | -------------------------------------------------------------------------------- /src/types/queryBuilder/regex.ts: -------------------------------------------------------------------------------- 1 | import { TextArg } from '..'; 2 | 3 | export type RegexArgs = { text: TextArg; regex: TextArg }; 4 | export type RegexReplaceArgs = { 5 | text: TextArg; 6 | regex: TextArg; 7 | replacement: TextArg; 8 | }; 9 | export type RegexFunc = (arg: RegexArgs) => string; 10 | export type RegexReplaceFunc = (arg: RegexReplaceArgs) => string; 11 | export interface RegexFunctions extends Record { 12 | $regexMatch: RegexFunc; 13 | $regexExtract: RegexFunc; 14 | } 15 | export interface RegexReplaceFunction extends Record { 16 | $regexReplace: RegexReplaceFunc; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/queryBuilder/text.ts: -------------------------------------------------------------------------------- 1 | import { NumArg, TextArg } from '..'; 2 | import { QueryObject } from '../queryBuilder'; 3 | 4 | export interface TextSearchArgs extends QueryObject { 5 | stringToFind: TextArg; 6 | whereToSearch: TextArg; 7 | index?: NumArg; 8 | } 9 | 10 | export interface TextReplaceArgs extends QueryObject { 11 | text: TextArg; 12 | startChar: NumArg; 13 | numChars: NumArg; 14 | replacement: TextArg; 15 | } 16 | 17 | export interface TextSubArgs extends QueryObject { 18 | text: TextArg; 19 | oldText: TextArg; 20 | newText: TextArg; 21 | index?: NumArg; 22 | } 23 | 24 | export interface TextMidArgs extends QueryObject { 25 | text: TextArg; 26 | whereToStart: NumArg; 27 | num: NumArg; 28 | } 29 | 30 | export interface TextDoubleArg extends QueryObject { 31 | text: TextArg; 32 | num: NumArg; 33 | } 34 | /** @ignore */ 35 | type TextSearchFunction = (arg: TextSearchArgs) => string; 36 | type TextReplaceFunction = (arg: TextReplaceArgs) => string; 37 | type TextSubFunction = (arg: TextSubArgs) => string; 38 | type TextConcatFunction = (args: TextArg[]) => string; 39 | type TextDoubleArgFunc = (arg: TextDoubleArg) => string; 40 | type TextSingleArgFunc = (arg: TextArg) => string; 41 | type TextMidFunc = (arg: TextMidArgs) => string; 42 | export interface TextSearchFunctions 43 | extends Record { 44 | $find: TextSearchFunction; 45 | $search: TextSearchFunction; 46 | } 47 | export interface TextReplaceFunctions 48 | extends Record { 49 | $replace: TextReplaceFunction; 50 | } 51 | export interface TextSubFunctions extends Record { 52 | $substitute: TextSubFunction; 53 | } 54 | export interface TextConcatFunctions 55 | extends Record { 56 | $concatenate: TextConcatFunction; 57 | } 58 | 59 | export interface TextDoubleArgumentFunctions 60 | extends Record { 61 | $left: TextDoubleArgFunc; 62 | $right: TextDoubleArgFunc; 63 | $rept: TextDoubleArgFunc; 64 | } 65 | 66 | export interface TextMidFunction extends Record { 67 | $mid: TextMidFunc; 68 | } 69 | 70 | export interface TextSingleArgumentFunctions 71 | extends Record { 72 | $encodeUrlComponent: TextSingleArgFunc; 73 | $len: TextSingleArgFunc; 74 | $lower: TextSingleArgFunc; 75 | $trim: TextSingleArgFunc; 76 | $upper: TextSingleArgFunc; 77 | } 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "declaration": true, 5 | "outDir": "./lib", 6 | "resolveJsonModule": true, 7 | "moduleResolution": "node", 8 | "module": "CommonJS", 9 | "strict": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "useUnknownInCatchVariables": false 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "**/tests/*"] 16 | } 17 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./src/asyncAirtable.ts", "./src/types/index.ts"], 3 | "out": "docs", 4 | "includeVersion": true 5 | } 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/asyncAirtable.ts', 5 | devtool: 'inline-source-map', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/, 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['.ts'], 17 | }, 18 | output: { 19 | filename: 'asyncAirtable.js', 20 | path: path.resolve(__dirname, 'dist'), 21 | }, 22 | optimization: { 23 | minimize: false, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /webpack.config.min.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/asyncAirtable.ts', 5 | devtool: 'inline-source-map', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/, 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['.ts'], 17 | }, 18 | output: { 19 | filename: 'asyncAirtable.min.js', 20 | path: path.resolve(__dirname, 'dist'), 21 | }, 22 | }; 23 | --------------------------------------------------------------------------------