Skip to content

Commit 3de1f07

Browse files
authored
Merge branch 'staging' into feat/block/airweave-integration
2 parents 309a956 + 2d7e6c9 commit 3de1f07

File tree

1,667 files changed

+66355
-16132
lines changed

Some content is hidden

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

1,667 files changed

+66355
-16132
lines changed

.claude/commands/add-block.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,109 @@ export const {ServiceName}Block: BlockConfig = {
183183
}
184184
```
185185

186+
## File Input Handling
187+
188+
When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.
189+
190+
### Basic/Advanced File Pattern
191+
192+
```typescript
193+
// Basic mode: Visual file upload
194+
{
195+
id: 'uploadFile',
196+
title: 'File',
197+
type: 'file-upload',
198+
canonicalParamId: 'file', // Both map to 'file' param
199+
placeholder: 'Upload file',
200+
mode: 'basic',
201+
multiple: false,
202+
required: true,
203+
condition: { field: 'operation', value: 'upload' },
204+
},
205+
// Advanced mode: Reference from other blocks
206+
{
207+
id: 'fileRef',
208+
title: 'File',
209+
type: 'short-input',
210+
canonicalParamId: 'file', // Both map to 'file' param
211+
placeholder: 'Reference file (e.g., {{file_block.output}})',
212+
mode: 'advanced',
213+
required: true,
214+
condition: { field: 'operation', value: 'upload' },
215+
},
216+
```
217+
218+
**Critical constraints:**
219+
- `canonicalParamId` must NOT match any subblock's `id` in the same block
220+
- Values are stored under subblock `id`, not `canonicalParamId`
221+
222+
### Normalizing File Input in tools.config
223+
224+
Use `normalizeFileInput` to handle all input variants:
225+
226+
```typescript
227+
import { normalizeFileInput } from '@/blocks/utils'
228+
229+
tools: {
230+
access: ['service_upload'],
231+
config: {
232+
tool: (params) => {
233+
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
234+
const normalizedFile = normalizeFileInput(
235+
params.uploadFile || params.fileRef || params.fileContent,
236+
{ single: true }
237+
)
238+
if (normalizedFile) {
239+
params.file = normalizedFile
240+
}
241+
return `service_${params.operation}`
242+
},
243+
},
244+
}
245+
```
246+
247+
**Why this pattern?**
248+
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
249+
- `canonicalParamId` only controls UI/schema mapping, not runtime values
250+
- `normalizeFileInput` handles JSON strings from advanced mode template resolution
251+
252+
### File Input Types in `inputs`
253+
254+
Use `type: 'json'` for file inputs:
255+
256+
```typescript
257+
inputs: {
258+
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
259+
fileRef: { type: 'json', description: 'File reference from previous block' },
260+
// Legacy field for backwards compatibility
261+
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
262+
}
263+
```
264+
265+
### Multiple Files
266+
267+
For multiple file uploads:
268+
269+
```typescript
270+
{
271+
id: 'attachments',
272+
title: 'Attachments',
273+
type: 'file-upload',
274+
multiple: true, // Allow multiple files
275+
maxSize: 25, // Max size in MB per file
276+
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
277+
}
278+
279+
// In tools.config:
280+
const normalizedFiles = normalizeFileInput(
281+
params.attachments || params.attachmentRefs,
282+
// No { single: true } - returns array
283+
)
284+
if (normalizedFiles) {
285+
params.files = normalizedFiles
286+
}
287+
```
288+
186289
## Condition Syntax
187290

188291
Controls when a field is shown based on other field values.

.claude/commands/add-integration.md

Lines changed: 235 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,15 @@ export const {Service}Block: BlockConfig = {
206206
}
207207
```
208208

209-
**Critical:**
210-
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
211-
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
212-
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
209+
**Critical Canonical Param Rules:**
210+
- `canonicalParamId` must NOT match any subblock's `id` in the block
211+
- `canonicalParamId` must be unique per operation/condition context
212+
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
213+
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
214+
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
215+
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
216+
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
217+
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
213218

214219
## Step 4: Add Icon
215220

