files.com
Version:
Files.com SDK for JavaScript
261 lines (211 loc) • 7.36 kB
JavaScript
import fetch from 'cross-fetch'
import Files from './Files'
import * as errors from './Errors'
import Logger from './Logger'
import { isEmpty, isObject } from './utils'
const fetchWithTimeout = (url, { timeoutSecs, ...options } = {}) => {
let timeoutId
return timeoutSecs <= 0
? fetch(url, options)
: Promise.race([
fetch(url, options),
new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new errors.FilesError('Request timed out')), timeoutSecs * 1000)
}),
]).finally(() => clearTimeout(timeoutId))
}
const fetchWithRetry = async (url, options, retries = 0) => {
const maxRetries = Files.getMaxNetworkRetries()
const minRetryDelaySecs = Files.getMinNetworkRetryDelay()
const maxRetryDelaySecs = Files.getMaxNetworkRetryDelay()
try {
return await fetchWithTimeout(url, options)
} catch (error) {
Logger.info(`Request #${retries + 1} failed: ${error.message}`)
if (retries >= maxRetries) {
throw error
} else {
const nextRetries = retries + 1
Logger.info(`Retrying request (retry ${nextRetries} of ${maxRetries})`)
const delaySecs = Math.min(minRetryDelaySecs * 2 ** retries, maxRetryDelaySecs) // exponential backoff
await new Promise(resolve => { setTimeout(resolve, delaySecs * 1000) })
return fetchWithRetry(url, options, nextRetries)
}
}
}
class Api {
static _sendVerbatim = async (path, verb, optionsRaw) => {
const { getAgentForUrl, ...options } = optionsRaw || {}
const isExternal = /^[a-zA-Z]+:\/\//.test(path)
const baseUrl = Files.getBaseUrl()
if (!isExternal && !baseUrl) {
throw new errors.ConfigurationError('Base URL has not been set - use Files.setBaseUrl() to set it')
}
const url = isExternal
? path
: `${baseUrl}${Files.getEndpointPrefix()}${path}`
Logger.debug(`Sending request: ${verb} ${url}`)
Logger.debug('Sending options:', {
method: verb,
...options,
headers: {
...options.headers,
'X-FilesAPI-Key': '<redacted>',
},
})
try {
const agent = getAgentForUrl?.(url) || options?.agent || options?.httpsAgent || options?.httpAgent
const response = await fetchWithRetry(url, {
agent,
method: verb,
timeoutSecs: Files.getNetworkTimeout(),
...options,
})
const headers = Object.fromEntries(response.headers.entries())
Logger.debug(`Status: ${response.status} ${response.statusText}`)
if (Files.shouldDebugResponseHeaders()) {
Logger.debug('Response Headers: ')
Logger.debug(headers)
}
const contentType = headers['content-type'] || ''
let data
if (contentType.includes('application/json')) {
if (headers['content-length'] === '0') {
data = response.body
} else {
data = await response.json()
}
} else if (contentType.includes('text/')) {
data = await response.text()
} else if (contentType.includes('multipart/form-data')) {
data = await response.formData()
} else {
data = response.body
}
const normalizedResponse = {
data,
headers,
reason: response.statusText,
status: response.status,
}
if (!response.ok) {
/* eslint-disable-next-line no-throw-literal */
throw { response: normalizedResponse }
}
return normalizedResponse
} catch (error) {
errors.handleErrorResponse(error)
return null
}
}
static sendFilePart = (externalUrl, verb, data, optionsRaw = {}) => {
const options = {
...optionsRaw,
body: data,
}
return Api._sendVerbatim(externalUrl, verb, options)
}
static _autoPaginate = async (path, verb, params, options, response, metadata) => {
if (options.autoPaginate ?? Files.getAutoPaginate()) {
const nextCursor = response?.headers?.['x-files-cursor']
const {
autoPaginateCount,
previousAutoPaginateData,
} = metadata || {}
if (nextCursor) {
const nextPage = (Number(params?.page) || 1) + 1
const nextParams = {
...params,
cursor: nextCursor,
page: nextPage,
}
const nextMetadata = {
autoPaginateCount: (autoPaginateCount || 1) + 1,
previousAutoPaginateData: [
...previousAutoPaginateData || [],
...response?.data || [],
],
}
return Api.sendRequest(path, verb, nextParams, options, nextMetadata)
}
if (previousAutoPaginateData) {
return {
...response,
autoPaginateRequests: autoPaginateCount,
data: [...previousAutoPaginateData, ...response?.data || []],
}
}
}
return response
}
static sendRequest = async (path, verb, params = null, options = {}, metadata = null) => {
const languageHeader = Files.getLanguage() ? { 'Accept-Language': Files.getLanguage() } : {}
const headers = {
Accept: 'application/json',
...languageHeader,
...options.headers,
'User-Agent': Files.getUserAgent(),
}
const isExternal = /^[a-zA-Z]+:\/\//.test(path)
if (!isExternal) {
const sessionId = options.sessionId || Files.getSessionId()
if (sessionId) {
headers['X-FilesAPI-Auth'] = sessionId
} else {
const isCreatingSession = path === '/sessions' && verb.toUpperCase() === 'POST'
// api key cannot be used when creating a session
if (!isCreatingSession) {
const apiKey = options.apiKey || Files.getApiKey()
if (!apiKey) {
throw new errors.ConfigurationError('API key has not been set - use Files.setApiKey() to set it')
}
headers['X-FilesAPI-Key'] = apiKey
}
}
}
const updatedOptions = {
...options,
headers,
}
let requestPath = path
const hasParams = isObject(params) && !isEmpty(params)
if (hasParams) {
if (verb.toUpperCase() === 'GET') {
const _params = {}
for (const [key, value] of Object.entries(params)) {
if (isObject(value)) {
for (const [key2, value2] of Object.entries(value)) {
_params[`${key}[${key2}]`] = value2
}
} else {
_params[key] = value
}
}
const pairs = Object.entries(_params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
requestPath += path.includes('?') ? '&' : '?'
requestPath += pairs.join('&')
} else {
updatedOptions.body = JSON.stringify(params)
headers['Content-Type'] = 'application/json'
}
}
if (Files.shouldDebugRequest()) {
Logger.debug('Request Options:')
Logger.debug({
...updatedOptions,
body: hasParams
? `payload keys: ${Object.keys(params).join(', ')}`
: '(none)',
headers: {
...headers,
'X-FilesAPI-Key': '<redacted>',
},
})
}
const response = await Api._sendVerbatim(requestPath, verb, updatedOptions)
return Api._autoPaginate(path, verb, params, updatedOptions, response, metadata)
}
}
export default Api
module.exports = Api
module.exports.default = Api