fix(logs): relax fileSchema so execution logs with files render again (#4495)

* fix(logs): relax fileSchema so execution logs with files render again

* improvement(logs): align fileSchema with shared UserFile type

- contracts/logs.ts: replace local fileSchema with mediaUserFileSchema (the established UserFile boundary schema with .passthrough())
- file-download.tsx: drop local FileData interface, use UserFile from @/executor/types

* improvement(contracts): promote userFileSchema to primitives

Move the canonical UserFile boundary schema out of tools/media/shared.ts
(where it didn't belong — logs aren't media tools) into primitives.ts as
userFileSchema. Update logs, stt, and video contracts to import from the
shared primitive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-07 12:54:42 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 98f8e854eb
commit d081ab283e
6 changed files with 34 additions and 48 deletions
@@ -5,31 +5,20 @@ import { createLogger } from '@sim/logger'
import { ArrowDown } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Button, Loader } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'
import type { UserFile } from '@/executor/types'
const logger = createLogger('FileCards')
interface FileData {
id?: string
name: string
size: number
type: string
key: string
url: string
uploadedAt: string
expiresAt: string
storageProvider?: 's3' | 'blob' | 'local'
bucketName?: string
}
interface FileCardsProps {
files: FileData[]
files: UserFile[]
isExecutionFile?: boolean
workspaceId?: string
}
interface FileCardProps {
file: FileData
file: UserFile
isExecutionFile?: boolean
workspaceId?: string
}
@@ -157,7 +146,7 @@ export function FileDownload({
className,
workspaceId,
}: {
file: FileData
file: UserFile
isExecutionFile?: boolean
className?: string
workspaceId?: string
@@ -220,7 +209,7 @@ export function FileDownload({
return (
<Button
variant='ghost'
className={`h-7 px-2 text-xs ${className}`}
className={cn('h-7 px-2 text-xs', className)}
onClick={handleDownload}
disabled={isDownloading}
>
+2 -14
View File
@@ -1,4 +1,5 @@
import { z } from 'zod'
import { userFileSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
const comparisonOperatorSchema = z.enum(['=', '>', '<', '>=', '<=', '!='])
@@ -66,19 +67,6 @@ const workflowSummarySchema = z
})
.partial()
const fileSchema = z.object({
id: z.string(),
name: z.string(),
size: z.number(),
type: z.string(),
url: z.string(),
key: z.string(),
uploadedAt: z.string(),
expiresAt: z.string(),
storageProvider: z.enum(['s3', 'blob', 'local']).optional(),
bucketName: z.string().optional(),
})
const tokenBreakdownSchema = z
.object({
total: z.number().optional(),
@@ -237,7 +225,7 @@ export const workflowLogSummarySchema = z.object({
export const workflowLogDetailSchema = workflowLogSummarySchema.extend({
executionData: executionDataDetailSchema,
files: z.array(fileSchema).nullable(),
files: z.array(userFileSchema).nullable(),
})
export type WorkflowLogSummary = z.output<typeof workflowLogSummarySchema>
+20
View File
@@ -59,6 +59,26 @@ export const workflowIdSchema = z.string().min(1, 'Workflow ID is required')
* Use `.optional()` / `.default(...)` at the call site, not here, so each
* query field controls its own omission/default semantics.
*/
/**
* Canonical boundary schema for `UserFile` (`apps/sim/executor/types.ts`) — the
* shape produced by the executor and persisted in `workflowExecutionLogs.files`,
* forwarded through tool inputs, and rendered in the logs UI. `.passthrough()`
* tolerates legacy/extra fields on stored rows (e.g. `uploadedAt`, `expiresAt`,
* `storageProvider`) without rejecting the whole payload.
*/
export const userFileSchema = z
.object({
id: z.string().optional().default(''),
name: z.string().min(1),
url: z.string().optional().default(''),
size: z.coerce.number().nonnegative(),
type: z.string().optional().default('application/octet-stream'),
key: z.string().min(1),
context: z.string().optional(),
base64: z.string().optional(),
})
.passthrough()
export const booleanQueryFlagSchema = z.preprocess(
(value) => {
if (typeof value === 'boolean') return value
@@ -3,19 +3,6 @@ import { z } from 'zod'
export const AWS_REGION_PATTERN =
/^(eu-isoe|us-isob|us-iso|us-gov|af|ap|ca|cn|eu|il|me|mx|sa|us)-(central|north|northeast|northwest|south|southeast|southwest|east|west)-\d{1,2}$/
export const mediaUserFileSchema = z
.object({
id: z.string().optional().default(''),
name: z.string().min(1),
url: z.string().optional().default(''),
size: z.coerce.number().nonnegative(),
type: z.string().optional().default('application/octet-stream'),
key: z.string().min(1),
context: z.string().optional(),
base64: z.string().optional(),
})
.passthrough()
export const toolJsonResponseSchema = z
.object({
success: z.boolean().optional(),
@@ -1,11 +1,12 @@
import { z } from 'zod'
import { mediaUserFileSchema, toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared'
import { userFileSchema } from '@/lib/api/contracts/primitives'
import { toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
export const sttProviders = ['whisper', 'deepgram', 'elevenlabs', 'assemblyai', 'gemini'] as const
const MISSING_STT_FIELDS_ERROR = 'Missing required fields: provider and apiKey'
export const sttUserFileSchema = mediaUserFileSchema.extend({
export const sttUserFileSchema = userFileSchema.extend({
type: z.string().optional().default(''),
})
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { mediaUserFileSchema, toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared'
import { userFileSchema } from '@/lib/api/contracts/primitives'
import { toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
export const videoProviders = ['runway', 'veo', 'luma', 'minimax', 'falai'] as const
@@ -19,7 +20,7 @@ export const videoToolBodySchema = z
duration: z.coerce.number().optional(),
aspectRatio: z.string().optional(),
resolution: z.string().optional(),
visualReference: mediaUserFileSchema.optional(),
visualReference: userFileSchema.optional(),
cameraControl: z.unknown().optional(),
endpoint: z.string().optional(),
promptOptimizer: z.boolean().optional(),