From 90e227c794b331859f4b70c31babeff412bc99a4 Mon Sep 17 00:00:00 2001 From: Vasili Karaev Date: Wed, 4 Aug 2021 20:40:14 +0300 Subject: [PATCH] Add api module --- api/ApiMethodFactory.ts | 88 +++++++++++++++++++++++++++++++++++++++++ api/HttpMethod.ts | 9 +++++ api/error.ts | 4 ++ api/index.ts | 2 + api/request.ts | 28 +++++++++++++ fn/index.ts | 1 + fn/thread.ts | 12 ++++++ global.d.ts | 3 ++ logger/index.ts | 13 ++++++ package.json | 14 +++++++ 10 files changed, 174 insertions(+) create mode 100644 api/ApiMethodFactory.ts create mode 100644 api/HttpMethod.ts create mode 100644 api/error.ts create mode 100644 api/index.ts create mode 100644 api/request.ts create mode 100644 fn/index.ts create mode 100644 fn/thread.ts create mode 100644 global.d.ts create mode 100644 logger/index.ts create mode 100644 package.json diff --git a/api/ApiMethodFactory.ts b/api/ApiMethodFactory.ts new file mode 100644 index 0000000..bb597b2 --- /dev/null +++ b/api/ApiMethodFactory.ts @@ -0,0 +1,88 @@ +import Query from 'qs' +import * as R from 'ramda' +import { sprintf } from 'sprintf-js' +import request from './request' +import HttpMethod from './HttpMethod' + +class ApiMethodFactory { + private readonly apiPrefix: string + + constructor(apiPrefix: string) { + this.apiPrefix = apiPrefix + } + + private makePath = (data: T, pathKeys: string[]) => (template: string): string => { + const prefixedTemplate = `${this.apiPrefix}${template}` + + if (R.isEmpty(pathKeys)) { + return prefixedTemplate + } + + const pathData = R.pick(pathKeys, data) + + if (R.isEmpty(pathData)) { + throw Error('api: empty path data') + } + + return sprintf(prefixedTemplate, pathData) + } + + private makeEndpoint = ( + template: string, + data: T, + pathKeys: string[], + queryKeys: string[], + ): string => { + const make = R.compose( + this.addQuery(data, queryKeys), + this.makePath(data, pathKeys), + ) + + return make(template) + } + + private addQuery = (data: T, queryKeys: string[]) => (path: string): string => { + if (R.isEmpty(queryKeys)) { + return path + } + + const queryData = R.pick(queryKeys, data) + + if (R.isEmpty(queryData)) { + throw Error('api: empty query data') + } + + const query = Query.stringify(queryData) + + return `${path}?${query}` + } + + make = ( + template: string, + method: HttpMethod = HttpMethod.GET, { + path: pathKeys = [], + query: queryKeys = [], + }: { path?: string[], query?: string[] } = {}, + ) => async (data: Nullable = null): Promise => { + const getBody = R.pipe( + R.ifElse( + R.isNil, + R.always(null), + R.omit(R.concat(pathKeys, queryKeys)), + ), + R.when(R.isEmpty, R.always(null)), + R.unless(R.isNil, JSON.stringify), + ) + + const body = getBody(data) + const endpoint = this.makeEndpoint(template, data, pathKeys, queryKeys) + + return await request({ + method: method, + url: endpoint, + data: body, + }) + } +} + +export default ApiMethodFactory diff --git a/api/HttpMethod.ts b/api/HttpMethod.ts new file mode 100644 index 0000000..715059f --- /dev/null +++ b/api/HttpMethod.ts @@ -0,0 +1,9 @@ +enum HttpMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE' +} + +export default HttpMethod diff --git a/api/error.ts b/api/error.ts new file mode 100644 index 0000000..13ece56 --- /dev/null +++ b/api/error.ts @@ -0,0 +1,4 @@ +export interface ApiError { + errorCode: number + errorMessage: Nullable +} diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..d360ed5 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,2 @@ +export { default as HttpMethod } from './HttpMethod' +export { default as ApiMethodFactory } from './ApiMethodFactory' diff --git a/api/request.ts b/api/request.ts new file mode 100644 index 0000000..8ef08a2 --- /dev/null +++ b/api/request.ts @@ -0,0 +1,28 @@ +import axios, { AxiosRequestConfig } from 'axios' + +import logger from 'lib/logger' + +const retrieve = async ( + props: AxiosRequestConfig, + hasRetriedAfterAuthentication = false, +): Promise => { + try { + const { data } = await axios(props) + + return data + } catch (err) { + if (err?.hasAuthenticated && !hasRetriedAfterAuthentication) { + return retrieve(props, true) + } + + throw new Error(err) + } +} + +const request = (props: AxiosRequestConfig, { throwOnError = true } = {}) => { + logger.debug(props, `throwOnError: ${throwOnError}`) + + return retrieve(props) +} + +export default request diff --git a/fn/index.ts b/fn/index.ts new file mode 100644 index 0000000..58cbb70 --- /dev/null +++ b/fn/index.ts @@ -0,0 +1 @@ +export { thread } from './thread' diff --git a/fn/thread.ts b/fn/thread.ts new file mode 100644 index 0000000..25136fc --- /dev/null +++ b/fn/thread.ts @@ -0,0 +1,12 @@ +import * as R from 'ramda' + +type Transformer = (value: any) => any + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const pipe = R.pipe as unknown as (...fs: Transformer[]) => (value: any) => any + +export const thread = (value: any, ...fs: Transformer[]) => { + const transform = pipe(...fs) + + return transform(value) +} diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..aea1364 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,3 @@ +type Nullable = T | null | undefined + +type UnknownRecord = Record diff --git a/logger/index.ts b/logger/index.ts new file mode 100644 index 0000000..4f71e2e --- /dev/null +++ b/logger/index.ts @@ -0,0 +1,13 @@ +import loglevel from 'loglevel' + +const logger = loglevel.getLogger('default') + +logger.setLevel(process.env.NODE_ENV === 'production' ? 'WARN' : 'DEBUG') + +export const pipelog = (...args: unknown[]) => (value: unknown) => { + logger.debug(...args, value) + + return value +} + +export default logger diff --git a/package.json b/package.json new file mode 100644 index 0000000..875dd1e --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "lib", + "version": "0.1.0", + "dependencies": { + "@types/qs": "^6.9.7", + "@types/ramda": "^0.27.44", + "@types/sprintf-js": "^1.1.2", + "axios": "^0.21.1", + "loglevel": "^1.7.1", + "qs": "^6.10.1", + "ramda": "^0.27.1", + "sprintf-js": "^1.1.2" + } +}