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' import { createDataDrainContract, listDataDrainsContract } from '@/lib/api/contracts/data-drains' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { authorizeDrainAccess } from '@/lib/data-drains/access' import { getDestination } from '@/lib/data-drains/destinations/registry' import { encryptCredentials } from '@/lib/data-drains/encryption' import { serializeDrain } from '@/lib/data-drains/serializers' const logger = createLogger('DataDrainsAPI') type RouteContext = { params: Promise<{ id: string }> } export const GET = withRouteHandler(async (request: NextRequest, context: RouteContext) => { const { id: organizationId } = await context.params const access = await authorizeDrainAccess(organizationId, { requireMutating: false }) if (!access.ok) return access.response const parsed = await parseRequest(listDataDrainsContract, request, context) if (!parsed.success) return parsed.response const rows = await db .select() .from(dataDrains) .where(eq(dataDrains.organizationId, organizationId)) .orderBy(asc(dataDrains.createdAt)) return NextResponse.json({ drains: rows.map(serializeDrain) }) }) export const POST = withRouteHandler(async (request: NextRequest, context: RouteContext) => { const { id: organizationId } = await context.params const access = await authorizeDrainAccess(organizationId, { requireMutating: true }) if (!access.ok) return access.response const parsed = await parseRequest(createDataDrainContract, request, context) if (!parsed.success) return parsed.response const body = parsed.data.body if (!body.destinationCredentials) { return NextResponse.json( { error: 'destinationCredentials is required when creating a drain' }, { status: 400 } ) } const destination = getDestination(body.destinationType) const configResult = destination.configSchema.safeParse(body.destinationConfig) if (!configResult.success) return validationErrorResponse(configResult.error) const credentialsResult = destination.credentialsSchema.safeParse(body.destinationCredentials) if (!credentialsResult.success) return validationErrorResponse(credentialsResult.error) const encryptedCredentials = await encryptCredentials(credentialsResult.data) const [existing] = await db .select({ id: dataDrains.id }) .from(dataDrains) .where(and(eq(dataDrains.organizationId, organizationId), eq(dataDrains.name, body.name))) .limit(1) if (existing) { return NextResponse.json( { error: 'A data drain with this name already exists in this organization' }, { status: 409 } ) } const id = generateId() const now = new Date() 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, organizationId, source: body.source, destinationType: body.destinationType, }) recordAudit({ workspaceId: null, actorId: access.session.user.id, action: AuditAction.DATA_DRAIN_CREATED, resourceType: AuditResourceType.DATA_DRAIN, resourceId: id, actorName: access.session.user.name ?? undefined, actorEmail: access.session.user.email ?? undefined, resourceName: body.name, description: `Created data drain '${body.name}'`, metadata: { organizationId, source: body.source, destinationType: body.destinationType, scheduleCadence: body.scheduleCadence, }, request, }) return NextResponse.json({ drain: serializeDrain(inserted) }, { status: 201 }) })