import type { ApiErrorCode } from "@umt/contracts"; import type { z } from "zod"; import { ApiRequestError, apiFetch, type FetcherArg } from "~/lib/api.ts"; export { ApiRequestError }; /** 请求并用 contracts 的 zod schema 校验 data;失败抛 ApiRequestError(给 react-query 的 error 态) */ export async function apiParsed(schema: S, path: string, arg?: FetcherArg): Promise> { const r = await apiFetch(path, arg); if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message); const parsed = schema.safeParse(r.data); if (!parsed.success) { const issue = parsed.error.issues[0]; throw new ApiRequestError( "internal" satisfies ApiErrorCode, `响应格式不符合接口定义:${issue ? `${issue.path.join(".")} ${issue.message}` : "未知字段"}`, ); } return parsed.data; } /** 变更请求:返回 data,失败抛错(给 useMutation,由 onError 统一 toast) */ export async function apiMutate(path: string, arg: FetcherArg = {}): Promise { const r = await apiFetch(path, { method: "POST", ...arg }); if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message); return r.data; } export const errorCode = (err: unknown): string | null => (err instanceof ApiRequestError ? err.code : null); /** 端点是否不存在(后端尚未提供) */ export const isMissingEndpoint = (err: unknown) => errorCode(err) === "not_found"; /** 查询字符串:去掉空值 */ export function cleanParams(p: Record) { const out: Record = {}; for (const [k, v] of Object.entries(p)) if (v !== undefined && v !== null && v !== "") out[k] = v; return out; } /** 小规模配置集合:跟随游标取齐,保留列表返回形状供选择器与本地分页共用。 */ export async function fetchAllCursorPages

( fetchPage: (cursor?: string) => Promise

, ): Promise

{ const first = await fetchPage(); const items = [...first.items]; const seen = new Set(); let cursor = first.nextCursor; while (cursor) { if (seen.has(cursor)) throw new ApiRequestError("internal", "分页游标重复。重试"); seen.add(cursor); const page = await fetchPage(cursor); items.push(...page.items); cursor = page.nextCursor; } return { ...first, items, nextCursor: null }; }