- app.ts 单体路由拆分为 routes/ + helpers + middleware + static 独立模块 - 类型去重:CheckFailure/CheckResult 以 shared/api.ts 为唯一源头,收紧 phase 联合类型 - es-toolkit 替换:isPlainObject/isNil/isEmptyObject/isEqual/isError/Semaphore/groupBy - Bun 内置 API:Object.fromEntries 替代手写 headersToRecord - bun:sqlite 规范:prepare() → query() 利用内置缓存,避免 N+1 查询 - 新增 getLatestChecksMap/allGetTargetStats 批量查询方法 - 新增 backend-code-quality/api-route-separation/batch-data-queries 规范 - 补充 openspec/config.yaml 后端开发规范与 DEVELOPMENT.md 后端开发指引
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
import type { ApiErrorResponse, CheckFailure, CheckResult, HealthResponse, RuntimeMode } from "../shared/api";
|
|
import type { StoredCheckResult } from "./checker/types";
|
|
|
|
export function createApiError(error: string, status: number): ApiErrorResponse {
|
|
return { error, status };
|
|
}
|
|
|
|
export function createHeaders(mode: RuntimeMode, init: HeadersInit): Headers {
|
|
const headers = new Headers(init);
|
|
|
|
if (mode === "production") {
|
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
export function jsonResponse(
|
|
body: unknown,
|
|
options: { method?: string; mode: RuntimeMode; status?: number; headers?: HeadersInit },
|
|
): Response {
|
|
const headers = createHeaders(options.mode, {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
...options.headers,
|
|
});
|
|
const responseBody = options.method === "HEAD" ? null : JSON.stringify(body);
|
|
|
|
return new Response(responseBody, {
|
|
status: options.status,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
export function methodNotAllowedResponse(allow: string[], mode: RuntimeMode): Response {
|
|
return jsonResponse(createApiError("Method not allowed", 405), {
|
|
mode,
|
|
status: 405,
|
|
headers: { Allow: allow.join(", ") },
|
|
});
|
|
}
|
|
|
|
export function allowsGetHead(method: string): boolean {
|
|
return method === "GET" || method === "HEAD";
|
|
}
|
|
|
|
export function formatDuration(ms: number): string {
|
|
if (ms >= 60000 && ms % 60000 === 0) return `${ms / 60000}m`;
|
|
if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`;
|
|
return `${ms}ms`;
|
|
}
|
|
|
|
export function mapCheckResult(row: StoredCheckResult): CheckResult {
|
|
let failure: CheckFailure | null = null;
|
|
if (row.failure) {
|
|
try {
|
|
failure = JSON.parse(row.failure) as CheckFailure;
|
|
} catch {
|
|
console.warn(`无法解析 failure 数据: target_id=${row.target_id}, timestamp=${row.timestamp}`);
|
|
failure = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
timestamp: row.timestamp,
|
|
matched: row.matched === 1,
|
|
durationMs: row.duration_ms,
|
|
statusDetail: row.status_detail,
|
|
failure,
|
|
};
|
|
}
|
|
|
|
export function createHealthResponse(): HealthResponse {
|
|
return {
|
|
ok: true,
|
|
service: "dial-server",
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
}
|