@affinidi/tools-openapi
Version:
SDK core monorepo for affinity DID solution
161 lines (133 loc) • 5.47 kB
text/typescript
import keyBy from 'lodash.keyby'
import { fetch } from '@affinidi/platform-fetch'
import { SdkError } from '@affinidi/tools-common'
import { GenericApiSpec } from '../types/openapi'
import { ParseSpec } from '../types/openapiParser'
import { ResponseForOperation, RequestOptionsForOperation, RequestOptions } from '../types/request'
import { BuildApiTypeWithoutConstraint, BuiltApiOperationType, BuiltApiType } from '../types/typeBuilder'
import { createAdditionalHeaders, createHeaders } from './headers'
import { mapFunctions } from './mapFunctions'
export type ThisData = {
accessApiKey: string
sdkVersion?: string
serviceUrl: string
tenantToken?: string
}
type MethodTypeByOperation<TOperation extends BuiltApiOperationType> = (
this: ThisData,
requestOptions: RequestOptionsForOperation<TOperation>,
) => Promise<ResponseForOperation<TOperation>>
type ClientTypeByApi<TApi extends BuiltApiType> = {
[key in keyof TApi]: MethodTypeByOperation<TApi[key]>
}
export type ClientTypeByRawSpec<TRawSpec extends GenericApiSpec> = ClientTypeByApi<
BuildApiTypeWithoutConstraint<ParseSpec<TRawSpec>>
>
export type ClientOptions = Omit<ThisData, 'serviceUrl'>
export type ClientFactoryByRawSpec = {
<TRawSpec extends GenericApiSpec>(rawSpec: TRawSpec): ClientTypeByRawSpec<TRawSpec>
}
type GetRequestOptions<TOperation extends MethodTypeByOperation<any>> = Parameters<TOperation>[0]
export type GetParams<TOperation extends MethodTypeByOperation<any>> = GetRequestOptions<TOperation>['params']
const httpMethodsLowercase = ['get', 'post', 'put', 'delete'] as const
type HttpMethodLowercase = typeof httpMethodsLowercase[number]
type HttpMethodUppercase = Uppercase<HttpMethodLowercase>
const parseSpec = (rawSpec: GenericApiSpec) => {
const basePath = rawSpec.servers[0].url === '/' ? '' : rawSpec.servers[0].url
const spec = Object.entries(rawSpec.paths).flatMap(([operationPath, operation]) => {
const path = `${basePath}${operationPath}`
return httpMethodsLowercase.flatMap((method) => {
const operationForMethod = operation[method]
if (!operationForMethod) {
return []
}
const { operationId } = operationForMethod
return [
{
path,
method,
operationId,
},
]
})
})
return keyBy(spec, 'operationId')
}
const executeByOptions = async (
method: HttpMethodLowercase,
pathTemplate: string,
{ authorization, params, pathParams, queryParams, storageRegion }: RequestOptions<any, any, any>,
clientOptions: ThisData,
) => {
if (!clientOptions.serviceUrl) {
throw new Error('Service URL is empty')
}
const headers = {
...createHeaders(clientOptions),
...createAdditionalHeaders({ authorization, storageRegion }),
}
const fetchOptions = {
headers,
method: method.toUpperCase() as HttpMethodUppercase,
...(!!params && { body: JSON.stringify(params, null, 2) }),
}
// eslint-disable-next-line no-unused-vars
const path = pathTemplate.replace(/\{(\w+)\}/g, (_match, p1) => pathParams?.[p1])
const queryParamsString = Object.entries(queryParams ?? {})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(`${value}`)}`)
.join('&')
const url = `${clientOptions.serviceUrl}${path}${queryParamsString !== '' ? `?${queryParamsString}` : ''}`
const response = await fetch(url, fetchOptions)
const { status } = response
const contentType = response.headers.get('content-type')
const isContentTypeJSON = contentType?.includes('application/json')
const isContentTypeHTML = contentType?.includes('text/html')
if (!status.toString().startsWith('2')) {
if (isContentTypeJSON) {
const error = await response.json()
const { code, message, context } = error
throw new SdkError({ code, message }, context, Object.assign({}, error, { httpStatusCode: status }))
}
if (isContentTypeHTML) {
const errorText = await response.text()
throw new SdkError(
{ code: 'COR-0', message: errorText },
{},
Object.assign({}, { httpStatusCode: status, errorText }),
)
}
throw new SdkError(
{ code: 'COR-0', message: 'Content type error.' },
{},
Object.assign({}, { httpStatusCode: status }),
)
}
if (!isContentTypeJSON) {
return { body: {}, status }
}
const jsonResponse = await response.json()
return { body: jsonResponse, status }
}
export const createClientMethods: ClientFactoryByRawSpec = <TApiSpec extends GenericApiSpec>(rawSpec: TApiSpec) => {
type ClientType = ClientTypeByRawSpec<TApiSpec>
const parsedSpec = parseSpec(rawSpec)
const typedParsedSpec = parsedSpec as Pick<typeof parsedSpec, keyof ClientType & string>
return mapFunctions(typedParsedSpec, ({ path, method }) => {
return (self: ThisData, requestOptions: RequestOptions<any, any, any>) =>
executeByOptions(method, path, requestOptions, self)
}) as any // to avoid TS2589 "Type instantiation is excessively deep and possibly infinite"
}
export const createThisData = (serviceUrl: string, otherOptions: ClientOptions): ThisData => {
return {
...otherOptions,
serviceUrl,
}
}
export const createClient = <TMethods>(
methods: TMethods,
serviceUrl: string,
otherOptions: ClientOptions,
): TMethods & ThisData => {
const thisData: ThisData = createThisData(serviceUrl, otherOptions)
return Object.assign(Object.create(methods as any), thisData)
}