import { db } from '@sim/db' import { jobExecutionLogs, workflow, workflowExecutionLogs, workflowExecutionSnapshots, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { executionIdParamsSchema } from '@/lib/api/contracts/logs' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('LogsByExecutionIdAPI') export const GET = withRouteHandler( async (request: NextRequest, { params }: { params: Promise<{ executionId: string }> }) => { const requestId = generateRequestId() try { const { executionId } = executionIdParamsSchema.parse(await params) const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`) return NextResponse.json( { error: authResult.error || 'Authentication required' }, { status: 401 } ) } const authenticatedUserId = authResult.userId const [workflowLog] = await db .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) .where(eq(workflowExecutionLogs.executionId, executionId)) .limit(1) if ( workflowLog && !(await checkWorkspaceAccess(workflowLog.workspaceId, authenticatedUserId)).hasAccess ) { logger.warn(`[${requestId}] Execution access denied: ${executionId}`) return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) } // Fallback: check job_execution_logs if (!workflowLog) { const [jobLog] = await db .select({ id: jobExecutionLogs.id, workspaceId: jobExecutionLogs.workspaceId, executionId: jobExecutionLogs.executionId, trigger: jobExecutionLogs.trigger, startedAt: jobExecutionLogs.startedAt, endedAt: jobExecutionLogs.endedAt, totalDurationMs: jobExecutionLogs.totalDurationMs, cost: jobExecutionLogs.cost, executionData: jobExecutionLogs.executionData, }) .from(jobExecutionLogs) .where(eq(jobExecutionLogs.executionId, executionId)) .limit(1) if ( !jobLog || !(await checkWorkspaceAccess(jobLog.workspaceId, authenticatedUserId)).hasAccess ) { logger.warn(`[${requestId}] Execution not found or access denied: ${executionId}`) return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) } return NextResponse.json({ executionId, workflowId: null, workflowState: null, childWorkflowSnapshots: {}, executionMetadata: { trigger: jobLog.trigger, startedAt: jobLog.startedAt.toISOString(), endedAt: jobLog.endedAt?.toISOString(), totalDurationMs: jobLog.totalDurationMs, cost: jobLog.cost || null, }, }) } const [snapshot] = await db .select() .from(workflowExecutionSnapshots) .where(eq(workflowExecutionSnapshots.id, workflowLog.stateSnapshotId)) .limit(1) if (!snapshot) { logger.warn( `[${requestId}] Workflow state snapshot not found for execution: ${executionId}` ) return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 }) } const executionData = (await materializeExecutionData( workflowLog.executionData as Record | null, { workspaceId: workflowLog.workspaceId, workflowId: workflowLog.workflowId, executionId: workflowLog.executionId, } )) as WorkflowExecutionLog['executionData'] const traceSpans = (executionData?.traceSpans as TraceSpan[]) || [] const childSnapshotIds = new Set() const collectSnapshotIds = (spans: TraceSpan[]) => { spans.forEach((span) => { const snapshotId = span.childWorkflowSnapshotId if (typeof snapshotId === 'string') { childSnapshotIds.add(snapshotId) } if (span.children?.length) { collectSnapshotIds(span.children) } }) } if (traceSpans.length > 0) { collectSnapshotIds(traceSpans) } const childWorkflowSnapshots = childSnapshotIds.size > 0 ? await db .select() .from(workflowExecutionSnapshots) .where(inArray(workflowExecutionSnapshots.id, Array.from(childSnapshotIds))) : [] const childSnapshotMap = childWorkflowSnapshots.reduce>( (acc, snap) => { acc[snap.id] = snap.stateData return acc }, {} ) const response = { executionId, workflowId: workflowLog.workflowId, workflowState: snapshot.stateData, childWorkflowSnapshots: childSnapshotMap, executionMetadata: { trigger: workflowLog.trigger, startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, cost: workflowLog.costTotal != null ? { total: Number(workflowLog.costTotal) } : null, }, } return NextResponse.json(response) } catch (error) { logger.error(`[${requestId}] Error fetching execution data:`, error) return NextResponse.json({ error: 'Failed to fetch execution data' }, { status: 500 }) } } )