import { db } from '@sim/db' import { workflowCheckpoints } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createCopilotCheckpointContract, listCopilotCheckpointsContract, } from '@/lib/api/contracts/copilot' import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server' import { getAccessibleCopilotChatAuth } from '@/lib/copilot/chat/lifecycle' import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, createInternalServerErrorResponse, createRequestTracker, createUnauthorizedResponse, } from '@/lib/copilot/request/http' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('WorkflowCheckpointsAPI') /** * POST /api/copilot/checkpoints * Create a new checkpoint with JSON workflow state */ export const POST = withRouteHandler(async (req: NextRequest) => { const tracker = createRequestTracker() try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const parsed = await parseRequest( createCopilotCheckpointContract, req, {}, { validationErrorResponse: (error) => validationErrorResponse( error, getValidationErrorMessage(error, 'Invalid checkpoint payload') ), } ) if (!parsed.success) return parsed.response const { workflowId, chatId, messageId, workflowState } = parsed.data.body logger.info(`[${tracker.requestId}] Creating workflow checkpoint`, { userId, workflowId, chatId, messageId, parsedData: { workflowId, chatId, messageId }, messageIdType: typeof messageId, messageIdExists: !!messageId, }) // Verify that the chat belongs to the user const chat = await getAccessibleCopilotChatAuth(chatId, userId) if (!chat) { return createBadRequestResponse('Chat not found or unauthorized') } if (chat.workflowId !== workflowId) { return createBadRequestResponse('Chat does not belong to the requested workflow') } const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, action: 'write', }) if (!authorization.allowed) { return createUnauthorizedResponse() } // Parse the workflow state to validate it's valid JSON let parsedWorkflowState try { parsedWorkflowState = JSON.parse(workflowState) } catch (error) { return createBadRequestResponse('Invalid workflow state JSON') } // Create checkpoint with JSON workflow state const [checkpoint] = await db .insert(workflowCheckpoints) .values({ userId, workflowId, chatId, messageId, workflowState: parsedWorkflowState, // Store as JSON object }) .returning() logger.info(`[${tracker.requestId}] Workflow checkpoint created successfully`, { checkpointId: checkpoint.id, savedData: { checkpointId: checkpoint.id, userId: checkpoint.userId, workflowId: checkpoint.workflowId, chatId: checkpoint.chatId, messageId: checkpoint.messageId, createdAt: checkpoint.createdAt, }, }) return NextResponse.json({ success: true, checkpoint: { id: checkpoint.id, userId: checkpoint.userId, workflowId: checkpoint.workflowId, chatId: checkpoint.chatId, messageId: checkpoint.messageId, createdAt: checkpoint.createdAt, updatedAt: checkpoint.updatedAt, }, }) } catch (error) { logger.error(`[${tracker.requestId}] Failed to create workflow checkpoint:`, error) return createInternalServerErrorResponse('Failed to create checkpoint') } }) /** * GET /api/copilot/checkpoints?chatId=xxx * Retrieve workflow checkpoints for a chat */ export const GET = withRouteHandler(async (req: NextRequest) => { const tracker = createRequestTracker() try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const parsed = await parseRequest( listCopilotCheckpointsContract, req, {}, { validationErrorResponse: (error) => validationErrorResponse(error, getValidationErrorMessage(error)), } ) if (!parsed.success) return parsed.response const { chatId } = parsed.data.query logger.info(`[${tracker.requestId}] Fetching workflow checkpoints for chat`, { userId, chatId, }) const chat = await getAccessibleCopilotChatAuth(chatId, userId) if (!chat) { return createBadRequestResponse('Chat not found or unauthorized') } // Fetch checkpoints for this user and chat const checkpoints = await db .select({ id: workflowCheckpoints.id, userId: workflowCheckpoints.userId, workflowId: workflowCheckpoints.workflowId, chatId: workflowCheckpoints.chatId, messageId: workflowCheckpoints.messageId, createdAt: workflowCheckpoints.createdAt, updatedAt: workflowCheckpoints.updatedAt, }) .from(workflowCheckpoints) .where(and(eq(workflowCheckpoints.chatId, chatId), eq(workflowCheckpoints.userId, userId))) .orderBy(desc(workflowCheckpoints.createdAt)) logger.info(`[${tracker.requestId}] Retrieved ${checkpoints.length} workflow checkpoints`) return NextResponse.json({ success: true, checkpoints, }) } catch (error) { logger.error(`[${tracker.requestId}] Failed to fetch workflow checkpoints:`, error) return createInternalServerErrorResponse('Failed to fetch checkpoints') } })