mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 03:23:20 +00:00
Dispatchers need the design surface from the column and the detail, and an optional evidence file is sent only after the scan passes.
188 lines
5.7 KiB
TypeScript
188 lines
5.7 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);
|
|
}
|
|
|
|
/** Ky's beforeRequest sees request.body as a stream; check options.body for FormData. */
|
|
export function shouldStripJsonContentType(body: unknown): body is FormData {
|
|
return body instanceof FormData;
|
|
}
|
|
|
|
export const api: KyInstance = ky.create({
|
|
prefix: resolveApiPrefixUrl(env.apiUrl),
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
hooks: {
|
|
beforeRequest: [
|
|
({ request, options }) => {
|
|
const token = getAccessToken();
|
|
if (token) {
|
|
request.headers.set("Authorization", token);
|
|
}
|
|
// why: the instance default is application/json. Deleting that header after
|
|
// Request construction drops the multipart boundary, so the API never sees `file`.
|
|
// Rebuild from the original FormData with no Content-Type so the runtime sets
|
|
// multipart/form-data including the boundary.
|
|
if (shouldStripJsonContentType(options.body)) {
|
|
request.headers.delete("Content-Type");
|
|
return new Request(request, { body: options.body });
|
|
}
|
|
},
|
|
],
|
|
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 };
|