Skip to content

Commit a2f50d8

Browse files
committed
feat(project-files): add rendered delivery and indexed search
1 parent 16a7e9a commit a2f50d8

80 files changed

Lines changed: 3806 additions & 521 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/checks.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,14 @@ jobs:
243243
working-directory: packages/db
244244
run: bun run db:migrate
245245

246+
- name: Build native isolate addon for Project file rendering
247+
if: matrix.group == 'project-files'
248+
working-directory: apps/sim
249+
run: |
250+
addon_dir="$(node -p 'require("node:path").dirname(require.resolve("isolated-vm/package.json", { paths: ["./apps/sim"] }))')"
251+
cd "$addon_dir"
252+
bun run install
253+
246254
# No step timeout: the job's bound covers a hang without cutting a slow but healthy suite
247255
# short of writing its report.
248256
- name: Run end-to-end suites

‎apps/sim/app/api/desktop/tool/file/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
import { PayloadSizeLimitError, readStreamToBufferWithLimit } from '@/lib/core/utils/stream-limits'
2222
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2323
import { workspaceFileVfsPath } from '@/lib/uploads/contexts/workspace'
24+
import { encodeFilenameForHeader } from '@/lib/uploads/server/delivery'
2425
import { internalFileErrorPolicies } from '@/lib/workspace-files/api'
25-
import { encodeFilenameForHeader } from '@/app/api/files/utils'
2626

2727
export const dynamic = 'force-dynamic'
2828

