@anvilco/anvil
Advanced tools
Comparing version 2.1.0 to 2.2.0
@@ -10,6 +10,25 @@ # Changelog | ||
## [v2.1.0](https://github.com/anvilco/node-anvil/compare/v1.0.3...v2.1.0) - 2020-08-20 | ||
## [v2.2.0](https://github.com/anvilco/node-anvil/compare/v2.1.0...v2.2.0) - 2020-09-28 | ||
### Merged | ||
- Add `aliasId` AND `routingOrder` to `createEtchPacket` mutation query response default [`#24`](https://github.com/anvilco/node-anvil/pull/24) | ||
- rename `send` to `isDraft` (and flip the logic) in `createEtchPacket` [`#23`](https://github.com/anvilco/node-anvil/pull/23) | ||
- no `organizationEid` allowed when calling via API key [`#22`](https://github.com/anvilco/node-anvil/pull/22) | ||
- change `fillPayload` param to just `data` [`#21`](https://github.com/anvilco/node-anvil/pull/21) | ||
- Fix file Upload related stuff to reflect server-side [`#20`](https://github.com/anvilco/node-anvil/pull/20) | ||
- Etch API Support for Embedded Signers [`#18`](https://github.com/anvilco/node-anvil/pull/18) | ||
- Bump node-fetch from 2.6.0 to 2.6.1 [`#19`](https://github.com/anvilco/node-anvil/pull/19) | ||
- Add support for GraphQL API, starting with `createEtchPacket` mutation [`#16`](https://github.com/anvilco/node-anvil/pull/16) | ||
### Commits | ||
- dependency upgrades: nodemon, mocha, and yargs due to dot-prop and yargs-parser vulnerabilities [`c118d23`](https://github.com/anvilco/node-anvil/commit/c118d234c4717a429b8ebda8c1f69e1002c11f56) | ||
- added graphql folders. wrapping all request/responses in throttle and body parsing helper. tidied up a lot [`036c615`](https://github.com/anvilco/node-anvil/commit/036c615290fbd4c4d830dde6e8ea28275647ab64) | ||
- getting the file uploads stuff more in line with the actual way the API expects things now [`2c7bab6`](https://github.com/anvilco/node-anvil/commit/2c7bab6d44c9893884c3e73a60792f0e84a936a7) | ||
## [v2.1.0](https://github.com/anvilco/node-anvil/compare/v1.0.3...v2.1.0) - 2020-08-19 | ||
### Merged | ||
- replace `request` with `node-fetch` [`#13`](https://github.com/anvilco/node-anvil/pull/13) | ||
@@ -16,0 +35,0 @@ - Bump lodash from 4.17.15 to 4.17.19 [`#12`](https://github.com/anvilco/node-anvil/pull/12) |
{ | ||
"name": "@anvilco/anvil", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"description": "Anvil API Client", | ||
"main": "src/index.js", | ||
"scripts": { | ||
"test": "mocha --require test/environment.js 'test/**/*.test.js'", | ||
"test": "mocha --config ./test/mocha.js", | ||
"test:watch": "nodemon --signal SIGINT --watch test --watch src -x 'yarn test'", | ||
@@ -33,2 +33,3 @@ "version": "auto-changelog -p --template keepachangelog && git add CHANGELOG.md" | ||
"babel-eslint": "^10.0.3", | ||
"bdd-lazy-var": "^2.5.4", | ||
"chai": "^4.2.0", | ||
@@ -46,9 +47,12 @@ "chai-as-promised": "^7.1.1", | ||
"eslint-plugin-standard": "^4.0.1", | ||
"mocha": "^7.1.1", | ||
"nodemon": "^2.0.2", | ||
"mocha": "^8.1.3", | ||
"nodemon": "^2.0.4", | ||
"sinon": "^9.0.1", | ||
"sinon-chai": "^3.5.0", | ||
"yargs": "^15.1.0" | ||
"yargs": "^16.0.3" | ||
}, | ||
"dependencies": { | ||
"abort-controller": "^3.0.0", | ||
"extract-files": "^6", | ||
"form-data": "^3.0.0", | ||
"limiter": "^1.1.5", | ||
@@ -55,0 +59,0 @@ "node-fetch": "^2.6.0" |
@@ -51,4 +51,6 @@ # Anvil API Client for Node | ||
### new Anvil(options) | ||
### Instance Methods | ||
##### new Anvil(options) | ||
Creates an Anvil client instance. | ||
@@ -61,15 +63,6 @@ | ||
``` | ||
<br /> | ||
### Options | ||
##### fillPDF(pdfTemplateID, payload[, options]) | ||
Options for the Anvil Client. Defaults are shown after each option key. | ||
```js | ||
{ | ||
apiKey: <your_api_key> // Required. Your API key from your Anvil organization settings | ||
} | ||
``` | ||
### Anvil::fillPDF(pdfTemplateID, payload[, options]) | ||
Fills a PDF with your JSON data. | ||
@@ -120,2 +113,54 @@ | ||
##### createEtchPacket(options) | ||
Creates an Etch Packet and optionally sends it to the first signer. | ||
* `options` (Object) - An object with the following structure: | ||
* `variables` (Object) - See the [API Documentation](#api-documentation) area for details. See [Examples](#examples) area for examples. | ||
* `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. | ||
* `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: | ||
```graphql | ||
mutation CreateEtchPacket ( | ||
$name: String, | ||
... | ||
) { | ||
createEtchPacket ( | ||
name: $name, | ||
... | ||
) { | ||
id | ||
eid | ||
... | ||
} | ||
} | ||
``` | ||
##### generateEtchSignUrl(options) | ||
Generates an Etch sign URL for an Etch Packet signer. The Etch Packet and its signers must have already been created. | ||
* `options` (Object) - An object with the following structure: | ||
* `variables` (Object) - Requires `clientUserId` and `signerEid` | ||
* `clientUserId` (String) - your user eid | ||
* `signerEid` (String) - the eid of the Etch Packet signer, found in the response of the `createEtchPacket` instance method | ||
### Class Methods | ||
##### prepareGraphQLFile(pathOrStreamLikeThing[, options]) | ||
A nice helper to prepare a Stream-backed or Buffer-backed file upload for use with our GraphQL API. | ||
* `pathOrStreamLikeThing` (String | Stream | Buffer) - An existing `Stream`, `Buffer` or other Stream-like thing supported by [FormData.append](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-) OR a string representing a fully resolved path to a file to be read into a new `Stream`. | ||
* `options` (Object) - Anything supported by [FormData.append](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-). Likely required when providing a non-common stream. From the `form-data` docs: | ||
> Form-Data can recognize and fetch all the required information from common types of streams (fs.readStream, http.response and mikeal's request), for some other types of streams you'd need to provide "file"-related information manually | ||
* 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. | ||
### Types | ||
##### Options | ||
Options for the Anvil Client. Defaults are shown after each option key. | ||
```js | ||
{ | ||
apiKey: <your_api_key> // Required. Your API key from your Anvil organization settings | ||
} | ||
``` | ||
### Rate Limits | ||
@@ -127,6 +172,8 @@ | ||
### More Info | ||
## API Documentation | ||
See the [PDF filling API docs](https://useanvil.com/api/fill-pdf) for more information. | ||
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. | ||
See the [PDF filling API docs](https://useanvil.com/api/fill-pdf) for more information about the `fillPDF` method. | ||
## Examples | ||
@@ -133,0 +180,0 @@ |
313
src/index.js
@@ -0,6 +1,28 @@ | ||
const fs = require('fs') | ||
const fetch = require('node-fetch') | ||
const RateLimiter = require('limiter').RateLimiter | ||
const FormData = require('form-data') | ||
const AbortController = require('abort-controller') | ||
const { extractFiles } = require('extract-files') | ||
const { RateLimiter } = require('limiter') | ||
const UploadWithOptions = require('./UploadWithOptions') | ||
const { version, description } = require('../package.json') | ||
const { | ||
mutations: { | ||
createEtchPacket: { | ||
getMutation: getCreateEtchPacketMutation, | ||
}, | ||
generateEtchSignUrl: { | ||
getMutation: getGenerateEtchSignUrlMutation, | ||
}, | ||
}, | ||
} = require('./graphql') | ||
const { | ||
isFile, | ||
graphQLUploadSchemaIsValid, | ||
} = require('./validation') | ||
const DATA_TYPE_STREAM = 'stream' | ||
@@ -26,7 +48,11 @@ const DATA_TYPE_BUFFER = 'buffer' | ||
if (!options) throw new Error('options are required') | ||
if (!options.apiKey && !options.accessToken) throw new Error('apiKey or accessToken required') | ||
this.options = Object.assign({}, defaultOptions, options) | ||
this.options = { | ||
...defaultOptions, | ||
...options, | ||
} | ||
const { apiKey, accessToken } = this.options | ||
if (!(apiKey || accessToken)) throw new Error('apiKey or accessToken required') | ||
this.authHeader = accessToken | ||
@@ -42,2 +68,23 @@ ? `Bearer ${Buffer.from(accessToken, 'ascii').toString('base64')}` | ||
/** | ||
* Perform some handy/necessary things for a GraphQL file upload to make it work | ||
* with this client and with our backend | ||
* | ||
* @param {string|Buffer|Stream-like-thing} pathOrStreamLikeThing - Either a string path to a file, | ||
* a Buffer, or a Stream-like thing that is compatible with form-data as an append. | ||
* @param {object} formDataAppendOptions - User can specify options to be passed to the form-data.append | ||
* call. This should be done if a stream-like thing is not one of the common types that | ||
* form-data can figure out on its own. | ||
* | ||
* @return {UploadWithOptions} - A class that wraps the stream-like-thing and any options | ||
* up together nicely in a way that we can also tell that it was us who did it. | ||
*/ | ||
static prepareGraphQLFile (pathOrStreamLikeThing, formDataAppendOptions) { | ||
if (typeof pathOrStreamLikeThing === 'string') { | ||
pathOrStreamLikeThing = fs.createReadStream(pathOrStreamLikeThing) | ||
} | ||
return new UploadWithOptions(pathOrStreamLikeThing, formDataAppendOptions) | ||
} | ||
fillPDF (pdfTemplateID, payload, clientOptions = {}) { | ||
@@ -57,3 +104,2 @@ const supportedDataTypes = [DATA_TYPE_STREAM, DATA_TYPE_BUFFER] | ||
'Content-Type': 'application/json', | ||
Authorization: this.authHeader, | ||
}, | ||
@@ -68,14 +114,158 @@ }, | ||
// Private | ||
createEtchPacket ({ variables, responseQuery, mutation }) { | ||
return this.requestGraphQL( | ||
{ | ||
query: mutation || getCreateEtchPacketMutation(responseQuery), | ||
variables, | ||
}, | ||
{ dataType: DATA_TYPE_JSON }, | ||
) | ||
} | ||
async requestREST (url, options, clientOptions = {}) { | ||
return this.throttle(async (retry) => { | ||
const response = await this.request(url, options) | ||
async generateEtchSignUrl ({ variables }) { | ||
const { statusCode, data, errors } = await this.requestGraphQL( | ||
{ | ||
query: getGenerateEtchSignUrlMutation(), | ||
variables, | ||
}, | ||
{ dataType: DATA_TYPE_JSON }, | ||
) | ||
return { | ||
statusCode, | ||
url: data && data.data && data.data.generateEtchSignURL, | ||
errors, | ||
} | ||
} | ||
async requestGraphQL ({ query, variables = {} }, clientOptions) { | ||
// Some helpful resources on how this came to be: | ||
// https://github.com/jaydenseric/graphql-upload/issues/125#issuecomment-440853538 | ||
// https://zach.codes/building-a-file-upload-hook/ | ||
// https://github.com/jaydenseric/graphql-react/blob/1b1234de5de46b7a0029903a1446dcc061f37d09/src/universal/graphqlFetchOptions.mjs | ||
// https://www.npmjs.com/package/extract-files | ||
const options = { | ||
method: 'POST', | ||
headers: {}, | ||
} | ||
const originalOperation = { query, variables } | ||
const { | ||
clone: augmentedOperation, | ||
files: filesMap, | ||
} = extractFiles(originalOperation, '', isFile) | ||
const operationJSON = JSON.stringify(augmentedOperation) | ||
// Checks for both File uploads and Base64 uploads | ||
if (!graphQLUploadSchemaIsValid(originalOperation)) { | ||
throw new Error('Invalid File schema detected') | ||
} | ||
if (filesMap.size) { | ||
const abortController = new AbortController() | ||
const form = new FormData() | ||
form.append('operations', operationJSON) | ||
const map = {} | ||
let i = 0 | ||
filesMap.forEach(paths => { | ||
map[++i] = paths | ||
}) | ||
form.append('map', JSON.stringify(map)) | ||
i = 0 | ||
filesMap.forEach((paths, file) => { | ||
let appendOptions = {} | ||
if (file instanceof UploadWithOptions) { | ||
appendOptions = file.options | ||
file = file.file | ||
} | ||
// If this is a stream-like thing, attach a listener to the 'error' event so that we | ||
// can cancel the API call if something goes wrong | ||
if (typeof file.on === 'function') { | ||
file.on('error', (err) => { | ||
console.warn(err) | ||
abortController.abort() | ||
}) | ||
} | ||
// Pass in some things explicitly to the form.append so that we get the | ||
// desired/expected filename and mimetype, etc | ||
form.append(`${++i}`, file, appendOptions) | ||
}) | ||
options.signal = abortController.signal | ||
options.body = form | ||
} else { | ||
options.headers['Content-Type'] = 'application/json' | ||
options.body = operationJSON | ||
} | ||
const { | ||
statusCode, | ||
data, | ||
errors, | ||
} = await this._wrapRequest( | ||
() => this._request('/graphql', options), | ||
clientOptions, | ||
) | ||
return { | ||
statusCode, | ||
data, | ||
errors, | ||
} | ||
} | ||
async requestREST (url, fetchOptions, clientOptions) { | ||
const { | ||
response, | ||
statusCode, | ||
data, | ||
errors, | ||
} = await this._wrapRequest( | ||
() => this._request(url, fetchOptions), | ||
clientOptions, | ||
) | ||
return { | ||
response, | ||
statusCode, | ||
data, | ||
errors, | ||
} | ||
} | ||
// ****************************************************************************** | ||
// ___ _ __ | ||
// / _ \____(_) _____ _/ /____ | ||
// / ___/ __/ / |/ / _ `/ __/ -_) | ||
// /_/ /_/ /_/|___/\_,_/\__/\__/ | ||
// | ||
// ALL THE BELOW CODE IS CONSIDERED PRIVATE, AND THE API OR INTERNALS MAY CHANGE AT ANY TIME | ||
// USERS OF THIS MODULE SHOULD NOT USE ANY OF THESE METHODS DIRECTLY | ||
// ****************************************************************************** | ||
_request (url, options) { | ||
if (!url.startsWith(this.options.baseURL)) { | ||
url = this._url(url) | ||
} | ||
const opts = this._addDefaultHeaders(options) | ||
return fetch(url, opts) | ||
} | ||
_wrapRequest (retryableRequestFn, clientOptions = {}) { | ||
return this._throttle(async (retry) => { | ||
const response = await retryableRequestFn() | ||
const statusCode = response.status | ||
if (statusCode === 429) { | ||
return retry(getRetryMS(response.headers.get('retry-after'))) | ||
} | ||
if (statusCode >= 300) { | ||
if (statusCode === 429) { | ||
return retry(getRetryMS(response.headers.get('retry-after'))) | ||
} | ||
if (statusCode >= 300) { | ||
const json = await response.json() | ||
@@ -89,6 +279,4 @@ const errors = json.errors || (json.message && [json]) | ||
let data | ||
switch (dataType) { | ||
case DATA_TYPE_JSON: | ||
data = await response.json() | ||
break | ||
case DATA_TYPE_STREAM: | ||
@@ -100,12 +288,59 @@ data = response.body | ||
break | ||
case DATA_TYPE_JSON: | ||
data = await response.json() | ||
break | ||
default: | ||
data = await response.buffer() | ||
console.warn('Using default response dataType of "json". Please specifiy a dataType.') | ||
data = await response.json() | ||
break | ||
} | ||
return { statusCode, data } | ||
return { | ||
response, | ||
data, | ||
statusCode, | ||
} | ||
}) | ||
} | ||
throttle (fn) { | ||
_url (path) { | ||
return this.options.baseURL + path | ||
} | ||
_addHeaders ({ options: existingOptions, headers: newHeaders }, internalOptions = {}) { | ||
const { headers: existingHeaders = {} } = existingOptions | ||
const { defaults = false } = internalOptions | ||
newHeaders = defaults ? newHeaders : Object.entries(newHeaders).reduce((acc, [key, val]) => { | ||
if (val != null) { | ||
acc[key] = val | ||
} | ||
return acc | ||
}, {}) | ||
return { | ||
...existingOptions, | ||
headers: { | ||
...existingHeaders, | ||
...newHeaders, | ||
}, | ||
} | ||
} | ||
_addDefaultHeaders (options) { | ||
const { userAgent } = this.options | ||
return this._addHeaders( | ||
{ | ||
options, | ||
headers: { | ||
'User-Agent': userAgent, | ||
Authorization: this.authHeader, | ||
}, | ||
}, | ||
{ defaults: true }, | ||
) | ||
} | ||
_throttle (fn) { | ||
return new Promise((resolve, reject) => { | ||
@@ -119,3 +354,3 @@ this.limiter.removeTokens(1, async (err, remainingRequests) => { | ||
await sleep(ms) | ||
return this.throttle(fn) | ||
return this._throttle(fn) | ||
} | ||
@@ -131,34 +366,22 @@ try { | ||
request (url, options) { | ||
if (!url.startsWith(this.options.baseURL)) { | ||
url = this.url(url) | ||
static _prepareGraphQLBase64 (data, options = {}) { | ||
const { filename, mimetype } = options | ||
if (!filename) { | ||
throw new Error('options.filename must be provided for Base64 upload') | ||
} | ||
const opts = this.addDefaultHeaders(options) | ||
return fetch(url, opts) | ||
} | ||
if (!mimetype) { | ||
throw new Error('options.mimetype must be provided for Base64 upload') | ||
} | ||
url (path) { | ||
return this.options.baseURL + path | ||
} | ||
if (options.bufferize) { | ||
const buffer = Buffer.from(data, 'base64') | ||
return this._prepareGraphQLBuffer(buffer, options) | ||
} | ||
addHeaders ({ options: existingOptions, headers: newHeaders }) { | ||
const { headers: existingHeaders = {} } = existingOptions | ||
return { | ||
...existingOptions, | ||
headers: { | ||
...existingHeaders, | ||
...newHeaders, | ||
}, | ||
data, | ||
filename, | ||
mimetype, | ||
} | ||
} | ||
addDefaultHeaders (options) { | ||
const { userAgent } = this.options | ||
return this.addHeaders({ | ||
options, | ||
headers: { | ||
'User-Agent': userAgent, | ||
}, | ||
}) | ||
} | ||
} | ||
@@ -165,0 +388,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
107014
17
749
202
2
5
20
5
+ Addedabort-controller@^3.0.0
+ Addedextract-files@^6
+ Addedform-data@^3.0.0
+ Addedabort-controller@3.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedevent-target-shim@5.0.1(transitive)
+ Addedextract-files@6.0.0(transitive)
+ Addedform-data@3.0.2(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)