api
Version:
Magical SDK generation from an OpenAPI definition 🪄
149 lines (121 loc) • 4.44 kB
text/typescript
import type Oas from 'oas';
import type { Operation } from 'oas';
import type { HttpMethods } from 'oas/dist/rmoas.types';
import oasToHar from '@readme/oas-to-har';
import fetchHar from 'fetch-har';
import { FormDataEncoder } from 'form-data-encoder';
import 'isomorphic-fetch';
// `AbortController` was shipped in Node 15 so when Node 14 is EOL'd we can drop this dependency.
import { AbortController } from 'node-abort-controller';
import FetchError from './errors/fetchError';
import getJSONSchemaDefaults from './getJSONSchemaDefaults';
import parseResponse from './parseResponse';
import prepareAuth from './prepareAuth';
import prepareParams from './prepareParams';
import prepareServer from './prepareServer';
export interface ConfigOptions {
/**
* Override the default `fetch` request timeout of 30 seconds. This number should be represented
* in milliseconds.
*/
timeout?: number;
}
export interface FetchResponse<status, data> {
data: data;
headers: Headers;
res: Response;
status: status;
}
// https://stackoverflow.com/a/39495173
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>;
export type HTTPMethodRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
export { getJSONSchemaDefaults, parseResponse, prepareAuth, prepareParams, prepareServer };
export default class APICore {
spec: Oas;
private auth: (number | string)[] = [];
private server:
| false
| {
url?: string;
variables?: Record<string, string | number>;
} = false;
private config: ConfigOptions = {};
private userAgent: string;
constructor(spec?: Oas, userAgent?: string) {
this.spec = spec;
this.userAgent = userAgent;
}
setSpec(spec: Oas) {
this.spec = spec;
}
setConfig(config: ConfigOptions) {
this.config = config;
return this;
}
setUserAgent(userAgent: string) {
this.userAgent = userAgent;
return this;
}
setAuth(...values: string[] | number[]) {
this.auth = values;
return this;
}
setServer(url: string, variables: Record<string, string | number> = {}) {
this.server = { url, variables };
return this;
}
async fetch(path: string, method: HttpMethods, body?: unknown, metadata?: Record<string, unknown>) {
const operation = this.spec.operation(path, method);
return this.fetchOperation(operation, body, metadata);
}
async fetchOperation(operation: Operation, body?: unknown, metadata?: Record<string, unknown>) {
return prepareParams(operation, body, metadata).then(params => {
const data = { ...params };
// If `sdk.server()` has been issued data then we need to do some extra work to figure out
// how to use that supplied server, and also handle any server variables that were sent
// alongside it.
if (this.server) {
const preparedServer = prepareServer(this.spec, this.server.url, this.server.variables);
if (preparedServer) {
data.server = preparedServer;
}
}
// @ts-expect-error `this.auth` typing is off. FIXME
const har = oasToHar(this.spec, operation, data, prepareAuth(this.auth, operation));
let timeoutSignal: any;
const init: RequestInit = {};
if (this.config.timeout) {
const controller = new AbortController();
timeoutSignal = setTimeout(() => controller.abort(), this.config.timeout);
// @todo Typing on `AbortController` coming out of `node-abort-controler` isn't right so when
// we eventually drop that dependency we can remove the `as any` here.
init.signal = controller.signal as any;
}
return fetchHar(har as any, {
files: data.files || {},
init,
multipartEncoder: FormDataEncoder,
userAgent: this.userAgent,
})
.then(async (res: Response) => {
const parsed = await parseResponse(res);
if (res.status >= 400 && res.status <= 599) {
throw new FetchError<typeof parsed.status, typeof parsed.data>(
parsed.status,
parsed.data,
parsed.headers,
parsed.res,
);
}
return parsed;
})
.finally(() => {
if (this.config.timeout) {
clearTimeout(timeoutSignal);
}
});
});
}
}