@anvilco/anvil
Version:
Anvil API Client
855 lines (485 loc) • 16.1 kB
JavaScript
;Object.defineProperty(exports, "__esModule", { value: true });exports.default = void 0;var _fs = _interopRequireDefault(require("fs"));
var _stream = require("stream");
var _abortController = _interopRequireDefault(require("abort-controller"));
var _limiter = require("limiter");
var _UploadWithOptions = _interopRequireDefault(require("./UploadWithOptions"));
var _package = require("../package.json");
var _errors = require("./errors");
var _graphql = require("./graphql");
var _validation = require("./validation");function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}
class Warning extends Error {}
let extractFiles;
let FormDataModule;
let Fetch;
let fetch;
const {
mutations: {
createEtchPacket: {
generateMutation: generateCreateEtchPacketMutation
},
forgeSubmit: {
generateMutation: generateForgeSubmitMutation
},
generateEtchSignUrl: {
generateMutation: generateEtchSignUrlMutation
},
removeWeldData: {
generateMutation: generateRemoveWeldDataMutation
}
},
queries: {
etchPacket: {
generateQuery: generateEtchPacketQuery
}
}
} = { queries: _graphql.queries, mutations: _graphql.mutations };
const DATA_TYPE_STREAM = 'stream';
const DATA_TYPE_BUFFER = 'buffer';
const DATA_TYPE_ARRAY_BUFFER = 'arrayBuffer';
const DATA_TYPE_JSON = 'json';
const SUPPORTED_BINARY_DATA_TYPES = Object.freeze([
DATA_TYPE_STREAM,
DATA_TYPE_BUFFER,
DATA_TYPE_ARRAY_BUFFER]
);
const VERSION_LATEST = -1;
const VERSION_LATEST_PUBLISHED = -2;
const defaultOptions = {
baseURL: 'https://app.useanvil.com',
userAgent: `${_package.description}/${_package.version}`
};
const FILENAME_IGNORE_MESSAGE = 'If you think you can ignore this, please pass `options.ignoreFilenameValidation` as `true`.';
const failBufferMS = 50;
class Anvil {
constructor(options) {
if (!options) throw new Error('options are required');
this.options = {
...defaultOptions,
requestLimit: 1,
requestLimitMS: 1000,
...options
};
const { apiKey, accessToken } = this.options;
if (!(apiKey || accessToken)) throw new Error('apiKey or accessToken required');
this.authHeader = accessToken ?
`Bearer ${Buffer.from(accessToken, 'ascii').toString('base64')}` :
`Basic ${Buffer.from(`${apiKey}:`, 'ascii').toString('base64')}`;
this.hasSetLimiterFromResponse = false;
this.limiterSettingInProgress = false;
this.rateLimiterSetupPromise = new Promise((resolve) => {
this.rateLimiterPromiseResolver = resolve;
});
this._setRateLimiter({ tokens: this.options.requestLimit, intervalMs: this.options.requestLimitMS });
}
_setRateLimiter({ tokens, intervalMs }) {
if (
!(tokens && intervalMs) ||
this.limitTokens === tokens && this.limitIntervalMs === intervalMs)
{
return;
}
const newLimiter = new _limiter.RateLimiter({ tokensPerInterval: tokens, interval: intervalMs });
if (this.limiter) {
const tokensInUse = Math.max(
this.limitTokens - Math.floor(this.limiter.getTokensRemaining()),
0
);
const tokensToRemove = Math.min(tokens, tokensInUse);
if (tokensToRemove) {
newLimiter.tryRemoveTokens(tokensToRemove);
}
delete this.limiter;
}
this.limitTokens = tokens;
this.limitIntervalMs = intervalMs;
this.limiter = newLimiter;
}
static prepareGraphQLFile(pathOrStreamLikeThing, { ignoreFilenameValidation, ...formDataAppendOptions } = {}) {
if (typeof pathOrStreamLikeThing === 'string') {
} else if (
!formDataAppendOptions ||
formDataAppendOptions && !(
formDataAppendOptions.filename || ignoreFilenameValidation))
{
if (
pathOrStreamLikeThing instanceof Buffer ||
!(
pathOrStreamLikeThing.path && typeof pathOrStreamLikeThing.path === 'string' ||
pathOrStreamLikeThing.name && typeof pathOrStreamLikeThing.name === 'string'))
{
let message = 'For this type of input, `options.filename` must be provided to prepareGraphQLFile.' + ' ' + FILENAME_IGNORE_MESSAGE;
try {
if (pathOrStreamLikeThing && pathOrStreamLikeThing.constructor && pathOrStreamLikeThing.constructor.name) {
message = `When passing a ${pathOrStreamLikeThing.constructor.name} to prepareGraphQLFile, \`options.filename\` must be provided. ${FILENAME_IGNORE_MESSAGE}`;
}
} catch (err) {
console.error(err);
}
throw new Error(message);
}
}
return new _UploadWithOptions.default(pathOrStreamLikeThing, formDataAppendOptions);
}
createEtchPacket({ variables, responseQuery, mutation }) {
return this.requestGraphQL(
{
query: mutation || generateCreateEtchPacketMutation(responseQuery),
variables
},
{ dataType: DATA_TYPE_JSON }
);
}
downloadDocuments(documentGroupEid, clientOptions = {}) {
const { dataType = DATA_TYPE_BUFFER } = clientOptions;
if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) {
throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`);
}
return this.requestREST(
`/api/document-group/${documentGroupEid}.zip`,
{ method: 'GET' },
{
...clientOptions,
dataType
}
);
}
fillPDF(pdfTemplateID, payload, clientOptions = {}) {
const { dataType = DATA_TYPE_BUFFER } = clientOptions;
if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) {
throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`);
}
const versionNumber = clientOptions.versionNumber;
const url = versionNumber ?
`/api/v1/fill/${pdfTemplateID}.pdf?versionNumber=${versionNumber}` :
`/api/v1/fill/${pdfTemplateID}.pdf`;
return this.requestREST(
url,
{
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
},
{
...clientOptions,
dataType
}
);
}
forgeSubmit({ variables, responseQuery, mutation }) {
return this.requestGraphQL(
{
query: mutation || generateForgeSubmitMutation(responseQuery),
variables
},
{ dataType: DATA_TYPE_JSON }
);
}
generatePDF(payload, clientOptions = {}) {
const { dataType = DATA_TYPE_BUFFER } = clientOptions;
if (dataType && !SUPPORTED_BINARY_DATA_TYPES.includes(dataType)) {
throw new Error(`dataType must be one of: ${SUPPORTED_BINARY_DATA_TYPES.join('|')}`);
}
return this.requestREST(
'/api/v1/generate-pdf',
{
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
},
{
...clientOptions,
dataType
}
);
}
getEtchPacket({ variables, responseQuery }) {
return this.requestGraphQL(
{
query: generateEtchPacketQuery(responseQuery),
variables
},
{ dataType: DATA_TYPE_JSON }
);
}
async generateEtchSignUrl({ variables }) {
const { statusCode, data, errors } = await this.requestGraphQL(
{
query: generateEtchSignUrlMutation(),
variables
},
{ dataType: DATA_TYPE_JSON }
);
return {
statusCode,
url: data && data.data && data.data.generateEtchSignURL,
errors
};
}
removeWeldData({ variables, mutation }) {
return this.requestGraphQL(
{
query: mutation || generateRemoveWeldDataMutation(),
variables
},
{ dataType: DATA_TYPE_JSON }
);
}
async requestGraphQL({ query, variables = {} }, clientOptions) {
const options = {
method: 'POST',
headers: {}
};
const originalOperation = { query, variables };
extractFiles ?? (extractFiles = (await import('extract-files/extractFiles.mjs')).default);
const {
clone: augmentedOperation,
files: filesMap
} = extractFiles(originalOperation, _validation.isFile);
const operationJSON = JSON.stringify(augmentedOperation);
if (!(0, _validation.graphQLUploadSchemaIsValid)(originalOperation)) {
throw new Error('Invalid File schema detected');
}
if (filesMap.size) {
const abortController = new _abortController.default();
Fetch ?? (Fetch = await import('@anvilco/node-fetch'));
FormDataModule ?? (FormDataModule = await import('formdata-polyfill/esm.min.js'));
const form = new FormDataModule.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) => {
if (file instanceof _UploadWithOptions.default === false) {
file = Anvil.prepareGraphQLFile(file);
}
let { filename, mimetype, ignoreFilenameValidation } = file.options || {};
file = file.file;
if (!file) {
throw new Error('No file provided. Options were: ' + JSON.stringify(options));
}
if (typeof file.on === 'function') {
file.on('error', (err) => {
console.warn(err);
abortController.abort();
});
}
if (typeof file === 'string') {
file = Fetch.fileFromSync(file, mimetype);
} else if (file instanceof Buffer) {
const buffer = file;
file = new Fetch.File(
[buffer],
filename,
{
type: mimetype
}
);
} else if (file instanceof _stream.Stream) {
const stream = file;
file = {
[Symbol.toStringTag]: 'File',
size: _fs.default.statSync(stream.path).size,
stream: () => stream,
type: mimetype
};
filename ?? (filename = stream.path.split('/').pop());
} else if (file.constructor.name !== 'File') {
if (!filename) {
const name = file.name || file.path;
if (name) {
filename = name.split('/').pop();
}
if (!filename && !ignoreFilenameValidation) {
console.warn(new Warning('No filename provided. Please provide a filename to the file options.'));
}
}
}
form.append(`${++i}`, file, filename);
});
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
};
}
async _request(...args) {
Fetch = Fetch || (await import('@anvilco/node-fetch'));
fetch = Fetch.default;
this._request = this.__request;
return this._request(...args);
}
__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) => {
let { dataType, debug } = clientOptions;
const response = await retryableRequestFn();
if (!this.hasSetLimiterFromResponse) {
const tokens = parseInt(response.headers.get('x-ratelimit-limit'));
const intervalMs = parseInt(response.headers.get('x-ratelimit-interval-ms'));
this._setRateLimiter({ tokens, intervalMs });
this.hasSetLimiterFromResponse = true;
this.limiterSettingInProgress = false;
this.rateLimiterPromiseResolver();
}
const { status: statusCode, statusText } = response;
if (statusCode === 429) {
return retry(getRetryMS(response.headers.get('retry-after')));
}
let json;
let isError = false;
let nodeError;
const contentType = response.headers.get('content-type') || response.headers.get('Content-Type') || '';
if (contentType.toLowerCase().includes('application/json')) {
dataType = DATA_TYPE_JSON;
try {
json = await response.json();
isError = (0, _errors.looksLikeJsonError)({ json });
} catch (err) {
nodeError = err;
if (debug) {
console.warn(`Problem parsing JSON response for status ${statusCode}:`);
console.warn(err);
}
}
}
if (nodeError || isError || statusCode >= 300) {
const errors = nodeError ? (0, _errors.normalizeNodeError)({ error: nodeError }) : (0, _errors.normalizeJsonErrors)({ json, statusText });
return { response, statusCode, errors };
}
let data;
switch (dataType) {
case DATA_TYPE_STREAM:
data = response.body;
break;
case DATA_TYPE_BUFFER:
data = Buffer.from(await response.arrayBuffer());
break;
case DATA_TYPE_ARRAY_BUFFER:
data = await response.arrayBuffer();
break;
case DATA_TYPE_JSON:
data = json || (await response.json());
break;
default:
console.warn('Using default response dataType of "json". Please specify a dataType.');
data = await response.json();
break;
}
return {
response,
data,
statusCode
};
});
}
_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 }
);
}
async _throttle(fn) {
if (!this.hasSetLimiterFromResponse) {
if (this.limiterSettingInProgress) {
await this.rateLimiterSetupPromise;
} else {
this.limiterSettingInProgress = true;
}
}
const remainingRequests = await this.limiter.removeTokens(1);
if (remainingRequests < 1) {
await sleep(this.options.requestLimitMS + failBufferMS);
}
const retry = async (ms) => {
await sleep(ms);
return this._throttle(fn);
};
return fn(retry);
}
}
Anvil.UploadWithOptions = _UploadWithOptions.default;
function getRetryMS(retryAfterSeconds) {
return Math.round((Math.abs(parseFloat(retryAfterSeconds)) || 0) * 1000) + failBufferMS;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
Anvil.VERSION_LATEST = VERSION_LATEST;
Anvil.VERSION_LATEST_PUBLISHED = VERSION_LATEST_PUBLISHED;var _default = exports.default =
Anvil;