Database errors
把 Drizzle ORM 的底层 driver 错误翻译成有意义的 HTTP 状态码
Elogs 自带 Drizzle ORM 错误翻译器。driver 抛出来的 DrizzleError / DrizzleQueryError(携带 code 字段,比如 PG 的 23505、MySQL 的 ER_DUP_ENTRY)会被翻译成有 HTTP status 的 Error 实例,让 access log 自动落到正确的级别 —— 唯一约束冲突记 WARNING,连接错误记 ERROR。
翻译器只决定日志级别和内容,不劫持错误响应格式;响应仍由 Elysia 2 原生 .error("DrizzleError", ...) 或默认 application/problem+json 处理。
三种使用方式
| 方式 | 配置入口 | 适用场景 |
|---|---|---|
| 手动 | translateDrizzleError(err) 在 try/catch 里调 |
只在某条路由要处理 DB 错误 |
| 自动 | createElogs({ autoTranslate: { db: 'drizzle' } }) |
全局统一处理所有路由 |
| 混合 | autoTranslate.custom: [...] 在内置之前匹配 |
内置翻译器不够用,自己追加规则 |
方式 1
@eastgold15/elogs/translator 子路径导出了 translateDrizzleError 和 isDrizzleError。isDrizzleError 是类型守卫,避免误把普通 Error 当作 Drizzle 错误处理。
import { Elysia } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
import { translateDrizzleError } from '@eastgold15/elogs/translator'
const app = new Elysia()
.use(createElogs())
.post('/users', async ({ body }) => {
try {
return await db.insert(users).values(body).returning()
} catch (error) {
// try/catch 兜住 driver 错误,自己决定怎么翻译
throw translateDrizzleError(error)
}
})
适合“只在某条路由要处理 DB 错误”的场景,不影响其他路由。
方式 2
把翻译逻辑放到 createElogs 配置里,所有路由共用一份。onError 钩子跑翻译器,翻译结果决定日志级别,但不替换原 error。
import { Elysia, problem } from 'elysia'
import { createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(
createElogs({
autoTranslate: { db: 'drizzle' },
})
)
// 用户用 Elysia 2 原生 .error() 接管 DrizzleError 的响应格式
.error('DrizzleError', ({ code }) => {
if (code === '23505') return problem(409, { detail: 'Duplicate key' })
if (code === '23503') return problem(400, { detail: 'Foreign key violation' })
return problem(500, { detail: 'Unhandled DB error' })
})
.post('/users', async ({ body }) => {
// 抛原 DrizzleError,翻译在 onError 钩子里自动跑
return await db.insert(users).values(body).returning()
})
.get('/health/db', async () => {
return await db.select().from(users).limit(1)
})
路由本身只写业务代码,翻译自动接管。
方式 3
autoTranslate.custom 在内置翻译器之前匹配,可以覆盖内置行为或追加全新的错误类型。custom 命中后短路,内置不再跑。
import { Elysia, problem } from 'elysia'
import { httpError, createElogs } from '@eastgold15/elogs'
const app = new Elysia()
.use(
createElogs({
autoTranslate: {
db: 'drizzle',
custom: [
// 业务规则:某张表的 23505 不该返回 409
{
canHandle: (e) =>
typeof e === 'object' &&
e !== null &&
(e as { code?: string }).code === '23505' &&
(e as { table?: string }).table === 'audit_log',
translate: () =>
httpError(202, 'Duplicate audit event — accepted as idempotent'),
},
// 业务规则:把 40001(序列化失败)翻译成 503
{
canHandle: (e) =>
typeof e === 'object' &&
e !== null &&
(e as { code?: string }).code === '40001',
translate: () => httpError(503, 'Transaction conflict — retry'),
},
],
},
})
)
如果 custom 没传,等价于“内置翻译器直接跑”;如果传了但 canHandle 返回 false,内置翻译器兜底。
错误码映射表
translateDrizzleError 内置了 PG / MySQL / SQLite 三大主流 driver 的常见错误码。判断依据是 error.name === 'DrizzleError' || error.name === 'DrizzleQueryError' 且 error.code 命中下表。
| 类别 | 数据库 | 错误码 | HTTP 状态 | 翻译后的 message |
|---|---|---|---|---|
| 唯一约束冲突 | PostgreSQL | 23505 |
409 | Unique constraint violation |
| 唯一约束冲突 | MySQL | ER_DUP_ENTRY |
409 | Duplicate entry |
| 唯一约束冲突 | SQLite | SQLITE_CONSTRAINT_UNIQUE |
409 | Unique violation |
| 外键违反 | PostgreSQL | 23503 |
400 | Foreign key violation |
| 外键违反 | MySQL | ER_NO_REFERENCED_ROW_2 |
400 | Foreign key violation |
| 外键违反 | SQLite | SQLITE_CONSTRAINT_FOREIGNKEY |
400 | Foreign key violation |
| NOT NULL 违反 | PostgreSQL | 23502 |
422 | Required field missing |
| CHECK 约束 | PostgreSQL | 23514 |
422 | Constraint check failed |
| 数据库连接错误 | PostgreSQL | 08000 08001 08003 08004 08006 08007 |
503 | Database unavailable |
状态码语义遵循 REST 惯例:
409 Conflict—— 资源状态冲突(唯一键、重复键)400 Bad Request—— 请求违反约束(外键引用了不存在的行)422 Unprocessable Entity—— 字段值不合法(NULL 缺失、CHECK 不通过)503 Service Unavailable—— 上游依赖不可用(连接断开)
未在上表中的错误码不会被翻译,原 DrizzleError 继续传播。
关键不变量
翻译器的设计严格遵守以下规则,这些规则在 __tests__/plugin/auto-translate.test.ts 里全部断言过:
- 翻译只决定日志级别和内容。翻译后的 error 决定
status→ 派生WARNING/ERROR;翻译后丢弃,原 error 继续以原形态传播。用户的.error("DrizzleError", fn)拿到的还是原 error 引用。 onError钩子不 return value。钩子只记录日志,错误继续向下游传播 —— 用户的.error(MyClass, fn)链路、Elysia 默认application/problem+json响应都不受影响。custom在内置之前匹配。translateDrizzleError(err, custom)内部顺序是[...custom, ...DRIZZLE_TRANSLATORS],custom 命中即短路。- 不命中则原样返回。如果是
Error实例直接返回(同一引用,===相等);如果是string/null/ 普通对象,包成new Error(String(e))兜底。 - 类型守卫只看
name。isDrizzleError只判断name === 'DrizzleError' || 'DrizzleQueryError',不依赖drizzle-orm的类型导入,所以翻译器可以独立子路径导出。
边界情况
未知错误码
translateDrizzleError({ name: 'DrizzleError', code: '99999_UNKNOWN' })
// → 原 error 原样返回
未在映射表里的 code 不会被翻译,日志级别会按原 error 的 status(如果存在)决定,没有则 ERROR。
非 Drizzle 错误
translateDrizzleError(new Error('plain boom')) // → 原 Error 返回
translateDrizzleError('oops') // → new Error('oops')
translateDrizzleError(null) // → new Error('null')
translateDrizzleError({ code: '23505', name: 'Other' })// → new Error('[object Object]')
isDrizzleError 拒绝这些值(只看 name),所以不会走内置翻译器,直接兜底返回。
custom 命中但内置不命中
translateDrizzleError({ name: 'MyError', code: 'X1' }, [
{
canHandle: (e) => (e as { code?: string }).code === 'X1',
translate: () => httpError(422, 'Custom handled'),
},
])
// → httpError(422, 'Custom handled')
custom 命中,内置不参与。
httpError vs translateDrizzleError
两者都在 @eastgold15/elogs 主包和 translator 子路径下分别暴露,但用途不同:
| API | 路径 | 用途 |
|---|---|---|
httpError(status, message) |
@eastgold15/elogs |
手动构造一个 HTTPError(响应 + 日志同时生效) |
translateDrizzleError(error) |
@eastgold15/elogs/translator |
翻译一个已知的 Drizzle 错误(只决定日志级别) |
httpError 是主动抛错,translateDrizzleError 是被动翻译(路由里 try/catch 兜住 driver 错误后再翻译)。
翻译器是纯函数
translateDrizzleError 不依赖 drizzle-orm 的类型,只看 error.name 和 error.code 两个字段。可以直接用任何来源的 Drizzle 错误(比如 mock 的、第三方包抛的),不必真的安装 drizzle-orm。
API 参考
translateDrizzleError— 翻译一个 errorisDrizzleError— Drizzle 错误类型守卫DrizzleLikeError— Drizzle 错误最小形状AutoTranslateConfig—autoTranslate配置形状ErrorTranslator— 自定义翻译器形状