[Snow Team] express-types: auto-commit work
This commit is contained in:
+140
-113
@@ -1,33 +1,34 @@
|
||||
import { logError, logWarn } from './logger.js'
|
||||
import type { Request, Response } from "express";
|
||||
import { logError, logWarn } from "./logger.js";
|
||||
|
||||
const RESPONSE_TIME_ZONE = 'Asia/Shanghai'
|
||||
const RESPONSE_TIME_ZONE = "Asia/Shanghai";
|
||||
|
||||
export type HttpErrorLike = {
|
||||
statusCode?: number
|
||||
errorCode?: string
|
||||
cause?: unknown
|
||||
context?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
statusCode?: number;
|
||||
errorCode?: string;
|
||||
cause?: unknown;
|
||||
context?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type CreateHttpErrorOptions = {
|
||||
statusCode?: number
|
||||
errorCode?: string
|
||||
cause?: unknown
|
||||
context?: unknown
|
||||
}
|
||||
statusCode?: number;
|
||||
errorCode?: string;
|
||||
cause?: unknown;
|
||||
context?: unknown;
|
||||
};
|
||||
|
||||
export function buildSuccessPayload(data, msg = 'ok') {
|
||||
export function buildSuccessPayload(data: unknown, msg: string = "ok") {
|
||||
return {
|
||||
code: 0,
|
||||
msg,
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
data: normalizeResponseDateTime(data),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function buildErrorPayload(error, fallbackMessage) {
|
||||
const classification = classifyRouteError(error)
|
||||
export function buildErrorPayload(error: unknown, fallbackMessage: string) {
|
||||
const classification = classifyRouteError(error);
|
||||
|
||||
return {
|
||||
code: 1,
|
||||
@@ -35,214 +36,240 @@ export function buildErrorPayload(error, fallbackMessage) {
|
||||
errorCode: classification.errorCode,
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
data: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function sendRouteError(res, error, fallbackMessage, scope) {
|
||||
const classification = classifyRouteError(error)
|
||||
const logger = classification.statusCode >= 500 ? logError : logWarn
|
||||
export function sendRouteError(
|
||||
res: Response,
|
||||
error: unknown,
|
||||
fallbackMessage: string,
|
||||
scope: string
|
||||
): void {
|
||||
const classification = classifyRouteError(error);
|
||||
const logger = classification.statusCode >= 500 ? logError : logWarn;
|
||||
logger(scope, fallbackMessage, {
|
||||
statusCode: classification.statusCode,
|
||||
errorCode: classification.errorCode,
|
||||
error,
|
||||
})
|
||||
res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage))
|
||||
});
|
||||
res
|
||||
.status(classification.statusCode)
|
||||
.json(buildErrorPayload(error, fallbackMessage));
|
||||
}
|
||||
|
||||
export function buildNotFoundPayload(req) {
|
||||
export function buildNotFoundPayload(req: Request) {
|
||||
return {
|
||||
code: 1,
|
||||
msg: `未实现接口: ${req.method} ${req.originalUrl}`,
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
data: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createHttpError(message: string, options: CreateHttpErrorOptions = {}): Error & HttpErrorLike {
|
||||
const { statusCode = 500, errorCode = '', cause, context } = options
|
||||
const error = new Error(message) as Error & HttpErrorLike
|
||||
error.statusCode = statusCode
|
||||
error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode)
|
||||
export function createHttpError(
|
||||
message: string,
|
||||
options: CreateHttpErrorOptions = {}
|
||||
): Error & HttpErrorLike {
|
||||
const { statusCode = 500, errorCode = "", cause, context } = options;
|
||||
const error = new Error(message) as Error & HttpErrorLike;
|
||||
error.statusCode = statusCode;
|
||||
error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode);
|
||||
|
||||
if (typeof cause !== 'undefined') {
|
||||
error.cause = cause
|
||||
if (typeof cause !== "undefined") {
|
||||
error.cause = cause;
|
||||
}
|
||||
|
||||
if (typeof context !== 'undefined') {
|
||||
error.context = context
|
||||
if (typeof context !== "undefined") {
|
||||
error.context = context;
|
||||
}
|
||||
|
||||
return error
|
||||
return error;
|
||||
}
|
||||
|
||||
function classifyRouteError(error) {
|
||||
if (error && typeof error === 'object') {
|
||||
const currentError = error as HttpErrorLike
|
||||
const statusCode = Number(currentError.statusCode)
|
||||
const errorCode = String(currentError.errorCode || '').trim()
|
||||
function classifyRouteError(error: unknown): {
|
||||
statusCode: number;
|
||||
errorCode: string;
|
||||
} {
|
||||
if (error && typeof error === "object") {
|
||||
const currentError = error as HttpErrorLike;
|
||||
const statusCode = Number(currentError.statusCode);
|
||||
const errorCode = String(currentError.errorCode || "").trim();
|
||||
|
||||
if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) {
|
||||
return {
|
||||
statusCode,
|
||||
errorCode: errorCode || defaultErrorCodeForStatus(statusCode),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error || '')
|
||||
const message = error instanceof Error ? error.message : String(error || "");
|
||||
|
||||
if (message.includes('缺少') || message.includes('无效') || message.includes('验签失败')) {
|
||||
return { statusCode: 400, errorCode: 'invalid_request' }
|
||||
if (
|
||||
message.includes("缺少") ||
|
||||
message.includes("无效") ||
|
||||
message.includes("验签失败")
|
||||
) {
|
||||
return { statusCode: 400, errorCode: "invalid_request" };
|
||||
}
|
||||
|
||||
if (message.includes('不存在')) {
|
||||
return { statusCode: 404, errorCode: 'not_found' }
|
||||
if (message.includes("不存在")) {
|
||||
return { statusCode: 404, errorCode: "not_found" };
|
||||
}
|
||||
|
||||
if (message.includes('已关闭')) {
|
||||
return { statusCode: 410, errorCode: 'gone' }
|
||||
if (message.includes("已关闭")) {
|
||||
return { statusCode: 410, errorCode: "gone" };
|
||||
}
|
||||
|
||||
if (message.includes('不能') || message.includes('冲突')) {
|
||||
return { statusCode: 409, errorCode: 'conflict' }
|
||||
if (message.includes("不能") || message.includes("冲突")) {
|
||||
return { statusCode: 409, errorCode: "conflict" };
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes('OCR') ||
|
||||
message.includes('uv sync') ||
|
||||
message.includes('spawn uv ENOENT') ||
|
||||
message.includes("OCR") ||
|
||||
message.includes("uv sync") ||
|
||||
message.includes("spawn uv ENOENT") ||
|
||||
message.includes("Executable doesn't exist") ||
|
||||
message.includes('please run the following command to download new browsers')
|
||||
message.includes(
|
||||
"please run the following command to download new browsers"
|
||||
)
|
||||
) {
|
||||
return { statusCode: 503, errorCode: 'dependency_unavailable' }
|
||||
return { statusCode: 503, errorCode: "dependency_unavailable" };
|
||||
}
|
||||
|
||||
return { statusCode: 500, errorCode: 'internal_error' }
|
||||
return { statusCode: 500, errorCode: "internal_error" };
|
||||
}
|
||||
|
||||
function defaultErrorCodeForStatus(statusCode) {
|
||||
function defaultErrorCodeForStatus(statusCode: number): string {
|
||||
if (statusCode === 400) {
|
||||
return 'invalid_request'
|
||||
return "invalid_request";
|
||||
}
|
||||
|
||||
if (statusCode === 401) {
|
||||
return 'unauthorized'
|
||||
return "unauthorized";
|
||||
}
|
||||
|
||||
if (statusCode === 403) {
|
||||
return 'forbidden'
|
||||
return "forbidden";
|
||||
}
|
||||
|
||||
if (statusCode === 404) {
|
||||
return 'not_found'
|
||||
return "not_found";
|
||||
}
|
||||
|
||||
if (statusCode === 409) {
|
||||
return 'conflict'
|
||||
return "conflict";
|
||||
}
|
||||
|
||||
if (statusCode === 410) {
|
||||
return 'gone'
|
||||
return "gone";
|
||||
}
|
||||
|
||||
if (statusCode === 503) {
|
||||
return 'dependency_unavailable'
|
||||
return "dependency_unavailable";
|
||||
}
|
||||
|
||||
return 'internal_error'
|
||||
return "internal_error";
|
||||
}
|
||||
|
||||
function normalizeResponseDateTime(value) {
|
||||
function normalizeResponseDateTime(value: unknown): unknown {
|
||||
if (value instanceof Date) {
|
||||
return formatResponseDateTime(value.toISOString())
|
||||
return formatResponseDateTime(value.toISOString());
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeResponseDateTime(item))
|
||||
return value.map((item) => normalizeResponseDateTime(item));
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, currentValue]) => [key, normalizeResponseDateTime(currentValue)]),
|
||||
)
|
||||
Object.entries(value).map(([key, currentValue]) => [
|
||||
key,
|
||||
normalizeResponseDateTime(currentValue),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (isIsoDateTimeString(value)) {
|
||||
return formatResponseDateTime(value)
|
||||
return formatResponseDateTime(value as string);
|
||||
}
|
||||
|
||||
return value
|
||||
return value;
|
||||
}
|
||||
|
||||
function isIsoDateTimeString(value) {
|
||||
function isIsoDateTimeString(value: unknown): boolean {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value.trim())
|
||||
)
|
||||
typeof value === "string" &&
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(
|
||||
value.trim()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function formatResponseDateTime(value) {
|
||||
const date = new Date(value)
|
||||
function formatResponseDateTime(value: string): string {
|
||||
const date = new Date(value);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value || '').replace('T', ' ').replace('Z', '')
|
||||
return String(value || "")
|
||||
.replace("T", " ")
|
||||
.replace("Z", "");
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat('zh-CN', {
|
||||
const parts = new Intl.DateTimeFormat("zh-CN", {
|
||||
timeZone: RESPONSE_TIME_ZONE,
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).formatToParts(date)
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).formatToParts(date);
|
||||
|
||||
const tokens = Object.fromEntries(
|
||||
parts
|
||||
.filter((item) => item.type !== 'literal')
|
||||
.map((item) => [item.type, item.value]),
|
||||
)
|
||||
.filter((item) => item.type !== "literal")
|
||||
.map((item) => [item.type, item.value])
|
||||
);
|
||||
|
||||
return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`
|
||||
return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`;
|
||||
}
|
||||
|
||||
type RouteHandlerOptions = {
|
||||
successMessage?: string
|
||||
errorMessage?: string
|
||||
scope?: string
|
||||
}
|
||||
successMessage?: string;
|
||||
errorMessage?: string;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
type RouteFileHandlerOptions = {
|
||||
errorMessage?: string
|
||||
scope?: string
|
||||
}
|
||||
errorMessage?: string;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
export function createRouteHandler(
|
||||
action: (req: any, res: any) => unknown | Promise<unknown>,
|
||||
{ successMessage = 'ok', errorMessage, scope }: RouteHandlerOptions = {},
|
||||
action: (req: Request, res: Response) => unknown | Promise<unknown>,
|
||||
{ successMessage = "ok", errorMessage, scope }: RouteHandlerOptions = {}
|
||||
) {
|
||||
return async (req, res) => {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const data = await action(req, res)
|
||||
res.json(buildSuccessPayload(data, successMessage))
|
||||
const data = await action(req, res);
|
||||
res.json(buildSuccessPayload(data, successMessage));
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, errorMessage, scope)
|
||||
sendRouteError(res, error, errorMessage, scope);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createRouteFileHandler(
|
||||
action: (req: any, res: any) => string | Promise<string>,
|
||||
{ errorMessage, scope }: RouteFileHandlerOptions = {},
|
||||
action: (req: Request, res: Response) => string | Promise<string>,
|
||||
{ errorMessage, scope }: RouteFileHandlerOptions = {}
|
||||
) {
|
||||
return async (req, res) => {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const filePath = await action(req, res)
|
||||
res.sendFile(filePath)
|
||||
const filePath = await action(req, res);
|
||||
res.sendFile(filePath);
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, errorMessage, scope)
|
||||
sendRouteError(res, error, errorMessage, scope);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user