From 58c28d58c0c14e916aca30853a74943212accf63 Mon Sep 17 00:00:00 2001 From: TonCherAmi Date: Thu, 5 Aug 2021 12:50:37 +0300 Subject: [PATCH] Add modules (#3) * Add api module * Update eslintrc * Make thread non-variadic * Set default api request/response types to any * Wrap api factory constructor parameter with object * Add module documentation --- .eslintrc | 24 +++++++++-- README.md | 7 +++- api/ApiMethodFactory.ts | 88 +++++++++++++++++++++++++++++++++++++++++ api/HttpMethod.ts | 9 +++++ api/README.md | 56 ++++++++++++++++++++++++++ api/error.ts | 4 ++ api/index.ts | 2 + api/request.ts | 28 +++++++++++++ fn/README.md | 37 +++++++++++++++++ fn/index.ts | 1 + fn/thread.ts | 11 ++++++ global.d.ts | 3 ++ logger/README.md | 3 ++ logger/index.ts | 13 ++++++ package.json | 14 +++++++ 15 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 api/ApiMethodFactory.ts create mode 100644 api/HttpMethod.ts create mode 100644 api/README.md create mode 100644 api/error.ts create mode 100644 api/index.ts create mode 100644 api/request.ts create mode 100644 fn/README.md create mode 100644 fn/index.ts create mode 100644 fn/thread.ts create mode 100644 global.d.ts create mode 100644 logger/README.md create mode 100644 logger/index.ts create mode 100644 package.json diff --git a/.eslintrc b/.eslintrc index 88d4fa0..e400467 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,11 +15,9 @@ "indent": ["error", 2], "eol-last": "error", "prefer-const": "error", - "no-shadow": "warn", "no-console": "error", "no-else-return": "warn", "comma-dangle": ["error", "always-multiline"], - "lines-between-class-members": ["error", "always"], "padding-line-between-statements": ["error", { "blankLine": "always", @@ -99,6 +97,24 @@ "shorthandFirst": true, "callbacksLast": true, "noSortAlphabetically": true - }] - } + }], + "jsx-control-statements/jsx-jcs-no-undef": "off" + }, + "overrides": [ + { + "files": [ + "**/*.ts?(x)" + ], + "rules": { + "indent": "off", + "@typescript-eslint/no-shadow": "warn", + "@typescript-eslint/indent": ["error", 2], + "@typescript-eslint/space-before-function-paren": ["error", { + "named": "never", + "anonymous": "always", + "asyncArrow": "always" + }] + } + } + ] } diff --git a/README.md b/README.md index bb048df..ab6faf8 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# TLK-Frontend \ No newline at end of file +## Frontend Common + +### Modules +- [api](./api/README.md) +- [fn](./fn/README.md) +- [logger](./fn/README.md) diff --git a/api/ApiMethodFactory.ts b/api/ApiMethodFactory.ts new file mode 100644 index 0000000..37d5c32 --- /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 }: { 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/README.md b/api/README.md new file mode 100644 index 0000000..9076f57 --- /dev/null +++ b/api/README.md @@ -0,0 +1,56 @@ +## api + +This module helps define callable API methods. + +### Usage example + +```typescript +import { ApiMethodFactory, HttpMethod } from 'lib/api' + +const { make } = new ApiMethodFactory({ apiPrefix: '/api' }) + +interface User { + id: string + name: string + email: string +} + +interface UserRequest { + id: string +} + +interface UserListRequest { + limit: number + offset: number +} + +type UserResponse = User + +type UserListResponse = User[] + +type _ = unknown + +const API = { + user: { + get: make('/users/%(id)s', HttpMethod.GET, { + path: ['id'], + }), + list: make('/users', HttpMethod.GET, { + query: ['limit', 'offset'], + }), + delete: make<_, UserRequest>('/users/%(id)s', HttpMethod.DELETE, { + path: ['id'], + }), + }, +} + +const run = async () => { + const users = await API.user.list({ limit: 10, offset: 0 }) + + users.forEach(async (user) => { + await API.user.delete({ + id: user.id, + }) + }) +} +``` 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/README.md b/fn/README.md new file mode 100644 index 0000000..0c8eb52 --- /dev/null +++ b/fn/README.md @@ -0,0 +1,37 @@ +## fn + +This module contains various ramda combinations/extensions. + +### Available functions: + +- `thread` - sequentially applies transforms from the array to the first argument. + + **Example usage**: + ```typescript + import * as R from 'ramda' + import * as F from 'lib/fn' + + const a = F.thread(5, [ + R.add(3), + R.multiply(2), + R.dec, + ]) + + a === 15 // true + + // equivalent to: + const transform = R.pipe( + R.add(3), + R.multiply(2), + R.dec, + ) + + const b = transform(5) + + // or + const c = R.pipe( + R.add(3), + R.multiply(2), + R.dec, + )(5) + ``` 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..4dce67f --- /dev/null +++ b/fn/thread.ts @@ -0,0 +1,11 @@ +import * as R from 'ramda' + +type Transformer = (value: any) => 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/README.md b/logger/README.md new file mode 100644 index 0000000..269433a --- /dev/null +++ b/logger/README.md @@ -0,0 +1,3 @@ +## logger + +This module provides logging functionality. 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" + } +}