Skip to content

Commit 46822e9

Browse files
authored
v0.5.80: lock feature, enterprise modules, time formatting consolidation, files, UX and UI improvements, longer timeouts
2 parents 2bb6833 + 36ec68d commit 46822e9

File tree

455 files changed

+36729
-6375
lines changed

Some content is hidden

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

455 files changed

+36729
-6375
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: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,11 +457,236 @@ You can usually find this in the service's brand/press kit page, or copy it from
457457
Paste the SVG code here and I'll convert it to a React component.
458458
```
459459

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

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

0 commit comments

Comments
 (0)