‎apps/sim/app/api/files/export/[id]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { captureServerEvent } from '@/lib/posthog/server'
1313
import type { StorageContext } from '@/lib/uploads/config'
1414
import { getServeStoragePrefix } from '@/lib/uploads/config'
1515
import { downloadFile } from '@/lib/uploads/core/storage-service'
16+
import { encodeFilenameForHeader } from '@/lib/uploads/server/delivery'
1617
import { extractEmbeddedFileRefs } from '@/lib/uploads/server/embedded-image-refs'
1718
import {
1819
createMarkdownExport,
@@ -27,7 +28,6 @@ import { getWorkspaceFileSize } from '@/lib/uploads/shared/types'
2728
import { storedFileId } from '@/lib/uploads/utils/embedded-image-ref'
2829
import { formatFileSize } from '@/lib/uploads/utils/file'
2930
import { verifyFileAccess } from '@/app/api/files/authorization'
30-
import { encodeFilenameForHeader } from '@/app/api/files/utils'
3131

3232
const logger = createLogger('FilesExportAPI')
3333

‎apps/sim/app/api/files/serve/[...path]/route.test.ts‎

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import {
1313
createSessionPrincipal,
1414
} from '@sim/testing/factories/principal.factory'
1515
import { createRouteContext } from '@sim/testing/helpers/http'
16+
import { fileReadReceiptMock } from '@sim/testing/mocks/file-read-receipt.mock'
1617
import { fileUtilsMock, fileUtilsMockFns } from '@sim/testing/mocks/file-utils.mock'
18+
import {
19+
fileUtilsServerMock,
20+
fileUtilsServerMockFns,
21+
} from '@sim/testing/mocks/file-utils-server.mock'
1722
import {
1823
filesAuthorizationMock,
1924
filesAuthorizationMockFns,
@@ -36,6 +41,8 @@ const {
3641
mockReadFile,
3742
mockAuthenticateWorkspaceFile,
3843
mockReadWorkspaceFileContentByKey,
44+
mockReadWorkspaceFileRecordByKey,
45+
mockAuthorizeWorkspaceFileRecordByKey,
3946
mockResolveServableDocBytes,
4047
mockGetContentType,
4148
mockFindLocalFile,
@@ -57,6 +64,8 @@ const {
5764
mockReadFile: vi.fn(),
5865
mockAuthenticateWorkspaceFile: vi.fn(),
5966
mockReadWorkspaceFileContentByKey: vi.fn(),
67+
mockReadWorkspaceFileRecordByKey: vi.fn(),
68+
mockAuthorizeWorkspaceFileRecordByKey: vi.fn(async () => undefined),
6069
mockResolveServableDocBytes: vi.fn(),
6170
mockGetContentType: vi.fn(),
6271
mockFindLocalFile: vi.fn(),
@@ -86,6 +95,9 @@ vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
8695

8796
vi.mock('@/lib/uploads/utils/file', () => fileUtilsMock)
8897

98+
vi.mock('@/lib/uploads/utils/file.server', () => fileUtilsServerMock)
99+
vi.mock('@/lib/workspace-files/read-receipt', () => fileReadReceiptMock)
100+
89101
vi.mock('@/lib/uploads/server/metadata', () => uploadsMetadataMock)
90102

91103
vi.mock('@/lib/uploads/setup.server', () => ({}))
@@ -106,6 +118,10 @@ vi.mock('@/lib/workspace-files/api', () => ({
106118

107119
vi.mock('@/lib/workspace-files/application/read-workspace-file-content-by-key', () => ({
108120
readWorkspaceFileContentByKey: { execute: mockReadWorkspaceFileContentByKey },
121+
readWorkspaceFileRecordByKey: {
122+
execute: mockReadWorkspaceFileRecordByKey,
123+
authorize: mockAuthorizeWorkspaceFileRecordByKey,
124+
},
109125
}))
110126

111127
vi.mock('@/lib/uploads/documents/compile', () => ({
@@ -161,6 +177,17 @@ describe('File Serve API Route', () => {
161177
},
162178
content: Buffer.from('generated source'),
163179
})
180+
mockReadWorkspaceFileRecordByKey.mockResolvedValue({
181+
file: {
182+
id: 'file-1',
183+
workspaceId: 'test-workspace-id',
184+
name: 'report.pdf',
185+
key: 'workspace/test-workspace-id/report.pdf',
186+
type: 'text/x-pdflibjs',
187+
size: 16,
188+
contentUpdatedAt: new Date('2026-09-06'),
189+
},
190+
})
164191
mockResolveServableDocBytes.mockImplementation(
165192
async ({ rawBuffer, fileName }: { rawBuffer: Buffer; fileName: string }) => ({
166193
buffer: rawBuffer,
@@ -173,24 +200,23 @@ describe('File Serve API Route', () => {
173200
mockReadFile(filePath)
174201
)
175202
mockCreateFileResponse.mockImplementation(
176-
(file: { buffer: Buffer; contentType: string; filename: string }) => {
203+
(file: { buffer: Buffer; contentType: string; filename: string; cacheControl?: string }) => {
177204
return new Response(file.buffer, {
178205
status: 200,
179206
headers: {
180207
'Content-Type': file.contentType,
208+
...(file.cacheControl ? { 'Cache-Control': file.cacheControl } : {}),
181209
'Content-Disposition': `inline; filename="${file.filename}"`,
182210
},
183211
})
184212
}
185213
)
186-
// Delegates so the existing assertions on the response payload — including its
187-
// Cache-Control — read the same call list whichever helper the route reached for.
188214
mockCreateConditionalFileResponse.mockImplementation((file: unknown) =>
189215
mockCreateFileResponse(file)
190216
)
191-
mockFileErrorResponse.mockImplementation((error: Error) => {
217+
mockFileErrorResponse.mockImplementation((error: Error, status = 500) => {
192218
return new Response(JSON.stringify({ error: error.name, message: error.message }), {
193-
status: error.name === 'FileNotFoundError' ? 404 : 500,
219+
status: error.name === 'FileNotFoundError' ? 404 : status,
194220
headers: { 'Content-Type': 'application/json' },
195221
})
196222
})
@@ -284,9 +310,9 @@ describe('File Serve API Route', () => {
284310
// The real createFileErrorResponse owns the status mapping; mirror it here so the
285311
// route's own error path is what decides, not the mock's default 500.
286312
mockFileErrorResponse.mockImplementation(
287-
(error: Error) =>
313+
(error: Error, status = 500) =>
288314
new Response(JSON.stringify({ error: error.name }), {
289-
status: error.name === 'PayloadSizeLimitError' ? 413 : 500,
315+
status: error.name === 'PayloadSizeLimitError' ? 413 : status,
290316
})
291317
)
292318

@@ -316,7 +342,7 @@ describe('File Serve API Route', () => {
316342
mockResolveStoredFileContext.mockResolvedValue('workspace')
317343
mockParseWorkspaceFileKey.mockReturnValue('test-workspace-id')
318344
mockAuthenticateWorkspaceFile.mockResolvedValue(principal)
319-
mockResolveServableDocBytes.mockResolvedValue({
345+
fileUtilsServerMockFns.mockDownloadServableFileFromStorage.mockResolvedValue({
320346
buffer: Buffer.from('compiled'),
321347
contentType: 'application/pdf',
322348
...(dependsOnReferencedFiles ? { dependsOnReferencedFiles: true } : {}),
@@ -325,19 +351,22 @@ describe('File Serve API Route', () => {
325351
const req = createMockRequest({
326352
url: 'http://localhost:3000/api/files/serve/workspace/test-workspace-id/report.pdf?v=1756684800000',
327353
})
328-
await GET(req, createRouteContext({ path: ['workspace', 'test-workspace-id', 'report.pdf'] }))
329-
return mockCreateFileResponse.mock.calls.at(-1)?.[0]
354+
const response = await GET(
355+
req,
356+
createRouteContext({ path: ['workspace', 'test-workspace-id', 'report.pdf'] })
357+
)
358+
expect(response.status).toBe(200)
359+
return response
330360
}
331361

332362
it('caches a versioned document immutably when its bytes derive from the stored source alone', async () => {
333-
expect(await serveVersionedDoc(false)).toEqual(
334-
expect.objectContaining({ cacheControl: 'private, max-age=31536000, immutable' })
335-
)
363+
const response = await serveVersionedDoc(false)
364+
expect(response.headers.get('Cache-Control')).toBe('private, max-age=31536000, immutable')
336365
})
337366

338-
it('attaches a validator to a revalidated response so the next check can be answered 304', async () => {
339-
await serveVersionedDoc(true)
340-
expect(mockCreateConditionalFileResponse).toHaveBeenCalled()
367+
it('requires revalidation for a versioned document with referenced inputs', async () => {
368+
const response = await serveVersionedDoc(true)
369+
expect(response.headers.get('Cache-Control')).toBe('private, no-cache, must-revalidate')
341370
})
342371
})
343372
})

0 commit comments

Comments
 (0)