Files
medusa-store/packages/medusa-js/src/request.ts
Philip Korsholm 5754534beb Feat: Client admin uploads (#952)
* convert uploads to ts

* add custom headers to request

* uploads resource

* uploads routes updates

* index export

* add oas

* remove delete uploads endpoint

* add new package for form-data

* remove exports for delete upload endpoint

* remove dev package from medusa-js

* automatic package upgrade of medusa
2022-01-04 16:18:17 +01:00

208 lines
5.4 KiB
TypeScript

import axios, { AxiosError, AxiosInstance, AxiosRequestHeaders } from "axios"
import * as rax from "retry-axios"
import { v4 as uuidv4 } from "uuid"
const unAuthenticatedAdminEndpoints = {
"/admin/auth": "POST",
"/admin/users/password-token": "POST",
"/admin/users/reset-password": "POST",
"/admin/invites/accept": "POST",
}
export interface Config {
baseUrl: string
maxRetries: number
apiKey?: string
}
export interface RequestOptions {
timeout?: number
numberOfRetries?: number
}
export type RequestMethod = "DELETE" | "POST" | "GET"
const defaultConfig = {
maxRetries: 0,
baseUrl: "http://localhost:9000",
}
class Client {
private axiosClient: AxiosInstance
private config: Config
constructor(config: Config) {
/** @private @constant {AxiosInstance} */
this.axiosClient = this.createClient({ ...defaultConfig, ...config })
/** @private @constant {Config} */
this.config = { ...defaultConfig, ...config }
}
shouldRetryCondition(
err: AxiosError,
numRetries: number,
maxRetries: number
): boolean {
// Obviously, if we have reached max. retries we stop
if (numRetries >= maxRetries) {
return false
}
// If no response, we assume a connection error and retry
if (!err.response) {
return true
}
// Retry on conflicts
if (err.response.status === 409) {
return true
}
// All 5xx errors are retried
// OBS: We are currently not retrying 500 requests, since our core needs proper error handling.
// At the moment, 500 will be returned on all errors, that are not of type MedusaError.
if (err.response.status > 500 && err.response.status <= 599) {
return true
}
return false
}
// Stolen from https://github.com/stripe/stripe-node/blob/fd0a597064289b8c82f374f4747d634050739043/lib/utils.js#L282
normalizeHeaders(obj: object): object {
if (!(obj && typeof obj === "object")) {
return obj
}
return Object.keys(obj).reduce((result, header) => {
result[this.normalizeHeader(header)] = obj[header]
return result
}, {})
}
// Stolen from https://github.com/marten-de-vries/header-case-normalizer/blob/master/index.js#L36-L41
normalizeHeader(header: string): string {
return header
.split("-")
.map(
(text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()
)
.join("-")
}
requiresAuthentication(path, method): boolean {
return (
path.startsWith("/admin") &&
unAuthenticatedAdminEndpoints[path] !== method
)
}
/**
* Creates all the initial headers.
* We add the idempotency key, if the request is configured to retry.
* @param {object} userHeaders user supplied headers
* @param {Types.RequestMethod} method request method
* @param {string} path request path
* @param {object} customHeaders user supplied headers
* @return {object}
*/
setHeaders(
userHeaders: RequestOptions,
method: RequestMethod,
path: string,
customHeaders: object = {}
): AxiosRequestHeaders {
let defaultHeaders: object = {
Accept: "application/json",
"Content-Type": "application/json",
}
if (this.config.apiKey && this.requiresAuthentication(path, method)) {
defaultHeaders = {
...defaultHeaders,
Authorization: `Bearer ${this.config.apiKey}`,
}
}
// only add idempotency key, if we want to retry
if (this.config.maxRetries > 0 && method === "POST") {
defaultHeaders["Idempotency-Key"] = uuidv4()
}
return Object.assign(
{},
defaultHeaders,
this.normalizeHeaders(userHeaders),
customHeaders
)
}
/**
* Creates the axios client used for requests
* As part of the creation, we configure the retry conditions
* and the exponential backoff approach.
* @param {Config} config user supplied configurations
* @return {AxiosInstance}
*/
createClient(config: Config): AxiosInstance {
const client = axios.create({
baseURL: config.baseUrl,
})
rax.attach(client)
client.defaults.raxConfig = {
instance: client,
retry: config.maxRetries,
backoffType: "exponential",
shouldRetry: (err: AxiosError): boolean => {
const cfg = rax.getConfig(err)
if (cfg) {
return this.shouldRetryCondition(
err,
cfg.currentRetryAttempt || 1,
cfg.retry || 3
)
} else {
return false
}
},
}
return client
}
/**
* Axios request
* @param {Types.RequestMethod} method request method
* @param {string} path request path
* @param {object} payload request payload
* @param {RequestOptions} options axios configuration
* @param {object} customHeaders custom request headers
* @return {object}
*/
async request(
method: RequestMethod,
path: string,
payload: object = {},
options: RequestOptions = {},
customHeaders: object = {}
): Promise<any> {
const reqOpts = {
method,
withCredentials: true,
url: path,
data: payload,
json: true,
headers: this.setHeaders(options, method, path, customHeaders),
}
// e.g. data = { cart: { ... } }, response = { status, headers, ... }
const { data, ...response } = await this.axiosClient(reqOpts)
// e.g. would return an object like of this shape { cart, response }
return { ...data, response }
}
}
export default Client