import Resource, { ListResponse } from './index' import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse, AxiosError, AxiosInstance } from 'axios' export * from 'axios' export interface RequestConfig extends AxiosRequestConfig { useCache?: boolean query?: any } export interface ResourceResponse extends Record { response: AxiosResponse resources: T[] count?: () => number pages?: () => number currentPage?: () => number perPage?: () => number next?: () => Promise> previous?: () => Promise> } export type ExtractorFunction = (result: ResourceResponse['response']) => ResourceResponse export class BaseClient { axios: AxiosInstance config: AxiosRequestConfig = {} constructor(baseURL: string, config: AxiosRequestConfig = {}) { this.config = Object.assign({ baseURL }, config) this.axios = axios.create(this.config) } get hostname() { return this.config.baseURL } set hostname(value: string) { this.config.baseURL = value this.axios = axios.create(this.config) } static extend(this: U, classProps: T): U & T { // @todo Figure out typings here -- this works perfectly but typings are not happy // @ts-ignore return Object.assign(class extends this {}, classProps) } negotiateContent(ResourceClass: T): ExtractorFunction> { // Should always return a function return (response: ResourceResponse>['response']) => { let objects: InstanceType[] = [] if (Array.isArray(response.data)) { response.data.forEach((attributes) => objects.push(new ResourceClass(attributes) as InstanceType)) } else { objects.push(new ResourceClass(response.data) as InstanceType) } return { response, resources: objects, count: () => response.headers['Pagination-Count'], pages: () => Math.ceil(response.headers['Pagination-Count'] / response.headers['Pagination-Limit']), currentPage: () => response.headers['Pagination-Page'], perPage: () => response.headers['Pagination-Limit'], } as ResourceResponse> } } /** * Client.prototype.list() and Client.prototype.detail() are the primary purpose of defining these here. Simply runs a GET on the list route path (eg. /users) and negotiates the content * @param ResourceClass * @param options */ list(ResourceClass: T, options: RequestConfig = {}): ListResponse { return this.get(ResourceClass.getListRoutePath(options.query), options).then(this.negotiateContent(ResourceClass)) } /** * Client.prototype.detail() and Client.prototype.list() are the primary purpose of defining these here. Simply runs a GET on the detail route path (eg. /users/123) and negotiates the content * @param ResourceClass * @param options */ detail(ResourceClass: T, id: string, options: RequestConfig = {}) { return this.get(ResourceClass.getDetailRoutePath(id, options.query), options).then(this.negotiateContent(ResourceClass)) } get(path: string, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.get(path, options).catch((e: Error) => this.onError(e)) } put(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.put(path, body, options).catch((e: Error) => this.onError(e)) } post(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.post(path, body, options).catch((e: Error) => this.onError(e)) } patch(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.patch(path, body, options).catch((e: Error) => this.onError(e)) } delete(path: string, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.delete(path, options).catch((e: Error) => this.onError(e)) } head(path: string, options: AxiosRequestConfig = {}): AxiosPromise { return this.axios.head(path, options).catch((e: Error) => this.onError(e)) } options(path: string, options: AxiosRequestConfig = {}): AxiosPromise { // @ts-ignore -- Axios forgot to add options to AxiosInstance interface return this.axios.options(path, options).catch((e: Error) => this.onError(e)) } bindMethodsToPath(relativePath: string) { return { get: this.get.bind(this, relativePath), post: this.post.bind(this, relativePath), put: this.put.bind(this, relativePath), patch: this.patch.bind(this, relativePath), head: this.head.bind(this, relativePath), options: this.options.bind(this, relativePath), delete: this.delete.bind(this, relativePath), } } // Optionally catch all errors in client class (default: rethrow) onError(exception: Error | AxiosError): any { throw exception } } export class DefaultClient extends BaseClient {} export class JWTBearerClient extends BaseClient { token: string // This is just a basic client except we're including a token in the requests constructor(baseURL: string, token: string = '', options: RequestConfig = {}) { let headers = Object.assign( { Authorization: `Bearer ${token}`, }, options.headers ) options.headers = headers super(baseURL, options) this.token = token } getTokenPayload(): any { try { let jwtPieces = this.token.split('.') let payloadBase64 = jwtPieces[1] let payloadBuffer = Buffer.from(payloadBase64, 'base64').toString() return JSON.parse(payloadBuffer.toString()) } catch (e) { return undefined } } tokenIsExpired(): boolean { try { let payload = this.getTokenPayload() let nowInSeconds = Math.floor(Date.now() / 1000) return payload.exp < nowInSeconds } catch (e) { return true } } tokenIsValid(): boolean { return this.token && !this.tokenIsExpired() } }