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:
TonCherAmi 2021-08-05 12:50:37 +03:00 committed by GitHub
parent 45d8a6bca8
commit 58c28d58c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 295 additions and 5 deletions

View File

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

View File

@ -1 +1,6 @@
# TLK-Frontend
## Frontend Common
### Modules
- [api](./api/README.md)
- [fn](./fn/README.md)
- [logger](./fn/README.md)

88
api/ApiMethodFactory.ts Normal file
View File

@ -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

9
api/HttpMethod.ts Normal file
View File

@ -0,0 +1,9 @@
enum HttpMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
}
export default HttpMethod

56
api/README.md Normal file
View File

@ -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,
})
})
}
```

4
api/error.ts Normal file
View File

@ -0,0 +1,4 @@
export interface ApiError {
errorCode: number
errorMessage: Nullable<string>
}

2
api/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { default as HttpMethod } from './HttpMethod'
export { default as ApiMethodFactory } from './ApiMethodFactory'

28
api/request.ts Normal file
View File

@ -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

37
fn/README.md Normal file
View File

@ -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)
```

1
fn/index.ts Normal file
View File

@ -0,0 +1 @@
export { thread } from './thread'

11
fn/thread.ts Normal file
View File

@ -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)
}

3
global.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
type Nullable<T> = T | null | undefined
type UnknownRecord = Record<string, unknown>

3
logger/README.md Normal file
View File

@ -0,0 +1,3 @@
## logger
This module provides logging functionality.

13
logger/index.ts Normal file
View File

@ -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

14
package.json Normal file
View File

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