117 lines
3.3 KiB
TypeScript
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;
|
|
}
|
|
}
|