UNPKG

storyblok-ts-client

Version:

Typescript library for working with Storyblok management API.

303 lines (288 loc) 9.59 kB
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, } from 'axios' import pThrottle from 'p-throttle' const apiServerUrl = 'http://api.storyblok.com/v1/spaces' const throttleLimit: number = 3 export interface ICustomAxiosRequestConfig extends AxiosRequestConfig { retries?: number retryDelay?: number } interface ICustomAxiosRequestConfigInternal extends ICustomAxiosRequestConfig { retryCount?: number } type RequestWithConfig = ( url: string, config?: ICustomAxiosRequestConfig ) => Promise<AxiosResponse> type RequestWithConfigAndData = ( url: string, data?: any, config?: ICustomAxiosRequestConfig ) => Promise<AxiosResponse> interface IStoryblokClass { delete: RequestWithConfig get: RequestWithConfig post: RequestWithConfigAndData put: RequestWithConfigAndData } /** * A class to provide basic CRUD request methods to Storyblok's management API with failure-retry options and built-in request throttling. Uses axios library to facilitation the API calls. * * @export * @class Storyblok * @implements {IStoryblokClass} * @param {string} apiToken - API access token. * @example * const {Storyblok} = require('storyblok-ts-client') * const storyblok = new Storyblok('fake_api_token') */ export class Storyblok implements IStoryblokClass { private apiToken: string private axiosInstance: AxiosInstance private throttled: { delete: RequestWithConfig get: RequestWithConfig post: RequestWithConfigAndData put: RequestWithConfigAndData } constructor(apiToken: string) { this.apiToken = apiToken this.axiosInstance = axios.create({ baseURL: apiServerUrl, headers: {Authorization: this.apiToken}, }) this.throttled = { delete: pThrottle(this.axiosInstance.delete, throttleLimit, 1000), get: pThrottle(this.axiosInstance.get, throttleLimit, 1000), post: pThrottle(this.axiosInstance.post, throttleLimit, 1000), put: pThrottle(this.axiosInstance.put, throttleLimit, 1000), } } /** * DELETE request method. * * @param {string} [url='/'] - Request url. * @param {ICustomAxiosRequestConfig} [config] - Request config. * @returns {Promise<any>} * @example * const {Storyblok} = require('storyblok-ts-client') * const storyblok = new Storyblok('fake_api_token') * const spaceId = 12345 * const storyId = 123456 * const url = `/${spaceId}/stories/${storyId}` * storyblok.delete(url, {retries: 3, retryDelay: 1000}) * .then(res => console.log('deleted story id:', res.story.id)) * // => deleted story id: 123456 * @name Storyblok#delete * @memberof Storyblok */ public delete( url: string = '/', config?: ICustomAxiosRequestConfig ): Promise<AxiosResponse> { const interceptor = this.activateRetry() return this.throttled .delete(url, config || {}) .then( (response: AxiosResponse): AxiosResponse => { this.deactivateRetry(interceptor) return response } ) .catch((error: AxiosError) => { this.deactivateRetry(interceptor) return Promise.reject(error) }) } /** * GET request method. * * @param {string} [url='/'] - Request url. * @param {ICustomAxiosRequestConfig} [config] - Request config. * @returns {Promise<any>} * @example * const {Storyblok} = require('storyblok-ts-client') * const storyblok = new Storyblok('fake_api_token') * const spaceId = 12345 * const url = `/${spaceId}` * storyblok.get(url, {retries: 3, retryDelay: 1000}) * .then(res => console.log('space id:', res.space.id)) * // => space id: 12345 * @name Storyblok#get * @memberof Storyblok */ public get( url: string = '/', config?: ICustomAxiosRequestConfig ): Promise<AxiosResponse> { const interceptor = this.activateRetry() return this.throttled .get(url, config || {}) .then( (response: AxiosResponse): AxiosResponse => { this.deactivateRetry(interceptor) return response } ) .catch((error: AxiosError) => { this.deactivateRetry(interceptor) return Promise.reject(error) }) } /** * POST request method. * * @param {string} [url='/'] - Request url. * @param {any} [data] - Request data body. * @param {ICustomAxiosRequestConfig} [config] - Request config. * @returns {Promise<any>} * @example * const {Storyblok} = require('storyblok-ts-client') * const storyblok = new Storyblok('fake_api_token') * const spaceId = 12345 * const url = `/${spaceId}/stories` * const story = { * name: 'test', * slug: 'test', * } * storyblok.post(url, {story}, {retries: 3, retryDelay: 1000}) * .then(res => console.log('new story id:', res.story.id)) * // => new story id: 123456 * @name Storyblok#post * @memberof Storyblok */ public post( url: string = '/', data?: any, config?: ICustomAxiosRequestConfig ): Promise<AxiosResponse> { const interceptor = this.activateRetry() return this.throttled .post(url, data || undefined, config || {}) .then( (response: AxiosResponse): AxiosResponse => { this.deactivateRetry(interceptor) return response } ) .catch((error: AxiosError) => { this.deactivateRetry(interceptor) return Promise.reject(error) }) } /** * PUT request method. * * @param {string} [url='/'] - Request url. * @param {any} [data] - Request data body. * @param {ICustomAxiosRequestConfig} [config] - Request config. * @returns {Promise<any>} * @example * const {Storyblok} = require('storyblok-ts-client') * const storyblok = new Storyblok('fake_api_token') * const spaceId = 12345 * const url = `/${spaceId}/stories` * const story = {name: 'test', slug: 'test'} * storyblok.post(url, {story}, {retries: 3, retryDelay: 1000}) * .then(res => { * const newStoryId = res.story.id * console.log('new story id:', newStoryId) * console.log('new story name:', res.story.name) * const updateContent = {name: 'new test', slug: 'test'} * return storyblok.put( * url + `/${newStoryId}`, * {story: updateContent}, * {retries: 3, retryDelay: 1000} * ) * }) * .then(res => console.log('updated story name:', res.story.name)) * .catch(e => console.log(e.config)) * // => new story id: 123456 * // => new story name: test * // => updated story name: new test * @name Storyblok#put * @memberof Storyblok */ public put( url: string = '/', data?: any, config?: ICustomAxiosRequestConfig ): Promise<any> { const interceptor = this.activateRetry() return this.throttled .put(url, data || undefined, config || {}) .then( (response: AxiosResponse): AxiosResponse => { this.deactivateRetry(interceptor) return response } ) .catch((error: AxiosError) => { this.deactivateRetry(interceptor) return Promise.reject(error) }) } /** * Uses axios's interceptors to faciliate failure-retry's with incremental delay period and a +/- 500ms variance. Based on: "http://www.itomtan.com/2017/10/17/vue-axios-timeout-retry-callback" * * @private * @returns {number} - Id for the interceptor, so failure-retry action can be removed after the API request is completed. * @memberof Storyblok */ private activateRetry(): number { return this.axiosInstance.interceptors.response.use( response => Promise.resolve(response), (error: AxiosError) => { const config: ICustomAxiosRequestConfigInternal = error.config if (!config || !config.retries) { return Promise.reject(error) } config.retryCount = config.retryCount || 0 if (config.retryCount > config.retries) { console.log('retry threshhold reached, promise is rejected') return Promise.reject(error) } const response = error.response if (response) { if (response.status !== 429 && response.status < 500) { console.log('terminal failure, promise is rejected') console.log('status:', response.status) console.log('message:', response.data) console.dir(response.config) return Promise.reject(error) } else { config.retryCount += 1 return new Promise(resolve => { const variance = () => Math.floor(Math.random() * 500) + 1 const delay: number = (config.retryDelay || 1250) - variance() const factor: number = config.retryCount as number setTimeout(() => { console.log(`retry no. ${config.retryCount}`) console.log(config.method + ' - ' + config.url) console.log( response.status + ' error: ' + response.data.error || response.statusText ) return resolve() }, delay * factor) }).then(() => this.axiosInstance(config)) } } } ) } /** * Used to deactivate the failure-retry mechanism, by removing the interceptor. * * @private * @param {number} interceptor - Id of the interceptor * @memberof Storyblok */ private deactivateRetry(interceptor: number): void { this.axiosInstance.interceptors.response.eject(interceptor) } }