Skip to content

Commit ae87481

Browse files
authored
fix(data-drains): convert unique-name violations to 409 on POST/PUT (#4471)
Catch Postgres 23505 on insert/update so concurrent name conflicts return a clean 409 instead of a 500. The data_drains_org_name_unique index already prevents duplicate rows; this just improves the UX.
1 parent a24e851 commit ae87481

2 files changed

Lines changed: 51 additions & 23 deletions

File tree

apps/sim/app/api/organizations/[id]/data-drains/[drainId]/route.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
33
import { dataDrains } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5+
import { getPostgresErrorCode } from '@sim/utils/errors'
56
import { and, eq, ne } from 'drizzle-orm'
67
import { type NextRequest, NextResponse } from 'next/server'
78
import {
@@ -99,11 +100,22 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: RouteC
99100
}
100101
}
101102

102-
const [updated] = await db
103-
.update(dataDrains)
104-
.set(updates)
105-
.where(eq(dataDrains.id, drainId))
106-
.returning()
103+
let updated: typeof dataDrains.$inferSelect | undefined
104+
try {
105+
;[updated] = await db
106+
.update(dataDrains)
107+
.set(updates)
108+
.where(eq(dataDrains.id, drainId))
109+
.returning()
110+
} catch (error) {
111+
if (getPostgresErrorCode(error) === '23505') {
112+
return NextResponse.json(
113+
{ error: 'A data drain with this name already exists in this organization' },
114+
{ status: 409 }
115+
)
116+
}
117+
throw error
118+
}
107119

108120
if (!updated) {
109121
// Concurrent DELETE landed between loadDrain() and this UPDATE.

apps/sim/app/api/organizations/[id]/data-drains/route.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
33
import { dataDrains } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5+
import { getPostgresErrorCode } from '@sim/utils/errors'
56
import { generateId } from '@sim/utils/id'
67
import { and, asc, eq } from 'drizzle-orm'
78
import { type NextRequest, NextResponse } from 'next/server'
@@ -71,24 +72,39 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route
7172

7273
const id = generateId()
7374
const now = new Date()
74-
const [inserted] = await db
75-
.insert(dataDrains)
76-
.values({
77-
id,
78-
organizationId,
79-
name: body.name,
80-
source: body.source,
81-
destinationType: body.destinationType,
82-
destinationConfig: configResult.data as Record<string, unknown>,
83-
destinationCredentials: encryptedCredentials,
84-
scheduleCadence: body.scheduleCadence,
85-
enabled: body.enabled ?? true,
86-
cursor: null,
87-
createdBy: access.session.user.id,
88-
createdAt: now,
89-
updatedAt: now,
90-
})
91-
.returning()
75+
let inserted: typeof dataDrains.$inferSelect | undefined
76+
try {
77+
;[inserted] = await db
78+
.insert(dataDrains)
79+
.values({
80+
id,
81+
organizationId,
82+
name: body.name,
83+
source: body.source,
84+
destinationType: body.destinationType,
85+
destinationConfig: configResult.data as Record<string, unknown>,
86+
destinationCredentials: encryptedCredentials,
87+
scheduleCadence: body.scheduleCadence,
88+
enabled: body.enabled ?? true,
89+
cursor: null,
90+
createdBy: access.session.user.id,
91+
createdAt: now,
92+
updatedAt: now,
93+
})
94+
.returning()
95+
} catch (error) {
96+
if (getPostgresErrorCode(error) === '23505') {
97+
return NextResponse.json(
98+
{ error: 'A data drain with this name already exists in this organization' },
99+
{ status: 409 }
100+
)
101+
}
102+
throw error
103+
}
104+
105+
if (!inserted) {
106+
throw new Error('Insert returned no row')
107+
}
92108

93109
logger.info('Data drain created', {
94110
drainId: id,

0 commit comments

Comments
 (0)