From 331a52f5e360816f7c46f7a723cf8e9640c51b25 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 23:25:57 -0700 Subject: [PATCH] fix(data-drains): convert unique-name violations to 409 on POST/PUT 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. --- .../[id]/data-drains/[drainId]/route.ts | 22 ++++++-- .../organizations/[id]/data-drains/route.ts | 52 ++++++++++++------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/api/organizations/[id]/data-drains/[drainId]/route.ts b/apps/sim/app/api/organizations/[id]/data-drains/[drainId]/route.ts index c131917f4b..b0b291b080 100644 --- a/apps/sim/app/api/organizations/[id]/data-drains/[drainId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-drains/[drainId]/route.ts @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { dataDrains } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getPostgresErrorCode } from '@sim/utils/errors' import { and, eq, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { @@ -99,11 +100,22 @@ export const PUT = withRouteHandler(async (request: NextRequest, context: RouteC } } - const [updated] = await db - .update(dataDrains) - .set(updates) - .where(eq(dataDrains.id, drainId)) - .returning() + let updated: typeof dataDrains.$inferSelect | undefined + try { + ;[updated] = await db + .update(dataDrains) + .set(updates) + .where(eq(dataDrains.id, drainId)) + .returning() + } catch (error) { + if (getPostgresErrorCode(error) === '23505') { + return NextResponse.json( + { error: 'A data drain with this name already exists in this organization' }, + { status: 409 } + ) + } + throw error + } if (!updated) { // Concurrent DELETE landed between loadDrain() and this UPDATE. diff --git a/apps/sim/app/api/organizations/[id]/data-drains/route.ts b/apps/sim/app/api/organizations/[id]/data-drains/route.ts index a017e36a67..d78655ae28 100644 --- a/apps/sim/app/api/organizations/[id]/data-drains/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-drains/route.ts @@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { dataDrains } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, asc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' @@ -71,24 +72,39 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Route const id = generateId() const now = new Date() - const [inserted] = await db - .insert(dataDrains) - .values({ - id, - organizationId, - name: body.name, - source: body.source, - destinationType: body.destinationType, - destinationConfig: configResult.data as Record, - destinationCredentials: encryptedCredentials, - scheduleCadence: body.scheduleCadence, - enabled: body.enabled ?? true, - cursor: null, - createdBy: access.session.user.id, - createdAt: now, - updatedAt: now, - }) - .returning() + let inserted: typeof dataDrains.$inferSelect | undefined + try { + ;[inserted] = await db + .insert(dataDrains) + .values({ + id, + organizationId, + name: body.name, + source: body.source, + destinationType: body.destinationType, + destinationConfig: configResult.data as Record, + destinationCredentials: encryptedCredentials, + scheduleCadence: body.scheduleCadence, + enabled: body.enabled ?? true, + cursor: null, + createdBy: access.session.user.id, + createdAt: now, + updatedAt: now, + }) + .returning() + } catch (error) { + if (getPostgresErrorCode(error) === '23505') { + return NextResponse.json( + { error: 'A data drain with this name already exists in this organization' }, + { status: 409 } + ) + } + throw error + } + + if (!inserted) { + throw new Error('Insert returned no row') + } logger.info('Data drain created', { drainId: id,