mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
Also drops ticket keys from shipped comments and test names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
import ky, { type KyInstance, type Options } from "ky";
|
|
import { env } from "@/lib/env";
|
|
import { resolveApiPrefixUrl } from "@/lib/resolve-api-prefix-url";
|
|
import { API_PATHS } from "@/api/api-paths";
|
|
import { mapHttpStatusToMessage, normalizeApiRequestError } from "@/api/api-error";
|
|
import { getAccessToken } from "@/lib/auth/auth-storage";
|
|
import { expireSessionAndRedirectToLogin } from "@/lib/auth/expire-session";
|
|
import { traceHttpOperation } from "@/observability/http-transaction";
|
|
|
|
function isAuthLoginRequest(request: Request): boolean {
|
|
return request.url.includes(API_PATHS.authentication.login);
|
|
}
|
|
|
|
export const api: KyInstance = ky.create({
|
|
prefix: resolveApiPrefixUrl(env.apiUrl),
|
|
// hazard: no instance-wide Content-Type. Ky sets application/json for `json` bodies, and the
|
|
// runtime must set `multipart/form-data; boundary=…` for FormData while it builds the Request.
|
|
// A default here is baked into that Request; deleting it in a hook leaves the multipart body
|
|
// with no Content-Type at all, so ASP.NET cannot bind [FromForm] fields.
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
hooks: {
|
|
beforeRequest: [
|
|
({ request }) => {
|
|
const token = getAccessToken();
|
|
if (token) {
|
|
request.headers.set("Authorization", token);
|
|
}
|
|
},
|
|
],
|
|
beforeError: [({ error }) => normalizeApiRequestError(error)],
|
|
afterResponse: [
|
|
({ request, response }) => {
|
|
if (response.status !== 401 || isAuthLoginRequest(request)) {
|
|
return response;
|
|
}
|
|
|
|
expireSessionAndRedirectToLogin();
|
|
return response;
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
export type ApiRawMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
|
|
export async function apiRequestRaw(
|
|
method: ApiRawMethod,
|
|
url: string,
|
|
options?: Options,
|
|
entryPoint?: string,
|
|
): Promise<Response> {
|
|
return traceHttpOperation(
|
|
{
|
|
method: method.toUpperCase(),
|
|
url,
|
|
entryPoint: entryPoint ?? `api.${method}`,
|
|
},
|
|
async ({ reportStatus }) => {
|
|
const response = await api[method](url, options);
|
|
reportStatus(response.status);
|
|
return response;
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiGet<T>(url: string, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "GET", url, entryPoint: "api.apiGet" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.get(url, options);
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiPost<T>(url: string, body?: unknown, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "POST", url, entryPoint: "api.apiPost" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.post(url, { ...options, json: body });
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiPostForm<T>(url: string, body: FormData, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "POST", url, entryPoint: "api.apiPostForm" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.post(url, { ...options, body });
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiPut<T>(url: string, body?: unknown, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "PUT", url, entryPoint: "api.apiPut" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.put(url, { ...options, json: body });
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiPatch<T>(url: string, body?: unknown, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "PATCH", url, entryPoint: "api.apiPatch" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.patch(url, { ...options, json: body });
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiDelete<T>(url: string, options?: Options): Promise<T> {
|
|
return traceHttpOperation(
|
|
{ method: "DELETE", url, entryPoint: "api.apiDelete" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.delete(url, options);
|
|
reportStatus(response.status);
|
|
return response.json<T>();
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiPostNoContent(
|
|
url: string,
|
|
body?: unknown,
|
|
options?: Options,
|
|
): Promise<void> {
|
|
return traceHttpOperation(
|
|
{ method: "POST", url, entryPoint: "api.apiPostNoContent" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.post(url, { ...options, json: body });
|
|
reportStatus(response.status);
|
|
if (response.status === 204) {
|
|
return;
|
|
}
|
|
|
|
const text = await response.text();
|
|
if (!text.trim()) {
|
|
return;
|
|
}
|
|
|
|
JSON.parse(text);
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function apiDeleteNoContent(url: string, options?: Options): Promise<void> {
|
|
return traceHttpOperation(
|
|
{ method: "DELETE", url, entryPoint: "api.apiDeleteNoContent" },
|
|
async ({ reportStatus }) => {
|
|
const response = await api.delete(url, options);
|
|
reportStatus(response.status);
|
|
if (response.status === 204) {
|
|
return;
|
|
}
|
|
|
|
const text = await response.text();
|
|
if (!text.trim()) {
|
|
return;
|
|
}
|
|
|
|
JSON.parse(text);
|
|
},
|
|
);
|
|
}
|
|
|
|
export { mapHttpStatusToMessage };
|