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
This commit is contained in:
parent
45d8a6bca8
commit
58c28d58c0
24
.eslintrc
24
.eslintrc
|
|
@ -15,11 +15,9 @@
|
||||||
"indent": ["error", 2],
|
"indent": ["error", 2],
|
||||||
"eol-last": "error",
|
"eol-last": "error",
|
||||||
"prefer-const": "error",
|
"prefer-const": "error",
|
||||||
"no-shadow": "warn",
|
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
"no-else-return": "warn",
|
"no-else-return": "warn",
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
"lines-between-class-members": ["error", "always"],
|
|
||||||
"padding-line-between-statements": ["error",
|
"padding-line-between-statements": ["error",
|
||||||
{
|
{
|
||||||
"blankLine": "always",
|
"blankLine": "always",
|
||||||
|
|
@ -99,6 +97,24 @@
|
||||||
"shorthandFirst": true,
|
"shorthandFirst": true,
|
||||||
"callbacksLast": true,
|
"callbacksLast": true,
|
||||||
"noSortAlphabetically": 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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,6 @@
|
||||||
# TLK-Frontend
|
## Frontend Common
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
- [api](./api/README.md)
|
||||||
|
- [fn](./fn/README.md)
|
||||||
|
- [logger](./fn/README.md)
|
||||||
|
|
|
||||||
|
|
@ -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 = <T>(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 = <T>(
|
||||||
|
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 = <T>(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 = <R = any, T = any>(
|
||||||
|
template: string,
|
||||||
|
method: HttpMethod = HttpMethod.GET, {
|
||||||
|
path: pathKeys = [],
|
||||||
|
query: queryKeys = [],
|
||||||
|
}: { path?: string[], query?: string[] } = {},
|
||||||
|
) => async (data: Nullable<T> = null): Promise<R> => {
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
enum HttpMethod {
|
||||||
|
GET = 'GET',
|
||||||
|
POST = 'POST',
|
||||||
|
PUT = 'PUT',
|
||||||
|
PATCH = 'PATCH',
|
||||||
|
DELETE = 'DELETE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HttpMethod
|
||||||
|
|
@ -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<UserResponse, UserRequest>('/users/%(id)s', HttpMethod.GET, {
|
||||||
|
path: ['id'],
|
||||||
|
}),
|
||||||
|
list: make<UserListResponse, UserListRequest>('/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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ApiError {
|
||||||
|
errorCode: number
|
||||||
|
errorMessage: Nullable<string>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as HttpMethod } from './HttpMethod'
|
||||||
|
export { default as ApiMethodFactory } from './ApiMethodFactory'
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
|
import logger from 'lib/logger'
|
||||||
|
|
||||||
|
const retrieve = async (
|
||||||
|
props: AxiosRequestConfig,
|
||||||
|
hasRetriedAfterAuthentication = false,
|
||||||
|
): Promise<any> => {
|
||||||
|
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
|
||||||
|
|
@ -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)
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { thread } from './thread'
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
type Nullable<T> = T | null | undefined
|
||||||
|
|
||||||
|
type UnknownRecord = Record<string, unknown>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
## logger
|
||||||
|
|
||||||
|
This module provides logging functionality.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue