Skip to content

Commit 43c19cb

Browse files
authored
fix(drizzle): unique field errors were not thrown as ValidationErrors (#15146)
The optimized upsert path in `upsertRow` was not wrapped in the error handling try/catch block, causing unique constraint violations to throw raw database errors instead of Payload `ValidationError`s. **Changes:** - Created `handleUpsertError.ts` utility to centralize unique constraint error handling, similar to how db-mongodb as `handleError` - Wrapped optimized upsert path in error handling - Added test for unique constraint errors in optimized update path
1 parent cd77e5d commit 43c19cb

7 files changed

Lines changed: 393 additions & 276 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { PayloadRequest } from 'payload'
2+
3+
import { ValidationError } from 'payload'
4+
5+
import type { DrizzleAdapter } from '../types.js'
6+
7+
type HandleUpsertErrorArgs = {
8+
adapter: DrizzleAdapter
9+
error: unknown
10+
id?: number | string
11+
req?: Partial<PayloadRequest>
12+
tableName: string
13+
}
14+
15+
/**
16+
* Handles unique constraint violation errors from PostgreSQL and SQLite,
17+
* converting them to Payload ValidationErrors.
18+
* Re-throws non-constraint errors unchanged.
19+
*/
20+
export const handleUpsertError = ({
21+
id,
22+
adapter,
23+
error: caughtError,
24+
req,
25+
tableName,
26+
}: HandleUpsertErrorArgs): never => {
27+
let error: any = caughtError
28+
if (typeof caughtError === 'object' && caughtError !== null && 'cause' in caughtError) {
29+
error = caughtError.cause
30+
}
31+
32+
// PostgreSQL: 23505, SQLite: SQLITE_CONSTRAINT_UNIQUE
33+
if (error?.code === '23505' || error?.code === 'SQLITE_CONSTRAINT_UNIQUE') {
34+
let fieldName: null | string = null
35+
36+
if (error.code === '23505') {
37+
// PostgreSQL - extract field name from constraint
38+
if (adapter.fieldConstraints?.[tableName]?.[error.constraint]) {
39+
fieldName = adapter.fieldConstraints[tableName][error.constraint]
40+
} else {
41+
const replacement = `${tableName}_`
42+
if (error.constraint?.includes(replacement)) {
43+
const replacedConstraint = error.constraint.replace(replacement, '')
44+
if (replacedConstraint && adapter.fieldConstraints[tableName]?.[replacedConstraint]) {
45+
fieldName = adapter.fieldConstraints[tableName][replacedConstraint]
46+
}
47+
}
48+
}
49+
50+
if (!fieldName && error.detail) {
51+
// Extract from detail: "Key (field)=(value) already exists."
52+
const regex = /Key \(([^)]+)\)=\(([^)]+)\)/
53+
const match: string[] = error.detail.match(regex)
54+
if (match && match[1]) {
55+
fieldName = match[1]
56+
}
57+
}
58+
} else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
59+
// SQLite - extract from message: "UNIQUE constraint failed: table.field"
60+
const regex = /UNIQUE constraint failed: ([^.]+)\.([^.]+)/
61+
const match: string[] = error.message?.match(regex)
62+
if (match && match[2]) {
63+
if (adapter.fieldConstraints[tableName]) {
64+
fieldName = adapter.fieldConstraints[tableName][`${match[2]}_idx`]
65+
}
66+
if (!fieldName) {
67+
fieldName = match[2]
68+
}
69+
}
70+
}
71+
72+
throw new ValidationError(
73+
{
74+
id,
75+
errors: [
76+
{
77+
message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
78+
path: fieldName,
79+
},
80+
],
81+
req,
82+
},
83+
req?.t,
84+
)
85+
}
86+
87+
// Re-throw non-constraint errors
88+
throw caughtError
89+
}

0 commit comments

Comments
 (0)