UNPKG

@affinidi/tools-openapi

Version:
161 lines (133 loc) 5.47 kB
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) }