Files
moteva/frontend/src/infrastructure/userMessages.ts
T

187 lines
6.6 KiB
TypeScript

export const USER_MESSAGE_KEYS = {
unknown: "messages.common.unknown",
network: "messages.network.unavailable",
badRequest: "messages.http.bad_request",
unauthorized: "messages.http.unauthorized",
forbidden: "messages.http.forbidden",
notFound: "messages.http.not_found",
conflict: "messages.http.conflict",
payloadTooLarge: "messages.http.payload_too_large",
timeout: "messages.http.timeout",
tooManyRequests: "messages.http.too_many_requests",
server: "messages.http.server",
promptRequired: "messages.validation.prompt_required",
authLoginRequired: "messages.auth.login_required",
authCodeInvalid: "messages.auth.code_invalid",
authCodeExpired: "messages.auth.code_expired",
authInvalidCredentials: "messages.auth.invalid_credentials",
authInvalidRegion: "messages.auth.invalid_region",
authProviderNotReady: "messages.auth.provider_not_ready",
authHumanVerificationRequired: "messages.auth.human_verification_required",
authHumanVerificationFailed: "messages.auth.human_verification_failed",
shareSelfInvite: "messages.share.self_invite",
shareMemberExists: "messages.share.member_exists"
} as const;
const HTTP_STATUS_MESSAGE_KEYS: Record<number, string> = {
400: USER_MESSAGE_KEYS.badRequest,
401: USER_MESSAGE_KEYS.unauthorized,
403: USER_MESSAGE_KEYS.forbidden,
404: USER_MESSAGE_KEYS.notFound,
408: USER_MESSAGE_KEYS.timeout,
409: USER_MESSAGE_KEYS.conflict,
413: USER_MESSAGE_KEYS.payloadTooLarge,
429: USER_MESSAGE_KEYS.tooManyRequests
};
const BACKEND_STATUS_MESSAGE_KEYS: Record<string, string> = {
"auth.login_required": USER_MESSAGE_KEYS.authLoginRequired,
"auth.code_invalid": USER_MESSAGE_KEYS.authCodeInvalid,
"auth.code_expired": USER_MESSAGE_KEYS.authCodeExpired,
"auth.invalid_credentials": USER_MESSAGE_KEYS.authInvalidCredentials,
"auth.invalid_region": USER_MESSAGE_KEYS.authInvalidRegion,
"auth.provider_not_ready": USER_MESSAGE_KEYS.authProviderNotReady,
"auth.human_verification_required": USER_MESSAGE_KEYS.authHumanVerificationRequired,
"auth.human_verification_failed": USER_MESSAGE_KEYS.authHumanVerificationFailed,
"share.self_invite": USER_MESSAGE_KEYS.shareSelfInvite,
"share.member_exists": USER_MESSAGE_KEYS.shareMemberExists
};
const RAW_STATUS_MESSAGE_KEYS: Array<{ pattern: RegExp; key: string }> = [
{ pattern: /verification code is invalid/i, key: USER_MESSAGE_KEYS.authCodeInvalid },
{ pattern: /verification code is expired/i, key: USER_MESSAGE_KEYS.authCodeExpired },
{ pattern: /login required/i, key: USER_MESSAGE_KEYS.authLoginRequired },
{ pattern: /prompt is required/i, key: USER_MESSAGE_KEYS.promptRequired }
];
export type BackendStatus = string | number;
export type BackendStatusBody = {
error?: string;
message?: string;
errorCode?: string;
code?: number;
requestId?: string;
traceId?: string;
};
export type RequestDomain = "auth" | "design" | "api";
export type Translate = (key: string, values?: Record<string, string | number>) => string;
export class ApiFailure extends Error {
status: number;
backendStatus: BackendStatus;
userMessageKey: string;
rawMessage: string;
body: unknown;
domain: RequestDomain;
requestId?: string;
traceId?: string;
constructor(domain: RequestDomain, status: number, statusText: string, body: unknown) {
const parsed = parseStatusBody(body);
const backendStatus = parsed.errorCode ?? parsed.code ?? status;
const userMessageKey = resolveUserMessageKey(backendStatus, status, parsed.rawMessage);
super(userMessageKey);
this.name = "ApiFailure";
this.domain = domain;
this.status = status;
this.backendStatus = backendStatus;
this.userMessageKey = userMessageKey;
this.rawMessage = parsed.rawMessage || statusText;
this.body = body;
this.requestId = parsed.requestId;
this.traceId = parsed.traceId;
}
}
export class NetworkFailure extends Error {
backendStatus = "network.unavailable";
userMessageKey = USER_MESSAGE_KEYS.network;
domain: RequestDomain;
cause?: unknown;
constructor(domain: RequestDomain, cause?: unknown) {
super(USER_MESSAGE_KEYS.network);
this.name = "NetworkFailure";
this.domain = domain;
this.cause = cause;
}
}
export function createApiFailure(domain: RequestDomain, response: Response, body: unknown) {
return new ApiFailure(domain, response.status, response.statusText, body);
}
export function createNetworkFailure(domain: RequestDomain, cause?: unknown) {
return new NetworkFailure(domain, cause);
}
export function backendStatusOf(value: unknown): BackendStatus | undefined {
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
return value.backendStatus;
}
return undefined;
}
export function userMessageKeyOf(value: unknown) {
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
return value.userMessageKey;
}
return USER_MESSAGE_KEYS.unknown;
}
export function httpStatusOf(value: unknown) {
return value instanceof ApiFailure ? value.status : undefined;
}
export function isBackendStatus(value: unknown, status: BackendStatus) {
return backendStatusOf(value) === status;
}
export function isUserMessageKey(value: unknown, key: string) {
return userMessageKeyOf(value) === key;
}
export function toUserMessage(value: unknown, t: Translate, fallbackKey: string = USER_MESSAGE_KEYS.unknown) {
const key = userMessageKeyOf(value);
const message = t(key);
if (message !== key) return message;
return t(fallbackKey);
}
function parseStatusBody(body: unknown) {
if (!body || typeof body !== "object") {
return {};
}
const data = body as BackendStatusBody;
return {
rawMessage: stringValue(data.message) || stringValue(data.error),
errorCode: stringValue(data.errorCode),
code: typeof data.code === "number" ? data.code : undefined,
requestId: stringValue(data.requestId),
traceId: stringValue(data.traceId)
};
}
function resolveUserMessageKey(backendStatus: BackendStatus, httpStatus: number, rawMessage: string | undefined) {
if (typeof backendStatus === "string" && BACKEND_STATUS_MESSAGE_KEYS[backendStatus]) {
return BACKEND_STATUS_MESSAGE_KEYS[backendStatus];
}
for (const item of RAW_STATUS_MESSAGE_KEYS) {
if (item.pattern.test(rawMessage ?? "")) return item.key;
}
if (typeof backendStatus === "number" && HTTP_STATUS_MESSAGE_KEYS[backendStatus]) {
return HTTP_STATUS_MESSAGE_KEYS[backendStatus];
}
if (HTTP_STATUS_MESSAGE_KEYS[httpStatus]) {
return HTTP_STATUS_MESSAGE_KEYS[httpStatus];
}
return httpStatus >= 500 ? USER_MESSAGE_KEYS.server : USER_MESSAGE_KEYS.unknown;
}
function stringValue(value: unknown) {
return typeof value === "string" && value.trim() ? value : undefined;
}