< Summary

Information
Class: typed.ts
Assembly: app.api
File(s): /home/runner/work/ClutterStock/ClutterStock/frontend/app/api/typed.ts
Tag: 58_25416222083
Line coverage
94%
Covered lines: 32
Uncovered lines: 2
Coverable lines: 34
Total lines: 148
Line coverage: 94.1%
Branch coverage
95%
Covered branches: 22
Total branches: 23
Branch coverage: 95.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/ClutterStock/ClutterStock/frontend/app/api/typed.ts

#LineLine coverage
 1import type { paths } from "~/api/types";
 2import { apiFetch } from "~/api/http";
 3import type { ProblemBody } from "~/api/problem";
 4
 5type HttpMethod = "get" | "post" | "put" | "delete";
 6
 7type PathsWithMethod<M extends HttpMethod> = {
 8  [P in keyof paths]: paths[P] extends Record<M, object> ? P : never;
 9}[keyof paths];
 10
 11type Op<P extends keyof paths, M extends HttpMethod> = P extends keyof paths
 12  ? M extends keyof paths[P]
 13    ? paths[P][M]
 14    : never
 15  : never;
 16
 17type RequestBody<O> = O extends {
 18  requestBody: { content: { "application/json": infer B } };
 19}
 20  ? B
 21  : never;
 22
 23type SuccessBody<O> = O extends { responses: infer R }
 24  ? R extends { 200: { content: { "application/json": infer S } } }
 25    ? S
 26    : R extends { 201: { content: { "application/json": infer S } } }
 27      ? S
 28      : R extends { 204: unknown }
 29        ? void
 30        : unknown
 31  : unknown;
 32
 33type PathParams<O> = O extends { parameters: { path: infer P } }
 34  ? P extends Record<string, unknown>
 35    ? P
 36    : never
 37  : never;
 38
 39type HasPathParams<O> = O extends { parameters: { path: Record<string, unknown> } }
 40  ? true
 41  : false;
 42
 43type HasBody<O> = O extends { requestBody: { content: unknown } } ? true : false;
 44
 45export interface CallOptions {
 46  ssrRequest?: Request;
 47  signal?: AbortSignal;
 48}
 49
 50type WithParams<P extends keyof paths, M extends HttpMethod> =
 51  HasPathParams<Op<P, M>> extends true
 52    ? { params: PathParams<Op<P, M>> }
 53    : { params?: never };
 54
 55type WithBody<P extends keyof paths, M extends HttpMethod> =
 56  HasBody<Op<P, M>> extends true
 57    ? { body: RequestBody<Op<P, M>> }
 58    : { body?: never };
 59
 60export type Options<P extends keyof paths, M extends HttpMethod> =
 61  CallOptions & WithParams<P, M> & WithBody<P, M>;
 62
 1063function substitutePath(path: string, params?: Record<string, unknown>): string {
 1064  if (!params) return path;
 665  return path.replace(/\{(\w+)\}/g, (_, key) => {
 666    const v = params[key];
 667    if (v === undefined || v === null) {
 068      throw new Error(`Missing path parameter "${key}" for ${path}`);
 69    }
 670    return encodeURIComponent(String(v));
 71  });
 72}
 73
 374async function parseProblem(response: Response): Promise<ProblemBody | null> {
 375  const ct = response.headers.get("content-type") ?? "";
 376  if (!ct.includes("problem+json") && !ct.includes("application/json")) return null;
 277  try {
 278    return (await response.json()) as ProblemBody;
 79  } catch {
 080    return null;
 81  }
 82}
 83
 1084async function call<P extends keyof paths, M extends HttpMethod>(
 85  method: M,
 86  path: P,
 87  opts?: Options<P, M>,
 88): Promise<SuccessBody<Op<P, M>>> {
 1089  const url = substitutePath(
 90    path as string,
 91    opts?.params as Record<string, unknown> | undefined,
 92  );
 93
 1094  const init: RequestInit = { method: method.toUpperCase(), signal: opts?.signal };
 1095  if (opts && "body" in opts && opts.body !== undefined) {
 396    init.headers = { "Content-Type": "application/json" };
 397    init.body = JSON.stringify(opts.body);
 98  }
 99
 10100  const response = await apiFetch(url, init, opts?.ssrRequest);
 101
 10102  if (!response.ok) {
 3103    const body = await parseProblem(response);
 3104    const fallback: ProblemBody = body ?? { title: response.statusText, status: response.status };
 105    // Throw a `Response` (not a custom Error) so RR7 serializes the body to the
 106    // client across SSR — `useRouteError` exposes the parsed body via `error.data`
 107    // and `isRouteErrorResponse(error)` returns true. Custom Error subclass
 108    // fields would be stripped by `serializeError`.
 3109    throw new Response(JSON.stringify(fallback), {
 110      status: response.status,
 111      statusText: typeof fallback.title === "string"
 112        ? fallback.title.slice(0, 200)
 113        : response.statusText,
 114      headers: { "Content-Type": "application/problem+json" },
 115    });
 116  }
 117
 7118  if (response.status === 204) return undefined as SuccessBody<Op<P, M>>;
 6119  return (await response.json()) as SuccessBody<Op<P, M>>;
 120}
 121
 6122export function get<P extends PathsWithMethod<"get">>(
 123  path: P,
 124  opts?: Options<P, "get">,
 125): Promise<SuccessBody<Op<P, "get">>> {
 6126  return call("get", path, opts);
 127}
 128
 2129export function post<P extends PathsWithMethod<"post">>(
 130  path: P,
 131  opts?: Options<P, "post">,
 132): Promise<SuccessBody<Op<P, "post">>> {
 2133  return call("post", path, opts);
 134}
 135
 1136export function put<P extends PathsWithMethod<"put">>(
 137  path: P,
 138  opts?: Options<P, "put">,
 139): Promise<SuccessBody<Op<P, "put">>> {
 1140  return call("put", path, opts);
 141}
 142
 1143export function del<P extends PathsWithMethod<"delete">>(
 144  path: P,
 145  opts?: Options<P, "delete">,
 146): Promise<SuccessBody<Op<P, "delete">>> {
 1147  return call("delete", path, opts);
 148}