├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── lint-and-test.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── dev ├── build-e2e.mjs └── utils.mjs ├── example ├── README.md ├── script │ ├── create-etch-packet.js │ ├── download-documents.js │ ├── fill-pdf.js │ ├── generate-etch-sign-url.js │ ├── generate-html-to-pdf.js │ ├── generate-markdown-pdf.js │ └── get-etch-packet.js └── static │ └── test-pdf-nda.pdf ├── index.js ├── issue_template.md ├── package.json ├── pull_request_template.md ├── src ├── UploadWithOptions.js ├── errors.js ├── graphql │ ├── index.js │ ├── mutations │ │ ├── createEtchPacket.js │ │ ├── forgeSubmit.js │ │ ├── generateEtchSignUrl.js │ │ ├── index.js │ │ └── removeWeldData.js │ └── queries │ │ ├── etchPacket.js │ │ └── index.js ├── index.js └── validation.js ├── test ├── .eslintrc.js ├── assets │ └── dummy.pdf ├── e2e │ ├── .gitignore │ ├── node-anvil.tgz │ └── package.json ├── environment.js ├── index.test.js ├── mocha.js └── setup.js ├── tsconfig.json ├── types └── src │ ├── UploadWithOptions.d.ts │ ├── UploadWithOptions.d.ts.map │ ├── errors.d.ts │ ├── errors.d.ts.map │ ├── graphql │ ├── index.d.ts │ ├── index.d.ts.map │ ├── mutations │ │ ├── createEtchPacket.d.ts │ │ ├── createEtchPacket.d.ts.map │ │ ├── forgeSubmit.d.ts │ │ ├── forgeSubmit.d.ts.map │ │ ├── generateEtchSignUrl.d.ts │ │ ├── generateEtchSignUrl.d.ts.map │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── removeWeldData.d.ts │ │ └── removeWeldData.d.ts.map │ └── queries │ │ ├── etchPacket.d.ts │ │ ├── etchPacket.d.ts.map │ │ ├── index.d.ts │ │ └── index.d.ts.map │ ├── index.d.ts │ ├── index.d.ts.map │ ├── validation.d.ts │ └── validation.d.ts.map └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['nicenice'] 3 | } 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - newhouse 10 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Lint And Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | env: 9 | GITHUB_SHA: ${{ github.event.pull_request.head.sha }} 10 | TARBALL_PATH: test/e2e/node-anvil.tgz 11 | 12 | jobs: 13 | 14 | # Several things need this, so we do it up front once for caching/performance 15 | prepare-node: 16 | name: Prepare Node 17 | runs-on: ubuntu-latest 18 | outputs: 19 | build-node-version: ${{ steps.setup-node.outputs.node-version }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Setup Node.js 25 | id: setup-node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version-file: '.nvmrc' 29 | cache: 'yarn' 30 | - run: yarn install 31 | 32 | lint: 33 | name: Lint 34 | runs-on: ubuntu-latest 35 | needs: prepare-node 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version-file: '.nvmrc' 44 | cache: 'yarn' 45 | 46 | - run: yarn install 47 | - run: yarn lint:quiet 48 | 49 | unit-test: 50 | name: Unit Test 51 | runs-on: ubuntu-latest 52 | needs: 53 | - prepare-node 54 | 55 | steps: 56 | - uses: actions/checkout@v3 57 | 58 | - name: Setup Node.js 59 | id: setup-node 60 | uses: actions/setup-node@v3 61 | with: 62 | node-version-file: '.nvmrc' 63 | cache: 'yarn' 64 | - run: yarn install 65 | - run: yarn test 66 | 67 | build-e2e-package: 68 | name: Build E2E Package 69 | runs-on: ubuntu-latest 70 | needs: prepare-node 71 | 72 | steps: 73 | - uses: actions/checkout@v3 74 | 75 | - name: Setup Node.js 76 | id: setup-node 77 | uses: actions/setup-node@v3 78 | with: 79 | node-version-file: '.nvmrc' 80 | cache: 'yarn' 81 | 82 | - name: Cache Package Build 83 | uses: actions/cache@v3 84 | with: 85 | # We'll cache this file 86 | path: ${{ env.TARBALL_PATH }} 87 | key: ${{ runner.os }}-node-${{ needs.prepare-node.outputs.build-node-version }}-${{ env.GITHUB_SHA }} 88 | 89 | - run: yarn install 90 | - run: yarn test-e2e:build 91 | 92 | supported-version-sanity-checks: 93 | name: Sanity Checks 94 | runs-on: ubuntu-latest 95 | needs: 96 | - prepare-node 97 | - build-e2e-package 98 | strategy: 99 | matrix: 100 | node-version: [14, 16, 18, 20] 101 | include: 102 | - node-version: 14 103 | npm-version: 7 104 | steps: 105 | - uses: actions/checkout@v3 106 | 107 | - name: Setup Node.js 108 | id: setup-node 109 | uses: actions/setup-node@v3 110 | with: 111 | node-version: ${{ matrix.node-version }} 112 | cache: 'yarn' 113 | 114 | - name: Restore Cached Package Build 115 | uses: actions/cache@v3 116 | with: 117 | # This is the file to cache / restore 118 | path: ${{ env.TARBALL_PATH }} 119 | key: ${{ runner.os }}-node-${{ needs.prepare-node.outputs.build-node-version }}-${{ env.GITHUB_SHA }} 120 | 121 | # Some versions of Node (like Node 14) ships with a version of NPM that does not work for us 122 | # so we need to install a specific version 123 | - name: Optionally update NPM if needed 124 | if: ${{ matrix.npm-version }} 125 | run: npm i -g npm@${{ matrix.npm-version }} 126 | # Just make sure it installs correctly for now and we'll call it good. No testing. 127 | - run: yarn test-e2e:install 128 | 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Babel's output 2 | dist/ 3 | 4 | yarn-error.log 5 | 6 | *.DS_Store 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Output of 'npm pack' 12 | *.tgz 13 | 14 | .DS_STORE 15 | *.log 16 | *.zip 17 | 18 | node_modules 19 | package-lock.json 20 | example/script/*.pdf 21 | example/script/*.json 22 | 23 | scratch/ 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | example/ 3 | yarn.lock 4 | .eslintrc.js 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [v3.3.2] 9 | - Update the default `createEtchPacket` mutation to support `allowUpdates`[`#502`](https://github.com/anvilco/node-anvil/pull/502) 10 | 11 | ## [v3.3.1] 12 | - Added a `NodeError` type. https://github.com/anvilco/node-anvil/issues/476 13 | - Updated various dependencies 14 | 15 | ## [v3.3.0] 16 | - Add support for `arrayBuffer` response type. 17 | - Use `arrayBuffer` type underneath `buffer` response type to stop triggering `buffer` deprecation warning. https://github.com/anvilco/node-anvil/pull/442 18 | - Updated various dependencies 19 | 20 | ## [v3.2.0] 21 | - Swap out `node-fetch` for `@anvilco/node-fetch` in order to fix `Premature close`. https://github.com/anvilco/node-fetch/pull/1 22 | - Fixed bug around some error handling. https://github.com/anvilco/node-anvil/issues/376 23 | - Updated various dependencies 24 | 25 | ## [v3.1.0] 26 | - Update `node-fetch` to latest and drop `form-data`. https://github.com/anvilco/node-anvil/issues/239 27 | - Update various dependencies 28 | 29 | ## [v3.0.1] 30 | - Fix "double-default" ESM exports, but also leave for backwards compatibility. https://github.com/anvilco/node-anvil/issues/354 31 | 32 | ## [v3.0.0] 33 | - BREAKING: Drop support for `Node 12`. 34 | - BREAKING: Changed to `ES6` style exports. 35 | - Add support for `Node 20`. 36 | - Babelizing the source to `/dist`. 37 | - Dropped some dev dependencies. 38 | - Dependency updates. 39 | 40 | ## [v2.15.1] 41 | - Non-dynamic `require`s in GraphQL modules. 42 | - Dependency updates. 43 | 44 | ## [v2.15.0] 45 | - Assert that the `filename` property is (likely) there [`312`](https://github.com/anvilco/node-anvil/pull/312) 46 | - Dependency updates. 47 | 48 | ## [v2.14.1] 49 | - Dependency updates. 50 | 51 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 52 | 53 | ## [v2.14.0](https://github.com/anvilco/node-anvil/compare/v2.13.1...v2.14.0) 54 | 55 | ### Merged 56 | 57 | - roll back node-fetch to not warn about form-data [`#238`](https://github.com/anvilco/node-anvil/pull/238) 58 | - [1-min] update extract files [`#236`](https://github.com/anvilco/node-anvil/pull/236) 59 | - Bump node-fetch from 2.6.7 to 3.3.0 [`#212`](https://github.com/anvilco/node-anvil/pull/212) 60 | - Bump eslint-plugin-promise from 6.0.0 to 6.1.1 [`#204`](https://github.com/anvilco/node-anvil/pull/204) 61 | - Bump @types/node from 17.0.35 to 18.11.18 [`#226`](https://github.com/anvilco/node-anvil/pull/226) 62 | - Bump @babel/eslint-parser from 7.18.2 to 7.19.1 [`#188`](https://github.com/anvilco/node-anvil/pull/188) 63 | - Bump eslint-plugin-react from 7.30.0 to 7.32.1 [`#235`](https://github.com/anvilco/node-anvil/pull/235) 64 | - Bump typescript from 4.7.2 to 4.9.4 [`#222`](https://github.com/anvilco/node-anvil/pull/222) 65 | - Bump json5 from 1.0.1 to 1.0.2 [`#228`](https://github.com/anvilco/node-anvil/pull/228) 66 | - Bump eslint-plugin-n from 15.2.0 to 15.6.1 [`#232`](https://github.com/anvilco/node-anvil/pull/232) 67 | - Bump eslint from 8.16.0 to 8.32.0 [`#230`](https://github.com/anvilco/node-anvil/pull/230) 68 | - Bump @babel/core from 7.18.2 to 7.20.12 [`#229`](https://github.com/anvilco/node-anvil/pull/229) 69 | - Update queries and mutations [`#234`](https://github.com/anvilco/node-anvil/pull/234) 70 | 71 | ## [v2.13.1](https://github.com/anvilco/node-anvil/compare/v2.13.0...v2.13.1) 72 | 73 | ### Merged 74 | 75 | - Remove `?.` usage to support older node versions [`#181`](https://github.com/anvilco/node-anvil/pull/181) 76 | 77 | ## [v2.13.0](https://github.com/anvilco/node-anvil/compare/v2.12.0...v2.13.0) - 2022-09-09 78 | 79 | ### Merged 80 | 81 | - Updated types [`#180`](https://github.com/anvilco/node-anvil/pull/180) 82 | - Add `versionNumber` support for fillPDF [`#179`](https://github.com/anvilco/node-anvil/pull/179) 83 | 84 | ### Commits 85 | 86 | - Add VERSION_LATEST_PUBLISHED, documentation [`ffa8ab7`](https://github.com/anvilco/node-anvil/commit/ffa8ab70ae9dad186a5989d5d2a9829aa7b3cd25) 87 | 88 | ## [v2.12.0](https://github.com/anvilco/node-anvil/compare/v2.11.1...v2.12.0) - 2022-07-14 89 | 90 | ### Merged 91 | 92 | - Improve TypeScript types [`#153`](https://github.com/anvilco/node-anvil/pull/153) 93 | 94 | ### Commits 95 | 96 | - Bump nodemon from 2.0.16 to 2.0.19 [`d4c7b2a`](https://github.com/anvilco/node-anvil/commit/d4c7b2ab418f610321edd20c194f3715a04cc981) 97 | - More response types [`0d3c332`](https://github.com/anvilco/node-anvil/commit/0d3c332f557e716ad6633922ae33f4fa5ea5cb91) 98 | - Improve optional arguments [`19f6365`](https://github.com/anvilco/node-anvil/commit/19f6365e1a8fb865d291ff6eb2c754f09c237b88) 99 | 100 | ## [v2.11.1](https://github.com/anvilco/node-anvil/compare/v2.11.0...v2.11.1) - 2022-05-25 101 | 102 | ### Merged 103 | 104 | - Upgrade the babel deps [`#129`](https://github.com/anvilco/node-anvil/pull/129) 105 | - Upgrade dependencies [`#126`](https://github.com/anvilco/node-anvil/pull/126) 106 | - Fix readme broken link [`#125`](https://github.com/anvilco/node-anvil/pull/125) 107 | - Update Readme to include Anvil description [`#124`](https://github.com/anvilco/node-anvil/pull/124) 108 | 109 | ### Commits 110 | 111 | - Upgrade packages [`fcbe1ab`](https://github.com/anvilco/node-anvil/commit/fcbe1ab8310b89faa86ac5bcdd88b60cb52bb75e) 112 | - Fix lint errors [`a8d7c8b`](https://github.com/anvilco/node-anvil/commit/a8d7c8baad6a74c52f96ac3ef09c79f9c4092898) 113 | - Integrate the blurb [`6c24f32`](https://github.com/anvilco/node-anvil/commit/6c24f322a8a2aa98008c991b37890785dcec50a7) 114 | 115 | ## [v2.11.0](https://github.com/anvilco/node-anvil/compare/v2.10.1...v2.11.0) - 2022-05-11 116 | 117 | ### Merged 118 | 119 | - Add mergePDFs support to createEtchPacket [`#119`](https://github.com/anvilco/node-anvil/pull/119) 120 | - Bump sinon from 13.0.1 to 13.0.2 [`#108`](https://github.com/anvilco/node-anvil/pull/108) 121 | - Bump @types/node from 17.0.23 to 17.0.25 [`#109`](https://github.com/anvilco/node-anvil/pull/109) 122 | 123 | ### Commits 124 | 125 | - Update example script with mergePDFs info [`9e69cfa`](https://github.com/anvilco/node-anvil/commit/9e69cfa72dfa8fc202525e13ba1c9f87b9c8c0bc) 126 | - Add mergePDFs support on mutation [`5ea0557`](https://github.com/anvilco/node-anvil/commit/5ea05574a7f1c48cd24ee33ccf8feb2cd55c6ce9) 127 | 128 | ## [v2.10.1](https://github.com/anvilco/node-anvil/compare/v2.10.0...v2.10.1) - 2022-04-05 129 | 130 | ### Merged 131 | 132 | - Fix types path, Anvil options params [`#103`](https://github.com/anvilco/node-anvil/pull/103) 133 | 134 | ### Commits 135 | 136 | - JSDoc `?` isn't the same as the TS version apparently [`b8636bf`](https://github.com/anvilco/node-anvil/commit/b8636bf511b56657cbf880e35677e59367596e19) 137 | - Fix types path. Generated includes `src/`. [`b11d336`](https://github.com/anvilco/node-anvil/commit/b11d33645a7c7aba5518348b9dc512b71622ce55) 138 | 139 | ## [v2.10.0](https://github.com/anvilco/node-anvil/compare/v2.9.4...v2.10.0) - 2022-04-04 140 | 141 | ### Merged 142 | 143 | - Add initial Typescript support [`#102`](https://github.com/anvilco/node-anvil/pull/102) 144 | 145 | ### Commits 146 | 147 | - Add JSDoc comments for some func params [`4c28568`](https://github.com/anvilco/node-anvil/commit/4c285686efb0c27549da97dae72ceb3ac5ad4b58) 148 | - Move dist -> types, "types" in package.json [`627b5c1`](https://github.com/anvilco/node-anvil/commit/627b5c1734df38568a21ba7fb00cb34c5ff39065) 149 | - Add typescript types [`8e25ca4`](https://github.com/anvilco/node-anvil/commit/8e25ca4e830ad80b9cdc4abe8a9a6ad8847f645d) 150 | 151 | ## [v2.9.4](https://github.com/anvilco/node-anvil/compare/v2.9.3...v2.9.4) - 2022-03-25 152 | 153 | ### Merged 154 | 155 | - Update the readme intro to be more informative [`#101`](https://github.com/anvilco/node-anvil/pull/101) 156 | 157 | ### Commits 158 | 159 | - Update the intro to be more informative [`fffcbbc`](https://github.com/anvilco/node-anvil/commit/fffcbbc89910280a39c46036167d0c1d04a7ba2e) 160 | 161 | ## [v2.9.3](https://github.com/anvilco/node-anvil/compare/v2.2.0...v2.9.3) - 2022-03-25 162 | 163 | ### Merged 164 | 165 | - Upgrade dependencies [`#99`](https://github.com/anvilco/node-anvil/pull/99) 166 | - Bump mocha from 8.1.3 to 9.1.3 [`#70`](https://github.com/anvilco/node-anvil/pull/70) 167 | - Bump sinon from 9.0.1 to 11.1.2 [`#66`](https://github.com/anvilco/node-anvil/pull/66) 168 | - Bump eslint from 6.8.0 to 7.32.0 [`#74`](https://github.com/anvilco/node-anvil/pull/74) 169 | - Bump eslint-plugin-node from 11.0.0 to 11.1.0 [`#77`](https://github.com/anvilco/node-anvil/pull/77) 170 | - Bump auto-changelog from 2.2.1 to 2.3.0 [`#75`](https://github.com/anvilco/node-anvil/pull/75) 171 | - Bump eslint-plugin-promise from 4.2.1 to 4.3.1 [`#73`](https://github.com/anvilco/node-anvil/pull/73) 172 | - Bump eslint-plugin-standard from 4.0.1 to 5.0.0 [`#61`](https://github.com/anvilco/node-anvil/pull/61) 173 | - Bump nodemon from 2.0.4 to 2.0.14 [`#72`](https://github.com/anvilco/node-anvil/pull/72) 174 | - Bump chai from 4.2.0 to 4.3.4 [`#64`](https://github.com/anvilco/node-anvil/pull/64) 175 | - Bump eslint-plugin-no-only-tests from 2.4.0 to 2.6.0 [`#68`](https://github.com/anvilco/node-anvil/pull/68) 176 | - Bump bdd-lazy-var from 2.5.4 to 2.6.1 [`#69`](https://github.com/anvilco/node-anvil/pull/69) 177 | - Bump eslint-plugin-import from 2.20.0 to 2.25.2 [`#71`](https://github.com/anvilco/node-anvil/pull/71) 178 | - Handle different error patterns better [`#54`](https://github.com/anvilco/node-anvil/pull/54) 179 | - Bump babel-eslint from 10.0.3 to 10.1.0 [`#60`](https://github.com/anvilco/node-anvil/pull/60) 180 | - Bump sinon-chai from 3.5.0 to 3.7.0 [`#62`](https://github.com/anvilco/node-anvil/pull/62) 181 | - Bump eslint-plugin-react from 7.18.0 to 7.26.1 [`#63`](https://github.com/anvilco/node-anvil/pull/63) 182 | - Bump eslint-config-standard from 14.1.0 to 14.1.1 [`#59`](https://github.com/anvilco/node-anvil/pull/59) 183 | - [1-min] add dependabot yaml [`#55`](https://github.com/anvilco/node-anvil/pull/55) 184 | - Bump path-parse from 1.0.6 to 1.0.7 [`#52`](https://github.com/anvilco/node-anvil/pull/52) 185 | - Improve docs [`#51`](https://github.com/anvilco/node-anvil/pull/51) 186 | - Bump glob-parent from 5.1.0 to 5.1.2 [`#49`](https://github.com/anvilco/node-anvil/pull/49) 187 | - Bump normalize-url from 4.5.0 to 4.5.1 [`#50`](https://github.com/anvilco/node-anvil/pull/50) 188 | - Adjust rate limiting dynamically based on API response [`#48`](https://github.com/anvilco/node-anvil/pull/48) 189 | - bump handlebars to 4.7.7 [`#47`](https://github.com/anvilco/node-anvil/pull/47) 190 | - Bump hosted-git-info from 2.8.5 to 2.8.9 [`#46`](https://github.com/anvilco/node-anvil/pull/46) 191 | - Bump lodash from 4.17.19 to 4.17.21 [`#45`](https://github.com/anvilco/node-anvil/pull/45) 192 | - [security] update lockfile to fix security vuln [`#42`](https://github.com/anvilco/node-anvil/pull/42) 193 | - Allow passing ratelimit options to the client [`#41`](https://github.com/anvilco/node-anvil/pull/41) 194 | - allow null values for forge update mutation [`#37`](https://github.com/anvilco/node-anvil/pull/37) 195 | - add forgeSubmit and removeWeldData [`#40`](https://github.com/anvilco/node-anvil/pull/40) 196 | - Bump y18n from 4.0.0 to 4.0.1 [`#39`](https://github.com/anvilco/node-anvil/pull/39) 197 | - fix a bug that can happen when parsing errors in node client [`#38`](https://github.com/anvilco/node-anvil/pull/38) 198 | - Update createEtchPacket example to use IRS W-4 / NDA [`#36`](https://github.com/anvilco/node-anvil/pull/36) 199 | - Bump ini from 1.3.5 to 1.3.8 [`#34`](https://github.com/anvilco/node-anvil/pull/34) 200 | - v2.6.0 [`#33`](https://github.com/anvilco/node-anvil/pull/33) 201 | - Add webhookURL to default createEtchPacket mutation [`#32`](https://github.com/anvilco/node-anvil/pull/32) 202 | - v2.5.0 [`#31`](https://github.com/anvilco/node-anvil/pull/31) 203 | - Add generatePDF() function [`#30`](https://github.com/anvilco/node-anvil/pull/30) 204 | - v2.4.0 [`#29`](https://github.com/anvilco/node-anvil/pull/29) 205 | - Add `signatureProvider` to `createEtchPacket` Mutation [`#28`](https://github.com/anvilco/node-anvil/pull/28) 206 | - v2.3.0 [`#27`](https://github.com/anvilco/node-anvil/pull/27) 207 | - Add getEtchPacket and downloadDocuments Methods [`#26`](https://github.com/anvilco/node-anvil/pull/26) 208 | 209 | ### Commits 210 | 211 | - Upgrade more [`951cdfa`](https://github.com/anvilco/node-anvil/commit/951cdfa9cdc69194f9e5ed72f10be167f8b18459) 212 | - Update the create-etch-packet example to use the same docs as demo [`43bc77d`](https://github.com/anvilco/node-anvil/commit/43bc77d766e59a9c9d713958dfa460a51b246190) 213 | - tests for downloadDocuments, generateEtchSignUrl, getEtchPacket [`dec02fb`](https://github.com/anvilco/node-anvil/commit/dec02fb1831b148a7b23ddee4c85453cfe1f6c75) 214 | 215 | ## [v2.2.0](https://github.com/anvilco/node-anvil/compare/v1.0.3...v2.2.0) - 2020-09-29 216 | 217 | ### Merged 218 | 219 | - Cut new version: v2.2.0 [`#25`](https://github.com/anvilco/node-anvil/pull/25) 220 | - Add `aliasId` AND `routingOrder` to `createEtchPacket` mutation query response default [`#24`](https://github.com/anvilco/node-anvil/pull/24) 221 | - rename send to draft (and flip the logic) in createEtchPacket [`#23`](https://github.com/anvilco/node-anvil/pull/23) 222 | - no organizationEid allowed when calling via API key [`#22`](https://github.com/anvilco/node-anvil/pull/22) 223 | - change fillPayload param to just data [`#21`](https://github.com/anvilco/node-anvil/pull/21) 224 | - Fix file upload related stuff to reflect reality [`#20`](https://github.com/anvilco/node-anvil/pull/20) 225 | - Etch API Support for Embedded Signers [`#18`](https://github.com/anvilco/node-anvil/pull/18) 226 | - Bump node-fetch from 2.6.0 to 2.6.1 [`#19`](https://github.com/anvilco/node-anvil/pull/19) 227 | - Add support for GraphQL API, starting with createEtchPacket [`#16`](https://github.com/anvilco/node-anvil/pull/16) 228 | - v2.1.0 [`#14`](https://github.com/anvilco/node-anvil/pull/14) 229 | - replace `request` with `node-fetch` [`#13`](https://github.com/anvilco/node-anvil/pull/13) 230 | - Bump lodash from 4.17.15 to 4.17.19 [`#12`](https://github.com/anvilco/node-anvil/pull/12) 231 | - Add back in the dirName and add ability to autogen CHANGELOG [`#11`](https://github.com/anvilco/node-anvil/pull/11) 232 | - Use module.exports instead of import/export [`#10`](https://github.com/anvilco/node-anvil/pull/10) 233 | - Target node 8; remove regeneratorRuntime dependency [`#5`](https://github.com/anvilco/node-anvil/pull/5) 234 | - Dependencies update to mitigate security vulnerability [`#9`](https://github.com/anvilco/node-anvil/pull/9) 235 | - Bump acorn from 7.1.0 to 7.1.1 [`#8`](https://github.com/anvilco/node-anvil/pull/8) 236 | 237 | ### Commits 238 | 239 | - don't need babel anymore [`2b9daad`](https://github.com/anvilco/node-anvil/commit/2b9daad80b47a82a5e3493e70b6341aad4bee18d) 240 | - dependency upgrades: nodemon, mocha, and yargs due to dot-prop and yargs-parser vulnerabilities [`c118d23`](https://github.com/anvilco/node-anvil/commit/c118d234c4717a429b8ebda8c1f69e1002c11f56) 241 | - bump deps to get minimist to >= 1.2.2 [`26e4cd1`](https://github.com/anvilco/node-anvil/commit/26e4cd1d9d2f3070309f85f3fb21e5f7cab484fa) 242 | 243 | ## [v1.0.3](https://github.com/anvilco/node-anvil/compare/v1.0.2...v1.0.3) - 2020-02-05 244 | 245 | ### Commits 246 | 247 | - Add registry ovrride for @anvilco [`c2bcdd6`](https://github.com/anvilco/node-anvil/commit/c2bcdd629806124f09b7fe56d13a3d5c301d415c) 248 | - Ignore example [`82b5340`](https://github.com/anvilco/node-anvil/commit/82b53403be3ce73190a3c3cce2a1e1a61b619d03) 249 | 250 | ## [v1.0.2](https://github.com/anvilco/node-anvil/compare/v1.0.1...v1.0.2) - 2020-02-05 251 | 252 | ### Commits 253 | 254 | - Add public NPM registry [`6e5532e`](https://github.com/anvilco/node-anvil/commit/6e5532e148171571e92ca82c7a0babe5f5dabea3) 255 | 256 | ## v1.0.1 - 2020-02-05 257 | 258 | ### Merged 259 | 260 | - Return all errors that come from the server [`#3`](https://github.com/anvilco/node-anvil/pull/3) 261 | - Bo readme changes [`#2`](https://github.com/anvilco/node-anvil/pull/2) 262 | - Few suggestions [`#1`](https://github.com/anvilco/node-anvil/pull/1) 263 | 264 | ### Commits 265 | 266 | - Initial [`186d2f1`](https://github.com/anvilco/node-anvil/commit/186d2f1258a335687fd40a77fa08600c4f2633ba) 267 | - Add initial client and cli example [`09a2873`](https://github.com/anvilco/node-anvil/commit/09a28733142597a0fb4ef13dd8fdbeee50c55c43) 268 | - Add test:watch command [`7f07f15`](https://github.com/anvilco/node-anvil/commit/7f07f15521df392655976d51b9378c1b6c3e6837) 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Horizontal Lockupblack](https://user-images.githubusercontent.com/293079/169453889-ae211c6c-7634-4ccd-8ca9-8970c2621b6f.png#gh-light-mode-only) 2 | ![Horizontal Lockup copywhite](https://user-images.githubusercontent.com/293079/169453892-895f637b-4633-4a14-b997-960c9e17579b.png#gh-dark-mode-only) 3 | 4 | # Anvil API Client for Node 5 | 6 | This library will allow you to interact with the [Anvil API](www.useanvil.com/developers) in JavaScript / NodeJS. 7 | 8 | [Anvil](https://www.useanvil.com/developers/) provides easy APIs for all things paperwork. 9 | 10 | 1. [PDF filling API](https://www.useanvil.com/products/pdf-filling-api/) - fill out a PDF template with a web request and structured JSON data. 11 | 2. [PDF generation API](https://www.useanvil.com/products/pdf-generation-api/) - send markdown or HTML and Anvil will render it to a PDF. 12 | 3. [Etch e-sign with API](https://www.useanvil.com/products/etch/) - customizable, embeddable, e-signature platform with an API to control the signing process end-to-end. 13 | 4. [Anvil Workflows (w/ API)](https://www.useanvil.com/products/workflows/) - Webforms + PDF + e-sign with a powerful no-code builder. Easily collect structured data, generate PDFs, and request signatures. 14 | 15 | Learn more on our [Anvil developer page](https://www.useanvil.com/developers/). See the [API guide](https://www.useanvil.com/docs) and the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for full documentation. 16 | 17 | ## Usage 18 | 19 | This library and the Anvil APIs are intended to be used on a server or server-like environment. It will fail in a browser environment. 20 | 21 | ```sh 22 | yarn add @anvilco/anvil 23 | ``` 24 | 25 | ```sh 26 | npm install @anvilco/anvil 27 | ``` 28 | 29 | A basic example converting your JSON to a filled PDF, then saving the PDF to a file: 30 | 31 | ```js 32 | import fs from 'fs' 33 | import Anvil from '@anvilco/anvil' 34 | 35 | // The ID of the PDF template to fill 36 | const pdfTemplateID = 'kA6Da9CuGqUtc6QiBDRR' 37 | // Your API key from your Anvil organization settings 38 | const apiKey = '7j2JuUWmN4fGjBxsCltWaybHOEy3UEtt' 39 | 40 | // JSON data to fill the PDF 41 | const exampleData = { 42 | "title": "My PDF Title", 43 | "fontSize": 10, 44 | "textColor": "#CC0000", 45 | "data": { 46 | "someFieldId": "Hello World!" 47 | } 48 | } 49 | const anvilClient = new Anvil({ apiKey }) 50 | const { statusCode, data } = await anvilClient.fillPDF(pdfTemplateID, exampleData) 51 | 52 | // By default, if the PDF has been published then the published version is what will 53 | // be filled. If the PDF has not been published, then the most recent version will 54 | // be filled. 55 | // 56 | // However, a version number can also be passed in that will be used retrieve and 57 | // fill a specific version of the PDF. 58 | // You can also use the constant `Anvil.VERSION_LATEST` (or `-1`) to fill the most 59 | // recent version of your PDF, whether that version has been published or not. 60 | // Use this if you'd like to fill out a draft version of your template/PDF. 61 | const options = { versionNumber: Anvil.VERSION_LATEST } 62 | const { statusCode, data } = await anvilClient.fillPDF(pdfTemplateID, exampleData, options) 63 | 64 | console.log(statusCode) // => 200 65 | 66 | // Data will be the filled PDF raw bytes 67 | fs.writeFileSync('output.pdf', data, { encoding: null }) 68 | ``` 69 | 70 | ## API 71 | 72 | * [Constructor](#new-anviloptions) 73 | * [fillPDF(pdfTemplateID, payload[, options])](#fillpdfpdftemplateid-payload-options) 74 | * [generatePDF(payload[, options])](#generatepdfpayload-options) 75 | * [createEtchPacket(options)](#createetchpacketoptions) 76 | * [getEtchPacket(options)](#getetchpacketoptions) 77 | * [generateEtchSignURL(options)](#generateetchsignurloptions) 78 | * [downloadDocuments(documentGroupEid[, options])](#downloaddocumentsdocumentgroupeid-options) 79 | * [requestGraphQL(queryInfo[, options])](#requestgraphqlqueryinfo-options) 80 | * [requestREST(url, fetchOptions[, clientOptions])](#requestresturl-fetchoptions-clientoptions) 81 | 82 | ### Instance Methods 83 | 84 | ##### new Anvil(options) 85 | 86 | Creates an Anvil client instance. 87 | 88 | * `options` (Object) - [Options](#options) for the Anvil Client instance. 89 | 90 | ```js 91 | const anvilClient = new Anvil({ apiKey: 'abc123' }) 92 | ``` 93 |
94 | 95 | ##### fillPDF(pdfTemplateID, payload[, options]) 96 | 97 | Fills a PDF template with your JSON data. 98 | 99 | First, you will need to have [uploaded a PDF to Anvil](https://useanvil.com/docs/api/fill-pdf#creating-a-pdf-template). You can find the PDF template's id on the `API Info` tab of your PDF template's page: 100 | 101 | pdf-template-id 102 | 103 | An example: 104 | 105 | ```js 106 | const fs = require('fs') 107 | 108 | // PDF template you uploaded to Anvil 109 | const pdfTemplateID = 'kA6Da9CuGqUtc6QiBDRR' 110 | 111 | // Your API key from your Anvil organization settings 112 | const apiKey = '7j2JuUWmN4fGjBxsCltWaybHOEy3UEtt' 113 | 114 | // JSON data to fill the PDF 115 | const payload = { 116 | "title": "My PDF Title", 117 | "fontSize": 10, 118 | "textColor": "#CC0000", 119 | "data": { 120 | "someFieldId": "Hello World!" 121 | } 122 | } 123 | // The 'options' parameter is optional 124 | const options = { 125 | "dataType": "buffer" 126 | } 127 | const anvilClient = new Anvil({ apiKey }) 128 | const { statusCode, data } = await anvilClient.fillPDF(pdfTemplateID, payload, options) 129 | 130 | // Be sure to write the file as raw bytes 131 | fs.writeFileSync('filled.pdf', data, { encoding: null }) 132 | ``` 133 | 134 | * `pdfTemplateID` (String) - The id of your PDF template from the Anvil UI 135 | * `payload` (Object) - The JSON data that will fill the PDF template 136 | * `title` (String) - _optional_ Set the title encoded into the PDF document 137 | * `fontSize` (Number) - _optional_ Set the fontSize of all filled text. Default is 10. 138 | * `color` (String) - _optional_ Set the text color of all filled text. Default is dark blue. 139 | * `data` (Object) - The data to fill the PDF. The keys in this object will correspond to a field's ID in the PDF. These field IDs and their types are available on the `API Info` tab on your PDF template's page in the Anvil dashboard. 140 | * For example `{ "someFieldId": "Hello World!" }` 141 | * `options` (Object) - _optional_ Any additional options for the request 142 | * `dataType` (Enum[String]) - _optional_ Set the type of the `data` value that is returned in the resolved `Promise`. Defaults to `'buffer'`, but `'arrayBuffer'` and `'stream'` are also supported. 143 | * Returns a `Promise` that resolves to an `Object` 144 | * `statusCode` (Number) - the HTTP status code; `200` is success 145 | * `data` (Buffer | ArrayBuffer | Stream) - The raw binary data of the filled PDF if success. Will be either a Buffer, ArrayBuffer, or a Stream, depending on `dataType` option supplied to the request. 146 | * `errors` (Array of Objects) - Will be present if status >= 400. See Errors 147 | * `message` (String) 148 | 149 | ##### generatePDF(payload[, options]) 150 | 151 | Dynamically generate a new PDF from your HTML and CSS or markdown. 152 | 153 | Useful for agreements, invoices, disclosures, or any other text-heavy documents. This does not require you do anything in the Anvil UI other than setup your API key, just send it data, get a PDF. See [the generate PDF docs](https://useanvil.com/api/generate-pdf) for full details. 154 | 155 | * [HTML to PDF docs](https://www.useanvil.com/docs/api/generate-pdf#html--css-to-pdf) 156 | * [Markdown to PDF docs](https://www.useanvil.com/docs/api/generate-pdf#markdown-to-pdf) 157 | 158 | Check out our [HTML invoice template](https://github.com/anvilco/html-pdf-invoice-template) for a complete HTML to PDF example. 159 | 160 | An example: 161 | 162 | ```js 163 | const fs = require('fs') 164 | 165 | // Your API key from your Anvil organization settings 166 | const apiKey = '7j2JuUWmN4fGjBxsCltWaybHOEy3UEtt' 167 | 168 | // An example using an HTML to PDF payload 169 | const payload = { 170 | title: 'Example', 171 | type: 'html', 172 | data: { 173 | html: ` 174 |

