Skip to content

Commit 13fcf40

Browse files
committed
fix(uploads): align register response schema with UserFile; skip presigned for KB large files
- registerWorkspaceFileResponseSchema now matches the UserFile shape the route actually returns; previous schema required workspace DB-row fields that were never populated, causing requestJson validation to reject successful uploads. - KB batch presigned fetch now skips files >= LARGE_FILE_THRESHOLD since multipart bypasses the per-file presigned URL anyway.
1 parent 0147ffc commit 13fcf40

4 files changed

Lines changed: 53 additions & 35 deletions

File tree

apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { sleep } from '@sim/utils/helpers'
44
import { useQueryClient } from '@tanstack/react-query'
55
import {
66
DirectUploadError,
7+
LARGE_FILE_THRESHOLD,
78
MULTIPART_MAX_RETRIES,
89
MULTIPART_RETRY_BACKOFF,
910
MULTIPART_RETRY_DELAY_MS,
@@ -98,43 +99,49 @@ interface BatchPresignedFile {
9899
}
99100

100101
/**
101-
* Fetch presigned upload data for many files in one round trip.
102-
* Returns one PresignedUploadInfo per input file (in order).
102+
* Fetch presigned upload data for the small files in `files`. Returns a sparse
103+
* array aligned with the input: entries for files >= LARGE_FILE_THRESHOLD are
104+
* `undefined` because those uploads use multipart and never consume a presigned
105+
* single-PUT URL.
103106
*/
104-
const fetchBatchPresignedData = async (files: File[]): Promise<PresignedUploadInfo[]> => {
105-
const batches: File[][] = []
106-
for (let start = 0; start < files.length; start += BATCH_REQUEST_SIZE) {
107-
batches.push(files.slice(start, start + BATCH_REQUEST_SIZE))
107+
const fetchBatchPresignedData = async (
108+
files: File[]
109+
): Promise<(PresignedUploadInfo | undefined)[]> => {
110+
const result: (PresignedUploadInfo | undefined)[] = new Array(files.length).fill(undefined)
111+
const smallFileIndices: number[] = []
112+
for (let i = 0; i < files.length; i++) {
113+
if (files[i].size < LARGE_FILE_THRESHOLD) smallFileIndices.push(i)
108114
}
115+
if (smallFileIndices.length === 0) return result
116+
117+
for (let start = 0; start < smallFileIndices.length; start += BATCH_REQUEST_SIZE) {
118+
const batchIndices = smallFileIndices.slice(start, start + BATCH_REQUEST_SIZE)
119+
const batchFiles = batchIndices.map((i) => files[i])
120+
const body: { files: BatchPresignedFile[] } = {
121+
files: batchFiles.map((file) => ({
122+
fileName: file.name,
123+
contentType: getFileContentType(file),
124+
fileSize: file.size,
125+
})),
126+
}
109127

110-
const batchResults = await Promise.all(
111-
batches.map(async (batch, batchIndex) => {
112-
const body: { files: BatchPresignedFile[] } = {
113-
files: batch.map((file) => ({
114-
fileName: file.name,
115-
contentType: getFileContentType(file),
116-
fileSize: file.size,
117-
})),
118-
}
119-
120-
const response = await fetch(KB_BATCH_PRESIGNED_ENDPOINT, {
121-
method: 'POST',
122-
headers: { 'Content-Type': 'application/json' },
123-
body: JSON.stringify(body),
124-
})
128+
const response = await fetch(KB_BATCH_PRESIGNED_ENDPOINT, {
129+
method: 'POST',
130+
headers: { 'Content-Type': 'application/json' },
131+
body: JSON.stringify(body),
132+
})
125133

126-
if (!response.ok) {
127-
throw new Error(
128-
`Batch ${batchIndex + 1} presigned URL generation failed: ${response.statusText}`
129-
)
130-
}
134+
if (!response.ok) {
135+
throw new Error(`Batch presigned URL generation failed: ${response.statusText}`)
136+
}
131137

132-
const { files: presignedItems } = (await response.json()) as { files: unknown[] }
133-
return batch.map((file, idx) => normalizePresignedData(presignedItems[idx], file.name))
138+
const { files: presignedItems } = (await response.json()) as { files: unknown[] }
139+
batchIndices.forEach((fileIdx, batchPos) => {
140+
result[fileIdx] = normalizePresignedData(presignedItems[batchPos], batchFiles[batchPos].name)
134141
})
135-
)
142+
}
136143

137-
return batchResults.flat()
144+
return result
138145
}
139146

140147
/**
@@ -232,7 +239,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
232239
const uploadOneFile = async (
233240
file: File,
234241
fileIndex: number,
235-
presigned: PresignedUploadInfo
242+
presigned: PresignedUploadInfo | undefined
236243
): Promise<UploadedFile> => {
237244
if (!options.workspaceId) {
238245
throw new KnowledgeUploadError('workspaceId is required for upload', 'MISSING_WORKSPACE_ID')

apps/sim/hooks/queries/workspace-files.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type UploadProgressEvent,
1919
} from '@/lib/uploads/client/direct-upload'
2020
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
21+
import type { UserFile } from '@/executor/types'
2122

2223
const logger = createLogger('WorkspaceFilesQuery')
2324

@@ -207,7 +208,7 @@ interface UploadFileParams {
207208

208209
interface UploadFileResponse {
209210
success: boolean
210-
file: WorkspaceFileRecord
211+
file: UserFile
211212
}
212213

213214
async function uploadViaApiFallback(
@@ -232,7 +233,7 @@ async function parseUploadResponse(
232233
response: Response,
233234
fallbackMessage: string
234235
): Promise<UploadFileResponse> {
235-
let data: { success?: boolean; error?: string; file?: WorkspaceFileRecord } | null = null
236+
let data: { success?: boolean; error?: string; file?: UserFile } | null = null
236237
try {
237238
data = await response.json()
238239
} catch {}

apps/sim/lib/api/contracts/workspace-files.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,19 @@ export const registerWorkspaceFileBodySchema = z.object({
188188

189189
export type RegisterWorkspaceFileBody = z.input<typeof registerWorkspaceFileBodySchema>
190190

191+
const registeredWorkspaceFileSchema = z.object({
192+
id: z.string(),
193+
name: z.string(),
194+
url: z.string(),
195+
size: z.number(),
196+
type: z.string(),
197+
key: z.string(),
198+
context: z.string().optional(),
199+
})
200+
191201
const registerWorkspaceFileResponseSchema = z.object({
192202
success: z.boolean(),
193-
file: workspaceFileRecordSchema.optional(),
203+
file: registeredWorkspaceFileSchema.optional(),
194204
error: z.string().optional(),
195205
isDuplicate: z.boolean().optional(),
196206
})

apps/sim/lib/uploads/client/direct-upload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getFileContentType, isAbortError } from '@/lib/uploads/utils/file-utils
55
const logger = createLogger('DirectUpload')
66

77
const CHUNK_SIZE = 8 * 1024 * 1024
8-
const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024
8+
export const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024
99
const BASE_TIMEOUT_MS = 2 * 60 * 1000
1010
const TIMEOUT_PER_MB_MS = 1500
1111
const MAX_TIMEOUT_MS = 10 * 60 * 1000

0 commit comments

Comments
 (0)