├── .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 | 
2 | 
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 |
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 |
175 |
176 | Lorem Ipsum is simply dummy text...
177 |
178 |
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 |
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 |
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"}
--------------------------------------------------------------------------------