@@ -457,11 +462,236 @@ You can usually find this in the service's brand/press kit page, or copy it from
457462
Paste the SVG code here and I'll convert it to a React component.
458463
```
459464

460-
## Common Gotchas
465+
## File Handling
466+
467+
When your integration handles file uploads or downloads, follow these patterns to work with `UserFile` objects consistently.
468+
469+
### What is a UserFile?
470+
471+
A `UserFile` is the standard file representation in Sim:
472+
473+
```typescript
474+
interface UserFile {
475+
id: string // Unique identifier
476+
name: string // Original filename
477+
url: string // Presigned URL for download
478+
size: number // File size in bytes
479+
type: string // MIME type (e.g., 'application/pdf')
480+
base64?: string // Optional base64 content (if small file)
481+
key?: string // Internal storage key
482+
context?: object // Storage context metadata
483+
}
484+
```
485+
486+
### File Input Pattern (Uploads)
487+
488+
For tools that accept file uploads, **always route through an internal API endpoint** rather than calling external APIs directly. This ensures proper file content retrieval.
489+
490+
#### 1. Block SubBlocks for File Input
491+
492+
Use the basic/advanced mode pattern:
493+
494+
```typescript
495+
// Basic mode: File upload UI
496+
{
497+
id: 'uploadFile',
498+
title: 'File',
499+
type: 'file-upload',
500+
canonicalParamId: 'file', // Maps to 'file' param
501+
placeholder: 'Upload file',
502+
mode: 'basic',
503+
multiple: false,
504+
required: true,
505+
condition: { field: 'operation', value: 'upload' },
506+
},
507+
// Advanced mode: Reference from previous block
508+
{
509+
id: 'fileRef',
510+
title: 'File',
511+
type: 'short-input',
512+
canonicalParamId: 'file', // Same canonical param
513+
placeholder: 'Reference file (e.g., {{file_block.output}})',
514+
mode: 'advanced',
515+
required: true,
516+
condition: { field: 'operation', value: 'upload' },
517+
},
518+
```
519+
520+
**Critical:** `canonicalParamId` must NOT match any subblock `id`.
521+
522+
#### 2. Normalize File Input in Block Config
523+
524+
In `tools.config.tool`, use `normalizeFileInput` to handle all input variants:
525+
526+
```typescript
527+
import { normalizeFileInput } from '@/blocks/utils'
528+
529+
tools: {
530+
config: {
531+
tool: (params) => {
532+
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
533+
const normalizedFile = normalizeFileInput(
534+
params.uploadFile || params.fileRef || params.fileContent,
535+
{ single: true }
536+
)
537+
if (normalizedFile) {
538+
params.file = normalizedFile
539+
}
540+
return `{service}_${params.operation}`
541+
},
542+
},
543+
}
544+
```
545+
546+
#### 3. Create Internal API Route
547+
548+
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
549+
550+
```typescript
551+
import { createLogger } from '@sim/logger'
552+
import { NextResponse, type NextRequest } from 'next/server'
553+
import { z } from 'zod'
554+
import { checkInternalAuth } from '@/lib/auth/hybrid'
555+
import { generateRequestId } from '@/lib/core/utils/request'
556+
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
557+
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
558+
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
559+
560+
const logger = createLogger('{Service}UploadAPI')
561+
562+
const RequestSchema = z.object({
563+
accessToken: z.string(),
564+
file: FileInputSchema.optional().nullable(),
565+
// Legacy field for backwards compatibility
566+
fileContent: z.string().optional().nullable(),
567+
// ... other params
568+
})
569+
570+
export async function POST(request: NextRequest) {
571+
const requestId = generateRequestId()
572+
573+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
574+
if (!authResult.success) {
575+
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
576+
}
577+
578+
const body = await request.json()
579+
const data = RequestSchema.parse(body)
580+
581+
let fileBuffer: Buffer
582+
let fileName: string
583+
584+
// Prefer UserFile input, fall back to legacy base64
585+
if (data.file) {
586+
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
587+
if (userFiles.length === 0) {
588+
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
589+
}
590+
const userFile = userFiles[0]
591+
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
592+
fileName = userFile.name
593+
} else if (data.fileContent) {
594+
// Legacy: base64 string (backwards compatibility)
595+
fileBuffer = Buffer.from(data.fileContent, 'base64')
596+
fileName = 'file'
597+
} else {
598+
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
599+
}
600+
601+
// Now call external API with fileBuffer
602+
const response = await fetch('https://api.{service}.com/upload', {
603+
method: 'POST',
604+
headers: { Authorization: `Bearer ${data.accessToken}` },
605+
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
606+
})
607+
608+
// ... handle response
609+
}
610+
```
611+
612+
#### 4. Update Tool to Use Internal Route
613+
614+
```typescript
615+
export const {service}UploadTool: ToolConfig<Params, Response> = {
616+
id: '{service}_upload',
617+
// ...
618+
params: {
619+
file: { type: 'file', required: false, visibility: 'user-or-llm' },
620+
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
621+
},
622+
request: {
623+
url: '/api/tools/{service}/upload', // Internal route
624+
method: 'POST',
625+
body: (params) => ({
626+
accessToken: params.accessToken,
627+
file: params.file,
628+
fileContent: params.fileContent,
629+
}),
630+
},
631+
}
632+
```
633+
634+
### File Output Pattern (Downloads)
635+
636+
For tools that return files, use `FileToolProcessor` to store files and return `UserFile` objects.
637+
638+
#### In Tool transformResponse
639+
640+
```typescript
641+
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
642+
643+
transformResponse: async (response, context) => {
644+
const data = await response.json()
645+
646+
// Process file outputs to UserFile objects
647+
const fileProcessor = new FileToolProcessor(context)
648+
const file = await fileProcessor.processFileData({
649+
data: data.content, // base64 or buffer
650+
mimeType: data.mimeType,
651+
filename: data.filename,
652+
})
653+
654+
return {
655+
success: true,
656+
output: { file },
657+
}
658+
}
659+
```
660+
661+
#### In API Route (for complex file handling)
662+
663+
```typescript
664+
// Return file data that FileToolProcessor can handle
665+
return NextResponse.json({
666+
success: true,
667+
output: {
668+
file: {
669+
data: base64Content,
670+
mimeType: 'application/pdf',
671+
filename: 'document.pdf',
672+
},
673+
},
674+
})
675+
```
676+
677+
### Key Helpers Reference
678+
679+
| Helper | Location | Purpose |
680+
|--------|----------|---------|
681+
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
682+
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
683+
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get file Buffer from UserFile |
684+
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
685+
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
686+
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |
687+
688+
### Common Gotchas
461689

462690
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
463691
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
464692
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
465693
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
466694
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
467695
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
696+
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
697+
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility

0 commit comments

Comments
 (0)