Skip to content

Commit 855dca5

Browse files
committed
fix client-server sep
1 parent 6388044 commit 855dca5

8 files changed

Lines changed: 221 additions & 105 deletions

File tree

apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
44
import { isUserFileWithMetadata } from '@/lib/core/utils/user-file'
55
import type { ExecutionContext } from '@/lib/uploads/contexts/execution/utils'
66
import { generateExecutionFileKey, generateFileId } from '@/lib/uploads/contexts/execution/utils'
7-
import * as StorageService from '@/lib/uploads/core/storage-service'
87
import type { UserFile } from '@/executor/types'
98

109
const logger = createLogger('ExecutionFileStorage')
1110

11+
async function getStorageService() {
12+
return import('@/lib/uploads/core/storage-service')
13+
}
14+
1215
function isSerializedBuffer(value: unknown): value is { type: string; data: number[] } {
1316
return (
1417
!!value &&
@@ -92,6 +95,7 @@ export async function uploadExecutionFile(
9295
}
9396

9497
try {
98+
const StorageService = await getStorageService()
9599
const fileInfo = await StorageService.uploadFile({
96100
file: fileBuffer,
97101
fileName: storageKey,
@@ -138,6 +142,7 @@ export async function downloadExecutionFile(
138142
logger.info(`Downloading execution file: ${userFile.name}`)
139143

140144
try {
145+
const StorageService = await getStorageService()
141146
const fileBuffer = await StorageService.downloadFile({
142147
key: userFile.key,
143148
context: 'execution',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
2+
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
3+
import {
4+
FORMAT_TO_MIME,
5+
getGoogleSlidesExportExecutionContext,
6+
MAX_LEGACY_INLINE_EXPORT_BYTES,
7+
readGoogleSlidesExportResponse,
8+
} from '@/tools/google_slides/export_presentation'
9+
import { presentationUrl } from '@/tools/google_slides/utils'
10+
11+
interface ExportPresentationParams {
12+
accessToken: string
13+
presentationId: string
14+
exportFormat?: 'PDF' | 'PPTX' | 'ODP' | 'TXT' | 'PNG' | 'JPEG' | 'SVG'
15+
_context?: Record<string, unknown>
16+
}
17+
18+
export async function transformGoogleSlidesExportResponse(
19+
response: Response,
20+
params?: ExportPresentationParams
21+
) {
22+
const buffer = await readGoogleSlidesExportResponse(response)
23+
const presentationId = params?.presentationId?.trim() || ''
24+
const format = (params?.exportFormat || 'PDF').toUpperCase()
25+
const mime = FORMAT_TO_MIME[format] ?? 'application/octet-stream'
26+
const { context, userId } = getGoogleSlidesExportExecutionContext(params)
27+
const filename = `${presentationId || 'presentation'}.${format.toLowerCase()}`
28+
const userFile = context
29+
? await uploadExecutionFile(context, Buffer.from(buffer), filename, mime, userId)
30+
: undefined
31+
32+
if (!userFile && buffer.length > MAX_LEGACY_INLINE_EXPORT_BYTES) {
33+
throw new PayloadSizeLimitError({
34+
label: 'Google Slides legacy inline export',
35+
maxBytes: MAX_LEGACY_INLINE_EXPORT_BYTES,
36+
observedBytes: buffer.length,
37+
})
38+
}
39+
40+
const contentBase64 =
41+
!userFile && buffer.length <= MAX_LEGACY_INLINE_EXPORT_BYTES
42+
? buffer.toString('base64')
43+
: undefined
44+
45+
return {
46+
success: true,
47+
output: {
48+
...(userFile ? { file: { ...userFile, mimeType: mime } } : {}),
49+
...(contentBase64 ? { contentBase64 } : {}),
50+
mimeType: mime,
51+
sizeBytes: buffer.length,
52+
metadata: {
53+
presentationId,
54+
url: presentationUrl(presentationId),
55+
exportFormat: format,
56+
},
57+
},
58+
}
59+
}

apps/sim/tools/google_slides/export_presentation.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ vi.mock('@/lib/uploads/contexts/execution', () => ({
1212
}))
1313

1414
import { exportPresentationTool } from '@/tools/google_slides/export_presentation'
15+
import { transformGoogleSlidesExportResponse } from '@/tools/google_slides/export_presentation.server'
1516

1617
describe('Google Slides export presentation tool', () => {
1718
beforeEach(() => {
@@ -33,7 +34,7 @@ describe('Google Slides export presentation tool', () => {
3334
headers: { 'content-type': 'application/pdf' },
3435
})
3536

36-
const result = await exportPresentationTool.transformResponse?.(response, {
37+
const result = await transformGoogleSlidesExportResponse(response, {
3738
accessToken: 'token',
3839
presentationId: 'presentation-1',
3940
exportFormat: 'PDF',

apps/sim/tools/google_slides/export_presentation.ts

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
readResponseTextWithLimit,
55
readResponseToBufferWithLimit,
66
} from '@/lib/core/utils/stream-limits'
7-
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
87
import type { UserFile } from '@/executor/types'
98
import { presentationUrl } from '@/tools/google_slides/utils'
109
import type { ToolConfig } from '@/tools/types'
@@ -29,10 +28,10 @@ interface ExportPresentationResponse {
2928
}
3029
}
3130

32-
const MAX_GOOGLE_SLIDES_EXPORT_BYTES = 10 * 1024 * 1024
33-
const MAX_LEGACY_INLINE_EXPORT_BYTES = 7 * 1024 * 1024
31+
export const MAX_GOOGLE_SLIDES_EXPORT_BYTES = 10 * 1024 * 1024
32+
export const MAX_LEGACY_INLINE_EXPORT_BYTES = 7 * 1024 * 1024
3433

35-
const FORMAT_TO_MIME: Record<string, string> = {
34+
export const FORMAT_TO_MIME: Record<string, string> = {
3635
PDF: 'application/pdf',
3736
PPTX: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3837
ODP: 'application/vnd.oasis.opendocument.presentation',
@@ -42,7 +41,7 @@ const FORMAT_TO_MIME: Record<string, string> = {
4241
SVG: 'image/svg+xml',
4342
}
4443

45-
function getExecutionContext(params?: ExportPresentationParams): {
44+
export function getGoogleSlidesExportExecutionContext(params?: ExportPresentationParams): {
4645
context?: { workspaceId: string; workflowId: string; executionId: string }
4746
userId?: string
4847
} {
@@ -61,6 +60,27 @@ function getExecutionContext(params?: ExportPresentationParams): {
6160
return { context: { workspaceId, workflowId, executionId }, userId }
6261
}
6362

63+
export async function readGoogleSlidesExportResponse(response: Response): Promise<Buffer> {
64+
if (!response.ok) {
65+
let errorMessage = `Failed to export presentation (status ${response.status})`
66+
try {
67+
const text = await readResponseTextWithLimit(response, {
68+
maxBytes: 64 * 1024,
69+
label: 'Google Slides export error response',
70+
})
71+
const data = JSON.parse(text)
72+
errorMessage = data.error?.message || errorMessage
73+
logger.error('Drive API error during export:', { data })
74+
} catch {}
75+
throw new Error(errorMessage)
76+
}
77+
78+
return readResponseToBufferWithLimit(response, {
79+
maxBytes: MAX_GOOGLE_SLIDES_EXPORT_BYTES,
80+
label: 'Google Slides export',
81+
})
82+
}
83+
6484
export const exportPresentationTool: ToolConfig<
6585
ExportPresentationParams,
6686
ExportPresentationResponse
@@ -111,52 +131,23 @@ export const exportPresentationTool: ToolConfig<
111131
},
112132

113133
transformResponse: async (response: Response, params) => {
114-
if (!response.ok) {
115-
let errorMessage = `Failed to export presentation (status ${response.status})`
116-
try {
117-
const text = await readResponseTextWithLimit(response, {
118-
maxBytes: 64 * 1024,
119-
label: 'Google Slides export error response',
120-
})
121-
const data = JSON.parse(text)
122-
errorMessage = data.error?.message || errorMessage
123-
logger.error('Drive API error during export:', { data })
124-
} catch {
125-
// Body wasn't JSON — fall through with default error message.
126-
}
127-
throw new Error(errorMessage)
128-
}
129-
130-
const buffer = await readResponseToBufferWithLimit(response, {
131-
maxBytes: MAX_GOOGLE_SLIDES_EXPORT_BYTES,
132-
label: 'Google Slides export',
133-
})
134-
134+
const buffer = await readGoogleSlidesExportResponse(response)
135135
const presentationId = params?.presentationId?.trim() || ''
136136
const format = (params?.exportFormat || 'PDF').toUpperCase()
137137
const mime = FORMAT_TO_MIME[format] ?? 'application/octet-stream'
138-
const { context, userId } = getExecutionContext(params)
139-
const filename = `${presentationId || 'presentation'}.${format.toLowerCase()}`
140-
const userFile = context
141-
? await uploadExecutionFile(context, Buffer.from(buffer), filename, mime, userId)
142-
: undefined
143-
if (!userFile && buffer.length > MAX_LEGACY_INLINE_EXPORT_BYTES) {
138+
if (buffer.length > MAX_LEGACY_INLINE_EXPORT_BYTES) {
144139
throw new PayloadSizeLimitError({
145140
label: 'Google Slides legacy inline export',
146141
maxBytes: MAX_LEGACY_INLINE_EXPORT_BYTES,
147142
observedBytes: buffer.length,
148143
})
149144
}
150-
const contentBase64 =
151-
!userFile && buffer.length <= MAX_LEGACY_INLINE_EXPORT_BYTES
152-
? buffer.toString('base64')
153-
: undefined
145+
const contentBase64 = buffer.toString('base64')
154146

155147
return {
156148
success: true,
157149
output: {
158-
...(userFile ? { file: { ...userFile, mimeType: mime } } : {}),
159-
...(contentBase64 ? { contentBase64 } : {}),
150+
contentBase64,
160151
mimeType: mime,
161152
sizeBytes: buffer.length,
162153
metadata: {

apps/sim/tools/index.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ import * as toolsUtilsServer from '@/tools/utils.server'
4444

4545
const logger = createLogger('Tools')
4646

47+
type ToolTransformResponse = NonNullable<ToolConfig['transformResponse']>
48+
49+
async function getServerTransformResponse(
50+
toolId: string
51+
): Promise<ToolTransformResponse | undefined> {
52+
switch (toolId) {
53+
case 'google_slides_export_presentation':
54+
return (await import('@/tools/google_slides/export_presentation.server'))
55+
.transformGoogleSlidesExportResponse
56+
case 'typeform_files':
57+
return (await import('@/tools/typeform/files.server')).transformTypeformFilesResponse
58+
default:
59+
return undefined
60+
}
61+
}
62+
4763
interface ToolExecutionScope {
4864
workspaceId?: string
4965
workflowId?: string
@@ -1728,7 +1744,9 @@ async function executeToolRequest(
17281744
}
17291745

17301746
// Success case: use transformResponse if available
1731-
if (tool.transformResponse) {
1747+
const transformResponse = (await getServerTransformResponse(toolId)) ?? tool.transformResponse
1748+
1749+
if (transformResponse) {
17321750
try {
17331751
// Create a mock response object that provides the methods transformResponse needs
17341752
const mockResponse = {
@@ -1743,7 +1761,7 @@ async function executeToolRequest(
17431761
blob: () => response.blob(),
17441762
} as Response
17451763

1746-
const data = await tool.transformResponse(mockResponse, params)
1764+
const data = await transformResponse(mockResponse, params)
17471765
return data
17481766
} catch (transformError) {
17491767
logger.error(`[${requestId}] Transform response error for ${toolId}:`, {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
2+
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
3+
import {
4+
getTypeformExecutionContext,
5+
MAX_LEGACY_INLINE_FILE_BYTES,
6+
readTypeformFileResponse,
7+
} from '@/tools/typeform/files'
8+
import type { TypeformFilesParams, TypeformFilesResponse } from '@/tools/typeform/types'
9+
10+
export async function transformTypeformFilesResponse(
11+
response: Response,
12+
params?: TypeformFilesParams
13+
): Promise<TypeformFilesResponse> {
14+
const { buffer, contentType, filename, fileUrl } = await readTypeformFileResponse(
15+
response,
16+
params
17+
)
18+
const { context, userId } = getTypeformExecutionContext(params)
19+
const storedFile = context
20+
? {
21+
...(await uploadExecutionFile(context, buffer, filename, contentType, userId)),
22+
mimeType: contentType,
23+
}
24+
: undefined
25+
26+
if (!storedFile && buffer.length > MAX_LEGACY_INLINE_FILE_BYTES) {
27+
throw new PayloadSizeLimitError({
28+
label: 'Typeform legacy inline file',
29+
maxBytes: MAX_LEGACY_INLINE_FILE_BYTES,
30+
observedBytes: buffer.length,
31+
})
32+
}
33+
34+
return {
35+
success: true,
36+
output: {
37+
fileUrl: storedFile?.url || fileUrl || '',
38+
file: storedFile ?? {
39+
name: filename,
40+
mimeType: contentType,
41+
data: buffer.toString('base64'),
42+
size: buffer.length,
43+
},
44+
contentType,
45+
filename,
46+
},
47+
}
48+
}

apps/sim/tools/typeform/files.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ vi.mock('@/lib/uploads/contexts/execution', () => ({
1212
}))
1313

1414
import { filesTool } from '@/tools/typeform/files'
15+
import { transformTypeformFilesResponse } from '@/tools/typeform/files.server'
1516

1617
describe('Typeform files tool', () => {
1718
beforeEach(() => {
@@ -36,7 +37,7 @@ describe('Typeform files tool', () => {
3637
},
3738
})
3839

39-
const result = await filesTool.transformResponse?.(response, {
40+
const result = await transformTypeformFilesResponse(response, {
4041
formId: 'form-1',
4142
responseId: 'response-1',
4243
fieldId: 'field-1',

0 commit comments

Comments
 (0)