Files
apios/src/index.ts
2026-06-18 00:54:46 +02:00

117 lines
3.3 KiB
TypeScript

import { Result } from "./Result.js";
import type { StandardSchemaV1 } from "./StandardSchemaV1.js";
type RequestBody<Args extends ArgsBase> = Args["requestBody"] extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<Args["requestBody"]> : void;
type ResponseBody<Args extends ArgsBase> = Args["responseBody"] extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<Args["responseBody"]> : void;
type ErrorBody<Args extends ArgsBase> = Args["errorBody"] extends StandardSchemaV1<any> ? StandardSchemaV1.InferOutput<Args["errorBody"]> : void;
type Api<Args extends ArgsBase> = (requestBody: RequestBody<Args>, signal?: AbortSignal) => Promise<Result<ResponseBody<Args>, ErrorBody<Args>>>;
interface ArgsBase {
readonly url: string;
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | (string & {});
readonly requestBody?: StandardSchemaV1<any> | undefined;
readonly responseBody?: StandardSchemaV1<any> | undefined;
readonly errorBody?: StandardSchemaV1<any> | undefined;
readonly cache?: RequestInit["cache"];
readonly credentials?: RequestInit["credentials"];
readonly headers?: RequestInit["headers"];
}
export const defineApi = <const Args extends ArgsBase>({
url,
method,
requestBody: requestBodySchema,
responseBody: responseBodySchema,
errorBody: errorBodySchema,
cache,
credentials,
headers: headersInit,
}: Args): Api<Args> => {
const headers = new Headers(headersInit);
if (requestBodySchema !== undefined) {
headers.set("Content-Type", "application/json");
}
const init: RequestInit = {
method,
headers,
...(cache !== undefined ? { cache } : null),
...(credentials !== undefined ? { credentials } : null),
};
return async (requestBody, signal) => {
const body = requestBodySchema !== undefined ? JSON.stringify(requestBody) : null;
const response = await fetch(url, {
...init,
body,
...(signal !== undefined ? { signal } : null),
});
if (response.ok) {
if (responseBodySchema !== undefined) {
const json = await response.json();
const validateResult = await responseBodySchema["~standard"].validate(json);
if (validateResult.issues !== undefined) {
throw new ValidationError({
schema: responseBodySchema,
input: json,
issues: validateResult.issues,
});
} else {
return Result.success(validateResult.value);
}
} else {
return Result.success(undefined);
}
} else {
if (errorBodySchema !== undefined) {
const json = await response.json();
const validateResult = await errorBodySchema["~standard"].validate(json);
if (validateResult.issues !== undefined) {
throw new ValidationError({
schema: errorBodySchema,
input: json,
issues: validateResult.issues,
});
} else {
return Result.failure(validateResult.value);
}
} else {
return Result.failure(undefined);
}
}
};
};
export namespace ValidationError {
export interface Props {
readonly schema: StandardSchemaV1<any>;
readonly input: unknown;
readonly issues: readonly StandardSchemaV1.Issue[];
}
}
export class ValidationError extends Error {
readonly schema: StandardSchemaV1;
readonly input: unknown;
readonly issues: readonly StandardSchemaV1.Issue[];
constructor({
schema,
input,
issues,
}: ValidationError.Props) {
super();
this.schema = schema;
this.input = input;
this.issues = issues;
}
}