What is Lorem Ipsum?

175 |

176 | Lorem Ipsum is simply dummy text... 177 |

178 |

Where does it come from?

179 |

180 | Contrary to popular belief, Lorem Ipsum is not simply random text 181 |

182 | `, 183 | css: ` 184 | body { font-size: 14px; color: #171717; } 185 | .header-one { text-decoration: underline; } 186 | .header-two { font-style: underline; } 187 | `, 188 | }, 189 | } 190 | 191 | // An example using a Markdown payload 192 | const payload = { 193 | title: 'Example Invoice', 194 | data: [{ 195 | label: 'Name', 196 | content: 'Sally Jones', 197 | }, { 198 | content: 'Lorem **ipsum** dolor sit _amet_', 199 | }, { 200 | table: { 201 | firstRowHeaders: true, 202 | rows: [ 203 | ['Description', 'Quantity', 'Price'], 204 | ['4x Large Widgets', '4', '$40.00'], 205 | ['10x Medium Sized Widgets in dark blue', '10', '$100.00'], 206 | ['10x Small Widgets in white', '6', '$60.00'], 207 | ], 208 | }, 209 | }], 210 | } 211 | // The 'options' parameter is optional 212 | const options = { 213 | "dataType": "buffer" 214 | } 215 | const anvilClient = new Anvil({ apiKey }) 216 | const { statusCode, data } = await anvilClient.generatePDF(payload, options) 217 | 218 | // Be sure to write the file as raw bytes 219 | fs.writeFileSync('generated.pdf', data, { encoding: null }) 220 | ``` 221 | 222 | * `payload` (Object) - The JSON data that will fill the PDF template 223 | * `title` (String) - _optional_ Set the title encoded into the PDF document 224 | * `data` (Array of Objects) - The data that generates the PDF. See [the docs](https://useanvil.com/docs/api/generate-pdf#supported-format-of-data) for all supported objects 225 | * For example `[{ "label": "Hello World!", "content": "Test" }]` 226 | * `options` (Object) - _optional_ Any additional options for the request 227 | * `dataType` (Enum[String]) - _optional_ Set the type of the `data` value that is returned in the resolved `Promise`. Defaults to `'buffer'`, but `'arrayBuffer'` and `'stream'` are also supported. 228 | * Returns a `Promise` that resolves to an `Object` 229 | * `statusCode` (Number) - the HTTP status code; `200` is success 230 | * `data` (Buffer | ArrayBuffer | Stream) - The raw binary data of the filled PDF if success. Will be either a Buffer, ArrayBuffer, or a Stream, depending on `dataType` option supplied to the request. 231 | * `errors` (Array of Objects) - Will be present if status >= 400. See Errors 232 | * `message` (String) 233 | 234 | ##### createEtchPacket(options) 235 | 236 | Creates an Etch Packet and optionally sends it to the first signer. 237 | 238 | * `options` (Object) - An object with the following structure: 239 | * `variables` (Object) - See the [API Documentation](#api-documentation) area for details. See [Examples](#examples) area for examples. 240 | * `responseQuery` (String) - _optional_ A GraphQL Query compliant query to use for the data desired in the mutation response. Can be left out to use default. 241 | * `mutation` (String) - _optional_ If you'd like complete control of the GraphQL mutation, you can pass in a GraphQL Mutation compliant string that will be used in the mutation call. This string should also include your response query, as the `responseQuery` param is ignored if `mutation` is passed. Example: 242 | ```graphql 243 | mutation CreateEtchPacket ( 244 | $name: String, 245 | ... 246 | ) { 247 | createEtchPacket ( 248 | name: $name, 249 | ... 250 | ) { 251 | id 252 | eid 253 | ... 254 | } 255 | } 256 | ``` 257 | 258 | ##### getEtchPacket(options) 259 | 260 | Gets the details of an Etch Packet. 261 | * `options` (Object) - An object with the following structure: 262 | * `variables` (Object) - Requires `eid` 263 | * `eid` (String) - your Etch Packet eid 264 | * `responseQuery` (String) - _optional_ A GraphQL Query compliant query to use for the data desired in the query response. Can be left out to use default. 265 | 266 | ##### generateEtchSignUrl(options) 267 | 268 | Generates an Etch sign URL for an Etch Packet signer. The Etch Packet and its signers must have already been created. 269 | * `options` (Object) - An object with the following structure: 270 | * `variables` (Object) - Requires `clientUserId` and `signerEid` 271 | * `clientUserId` (String) - your user eid 272 | * `signerEid` (String) - the eid of the Etch Packet signer, found in the response of the `createEtchPacket` instance method 273 | 274 | ##### downloadDocuments(documentGroupEid[, options]) 275 | 276 | Returns a Buffer, ArrayBuffer, or Stream of the document group specified by the documentGroupEid in Zip file format. 277 | * `documentGroupEid` (string) - the eid of the document group to download 278 | * `options` (Object) - _optional_ Any additional options for the request 279 | * `dataType` (Enum[String]) - _optional_ Set the type of the `data` value that is returned in the resolved `Promise`. Defaults to `'buffer'`, but `'arrayBuffer'` and `'stream'` are also supported. 280 | * Returns a `Promise` that resolves to an `Object` 281 | * `statusCode` (Number) - the HTTP status code, `200` is success 282 | * `response` (Object) - the Response object resulting from the client's request to the Anvil app 283 | * `data` (Buffer | ArrayBuffer | Stream) - The raw binary data of the downloaded documents if success. Will be in the format of either a Buffer, ArrayBuffer, or a Stream, depending on `dataType` option supplied to the request. 284 | * `errors` (Array of Objects) - Will be present if status >= 400. See Errors 285 | * `message` (String) 286 | 287 | 288 | ##### requestGraphQL(queryInfo[, options]) 289 | 290 | A fallback function for queries and mutations without a specialized function in this client. 291 | 292 | See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for a listing on all possible queries 293 | 294 | ```js 295 | const result = await client.requestGraphQL({ 296 | query: ` 297 | query WeldDataQuery ($eid: String!) { 298 | weldData (eid: $eid) { 299 | eid 300 | isComplete 301 | isTest 302 | } 303 | } 304 | `, 305 | variables: { eid: 'nxflNZqxDUbltLUbYWK' }, 306 | }) 307 | const statusCode = result.statusCode 308 | const httpErrors = result.errors 309 | 310 | // These will only be available if the statusCode === 200 311 | const graphqlErrors = result.data.errors 312 | const resultObject = result.data.data.weldData 313 | ``` 314 | 315 | * `queryInfo` (Object) - The JSON data that will fill the PDF template 316 | * `query` (String) - GraphQL query or mutation to run. See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for a listing on all possible queries 317 | * `variables` (Object) - GraphQL variables for the query 318 | * Returns a `Promise` that resolves to an `Object` 319 | * `statusCode` (Number) - 200 when successful or when there is a GraphQL error. You will only see > 200 if your query is not found or malformed 320 | * `errors` (String) - HTTP errors when status code > 200 321 | * `data` (Object) - Contains query result and any GraphQL errors 322 | * `errors` (Array of Objects) - If there are validation errors or errors running the query, they will show here 323 | * `data` (Object) - Contains the actual result of the query 324 | * `[queryName]` (Object) - Use the query or mutation name to reference the data that you requested! 325 | 326 | ##### requestREST(url, fetchOptions[, clientOptions]) 327 | 328 | A fallback function for REST endpoints without a specialized function in this client. 329 | 330 | See the [GraphQL reference](https://www.useanvil.com/docs/api/graphql/reference/) for a listing on all possible queries 331 | 332 | ```js 333 | const result = await this.requestREST( 334 | `/api/v1/fill/${pdfTemplateID}.pdf`, 335 | { 336 | method: 'POST', 337 | body: JSON.stringify(payload), 338 | headers: { 339 | 'Content-Type': 'application/json', 340 | }, 341 | }, 342 | { 343 | dataType: 'stream', 344 | }, 345 | ) 346 | ``` 347 | 348 | * `url` (String) - URL from the baseURL. e.g. `/api/v1/fill` 349 | * `fetchOptions` (Object) - Options passed to [node-fetch](https://github.com/node-fetch/node-fetch) 350 | * `clientOptions` (Object) - _optional_ Any additional options for the request 351 | * `dataType` (Enum[String]) - _optional_ Set the type of the `data` value that is returned in the resolved `Promise`. Defaults to `'buffer'`, `'arrayBuffer'`, `'stream'`, and `'json'` are also supported. 352 | * Returns a `Promise` that resolves to an `Object` 353 | * `statusCode` (Number) - the HTTP status code; `200` is success 354 | * `data` (Buffer | ArrayBuffer | Stream) - The raw binary data of the filled PDF if success. Will be either a Buffer, ArrayBuffer, or a Stream, depending on `dataType` option supplied to the request. 355 | * `errors` (Array of Objects) - Will be present if status >= 400. See Errors 356 | * `message` (String) 357 | 358 | ### Class Methods 359 | 360 | * [prepareGraphQLFile(pathOrStreamLikeThing[, options])](#preparegraphqlfilepathorstreamlikething-options) 361 | 362 | ##### prepareGraphQLFile(pathOrStreamLikeThing[, options]) 363 | 364 | A nice helper to prepare a [File](https://developer.mozilla.org/en-US/docs/Web/API/File) upload for use with our GraphQL API. By default, this will upload your files as multipart uploads over the [jaydenseric / GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec). We use `node-fetch` under the hood, and you can see [this example](https://github.com/node-fetch/node-fetch#post-data-using-a-file) to get a bit of an understanding of what's happening behind the scenes. NOTE: Please see below about certain scenarios where you may need to manually provide a `filename`. 365 | 366 | * `pathOrSupportedInstance` (String | File | Blob | Stream | Buffer) - Can be one of several things. Here's a list of what's supported, in order of preference: 367 | 1. A `File` instance. 368 | 1. A `Blob` instance. 369 | 1. A `string` that is a path to a file to be used. 370 | 1. A `ReadStream` instance that must either have either: 371 | 1. A `path` property (this will usually be present when the stream was loaded from the local file system) 372 | 1. A `filename` provided in the `options` parameter. 373 | 1. A `Buffer` instance with a `filename` provided in the `options` parameter. 374 | * `options` (Object) - Options that may be required when providing certain types of values as the first parameter. For example, `Blob`s, `Buffer`s and certain kinds of `ReadStream`s will not have any notion of what the name of the file is or should be when uploaded. 375 | * `filename` (String) - Override the filename of the uploaded file here. If providing a generic ReadStream or Buffer, you will be required to provide a filename here 376 | * `mimetype` (String) - Optional mimetype to specify with the resulting file that can be used when a `string` path, `Buffer` or `ReadStream` are provided as the first parameter. 377 | * Returns an `Object` that is properly formatted to be coerced by the client for use against our GraphQL API wherever an `Upload` type is required. 378 | 379 | ### Types 380 | 381 | * [Options](#options) 382 | 383 | ##### Options 384 | 385 | Options for the Anvil Client. Defaults are shown after each option key. 386 | 387 | ```js 388 | { 389 | apiKey: , // Required. Your API key from your Anvil organization settings 390 | } 391 | ``` 392 | 393 | ### Rate Limits 394 | 395 | Our API has request rate limits in place. The initial request made by this client will parse the limits for your account from the response headers, and then handle the throttling of subsequent requests for you automatically. In the event that this client still receives a `429 Too Many Requests` error response, it will wait the specified duration then retry the request. The client attempts to avoid `429` errors by throttling requests after the number of requests within the specified time period has been reached. 396 | 397 | See the [Anvil API docs](https://useanvil.com/docs/api/fill-pdf) for more information on the specifics of the rate limits. 398 | 399 | ## API Documentation 400 | 401 | Our general API Documentation can be found [here](https://www.useanvil.com/api/). It's the best resource for up-to-date information about our API and its capabilities. 402 | 403 | See the [PDF filling API docs](https://useanvil.com/docs/api/fill-pdf) for more information about the `fillPDF` method. 404 | 405 | ## Examples 406 | 407 | Check out the [example](https://github.com/anvilco/node-anvil/tree/main/example) folder for running usage examples! 408 | 409 | ## Development 410 | 411 | First install the dependencies 412 | 413 | ```sh 414 | yarn install 415 | ``` 416 | 417 | Running tests 418 | 419 | ```sh 420 | yarn test 421 | yarn test:watch 422 | ``` 423 | 424 | Building with babel will output in the `/lib` directory. 425 | 426 | ```sh 427 | yarn test 428 | 429 | # Watches the `src` and `test` directories 430 | yarn test:watch 431 | ``` 432 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://babeljs.io/docs/en/options#retainlines 3 | retainLines: true, 4 | // https://babeljs.io/docs/en/options#comments 5 | comments: false, 6 | // https://babeljs.io/docs/en/options#compact 7 | // compact: true, 8 | // https://babeljs.io/docs/en/options#minified 9 | // minified: true, 10 | presets: [ 11 | [ 12 | // Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set. 13 | '@babel/preset-env', 14 | { 15 | // modules: 'commonjs', 16 | targets: { 17 | // Keep this roughly in-line with our "engines.node" value in package.json 18 | node: '14', 19 | }, 20 | exclude: [ 21 | // Node 14+ supports this natively AND we need it to operate natively 22 | // so do NOT transpile it 23 | 'proposal-dynamic-import', 24 | ], 25 | }, 26 | ], 27 | ], 28 | plugins: [], 29 | ignore: [], 30 | } 31 | -------------------------------------------------------------------------------- /dev/build-e2e.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { execSync } from 'child_process' 3 | 4 | import { 5 | root, 6 | isDryRun as isDryRunFn, 7 | getTarballNameFromOutput, 8 | ensureDirectory, 9 | } from './utils.mjs' 10 | 11 | let isDryRun = isDryRunFn() 12 | // isDryRun = true 13 | 14 | const e2eDir = path.join(root, 'test/e2e') 15 | ensureDirectory(e2eDir) 16 | 17 | // Pack the thing.... 18 | const packageName = 'node-anvil' 19 | const options = [ 20 | `--pack-destination ${e2eDir}`, 21 | ] 22 | 23 | if (isDryRun) { 24 | options.push( 25 | '--dry-run', 26 | ) 27 | } 28 | 29 | const args = options.join(' ') 30 | const command = `npm pack ${args}` 31 | 32 | let tarballName = await execSync( 33 | command, 34 | { 35 | cwd: root, 36 | }, 37 | ) 38 | tarballName = getTarballNameFromOutput(tarballName.toString()) 39 | 40 | // Rename the thing 41 | const orginalPath = path.join(e2eDir, tarballName) 42 | const newPath = path.join(e2eDir, `${packageName}.tgz`) 43 | 44 | await execSync(`mv ${orginalPath} ${newPath}`) 45 | -------------------------------------------------------------------------------- /dev/utils.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import { fileURLToPath } from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = path.dirname(__filename) 7 | 8 | export const root = path.join(__dirname, '..') 9 | 10 | export function isDryRun () { 11 | return process.env.npm_config_dry_run === true 12 | } 13 | 14 | export function stripSpecial (str) { 15 | while (['\n', '\t'].includes(str[str.length - 1])) { 16 | str = str.slice(0, -1) 17 | } 18 | return str 19 | } 20 | 21 | export function ensureDirectory (path) { 22 | if (!fs.existsSync(path)) { 23 | fs.mkdirSync(path) 24 | } 25 | } 26 | 27 | export function getTarballNameFromOutput (str) { 28 | str = stripSpecial(str) 29 | return str.split('\n').pop() 30 | } 31 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Anvil API Client Examples 2 | 3 | ## fill-pdf.js script 4 | 5 | Calls the fillPDF client function with data specified to fill a PDF with the Anvil API. Outputs the filled PDF in `example/script/fill.output.pdf` 6 | 7 | Usage example: 8 | 9 | ```sh 10 | # Fills a PDF then opens it in preview 11 | yarn node example/script/fill-pdf.js 12 | 13 | # An example 14 | yarn node example/script/fill-pdf.js idabc123 apiKeydef345 ./payload.json && open example/script/fill.output.pdf 15 | ``` 16 | 17 | `payload.json` is a json file with the JSON data used to fill the PDF. e.g. 18 | 19 | ```json 20 | { 21 | "title": "My PDF Title", 22 | "fontSize": 10, 23 | "textColor": "#CC0000", 24 | "data": { 25 | "someFieldId": "Hello World!" 26 | } 27 | } 28 | ``` 29 | 30 | ## generate-pdf.js script 31 | 32 | Calls the generatePDF client function with data specified to generate a PDF with the Anvil API. Outputs the generated PDF in `example/script/generate.output.pdf` 33 | 34 | Usage example: 35 | 36 | ```sh 37 | # Generates a PDF 38 | yarn node example/script/generate-pdf.js [] 39 | 40 | # Generates a PDF with default data, then open the new PDF in preview 41 | yarn node example/script/generate-pdf.js 5vqCxtgNsA2uzgMH0ps4cyQyadhA2Wdt && open example/script/generate.output.pdf 42 | 43 | # Generate a PDF with your payload, then open the new PDF in preview 44 | yarn node example/script/generate-pdf.js apiKeydef345 ./payload.json && open example/script/generate.output.pdf 45 | ``` 46 | 47 | `payload.json` is an optional JSON file with the JSON data used to generate the PDF. e.g. 48 | 49 | ```json 50 | { 51 | "title": "My PDF Title", 52 | "data": [{ 53 | "label": "Hello World!", 54 | "content": "Lorem **ipsum** dolor sit _amet_." 55 | }] 56 | } 57 | ``` 58 | 59 | ## create-etch-packet.js script 60 | 61 | Calls the createEtchPacket Anvil endpoint with data specified to generate an Etch Packet with the Anvil API. Returns 62 | the status and the generated packet. 63 | 64 | Usage example: 65 | 66 | ```sh 67 | # Creates an Etch Packet with given information, either a castEid or filename must be supplied 68 | yarn node example/script/create-etch-packet.js 69 | 70 | # An example 71 | yarn node example/script/create-etch-packet.js WHG3ylq0EE930IR2LZDtgoqgl55M3TwQ 99u7QvvHr8hDQ4BW9GYv ../../../simple-anvil-finovate-non-qualified.pdf 72 | ``` 73 | 74 | ## get-etch-packet.js script 75 | 76 | Calls the etchPacket Anvil endpoint with the specified Etch packet eid to get the packet details. 77 | 78 | Usage example: 79 | 80 | ```sh 81 | # Gets the details of an Etch Packet, a packet eid must be supplied 82 | yarn node example/script/get-etch-packet.js 83 | 84 | # An example 85 | yarn node example/script/get-etch-packet.js WHG3ylq0EE930IR2LZDtgoqgl55M3TwQ QJhbdpK75RHRQcgPz5Fc 86 | ``` 87 | 88 | ## generate-etch-sign-url.js script 89 | 90 | Calls the generateEtchSignUrl Anvil endpoint with data specified to generate an Etch sign link with the Anvil API. Returns the sign link. 91 | 92 | Usage example: 93 | 94 | ```sh 95 | # Generates a sign link for the given signer and client. 96 | yarn node example/script/generate-etch-sign-url.js 97 | 98 | # An example 99 | yarn node example/script/generate-etch-sign-url.js WHG3ylq0EE930IR2LZDtgoqgl55M3TwQ eBim2Vsv2GqCTJxpjTru ZTlbNhxP2lGkNFsNzcus 100 | ``` 101 | 102 | ## download-documents.js script 103 | 104 | Calls the downloadDocuments Anvil endpoint to download documents with the specified documentGroupEid in Zip file format. Outputs the downloaded Zip file in `example/script/{etchPacketName}.zip`. The default response data is returned in the form of a buffer. This can be changed by adding the `-s` flag to instead be returned as a PassThrough stream. 105 | 106 | Usage example: 107 | 108 | ```sh 109 | # Downloads a Document Group in a Zip file and outputs in the example/script folder 110 | yarn node example/script/download-documents.js 111 | 112 | # An example 113 | yarn node example/script/download-documents.js WHG3ylq0EE930IR2LZDtgoqgl55M3TwQ uQiXw4P4DTmXV1eNDmzH 114 | ``` 115 | -------------------------------------------------------------------------------- /example/script/create-etch-packet.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Anvil = require('../../src/index') 3 | const argv = require('yargs') 4 | .usage('Usage: $0 apiKey employeeEmailAddress employerEmailAddress') 5 | .demandCommand(3).argv 6 | 7 | // This example will create a signature packet with 2 PDFs: 8 | // * An IRS W-4 template is specified and filled with employee data 9 | // * A demo NDA PDF is uploaded, then filled with employee and employer data 10 | // 11 | // Then both the employee and employer will sign the PDFs: 12 | // * The employee signs first on both the IRS W-4 and the NDA 13 | // * The employer signs second (after the employee is finished) 14 | // and only signs the NDA 15 | // 16 | // Both signers will get emails when it is their turn to sign, then you (as the 17 | // Anvil organization admin) will get an email when it is all completed. 18 | // 19 | // See https://esign-demo.useanvil.com for a live example 20 | 21 | // The API key from your Anvil organization settings 22 | const apiKey = argv._[0] 23 | 24 | // Signer emails. Make sure these are valid email addresses 25 | const employeeEmail = argv._[1] 26 | const employerEmail = argv._[2] 27 | 28 | // You shouldn't need to update these... 29 | const employeeName = 'Sally Employee' 30 | const employerName = 'Bill AcmeManager' 31 | const irsW4Eid = 'XnuTZKVZg1Mljsu999od' 32 | 33 | async function main () { 34 | const anvilClient = new Anvil({ apiKey }) 35 | const ndaFile = Anvil.prepareGraphQLFile(path.join(__dirname, '../static/test-pdf-nda.pdf')) 36 | const variables = getPacketVariables(ndaFile) 37 | const { statusCode, data: result, errors } = await anvilClient.createEtchPacket({ variables }) 38 | console.log(statusCode) 39 | if (errors) { 40 | console.log('Error', errors) 41 | } else { 42 | const { data } = result 43 | console.log(data.createEtchPacket) 44 | } 45 | } 46 | 47 | function getPacketVariables (ndaFile) { 48 | return { 49 | // Indicate the packet is all ready to send to the 50 | // signers. An email will be sent to the first signer. 51 | isDraft: false, 52 | 53 | // Test packets will use development signatures and 54 | // not count toward your billed packets 55 | isTest: true, 56 | 57 | // Subject & body of the emails to signers 58 | name: `HR Docs - ${employeeName}`, 59 | signatureEmailSubject: 'HR Documents ok', 60 | signatureEmailBody: 'Please sign these HR documents....', 61 | 62 | // Merge all PDFs into one PDF before signing. 63 | // Signing users will get one PDF instead of all PDFs as separate files. 64 | // mergePDFs: false, 65 | 66 | files: [ 67 | { 68 | // Our ID we will use to reference and fill it with data. 69 | // It can be any string you want! 70 | id: 'templatePdfIrsW4', 71 | 72 | // The id to the ready-made W-4 template. Fields and their ids are 73 | // specified when building out the template 74 | castEid: irsW4Eid, 75 | }, 76 | { 77 | // This is a file we will upload and specify the fields ourselves 78 | id: 'fileUploadNDA', 79 | title: 'Demo NDA', 80 | file: ndaFile, // The file to be uploaded 81 | fields: [ 82 | { 83 | id: 'effectiveDate', 84 | type: 'date', 85 | rect: { x: 326, y: 92, height: 12, width: 112 }, 86 | format: 'MM/DD/YYYY', 87 | pageNum: 0, 88 | }, 89 | { 90 | id: 'disclosingPartyName', 91 | type: 'fullName', 92 | rect: { x: 215, y: 107, height: 12, width: 140 }, 93 | pageNum: 0, 94 | }, 95 | { 96 | id: 'disclosingPartyEmail', 97 | type: 'email', 98 | rect: { x: 360, y: 107, height: 12, width: 166 }, 99 | pageNum: 0, 100 | }, 101 | { 102 | id: 'recipientName', 103 | type: 'fullName', 104 | rect: { x: 223, y: 120, height: 12, width: 140 }, 105 | pageNum: 0, 106 | }, 107 | { 108 | id: 'recipientEmail', 109 | type: 'email', 110 | rect: { x: 367, y: 120, height: 12, width: 166 }, 111 | pageNum: 0, 112 | }, 113 | { 114 | id: 'purposeOfBusiness', 115 | type: 'shortText', 116 | rect: { x: 314, y: 155, height: 12, width: 229 }, 117 | pageNum: 0, 118 | }, 119 | { 120 | id: 'placeOfGovernance', 121 | type: 'shortText', 122 | rect: { x: 237, y: 236, height: 12, width: 112 }, 123 | pageNum: 1, 124 | }, 125 | { 126 | id: 'recipientSignatureName', 127 | type: 'fullName', 128 | rect: { x: 107, y: 374, height: 22, width: 157 }, 129 | pageNum: 1, 130 | }, 131 | { 132 | id: 'recipientSignature', 133 | type: 'signature', 134 | rect: { x: 270, y: 374, height: 22, width: 142 }, 135 | pageNum: 1, 136 | }, 137 | { 138 | id: 'recipientSignatureDate', 139 | type: 'signatureDate', 140 | rect: { x: 419, y: 374, height: 22, width: 80 }, 141 | pageNum: 1, 142 | }, 143 | { 144 | id: 'disclosingPartySignatureName', 145 | type: 'fullName', 146 | rect: { x: 107, y: 416, height: 22, width: 159 }, 147 | pageNum: 1, 148 | }, 149 | { 150 | id: 'disclosingPartySignature', 151 | type: 'signature', 152 | rect: { x: 272, y: 415, height: 22, width: 138 }, 153 | pageNum: 1, 154 | }, 155 | { 156 | id: 'disclosingPartySignatureDate', 157 | type: 'signatureDate', 158 | rect: { x: 418, y: 414, height: 22, width: 82 }, 159 | pageNum: 1, 160 | }, 161 | ], 162 | }, 163 | ], 164 | 165 | data: { 166 | // This data will fill the PDF before it's sent to any signers. 167 | // IDs here were set up on each field while templatizing the PDF. 168 | payloads: { 169 | // 'templatePdfIrsW4' is the W-4 file ID specified above 170 | templatePdfIrsW4: { 171 | data: { 172 | name: employeeName, 173 | ssn: '456454567', 174 | filingStatus: 'Joint', 175 | address: { 176 | street1: '123 Main St #234', 177 | city: 'San Francisco', 178 | state: 'CA', 179 | zip: '94106', 180 | country: 'US', 181 | }, 182 | employerEin: '897654321', 183 | employerAddress: { 184 | street1: '555 Market St', 185 | city: 'San Francisco', 186 | state: 'CA', 187 | zip: '94103', 188 | country: 'US', 189 | }, 190 | }, 191 | }, 192 | 193 | // 'fileUploadNDA' is the NDA's file ID specified above 194 | fileUploadNDA: { 195 | fontSize: 8, 196 | textColor: '#0000CC', 197 | data: { 198 | // The IDs here match the fields we created in the 199 | // files property above 200 | effectiveDate: '2024-01-30', 201 | recipientName: employeeName, 202 | recipientSignatureName: employeeName, 203 | recipientEmail: employeeEmail, 204 | 205 | disclosingPartyName: 'Acme Co.', 206 | disclosingPartySignatureName: employerName, 207 | disclosingPartyEmail: employerEmail, 208 | 209 | purposeOfBusiness: 'DEMO!!', 210 | placeOfGovernance: 'The Land', 211 | }, 212 | }, 213 | }, 214 | }, 215 | 216 | signers: [ 217 | // Signers will sign in the order they are specified in this array. 218 | // e.g. `employer` will sign after `employee` has finished signing 219 | { 220 | // `employee` is the first signer 221 | id: 'employee', 222 | name: employeeName, 223 | email: employeeEmail, 224 | 225 | // These fields will be presented when this signer signs. 226 | // The signer will need to click through the signatures in 227 | // the order of this array. 228 | fields: [ 229 | { 230 | // File IDs are specified in the `files` property above 231 | fileId: 'templatePdfIrsW4', 232 | fieldId: 'employeeSignature', 233 | }, 234 | { 235 | fileId: 'templatePdfIrsW4', 236 | fieldId: 'employeeSignatureDate', 237 | }, 238 | { 239 | fileId: 'fileUploadNDA', 240 | // NDA field IDs are specified in the `files[].fields` property above 241 | fieldId: 'recipientSignature', 242 | }, 243 | { 244 | fileId: 'fileUploadNDA', 245 | fieldId: 'recipientSignatureDate', 246 | }, 247 | ], 248 | }, 249 | { 250 | // `employer` is the 2nd signer 251 | id: 'employer', 252 | name: employerName, 253 | email: employerEmail, 254 | fields: [ 255 | { 256 | fileId: 'fileUploadNDA', 257 | fieldId: 'disclosingPartySignature', 258 | }, 259 | { 260 | fileId: 'fileUploadNDA', 261 | fieldId: 'disclosingPartySignatureDate', 262 | }, 263 | ], 264 | }, 265 | ], 266 | } 267 | } 268 | 269 | function run (fn) { 270 | fn().then(() => { 271 | process.exit(0) 272 | }).catch((err) => { 273 | console.log(err.stack || err.message) 274 | process.exit(1) 275 | }) 276 | } 277 | 278 | run(main) 279 | -------------------------------------------------------------------------------- /example/script/download-documents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Anvil = require('../../src/index') 4 | const argv = require('yargs') 5 | .usage('Usage: $0 apiKey documentGroupEid [-s]') 6 | .option('stream', { 7 | alias: 's', 8 | type: 'boolean', 9 | description: 'Return the data as a stream (default is buffer)', 10 | }) 11 | .demandCommand(2).argv 12 | 13 | const [apiKey, documentGroupEid] = argv._ 14 | const returnAStream = argv.stream 15 | 16 | async function main () { 17 | const clientOptions = { 18 | apiKey, 19 | } 20 | 21 | const client = new Anvil(clientOptions) 22 | 23 | const downloadOptions = {} 24 | if (returnAStream) downloadOptions.dataType = 'stream' 25 | 26 | const { statusCode, response, data, errors } = await client.downloadDocuments(documentGroupEid, downloadOptions) 27 | if (statusCode === 200) { 28 | const contentDisposition = response.headers.get('content-disposition') 29 | const fileTitle = contentDisposition.split('"')[1] 30 | const scriptDir = __dirname 31 | const outputFilePath = path.join(scriptDir, fileTitle) 32 | 33 | if (returnAStream) { 34 | const writeStream = fs.createWriteStream(outputFilePath, { encoding: null }) 35 | await new Promise((resolve, reject) => { 36 | data.pipe(writeStream) 37 | data.on('error', reject) 38 | writeStream.on('finish', resolve) 39 | }) 40 | } else { 41 | fs.writeFileSync(outputFilePath, data, { encoding: null }) 42 | } 43 | 44 | console.log(statusCode) 45 | } else { 46 | console.log(statusCode, JSON.stringify(errors, null, 2)) 47 | } 48 | } 49 | 50 | main() 51 | .then(() => { 52 | process.exit(0) 53 | }) 54 | .catch((err) => { 55 | console.log(err.stack || err.message) 56 | process.exit(1) 57 | }) 58 | -------------------------------------------------------------------------------- /example/script/fill-pdf.js: -------------------------------------------------------------------------------- 1 | // Calls the fillPDF Anvil endpoint with data specified to fill a PDF with the 2 | // Anvil API. Outputs the filled PDF in `example/script/fill.output.pdf` 3 | // 4 | // Usage example: 5 | // 6 | // # Fills a PDF 7 | // yarn node example/script/fill-pdf.js 8 | // 9 | // # An example that fills then opens the PDF in preview 10 | // yarn node example/script/fill-pdf.js idabc123 apiKeydef345 ./payload.json && open example/script/fill.output.pdf 11 | // 12 | // `payload.json` is a json file with the JSON data used to fill the PDF. e.g. 13 | // 14 | // { 15 | // "title": "My PDF Title", 16 | // "fontSize": 10, 17 | // "textColor": "#CC0000", 18 | // "data": { 19 | // "someFieldId": "Hello World!" 20 | // } 21 | // } 22 | 23 | const fs = require('fs') 24 | const path = require('path') 25 | const Anvil = require('../../src/index') 26 | const argv = require('yargs') 27 | .usage('Usage: $0 pdfTemplateID apiKey jsonPath.json') 28 | .option('user-agent', { 29 | alias: 'a', 30 | type: 'string', 31 | description: 'Set the User-Agent on any requests made (default is "Anvil API Client")', 32 | }) 33 | .option('stream', { 34 | alias: 's', 35 | type: 'boolean', 36 | description: 'Return the data as a stream (default is buffer)', 37 | }) 38 | .demandCommand(3).argv 39 | 40 | const [eid, apiKey, jsonPath] = argv._ 41 | const returnAStream = argv.stream 42 | const userAgent = argv['user-agent'] 43 | 44 | const baseURL = 'https://app.useanvil.com' 45 | const exampleData = JSON.parse(fs.readFileSync(jsonPath, { encoding: 'utf8' })) 46 | 47 | async function main () { 48 | const clientOptions = { 49 | baseURL, 50 | apiKey, 51 | } 52 | if (userAgent) { 53 | clientOptions.userAgent = userAgent 54 | } 55 | 56 | const client = new Anvil(clientOptions) 57 | 58 | const fillOptions = {} 59 | if (returnAStream) { 60 | fillOptions.dataType = 'stream' 61 | } 62 | 63 | // A version number can also be passed in. This will retrieve a specific 64 | // version of the PDF to be filled if you don't want the current version 65 | // to be used. 66 | // You can also use the constant `Anvil.VERSION_LATEST` to fill a PDF that has not 67 | // been published yet. Use this if you'd like to fill out a draft version of 68 | // your template/PDF. 69 | // 70 | // fillOptions.versionNumber = 3 71 | // // or 72 | // fillOptions.versionNumber = Anvil.VERSION_LATEST 73 | 74 | const { statusCode, data, errors } = await client.fillPDF(eid, exampleData, fillOptions) 75 | 76 | if (statusCode === 200) { 77 | const scriptDir = __dirname 78 | const outputFilePath = path.join(scriptDir, 'fill.output.pdf') 79 | 80 | if (returnAStream) { 81 | const writeStream = fs.createWriteStream(outputFilePath, { encoding: null }) 82 | await new Promise((resolve, reject) => { 83 | data.pipe(writeStream) 84 | data.on('error', reject) 85 | writeStream.on('finish', resolve) 86 | }) 87 | } else { 88 | fs.writeFileSync(outputFilePath, data, { encoding: null }) 89 | } 90 | } else { 91 | console.log(statusCode, JSON.stringify(errors || data, null, 2)) 92 | } 93 | } 94 | 95 | main() 96 | .then(() => { 97 | process.exit(0) 98 | }) 99 | .catch((err) => { 100 | console.log(err.stack || err.message) 101 | process.exit(1) 102 | }) 103 | -------------------------------------------------------------------------------- /example/script/generate-etch-sign-url.js: -------------------------------------------------------------------------------- 1 | const Anvil = require('../../src/index') 2 | const argv = require('yargs') 3 | .usage('Usage: $0 apiKey clientUserId signerEid') 4 | .demandCommand(3).argv 5 | 6 | const [apiKey, clientUserId, signerEid] = argv._ 7 | 8 | async function main () { 9 | const clientOptions = { 10 | apiKey, 11 | } 12 | 13 | const client = new Anvil(clientOptions) 14 | 15 | const variables = { 16 | clientUserId, 17 | signerEid, 18 | } 19 | 20 | const { statusCode, url, errors } = await client.generateEtchSignUrl({ variables }) 21 | console.log( 22 | JSON.stringify({ 23 | statusCode, 24 | url, 25 | errors, 26 | }, null, 2), 27 | ) 28 | } 29 | 30 | main() 31 | .then(() => { 32 | process.exit(0) 33 | }) 34 | .catch((err) => { 35 | console.log(err.stack || err.message) 36 | process.exit(1) 37 | }) 38 | -------------------------------------------------------------------------------- /example/script/generate-html-to-pdf.js: -------------------------------------------------------------------------------- 1 | // See our invoice template repo for a more complete HTML to PDF example: 2 | // https://github.com/anvilco/html-pdf-invoice-template 3 | 4 | // Calls the generatePDF Anvil endpoint with HTML and CSS data specified to 5 | // generate a PDF with the Anvil API. Outputs the generated PDF in 6 | // `example/script/generate.output.pdf` 7 | // 8 | // Usage examples: 9 | // 10 | // # Generates a PDF 11 | // yarn node example/script/generate-html-to-pdf.js 12 | // 13 | // # Generates a PDF with default data, then open the new PDF in preview 14 | // yarn node example/script/generate-html-to-pdf.js 5vqCxtgNsA2uzgMH0ps4cyQyadhA2Wdt && open example/script/generate.output.pdf 15 | 16 | const fs = require('fs') 17 | const path = require('path') 18 | const Anvil = require('../../src/index') 19 | const argv = require('yargs') 20 | .usage('Usage: $0 apiKey') 21 | .option('user-agent', { 22 | alias: 'a', 23 | type: 'string', 24 | description: 'Set the User-Agent on any requests made (default is "Anvil API Client")', 25 | }) 26 | .option('stream', { 27 | alias: 's', 28 | type: 'boolean', 29 | description: 'Return the data as a stream (default is buffer)', 30 | }) 31 | .demandCommand(1).argv 32 | 33 | const [apiKey] = argv._ 34 | const returnAStream = argv.stream 35 | const userAgent = argv['user-agent'] 36 | 37 | const baseURL = 'https://app.useanvil.com' 38 | 39 | const exampleData = { 40 | title: 'Example HTML to PDF', 41 | type: 'html', 42 | data: { 43 | html: ` 44 |

What is Lorem Ipsum?

45 |

46 | Lorem Ipsum is simply dummy text of the printing and typesetting 47 | industry. Lorem Ipsum has been the industry's standard dummy text 48 | ever since the 1500s, when an unknown printer took 49 | a galley of type and scrambled it to make a type specimen book. 50 |

51 |

Where does it come from?

52 |

53 | Contrary to popular belief, Lorem Ipsum is not simply random text. 54 | It has roots in a piece of classical Latin literature from 55 | 45 BC, making it over 2000 years old. 56 |

57 | `, 58 | css: ` 59 | body { font-size: 14px; color: #171717; } 60 | .header-one { text-decoration: underline; } 61 | .header-two { font-style: underline; } 62 | `, 63 | }, 64 | } 65 | 66 | async function main () { 67 | const clientOptions = { 68 | baseURL, 69 | apiKey, 70 | } 71 | if (userAgent) { 72 | clientOptions.userAgent = userAgent 73 | } 74 | 75 | const client = new Anvil(clientOptions) 76 | 77 | const generateOptions = {} 78 | if (returnAStream) { 79 | generateOptions.dataType = 'stream' 80 | } 81 | 82 | const { statusCode, data, errors } = await client.generatePDF(exampleData, generateOptions) 83 | 84 | if (statusCode === 200) { 85 | const scriptDir = __dirname 86 | const outputFilePath = path.join(scriptDir, 'generate.output.pdf') 87 | 88 | if (returnAStream) { 89 | const writeStream = fs.createWriteStream(outputFilePath, { encoding: null }) 90 | await new Promise((resolve, reject) => { 91 | data.pipe(writeStream) 92 | data.on('error', reject) 93 | writeStream.on('finish', resolve) 94 | }) 95 | } else { 96 | fs.writeFileSync(outputFilePath, data, { encoding: null }) 97 | } 98 | } else { 99 | console.log(statusCode, JSON.stringify(errors || data, null, 2)) 100 | } 101 | } 102 | 103 | main() 104 | .then(() => { 105 | process.exit(0) 106 | }) 107 | .catch((err) => { 108 | console.log(err.stack || err.message) 109 | process.exit(1) 110 | }) 111 | -------------------------------------------------------------------------------- /example/script/generate-markdown-pdf.js: -------------------------------------------------------------------------------- 1 | // Calls the generatePDF Anvil endpoint with data specified to generate a PDF 2 | // with the Anvil API. Outputs the generated PDF in `example/script/generate.output.pdf` 3 | // 4 | // Usage examples: 5 | // 6 | // # Generates a PDF 7 | // yarn node example/script/generate-markdown-pdf.js [] 8 | // 9 | // # Generates a PDF with default data, then open the new PDF in preview 10 | // yarn node example/script/generate-markdown-pdf.js 5vqCxtgNsA2uzgMH0ps4cyQyadhA2Wdt && open example/script/generate.output.pdf 11 | // 12 | // # Generate a PDF with your payload, then open the new PDF in preview 13 | // yarn node example/script/generate-markdown-pdf.js apiKeydef345 ./payload.json && open example/script/generate.output.pdf 14 | // 15 | // `payload.json` is an optional JSON file with the JSON data used to generate the PDF. e.g. 16 | // 17 | // { 18 | // "title": "My PDF Title", 19 | // "data": [{ 20 | // "label": "Hello World!", 21 | // "content": "Lorem **ipsum** dolor sit _amet_." 22 | // }] 23 | // } 24 | 25 | const fs = require('fs') 26 | const path = require('path') 27 | const Anvil = require('../../src/index') 28 | const argv = require('yargs') 29 | .usage('Usage: $0 apiKey') 30 | .option('user-agent', { 31 | alias: 'a', 32 | type: 'string', 33 | description: 'Set the User-Agent on any requests made (default is "Anvil API Client")', 34 | }) 35 | .option('stream', { 36 | alias: 's', 37 | type: 'boolean', 38 | description: 'Return the data as a stream (default is buffer)', 39 | }) 40 | .demandCommand(1).argv 41 | 42 | const [apiKey, jsonPath] = argv._ 43 | const returnAStream = argv.stream 44 | const userAgent = argv['user-agent'] 45 | 46 | const baseURL = 'https://app.useanvil.com' 47 | 48 | const exampleData = jsonPath 49 | ? JSON.parse(fs.readFileSync(jsonPath, { encoding: 'utf8' })) 50 | : { 51 | title: 'Example Invoice', 52 | data: [{ 53 | label: 'Name', 54 | content: 'Sally Jones', 55 | }, { 56 | content: 'Lorem **ipsum** dolor sit _amet_, consectetur adipiscing elit, sed [do eiusmod](https://www.useanvil.com/docs) tempor incididunt ut labore et dolore magna aliqua. Ut placerat orci nulla pellentesque dignissim enim sit amet venenatis.\n\nMi eget mauris pharetra et ultrices neque ornare aenean.\n\n* Sagittis eu volutpat odio facilisis.\n\n* Erat nam at lectus urna.', 57 | }, { 58 | table: { 59 | firstRowHeaders: true, 60 | rows: [ 61 | ['Description', 'Quantity', 'Price'], 62 | ['4x Large Widgets', '4', '$40.00'], 63 | ['10x Medium Sized Widgets in dark blue', '10', '$100.00'], 64 | ['10x Small Widgets in white', '6', '$60.00'], 65 | ], 66 | }, 67 | }], 68 | } 69 | 70 | async function main () { 71 | const clientOptions = { 72 | baseURL, 73 | apiKey, 74 | } 75 | if (userAgent) { 76 | clientOptions.userAgent = userAgent 77 | } 78 | 79 | const client = new Anvil(clientOptions) 80 | 81 | const generateOptions = {} 82 | if (returnAStream) { 83 | generateOptions.dataType = 'stream' 84 | } 85 | 86 | const { statusCode, data, errors } = await client.generatePDF(exampleData, generateOptions) 87 | 88 | if (statusCode === 200) { 89 | const scriptDir = __dirname 90 | const outputFilePath = path.join(scriptDir, 'generate.output.pdf') 91 | 92 | if (returnAStream) { 93 | const writeStream = fs.createWriteStream(outputFilePath, { encoding: null }) 94 | await new Promise((resolve, reject) => { 95 | data.pipe(writeStream) 96 | data.on('error', reject) 97 | writeStream.on('finish', resolve) 98 | }) 99 | } else { 100 | fs.writeFileSync(outputFilePath, data, { encoding: null }) 101 | } 102 | } else { 103 | console.log(statusCode, JSON.stringify(errors || data, null, 2)) 104 | } 105 | } 106 | 107 | main() 108 | .then(() => { 109 | process.exit(0) 110 | }) 111 | .catch((err) => { 112 | console.log(err.stack || err.message) 113 | process.exit(1) 114 | }) 115 | -------------------------------------------------------------------------------- /example/script/get-etch-packet.js: -------------------------------------------------------------------------------- 1 | const Anvil = require('../../src/index') 2 | const argv = require('yargs') 3 | .usage('Usage: $0 apiKey etchPacketEid') 4 | .demandCommand(2).argv 5 | 6 | const [apiKey, etchPacketEid] = argv._ 7 | 8 | async function main () { 9 | const clientOptions = { 10 | apiKey, 11 | } 12 | 13 | const client = new Anvil(clientOptions) 14 | 15 | const variables = { 16 | eid: etchPacketEid, 17 | } 18 | 19 | const { statusCode, data, errors } = await client.getEtchPacket({ variables }) 20 | console.log( 21 | JSON.stringify({ 22 | statusCode, 23 | data, 24 | errors, 25 | }, null, 2), 26 | ) 27 | } 28 | 29 | main() 30 | .then(() => { 31 | process.exit(0) 32 | }) 33 | .catch((err) => { 34 | console.log(err.stack || err.message) 35 | process.exit(1) 36 | }) 37 | -------------------------------------------------------------------------------- /example/static/test-pdf-nda.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilco/node-anvil/ff56ea27765f50ddc0d4fdfb22e2d9fa40bf51bb/example/static/test-pdf-nda.pdf -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Dist = require('./dist') 2 | 3 | // This file is mainly just for cleaning up the exports to be intuitive/commonjs 4 | module.exports = Dist.default 5 | // This is here just for backwards compatibilty. Should be removed at next 6 | // major verison to avoid a breaking change 7 | module.exports.default = Dist.default 8 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@anvilco/anvil", 3 | "version": "3.3.2", 4 | "description": "Anvil API Client", 5 | "author": "Anvil Foundry Inc.", 6 | "homepage": "https://github.com/anvilco/node-anvil#readme", 7 | "license": "MIT", 8 | "keywords": [ 9 | "pdf", 10 | "pdf-fill", 11 | "json-to-pdf" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/anvilco/node-anvil/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/anvilco/node-anvil.git" 19 | }, 20 | "engines": { 21 | "node": ">=14" 22 | }, 23 | "publishConfig": { 24 | "registry": "https://registry.npmjs.org", 25 | "@anvilco:registry": "https://registry.npmjs.org" 26 | }, 27 | "main": "index.js", 28 | "files": [ 29 | "package.json", 30 | "README.md", 31 | "LICENSE.md", 32 | "CHANGELOG.md", 33 | "dist/", 34 | "types/", 35 | "example/", 36 | "!**/.DS_STORE", 37 | "!**/.DS_Store" 38 | ], 39 | "types": "./types/src/index.d.ts", 40 | "scripts": { 41 | "build": "babel src --out-dir ./dist", 42 | "clean": "yarn rimraf ./dist", 43 | "prepare": "yarn tsc && yarn clean && yarn build", 44 | "publish:beta": "npm publish --tag beta", 45 | "test": "yarn prepare && mocha --config ./test/mocha.js", 46 | "lint": "eslint 'src/**/*.js' 'test/**/*.js'", 47 | "lint:quiet": "yarn run lint --quiet", 48 | "test:debug": "yarn test --inspect-brk=0.0.0.0:9223", 49 | "test:watch": "nodemon --signal SIGINT --watch test --watch src -x 'yarn test'", 50 | "test-e2e:build": "rimraf test/e2e/node-anvil.tgz && node dev/build-e2e.mjs", 51 | "test-e2e:install": "npm --prefix test/e2e run prep && npm --prefix test/e2e install" 52 | }, 53 | "dependencies": { 54 | "@anvilco/node-fetch": "^3.3.3-beta.0", 55 | "abort-controller": "^3.0.0", 56 | "extract-files": "^13", 57 | "limiter": "^2.1.0" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.22.10", 61 | "@babel/core": "^7.18.2", 62 | "@babel/eslint-parser": "^7.18.2", 63 | "@babel/preset-env": "^7.22.10", 64 | "@babel/register": "^7.22.5", 65 | "@types/node": "^20.1.1", 66 | "bdd-lazy-var": "^2.5.4", 67 | "chai": "^4.3.6", 68 | "chai-as-promised": "^7.1.1", 69 | "eslint": "^8.11.0", 70 | "eslint-config-nicenice": "^3.0.0", 71 | "eslint-config-standard": "^17.0.0", 72 | "eslint-config-standard-jsx": "^11.0.0", 73 | "eslint-plugin-import": "^2.25.4", 74 | "eslint-plugin-n": "^16.0.1", 75 | "eslint-plugin-no-only-tests": "^2.4.0", 76 | "eslint-plugin-promise": "^6.0.0", 77 | "eslint-plugin-react": "^7.29.4", 78 | "eslint-plugin-react-camel-case": "^1.1.1", 79 | "eslint-plugin-standard": "^5.0.0", 80 | "mocha": "^10.0.0", 81 | "nodemon": "^3.0.1", 82 | "rimraf": "^5.0.0", 83 | "sinon": "^17.0.1", 84 | "sinon-chai": "^3.5.0", 85 | "typescript": "^5.0.4", 86 | "yargs": "^17.4.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description of the change 2 | 3 | Description here 4 | 5 | ## Type of change 6 | 7 | - [ ] Bug fix (non-breaking change that fixes an issue) 8 | - [ ] New feature (non-breaking change that adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | 11 | ## Related issues 12 | 13 | Fixes 14 | 15 | ## Checklists 16 | 17 | ### Development 18 | 19 | - [ ] The code changed/added as part of this pull request has been covered with tests 20 | - [ ] All tests related to the changed code pass in development 21 | - [ ] No previous tests unrelated to the changed code fail in development 22 | 23 | ### Code review 24 | 25 | - [ ] This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached. 26 | - [ ] At least one reviewer has been requested 27 | - [ ] Changes have been reviewed by at least one other engineer 28 | - [ ] The relevant project board has been selected in Projects to auto-link to this pull request 29 | -------------------------------------------------------------------------------- /src/UploadWithOptions.js: -------------------------------------------------------------------------------- 1 | export default class UploadWithOptions { 2 | constructor (streamLikeThing, formDataAppendOptions) { 3 | this.streamLikeThing = streamLikeThing 4 | this.formDataAppendOptions = formDataAppendOptions 5 | } 6 | 7 | get options () { 8 | return this.formDataAppendOptions 9 | } 10 | 11 | get file () { 12 | return this.streamLikeThing 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | // See if the JSON looks like it's got errors 2 | export function looksLikeJsonError ({ json }) { 3 | return !!(json && (json.errors || json.message || json.name)) 4 | } 5 | 6 | // Should return an array 7 | export function normalizeJsonErrors ({ json, statusText = 'Unknown Error' }) { 8 | if (json) { 9 | // Normal, GraphQL way 10 | if (json.errors) { 11 | return json.errors 12 | } 13 | 14 | // Alternative way from some REST calls: 15 | // { 16 | // "name": "AssertionError", 17 | // "message": "PDF did not generate properly from given HTML!" 18 | // } 19 | // 20 | // OR 21 | // 22 | // { 23 | // "name": "ValidationError", 24 | // "fields":[{ "message": "Required", "property": "data" }] 25 | // } 26 | if (json.message || json.name) { 27 | return [json] 28 | } 29 | } 30 | 31 | // Hmm, ok. Default way 32 | return [{ name: statusText, message: statusText }] 33 | } 34 | 35 | // Should return an array 36 | export function normalizeNodeError ({ error, statusText = 'Unknown Error' }) { 37 | if (error) { 38 | return [pickError(error)] 39 | } 40 | 41 | // Hmm, ok. Default way 42 | return [{ name: statusText, message: statusText }] 43 | } 44 | 45 | function pickError (error) { 46 | return (({ name, message, code, cause, stack }) => ({ name, message, code, cause, stack }))(error) 47 | } 48 | -------------------------------------------------------------------------------- /src/graphql/index.js: -------------------------------------------------------------------------------- 1 | export * as queries from './queries' 2 | export * as mutations from './mutations' 3 | -------------------------------------------------------------------------------- /src/graphql/mutations/createEtchPacket.js: -------------------------------------------------------------------------------- 1 | const defaultResponseQuery = `{ 2 | eid 3 | name 4 | status 5 | isTest 6 | allowUpdates 7 | numberRemainingSigners 8 | detailsURL 9 | webhookURL 10 | completedAt 11 | archivedAt 12 | createdAt 13 | updatedAt 14 | documentGroup { 15 | eid 16 | status 17 | files 18 | signers { 19 | eid 20 | aliasId 21 | routingOrder 22 | name 23 | email 24 | status 25 | signActionType 26 | } 27 | } 28 | }` 29 | 30 | export const generateMutation = (responseQuery = defaultResponseQuery) => ` 31 | mutation CreateEtchPacket( 32 | $name: String, 33 | $organizationEid: String, 34 | $files: [EtchFile!], 35 | $isDraft: Boolean, 36 | $isTest: Boolean, 37 | $signatureEmailSubject: String, 38 | $signatureEmailBody: String, 39 | $signatureProvider: String, 40 | $signaturePageOptions: JSON, 41 | $signers: [JSON!], 42 | $data: JSON, 43 | $webhookURL: String, 44 | $replyToName: String, 45 | $replyToEmail: String, 46 | $enableEmails: JSON, 47 | $createCastTemplatesFromUploads: Boolean, 48 | $duplicateCasts: Boolean, 49 | $mergePDFs: Boolean, 50 | $allowUpdates: Boolean 51 | ) { 52 | createEtchPacket( 53 | name: $name, 54 | organizationEid: $organizationEid, 55 | files: $files, 56 | isDraft: $isDraft, 57 | isTest: $isTest, 58 | signatureEmailSubject: $signatureEmailSubject, 59 | signatureEmailBody: $signatureEmailBody, 60 | signatureProvider: $signatureProvider, 61 | signaturePageOptions: $signaturePageOptions, 62 | signers: $signers, 63 | data: $data, 64 | webhookURL: $webhookURL, 65 | replyToName: $replyToName, 66 | replyToEmail: $replyToEmail, 67 | enableEmails: $enableEmails, 68 | createCastTemplatesFromUploads: $createCastTemplatesFromUploads, 69 | duplicateCasts: $duplicateCasts, 70 | mergePDFs: $mergePDFs, 71 | allowUpdates: $allowUpdates 72 | ) ${responseQuery} 73 | }` 74 | -------------------------------------------------------------------------------- /src/graphql/mutations/forgeSubmit.js: -------------------------------------------------------------------------------- 1 | const defaultResponseQuery = `{ 2 | id 3 | eid 4 | status 5 | continueURL 6 | payloadValue 7 | currentStep 8 | completedAt 9 | createdAt 10 | updatedAt 11 | signer { 12 | name 13 | email 14 | status 15 | routingOrder 16 | } 17 | weldData { 18 | id 19 | eid 20 | isTest 21 | isComplete 22 | agents 23 | } 24 | }` 25 | 26 | export const generateMutation = (responseQuery = defaultResponseQuery) => ` 27 | mutation ForgeSubmit( 28 | $forgeEid: String!, 29 | $weldDataEid: String, 30 | $submissionEid: String, 31 | $payload: JSON!, 32 | $enforcePayloadValidOnCreate: Boolean, 33 | $currentStep: Int, 34 | $complete: Boolean, 35 | $isTest: Boolean, 36 | $timezone: String, 37 | $webhookURL: String, 38 | $groupArrayId: String, 39 | $groupArrayIndex: Int 40 | ) { 41 | forgeSubmit( 42 | forgeEid: $forgeEid, 43 | weldDataEid: $weldDataEid, 44 | submissionEid: $submissionEid, 45 | payload: $payload, 46 | enforcePayloadValidOnCreate: $enforcePayloadValidOnCreate, 47 | currentStep: $currentStep, 48 | complete: $complete, 49 | isTest: $isTest, 50 | timezone: $timezone, 51 | webhookURL: $webhookURL, 52 | groupArrayId: $groupArrayId, 53 | groupArrayIndex: $groupArrayIndex 54 | ) ${responseQuery} 55 | }` 56 | -------------------------------------------------------------------------------- /src/graphql/mutations/generateEtchSignUrl.js: -------------------------------------------------------------------------------- 1 | export const generateMutation = () => ` 2 | mutation GenerateEtchSignURL ( 3 | $signerEid: String!, 4 | $clientUserId: String!, 5 | ) { 6 | generateEtchSignURL ( 7 | signerEid: $signerEid, 8 | clientUserId: $clientUserId 9 | ) 10 | } 11 | ` 12 | -------------------------------------------------------------------------------- /src/graphql/mutations/index.js: -------------------------------------------------------------------------------- 1 | export * as createEtchPacket from './createEtchPacket' 2 | export * as forgeSubmit from './forgeSubmit' 3 | export * as generateEtchSignUrl from './generateEtchSignUrl' 4 | export * as removeWeldData from './removeWeldData' 5 | -------------------------------------------------------------------------------- /src/graphql/mutations/removeWeldData.js: -------------------------------------------------------------------------------- 1 | export const generateMutation = () => ` 2 | mutation RemoveWeldData ( 3 | $eid: String!, 4 | ) { 5 | removeWeldData ( 6 | eid: $eid, 7 | ) 8 | }` 9 | -------------------------------------------------------------------------------- /src/graphql/queries/etchPacket.js: -------------------------------------------------------------------------------- 1 | const defaultResponseQuery = `{ 2 | id 3 | eid 4 | name 5 | status 6 | isTest 7 | numberRemainingSigners 8 | webhookURL 9 | detailsURL 10 | completedAt 11 | archivedAt 12 | createdAt 13 | updatedAt 14 | documentGroup { 15 | id 16 | eid 17 | status 18 | files 19 | signers { 20 | id 21 | eid 22 | aliasId 23 | routingOrder 24 | name 25 | email 26 | status 27 | signActionType 28 | } 29 | } 30 | }` 31 | 32 | export const generateQuery = (responseQuery = defaultResponseQuery) => ` 33 | query EtchPacket ( 34 | $eid: String!, 35 | ) { 36 | etchPacket ( 37 | eid: $eid, 38 | ) ${responseQuery} 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /src/graphql/queries/index.js: -------------------------------------------------------------------------------- 1 | export * as etchPacket from './etchPacket' 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | // We are only importing this for the type.. 3 | import { Stream } from 'stream' // eslint-disable-line no-unused-vars 4 | 5 | import AbortController from 'abort-controller' 6 | import { RateLimiter } from 'limiter' 7 | 8 | import UploadWithOptions from './UploadWithOptions' 9 | import { version, description } from '../package.json' 10 | import { looksLikeJsonError, normalizeNodeError, normalizeJsonErrors } from './errors' 11 | import { queries, mutations } from './graphql' 12 | import { 13 | isFile, 14 | graphQLUploadSchemaIsValid, 15 | } from './validation' 16 | 17 | class Warning extends Error {} 18 | 19 | let extractFiles 20 | let FormDataModule 21 | let Fetch 22 | let fetch 23 | 24 | /** 25 | * @typedef AnvilOptions 26 | * @type {Object} 27 | * @property {string} [apiKey] 28 | * @property {string} [accessToken] 29 | * @property {string} [baseURL] 30 | * @property {string} [userAgent] 31 | * @property {number} [requestLimit] 32 | * @property {number} [requestLimitMS] 33 | */ 34 | 35 | /** 36 | * @typedef GraphQLResponse 37 | * @type {Object} 38 | * @property {number} statusCode 39 | * @property {GraphQLResponseData} [data] 40 | * @property {Array} [errors] 41 | */ 42 | 43 | /** @typedef {{ 44 | data: {[ key: string]: any } 45 | }} GraphQLResponseData */ 46 | 47 | /** 48 | * @typedef RESTResponse 49 | * @type {Object} 50 | * @property {number} statusCode 51 | * @property {Buffer|Stream|Object} [data] 52 | * @property {Array} [errors] 53 | * @property {any} [response] node-fetch Response 54 | */ 55 | 56 | /** @typedef {{ 57 | name: string; 58 | message: string; 59 | stack: string; 60 | code: string; 61 | cause?: any; 62 | }} NodeError */ 63 | 64 | /** @typedef {{ 65 | message: string, 66 | status?: number, 67 | name?: string, 68 | fields?: Array 69 | [key: string]: any 70 | }} ResponseError */ 71 | 72 | /** @typedef {{ 73 | message: string, 74 | property?: string, 75 | [key: string]: any 76 | }} ResponseErrorField */ 77 | 78 | /** @typedef {{ 79 | path: string 80 | }} Readable */ 81 | 82 | // Ignoring the below since they are dynamically created depepending on what's 83 | // inside the `src/graphql` directory. 84 | const { 85 | mutations: { 86 | // @ts-ignore 87 | createEtchPacket: { 88 | generateMutation: generateCreateEtchPacketMutation, 89 | }, 90 | // @ts-ignore 91 | forgeSubmit: { 92 | generateMutation: generateForgeSubmitMutation, 93 | }, 94 | // @ts-ignore 95 | generateEtchSignUrl: { 96 | generateMutation: generateEtchSignUrlMutation, 97 | }, 98 | // @ts-ignore 99 | removeWeldData: { 100 | generateMutation: generateRemoveWeldDataMutation, 101 | }, 102 | }, 103 | queries: { 104 | // @ts-ignore 105 | etchPacket: { 106 | generateQuery: generateEtchPacketQuery, 107 | }, 108 | }, 109 | } = { queries, mutations } 110 | 111 | const DATA_TYPE_STREAM = 'stream' 112 | const DATA_TYPE_BUFFER = 'buffer' 113 | const DATA_TYPE_ARRAY_BUFFER = 'arrayBuffer' 114 | const DATA_TYPE_JSON = 'json' 115 | 116 | const SUPPORTED_BINARY_DATA_TYPES = Object.freeze([ 117 | DATA_TYPE_STREAM, 118 | DATA_TYPE_BUFFER, 119 | DATA_TYPE_ARRAY_BUFFER, 120 | ]) 121 | 122 | // Version number to use for latest versions (usually drafts) 123 | const VERSION_LATEST = -1 124 | // Version number to use for the latest published version. 125 | // This is the default when a version is not provided. 126 | const VERSION_LATEST_PUBLISHED = -2 127 | 128 | const defaultOptions = { 129 | baseURL: 'https://app.useanvil.com', 130 | userAgent: `${description}/${version}`, 131 | } 132 | 133 | const FILENAME_IGNORE_MESSAGE = 'If you think you can ignore this, please pass `options.ignoreFilenameValidation` as `true`.' 134 | 135 | const failBufferMS = 50 136 | 137 | class Anvil { 138 | // { 139 | // apiKey: , 140 | // accessToken: , // OR oauth access token 141 | // baseURL: 'https://app.useanvil.com' 142 | // userAgent: 'Anvil API Client/2.0.0' 143 | // } 144 | /** 145 | * @param {AnvilOptions?} options 146 | */ 147 | constructor (options) { 148 | if (!options) throw new Error('options are required') 149 | 150 | this.options = { 151 | ...defaultOptions, 152 | requestLimit: 1, 153 | requestLimitMS: 1000, 154 | ...options, 155 | } 156 | 157 | const { apiKey, accessToken } = this.options 158 | if (!(apiKey || accessToken)) throw new Error('apiKey or accessToken required') 159 | 160 | this.authHeader = accessToken 161 | ? `Bearer ${Buffer.from(accessToken, 'ascii').toString('base64')}` 162 | : `Basic ${Buffer.from(`${apiKey}:`, 'ascii').toString('base64')}` 163 | 164 | // Indicates that we have not dynamically set the Rate Limit from the API response 165 | this.hasSetLimiterFromResponse = false 166 | // Indicates that we are in the process setting the Rate Limit from an API response 167 | this.limiterSettingInProgress = false 168 | // A Promise that all early requests will have to wait for before continuing on. This 169 | // promise will be resolved by the first API response 170 | this.rateLimiterSetupPromise = new Promise((resolve) => { 171 | this.rateLimiterPromiseResolver = resolve 172 | }) 173 | 174 | // Set our initial limiter 175 | this._setRateLimiter({ tokens: this.options.requestLimit, intervalMs: this.options.requestLimitMS }) 176 | } 177 | 178 | /** 179 | * @param {Object} options 180 | * @param {number} options.tokens 181 | * @param {number} options.intervalMs 182 | * @private 183 | */ 184 | _setRateLimiter ({ tokens, intervalMs }) { 185 | if ( 186 | // Both must be truthy 187 | !(tokens && intervalMs) || 188 | // Things should not be the same as they already are 189 | (this.limitTokens === tokens && this.limitIntervalMs === intervalMs) 190 | ) { 191 | return 192 | } 193 | 194 | const newLimiter = new RateLimiter({ tokensPerInterval: tokens, interval: intervalMs }) 195 | 196 | // If we already had a limiter, let's try to pick up where it left off 197 | if (this.limiter) { 198 | const tokensInUse = Math.max( 199 | // getTokensRemaining() can return a decimal, so we round it down 200 | // so as to be conservative about potentially hitting the API again 201 | this.limitTokens - Math.floor(this.limiter.getTokensRemaining()), 202 | 0, 203 | ) 204 | const tokensToRemove = Math.min(tokens, tokensInUse) 205 | if (tokensToRemove) { 206 | newLimiter.tryRemoveTokens(tokensToRemove) 207 | } 208 | delete this.limiter 209 | } 210 | 211 | this.limitTokens = tokens 212 | this.limitIntervalMs = intervalMs 213 | this.limiter = newLimiter 214 | } 215 | 216 | /** 217 | * Perform some handy/necessary things for a GraphQL file upload to make it work 218 | * with this client and with our backend 219 | * 220 | * @param {string|Buffer|Readable|File|Blob} pathOrStreamLikeThing - Either a string path to a file, 221 | * a Buffer, or a Stream-like thing that is compatible with form-data as an append. 222 | * @param {Object} [formDataAppendOptions] - User can specify options to be passed to the form-data.append 223 | * call. This should be done if a stream-like thing is not one of the common types that 224 | * form-data can figure out on its own. 225 | * 226 | * @return {UploadWithOptions} - A class that wraps the stream-like-thing and any options 227 | * up together nicely in a way that we can also tell that it was us who did it. 228 | */ 229 | static prepareGraphQLFile (pathOrStreamLikeThing, { ignoreFilenameValidation, ...formDataAppendOptions } = {}) { 230 | if (typeof pathOrStreamLikeThing === 'string') { 231 | // @ts-ignore 232 | // no-op for this logic path. It's a path and we will load it later and it will at least 233 | // have the file's name as a filename to possibly use. 234 | } else if ( 235 | !formDataAppendOptions || 236 | ( 237 | formDataAppendOptions && !( 238 | // Require the filename or the ignoreFilenameValidation option. This is an escape hatch 239 | // for things we didn't anticipate to cause problems 240 | formDataAppendOptions.filename || ignoreFilenameValidation 241 | ) 242 | ) 243 | ) { 244 | // OK, there's a chance here that a `filename` needs to be provided via formDataAppendOptions 245 | if ( 246 | // Buffer has no way to get the filename 247 | pathOrStreamLikeThing instanceof Buffer || 248 | !( 249 | // Some stream things have a string path in them (can also be a buffer, but we want/need string) 250 | // @ts-ignore 251 | (pathOrStreamLikeThing.path && typeof pathOrStreamLikeThing.path === 'string') || 252 | // A File might look like this 253 | // @ts-ignore 254 | (pathOrStreamLikeThing.name && typeof pathOrStreamLikeThing.name === 'string') 255 | ) 256 | ) { 257 | let message = 'For this type of input, `options.filename` must be provided to prepareGraphQLFile.' + ' ' + FILENAME_IGNORE_MESSAGE 258 | try { 259 | if (pathOrStreamLikeThing && pathOrStreamLikeThing.constructor && pathOrStreamLikeThing.constructor.name) { 260 | message = `When passing a ${pathOrStreamLikeThing.constructor.name} to prepareGraphQLFile, \`options.filename\` must be provided. ${FILENAME_IGNORE_MESSAGE}` 261 | } 262 | } catch (err) { 263 | console.error(err) 264 | } 265 | 266 | throw new Error(message) 267 | } 268 | } 269 | 270 | return new UploadWithOptions(pathOrStreamLikeThing, formDataAppendOptions) 271 | } 272 | 273 | /** 274 | * Runs the createEtchPacket mutation. 275 | * @param {Object} data 276 | * @param {Object} data.variables 277 | * @param {string} [data.responseQuery] 278 | * @param {string} [data.mutation] 279 | * @returns {Promise} 280 | */ 281 | createEtchPacket ({ variables, responseQuery, mutation }) { 282 | return this.requestGraphQL( 283 | { 284 | query: mutation || generateCreateEtchPacketMutation(responseQuery), 285 | variables, 286 | }, 287 | { dataType: DATA_TYPE_JSON }, 288 | ) 289 | } 290 | 291 | /** 292 | * @param {string} documentGroupEid 293 | * @param {Object} [clientOptions] 294 | * @returns {Promise} 295 | */ 296 | downloadDocuments (documentGroupEid, clientOptions = {}) { 297 | const { dataType = DATA_TYPE_BUFFER } = clientOptions 298 | if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) { 299 | throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`) 300 | } 301 | return this.requestREST( 302 | `/api/document-group/${documentGroupEid}.zip`, 303 | { method: 'GET' }, 304 | { 305 | ...clientOptions, 306 | dataType, 307 | }, 308 | ) 309 | } 310 | 311 | /** 312 | * @param {string} pdfTemplateID 313 | * @param {Object} payload 314 | * @param {Object} [clientOptions] 315 | * @returns {Promise} 316 | */ 317 | fillPDF (pdfTemplateID, payload, clientOptions = {}) { 318 | const { dataType = DATA_TYPE_BUFFER } = clientOptions 319 | if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) { 320 | throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`) 321 | } 322 | 323 | const versionNumber = clientOptions.versionNumber 324 | const url = versionNumber 325 | ? `/api/v1/fill/${pdfTemplateID}.pdf?versionNumber=${versionNumber}` 326 | : `/api/v1/fill/${pdfTemplateID}.pdf` 327 | 328 | return this.requestREST( 329 | url, 330 | { 331 | method: 'POST', 332 | body: JSON.stringify(payload), 333 | headers: { 334 | 'Content-Type': 'application/json', 335 | }, 336 | }, 337 | { 338 | ...clientOptions, 339 | dataType, 340 | }, 341 | ) 342 | } 343 | 344 | /** 345 | * @param {Object} data 346 | * @param {Object} data.variables 347 | * @param {string} [data.responseQuery] 348 | * @param {string} [data.mutation] 349 | * @returns {Promise} 350 | */ 351 | forgeSubmit ({ variables, responseQuery, mutation }) { 352 | return this.requestGraphQL( 353 | { 354 | query: mutation || generateForgeSubmitMutation(responseQuery), 355 | variables, 356 | }, 357 | { dataType: DATA_TYPE_JSON }, 358 | ) 359 | } 360 | 361 | /** 362 | * @param {Object} payload 363 | * @param {Object} [clientOptions] 364 | * @returns {Promise} 365 | */ 366 | generatePDF (payload, clientOptions = {}) { 367 | const { dataType = DATA_TYPE_BUFFER } = clientOptions 368 | if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) { 369 | throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`) 370 | } 371 | 372 | return this.requestREST( 373 | '/api/v1/generate-pdf', 374 | { 375 | method: 'POST', 376 | body: JSON.stringify(payload), 377 | headers: { 378 | 'Content-Type': 'application/json', 379 | }, 380 | }, 381 | { 382 | ...clientOptions, 383 | dataType, 384 | }, 385 | ) 386 | } 387 | 388 | /** 389 | * @param {Object} data 390 | * @param {Object} data.variables 391 | * @param {string} [data.responseQuery] 392 | * @returns {Promise} 393 | */ 394 | getEtchPacket ({ variables, responseQuery }) { 395 | return this.requestGraphQL( 396 | { 397 | query: generateEtchPacketQuery(responseQuery), 398 | variables, 399 | }, 400 | { dataType: DATA_TYPE_JSON }, 401 | ) 402 | } 403 | 404 | /** 405 | * @param {Object} data 406 | * @param {Object} data.variables 407 | * @returns {Promise<{url?: string, errors?: Array, statusCode: number}>} 408 | */ 409 | async generateEtchSignUrl ({ variables }) { 410 | const { statusCode, data, errors } = await this.requestGraphQL( 411 | { 412 | query: generateEtchSignUrlMutation(), 413 | variables, 414 | }, 415 | { dataType: DATA_TYPE_JSON }, 416 | ) 417 | 418 | return { 419 | statusCode, 420 | url: data && data.data && data.data.generateEtchSignURL, 421 | errors, 422 | } 423 | } 424 | 425 | /** 426 | * @param {Object} data 427 | * @param {Object} data.variables 428 | * @param {string} [data.mutation] 429 | * @returns {Promise} 430 | */ 431 | removeWeldData ({ variables, mutation }) { 432 | return this.requestGraphQL( 433 | { 434 | query: mutation || generateRemoveWeldDataMutation(), 435 | variables, 436 | }, 437 | { dataType: DATA_TYPE_JSON }, 438 | ) 439 | } 440 | 441 | /** 442 | * @param {Object} data 443 | * @param {string} data.query 444 | * @param {Object} [data.variables] 445 | * @param {Object} [clientOptions] 446 | * @returns {Promise} 447 | */ 448 | async requestGraphQL ({ query, variables = {} }, clientOptions) { 449 | // Some helpful resources on how this came to be: 450 | // https://github.com/jaydenseric/graphql-upload/issues/125#issuecomment-440853538 451 | // https://zach.codes/building-a-file-upload-hook/ 452 | // https://github.com/jaydenseric/graphql-react/blob/1b1234de5de46b7a0029903a1446dcc061f37d09/src/universal/graphqlFetchOptions.mjs 453 | // https://www.npmjs.com/package/extract-files 454 | 455 | const options = { 456 | method: 'POST', 457 | headers: {}, 458 | } 459 | 460 | const originalOperation = { query, variables } 461 | 462 | extractFiles ??= (await import('extract-files/extractFiles.mjs')).default 463 | 464 | const { 465 | clone: augmentedOperation, 466 | files: filesMap, 467 | } = extractFiles(originalOperation, isFile) 468 | 469 | const operationJSON = JSON.stringify(augmentedOperation) 470 | 471 | // Checks for both File uploads and Base64 uploads 472 | if (!graphQLUploadSchemaIsValid(originalOperation)) { 473 | throw new Error('Invalid File schema detected') 474 | } 475 | 476 | if (filesMap.size) { 477 | // @ts-ignore 478 | const abortController = new AbortController() 479 | Fetch ??= await import('@anvilco/node-fetch') 480 | // This is a dependency of 'node-fetch'` 481 | FormDataModule ??= await import('formdata-polyfill/esm.min.js') 482 | const form = new FormDataModule.FormData() 483 | 484 | form.append('operations', operationJSON) 485 | 486 | const map = {} 487 | let i = 0 488 | filesMap.forEach(paths => { 489 | map[++i] = paths 490 | }) 491 | form.append('map', JSON.stringify(map)) 492 | 493 | i = 0 494 | filesMap.forEach((paths, file) => { 495 | // Ensure that the file has been run through the prepareGraphQLFile process 496 | // and checks 497 | if (file instanceof UploadWithOptions === false) { 498 | file = Anvil.prepareGraphQLFile(file) 499 | } 500 | let { filename, mimetype, ignoreFilenameValidation } = file.options || {} 501 | file = file.file 502 | 503 | if (!file) { 504 | throw new Error('No file provided. Options were: ' + JSON.stringify(options)) 505 | } 506 | 507 | // If this is a stream-like thing, attach a listener to the 'error' event so that we 508 | // can cancel the API call if something goes wrong 509 | if (typeof file.on === 'function') { 510 | file.on('error', (err) => { 511 | console.warn(err) 512 | abortController.abort() 513 | }) 514 | } 515 | 516 | // If file a path to a file? 517 | if (typeof file === 'string') { 518 | file = Fetch.fileFromSync(file, mimetype) 519 | } else if (file instanceof Buffer) { 520 | const buffer = file 521 | // https://developer.mozilla.org/en-US/docs/Web/API/File/File 522 | file = new Fetch.File( 523 | [buffer], 524 | filename, 525 | { 526 | type: mimetype, 527 | }, 528 | ) 529 | } else if (file instanceof Stream) { 530 | // https://github.com/node-fetch/node-fetch#post-data-using-a-file 531 | const stream = file 532 | file = { 533 | [Symbol.toStringTag]: 'File', 534 | // @ts-ignore 535 | size: fs.statSync(stream.path).size, 536 | stream: () => stream, 537 | type: mimetype, 538 | } 539 | 540 | // @ts-ignore 541 | filename ??= stream.path.split('/').pop() 542 | } else if (file.constructor.name !== 'File') { 543 | // Like a Blob or something 544 | if (!filename) { 545 | const name = file.name || file.path 546 | if (name) { 547 | filename = name.split('/').pop() 548 | } 549 | 550 | if (!filename && !ignoreFilenameValidation) { 551 | console.warn(new Warning('No filename provided. Please provide a filename to the file options.')) 552 | } 553 | } 554 | } 555 | 556 | // https://developer.mozilla.org/en-US/docs/Web/API/FormData/append 557 | form.append(`${++i}`, file, filename) 558 | }) 559 | 560 | options.signal = abortController.signal 561 | options.body = form 562 | } else { 563 | options.headers['Content-Type'] = 'application/json' 564 | options.body = operationJSON 565 | } 566 | 567 | const { 568 | statusCode, 569 | data, 570 | errors, 571 | } = await this._wrapRequest( 572 | () => this._request('/graphql', options), 573 | clientOptions, 574 | ) 575 | 576 | return { 577 | statusCode, 578 | data, 579 | errors, 580 | } 581 | } 582 | 583 | /** 584 | * @param {string} url 585 | * @param {Object} fetchOptions 586 | * @param {Object} [clientOptions] 587 | * @returns {Promise} 588 | */ 589 | async requestREST (url, fetchOptions, clientOptions) { 590 | const { 591 | response, 592 | statusCode, 593 | data, 594 | errors, 595 | } = await this._wrapRequest( 596 | () => this._request(url, fetchOptions), 597 | clientOptions, 598 | ) 599 | 600 | return { 601 | response, 602 | statusCode, 603 | data, 604 | errors, 605 | } 606 | } 607 | 608 | // ****************************************************************************** 609 | // ___ _ __ 610 | // / _ \____(_) _____ _/ /____ 611 | // / ___/ __/ / |/ / _ `/ __/ -_) 612 | // /_/ /_/ /_/|___/\_,_/\__/\__/ 613 | // 614 | // ALL THE BELOW CODE IS CONSIDERED PRIVATE, AND THE API OR INTERNALS MAY CHANGE AT ANY TIME 615 | // USERS OF THIS MODULE SHOULD NOT USE ANY OF THESE METHODS DIRECTLY 616 | // ****************************************************************************** 617 | 618 | async _request (...args) { 619 | // Only load Fetch once per module process lifetime 620 | Fetch = Fetch || await import('@anvilco/node-fetch') 621 | fetch = Fetch.default 622 | // Monkey-patch so we only try any of this once per Anvil Client instance 623 | this._request = this.__request 624 | return this._request(...args) 625 | } 626 | 627 | /** 628 | * @param {string} url 629 | * @param {Object} options 630 | * @returns {Promise} 631 | * @private 632 | */ 633 | __request (url, options) { 634 | if (!url.startsWith(this.options.baseURL)) { 635 | url = this._url(url) 636 | } 637 | const opts = this._addDefaultHeaders(options) 638 | return fetch(url, opts) 639 | } 640 | 641 | /** 642 | * @param {CallableFunction} retryableRequestFn 643 | * @param {Object} [clientOptions] 644 | * @returns {Promise<*>} 645 | * @private 646 | */ 647 | _wrapRequest (retryableRequestFn, clientOptions = {}) { 648 | return this._throttle(async (retry) => { 649 | let { dataType, debug } = clientOptions 650 | const response = await retryableRequestFn() 651 | 652 | if (!this.hasSetLimiterFromResponse) { 653 | // OK, this is the response sets the rate-limiter values from the 654 | // server response: 655 | 656 | // Set up the new Rate Limiter 657 | const tokens = parseInt(response.headers.get('x-ratelimit-limit')) 658 | const intervalMs = parseInt(response.headers.get('x-ratelimit-interval-ms')) 659 | this._setRateLimiter({ tokens, intervalMs }) 660 | 661 | // Adjust the gates that make this only happen once. 662 | this.hasSetLimiterFromResponse = true 663 | this.limiterSettingInProgress = false 664 | // Resolve the Promise that everyone else was waiting for 665 | this.rateLimiterPromiseResolver() 666 | } 667 | 668 | const { status: statusCode, statusText } = response 669 | 670 | if (statusCode === 429) { 671 | return retry(getRetryMS(response.headers.get('retry-after'))) 672 | } 673 | 674 | let json 675 | let isError = false 676 | let nodeError 677 | 678 | const contentType = response.headers.get('content-type') || response.headers.get('Content-Type') || '' 679 | 680 | // No matter what we were expecting, if the response is JSON, let's parse it and look for 681 | // signs of errors 682 | if (contentType.toLowerCase().includes('application/json')) { 683 | // Re-set the dataType so we don't fall into the wrong flow later on 684 | dataType = DATA_TYPE_JSON 685 | try { 686 | json = await response.json() 687 | isError = looksLikeJsonError({ json }) 688 | } catch (err) { 689 | nodeError = err 690 | if (debug) { 691 | console.warn(`Problem parsing JSON response for status ${statusCode}:`) 692 | console.warn(err) 693 | } 694 | } 695 | } 696 | 697 | if (nodeError || isError || statusCode >= 300) { 698 | const errors = nodeError ? normalizeNodeError({ error: nodeError }) : normalizeJsonErrors({ json, statusText }) 699 | return { response, statusCode, errors } 700 | } 701 | 702 | let data 703 | 704 | switch (dataType) { 705 | case DATA_TYPE_STREAM: 706 | data = response.body 707 | break 708 | case DATA_TYPE_BUFFER: 709 | // Will ask for it as an arrayBuffer (to avoid deprecation warning) but then convert it to a 710 | // Node Buffer. 711 | // https://github.com/node-fetch/node-fetch/pull/1212 712 | // https://github.com/node-fetch/node-fetch/pull/1345 713 | // https://github.com/anvilco/node-anvil/pull/442 714 | data = Buffer.from(await response.arrayBuffer()) 715 | break 716 | case DATA_TYPE_ARRAY_BUFFER: 717 | data = await response.arrayBuffer() 718 | break 719 | case DATA_TYPE_JSON: 720 | // Can't call json() twice, so we'll see if we already did that 721 | data = json || await response.json() 722 | break 723 | default: 724 | console.warn('Using default response dataType of "json". Please specify a dataType.') 725 | data = await response.json() 726 | break 727 | } 728 | 729 | return { 730 | response, 731 | data, 732 | statusCode, 733 | } 734 | }) 735 | } 736 | 737 | /** 738 | * @param {string} path 739 | * @returns {string} 740 | * @private 741 | */ 742 | _url (path) { 743 | return this.options.baseURL + path 744 | } 745 | 746 | /** 747 | * @param {Object} headerObject 748 | * @param {Object} headerObject.options 749 | * @param {Object} headerObject.headers 750 | * @param {Object} [internalOptions] 751 | * @returns {*&{headers: {}}} 752 | * @private 753 | */ 754 | _addHeaders ({ options: existingOptions, headers: newHeaders }, internalOptions = {}) { 755 | const { headers: existingHeaders = {} } = existingOptions 756 | const { defaults = false } = internalOptions 757 | 758 | newHeaders = defaults 759 | ? newHeaders 760 | : Object.entries(newHeaders).reduce((acc, [key, val]) => { 761 | if (val != null) { 762 | acc[key] = val 763 | } 764 | 765 | return acc 766 | }, {}) 767 | 768 | return { 769 | ...existingOptions, 770 | headers: { 771 | ...existingHeaders, 772 | ...newHeaders, 773 | }, 774 | } 775 | } 776 | 777 | /** 778 | * @param {Object} options 779 | * @returns {*} 780 | * @private 781 | */ 782 | _addDefaultHeaders (options) { 783 | const { userAgent } = this.options 784 | return this._addHeaders( 785 | { 786 | options, 787 | headers: { 788 | 'User-Agent': userAgent, 789 | Authorization: this.authHeader, 790 | }, 791 | }, 792 | { defaults: true }, 793 | ) 794 | } 795 | 796 | /** 797 | * @param {CallableFunction} fn 798 | * @returns {Promise<*>} 799 | * @private 800 | */ 801 | async _throttle (fn) { 802 | // If this is one of the first requests being made, we'll want to dynamically 803 | // set the Rate Limiter values from the API response, and hold up everyone else 804 | // while this is happening. 805 | // If we've already gone through the whole setup from the response, then nothing 806 | // special to do 807 | if (!this.hasSetLimiterFromResponse) { 808 | // If limiter setting is already in progress, then this request will have to wait 809 | if (this.limiterSettingInProgress) { 810 | await this.rateLimiterSetupPromise 811 | } else { 812 | // Set the gate so that subsequent calls will have to wait for the resolution 813 | this.limiterSettingInProgress = true 814 | } 815 | } 816 | 817 | const remainingRequests = await this.limiter.removeTokens(1) 818 | if (remainingRequests < 1) { 819 | await sleep(this.options.requestLimitMS + failBufferMS) 820 | } 821 | const retry = async (ms) => { 822 | await sleep(ms) 823 | return this._throttle(fn) 824 | } 825 | 826 | return fn(retry) 827 | } 828 | } 829 | 830 | Anvil.UploadWithOptions = UploadWithOptions 831 | 832 | /** 833 | * @param {string} retryAfterSeconds 834 | * @returns {number} 835 | * @private 836 | */ 837 | function getRetryMS (retryAfterSeconds) { 838 | return Math.round((Math.abs(parseFloat(retryAfterSeconds)) || 0) * 1000) + failBufferMS 839 | } 840 | 841 | /** 842 | * @param {number} ms 843 | * @returns {Promise} 844 | * @private 845 | */ 846 | function sleep (ms) { 847 | return new Promise((resolve) => { 848 | setTimeout(resolve, ms) 849 | }) 850 | } 851 | 852 | Anvil.VERSION_LATEST = VERSION_LATEST 853 | Anvil.VERSION_LATEST_PUBLISHED = VERSION_LATEST_PUBLISHED 854 | 855 | export default Anvil 856 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import UploadWithOptions from './UploadWithOptions' 3 | 4 | // https://www.npmjs.com/package/extract-files/v/6.0.0#type-extractablefilematcher 5 | export function isFile (value) { 6 | return value instanceof UploadWithOptions || value instanceof fs.ReadStream || value instanceof Buffer 7 | } 8 | 9 | export function graphQLUploadSchemaIsValid (schema, parent, key) { 10 | // schema is null or undefined 11 | if (schema == null) { 12 | return true 13 | } 14 | 15 | if (key !== 'file') { 16 | if (schema instanceof Array) { 17 | return schema.every((subSchema) => graphQLUploadSchemaIsValid(subSchema, schema)) 18 | } 19 | 20 | if (schema.constructor.name === 'Object') { 21 | return Object.entries(schema).every(([key, subSchema]) => graphQLUploadSchemaIsValid(subSchema, schema, key)) 22 | } 23 | 24 | return !isFile(schema) 25 | } 26 | 27 | // OK, the key is 'file' 28 | 29 | // All flavors should be nested, and not top-level 30 | if (!(parent && parent.file === schema)) { 31 | return false 32 | } 33 | 34 | // Base64 Upload 35 | if (schema.data) { 36 | // Must be a string and also have the provided keys 37 | return ( 38 | typeof schema.data === 'string' && 39 | ['filename', 'mimetype'].every((requiredKey) => schema[requiredKey]) 40 | ) 41 | } 42 | 43 | return isFile(schema) 44 | } 45 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.js', 3 | env: { 4 | mocha: true, 5 | }, 6 | globals: { 7 | expect: 'readonly', 8 | should: 'readonly', 9 | sinon: 'readonly', 10 | mount: 'readonly', 11 | render: 'readonly', 12 | shallow: 'readonly', 13 | //* ************************************************ 14 | // bdd-lazy-var 15 | // 16 | // In order to get around eslint complaining for now: 17 | // https://github.com/stalniy/bdd-lazy-var/issues/56#issuecomment-639248242 18 | $: 'readonly', 19 | its: 'readonly', 20 | def: 'readonly', 21 | subject: 'readonly', 22 | get: 'readonly', 23 | sharedExamplesFor: 'readonly', 24 | includeExamplesFor: 'readonly', 25 | itBehavesLike: 'readonly', 26 | is: 'readonly', 27 | // 28 | //* ************************************************ 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /test/assets/dummy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilco/node-anvil/ff56ea27765f50ddc0d4fdfb22e2d9fa40bf51bb/test/assets/dummy.pdf -------------------------------------------------------------------------------- /test/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | node_modules 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /test/e2e/node-anvil.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvilco/node-anvil/ff56ea27765f50ddc0d4fdfb22e2d9fa40bf51bb/test/e2e/node-anvil.tgz -------------------------------------------------------------------------------- /test/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "e2e tester", 5 | "main": "index.js", 6 | "scripts": { 7 | "prep": "npm run clean-cache && yarn remove-files", 8 | "remove-files": "rm -rf yarn.lock node_modules/node-anvil", 9 | "clean-cache": "rm -rf $(yarn cache dir)/.tmp" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "node-anvil": "file:node-anvil.tgz" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/environment.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const chai = require('chai') 3 | const sinonChai = require('sinon-chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | 6 | chai.use(sinonChai) 7 | chai.use(chaiAsPromised) 8 | 9 | global.chai = chai 10 | global.sinon = sinon 11 | global.expect = chai.expect 12 | global.should = chai.should() 13 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { RateLimiter } from 'limiter' 5 | import { AbortSignal } from 'abort-controller' 6 | 7 | import Anvil from '../index' 8 | 9 | const assetsDir = path.join(__dirname, 'assets') 10 | 11 | function mockNodeFetchResponse (options = {}) { 12 | const { headers: headersIn = {}, ...rest } = options 13 | const { 14 | status, 15 | statusText, 16 | json, 17 | buffer, 18 | arrayBuffer, 19 | headers = { 20 | 'x-ratelimit-limit': 1, 21 | 'x-ratelimit-interval-ms': 1000, 22 | ...headersIn, 23 | }, 24 | body, 25 | } = rest 26 | 27 | const mock = { 28 | status, 29 | statusText: statusText || ((status && status >= 200 && status < 300) ? 'OK' : 'Please specify error statusText for testing'), 30 | } 31 | 32 | mock.json = typeof json === 'function' ? json : () => json 33 | if (json) { 34 | headers['content-type'] = 'application/json' 35 | } 36 | 37 | mock.buffer = typeof buffer === 'function' ? buffer : () => buffer instanceof Buffer ? buffer : Buffer.from(buffer) 38 | if (arrayBuffer) { 39 | mock.arrayBuffer = typeof buffer === 'function' ? arrayBuffer : () => arrayBuffer 40 | } else { 41 | mock.arrayBuffer = () => { 42 | const buffer = mock.buffer() 43 | const ab = new ArrayBuffer(buffer.length) 44 | const view = new Uint8Array(ab) 45 | for (let i = 0; i < buffer.length; ++i) { 46 | view[i] = buffer[i] 47 | } 48 | return ab 49 | } 50 | } 51 | 52 | mock.body = body 53 | 54 | mock.headers = { 55 | get: (header) => headers[header], 56 | } 57 | 58 | return mock 59 | } 60 | 61 | function fakeThrottle (fn) { 62 | return fn(() => fakeThrottle(fn)) 63 | } 64 | 65 | let FormDataModule 66 | 67 | describe('Anvil API Client', function () { 68 | before(async function () { 69 | FormDataModule ??= await import('formdata-polyfill/esm.min.js') 70 | }) 71 | 72 | beforeEach(function () { 73 | sinon.stub(Anvil.prototype, '_throttle').callsFake(fakeThrottle) 74 | }) 75 | afterEach(function () { 76 | sinon.restore() 77 | }) 78 | 79 | describe('constructor', function () { 80 | it('throws an error when no options specified', async function () { 81 | expect(() => new Anvil()).to.throw('options are required') 82 | }) 83 | 84 | it('throws an error when no apiKey or accessToken specified', async function () { 85 | expect(() => new Anvil({})).to.throw('apiKey or accessToken required') 86 | }) 87 | 88 | it('builds a Basic auth header when apiKey passed in', async function () { 89 | const apiKey = 'abc123' 90 | const client = new Anvil({ apiKey }) 91 | expect(client.authHeader).to.equal(`Basic ${Buffer.from(`${apiKey}:`, 'ascii').toString('base64')}`) 92 | }) 93 | 94 | it('builds a Bearer auth header when accessToken passed in', async function () { 95 | const accessToken = 'def345' 96 | const client = new Anvil({ accessToken }) 97 | expect(client.authHeader).to.equal(`Bearer ${Buffer.from(accessToken, 'ascii').toString('base64')}`) 98 | }) 99 | }) 100 | 101 | describe('REST endpoints', function () { 102 | let client 103 | 104 | beforeEach(async function () { 105 | client = new Anvil({ apiKey: 'abc123' }) 106 | sinon.stub(client, '_request') 107 | }) 108 | 109 | describe('requestREST', function () { 110 | let options, clientOptions, data, result 111 | 112 | it('returns statusCode and data when specified', async function () { 113 | options = { 114 | method: 'POST', 115 | } 116 | clientOptions = { 117 | dataType: 'json', 118 | } 119 | data = { result: 'ok' } 120 | 121 | client._request.callsFake((url, options) => { 122 | return Promise.resolve( 123 | mockNodeFetchResponse({ 124 | status: 200, 125 | json: data, 126 | headers: { 'content-type': 'application/json' }, 127 | }), 128 | ) 129 | }) 130 | const result = await client.requestREST('/test', options, clientOptions) 131 | 132 | expect(client._request).to.have.been.calledOnce 133 | expect(result.statusCode).to.eql(200) 134 | expect(result.data).to.eql(data) 135 | }) 136 | 137 | it('rejects promise when error', async function () { 138 | options = { method: 'POST' } 139 | 140 | client._request.callsFake((url, options) => { 141 | throw new Error('problem') 142 | }) 143 | 144 | await expect(client.requestREST('/test', options)).to.eventually.have.been.rejectedWith('problem') 145 | }) 146 | 147 | it('handles various error response structures', async function () { 148 | options = { 149 | method: 'GET', 150 | } 151 | clientOptions = { 152 | dataType: 'json', 153 | } 154 | 155 | const errors = [ 156 | { 157 | name: 'AssertionError', 158 | message: 'PDF did not generate properly from given HTML!', 159 | }, 160 | { 161 | name: 'ValidationError', 162 | fields: [{ message: 'Required', property: 'data' }], 163 | }, 164 | ] 165 | 166 | for (const error of errors) { 167 | client._request.callsFake((url, options) => { 168 | return Promise.resolve( 169 | mockNodeFetchResponse({ 170 | // Some calls (like those to GraphQL) will return 200 / OKs but actually contain 171 | // errors 172 | status: 200, 173 | statusText: 'OK', 174 | json: () => error, 175 | headers: { 'content-type': 'application/json' }, 176 | }), 177 | ) 178 | }) 179 | 180 | const result = await client.requestREST('/some-endpoint', options, clientOptions) 181 | expect(result.statusCode).to.eql(200) 182 | expect(result.errors).to.eql([error]) 183 | } 184 | }) 185 | 186 | it('recovers when JSON parsing of error response fails AND gives default error structure', async function () { 187 | options = { 188 | method: 'GET', 189 | } 190 | clientOptions = { 191 | dataType: 'json', 192 | } 193 | 194 | client._request.callsFake((url, options) => { 195 | return Promise.resolve( 196 | mockNodeFetchResponse({ 197 | status: 404, 198 | statusText: 'Not Found', 199 | json: () => JSON.parse('will not parse'), 200 | }), 201 | ) 202 | }) 203 | 204 | const result = await client.requestREST('/non-existing-endpoint', options, clientOptions) 205 | expect(result.statusCode).to.eql(404) 206 | expect(result.errors).to.be.an('array').of.length(1) 207 | expect(result.errors[0]).to.include({ 208 | name: 'SyntaxError', 209 | message: 'Unexpected token w in JSON at position 0', 210 | code: undefined, 211 | cause: undefined, 212 | }) 213 | }) 214 | 215 | it('sets the rate limiter from the response headers', async function () { 216 | // Originally, these are true 217 | expect(client.hasSetLimiterFromResponse).to.eql(false) 218 | expect(client.limiterSettingInProgress).to.eql(false) 219 | expect(client.rateLimiterSetupPromise).to.be.an.instanceof(Promise) 220 | expect(client.limitTokens).to.eql(1) 221 | expect(client.limitIntervalMs).to.eql(1000) 222 | expect(client.limiter).to.be.an.instanceof(RateLimiter) 223 | 224 | client._request.callsFake((url, options) => { 225 | return Promise.resolve( 226 | mockNodeFetchResponse({ 227 | status: 200, 228 | json: data, 229 | headers: { 230 | 'x-ratelimit-limit': 42, 231 | 'x-ratelimit-interval-ms': 4200, 232 | }, 233 | }), 234 | ) 235 | }) 236 | 237 | const result = await client.requestREST('/test', options, clientOptions) 238 | 239 | // Afterwards, these are true 240 | expect(client._request).to.have.been.calledOnce 241 | expect(result.statusCode).to.eql(200) 242 | expect(result.data).to.eql(data) 243 | 244 | expect(client.hasSetLimiterFromResponse).to.eql(true) 245 | expect(client.limitTokens).to.eql(42) 246 | expect(client.limitIntervalMs).to.eql(4200) 247 | expect(client.limiter).to.be.an.instanceof(RateLimiter) 248 | }) 249 | 250 | it('retries when a 429 response', async function () { 251 | options = { method: 'POST' } 252 | clientOptions = { dataType: 'json' } 253 | data = { result: 'ok' } 254 | 255 | client._request.onCall(0).callsFake((url, options) => { 256 | return Promise.resolve( 257 | mockNodeFetchResponse({ 258 | status: 429, 259 | headers: { 260 | 'retry-after': '0.2', // in seconds 261 | }, 262 | }), 263 | ) 264 | }) 265 | 266 | client._request.onCall(1).callsFake((url, options) => { 267 | return Promise.resolve( 268 | mockNodeFetchResponse({ 269 | status: 200, 270 | json: data, 271 | }), 272 | ) 273 | }) 274 | 275 | result = await client.requestREST('/test', options, clientOptions) 276 | 277 | expect(client._request).to.have.been.calledTwice 278 | expect(result.statusCode).to.eql(200) 279 | expect(result.data).to.eql(data) 280 | }) 281 | }) 282 | 283 | describe('fillPDF', function () { 284 | def('statusCode', 200) 285 | 286 | beforeEach(async function () { 287 | client._request.callsFake((url, options) => { 288 | return Promise.resolve( 289 | mockNodeFetchResponse({ 290 | status: $.statusCode, 291 | buffer: $.buffer, 292 | json: $.json, 293 | }), 294 | ) 295 | }) 296 | }) 297 | 298 | context('everything goes well', function () { 299 | def('buffer', 'This would be PDF data...') 300 | def('payload', { 301 | title: 'Test', 302 | fontSize: 8, 303 | textColor: '#CC0000', 304 | data: { 305 | helloId: 'hello!', 306 | }, 307 | }) 308 | 309 | it('returns data', async function () { 310 | const payload = $.payload 311 | const result = await client.fillPDF('cast123', payload) 312 | 313 | expect(result.statusCode).to.eql(200) 314 | expect(result.data.toString()).to.eql('This would be PDF data...') 315 | 316 | expect(client._request).to.have.been.calledOnce 317 | 318 | const [url, options] = client._request.lastCall.args 319 | expect(url).to.eql('/api/v1/fill/cast123.pdf') 320 | expect(options).to.eql({ 321 | method: 'POST', 322 | body: JSON.stringify(payload), 323 | headers: { 324 | 'Content-Type': 'application/json', 325 | }, 326 | }) 327 | }) 328 | 329 | it('works with `versionNumber`', async function () { 330 | const payload = $.payload 331 | const result = await client.fillPDF('cast123', payload, { versionNumber: 5 }) 332 | 333 | expect(result.statusCode).to.eql(200) 334 | expect(result.data.toString()).to.eql('This would be PDF data...') 335 | 336 | expect(client._request).to.have.been.calledOnce 337 | 338 | const [url, options] = client._request.lastCall.args 339 | expect(url).to.eql('/api/v1/fill/cast123.pdf?versionNumber=5') 340 | expect(options).to.eql({ 341 | method: 'POST', 342 | body: JSON.stringify(payload), 343 | headers: { 344 | 'Content-Type': 'application/json', 345 | }, 346 | }) 347 | }) 348 | }) 349 | 350 | context('server 400s with errors array in JSON', function () { 351 | const errors = [{ message: 'problem' }] 352 | def('statusCode', 400) 353 | def('json', { errors }) 354 | 355 | it('finds errors and puts them in response', async function () { 356 | const result = await client.fillPDF('cast123', {}) 357 | 358 | expect(client._request).to.have.been.calledOnce 359 | expect(result.statusCode).to.eql(400) 360 | expect(result.errors).to.eql(errors) 361 | }) 362 | }) 363 | 364 | context('server 401s with single error in response', function () { 365 | const error = { name: 'AuthorizationError', message: 'problem' } 366 | def('statusCode', 401) 367 | def('json', error) 368 | 369 | it('finds error and puts it in the response', async function () { 370 | const result = await client.fillPDF('cast123', {}) 371 | 372 | expect(client._request).to.have.been.calledOnce 373 | expect(result.statusCode).to.eql(401) 374 | expect(result.errors).to.eql([error]) 375 | }) 376 | }) 377 | }) 378 | 379 | describe('generatePDF', function () { 380 | def('statusCode', 200) 381 | 382 | beforeEach(async function () { 383 | client._request.callsFake((url, options) => { 384 | return Promise.resolve( 385 | mockNodeFetchResponse({ 386 | status: $.statusCode, 387 | buffer: $.buffer, 388 | json: $.json, 389 | }), 390 | ) 391 | }) 392 | }) 393 | 394 | context('everything goes well', function () { 395 | def('buffer', 'This would be PDF data...') 396 | 397 | it('returns data', async function () { 398 | const payload = { 399 | title: 'Test', 400 | data: [{ 401 | label: 'hello!', 402 | }], 403 | } 404 | 405 | const result = await client.generatePDF(payload) 406 | 407 | expect(result.statusCode).to.eql(200) 408 | expect(result.data.toString()).to.eql('This would be PDF data...') 409 | 410 | expect(client._request).to.have.been.calledOnce 411 | 412 | const [url, options] = client._request.lastCall.args 413 | expect(url).to.eql('/api/v1/generate-pdf') 414 | expect(options).to.eql({ 415 | method: 'POST', 416 | body: JSON.stringify(payload), 417 | headers: { 418 | 'Content-Type': 'application/json', 419 | }, 420 | }) 421 | }) 422 | }) 423 | 424 | context('server 400s with errors array in JSON', function () { 425 | const errors = [{ message: 'problem' }] 426 | def('statusCode', 400) 427 | def('json', { errors }) 428 | 429 | it('finds errors and puts them in response', async function () { 430 | const result = await client.generatePDF('cast123', {}) 431 | 432 | expect(client._request).to.have.been.calledOnce 433 | expect(result.statusCode).to.eql(400) 434 | expect(result.errors).to.eql(errors) 435 | }) 436 | }) 437 | 438 | context('server 401s with single error in response', function () { 439 | const error = { name: 'AuthorizationError', message: 'Not logged in.' } 440 | def('statusCode', 401) 441 | def('json', error) 442 | 443 | it('finds error and puts it in the response', async function () { 444 | const result = await client.generatePDF('cast123', {}) 445 | 446 | expect(client._request).to.have.been.calledOnce 447 | expect(result.statusCode).to.eql(401) 448 | expect(result.errors).to.eql([error]) 449 | }) 450 | }) 451 | }) 452 | 453 | describe('downloadDocuments', function () { 454 | def('statusCode', 200) 455 | def('buffer', 'This would be Zip file data buffer...') 456 | def('body', 'This would be Zip file data stream') 457 | def('nodeFetchResponse', () => mockNodeFetchResponse({ 458 | status: $.statusCode, 459 | buffer: $.buffer, 460 | body: $.body, 461 | json: $.json, 462 | })) 463 | 464 | beforeEach(async function () { 465 | client._request.callsFake((url, options) => { 466 | return Promise.resolve($.nodeFetchResponse) 467 | }) 468 | }) 469 | 470 | context('everything goes well', function () { 471 | it('returns data as buffer', async function () { 472 | const { statusCode, response, data, errors } = await client.downloadDocuments('docGroupEid123') 473 | expect(data).to.be.an.instanceOf(Buffer) 474 | expect(statusCode).to.eql(200) 475 | expect(response).to.deep.eql($.nodeFetchResponse) 476 | expect(data.toString()).to.eql($.buffer) 477 | expect(errors).to.be.undefined 478 | }) 479 | 480 | it('returns data asn arrayBuffer', async function () { 481 | const { statusCode, response, data, errors } = await client.downloadDocuments('docGroupEid123', { dataType: 'arrayBuffer' }) 482 | expect(data).to.be.an.instanceOf(ArrayBuffer) 483 | expect(statusCode).to.eql(200) 484 | expect(response).to.deep.eql($.nodeFetchResponse) 485 | expect(Buffer.from(data).toString()).to.eql($.buffer) 486 | expect(errors).to.be.undefined 487 | }) 488 | 489 | it('returns data as stream', async function () { 490 | const { statusCode, response, data, errors } = await client.downloadDocuments('docGroupEid123', { dataType: 'stream' }) 491 | expect(statusCode).to.eql(200) 492 | expect(response).to.deep.eql($.nodeFetchResponse) 493 | expect(data).to.eql($.body) 494 | expect(errors).to.be.undefined 495 | }) 496 | }) 497 | 498 | context('unsupported options', function () { 499 | it('raises appropriate error', async function () { 500 | try { 501 | await client.downloadDocuments('docGroupEid123', { dataType: 'json' }) 502 | } catch (e) { 503 | expect(e.message).to.eql('dataType must be one of: stream|buffer|arrayBuffer') 504 | } 505 | }) 506 | }) 507 | 508 | context('server 400s with errors array in JSON', function () { 509 | const errors = [{ message: 'problem' }] 510 | def('statusCode', 400) 511 | def('json', { errors }) 512 | 513 | it('finds errors and puts them in response', async function () { 514 | const { statusCode, errors } = await client.downloadDocuments('docGroupEid123') 515 | 516 | expect(client._request).to.have.been.calledOnce 517 | expect(statusCode).to.eql(400) 518 | expect(errors).to.eql(errors) 519 | }) 520 | }) 521 | 522 | context('server 401s with single error in response', function () { 523 | const error = { name: 'AuthorizationError', message: 'problem' } 524 | def('statusCode', 401) 525 | def('json', error) 526 | 527 | it('finds error and puts it in the response', async function () { 528 | const { statusCode, errors } = await client.downloadDocuments('docGroupEid123') 529 | 530 | expect(client._request).to.have.been.calledOnce 531 | expect(statusCode).to.eql(401) 532 | expect(errors).to.eql([error]) 533 | }) 534 | }) 535 | }) 536 | }) 537 | 538 | describe('GraphQL', function () { 539 | const client = new Anvil({ apiKey: 'abc123' }) 540 | 541 | describe('requestGraphQL', function () { 542 | beforeEach(function () { 543 | sinon.stub(client, '_wrapRequest') 544 | client._wrapRequest.callsFake(async () => ({})) 545 | sinon.stub(client, '_request') 546 | }) 547 | 548 | describe('without files', function () { 549 | it('stringifies query and variables', async function () { 550 | const query = { foo: 'bar' } 551 | const variables = { baz: 'bop' } 552 | const clientOptions = { yo: 'mtvRaps' } 553 | 554 | await client.requestGraphQL({ query, variables }, clientOptions) 555 | 556 | expect(client._wrapRequest).to.have.been.calledOnce 557 | 558 | const [fn, clientOptionsReceived] = client._wrapRequest.lastCall.args 559 | expect(clientOptions).to.eql(clientOptionsReceived) 560 | 561 | fn() 562 | 563 | expect(client._request).to.have.been.calledOnce 564 | const [, options] = client._request.lastCall.args 565 | const { 566 | method, 567 | headers, 568 | body, 569 | } = options 570 | 571 | expect(method).to.eql('POST') 572 | expect(headers).to.eql({ 'Content-Type': 'application/json' }) 573 | expect(body).to.eql(JSON.stringify({ query, variables })) 574 | }) 575 | }) 576 | 577 | describe('with files', function () { 578 | beforeEach(function () { 579 | sinon.spy(FormDataModule.FormData.prototype, 'append') 580 | }) 581 | 582 | describe('schema is good', function () { 583 | const query = { foo: 'bar', baz: null } 584 | const clientOptions = { yo: 'mtvRaps' } 585 | 586 | afterEach(function () { 587 | if ($.willFail) { 588 | expect(client._wrapRequest).to.not.have.been.called 589 | return 590 | } 591 | 592 | expect(client._wrapRequest).to.have.been.calledOnce 593 | 594 | const [fn, clientOptionsReceived] = client._wrapRequest.lastCall.args 595 | expect(clientOptions).to.eql(clientOptionsReceived) 596 | 597 | fn() 598 | 599 | expect(client._request).to.have.been.calledOnce 600 | const [, options] = client._request.lastCall.args 601 | 602 | const { 603 | method, 604 | headers, 605 | body, 606 | signal, 607 | } = options 608 | 609 | expect(method).to.eql('POST') 610 | if ($.isBase64) { 611 | expect(headers).to.eql({ 612 | 'Content-Type': 'application/json', 613 | }) 614 | // Vars are untouched 615 | expect(JSON.parse(body).variables).to.eql($.variables) 616 | } else { 617 | expect(headers).to.eql({}) // node-fetch will add appropriate header 618 | expect(body).to.be.an.instanceof(FormDataModule.FormData) 619 | expect(signal).to.be.an.instanceof(AbortSignal) 620 | expect( 621 | FormDataModule.FormData.prototype.append.withArgs( 622 | 'map', 623 | JSON.stringify({ 1: ['variables.aNested.file'] }), 624 | ), 625 | ).calledOnce 626 | } 627 | }) 628 | 629 | context('using a Buffer', function () { 630 | def('variables', () => ({ 631 | aNested: { 632 | file: Anvil.prepareGraphQLFile(Buffer.from(''), { filename: 'test.pdf' }), 633 | }, 634 | })) 635 | 636 | it('creates a FormData and appends the files map when prepareGraphQLFile was used', async function () { 637 | await client.requestGraphQL({ query, variables: $.variables }, clientOptions) 638 | }) 639 | 640 | context('file is a Buffer', function () { 641 | def('willFail', () => true) 642 | def('variables', () => ({ 643 | aNested: { 644 | file: Buffer.from(''), 645 | }, 646 | })) 647 | 648 | it('throws an error creating a FormData and appending to the files map when a raw Buffer is used', async function () { 649 | await expect(client.requestGraphQL({ query, variables: $.variables }, clientOptions)) 650 | .to.eventually.be.rejectedWith('When passing a Buffer to prepareGraphQLFile, `options.filename` must be provided') 651 | }) 652 | }) 653 | }) 654 | 655 | context('file is a Stream', function () { 656 | def('variables', () => { 657 | const file = fs.createReadStream(path.join(assetsDir, 'dummy.pdf')) 658 | return { 659 | aNested: { 660 | file, 661 | }, 662 | } 663 | }) 664 | 665 | it('creates a FormData and appends the files map', async function () { 666 | await client.requestGraphQL({ query, variables: $.variables }, clientOptions) 667 | }) 668 | }) 669 | 670 | context('file is a base64 upload', function () { 671 | def('isBase64', () => true) 672 | def('variables', () => { 673 | return { 674 | aNested: { 675 | file: { 676 | data: Buffer.from('Base64 Data').toString('base64'), 677 | filename: 'omgwow.pdf', 678 | mimetype: 'application/pdf', 679 | }, 680 | }, 681 | } 682 | }) 683 | 684 | it('does not touch the variables at all', async function () { 685 | await client.requestGraphQL({ query, variables: $.variables }, clientOptions) 686 | }) 687 | }) 688 | }) 689 | 690 | describe('schema is not good', function () { 691 | context('file is not a stream or buffer', function () { 692 | it('throws error about the schema', async function () { 693 | const query = { foo: 'bar' } 694 | const variables = { 695 | file: 'i am not a file', 696 | } 697 | 698 | await expect(client.requestGraphQL({ query, variables })).to.eventually.be.rejectedWith('Invalid File schema detected') 699 | }) 700 | }) 701 | }) 702 | }) 703 | }) 704 | 705 | describe('createEtchPacket', function () { 706 | beforeEach(function () { 707 | sinon.stub(client, 'requestGraphQL') 708 | }) 709 | 710 | context('mutation is specified', function () { 711 | it('calls requestGraphQL with overridden mutation', async function () { 712 | const variables = { foo: 'bar' } 713 | const mutationOverride = 'createEtchPacketOverride()' 714 | 715 | await client.createEtchPacket({ variables, mutation: mutationOverride }) 716 | 717 | expect(client.requestGraphQL).to.have.been.calledOnce 718 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 719 | 720 | const { 721 | query, 722 | variables: variablesReceived, 723 | } = options 724 | 725 | expect(variables).to.eql(variablesReceived) 726 | expect(query).to.include(mutationOverride) 727 | expect(clientOptions).to.eql({ dataType: 'json' }) 728 | }) 729 | }) 730 | 731 | context('no responseQuery specified', function () { 732 | it('calls requestGraphQL with default responseQuery', async function () { 733 | const variables = { foo: 'bar' } 734 | 735 | await client.createEtchPacket({ variables }) 736 | 737 | expect(client.requestGraphQL).to.have.been.calledOnce 738 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 739 | 740 | const { 741 | query, 742 | variables: variablesReceived, 743 | } = options 744 | 745 | expect(variables).to.eql(variablesReceived) 746 | expect(query).to.include('documentGroup {') // "documentGroup" is in the default responseQuery 747 | expect(clientOptions).to.eql({ dataType: 'json' }) 748 | }) 749 | }) 750 | 751 | context('responseQuery specified', function () { 752 | it('calls requestGraphQL with overridden responseQuery', async function () { 753 | const variables = { foo: 'bar' } 754 | const responseQuery = 'onlyInATest {}' 755 | 756 | await client.createEtchPacket({ variables, responseQuery }) 757 | 758 | expect(client.requestGraphQL).to.have.been.calledOnce 759 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 760 | 761 | const { 762 | query, 763 | variables: variablesReceived, 764 | } = options 765 | 766 | expect(variables).to.eql(variablesReceived) 767 | expect(query).to.include(responseQuery) 768 | expect(clientOptions).to.eql({ dataType: 'json' }) 769 | }) 770 | }) 771 | }) 772 | 773 | describe('generateEtchSignUrl', function () { 774 | def('statusCode', 200) 775 | beforeEach(async function () { 776 | sinon.stub(client, '_request') 777 | client._request.callsFake((url, options) => { 778 | return Promise.resolve($.nodeFetchResponse) 779 | }) 780 | }) 781 | 782 | context('everything goes well', function () { 783 | def('data', { 784 | data: { 785 | generateEtchSignURL: 'http://www.testing.com', 786 | }, 787 | }) 788 | def('nodeFetchResponse', () => mockNodeFetchResponse({ 789 | status: $.statusCode, 790 | json: $.data, 791 | })) 792 | 793 | it('returns url successfully', async function () { 794 | const variables = { clientUserId: 'foo', signerEid: 'bar' } 795 | const { statusCode, url, errors } = await client.generateEtchSignUrl({ variables }) 796 | expect(statusCode).to.eql(200) 797 | expect(url).to.be.eql($.data.data.generateEtchSignURL) 798 | expect(errors).to.be.undefined 799 | }) 800 | }) 801 | 802 | context('generate URL failures', function () { 803 | def('data', { 804 | data: {}, 805 | }) 806 | def('nodeFetchResponse', () => mockNodeFetchResponse({ 807 | status: $.statusCode, 808 | json: $.data, 809 | })) 810 | 811 | it('returns undefined url', async function () { 812 | const variables = { clientUserId: 'foo', signerEid: 'bar' } 813 | const { statusCode, url, errors } = await client.generateEtchSignUrl({ variables }) 814 | expect(statusCode).to.eql(200) 815 | expect(url).to.be.undefined 816 | expect(errors).to.be.undefined 817 | }) 818 | }) 819 | }) 820 | 821 | describe('getEtchPacket', function () { 822 | def('variables', { eid: 'etchPacketEid123' }) 823 | beforeEach(function () { 824 | sinon.stub(client, 'requestGraphQL') 825 | }) 826 | 827 | context('no responseQuery specified', function () { 828 | it('calls requestGraphQL with default responseQuery', async function () { 829 | await client.getEtchPacket({ variables: $.variables }) 830 | 831 | expect(client.requestGraphQL).to.have.been.calledOnce 832 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 833 | 834 | const { 835 | query, 836 | variables: variablesReceived, 837 | } = options 838 | 839 | expect($.variables).to.eql(variablesReceived) 840 | expect(query).to.include('documentGroup {') 841 | expect(clientOptions).to.eql({ dataType: 'json' }) 842 | }) 843 | }) 844 | 845 | context('responseQuery specified', function () { 846 | it('calls requestGraphQL with overridden responseQuery', async function () { 847 | const responseQuery = 'myCustomResponseQuery' 848 | await client.getEtchPacket({ variables: $.variables, responseQuery }) 849 | 850 | expect(client.requestGraphQL).to.have.been.calledOnce 851 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 852 | 853 | const { 854 | query, 855 | variables: variablesReceived, 856 | } = options 857 | 858 | expect($.variables).to.eql(variablesReceived) 859 | expect(query).to.include(responseQuery) 860 | expect(clientOptions).to.eql({ dataType: 'json' }) 861 | }) 862 | }) 863 | }) 864 | 865 | describe('forgeSubmit', function () { 866 | beforeEach(function () { 867 | sinon.stub(client, 'requestGraphQL') 868 | }) 869 | 870 | it('calls requestGraphQL with overridden mutation', async function () { 871 | const variables = { foo: 'bar' } 872 | const mutationOverride = 'forgeSubmitOverride()' 873 | 874 | await client.forgeSubmit({ variables, mutation: mutationOverride }) 875 | 876 | expect(client.requestGraphQL).to.have.been.calledOnce 877 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 878 | 879 | const { 880 | query, 881 | variables: variablesReceived, 882 | } = options 883 | 884 | expect(variables).to.eql(variablesReceived) 885 | expect(query).to.include(mutationOverride) 886 | expect(clientOptions).to.eql({ dataType: 'json' }) 887 | }) 888 | 889 | it('calls requestGraphQL with default responseQuery', async function () { 890 | const variables = { foo: 'bar' } 891 | await client.forgeSubmit({ variables }) 892 | 893 | expect(client.requestGraphQL).to.have.been.calledOnce 894 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 895 | 896 | const { 897 | query, 898 | variables: variablesReceived, 899 | } = options 900 | 901 | expect(variables).to.eql(variablesReceived) 902 | expect(query).to.include('signer {') 903 | expect(clientOptions).to.eql({ dataType: 'json' }) 904 | }) 905 | 906 | it('calls requestGraphQL with overridden responseQuery', async function () { 907 | const variables = { foo: 'bar' } 908 | const customResponseQuery = 'myCustomResponseQuery' 909 | await client.forgeSubmit({ variables, responseQuery: customResponseQuery }) 910 | 911 | expect(client.requestGraphQL).to.have.been.calledOnce 912 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 913 | 914 | const { 915 | query, 916 | variables: variablesReceived, 917 | } = options 918 | 919 | expect(variables).to.eql(variablesReceived) 920 | expect(query).to.include(customResponseQuery) 921 | expect(clientOptions).to.eql({ dataType: 'json' }) 922 | }) 923 | }) 924 | 925 | describe('removeWeldData', function () { 926 | beforeEach(function () { 927 | sinon.stub(client, 'requestGraphQL') 928 | }) 929 | 930 | it('calls requestGraphQL with overridden mutation', async function () { 931 | const variables = { foo: 'bar' } 932 | const mutationOverride = 'removeWeldDataOverride()' 933 | 934 | await client.removeWeldData({ variables, mutation: mutationOverride }) 935 | 936 | expect(client.requestGraphQL).to.have.been.calledOnce 937 | const [options, clientOptions] = client.requestGraphQL.lastCall.args 938 | 939 | const { 940 | query, 941 | variables: variablesReceived, 942 | } = options 943 | 944 | expect(variables).to.eql(variablesReceived) 945 | expect(query).to.include(mutationOverride) 946 | expect(clientOptions).to.eql({ dataType: 'json' }) 947 | }) 948 | }) 949 | }) 950 | 951 | describe('prepareGraphQLFile', function () { 952 | it('works', function () { 953 | expect(() => 954 | Anvil.prepareGraphQLFile(Buffer.from('test')), 955 | ).to.throw('When passing a Buffer to prepareGraphQLFile, `options.filename` must be provided. If you think you can ignore this, please pass `options.ignoreFilenameValidation` as `true`.') 956 | 957 | let uploadWithOptions = Anvil.prepareGraphQLFile(Buffer.from('test'), { ignoreFilenameValidation: true }) 958 | expect(uploadWithOptions).to.be.ok 959 | expect(uploadWithOptions.streamLikeThing).to.be.ok 960 | expect(uploadWithOptions.formDataAppendOptions).to.eql({}) 961 | 962 | uploadWithOptions = Anvil.prepareGraphQLFile(Buffer.from('test'), { filename: 'test.pdf' }) 963 | expect(uploadWithOptions).to.be.ok 964 | expect(uploadWithOptions.streamLikeThing).to.be.ok 965 | expect(uploadWithOptions.formDataAppendOptions).to.include({ 966 | filename: 'test.pdf', 967 | }) 968 | }) 969 | }) 970 | }) 971 | -------------------------------------------------------------------------------- /test/mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | diff: true, 5 | delay: false, 6 | extension: ['js'], 7 | package: './package.json', 8 | reporter: 'spec', 9 | slow: 75, 10 | timeout: 2000, 11 | spec: './test/**/*.test.js', 12 | // Silly, Mocha. Don't run all the tests you can find... 13 | ignore: './test/**/node_modules/**/*', 14 | require: [ 15 | // https://mochajs.org/#-require-module-r-module 16 | '@babel/register', 17 | './test/environment.js', 18 | ], 19 | file: './test/setup.js', 20 | ui: 'bdd-lazy-var/getter', 21 | exit: true, 22 | } 23 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | const { get } = require('bdd-lazy-var/getter') 2 | 3 | // In order to get around eslint complaining for now: 4 | // https://github.com/stalniy/bdd-lazy-var/issues/56#issuecomment-639248242 5 | global.$ = get 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Change this to match your project 3 | "include": [ 4 | "src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "module": "node16", 8 | "moduleResolution": "node16", 9 | "noImplicitThis": true, 10 | "alwaysStrict": true, 11 | "checkJs": true, 12 | "resolveJsonModule": true, 13 | // Tells TypeScript to read JS files, as 14 | // normally they are ignored as source files 15 | "allowJs": true, 16 | // Generate d.ts files 17 | "declaration": true, 18 | // This compiler run should 19 | // only output d.ts files 20 | "emitDeclarationOnly": true, 21 | // Types should go into this directory. 22 | // Removing this would place the .d.ts files 23 | // next to the .js files 24 | "outDir": "types", 25 | // go to js file when using IDE functions like 26 | // "Go to Definition" in VSCode 27 | "declarationMap": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /types/src/UploadWithOptions.d.ts: -------------------------------------------------------------------------------- 1 | export default class UploadWithOptions { 2 | constructor(streamLikeThing: any, formDataAppendOptions: any); 3 | streamLikeThing: any; 4 | formDataAppendOptions: any; 5 | get options(): any; 6 | get file(): any; 7 | } 8 | //# sourceMappingURL=UploadWithOptions.d.ts.map -------------------------------------------------------------------------------- /types/src/UploadWithOptions.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"UploadWithOptions.d.ts","sourceRoot":"","sources":["../../src/UploadWithOptions.js"],"names":[],"mappings":"AAAA;IACE,8DAGC;IAFC,qBAAsC;IACtC,2BAAkD;IAGpD,mBAEC;IAED,gBAEC;CACF"} -------------------------------------------------------------------------------- /types/src/errors.d.ts: -------------------------------------------------------------------------------- 1 | export function looksLikeJsonError({ json }: { 2 | json: any; 3 | }): boolean; 4 | export function normalizeJsonErrors({ json, statusText }: { 5 | json: any; 6 | statusText?: string; 7 | }): any; 8 | export function normalizeNodeError({ error, statusText }: { 9 | error: any; 10 | statusText?: string; 11 | }): { 12 | name: any; 13 | message: any; 14 | code: any; 15 | cause: any; 16 | stack: any; 17 | }[] | { 18 | name: string; 19 | message: string; 20 | }[]; 21 | //# sourceMappingURL=errors.d.ts.map -------------------------------------------------------------------------------- /types/src/errors.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.js"],"names":[],"mappings":"AACA;;YAEC;AAGD;;;QA0BC;AAGD;;;;;;;;;;;;IAOC"} -------------------------------------------------------------------------------- /types/src/graphql/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as queries from "./queries"; 2 | export * as mutations from "./mutations"; 3 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/graphql/index.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/src/graphql/mutations/createEtchPacket.d.ts: -------------------------------------------------------------------------------- 1 | export function generateMutation(responseQuery?: string): string; 2 | //# sourceMappingURL=createEtchPacket.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/mutations/createEtchPacket.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createEtchPacket.d.ts","sourceRoot":"","sources":["../../../../src/graphql/mutations/createEtchPacket.js"],"names":[],"mappings":"AA4BO,iEAyCH"} -------------------------------------------------------------------------------- /types/src/graphql/mutations/forgeSubmit.d.ts: -------------------------------------------------------------------------------- 1 | export function generateMutation(responseQuery?: string): string; 2 | //# sourceMappingURL=forgeSubmit.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/mutations/forgeSubmit.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"forgeSubmit.d.ts","sourceRoot":"","sources":["../../../../src/graphql/mutations/forgeSubmit.js"],"names":[],"mappings":"AAyBO,iEA6BH"} -------------------------------------------------------------------------------- /types/src/graphql/mutations/generateEtchSignUrl.d.ts: -------------------------------------------------------------------------------- 1 | export function generateMutation(): string; 2 | //# sourceMappingURL=generateEtchSignUrl.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/mutations/generateEtchSignUrl.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"generateEtchSignUrl.d.ts","sourceRoot":"","sources":["../../../../src/graphql/mutations/generateEtchSignUrl.js"],"names":[],"mappings":"AAAO,2CAUN"} -------------------------------------------------------------------------------- /types/src/graphql/mutations/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as createEtchPacket from "./createEtchPacket"; 2 | export * as forgeSubmit from "./forgeSubmit"; 3 | export * as generateEtchSignUrl from "./generateEtchSignUrl"; 4 | export * as removeWeldData from "./removeWeldData"; 5 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/mutations/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/graphql/mutations/index.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/src/graphql/mutations/removeWeldData.d.ts: -------------------------------------------------------------------------------- 1 | export function generateMutation(): string; 2 | //# sourceMappingURL=removeWeldData.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/mutations/removeWeldData.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"removeWeldData.d.ts","sourceRoot":"","sources":["../../../../src/graphql/mutations/removeWeldData.js"],"names":[],"mappings":"AAAO,2CAOH"} -------------------------------------------------------------------------------- /types/src/graphql/queries/etchPacket.d.ts: -------------------------------------------------------------------------------- 1 | export function generateQuery(responseQuery?: string): string; 2 | //# sourceMappingURL=etchPacket.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/queries/etchPacket.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"etchPacket.d.ts","sourceRoot":"","sources":["../../../../src/graphql/queries/etchPacket.js"],"names":[],"mappings":"AA+BO,8DAQN"} -------------------------------------------------------------------------------- /types/src/graphql/queries/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as etchPacket from "./etchPacket"; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /types/src/graphql/queries/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/graphql/queries/index.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export default Anvil; 2 | export type AnvilOptions = { 3 | apiKey?: string; 4 | accessToken?: string; 5 | baseURL?: string; 6 | userAgent?: string; 7 | requestLimit?: number; 8 | requestLimitMS?: number; 9 | }; 10 | export type GraphQLResponse = { 11 | statusCode: number; 12 | data?: GraphQLResponseData; 13 | errors?: Array; 14 | }; 15 | export type GraphQLResponseData = { 16 | data: { 17 | [key: string]: any; 18 | }; 19 | }; 20 | export type RESTResponse = { 21 | statusCode: number; 22 | data?: Buffer | Stream | any; 23 | errors?: Array; 24 | /** 25 | * node-fetch Response 26 | */ 27 | response?: any; 28 | }; 29 | export type NodeError = { 30 | name: string; 31 | message: string; 32 | stack: string; 33 | code: string; 34 | cause?: any; 35 | }; 36 | export type ResponseError = { 37 | [key: string]: any; 38 | message: string; 39 | status?: number; 40 | name?: string; 41 | fields?: Array; 42 | }; 43 | export type ResponseErrorField = { 44 | [key: string]: any; 45 | message: string; 46 | property?: string; 47 | }; 48 | export type Readable = { 49 | path: string; 50 | }; 51 | declare class Anvil { 52 | /** 53 | * Perform some handy/necessary things for a GraphQL file upload to make it work 54 | * with this client and with our backend 55 | * 56 | * @param {string|Buffer|Readable|File|Blob} pathOrStreamLikeThing - Either a string path to a file, 57 | * a Buffer, or a Stream-like thing that is compatible with form-data as an append. 58 | * @param {Object} [formDataAppendOptions] - User can specify options to be passed to the form-data.append 59 | * call. This should be done if a stream-like thing is not one of the common types that 60 | * form-data can figure out on its own. 61 | * 62 | * @return {UploadWithOptions} - A class that wraps the stream-like-thing and any options 63 | * up together nicely in a way that we can also tell that it was us who did it. 64 | */ 65 | static prepareGraphQLFile(pathOrStreamLikeThing: string | Buffer | Readable | File | Blob, { ignoreFilenameValidation, ...formDataAppendOptions }?: any): UploadWithOptions; 66 | /** 67 | * @param {AnvilOptions?} options 68 | */ 69 | constructor(options: AnvilOptions | null); 70 | options: { 71 | apiKey?: string; 72 | accessToken?: string; 73 | baseURL: string; 74 | userAgent: string; 75 | requestLimit: number; 76 | requestLimitMS: number; 77 | }; 78 | authHeader: string; 79 | hasSetLimiterFromResponse: boolean; 80 | limiterSettingInProgress: boolean; 81 | rateLimiterSetupPromise: Promise; 82 | rateLimiterPromiseResolver: (value: any) => void; 83 | /** 84 | * @param {Object} options 85 | * @param {number} options.tokens 86 | * @param {number} options.intervalMs 87 | * @private 88 | */ 89 | private _setRateLimiter; 90 | limitTokens: any; 91 | limitIntervalMs: number; 92 | limiter: RateLimiter; 93 | /** 94 | * Runs the createEtchPacket mutation. 95 | * @param {Object} data 96 | * @param {Object} data.variables 97 | * @param {string} [data.responseQuery] 98 | * @param {string} [data.mutation] 99 | * @returns {Promise} 100 | */ 101 | createEtchPacket({ variables, responseQuery, mutation }: { 102 | variables: any; 103 | responseQuery?: string; 104 | mutation?: string; 105 | }): Promise; 106 | /** 107 | * @param {string} documentGroupEid 108 | * @param {Object} [clientOptions] 109 | * @returns {Promise} 110 | */ 111 | downloadDocuments(documentGroupEid: string, clientOptions?: any): Promise; 112 | /** 113 | * @param {string} pdfTemplateID 114 | * @param {Object} payload 115 | * @param {Object} [clientOptions] 116 | * @returns {Promise} 117 | */ 118 | fillPDF(pdfTemplateID: string, payload: any, clientOptions?: any): Promise; 119 | /** 120 | * @param {Object} data 121 | * @param {Object} data.variables 122 | * @param {string} [data.responseQuery] 123 | * @param {string} [data.mutation] 124 | * @returns {Promise} 125 | */ 126 | forgeSubmit({ variables, responseQuery, mutation }: { 127 | variables: any; 128 | responseQuery?: string; 129 | mutation?: string; 130 | }): Promise; 131 | /** 132 | * @param {Object} payload 133 | * @param {Object} [clientOptions] 134 | * @returns {Promise} 135 | */ 136 | generatePDF(payload: any, clientOptions?: any): Promise; 137 | /** 138 | * @param {Object} data 139 | * @param {Object} data.variables 140 | * @param {string} [data.responseQuery] 141 | * @returns {Promise} 142 | */ 143 | getEtchPacket({ variables, responseQuery }: { 144 | variables: any; 145 | responseQuery?: string; 146 | }): Promise; 147 | /** 148 | * @param {Object} data 149 | * @param {Object} data.variables 150 | * @returns {Promise<{url?: string, errors?: Array, statusCode: number}>} 151 | */ 152 | generateEtchSignUrl({ variables }: { 153 | variables: any; 154 | }): Promise<{ 155 | url?: string; 156 | errors?: Array; 157 | statusCode: number; 158 | }>; 159 | /** 160 | * @param {Object} data 161 | * @param {Object} data.variables 162 | * @param {string} [data.mutation] 163 | * @returns {Promise} 164 | */ 165 | removeWeldData({ variables, mutation }: { 166 | variables: any; 167 | mutation?: string; 168 | }): Promise; 169 | /** 170 | * @param {Object} data 171 | * @param {string} data.query 172 | * @param {Object} [data.variables] 173 | * @param {Object} [clientOptions] 174 | * @returns {Promise} 175 | */ 176 | requestGraphQL({ query, variables }: { 177 | query: string; 178 | variables?: any; 179 | }, clientOptions?: any): Promise; 180 | /** 181 | * @param {string} url 182 | * @param {Object} fetchOptions 183 | * @param {Object} [clientOptions] 184 | * @returns {Promise} 185 | */ 186 | requestREST(url: string, fetchOptions: any, clientOptions?: any): Promise; 187 | _request(...args: any[]): any; 188 | /** 189 | * @param {string} url 190 | * @param {Object} options 191 | * @returns {Promise} 192 | * @private 193 | */ 194 | private __request; 195 | /** 196 | * @param {CallableFunction} retryableRequestFn 197 | * @param {Object} [clientOptions] 198 | * @returns {Promise<*>} 199 | * @private 200 | */ 201 | private _wrapRequest; 202 | /** 203 | * @param {string} path 204 | * @returns {string} 205 | * @private 206 | */ 207 | private _url; 208 | /** 209 | * @param {Object} headerObject 210 | * @param {Object} headerObject.options 211 | * @param {Object} headerObject.headers 212 | * @param {Object} [internalOptions] 213 | * @returns {*&{headers: {}}} 214 | * @private 215 | */ 216 | private _addHeaders; 217 | /** 218 | * @param {Object} options 219 | * @returns {*} 220 | * @private 221 | */ 222 | private _addDefaultHeaders; 223 | /** 224 | * @param {CallableFunction} fn 225 | * @returns {Promise<*>} 226 | * @private 227 | */ 228 | private _throttle; 229 | } 230 | declare namespace Anvil { 231 | export { UploadWithOptions }; 232 | export { VERSION_LATEST }; 233 | export { VERSION_LATEST_PUBLISHED }; 234 | } 235 | import { Stream } from 'stream'; 236 | import { RateLimiter } from 'limiter'; 237 | import UploadWithOptions from './UploadWithOptions'; 238 | declare const VERSION_LATEST: -1; 239 | declare const VERSION_LATEST_PUBLISHED: -2; 240 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /types/src/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":";;aA0Bc,MAAM;kBACN,MAAM;cACN,MAAM;gBACN,MAAM;mBACN,MAAM;qBACN,MAAM;;;gBAMN,MAAM;WACN,mBAAmB;aACnB,MAAM,aAAa,GAAG,SAAS,CAAC;;;;;;;;gBAUhC,MAAM;WACN,MAAM,GAAC,MAAM,MAAO;aACpB,MAAM,aAAa,GAAG,SAAS,CAAC;;;;eAChC,GAAG;;wBAGH;IACZ,MAAM,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;;;aAGU,MAAM;aACN,MAAM;WACR,MAAM;aACJ,MAAM,kBAAkB,CAAC;;;;aAKzB,MAAM;eACJ,MAAM;;uBAIL;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAyDD;IA+EE;;;;;;;;;;;;OAYG;IACH,iDATY,MAAM,GAAC,MAAM,GAAC,QAAQ,GAAC,IAAI,GAAC,IAAI,iEAMhC,iBAAiB,CA6C5B;IA/HD;;OAEG;IACH,qBAFW,YAAY,SA+BtB;IA1BC;;;;;;;MAKC;IAKD,mBAEoE;IAGpE,mCAAsC;IAEtC,kCAAqC;IAGrC,sCAEE;IADA,iDAAyC;IAO7C;;;;;OAKG;IACH,wBA8BC;IAHC,iBAAyB;IACzB,wBAAiC;IACjC,qBAAyB;IA4D3B;;;;;;;OAOG;IACH;QALwB,SAAS;QACR,aAAa,GAA3B,MAAM;QACQ,QAAQ,GAAtB,MAAM;QACJ,QAAQ,eAAe,CAAC,CAUpC;IAED;;;;OAIG;IACH,oCAJW,MAAM,wBAEJ,QAAQ,YAAY,CAAC,CAejC;IAED;;;;;OAKG;IACH,uBALW,MAAM,sCAGJ,QAAQ,YAAY,CAAC,CA2BjC;IAED;;;;;;OAMG;IACH;QALwB,SAAS;QACR,aAAa,GAA3B,MAAM;QACQ,QAAQ,GAAtB,MAAM;QACJ,QAAQ,eAAe,CAAC,CAUpC;IAED;;;;OAIG;IACH,gDAFa,QAAQ,YAAY,CAAC,CAsBjC;IAED;;;;;OAKG;IACH;QAJwB,SAAS;QACR,aAAa,GAA3B,MAAM;QACJ,QAAQ,eAAe,CAAC,CAUpC;IAED;;;;OAIG;IACH;QAHwB,SAAS;;cACL,MAAM;iBAAW,MAAM,aAAa,GAAG,SAAS,CAAC;oBAAc,MAAM;OAgBhG;IAED;;;;;OAKG;IACH;QAJwB,SAAS;QACR,QAAQ,GAAtB,MAAM;QACJ,QAAQ,eAAe,CAAC,CAUpC;IAED;;;;;;OAMG;IACH;QALwB,KAAK,EAAlB,MAAM;QACQ,SAAS;6BAErB,QAAQ,eAAe,CAAC,CAuIpC;IAED;;;;;OAKG;IACH,iBALW,MAAM,2CAGJ,QAAQ,YAAY,CAAC,CAmBjC;IAYD,8BAOC;IAED;;;;;OAKG;IACH,kBAMC;IAED;;;;;OAKG;IACH,qBAwFC;IAED;;;;OAIG;IACH,aAEC;IAED;;;;;;;OAOG;IACH,oBAqBC;IAED;;;;OAIG;IACH,2BAYC;IAED;;;;OAIG;IACH,kBA0BC;CACF;;;;;;uBAzzBsB,QAAQ;4BAGH,SAAS;8BAEP,qBAAqB;AAmHnD,iCAAyB;AAGzB,2CAAmC"} -------------------------------------------------------------------------------- /types/src/validation.d.ts: -------------------------------------------------------------------------------- 1 | export function isFile(value: any): boolean; 2 | export function graphQLUploadSchemaIsValid(schema: any, parent: any, key: any): any; 3 | //# sourceMappingURL=validation.d.ts.map -------------------------------------------------------------------------------- /types/src/validation.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/validation.js"],"names":[],"mappings":"AAIA,4CAEC;AAED,oFAmCC"} --------------------------------------------------------------------------------