/** * 识别各驱动常见的「唯一 / 重复键」错误,供任意 service 复用。 * 具体业务(如用户名冲突 vs 主键重试)用 {@link uniqueConstraintTouchesField} 再区分即可。 */ /** 遍历包装错误(如 Drizzle)与底层驱动的整条 cause 链 */ export function* eachErrorInChain(err: unknown): Generator { let current: unknown = err; const seen = new Set(); while (current instanceof Error && !seen.has(current)) { seen.add(current); yield current; current = "cause" in current ? (current as { cause?: unknown }).cause : undefined; } } function escapeRegExp(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * 报错文案或 constraint 名是否涉及该字段。 * message 用词边界,避免 `guid` 误匹配 `id`;constraint 多为 `users_username_key` 式拼接,用子串匹配。 */ export function uniqueConstraintTouchesField(err: unknown, field: string): boolean { const fl = field.toLowerCase(); const re = new RegExp(`\\b${escapeRegExp(field)}\\b`, "i"); for (const e of eachErrorInChain(err)) { if (re.test(e.message)) { return true; } if ("constraint" in e) { const c = String((e as { constraint?: unknown }).constraint ?? "").toLowerCase(); if (c.includes(fl)) { return true; } } } return false; } export function isUniqueConstraintViolation(err: unknown): boolean { for (const e of eachErrorInChain(err)) { const code = "code" in e ? String((e as { code?: unknown }).code ?? "") : ""; const errno = "errno" in e ? (e as { errno?: unknown }).errno : undefined; if (code === "23505") { return true; } if (code === "ER_DUP_ENTRY") { return true; } if (typeof errno === "number" && errno === 1062) { return true; } if (code.startsWith("SQLITE_CONSTRAINT")) { return true; } const m = e.message.toLowerCase(); if (m.includes("unique constraint failed")) { return true; } if (m.includes("duplicate entry")) { return true; } if (m.includes("duplicate key") && m.includes("unique")) { return true; } } return false; } /** 唯一冲突且与指定字段相关(例如用户名占用) */ export function isUniqueConflictOnField(err: unknown, field: string): boolean { return isUniqueConstraintViolation(err) && uniqueConstraintTouchesField(err, field); } /** 唯一冲突且与指定字段无关(例如除 username 外仅有主键时的 id 撞车重试) */ export function isUniqueConflictExceptField(err: unknown, field: string): boolean { return isUniqueConstraintViolation(err) && !uniqueConstraintTouchesField(err, field); }