Files
core/packages/api-client/core/client.ts
2024-03-22 23:10:33 +08:00

262 lines
7.5 KiB
TypeScript

import type {
IAdaptorRequestResponseType,
IRequestAdapter,
} from '~/interfaces/adapter'
import type { ClientOptions } from '~/interfaces/client'
import type { IController } from '~/interfaces/controller'
import type { RequestOptions } from '~/interfaces/instance'
import type { IRequestHandler, Method } from '~/interfaces/request'
import type { Class } from '~/interfaces/types'
import { isPlainObject } from '~/utils'
import { camelcaseKeys } from '~/utils/camelcase-keys'
import { resolveFullPath } from '~/utils/path'
import { allControllerNames } from '../controllers'
import { attachRequestMethod } from './attach-request'
import { RequestError } from './error'
const methodPrefix = '_$'
export type { HTTPClient }
class HTTPClient<
T extends IRequestAdapter = IRequestAdapter,
ResponseWrapper = unknown,
> {
private readonly _proxy: IRequestHandler<ResponseWrapper>
constructor(
private readonly _endpoint: string,
private _adaptor: T,
private options: Omit<ClientOptions, 'controllers'> = {},
) {
this._endpoint = _endpoint
.replace(/\/*$/, '')
.replace('localhost', '127.0.0.1')
this._proxy = this.buildRoute(this)()
options.transformResponse ||= (data) => camelcaseKeys(data)
options.getDataFromResponse ||= (res: any) => res.data
this.initGetClient()
attachRequestMethod(this)
}
private initGetClient() {
for (const name of allControllerNames) {
Object.defineProperty(this, name, {
get() {
const client: any = Reflect.get(this, `${methodPrefix}${name}`)
if (!client) {
throw new ReferenceError(
`${
name.charAt(0).toUpperCase() + name.slice(1)
} Client not inject yet, please inject with client.injectClients(...)`,
)
}
return client
},
configurable: false,
enumerable: false,
})
}
}
public injectControllers(...Controller: Class<IController>[]): void
public injectControllers(Controller: Class<IController>[]): void
public injectControllers(Controller: any, ...rest: any[]) {
Controller = Array.isArray(Controller) ? Controller : [Controller, ...rest]
for (const Client of Controller) {
const cl = new Client(this)
if (Array.isArray(cl.name)) {
for (const name of cl.name) {
attach.call(this, name, cl)
}
} else {
attach.call(this, cl.name, cl)
}
}
function attach(this: any, name: string, cl: IController) {
Object.defineProperty(this, `${methodPrefix}${name.toLowerCase()}`, {
get() {
return cl
},
enumerable: false,
configurable: false,
})
}
}
get endpoint() {
return this._endpoint
}
get instance() {
return this._adaptor
}
public request(options: {
url: string
method?: string
data?: any
params?: any
}) {
return (this as any)[`$$${String(options.method || 'get').toLowerCase()}`](
options.url,
options,
) as Promise<IAdaptorRequestResponseType<any>>
}
public get proxy() {
return this._proxy
}
private buildRoute(manager: this): () => IRequestHandler<ResponseWrapper> {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {}
const methods = ['get', 'post', 'delete', 'patch', 'put']
const reflectors = [
'toString',
'valueOf',
'inspect',
'constructor',
Symbol.toPrimitive,
]
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
return () => {
const route = ['']
const handler: any = {
get(target: any, name: Method) {
if (reflectors.includes(name))
return (withBase?: boolean) => {
if (withBase) {
const path = resolveFullPath(that.endpoint, route.join('/'))
route.length = 0
return path
} else {
const path = route.join('/')
route.length = 0
return path.startsWith('/') ? path : `/${path}`
}
}
if (methods.includes(name)) {
return async (options: RequestOptions) => {
const url = resolveFullPath(that.endpoint, route.join('/'))
route.length = 0
let res: Record<string, any> & { data: any }
try {
res = await manager.request({
method: name,
...options,
url,
})
} catch (e: any) {
let message = e.message
let code =
e.code ||
e.status ||
e.statusCode ||
e.response?.status ||
e.response?.statusCode ||
e.response?.code ||
500
if (that.options.getCodeMessageFromException) {
const errorInfo = that.options.getCodeMessageFromException(e)
message = errorInfo.message || message
code = errorInfo.code || code
}
throw that.options.customThrowResponseError
? that.options.customThrowResponseError(e)
: new RequestError(message, code, url, e)
}
const data = that.options.getDataFromResponse!(res)
if (!data) {
return null
}
const cameledObject =
(Array.isArray(data) || isPlainObject(data)) &&
that.options.transformResponse
? that.options.transformResponse(data)
: data
let nextObject: any = cameledObject
if (cameledObject && typeof cameledObject === 'object') {
nextObject = { ...cameledObject }
Object.defineProperty(nextObject, '$raw', {
get() {
return res
},
enumerable: false,
configurable: false,
})
// attach request config onto response
Object.defineProperty(nextObject, '$request', {
get() {
return {
url,
method: name,
options,
}
},
enumerable: false,
})
Object.defineProperty(nextObject, '$serialized', {
get() {
return cameledObject
},
})
}
return nextObject
}
}
route.push(name)
return new Proxy(noop, handler)
},
// @ts-ignore
apply(target: any, _, args) {
route.push(...args.filter((x: string) => x !== null))
return new Proxy(noop, handler)
},
}
return new Proxy(noop, handler) as any
}
}
}
export function createClient<T extends IRequestAdapter>(adapter: T) {
return <
ResponseWrapper = T extends { responseWrapper: infer Type }
? Type extends undefined
? unknown
: Type
: unknown,
>(
endpoint: string,
options?: ClientOptions,
) => {
const client = new HTTPClient<T, ResponseWrapper>(
endpoint,
adapter,
options,
)
const { controllers } = options || {}
if (controllers) {
client.injectControllers(controllers)
}
return client
}
}