UNPKG

@anvilco/anvil

Version:
855 lines (485 loc) 16.1 kB
"use strict";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;