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],
|
||||
"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"
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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