Skip to content

Commit 96f123f

Browse files
committed
Merge remote-tracking branch 'origin/feat/sim-cli' into feat/sim-cli
2 parents d4c7464 + 9d7264c commit 96f123f

21 files changed

Lines changed: 946 additions & 802 deletions

File tree

apps/docs/openapi-v2-files-audit.json

Lines changed: 216 additions & 265 deletions
Large diffs are not rendered by default.

apps/sim/app/api/v2/files/[fileId]/content/route.test.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const mocks = vi.hoisted(() => ({
88
admit: vi.fn(),
9+
download: vi.fn(),
910
updateContent: vi.fn(),
1011
authenticateV2ApiKey: vi.fn(),
1112
checkRateLimitDirect: vi.fn(),
1213
checkRateLimitDirectOrThrow: vi.fn(),
1314
getUserEmailsByIds: vi.fn(),
1415
}))
1516

17+
vi.mock('@/lib/workspace-files/application/download-workspace-file', () => ({
18+
downloadWorkspaceFileStream: {
19+
operation: { id: 'files.download', minimumRole: 'read', workspaceApiKey: 'allow' },
20+
execute: mocks.download,
21+
},
22+
}))
23+
1624
vi.mock('@/lib/workspace-files/orchestration', () => ({
1725
MAX_WORKSPACE_FILE_INLINE_BODY_BYTES: 70 * 1024 * 1024,
1826
}))
@@ -48,7 +56,7 @@ vi.mock('@/lib/users/queries', () => ({
4856
}))
4957

5058
import { OrchestrationError } from '@/lib/core/orchestration/types'
51-
import { PUT } from '@/app/api/v2/files/[fileId]/content/route'
59+
import { GET, PUT } from '@/app/api/v2/files/[fileId]/content/route'
5260

5361
const WORKSPACE_ID = 'workspace-1'
5462
const FILE_ID = 'wf_1'
@@ -76,6 +84,15 @@ const record = {
7684
uploadedAt: new Date('2024-01-01T00:00:00Z'),
7785
updatedAt: new Date('2024-01-03T00:00:00Z'),
7886
}
87+
const context = { params: Promise.resolve({ fileId: FILE_ID }) }
88+
89+
const callGet = () =>
90+
GET(
91+
new NextRequest(
92+
`http://localhost:3000/api/v2/files/${FILE_ID}/content?workspaceId=${WORKSPACE_ID}`
93+
),
94+
context
95+
)
7996

8097
const callPut = (body: unknown, contentLength?: number) =>
8198
PUT(
@@ -87,9 +104,55 @@ const callPut = (body: unknown, contentLength?: number) =>
87104
},
88105
body: typeof body === 'string' ? body : JSON.stringify(body),
89106
}),
90-
{ params: Promise.resolve({ fileId: FILE_ID }) }
107+
context
91108
)
92109

110+
describe('GET /api/v2/files/[fileId]/content', () => {
111+
beforeEach(() => {
112+
vi.clearAllMocks()
113+
mocks.authenticateV2ApiKey.mockResolvedValue(auth)
114+
mocks.checkRateLimitDirect.mockResolvedValue({
115+
allowed: true,
116+
remaining: 599,
117+
resetAt: new Date('2024-01-01T01:00:00Z'),
118+
})
119+
mocks.checkRateLimitDirectOrThrow.mockResolvedValue({
120+
allowed: true,
121+
remaining: 99,
122+
resetAt: new Date('2024-01-01T01:00:00Z'),
123+
})
124+
mocks.download.mockResolvedValue({
125+
file: record,
126+
stream: new Blob(['id,name\n']).stream(),
127+
})
128+
})
129+
130+
it('streams bytes through the binary adapter', async () => {
131+
const response = await callGet()
132+
133+
expect(response.status).toBe(200)
134+
expect(response.headers.get('Content-Type')).toBe('text/csv')
135+
expect(response.headers.get('Content-Disposition')).toContain('data.csv')
136+
expect(await response.text()).toBe('id,name\n')
137+
expect(mocks.download).toHaveBeenCalledWith({
138+
principal: auth.principal,
139+
input: { fileId: FILE_ID, assertedWorkspaceId: WORKSPACE_ID },
140+
request: expect.anything(),
141+
})
142+
})
143+
144+
it('conceals content authorization failures', async () => {
145+
mocks.download.mockRejectedValue(
146+
new OrchestrationError('forbidden', 'Insufficient workspace permissions')
147+
)
148+
149+
const response = await callGet()
150+
151+
expect(response.status).toBe(404)
152+
expect((await response.json()).error.code).toBe('NOT_FOUND')
153+
})
154+
})
155+
93156
describe('PUT /api/v2/files/[fileId]/content', () => {
94157
beforeEach(() => {
95158
vi.clearAllMocks()

apps/sim/app/api/v2/files/[fileId]/content/route.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { v2UpdateFileContentContract } from '@/lib/api/contracts/v2/files'
2-
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
1+
import { v2GetFileContentContract, v2UpdateFileContentContract } from '@/lib/api/contracts/v2/files'
2+
import {
3+
defineV2BinaryRoute,
4+
defineV2JsonRoute,
5+
v2ApiKeyAuth,
6+
v2RateLimits,
7+
} from '@/lib/api/server/routes'
38
import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
9+
import { downloadWorkspaceFileStream } from '@/lib/workspace-files/application/download-workspace-file'
410
import { fileOperations } from '@/lib/workspace-files/application/operations'
511
import {
612
admitUpdateWorkspaceFileContent,
@@ -13,6 +19,26 @@ import { v2Error } from '@/app/api/v2/lib/response'
1319
export const dynamic = 'force-dynamic'
1420
export const revalidate = 0
1521

22+
/** GET /api/v2/files/[fileId]/content — Stream a file's bytes. */
23+
export const GET = defineV2BinaryRoute({
24+
contract: v2GetFileContentContract,
25+
auth: v2ApiKeyAuth,
26+
operation: fileOperations.download,
27+
rateLimit: v2RateLimits.publicApi,
28+
errorPolicy: v2FileErrorPolicies.concealResourceAuthorization,
29+
mapInput: ({ params, query }) => ({
30+
fileId: params.fileId,
31+
assertedWorkspaceId: query.workspaceId,
32+
}),
33+
useCase: downloadWorkspaceFileStream,
34+
present: ({ file, stream }) => ({
35+
body: stream,
36+
contentType: file.type || 'application/octet-stream',
37+
contentDisposition: `attachment; filename="${file.name.replace(/[^\w.-]/g, '_')}"; filename*=UTF-8''${encodeURIComponent(file.name)}`,
38+
contentLength: file.size,
39+
}),
40+
})
41+
1642
/** PUT /api/v2/files/[fileId]/content — Replace a file's bytes. */
1743
export const PUT = defineV2JsonRoute({
1844
contract: v2UpdateFileContentContract,

apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts

Lines changed: 0 additions & 139 deletions
This file was deleted.

apps/sim/app/api/v2/files/[fileId]/metadata/route.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)