import { db } from '@sim/db' import { userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { deleteTableRowContract, getTableQuerySchema, updateTableRowContract, } from '@/lib/api/contracts/tables' import { isZodError, parseRequest, validationErrorResponse } from '@/lib/api/server/validation' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { RowData, TableSchema } from '@/lib/table' import { deleteRow, updateRow } from '@/lib/table' import { rowWireTranslators } from '@/app/api/table/row-wire' import { accessError, checkAccess, rootErrorMessage, rowWriteErrorResponse, } from '@/app/api/table/utils' const logger = createLogger('TableRowAPI') interface RowRouteParams { params: Promise<{ tableId: string; rowId: string }> } /** GET /api/table/[tableId]/rows/[rowId] - Retrieves a single row. */ export const GET = withRouteHandler(async (request: NextRequest, { params }: RowRouteParams) => { const requestId = generateRequestId() const { tableId, rowId } = await params try { const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } const { searchParams } = new URL(request.url) const validated = getTableQuerySchema.parse({ workspaceId: searchParams.get('workspaceId'), }) const result = await checkAccess(tableId, authResult.userId, 'read') if (!result.ok) return accessError(result, requestId, tableId) const { table } = result if (table.workspaceId !== validated.workspaceId) { return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) } const [row] = await db .select({ id: userTableRows.id, data: userTableRows.data, position: userTableRows.position, createdAt: userTableRows.createdAt, updatedAt: userTableRows.updatedAt, }) .from(userTableRows) .where( and( eq(userTableRows.id, rowId), eq(userTableRows.tableId, tableId), eq(userTableRows.workspaceId, validated.workspaceId) ) ) .limit(1) if (!row) { return NextResponse.json({ error: 'Row not found' }, { status: 404 }) } logger.info(`[${requestId}] Retrieved row ${rowId} from table ${tableId}`) const wire = rowWireTranslators(authResult.authType, table.schema as TableSchema) return NextResponse.json({ success: true, data: { row: { id: row.id, data: wire.dataOut(row.data as RowData), position: row.position, createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt), updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt), }, }, }) } catch (error) { if (isZodError(error)) { return validationErrorResponse(error) } logger.error(`[${requestId}] Error getting row:`, error) return NextResponse.json({ error: 'Failed to get row' }, { status: 500 }) } }) /** PATCH /api/table/[tableId]/rows/[rowId] - Updates a single row (supports partial updates). */ export const PATCH = withRouteHandler(async (request: NextRequest, context: RowRouteParams) => { const requestId = generateRequestId() try { const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } const parsed = await parseRequest(updateTableRowContract, request, context, { validationErrorResponse: (error) => validationErrorResponse(error), }) if (!parsed.success) return parsed.response const { tableId, rowId } = parsed.data.params const validated = parsed.data.body const result = await checkAccess(tableId, authResult.userId, 'write') if (!result.ok) return accessError(result, requestId, tableId) const { table } = result if (table.workspaceId !== validated.workspaceId) { return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) } const wire = rowWireTranslators(authResult.authType, table.schema as TableSchema) const updatedRow = await updateRow( { tableId, rowId, data: wire.dataIn(validated.data as RowData), workspaceId: validated.workspaceId, actorUserId: authResult.userId, }, table, requestId ) // Only `null` when a `cancellationGuard` is supplied and the SQL guard // rejects the write — this route doesn't pass one, so reaching null is a bug. if (!updatedRow) throw new Error('updateRow returned null without a cancellationGuard') // Auto-dispatch for user edits is handled inside `updateRow` (mode: 'new'). // Firing a second mode: 'incomplete' dispatch here would race with the // `mode: 'new'` one AND bulk-clear sibling-group outputs (the incomplete // bulk-clear wipes ALL targeted columns when any one column on the row // is empty). return NextResponse.json({ success: true, data: { row: { id: updatedRow.id, data: wire.dataOut(updatedRow.data), position: updatedRow.position, createdAt: updatedRow.createdAt instanceof Date ? updatedRow.createdAt.toISOString() : updatedRow.createdAt, updatedAt: updatedRow.updatedAt instanceof Date ? updatedRow.updatedAt.toISOString() : updatedRow.updatedAt, }, message: 'Row updated successfully', }, }) } catch (error) { if (rootErrorMessage(error) === 'Row not found') { return NextResponse.json({ error: 'Row not found' }, { status: 404 }) } const response = rowWriteErrorResponse(error) if (response) return response logger.error(`[${requestId}] Error updating row:`, error) return NextResponse.json({ error: 'Failed to update row' }, { status: 500 }) } }) /** DELETE /api/table/[tableId]/rows/[rowId] - Deletes a single row. */ export const DELETE = withRouteHandler(async (request: NextRequest, context: RowRouteParams) => { const requestId = generateRequestId() try { const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } const parsed = await parseRequest(deleteTableRowContract, request, context, { validationErrorResponse: (error) => validationErrorResponse(error), }) if (!parsed.success) return parsed.response const { tableId, rowId } = parsed.data.params const validated = parsed.data.body const result = await checkAccess(tableId, authResult.userId, 'write') if (!result.ok) return accessError(result, requestId, tableId) const { table } = result if (table.workspaceId !== validated.workspaceId) { return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 }) } await deleteRow(tableId, rowId, validated.workspaceId, requestId) return NextResponse.json({ success: true, data: { message: 'Row deleted successfully', deletedCount: 1, }, }) } catch (error) { const errorMessage = toError(error).message if (errorMessage === 'Row not found') { return NextResponse.json({ error: errorMessage }, { status: 404 }) } logger.error(`[${requestId}] Error deleting row:`, error) return NextResponse.json({ error: 'Failed to delete row' }, { status: 500 }) } })