diff --git a/.gitignore b/.gitignore index 6617532dd8..2d791fcbf6 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ start-collector.sh ## Helm Chart Tests helm/sim/test i18n.cache + +## Claude Code +.claude/launch.json +.claude/worktrees/ diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dcd5741f2b..9e68974089 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -939,6 +939,25 @@ export function GoogleIcon(props: SVGProps) { ) } +export function DevinIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function DiscordIcon(props: SVGProps) { return ( ) { ) } +export function GoogleTasksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function SupabaseIcon(props: SVGProps) { const id = useId() const gradient0 = `supabase_paint0_${id}` @@ -3430,6 +3464,23 @@ export const ResendIcon = (props: SVGProps) => ( ) +export const GoogleBigQueryIcon = (props: SVGProps) => ( + + + + + +) + export const GoogleVaultIcon = (props: SVGProps) => ( ) { ) } +export function GoogleTranslateIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function DsPyIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 5121253240..822ce48aeb 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -25,6 +25,7 @@ import { ConfluenceIcon, CursorIcon, DatadogIcon, + DevinIcon, DiscordIcon, DocumentIcon, DropboxIcon, @@ -42,6 +43,7 @@ import { GitLabIcon, GmailIcon, GongIcon, + GoogleBigQueryIcon, GoogleBooksIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -52,6 +54,8 @@ import { GoogleMapsIcon, GoogleSheetsIcon, GoogleSlidesIcon, + GoogleTasksIcon, + GoogleTranslateIcon, GoogleVaultIcon, GrafanaIcon, GrainIcon, @@ -171,6 +175,7 @@ export const blockTypeToIconMap: Record = { confluence_v2: ConfluenceIcon, cursor_v2: CursorIcon, datadog: DatadogIcon, + devin: DevinIcon, discord: DiscordIcon, dropbox: DropboxIcon, dspy: DsPyIcon, @@ -187,6 +192,7 @@ export const blockTypeToIconMap: Record = { gitlab: GitLabIcon, gmail_v2: GmailIcon, gong: GongIcon, + google_bigquery: GoogleBigQueryIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, google_docs: GoogleDocsIcon, @@ -197,6 +203,8 @@ export const blockTypeToIconMap: Record = { google_search: GoogleIcon, google_sheets_v2: GoogleSheetsIcon, google_slides_v2: GoogleSlidesIcon, + google_tasks: GoogleTasksIcon, + google_translate: GoogleTranslateIcon, google_vault: GoogleVaultIcon, grafana: GrafanaIcon, grain: GrainIcon, diff --git a/apps/docs/content/docs/en/execution/basics.mdx b/apps/docs/content/docs/en/execution/basics.mdx index 87c8df7781..1541831e77 100644 --- a/apps/docs/content/docs/en/execution/basics.mdx +++ b/apps/docs/content/docs/en/execution/basics.mdx @@ -97,6 +97,7 @@ Understanding these core principles will help you build better workflows: 3. **Smart Data Flow**: Outputs flow automatically to connected blocks 4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths 5. **State Persistence**: All block outputs and execution details are preserved for debugging +6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops ## Next Steps diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 7ee0f0e73e..8ffb145f86 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -326,6 +326,8 @@ Get details about a specific version of a Confluence page. | --------- | ---- | ----------- | | `ts` | string | ISO 8601 timestamp of the operation | | `pageId` | string | ID of the page | +| `title` | string | Page title at this version | +| `content` | string | Page content with HTML tags stripped at this version | | `version` | object | Detailed version information | | ↳ `number` | number | Version number | | ↳ `message` | string | Version message | @@ -336,6 +338,9 @@ Get details about a specific version of a Confluence page. | ↳ `collaborators` | array | List of collaborator account IDs for this version | | ↳ `prevVersion` | number | Previous version number | | ↳ `nextVersion` | number | Next version number | +| `body` | object | Raw page body content in storage format at this version | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | ### `confluence_list_page_properties` @@ -1008,6 +1013,85 @@ Get details about a specific Confluence space. | ↳ `value` | string | Description text content | | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | +### `confluence_create_space` + +Create a new Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `name` | string | Yes | Name for the new space | +| `key` | string | Yes | Unique key for the space \(uppercase, no spaces\) | +| `description` | string | No | Description for the new space | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Created space ID | +| `name` | string | Space name | +| `key` | string | Space key | +| `type` | string | Space type | +| `status` | string | Space status | +| `url` | string | URL to view the space | +| `homepageId` | string | Homepage ID | +| `description` | object | Space description | +| ↳ `value` | string | Description text content | +| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | + +### `confluence_update_space` + +Update a Confluence space name or description. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | ID of the space to update | +| `name` | string | No | New name for the space | +| `description` | string | No | New description for the space | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Updated space ID | +| `name` | string | Space name | +| `key` | string | Space key | +| `type` | string | Space type | +| `status` | string | Space status | +| `url` | string | URL to view the space | +| `description` | object | Space description | +| ↳ `value` | string | Description text content | +| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | + +### `confluence_delete_space` + +Delete a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | ID of the space to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Deleted space ID | +| `deleted` | boolean | Deletion status | + ### `confluence_list_spaces` List all Confluence spaces accessible to the user. @@ -1040,4 +1124,311 @@ List all Confluence spaces accessible to the user. | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | | `nextCursor` | string | Cursor for fetching the next page of results | +### `confluence_list_space_properties` + +List properties on a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to list properties for | +| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `properties` | array | Array of space properties | +| ↳ `id` | string | Property ID | +| ↳ `key` | string | Property key | +| ↳ `value` | json | Property value | +| `spaceId` | string | Space ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_create_space_property` + +Create a property on a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to create the property on | +| `key` | string | Yes | Property key/name | +| `value` | json | No | Property value \(JSON\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `propertyId` | string | Created property ID | +| `key` | string | Property key | +| `value` | json | Property value | +| `spaceId` | string | Space ID | + +### `confluence_delete_space_property` + +Delete a property from a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID the property belongs to | +| `propertyId` | string | Yes | Property ID to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Space ID | +| `propertyId` | string | Deleted property ID | +| `deleted` | boolean | Deletion status | + +### `confluence_list_space_permissions` + +List permissions for a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to list permissions for | +| `limit` | number | No | Maximum number of permissions to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `permissions` | array | Array of space permissions | +| ↳ `id` | string | Permission ID | +| ↳ `principalType` | string | Principal type \(user, group, role\) | +| ↳ `principalId` | string | Principal ID | +| ↳ `operationKey` | string | Operation key \(read, create, delete, etc.\) | +| ↳ `operationTargetType` | string | Target type \(page, blogpost, space, etc.\) | +| ↳ `anonymousAccess` | boolean | Whether anonymous access is allowed | +| ↳ `unlicensedAccess` | boolean | Whether unlicensed access is allowed | +| `spaceId` | string | Space ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_page_descendants` + +Get all descendants of a Confluence page recursively. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Page ID to get descendants for | +| `limit` | number | No | Maximum number of descendants to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `descendants` | array | Array of descendant pages | +| ↳ `id` | string | Page ID | +| ↳ `title` | string | Page title | +| ↳ `type` | string | Content type \(page, whiteboard, database, etc.\) | +| ↳ `status` | string | Page status | +| ↳ `spaceId` | string | Space ID | +| ↳ `parentId` | string | Parent page ID | +| ↳ `childPosition` | number | Position among siblings | +| ↳ `depth` | number | Depth in the hierarchy | +| `pageId` | string | Parent page ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_list_tasks` + +List inline tasks from Confluence. Optionally filter by page, space, assignee, or status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | No | Filter tasks by page ID | +| `spaceId` | string | No | Filter tasks by space ID | +| `assignedTo` | string | No | Filter tasks by assignee account ID | +| `status` | string | No | Filter tasks by status \(complete or incomplete\) | +| `limit` | number | No | Maximum number of tasks to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `tasks` | array | Array of Confluence tasks | +| ↳ `id` | string | Task ID | +| ↳ `localId` | string | Local task ID | +| ↳ `spaceId` | string | Space ID | +| ↳ `pageId` | string | Page ID | +| ↳ `blogPostId` | string | Blog post ID | +| ↳ `status` | string | Task status \(complete or incomplete\) | +| ↳ `body` | string | Task body content in storage format | +| ↳ `createdBy` | string | Creator account ID | +| ↳ `assignedTo` | string | Assignee account ID | +| ↳ `completedBy` | string | Completer account ID | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| ↳ `dueAt` | string | Due date | +| ↳ `completedAt` | string | Completion timestamp | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_task` + +Get a specific Confluence inline task by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `taskId` | string | Yes | The ID of the task to retrieve | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `id` | string | Task ID | +| `localId` | string | Local task ID | +| `spaceId` | string | Space ID | +| `pageId` | string | Page ID | +| `blogPostId` | string | Blog post ID | +| `status` | string | Task status \(complete or incomplete\) | +| `body` | string | Task body content in storage format | +| `createdBy` | string | Creator account ID | +| `assignedTo` | string | Assignee account ID | +| `completedBy` | string | Completer account ID | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `dueAt` | string | Due date | +| `completedAt` | string | Completion timestamp | + +### `confluence_update_task` + +Update the status of a Confluence inline task (complete or incomplete). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `taskId` | string | Yes | The ID of the task to update | +| `status` | string | Yes | New status for the task \(complete or incomplete\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `id` | string | Task ID | +| `localId` | string | Local task ID | +| `spaceId` | string | Space ID | +| `pageId` | string | Page ID | +| `blogPostId` | string | Blog post ID | +| `status` | string | Updated task status | +| `body` | string | Task body content in storage format | +| `createdBy` | string | Creator account ID | +| `assignedTo` | string | Assignee account ID | +| `completedBy` | string | Completer account ID | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `dueAt` | string | Due date | +| `completedAt` | string | Completion timestamp | + +### `confluence_update_blogpost` + +Update an existing Confluence blog post title and/or content. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `blogPostId` | string | Yes | The ID of the blog post to update | +| `title` | string | No | New title for the blog post | +| `content` | string | No | New content for the blog post in storage format | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `blogPostId` | string | Updated blog post ID | +| `title` | string | Blog post title | +| `status` | string | Blog post status | +| `spaceId` | string | Space ID | +| `version` | json | Version information | +| `url` | string | URL to view the blog post | + +### `confluence_delete_blogpost` + +Delete a Confluence blog post. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `blogPostId` | string | Yes | The ID of the blog post to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `blogPostId` | string | Deleted blog post ID | +| `deleted` | boolean | Deletion status | + +### `confluence_get_user` + +Get display name and profile info for a Confluence user by account ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `accountId` | string | Yes | The Atlassian account ID of the user to look up | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `accountId` | string | Atlassian account ID of the user | +| `displayName` | string | Display name of the user | +| `email` | string | Email address of the user | +| `accountType` | string | Account type \(e.g., atlassian, app, customer\) | +| `profilePicture` | string | Path to the user profile picture | +| `publicName` | string | Public name of the user | + diff --git a/apps/docs/content/docs/en/tools/devin.mdx b/apps/docs/content/docs/en/tools/devin.mdx new file mode 100644 index 0000000000..4ea369fccc --- /dev/null +++ b/apps/docs/content/docs/en/tools/devin.mdx @@ -0,0 +1,157 @@ +--- +title: Devin +description: Autonomous AI software engineer +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Devin](https://devin.ai/) is an autonomous AI software engineer by Cognition that can independently write, run, debug, and deploy code. + +With Devin, you can: + +- **Automate coding tasks**: Assign software engineering tasks and let Devin autonomously write, test, and iterate on code +- **Manage sessions**: Create, monitor, and interact with Devin sessions to track progress on assigned tasks +- **Guide active work**: Send messages to running sessions to provide additional context, redirect efforts, or answer questions +- **Retrieve structured output**: Poll completed sessions for pull requests, structured results, and detailed status +- **Control costs**: Set ACU (Autonomous Compute Unit) limits to cap spending on long-running tasks +- **Standardize workflows**: Use playbook IDs to apply repeatable task patterns across sessions + +In Sim, the Devin integration enables your agents to programmatically manage Devin sessions as part of their workflows: + +- **Create sessions**: Kick off new Devin sessions with a prompt describing the task, optional playbook, ACU limits, and tags +- **Get session details**: Retrieve the full state of a session including status, pull requests, structured output, and resource consumption +- **List sessions**: Query all sessions in your organization with optional pagination +- **Send messages**: Communicate with active or suspended sessions to provide guidance, and automatically resume suspended sessions + +This allows for powerful automation scenarios such as triggering code generation from upstream events, polling for completion before consuming results, orchestrating multi-step development pipelines, and integrating Devin's output into broader agent workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code. + + + +## Tools + +### `devin_create_session` + +Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | +| `prompt` | string | Yes | The task prompt for Devin to work on | +| `playbookId` | string | No | Optional playbook ID to guide the session | +| `maxAcuLimit` | number | No | Maximum ACU limit for the session | +| `tags` | string | No | Comma-separated tags for the session | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sessionId` | string | Unique identifier for the session | +| `url` | string | URL to view the session in the Devin UI | +| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) | +| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) | +| `title` | string | Session title | +| `createdAt` | number | Unix timestamp when the session was created | +| `updatedAt` | number | Unix timestamp when the session was last updated | +| `acusConsumed` | number | ACUs consumed by the session | +| `tags` | json | Tags associated with the session | +| `pullRequests` | json | Pull requests created during the session | +| `structuredOutput` | json | Structured output from the session | +| `playbookId` | string | Associated playbook ID | + +### `devin_get_session` + +Retrieve details of an existing Devin session including status, tags, pull requests, and structured output. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | +| `sessionId` | string | Yes | The session ID to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sessionId` | string | Unique identifier for the session | +| `url` | string | URL to view the session in the Devin UI | +| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) | +| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) | +| `title` | string | Session title | +| `createdAt` | number | Unix timestamp when the session was created | +| `updatedAt` | number | Unix timestamp when the session was last updated | +| `acusConsumed` | number | ACUs consumed by the session | +| `tags` | json | Tags associated with the session | +| `pullRequests` | json | Pull requests created during the session | +| `structuredOutput` | json | Structured output from the session | +| `playbookId` | string | Associated playbook ID | + +### `devin_list_sessions` + +List Devin sessions in the organization. Returns up to 100 sessions by default. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | +| `limit` | number | No | Maximum number of sessions to return \(1-200, default: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sessions` | array | List of Devin sessions | +| ↳ `sessionId` | string | Unique identifier for the session | +| ↳ `url` | string | URL to view the session | +| ↳ `status` | string | Session status | +| ↳ `statusDetail` | string | Detailed status | +| ↳ `title` | string | Session title | +| ↳ `createdAt` | number | Creation timestamp \(Unix\) | +| ↳ `updatedAt` | number | Last updated timestamp \(Unix\) | +| ↳ `tags` | json | Session tags | + +### `devin_send_message` + +Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) | +| `sessionId` | string | Yes | The session ID to send the message to | +| `message` | string | Yes | The message to send to Devin | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sessionId` | string | Unique identifier for the session | +| `url` | string | URL to view the session in the Devin UI | +| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) | +| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) | +| `title` | string | Session title | +| `createdAt` | number | Unix timestamp when the session was created | +| `updatedAt` | number | Unix timestamp when the session was last updated | +| `acusConsumed` | number | ACUs consumed by the session | +| `tags` | json | Tags associated with the session | +| `pullRequests` | json | Pull requests created during the session | +| `structuredOutput` | json | Structured output from the session | +| `playbookId` | string | Associated playbook ID | + + diff --git a/apps/docs/content/docs/en/tools/google_bigquery.mdx b/apps/docs/content/docs/en/tools/google_bigquery.mdx new file mode 100644 index 0000000000..38cc4edaad --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_bigquery.mdx @@ -0,0 +1,168 @@ +--- +title: Google BigQuery +description: Query, list, and insert data in Google BigQuery +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Google BigQuery](https://cloud.google.com/bigquery) is Google Cloud's fully managed, serverless data warehouse designed for large-scale data analytics. BigQuery lets you run fast SQL queries on massive datasets, making it ideal for business intelligence, data exploration, and machine learning pipelines. It supports standard SQL, streaming inserts, and integrates with the broader Google Cloud ecosystem. + +In Sim, the Google BigQuery integration allows your agents to query datasets, list tables, inspect schemas, and insert rows as part of automated workflows. This enables use cases such as automated reporting, data pipeline orchestration, real-time data ingestion, and analytics-driven decision making. By connecting Sim with BigQuery, your agents can pull insights from petabytes of data, write results back to tables, and keep your analytics workflows running without manual intervention. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows. + + + +## Tools + +### `google_bigquery_query` + +Run a SQL query against Google BigQuery and return the results + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `query` | string | Yes | SQL query to execute | +| `useLegacySql` | boolean | No | Whether to use legacy SQL syntax \(default: false\) | +| `maxResults` | number | No | Maximum number of rows to return | +| `defaultDatasetId` | string | No | Default dataset for unqualified table names | +| `location` | string | No | Processing location \(e.g., "US", "EU"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `columns` | array | Array of column names from the query result | +| `rows` | array | Array of row objects keyed by column name | +| `totalRows` | string | Total number of rows in the complete result set | +| `jobComplete` | boolean | Whether the query completed within the timeout | +| `totalBytesProcessed` | string | Total bytes processed by the query | +| `cacheHit` | boolean | Whether the query result was served from cache | +| `jobReference` | object | Job reference \(useful when jobComplete is false\) | +| ↳ `projectId` | string | Project ID containing the job | +| ↳ `jobId` | string | Unique job identifier | +| ↳ `location` | string | Geographic location of the job | +| `pageToken` | string | Token for fetching additional result pages | + +### `google_bigquery_list_datasets` + +List all datasets in a Google BigQuery project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `maxResults` | number | No | Maximum number of datasets to return | +| `pageToken` | string | No | Token for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `datasets` | array | Array of dataset objects | +| ↳ `datasetId` | string | Unique dataset identifier | +| ↳ `projectId` | string | Project ID containing this dataset | +| ↳ `friendlyName` | string | Descriptive name for the dataset | +| ↳ `location` | string | Geographic location where the data resides | +| `nextPageToken` | string | Token for fetching next page of results | + +### `google_bigquery_list_tables` + +List all tables in a Google BigQuery dataset + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `maxResults` | number | No | Maximum number of tables to return | +| `pageToken` | string | No | Token for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tables` | array | Array of table objects | +| ↳ `tableId` | string | Table identifier | +| ↳ `datasetId` | string | Dataset ID containing this table | +| ↳ `projectId` | string | Project ID containing this table | +| ↳ `type` | string | Table type \(TABLE, VIEW, EXTERNAL, etc.\) | +| ↳ `friendlyName` | string | User-friendly name for the table | +| ↳ `creationTime` | string | Time when created, in milliseconds since epoch | +| `totalItems` | number | Total number of tables in the dataset | +| `nextPageToken` | string | Token for fetching next page of results | + +### `google_bigquery_get_table` + +Get metadata and schema for a Google BigQuery table + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `tableId` | string | Yes | BigQuery table ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tableId` | string | Table ID | +| `datasetId` | string | Dataset ID | +| `projectId` | string | Project ID | +| `type` | string | Table type \(TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL\) | +| `description` | string | Table description | +| `numRows` | string | Total number of rows | +| `numBytes` | string | Total size in bytes, excluding data in streaming buffer | +| `schema` | array | Array of column definitions | +| ↳ `name` | string | Column name | +| ↳ `type` | string | Data type \(STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.\) | +| ↳ `mode` | string | Column mode \(NULLABLE, REQUIRED, or REPEATED\) | +| ↳ `description` | string | Column description | +| `creationTime` | string | Table creation time \(milliseconds since epoch\) | +| `lastModifiedTime` | string | Last modification time \(milliseconds since epoch\) | +| `location` | string | Geographic location where the table resides | + +### `google_bigquery_insert_rows` + +Insert rows into a Google BigQuery table using streaming insert + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `tableId` | string | Yes | BigQuery table ID | +| `rows` | string | Yes | JSON array of row objects to insert | +| `skipInvalidRows` | boolean | No | Whether to insert valid rows even if some are invalid | +| `ignoreUnknownValues` | boolean | No | Whether to ignore columns not in the table schema | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `insertedRows` | number | Number of rows successfully inserted | +| `errors` | array | Array of per-row insertion errors \(empty if all succeeded\) | +| ↳ `index` | number | Zero-based index of the row that failed | +| ↳ `errors` | array | Error details for this row | +| ↳ `reason` | string | Short error code summarizing the error | +| ↳ `location` | string | Where the error occurred | +| ↳ `message` | string | Human-readable error description | + + diff --git a/apps/docs/content/docs/en/tools/google_books.mdx b/apps/docs/content/docs/en/tools/google_books.mdx index 2b370d1394..8e817dbc0d 100644 --- a/apps/docs/content/docs/en/tools/google_books.mdx +++ b/apps/docs/content/docs/en/tools/google_books.mdx @@ -10,6 +10,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#E0E0E0" /> +{/* MANUAL-CONTENT-START:intro */} +[Google Books](https://books.google.com) is Google's comprehensive book discovery and metadata service, providing access to millions of books from publishers, libraries, and digitized collections worldwide. The Google Books API enables programmatic search and retrieval of detailed book information including titles, authors, descriptions, ratings, and publication details. + +In Sim, the Google Books integration allows your agents to search for books and retrieve volume details as part of automated workflows. This enables use cases such as content research, reading list curation, bibliographic data enrichment, ISBN lookups, and knowledge gathering from published works. By connecting Sim with Google Books, your agents can discover and analyze book metadata, filter by availability or format, and incorporate literary references into their outputs—all without manual research. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details. diff --git a/apps/docs/content/docs/en/tools/google_tasks.mdx b/apps/docs/content/docs/en/tools/google_tasks.mdx new file mode 100644 index 0000000000..745b04c1e5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_tasks.mdx @@ -0,0 +1,205 @@ +--- +title: Google Tasks +description: Manage Google Tasks +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Google Tasks](https://support.google.com/tasks) is Google's lightweight task management service, integrated into Gmail, Google Calendar, and the standalone Google Tasks app. It provides a simple way to create, organize, and track to-do items with support for due dates, subtasks, and task lists. As part of Google Workspace, Google Tasks keeps your action items synchronized across all your devices. + +In Sim, the Google Tasks integration allows your agents to create, read, update, delete, and list tasks and task lists as part of automated workflows. This enables use cases such as automated task creation from incoming data, to-do list management based on workflow triggers, task status tracking, and deadline monitoring. By connecting Sim with Google Tasks, your agents can manage action items programmatically, keep teams organized, and ensure nothing falls through the cracks. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists. + + + +## Tools + +### `google_tasks_create` + +Create a new task in a Google Tasks list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) | +| `title` | string | Yes | Title of the task \(max 1024 characters\) | +| `notes` | string | No | Notes/description for the task \(max 8192 characters\) | +| `due` | string | No | Due date in RFC 3339 format \(e.g., 2025-06-03T00:00:00.000Z\) | +| `status` | string | No | Task status: "needsAction" or "completed" | +| `parent` | string | No | Parent task ID to create this task as a subtask. Omit for top-level tasks. | +| `previous` | string | No | Previous sibling task ID to position after. Omit to place first among siblings. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Task ID | +| `title` | string | Task title | +| `notes` | string | Task notes | +| `status` | string | Task status \(needsAction or completed\) | +| `due` | string | Due date | +| `updated` | string | Last modification time | +| `selfLink` | string | URL for the task | +| `webViewLink` | string | Link to task in Google Tasks UI | +| `parent` | string | Parent task ID | +| `position` | string | Position among sibling tasks | +| `completed` | string | Completion date | +| `deleted` | boolean | Whether the task is deleted | + +### `google_tasks_list` + +List all tasks in a Google Tasks list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) | +| `maxResults` | number | No | Maximum number of tasks to return \(default 20, max 100\) | +| `pageToken` | string | No | Token for pagination | +| `showCompleted` | boolean | No | Whether to show completed tasks \(default true\) | +| `showDeleted` | boolean | No | Whether to show deleted tasks \(default false\) | +| `showHidden` | boolean | No | Whether to show hidden tasks \(default false\) | +| `dueMin` | string | No | Lower bound for due date filter \(RFC 3339 timestamp\) | +| `dueMax` | string | No | Upper bound for due date filter \(RFC 3339 timestamp\) | +| `completedMin` | string | No | Lower bound for task completion date \(RFC 3339 timestamp\) | +| `completedMax` | string | No | Upper bound for task completion date \(RFC 3339 timestamp\) | +| `updatedMin` | string | No | Lower bound for last modification time \(RFC 3339 timestamp\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tasks` | array | List of tasks | +| ↳ `id` | string | Task identifier | +| ↳ `title` | string | Title of the task | +| ↳ `notes` | string | Notes/description for the task | +| ↳ `status` | string | Task status: "needsAction" or "completed" | +| ↳ `due` | string | Due date \(RFC 3339 timestamp\) | +| ↳ `completed` | string | Completion date \(RFC 3339 timestamp\) | +| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) | +| ↳ `selfLink` | string | URL pointing to this task | +| ↳ `webViewLink` | string | Link to task in Google Tasks UI | +| ↳ `parent` | string | Parent task identifier | +| ↳ `position` | string | Position among sibling tasks \(string-based ordering\) | +| ↳ `hidden` | boolean | Whether the task is hidden | +| ↳ `deleted` | boolean | Whether the task is deleted | +| ↳ `links` | array | Collection of links associated with the task | +| ↳ `type` | string | Link type \(e.g., "email", "generic", "chat_message"\) | +| ↳ `description` | string | Link description | +| ↳ `link` | string | The URL | +| `nextPageToken` | string | Token for retrieving the next page of results | + +### `google_tasks_get` + +Retrieve a specific task by ID from a Google Tasks list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) | +| `taskId` | string | Yes | The ID of the task to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Task ID | +| `title` | string | Task title | +| `notes` | string | Task notes | +| `status` | string | Task status \(needsAction or completed\) | +| `due` | string | Due date | +| `updated` | string | Last modification time | +| `selfLink` | string | URL for the task | +| `webViewLink` | string | Link to task in Google Tasks UI | +| `parent` | string | Parent task ID | +| `position` | string | Position among sibling tasks | +| `completed` | string | Completion date | +| `deleted` | boolean | Whether the task is deleted | + +### `google_tasks_update` + +Update an existing task in a Google Tasks list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) | +| `taskId` | string | Yes | The ID of the task to update | +| `title` | string | No | New title for the task | +| `notes` | string | No | New notes for the task | +| `due` | string | No | New due date in RFC 3339 format | +| `status` | string | No | New status: "needsAction" or "completed" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Task ID | +| `title` | string | Task title | +| `notes` | string | Task notes | +| `status` | string | Task status \(needsAction or completed\) | +| `due` | string | Due date | +| `updated` | string | Last modification time | +| `selfLink` | string | URL for the task | +| `webViewLink` | string | Link to task in Google Tasks UI | +| `parent` | string | Parent task ID | +| `position` | string | Position among sibling tasks | +| `completed` | string | Completion date | +| `deleted` | boolean | Whether the task is deleted | + +### `google_tasks_delete` + +Delete a task from a Google Tasks list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) | +| `taskId` | string | Yes | The ID of the task to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskId` | string | Deleted task ID | +| `deleted` | boolean | Whether deletion was successful | + +### `google_tasks_list_task_lists` + +Retrieve all task lists for the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `maxResults` | number | No | Maximum number of task lists to return \(default 20, max 100\) | +| `pageToken` | string | No | Token for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskLists` | array | List of task lists | +| ↳ `id` | string | Task list identifier | +| ↳ `title` | string | Title of the task list | +| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) | +| ↳ `selfLink` | string | URL pointing to this task list | +| `nextPageToken` | string | Token for retrieving the next page of results | + + diff --git a/apps/docs/content/docs/en/tools/google_translate.mdx b/apps/docs/content/docs/en/tools/google_translate.mdx new file mode 100644 index 0000000000..2e890b1149 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_translate.mdx @@ -0,0 +1,60 @@ +--- +title: Google Translate +description: Translate text using Google Cloud Translation +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Translate and detect languages using the Google Cloud Translation API. Supports auto-detection of the source language. + + + +## Tools + +### `google_translate_text` + +Translate text between languages using the Google Cloud Translation API. Supports auto-detection of the source language. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled | +| `text` | string | Yes | The text to translate | +| `target` | string | Yes | Target language code \(e.g., "es", "fr", "de", "ja"\) | +| `source` | string | No | Source language code. If omitted, the API will auto-detect the source language. | +| `format` | string | No | Format of the text: "text" for plain text, "html" for HTML content | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `translatedText` | string | The translated text | +| `detectedSourceLanguage` | string | The detected source language code \(if source was not specified\) | + +### `google_translate_detect` + +Detect the language of text using the Google Cloud Translation API. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Cloud API key with Cloud Translation API enabled | +| `text` | string | Yes | The text to detect the language of | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `language` | string | The detected language code \(e.g., "en", "es", "fr"\) | +| `confidence` | number | Confidence score of the detection | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9fc1cc577e..a089247e2e 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -21,6 +21,7 @@ "confluence", "cursor", "datadog", + "devin", "discord", "dropbox", "dspy", @@ -37,6 +38,7 @@ "gitlab", "gmail", "gong", + "google_bigquery", "google_books", "google_calendar", "google_docs", @@ -47,6 +49,8 @@ "google_search", "google_sheets", "google_slides", + "google_tasks", + "google_translate", "google_vault", "grafana", "grain", diff --git a/apps/docs/content/docs/en/tools/table.mdx b/apps/docs/content/docs/en/tools/table.mdx index acde8e300c..388005b772 100644 --- a/apps/docs/content/docs/en/tools/table.mdx +++ b/apps/docs/content/docs/en/tools/table.mdx @@ -5,11 +5,12 @@ description: User-defined data tables for storing and querying structured data import { BlockInfoCard } from "@/components/ui/block-info-card" - +{/* MANUAL-CONTENT-START:intro */} Tables allow you to create and manage custom data tables directly within Sim. Store, query, and manipulate structured data within your workflows without needing external database integrations. **Why Use Tables?** @@ -26,6 +27,7 @@ Tables allow you to create and manage custom data tables directly within Sim. St - Batch operations for bulk inserts - Bulk updates and deletes by filter - Up to 10,000 rows per table, 100 tables per workspace +{/* MANUAL-CONTENT-END */} ## Creating Tables diff --git a/apps/sim/app/api/mcp/tools/execute/route.ts b/apps/sim/app/api/mcp/tools/execute/route.ts index d44839590f..f748069b88 100644 --- a/apps/sim/app/api/mcp/tools/execute/route.ts +++ b/apps/sim/app/api/mcp/tools/execute/route.ts @@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { getExecutionTimeout } from '@/lib/core/execution-limits' import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types' +import { SIM_VIA_HEADER } from '@/lib/execution/call-chain' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpService } from '@/lib/mcp/service' import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types' @@ -178,8 +179,14 @@ export const POST = withMcpAuth('read')( 'sync' ) + const simViaHeader = request.headers.get(SIM_VIA_HEADER) + const extraHeaders: Record = {} + if (simViaHeader) { + extraHeaders[SIM_VIA_HEADER] = simViaHeader + } + const result = await Promise.race([ - mcpService.executeTool(userId, serverId, toolCall, workspaceId), + mcpService.executeTool(userId, serverId, toolCall, workspaceId, extraHeaders), new Promise((_, reject) => setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout) ), diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index c186d5ca58..cdfa12f593 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -283,3 +283,165 @@ export async function POST(request: NextRequest) { ) } } + +/** + * Update a blog post + */ +export async function PUT(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, blogPostId, title, content, cloudId: providedCloudId } = body + + if (!domain || !accessToken || !blogPostId) { + return NextResponse.json( + { error: 'Domain, access token, and blog post ID are required' }, + { status: 400 } + ) + } + + const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255) + if (!blogPostIdValidation.isValid) { + return NextResponse.json({ error: blogPostIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // Fetch current blog post to get version number + const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}?body-format=storage` + const currentResponse = await fetch(currentUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!currentResponse.ok) { + throw new Error(`Failed to fetch current blog post: ${currentResponse.status}`) + } + + const currentPost = await currentResponse.json() + + if (!currentPost.version?.number) { + return NextResponse.json( + { error: 'Unable to determine current blog post version' }, + { status: 422 } + ) + } + + const currentVersion = currentPost.version.number + + const updateBody: Record = { + id: blogPostId, + version: { number: currentVersion + 1 }, + status: 'current', + title: title || currentPost.title, + body: { + representation: 'storage', + value: content || currentPost.body?.storage?.value || '', + }, + } + + const response = await fetch(currentUrl, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to update blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error updating blog post:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Delete a blog post + */ +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, blogPostId, cloudId: providedCloudId } = body + + if (!domain || !accessToken || !blogPostId) { + return NextResponse.json( + { error: 'Domain, access token, and blog post ID are required' }, + { status: 400 } + ) + } + + const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255) + if (!blogPostIdValidation.isValid) { + return NextResponse.json({ error: blogPostIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to delete blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ blogPostId, deleted: true }) + } catch (error) { + logger.error('Error deleting blog post:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-descendants/route.ts b/apps/sim/app/api/tools/confluence/page-descendants/route.ts new file mode 100644 index 0000000000..e437b73750 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-descendants/route.ts @@ -0,0 +1,115 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { + validateAlphanumericId, + validateJiraCloudId, + validatePaginationCursor, +} from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePageDescendantsAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get all descendants of a Confluence page recursively. + * Uses GET /wiki/api/v2/pages/{id}/descendants + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + const cursorValidation = validatePaginationCursor(cursor, 'cursor') + if (!cursorValidation.isValid) { + return NextResponse.json({ error: cursorValidation.error }, { status: 400 }) + } + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/descendants?${queryParams.toString()}` + + logger.info(`Fetching descendants for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to get page descendants (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const descendants = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + type: page.type ?? null, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + parentId: page.parentId ?? null, + childPosition: page.childPosition ?? null, + depth: page.depth ?? null, + })) + + return NextResponse.json({ + descendants, + pageId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error getting page descendants:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-versions/route.ts b/apps/sim/app/api/tools/confluence/page-versions/route.ts index 9d7c162060..799fc28b0a 100644 --- a/apps/sim/app/api/tools/confluence/page-versions/route.ts +++ b/apps/sim/app/api/tools/confluence/page-versions/route.ts @@ -1,8 +1,13 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' -import { getConfluenceCloudId } from '@/tools/confluence/utils' +import { + validateAlphanumericId, + validateJiraCloudId, + validateNumericId, + validatePaginationCursor, +} from '@/lib/core/security/input-validation' +import { cleanHtmlContent, getConfluenceCloudId } from '@/tools/confluence/utils' const logger = createLogger('ConfluencePageVersionsAPI') @@ -55,42 +60,85 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - // If versionNumber is provided, get specific version + // If versionNumber is provided, get specific version with page content if (versionNumber !== undefined && versionNumber !== null) { - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}` + const versionValidation = validateNumericId(versionNumber, 'versionNumber', { min: 1 }) + if (!versionValidation.isValid) { + return NextResponse.json({ error: versionValidation.error }, { status: 400 }) + } + const safeVersion = versionValidation.sanitized - logger.info(`Fetching version ${versionNumber} for page ${pageId}`) + const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${safeVersion}` + const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${safeVersion}&body-format=storage` - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }) + logger.info(`Fetching version ${versionNumber} for page ${pageId}`) - if (!response.ok) { - const errorData = await response.json().catch(() => null) + const [versionResponse, pageResponse] = await Promise.all([ + fetch(versionUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }), + fetch(pageUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }), + ]) + + if (!versionResponse.ok) { + const errorData = await versionResponse.json().catch(() => null) logger.error('Confluence API error response:', { - status: response.status, - statusText: response.statusText, + status: versionResponse.status, + statusText: versionResponse.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = errorData?.message || `Failed to get page version (${response.status})` - return NextResponse.json({ error: errorMessage }, { status: response.status }) + const errorMessage = + errorData?.message || `Failed to get page version (${versionResponse.status})` + return NextResponse.json({ error: errorMessage }, { status: versionResponse.status }) } - const data = await response.json() + const versionData = await versionResponse.json() + + let title: string | null = null + let content: string | null = null + let body: Record | null = null + + if (pageResponse.ok) { + const pageData = await pageResponse.json() + title = pageData.title ?? null + body = pageData.body ?? null + + const rawContent = + pageData.body?.storage?.value || + pageData.body?.view?.value || + pageData.body?.atlas_doc_format?.value || + '' + if (rawContent) { + content = cleanHtmlContent(rawContent) + } + } else { + logger.warn( + `Could not fetch page content for version ${versionNumber}: ${pageResponse.status}` + ) + } return NextResponse.json({ version: { - number: data.number, - message: data.message ?? null, - minorEdit: data.minorEdit ?? false, - authorId: data.authorId ?? null, - createdAt: data.createdAt ?? null, + number: versionData.number, + message: versionData.message ?? null, + minorEdit: versionData.minorEdit ?? false, + authorId: versionData.authorId ?? null, + createdAt: versionData.createdAt ?? null, }, pageId, + title, + content, + body, }) } // List all versions @@ -98,6 +146,10 @@ export async function POST(request: NextRequest) { queryParams.append('limit', String(Math.min(limit, 250))) if (cursor) { + const cursorValidation = validatePaginationCursor(cursor, 'cursor') + if (!cursorValidation.isValid) { + return NextResponse.json({ error: cursorValidation.error }, { status: 400 }) + } queryParams.append('cursor', cursor) } diff --git a/apps/sim/app/api/tools/confluence/page/route.ts b/apps/sim/app/api/tools/confluence/page/route.ts index 232e453a95..191ddcef6f 100644 --- a/apps/sim/app/api/tools/confluence/page/route.ts +++ b/apps/sim/app/api/tools/confluence/page/route.ts @@ -185,7 +185,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}` + const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage` const currentPageResponse = await fetch(currentPageUrl, { headers: { Accept: 'application/json', diff --git a/apps/sim/app/api/tools/confluence/space-permissions/route.ts b/apps/sim/app/api/tools/confluence/space-permissions/route.ts new file mode 100644 index 0000000000..8a046a0f6c --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-permissions/route.ts @@ -0,0 +1,114 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { + validateAlphanumericId, + validateJiraCloudId, + validatePaginationCursor, +} from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpacePermissionsAPI') + +export const dynamic = 'force-dynamic' + +/** + * List permissions for a Confluence space. + * Uses GET /wiki/api/v2/spaces/{id}/permissions + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, cloudId: providedCloudId, limit = 50, cursor } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + const cursorValidation = validatePaginationCursor(cursor, 'cursor') + if (!cursorValidation.isValid) { + return NextResponse.json({ error: cursorValidation.error }, { status: 400 }) + } + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/permissions?${queryParams.toString()}` + + logger.info(`Fetching permissions for space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list space permissions (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const permissions = (data.results || []).map((perm: any) => ({ + id: perm.id, + principalType: perm.principal?.type ?? null, + principalId: perm.principal?.id ?? null, + operationKey: perm.operation?.key ?? null, + operationTargetType: perm.operation?.targetType ?? null, + anonymousAccess: perm.anonymousAccess ?? false, + unlicensedAccess: perm.unlicensedAccess ?? false, + })) + + return NextResponse.json({ + permissions, + spaceId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing space permissions:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space-properties/route.ts b/apps/sim/app/api/tools/confluence/space-properties/route.ts new file mode 100644 index 0000000000..5bdd176aff --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-properties/route.ts @@ -0,0 +1,209 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { + validateAlphanumericId, + validateJiraCloudId, + validatePaginationCursor, +} from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpacePropertiesAPI') + +export const dynamic = 'force-dynamic' + +/** + * List, create, or delete space properties. + * Uses GET/POST /wiki/api/v2/spaces/{id}/properties + * and DELETE /wiki/api/v2/spaces/{id}/properties/{propertyId} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + spaceId, + cloudId: providedCloudId, + action, + key, + value, + propertyId, + limit = 50, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/properties` + + // Validate required params for specific actions + if (action === 'delete' && !propertyId) { + return NextResponse.json( + { error: 'Property ID is required for delete action' }, + { status: 400 } + ) + } + + if (action === 'create' && !key) { + return NextResponse.json( + { error: 'Property key is required for create action' }, + { status: 400 } + ) + } + + // Delete a property + if (action === 'delete' && propertyId) { + const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255) + if (!propertyIdValidation.isValid) { + return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 }) + } + + const url = `${baseUrl}/${encodeURIComponent(propertyId)}` + + logger.info(`Deleting space property ${propertyId} from space ${spaceId}`) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete space property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ spaceId, propertyId, deleted: true }) + } + + // Create a property + if (action === 'create' && key) { + logger.info(`Creating space property '${key}' on space ${spaceId}`) + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ key, value: value ?? {} }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to create space property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + propertyId: data.id, + key: data.key, + value: data.value ?? null, + spaceId, + }) + } + + // List properties + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + const cursorValidation = validatePaginationCursor(cursor, 'cursor') + if (!cursorValidation.isValid) { + return NextResponse.json({ error: cursorValidation.error }, { status: 400 }) + } + queryParams.append('cursor', cursor) + } + + const url = `${baseUrl}?${queryParams.toString()}` + + logger.info(`Fetching properties for space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list space properties (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const properties = (data.results || []).map((prop: any) => ({ + id: prop.id, + key: prop.key, + value: prop.value ?? null, + })) + + return NextResponse.json({ + properties, + spaceId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error with space properties:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index a8e0186f79..ffe58f037a 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -78,3 +78,258 @@ export async function GET(request: NextRequest) { ) } } + +/** + * Create a new Confluence space. + * Uses POST /wiki/api/v2/spaces + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, name, key, description, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!name) { + return NextResponse.json({ error: 'Space name is required' }, { status: 400 }) + } + + if (!key) { + return NextResponse.json({ error: 'Space key is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces` + + const createBody: Record = { name, key } + if (description) { + createBody.description = { value: description, representation: 'plain' } + } + + logger.info(`Creating space with key ${key}`) + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(createBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to create space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error creating Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Update a Confluence space. + * Uses PUT /wiki/api/v2/spaces/{id} + */ +export async function PUT(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, name, description, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + + if (!name && description === undefined) { + return NextResponse.json( + { error: 'At least one of name or description is required for update' }, + { status: 400 } + ) + } + + const updateBody: Record = {} + + if (name) { + updateBody.name = name + } else { + const currentResponse = await fetch(url, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!currentResponse.ok) { + return NextResponse.json( + { error: `Failed to fetch current space: ${currentResponse.status}` }, + { status: currentResponse.status } + ) + } + const currentSpace = await currentResponse.json() + updateBody.name = currentSpace.name + } + + if (description !== undefined) { + updateBody.description = { value: description, representation: 'plain' } + } + + logger.info(`Updating space ${spaceId}`) + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to update space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error updating Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Delete a Confluence space. + * Uses DELETE /wiki/api/v2/spaces/{id} + */ +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + + logger.info(`Deleting space ${spaceId}`) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to delete space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ spaceId, deleted: true }) + } catch (error) { + logger.error('Error deleting Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/tasks/route.ts b/apps/sim/app/api/tools/confluence/tasks/route.ts new file mode 100644 index 0000000000..e74ca3b540 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/tasks/route.ts @@ -0,0 +1,278 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { + validateAlphanumericId, + validateJiraCloudId, + validatePaginationCursor, + validatePathSegment, +} from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceTasksAPI') + +export const dynamic = 'force-dynamic' + +/** + * List, get, or update Confluence inline tasks. + * Uses GET /wiki/api/v2/tasks, GET /wiki/api/v2/tasks/{id}, PUT /wiki/api/v2/tasks/{id} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + cloudId: providedCloudId, + action, + taskId, + status: taskStatus, + pageId, + spaceId, + assignedTo, + limit = 50, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // Update a task + if (action === 'update' && taskId) { + const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255) + if (!taskIdValidation.isValid) { + return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) + } + + // First fetch the current task to get required fields + const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + const getResponse = await fetch(getUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!getResponse.ok) { + const errorData = await getResponse.json().catch(() => null) + const errorMessage = errorData?.message || `Failed to fetch task (${getResponse.status})` + return NextResponse.json({ error: errorMessage }, { status: getResponse.status }) + } + + const currentTask = await getResponse.json() + + const updateBody: Record = { + id: taskId, + status: taskStatus || currentTask.status, + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + + logger.info(`Updating task ${taskId}`) + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to update task (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + task: { + id: data.id, + localId: data.localId ?? null, + spaceId: data.spaceId ?? null, + pageId: data.pageId ?? null, + blogPostId: data.blogPostId ?? null, + status: data.status, + body: data.body?.storage?.value ?? null, + createdBy: data.createdBy ?? null, + assignedTo: data.assignedTo ?? null, + completedBy: data.completedBy ?? null, + createdAt: data.createdAt ?? null, + updatedAt: data.updatedAt ?? null, + dueAt: data.dueAt ?? null, + completedAt: data.completedAt ?? null, + }, + }) + } + + // Get a specific task + if (taskId) { + const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255) + if (!taskIdValidation.isValid) { + return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + + logger.info(`Fetching task ${taskId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get task (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + task: { + id: data.id, + localId: data.localId ?? null, + spaceId: data.spaceId ?? null, + pageId: data.pageId ?? null, + blogPostId: data.blogPostId ?? null, + status: data.status, + body: data.body?.storage?.value ?? null, + createdBy: data.createdBy ?? null, + assignedTo: data.assignedTo ?? null, + completedBy: data.completedBy ?? null, + createdAt: data.createdAt ?? null, + updatedAt: data.updatedAt ?? null, + dueAt: data.dueAt ?? null, + completedAt: data.completedAt ?? null, + }, + }) + } + + // List tasks + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + const cursorValidation = validatePaginationCursor(cursor, 'cursor') + if (!cursorValidation.isValid) { + return NextResponse.json({ error: cursorValidation.error }, { status: 400 }) + } + queryParams.append('cursor', cursor) + } + if (taskStatus) queryParams.append('status', taskStatus) + if (pageId) { + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + queryParams.append('page-id', pageId) + } + if (spaceId) { + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + queryParams.append('space-id', spaceId) + } + if (assignedTo) { + // Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134 + const assignedToValidation = validatePathSegment(assignedTo, { + paramName: 'assignedTo', + maxLength: 128, + customPattern: /^[a-zA-Z0-9_|:-]+$/, + }) + if (!assignedToValidation.isValid) { + return NextResponse.json({ error: assignedToValidation.error }, { status: 400 }) + } + queryParams.append('assigned-to', assignedTo) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks?${queryParams.toString()}` + + logger.info('Fetching tasks') + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to list tasks (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const tasks = (data.results || []).map((task: any) => ({ + id: task.id, + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status, + body: task.body?.storage?.value ?? null, + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + })) + + return NextResponse.json({ + tasks, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error with tasks:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts new file mode 100644 index 0000000000..5b81116d80 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -0,0 +1,85 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId, validatePathSegment } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceUserAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get a Confluence user by account ID. + * Uses GET /wiki/rest/api/user?accountId={accountId} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, accountId, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!accountId) { + return NextResponse.json({ error: 'Account ID is required' }, { status: 400 }) + } + + // Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134 + const accountIdValidation = validatePathSegment(accountId, { + paramName: 'accountId', + maxLength: 128, + customPattern: /^[a-zA-Z0-9_|:-]+$/, + }) + if (!accountIdValidation.isValid) { + return NextResponse.json({ error: accountIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to get Confluence user (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error getting Confluence user:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/v1/admin/audit-logs/[id]/route.ts b/apps/sim/app/api/v1/admin/audit-logs/[id]/route.ts new file mode 100644 index 0000000000..848fbc8b31 --- /dev/null +++ b/apps/sim/app/api/v1/admin/audit-logs/[id]/route.ts @@ -0,0 +1,44 @@ +/** + * GET /api/v1/admin/audit-logs/[id] + * + * Get a single audit log entry by ID. + * + * Response: AdminSingleResponse + */ + +import { db } from '@sim/db' +import { auditLog } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + internalErrorResponse, + notFoundResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' +import { toAdminAuditLog } from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminAuditLogDetailAPI') + +interface RouteParams { + id: string +} + +export const GET = withAdminAuthParams(async (request, context) => { + const { id } = await context.params + + try { + const [log] = await db.select().from(auditLog).where(eq(auditLog.id, id)).limit(1) + + if (!log) { + return notFoundResponse('AuditLog') + } + + logger.info(`Admin API: Retrieved audit log ${id}`) + + return singleResponse(toAdminAuditLog(log)) + } catch (error) { + logger.error('Admin API: Failed to get audit log', { error, id }) + return internalErrorResponse('Failed to get audit log') + } +}) diff --git a/apps/sim/app/api/v1/admin/audit-logs/route.ts b/apps/sim/app/api/v1/admin/audit-logs/route.ts new file mode 100644 index 0000000000..895ac1ff3e --- /dev/null +++ b/apps/sim/app/api/v1/admin/audit-logs/route.ts @@ -0,0 +1,96 @@ +/** + * GET /api/v1/admin/audit-logs + * + * List all audit logs with pagination and filtering. + * + * Query Parameters: + * - limit: number (default: 50, max: 250) + * - offset: number (default: 0) + * - action: string (optional) - Filter by action (e.g., "workflow.created") + * - resourceType: string (optional) - Filter by resource type (e.g., "workflow") + * - resourceId: string (optional) - Filter by resource ID + * - workspaceId: string (optional) - Filter by workspace ID + * - actorId: string (optional) - Filter by actor user ID + * - actorEmail: string (optional) - Filter by actor email + * - startDate: string (optional) - ISO 8601 date, filter createdAt >= startDate + * - endDate: string (optional) - ISO 8601 date, filter createdAt <= endDate + * + * Response: AdminListResponse + */ + +import { db } from '@sim/db' +import { auditLog } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, count, desc, eq, gte, lte, type SQL } from 'drizzle-orm' +import { withAdminAuth } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + listResponse, +} from '@/app/api/v1/admin/responses' +import { + type AdminAuditLog, + createPaginationMeta, + parsePaginationParams, + toAdminAuditLog, +} from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminAuditLogsAPI') + +export const GET = withAdminAuth(async (request) => { + const url = new URL(request.url) + const { limit, offset } = parsePaginationParams(url) + + const actionFilter = url.searchParams.get('action') + const resourceTypeFilter = url.searchParams.get('resourceType') + const resourceIdFilter = url.searchParams.get('resourceId') + const workspaceIdFilter = url.searchParams.get('workspaceId') + const actorIdFilter = url.searchParams.get('actorId') + const actorEmailFilter = url.searchParams.get('actorEmail') + const startDateFilter = url.searchParams.get('startDate') + const endDateFilter = url.searchParams.get('endDate') + + if (startDateFilter && Number.isNaN(Date.parse(startDateFilter))) { + return badRequestResponse('Invalid startDate format. Use ISO 8601.') + } + if (endDateFilter && Number.isNaN(Date.parse(endDateFilter))) { + return badRequestResponse('Invalid endDate format. Use ISO 8601.') + } + + try { + const conditions: SQL[] = [] + + if (actionFilter) conditions.push(eq(auditLog.action, actionFilter)) + if (resourceTypeFilter) conditions.push(eq(auditLog.resourceType, resourceTypeFilter)) + if (resourceIdFilter) conditions.push(eq(auditLog.resourceId, resourceIdFilter)) + if (workspaceIdFilter) conditions.push(eq(auditLog.workspaceId, workspaceIdFilter)) + if (actorIdFilter) conditions.push(eq(auditLog.actorId, actorIdFilter)) + if (actorEmailFilter) conditions.push(eq(auditLog.actorEmail, actorEmailFilter)) + if (startDateFilter) conditions.push(gte(auditLog.createdAt, new Date(startDateFilter))) + if (endDateFilter) conditions.push(lte(auditLog.createdAt, new Date(endDateFilter))) + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined + + const [countResult, logs] = await Promise.all([ + db.select({ total: count() }).from(auditLog).where(whereClause), + db + .select() + .from(auditLog) + .where(whereClause) + .orderBy(desc(auditLog.createdAt)) + .limit(limit) + .offset(offset), + ]) + + const total = countResult[0].total + const data: AdminAuditLog[] = logs.map(toAdminAuditLog) + const pagination = createPaginationMeta(total, limit, offset) + + logger.info(`Admin API: Listed ${data.length} audit logs (total: ${total})`) + + return listResponse(data, pagination) + } catch (error) { + logger.error('Admin API: Failed to list audit logs', { error }) + return internalErrorResponse('Failed to list audit logs') + } +}) diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts index d7ec4f5c3c..3cfd515b25 100644 --- a/apps/sim/app/api/v1/admin/types.ts +++ b/apps/sim/app/api/v1/admin/types.ts @@ -6,6 +6,7 @@ */ import type { + auditLog, member, organization, referralCampaigns, @@ -694,3 +695,45 @@ export function toAdminReferralCampaign( updatedAt: dbCampaign.updatedAt.toISOString(), } } + +// ============================================================================= +// Audit Log Types +// ============================================================================= + +export type DbAuditLog = InferSelectModel + +export interface AdminAuditLog { + id: string + workspaceId: string | null + actorId: string | null + actorName: string | null + actorEmail: string | null + action: string + resourceType: string + resourceId: string | null + resourceName: string | null + description: string | null + metadata: unknown + ipAddress: string | null + userAgent: string | null + createdAt: string +} + +export function toAdminAuditLog(dbLog: DbAuditLog): AdminAuditLog { + return { + id: dbLog.id, + workspaceId: dbLog.workspaceId, + actorId: dbLog.actorId, + actorName: dbLog.actorName, + actorEmail: dbLog.actorEmail, + action: dbLog.action, + resourceType: dbLog.resourceType, + resourceId: dbLog.resourceId, + resourceName: dbLog.resourceName, + description: dbLog.description, + metadata: dbLog.metadata, + ipAddress: dbLog.ipAddress, + userAgent: dbLog.userAgent, + createdAt: dbLog.createdAt.toISOString(), + } +} diff --git a/apps/sim/app/api/v1/audit-logs/[id]/route.ts b/apps/sim/app/api/v1/audit-logs/[id]/route.ts new file mode 100644 index 0000000000..3cf6351d2b --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/[id]/route.ts @@ -0,0 +1,78 @@ +/** + * GET /api/v1/audit-logs/[id] + * + * Get a single audit log entry by ID, scoped to the authenticated user's organization. + * Requires enterprise subscription and org admin/owner role. + * + * Scope includes logs from current org members AND logs within org workspaces + * (including those from departed members or system actions with null actorId). + * + * Response: { data: AuditLogEntry, limits: UserLimits } + */ + +import { db } from '@sim/db' +import { auditLog, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, inArray, or } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' +import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' +import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' + +const logger = createLogger('V1AuditLogDetailAPI') + +export const revalidate = 0 + +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const rateLimit = await checkRateLimit(request, 'audit-logs') + if (!rateLimit.allowed) { + return createRateLimitResponse(rateLimit) + } + + const userId = rateLimit.userId! + const { id } = await params + + const authResult = await validateEnterpriseAuditAccess(userId) + if (!authResult.success) { + return authResult.response + } + + const { orgMemberIds } = authResult.context + + const orgWorkspaceIds = db + .select({ id: workspace.id }) + .from(workspace) + .where(inArray(workspace.ownerId, orgMemberIds)) + + const [log] = await db + .select() + .from(auditLog) + .where( + and( + eq(auditLog.id, id), + or( + inArray(auditLog.actorId, orgMemberIds), + inArray(auditLog.workspaceId, orgWorkspaceIds) + ) + ) + ) + .limit(1) + + if (!log) { + return NextResponse.json({ error: 'Audit log not found' }, { status: 404 }) + } + + const limits = await getUserLimits(userId) + const response = createApiResponse({ data: formatAuditLogEntry(log) }, limits, rateLimit) + + return NextResponse.json(response.body, { headers: response.headers }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Audit log detail fetch error`, { error: message }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/v1/audit-logs/auth.ts b/apps/sim/app/api/v1/audit-logs/auth.ts new file mode 100644 index 0000000000..085884488e --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/auth.ts @@ -0,0 +1,103 @@ +/** + * Enterprise audit log authorization. + * + * Validates that the authenticated user is an admin/owner of an enterprise organization + * and returns the organization context needed for scoped queries. + */ + +import { db } from '@sim/db' +import { member, subscription } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' + +const logger = createLogger('V1AuditLogsAuth') + +export interface EnterpriseAuditContext { + organizationId: string + orgMemberIds: string[] +} + +type AuthResult = + | { success: true; context: EnterpriseAuditContext } + | { success: false; response: NextResponse } + +/** + * Validates enterprise audit log access for the given user. + * + * Checks: + * 1. User belongs to an organization + * 2. User has admin or owner role + * 3. Organization has an active enterprise subscription + * + * Returns the organization ID and all member user IDs on success, + * or an error response on failure. + */ +export async function validateEnterpriseAuditAccess(userId: string): Promise { + const [membership] = await db + .select({ organizationId: member.organizationId, role: member.role }) + .from(member) + .where(eq(member.userId, userId)) + .limit(1) + + if (!membership) { + return { + success: false, + response: NextResponse.json({ error: 'Not a member of any organization' }, { status: 403 }), + } + } + + if (membership.role !== 'admin' && membership.role !== 'owner') { + return { + success: false, + response: NextResponse.json( + { error: 'Organization admin or owner role required' }, + { status: 403 } + ), + } + } + + const [orgSub, orgMembers] = await Promise.all([ + db + .select({ id: subscription.id }) + .from(subscription) + .where( + and( + eq(subscription.referenceId, membership.organizationId), + eq(subscription.plan, 'enterprise'), + eq(subscription.status, 'active') + ) + ) + .limit(1), + db + .select({ userId: member.userId }) + .from(member) + .where(eq(member.organizationId, membership.organizationId)), + ]) + + if (orgSub.length === 0) { + return { + success: false, + response: NextResponse.json( + { error: 'Active enterprise subscription required' }, + { status: 403 } + ), + } + } + + const orgMemberIds = orgMembers.map((m) => m.userId) + + logger.info('Enterprise audit access validated', { + userId, + organizationId: membership.organizationId, + memberCount: orgMemberIds.length, + }) + + return { + success: true, + context: { + organizationId: membership.organizationId, + orgMemberIds, + }, + } +} diff --git a/apps/sim/app/api/v1/audit-logs/format.ts b/apps/sim/app/api/v1/audit-logs/format.ts new file mode 100644 index 0000000000..5591f4f6f8 --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/format.ts @@ -0,0 +1,43 @@ +/** + * Enterprise audit log response formatting. + * + * Defines the shape returned by the enterprise audit log API. + * Excludes `ipAddress` and `userAgent` for privacy. + */ + +import type { auditLog } from '@sim/db/schema' +import type { InferSelectModel } from 'drizzle-orm' + +type DbAuditLog = InferSelectModel + +export interface EnterpriseAuditLogEntry { + id: string + workspaceId: string | null + actorId: string | null + actorName: string | null + actorEmail: string | null + action: string + resourceType: string + resourceId: string | null + resourceName: string | null + description: string | null + metadata: unknown + createdAt: string +} + +export function formatAuditLogEntry(log: DbAuditLog): EnterpriseAuditLogEntry { + return { + id: log.id, + workspaceId: log.workspaceId, + actorId: log.actorId, + actorName: log.actorName, + actorEmail: log.actorEmail, + action: log.action, + resourceType: log.resourceType, + resourceId: log.resourceId, + resourceName: log.resourceName, + description: log.description, + metadata: log.metadata, + createdAt: log.createdAt.toISOString(), + } +} diff --git a/apps/sim/app/api/v1/audit-logs/route.ts b/apps/sim/app/api/v1/audit-logs/route.ts new file mode 100644 index 0000000000..825cf37620 --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/route.ts @@ -0,0 +1,191 @@ +/** + * GET /api/v1/audit-logs + * + * List audit logs scoped to the authenticated user's organization. + * Requires enterprise subscription and org admin/owner role. + * + * Query Parameters: + * - action: string (optional) - Filter by action (e.g., "workflow.created") + * - resourceType: string (optional) - Filter by resource type (e.g., "workflow") + * - resourceId: string (optional) - Filter by resource ID + * - workspaceId: string (optional) - Filter by workspace ID + * - actorId: string (optional) - Filter by actor user ID (must be an org member) + * - startDate: string (optional) - ISO 8601 date, filter createdAt >= startDate + * - endDate: string (optional) - ISO 8601 date, filter createdAt <= endDate + * - includeDeparted: boolean (optional, default: false) - Include logs from departed members + * - limit: number (optional, default: 50, max: 100) + * - cursor: string (optional) - Opaque cursor for pagination + * + * Response: { data: AuditLogEntry[], nextCursor?: string, limits: UserLimits } + */ + +import { db } from '@sim/db' +import { auditLog, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, desc, eq, gte, inArray, lt, lte, or, type SQL } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' +import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' +import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' + +const logger = createLogger('V1AuditLogsAPI') + +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +const isoDateString = z.string().refine((val) => !Number.isNaN(Date.parse(val)), { + message: 'Invalid date format. Use ISO 8601.', +}) + +const QueryParamsSchema = z.object({ + action: z.string().optional(), + resourceType: z.string().optional(), + resourceId: z.string().optional(), + workspaceId: z.string().optional(), + actorId: z.string().optional(), + startDate: isoDateString.optional(), + endDate: isoDateString.optional(), + includeDeparted: z + .enum(['true', 'false']) + .transform((val) => val === 'true') + .optional() + .default('false'), + limit: z.coerce.number().min(1).max(100).optional().default(50), + cursor: z.string().optional(), +}) + +interface CursorData { + createdAt: string + id: string +} + +function encodeCursor(data: CursorData): string { + return Buffer.from(JSON.stringify(data)).toString('base64') +} + +function decodeCursor(cursor: string): CursorData | null { + try { + return JSON.parse(Buffer.from(cursor, 'base64').toString()) + } catch { + return null + } +} + +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const rateLimit = await checkRateLimit(request, 'audit-logs') + if (!rateLimit.allowed) { + return createRateLimitResponse(rateLimit) + } + + const userId = rateLimit.userId! + + const authResult = await validateEnterpriseAuditAccess(userId) + if (!authResult.success) { + return authResult.response + } + + const { orgMemberIds } = authResult.context + + const { searchParams } = new URL(request.url) + const rawParams = Object.fromEntries(searchParams.entries()) + const validationResult = QueryParamsSchema.safeParse(rawParams) + + if (!validationResult.success) { + return NextResponse.json( + { error: 'Invalid parameters', details: validationResult.error.errors }, + { status: 400 } + ) + } + + const params = validationResult.data + + if (params.actorId && !orgMemberIds.includes(params.actorId)) { + return NextResponse.json( + { error: 'actorId is not a member of your organization' }, + { status: 400 } + ) + } + + let scopeCondition: SQL + + if (params.includeDeparted) { + const orgWorkspaces = await db + .select({ id: workspace.id }) + .from(workspace) + .where(inArray(workspace.ownerId, orgMemberIds)) + + const orgWorkspaceIds = orgWorkspaces.map((w) => w.id) + + if (orgWorkspaceIds.length > 0) { + scopeCondition = or( + inArray(auditLog.actorId, orgMemberIds), + inArray(auditLog.workspaceId, orgWorkspaceIds) + )! + } else { + scopeCondition = inArray(auditLog.actorId, orgMemberIds) + } + } else { + scopeCondition = inArray(auditLog.actorId, orgMemberIds) + } + + const conditions: SQL[] = [scopeCondition] + + if (params.action) conditions.push(eq(auditLog.action, params.action)) + if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType)) + if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId)) + if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId)) + if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId)) + if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate))) + if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate))) + + if (params.cursor) { + const cursorData = decodeCursor(params.cursor) + if (cursorData?.createdAt && cursorData.id) { + const cursorDate = new Date(cursorData.createdAt) + if (!Number.isNaN(cursorDate.getTime())) { + conditions.push( + or( + lt(auditLog.createdAt, cursorDate), + and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id)) + )! + ) + } + } + } + + const rows = await db + .select() + .from(auditLog) + .where(and(...conditions)) + .orderBy(desc(auditLog.createdAt), desc(auditLog.id)) + .limit(params.limit + 1) + + const hasMore = rows.length > params.limit + const data = rows.slice(0, params.limit) + + let nextCursor: string | undefined + if (hasMore && data.length > 0) { + const last = data[data.length - 1] + nextCursor = encodeCursor({ + createdAt: last.createdAt.toISOString(), + id: last.id, + }) + } + + const formattedLogs = data.map(formatAuditLogEntry) + + const limits = await getUserLimits(userId) + const response = createApiResponse({ data: formattedLogs, nextCursor }, limits, rateLimit) + + return NextResponse.json(response.body, { headers: response.headers }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Audit logs fetch error`, { error: message }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/v1/middleware.ts b/apps/sim/app/api/v1/middleware.ts index 06b4109433..60a7b93474 100644 --- a/apps/sim/app/api/v1/middleware.ts +++ b/apps/sim/app/api/v1/middleware.ts @@ -19,7 +19,7 @@ export interface RateLimitResult { export async function checkRateLimit( request: NextRequest, - endpoint: 'logs' | 'logs-detail' | 'workflows' | 'workflow-detail' = 'logs' + endpoint: 'logs' | 'logs-detail' | 'workflows' | 'workflow-detail' | 'audit-logs' = 'logs' ): Promise { try { const auth = await authenticateV1Request(request) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index cf8431f9dc..b393ae492a 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -987,7 +987,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const onChildWorkflowInstanceReady = ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => { sendEvent({ type: 'block:childWorkflowStarted', @@ -1001,6 +1002,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: iterationCurrent: iterationContext.iterationCurrent, iterationContainerId: iterationContext.iterationContainerId, }), + ...(executionOrder !== undefined && { executionOrder }), }, }) } diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 6663b1de57..79445802be 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -21,6 +21,7 @@ interface TemplateCardProps { blocks?: string[] className?: string state?: WorkflowState + description?: string | null isStarred?: boolean isVerified?: boolean } @@ -124,6 +125,7 @@ function TemplateCardInner({ blocks = [], className, state, + description, isStarred = false, isVerified = false, }: TemplateCardProps) { @@ -270,6 +272,12 @@ function TemplateCardInner({ + {description && ( +

+ {description} +

+ )} +
{authorImageUrl ? ( diff --git a/apps/sim/app/templates/templates.tsx b/apps/sim/app/templates/templates.tsx index cbb3ab6938..08fe559555 100644 --- a/apps/sim/app/templates/templates.tsx +++ b/apps/sim/app/templates/templates.tsx @@ -196,6 +196,7 @@ export default function Templates({ key={template.id} id={template.id} title={template.name} + description={template.details?.tagline} author={template.creator?.name || 'Unknown'} authorImageUrl={template.creator?.profileImageUrl || null} usageCount={template.views.toString()} diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index 5e7e112f6c..a19809e4e0 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -18,6 +18,7 @@ interface TemplateCardProps { blocks?: string[] className?: string state?: WorkflowState + description?: string | null isStarred?: boolean isVerified?: boolean } @@ -127,6 +128,7 @@ function TemplateCardInner({ blocks = [], className, state, + description, isStarred = false, isVerified = false, }: TemplateCardProps) { @@ -277,6 +279,12 @@ function TemplateCardInner({
+ {description && ( +

+ {description} +

+ )} +
{authorImageUrl ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx b/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx index 884dce6d03..550c58bf3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx @@ -222,6 +222,7 @@ export default function Templates({ key={template.id} id={template.id} title={template.name} + description={template.details?.tagline} author={template.creator?.name || 'Unknown'} authorImageUrl={template.creator?.profileImageUrl || null} usageCount={template.views.toString()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx index e091849c82..a9c50bb21e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx @@ -26,16 +26,21 @@ export interface CanvasMenuProps { onOpenLogs: () => void onToggleVariables: () => void onToggleChat: () => void + onToggleWorkflowLock?: () => void isVariablesOpen?: boolean isChatOpen?: boolean hasClipboard?: boolean disableEdit?: boolean - disableAdmin?: boolean + canAdmin?: boolean canUndo?: boolean canRedo?: boolean isInvitationsDisabled?: boolean /** Whether the workflow has locked blocks (disables auto-layout) */ hasLockedBlocks?: boolean + /** Whether all blocks in the workflow are locked */ + allBlocksLocked?: boolean + /** Whether the workflow has any blocks */ + hasBlocks?: boolean } /** @@ -56,13 +61,17 @@ export function CanvasMenu({ onOpenLogs, onToggleVariables, onToggleChat, + onToggleWorkflowLock, isVariablesOpen = false, isChatOpen = false, hasClipboard = false, disableEdit = false, + canAdmin = false, canUndo = false, canRedo = false, hasLockedBlocks = false, + allBlocksLocked = false, + hasBlocks = false, }: CanvasMenuProps) { return ( Auto-layout ⇧L + {canAdmin && onToggleWorkflowLock && ( + { + onToggleWorkflowLock() + onClose() + }} + > + {allBlocksLocked ? 'Unlock workflow' : 'Lock workflow'} + + )} { onFitToView() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index ddd25134fc..cd5d8095b7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -61,6 +61,9 @@ export const Notifications = memo(function Notifications() { case 'refresh': window.location.reload() break + case 'unlock-workflow': + window.dispatchEvent(new CustomEvent('unlock-workflow')) + break default: logger.warn('Unknown action type', { notificationId, actionType: action.type }) } @@ -175,7 +178,9 @@ export const Notifications = memo(function Notifications() { ? 'Fix in Copilot' : notification.action!.type === 'refresh' ? 'Refresh' - : 'Take action'} + : notification.action!.type === 'unlock-workflow' + ? 'Unlock Workflow' + : 'Take action'} )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 6cac32e626..c8146a2801 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -40,10 +40,12 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files', 'https://www.googleapis.com/auth/drive': 'Access all Google Drive files', 'https://www.googleapis.com/auth/calendar': 'View and manage calendar', + 'https://www.googleapis.com/auth/tasks': 'Create, read, update, and delete Google Tasks', 'https://www.googleapis.com/auth/userinfo.email': 'View email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', 'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms', 'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms', + 'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery', 'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery', 'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage', 'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups', @@ -81,6 +83,15 @@ const SCOPE_DESCRIPTIONS: Record = { 'write:content.property:confluence': 'Create and manage content properties', 'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)', 'read:content.metadata:confluence': 'View content metadata (required for ancestors)', + 'read:user:confluence': 'View Confluence user profiles', + 'read:task:confluence': 'View Confluence inline tasks', + 'write:task:confluence': 'Update Confluence inline tasks', + 'delete:blogpost:confluence': 'Delete Confluence blog posts', + 'write:space:confluence': 'Create and update Confluence spaces', + 'delete:space:confluence': 'Delete Confluence spaces', + 'read:space.property:confluence': 'View Confluence space properties', + 'write:space.property:confluence': 'Create and manage space properties', + 'read:space.permission:confluence': 'View Confluence space permissions', 'read:me': 'Read profile information', 'database.read': 'Read database', 'database.write': 'Write to database', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 99d6779aa8..7011e69113 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -379,7 +379,7 @@ export function CredentialSelector({ filterOptions={true} isLoading={credentialsLoading} overlayContent={overlayContent} - className={selectedId || isCredentialSetSelected ? 'pl-[28px]' : ''} + className={overlayContent ? 'pl-[28px]' : ''} /> {needsUpdate && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 03510936d7..e10bedb79b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ArrowUp, Square } from 'lucide-react' +import { ArrowUp, Lock, Square, Unlock } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { @@ -41,8 +41,11 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks' import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables' import { useAutoLayout } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout' +import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' +import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useChatStore } from '@/stores/chat/store' import { useNotificationStore } from '@/stores/notifications/store' @@ -126,6 +129,15 @@ export const Panel = memo(function Panel() { Object.values(state.blocks).some((block) => block.locked) ) + const allBlocksLocked = useWorkflowStore((state) => { + const blockList = Object.values(state.blocks) + return blockList.length > 0 && blockList.every((block) => block.locked) + }) + + const hasBlocks = useWorkflowStore((state) => Object.keys(state.blocks).length > 0) + + const { collaborativeBatchToggleLocked } = useCollaborativeWorkflow() + // Delete workflow hook const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({ workspaceId, @@ -192,6 +204,7 @@ export const Panel = memo(function Panel() { ) const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null + const { isSnapshotView } = useCurrentWorkflow() /** * Mark hydration as complete on mount @@ -329,6 +342,17 @@ export const Panel = memo(function Panel() { workspaceId, ]) + /** + * Toggles the locked state of all blocks in the workflow + */ + const handleToggleWorkflowLock = useCallback(() => { + const blocks = useWorkflowStore.getState().blocks + const allLocked = Object.values(blocks).every((b) => b.locked) + const ids = getWorkflowLockToggleIds(blocks, !allLocked) + if (ids.length > 0) collaborativeBatchToggleLocked(ids) + setIsMenuOpen(false) + }, [collaborativeBatchToggleLocked]) + // Compute run button state const canRun = userPermissions.canRead // Running only requires read permissions const isLoadingPermissions = userPermissions.isLoading @@ -399,6 +423,16 @@ export const Panel = memo(function Panel() { Auto layout + {userPermissions.canAdmin && !isSnapshotView && ( + + {allBlocksLocked ? ( + + ) : ( + + )} + {allBlocksLocked ? 'Unlock workflow' : 'Lock workflow'} + + )} { setVariablesOpen(!isVariablesOpen)}> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 9d9b206b79..204ca166c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -160,12 +160,16 @@ const IterationNodeRow = memo(function IterationNodeRow({ onSelectEntry, isExpanded, onToggle, + expandedNodes, + onToggleNode, }: { node: EntryNode selectedEntryId: string | null onSelectEntry: (entry: ConsoleEntry) => void isExpanded: boolean onToggle: () => void + expandedNodes: Set + onToggleNode: (nodeId: string) => void }) { const { entry, children, iterationInfo } = node const hasError = Boolean(entry.error) || children.some((c) => c.entry.error) @@ -226,11 +230,13 @@ const IterationNodeRow = memo(function IterationNodeRow({ {isExpanded && hasChildren && (
{children.map((child) => ( - ))}
@@ -346,6 +352,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({ onSelectEntry={onSelectEntry} isExpanded={expandedNodes.has(iterNode.entry.id)} onToggle={() => onToggleNode(iterNode.entry.id)} + expandedNodes={expandedNodes} + onToggleNode={onToggleNode} /> ))}
@@ -520,6 +528,8 @@ const EntryNodeRow = memo(function EntryNodeRow({ onSelectEntry={onSelectEntry} isExpanded={expandedNodes.has(node.entry.id)} onToggle={() => onToggleNode(node.entry.id)} + expandedNodes={expandedNodes} + onToggleNode={onToggleNode} /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 07bf5e1430..a20c1c356f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -1298,7 +1298,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ )} - {!isEnabled && disabled} + {!isEnabled && !isLocked && disabled} {isLocked && locked} {type === 'schedule' && shouldShowScheduleBadge && scheduleInfo?.isDisabled && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index f1bcca15b7..1af79967c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -554,6 +554,7 @@ export function useWorkflowExecution() { childWorkflowInstanceId: string iterationCurrent?: number iterationContainerId?: string + executionOrder?: number }) => { if (isStaleExecution()) return updateConsole( @@ -564,6 +565,7 @@ export function useWorkflowExecution() { ...(data.iterationContainerId !== undefined && { iterationContainerId: data.iterationContainerId, }), + ...(data.executionOrder !== undefined && { executionOrder: data.executionOrder }), }, executionIdRef.current ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts index d86f1b3dce..602a8d784a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts @@ -71,3 +71,38 @@ export function filterProtectedBlocks( allProtected: protectedIds.length === blockIds.length && blockIds.length > 0, } } + +/** + * Returns block IDs ordered so that `batchToggleLocked` will target the desired state. + * + * `batchToggleLocked` determines its target locked state from `!firstBlock.locked`. + * When `targetLocked` is true (lock all), an unlocked block must come first. + * When `targetLocked` is false (unlock all), a locked block must come first. + * + * Returns an empty array when there are no blocks or all blocks already match `targetLocked`. + * + * @param blocks - Record of all blocks in the workflow + * @param targetLocked - The desired locked state for all blocks + * @returns Sorted block IDs, or empty array if no toggle is needed + */ +export function getWorkflowLockToggleIds( + blocks: Record, + targetLocked: boolean +): string[] { + const ids = Object.keys(blocks) + if (ids.length === 0) return [] + + // No-op if all blocks already match the desired state + const allMatch = Object.values(blocks).every((b) => Boolean(b.locked) === targetLocked) + if (allMatch) return [] + + ids.sort((a, b) => { + const aVal = blocks[a].locked ? 1 : 0 + const bVal = blocks[b].locked ? 1 : 0 + // To lock all (targetLocked=true): unlocked first (aVal - bVal) + // To unlock all (targetLocked=false): locked first (bVal - aVal) + return targetLocked ? aVal - bVal : bVal - aVal + }) + + return ids +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f88b9d9122..02c6175c2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -57,6 +57,7 @@ import { estimateBlockDimensions, filterProtectedBlocks, getClampedPositionForNode, + getWorkflowLockToggleIds, isBlockProtected, isEdgeProtected, isInEditableElement, @@ -393,6 +394,15 @@ const WorkflowContent = React.memo(() => { const { blocks, edges, lastSaved } = currentWorkflow + const allBlocksLocked = useMemo(() => { + const blockList = Object.values(blocks) + return blockList.length > 0 && blockList.every((b) => b.locked) + }, [blocks]) + + const hasBlocks = useMemo(() => Object.keys(blocks).length > 0, [blocks]) + + const hasLockedBlocks = useMemo(() => Object.values(blocks).some((b) => b.locked), [blocks]) + const isWorkflowReady = useMemo( () => hydration.phase === 'ready' && @@ -1175,6 +1185,91 @@ const WorkflowContent = React.memo(() => { collaborativeBatchToggleLocked(blockIds) }, [contextMenuBlocks, collaborativeBatchToggleLocked]) + const handleToggleWorkflowLock = useCallback(() => { + const currentBlocks = useWorkflowStore.getState().blocks + const allLocked = Object.values(currentBlocks).every((b) => b.locked) + const ids = getWorkflowLockToggleIds(currentBlocks, !allLocked) + if (ids.length > 0) collaborativeBatchToggleLocked(ids) + }, [collaborativeBatchToggleLocked]) + + // Show notification when all blocks in the workflow are locked + const lockNotificationIdRef = useRef(null) + + const clearLockNotification = useCallback(() => { + if (lockNotificationIdRef.current) { + useNotificationStore.getState().removeNotification(lockNotificationIdRef.current) + lockNotificationIdRef.current = null + } + }, []) + + // Clear persisted lock notifications on mount/workflow change (prevents duplicates after reload) + useEffect(() => { + // Reset ref so the main effect creates a fresh notification for the new workflow + clearLockNotification() + + if (!activeWorkflowId) return + const store = useNotificationStore.getState() + const stale = store.notifications.filter( + (n) => + n.workflowId === activeWorkflowId && + (n.action?.type === 'unlock-workflow' || n.message.startsWith('This workflow is locked')) + ) + for (const n of stale) { + store.removeNotification(n.id) + } + }, [activeWorkflowId, clearLockNotification]) + + const prevCanAdminRef = useRef(effectivePermissions.canAdmin) + useEffect(() => { + if (!isWorkflowReady) return + + const canAdminChanged = prevCanAdminRef.current !== effectivePermissions.canAdmin + prevCanAdminRef.current = effectivePermissions.canAdmin + + // Clear stale notification when admin status changes so it recreates with correct message + if (canAdminChanged) { + clearLockNotification() + } + + if (allBlocksLocked) { + if (lockNotificationIdRef.current) return + + const isAdmin = effectivePermissions.canAdmin + lockNotificationIdRef.current = addNotification({ + level: 'info', + message: isAdmin + ? 'This workflow is locked' + : 'This workflow is locked. Ask an admin to unlock it.', + workflowId: activeWorkflowId || undefined, + ...(isAdmin ? { action: { type: 'unlock-workflow' as const, message: '' } } : {}), + }) + } else { + clearLockNotification() + } + }, [ + allBlocksLocked, + isWorkflowReady, + effectivePermissions.canAdmin, + addNotification, + activeWorkflowId, + clearLockNotification, + ]) + + // Clean up notification on unmount + useEffect(() => clearLockNotification, [clearLockNotification]) + + // Listen for unlock-workflow events from notification action button + useEffect(() => { + const handleUnlockWorkflow = () => { + const currentBlocks = useWorkflowStore.getState().blocks + const ids = getWorkflowLockToggleIds(currentBlocks, false) + if (ids.length > 0) collaborativeBatchToggleLocked(ids) + } + + window.addEventListener('unlock-workflow', handleUnlockWorkflow) + return () => window.removeEventListener('unlock-workflow', handleUnlockWorkflow) + }, [collaborativeBatchToggleLocked]) + const handleContextRemoveFromSubflow = useCallback(() => { const blocksToRemove = contextMenuBlocks.filter( (block) => block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel') @@ -2439,6 +2534,16 @@ const WorkflowContent = React.memo(() => { window.removeEventListener('remove-from-subflow', handleRemoveFromSubflow as EventListener) }, [blocks, edgesForDisplay, getNodeAbsolutePosition, collaborativeBatchUpdateParent]) + useEffect(() => { + const handleToggleWorkflowLock = (e: CustomEvent<{ blockIds: string[] }>) => { + collaborativeBatchToggleLocked(e.detail.blockIds) + } + + window.addEventListener('toggle-workflow-lock', handleToggleWorkflowLock as EventListener) + return () => + window.removeEventListener('toggle-workflow-lock', handleToggleWorkflowLock as EventListener) + }, [collaborativeBatchToggleLocked]) + /** * Updates container dimensions in displayNodes during drag or keyboard movement. */ @@ -3699,7 +3804,11 @@ const WorkflowContent = React.memo(() => { disableEdit={!effectivePermissions.canEdit} canUndo={canUndo} canRedo={canRedo} - hasLockedBlocks={Object.values(blocks).some((b) => b.locked)} + hasLockedBlocks={hasLockedBlocks} + onToggleWorkflowLock={handleToggleWorkflowLock} + allBlocksLocked={allBlocksLocked} + canAdmin={effectivePermissions.canAdmin} + hasBlocks={hasBlocks} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index efb3e7eb0f..b20fea3d9c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -281,6 +281,24 @@ interface ContextMenuProps { * Set to true when user cannot leave (e.g., last admin) */ disableLeave?: boolean + /** + * Callback when lock/unlock is clicked + */ + onToggleLock?: () => void + /** + * Whether to show the lock option (default: false) + * Set to true for workflows that support locking + */ + showLock?: boolean + /** + * Whether the lock option is disabled (default: false) + * Set to true when user lacks permissions + */ + disableLock?: boolean + /** + * Whether the workflow is currently locked (all blocks locked) + */ + isLocked?: boolean } /** @@ -321,6 +339,10 @@ export function ContextMenu({ onLeave, showLeave = false, disableLeave = false, + onToggleLock, + showLock = false, + disableLock = false, + isLocked = false, }: ContextMenuProps) { const [hexInput, setHexInput] = useState(currentColor || '#ffffff') @@ -372,7 +394,8 @@ export function ContextMenu({ (showRename && onRename) || (showCreate && onCreate) || (showCreateFolder && onCreateFolder) || - (showColorChange && onColorChange) + (showColorChange && onColorChange) || + (showLock && onToggleLock) const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport) return ( @@ -495,6 +518,19 @@ export function ContextMenu({ )} + {showLock && onToggleLock && ( + { + onToggleLock() + onClose() + }} + > + {isLocked ? 'Unlock' : 'Lock'} + + )} + {/* Copy and export actions */} {hasEditSection && hasCopySection && } {showDuplicate && onDuplicate && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index 6963464d46..6651f6880f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx @@ -6,6 +6,7 @@ import { MoreHorizontal } from 'lucide-react' import Link from 'next/link' import { useParams } from 'next/navigation' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu' import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal' import { Avatars } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars' @@ -27,6 +28,7 @@ import { import { useFolderStore } from '@/stores/folders/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' interface WorkflowItemProps { workflow: WorkflowMetadata @@ -169,6 +171,29 @@ export function WorkflowItem({ [workflow.id, updateWorkflow] ) + const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + const isActiveWorkflow = workflow.id === activeWorkflowId + + const isWorkflowLocked = useWorkflowStore( + useCallback( + (state) => { + if (!isActiveWorkflow) return false + const blockValues = Object.values(state.blocks) + if (blockValues.length === 0) return false + return blockValues.every((block) => block.locked) + }, + [isActiveWorkflow] + ) + ) + + const handleToggleLock = useCallback(() => { + if (!isActiveWorkflow) return + const blocks = useWorkflowStore.getState().blocks + const blockIds = getWorkflowLockToggleIds(blocks, !isWorkflowLocked) + if (blockIds.length === 0) return + window.dispatchEvent(new CustomEvent('toggle-workflow-lock', { detail: { blockIds } })) + }, [isActiveWorkflow, isWorkflowLocked]) + const isEditingRef = useRef(false) const { @@ -461,6 +486,10 @@ export function WorkflowItem({ disableExport={!userPermissions.canEdit} disableColorChange={!userPermissions.canEdit} disableDelete={!userPermissions.canEdit || !canDeleteSelection} + onToggleLock={handleToggleLock} + showLock={isActiveWorkflow && !isMixedSelection && selectedWorkflows.size <= 1} + disableLock={!userPermissions.canAdmin} + isLocked={isWorkflowLocked} /> = { 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -414,6 +415,8 @@ export const ConfluenceV2Block: BlockConfig = { { label: 'List Blog Posts', id: 'list_blogposts' }, { label: 'Get Blog Post', id: 'get_blogpost' }, { label: 'Create Blog Post', id: 'create_blogpost' }, + { label: 'Update Blog Post', id: 'update_blogpost' }, + { label: 'Delete Blog Post', id: 'delete_blogpost' }, { label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' }, // Comment Operations { label: 'Create Comment', id: 'create_comment' }, @@ -432,7 +435,24 @@ export const ConfluenceV2Block: BlockConfig = { { label: 'List Space Labels', id: 'list_space_labels' }, // Space Operations { label: 'Get Space', id: 'get_space' }, + { label: 'Create Space', id: 'create_space' }, + { label: 'Update Space', id: 'update_space' }, + { label: 'Delete Space', id: 'delete_space' }, { label: 'List Spaces', id: 'list_spaces' }, + // Space Property Operations + { label: 'List Space Properties', id: 'list_space_properties' }, + { label: 'Create Space Property', id: 'create_space_property' }, + { label: 'Delete Space Property', id: 'delete_space_property' }, + // Space Permission Operations + { label: 'List Space Permissions', id: 'list_space_permissions' }, + // Page Descendant Operations + { label: 'Get Page Descendants', id: 'get_page_descendants' }, + // Task Operations + { label: 'List Tasks', id: 'list_tasks' }, + { label: 'Get Task', id: 'get_task' }, + { label: 'Update Task', id: 'update_task' }, + // User Operations + { label: 'Get User', id: 'get_user' }, ], value: () => 'read', }, @@ -472,6 +492,15 @@ export const ConfluenceV2Block: BlockConfig = { 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'delete:blogpost:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -507,13 +536,26 @@ export const ConfluenceV2Block: BlockConfig = { 'list_pages_in_space', 'list_blogposts', 'get_blogpost', + 'update_blogpost', + 'delete_blogpost', 'list_blogposts_in_space', 'search', 'search_in_space', 'get_space', + 'create_space', + 'update_space', + 'delete_space', 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', + 'list_tasks', + 'get_task', + 'update_task', + 'get_user', ], not: true, }, @@ -537,6 +579,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -553,13 +596,26 @@ export const ConfluenceV2Block: BlockConfig = { 'list_pages_in_space', 'list_blogposts', 'get_blogpost', + 'update_blogpost', + 'delete_blogpost', 'list_blogposts_in_space', 'search', 'search_in_space', 'get_space', + 'create_space', + 'update_space', + 'delete_space', 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', + 'list_tasks', + 'get_task', + 'update_task', + 'get_user', ], not: true, }, @@ -583,6 +639,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -597,11 +654,17 @@ export const ConfluenceV2Block: BlockConfig = { value: [ 'create', 'get_space', + 'update_space', + 'delete_space', 'list_pages_in_space', 'search_in_space', 'create_blogpost', 'list_blogposts_in_space', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', ], }, }, @@ -611,7 +674,10 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter blog post ID', required: true, - condition: { field: 'operation', value: 'get_blogpost' }, + condition: { + field: 'operation', + value: ['get_blogpost', 'update_blogpost', 'delete_blogpost'], + }, }, { id: 'versionNumber', @@ -621,6 +687,86 @@ export const ConfluenceV2Block: BlockConfig = { required: true, condition: { field: 'operation', value: 'get_page_version' }, }, + { + id: 'accountId', + title: 'Account ID', + type: 'short-input', + placeholder: 'Enter Atlassian account ID', + required: true, + condition: { field: 'operation', value: 'get_user' }, + }, + { + id: 'taskId', + title: 'Task ID', + type: 'short-input', + placeholder: 'Enter task ID', + required: true, + condition: { field: 'operation', value: ['get_task', 'update_task'] }, + }, + { + id: 'taskStatus', + title: 'Task Status', + type: 'dropdown', + options: [ + { label: 'Complete', id: 'complete' }, + { label: 'Incomplete', id: 'incomplete' }, + ], + value: () => 'complete', + condition: { field: 'operation', value: 'update_task' }, + }, + { + id: 'taskAssignedTo', + title: 'Assigned To', + type: 'short-input', + placeholder: 'Filter by assignee account ID (optional)', + condition: { field: 'operation', value: 'list_tasks' }, + }, + { + id: 'spaceName', + title: 'Space Name', + type: 'short-input', + placeholder: 'Enter space name', + required: true, + condition: { field: 'operation', value: 'create_space' }, + }, + { + id: 'spaceKey', + title: 'Space Key', + type: 'short-input', + placeholder: 'Enter space key (e.g., MYSPACE)', + required: true, + condition: { field: 'operation', value: 'create_space' }, + }, + { + id: 'spaceDescription', + title: 'Description', + type: 'long-input', + placeholder: 'Enter space description (optional)', + condition: { field: 'operation', value: ['create_space', 'update_space'] }, + }, + { + id: 'spacePropertyKey', + title: 'Property Key', + type: 'short-input', + placeholder: 'Enter property key/name', + required: true, + condition: { field: 'operation', value: 'create_space_property' }, + }, + { + id: 'spacePropertyValue', + title: 'Property Value', + type: 'long-input', + placeholder: 'Enter property value (JSON supported)', + condition: { field: 'operation', value: 'create_space_property' }, + }, + { + id: 'spacePropertyId', + title: 'Property ID', + type: 'short-input', + placeholder: 'Enter property ID to delete', + required: true, + condition: { field: 'operation', value: 'delete_space_property' }, + }, { id: 'propertyKey', title: 'Property Key', @@ -650,14 +796,20 @@ export const ConfluenceV2Block: BlockConfig = { title: 'Title', type: 'short-input', placeholder: 'Enter title', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, + condition: { + field: 'operation', + value: ['create', 'update', 'create_blogpost', 'update_blogpost', 'update_space'], + }, }, { id: 'content', title: 'Content', type: 'long-input', placeholder: 'Enter content', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, + condition: { + field: 'operation', + value: ['create', 'update', 'create_blogpost', 'update_blogpost'], + }, }, { id: 'parentId', @@ -813,6 +965,10 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'get_pages_by_label', 'list_space_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', + 'list_tasks', ], }, }, @@ -836,6 +992,10 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'get_pages_by_label', 'list_space_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', + 'list_tasks', ], }, }, @@ -921,7 +1081,27 @@ export const ConfluenceV2Block: BlockConfig = { 'confluence_list_space_labels', // Space Tools 'confluence_get_space', + 'confluence_create_space', + 'confluence_update_space', + 'confluence_delete_space', 'confluence_list_spaces', + // Space Property Tools + 'confluence_list_space_properties', + 'confluence_create_space_property', + 'confluence_delete_space_property', + // Space Permission Tools + 'confluence_list_space_permissions', + // Page Descendant Tools + 'confluence_get_page_descendants', + // Task Tools + 'confluence_list_tasks', + 'confluence_get_task', + 'confluence_update_task', + // Blog Post Update/Delete + 'confluence_update_blogpost', + 'confluence_delete_blogpost', + // User Tools + 'confluence_get_user', ], config: { tool: (params) => { @@ -965,6 +1145,10 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_get_blogpost' case 'create_blogpost': return 'confluence_create_blogpost' + case 'update_blogpost': + return 'confluence_update_blogpost' + case 'delete_blogpost': + return 'confluence_delete_blogpost' case 'list_blogposts_in_space': return 'confluence_list_blogposts_in_space' // Comment Operations @@ -997,8 +1181,37 @@ export const ConfluenceV2Block: BlockConfig = { // Space Operations case 'get_space': return 'confluence_get_space' + case 'create_space': + return 'confluence_create_space' + case 'update_space': + return 'confluence_update_space' + case 'delete_space': + return 'confluence_delete_space' case 'list_spaces': return 'confluence_list_spaces' + // Space Property Operations + case 'list_space_properties': + return 'confluence_list_space_properties' + case 'create_space_property': + return 'confluence_create_space_property' + case 'delete_space_property': + return 'confluence_delete_space_property' + // Space Permission Operations + case 'list_space_permissions': + return 'confluence_list_space_permissions' + // Page Descendant Operations + case 'get_page_descendants': + return 'confluence_get_page_descendants' + // Task Operations + case 'list_tasks': + return 'confluence_list_tasks' + case 'get_task': + return 'confluence_get_task' + case 'update_task': + return 'confluence_update_task' + // User Operations + case 'get_user': + return 'confluence_get_user' default: return 'confluence_retrieve' } @@ -1013,6 +1226,7 @@ export const ConfluenceV2Block: BlockConfig = { attachmentComment, blogPostId, versionNumber, + accountId, propertyKey, propertyValue, propertyId, @@ -1022,6 +1236,15 @@ export const ConfluenceV2Block: BlockConfig = { purge, bodyFormat, cursor, + taskId, + taskStatus, + taskAssignedTo, + spaceName, + spaceKey, + spaceDescription, + spacePropertyKey, + spacePropertyValue, + spacePropertyId, ...rest } = params @@ -1069,8 +1292,8 @@ export const ConfluenceV2Block: BlockConfig = { } // Operations that support generic cursor pagination. - // get_pages_by_label and list_space_labels have dedicated handlers - // below that pass cursor along with their required params (labelId, spaceId). + // get_pages_by_label, list_space_labels, and list_tasks have dedicated handlers + // below that pass cursor along with their required params. const supportsCursor = [ 'list_attachments', 'list_spaces', @@ -1081,6 +1304,9 @@ export const ConfluenceV2Block: BlockConfig = { 'list_page_versions', 'list_page_properties', 'list_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', ] if (supportsCursor.includes(operation) && cursor) { @@ -1152,6 +1378,122 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'get_user') { + return { + credential: oauthCredential, + operation, + accountId: accountId ? String(accountId).trim() : undefined, + ...rest, + } + } + + if (operation === 'update_blogpost' || operation === 'delete_blogpost') { + return { + credential: oauthCredential, + operation, + blogPostId: blogPostId || undefined, + ...rest, + } + } + + if (operation === 'create_space') { + return { + credential: oauthCredential, + operation, + name: spaceName, + key: spaceKey, + description: spaceDescription, + ...rest, + } + } + + if (operation === 'update_space') { + return { + credential: oauthCredential, + operation, + name: spaceName || rest.title, + description: spaceDescription, + ...rest, + } + } + + if (operation === 'delete_space') { + return { + credential: oauthCredential, + operation, + ...rest, + } + } + + if (operation === 'create_space_property') { + return { + credential: oauthCredential, + operation, + key: spacePropertyKey, + value: spacePropertyValue, + ...rest, + } + } + + if (operation === 'delete_space_property') { + return { + credential: oauthCredential, + operation, + propertyId: spacePropertyId, + ...rest, + } + } + + if (operation === 'list_space_permissions' || operation === 'list_space_properties') { + return { + credential: oauthCredential, + operation, + cursor: cursor || undefined, + ...rest, + } + } + + if (operation === 'get_page_descendants') { + return { + credential: oauthCredential, + pageId: effectivePageId, + operation, + cursor: cursor || undefined, + ...rest, + } + } + + if (operation === 'get_task') { + return { + credential: oauthCredential, + operation, + taskId, + ...rest, + } + } + + if (operation === 'update_task') { + return { + credential: oauthCredential, + operation, + taskId, + status: taskStatus, + ...rest, + } + } + + if (operation === 'list_tasks') { + return { + credential: oauthCredential, + operation, + pageId: effectivePageId || undefined, + assignedTo: taskAssignedTo || undefined, + status: taskStatus || undefined, + cursor: cursor || undefined, + ...rest, + } + } + return { credential: oauthCredential, pageId: effectivePageId || undefined, @@ -1171,6 +1513,7 @@ export const ConfluenceV2Block: BlockConfig = { spaceId: { type: 'string', description: 'Space identifier' }, blogPostId: { type: 'string', description: 'Blog post identifier' }, versionNumber: { type: 'number', description: 'Page version number' }, + accountId: { type: 'string', description: 'Atlassian account ID' }, propertyKey: { type: 'string', description: 'Property key/name' }, propertyValue: { type: 'json', description: 'Property value (JSON)' }, title: { type: 'string', description: 'Page or blog post title' }, @@ -1192,6 +1535,15 @@ export const ConfluenceV2Block: BlockConfig = { bodyFormat: { type: 'string', description: 'Body format for comments' }, limit: { type: 'number', description: 'Maximum number of results' }, cursor: { type: 'string', description: 'Pagination cursor from previous response' }, + taskId: { type: 'string', description: 'Task identifier' }, + taskStatus: { type: 'string', description: 'Task status (complete or incomplete)' }, + taskAssignedTo: { type: 'string', description: 'Filter tasks by assignee account ID' }, + spaceName: { type: 'string', description: 'Space name for create/update' }, + spaceKey: { type: 'string', description: 'Space key for create' }, + spaceDescription: { type: 'string', description: 'Space description' }, + spacePropertyKey: { type: 'string', description: 'Space property key' }, + spacePropertyValue: { type: 'json', description: 'Space property value' }, + spacePropertyId: { type: 'string', description: 'Space property identifier' }, }, outputs: { ts: { type: 'string', description: 'Timestamp' }, @@ -1242,6 +1594,23 @@ export const ConfluenceV2Block: BlockConfig = { propertyId: { type: 'string', description: 'Property identifier' }, propertyKey: { type: 'string', description: 'Property key' }, propertyValue: { type: 'json', description: 'Property value' }, + // User Results + accountId: { type: 'string', description: 'Atlassian account ID' }, + displayName: { type: 'string', description: 'User display name' }, + email: { type: 'string', description: 'User email address' }, + accountType: { type: 'string', description: 'Account type (atlassian, app, customer)' }, + profilePicture: { type: 'string', description: 'Path to user profile picture' }, + publicName: { type: 'string', description: 'User public name' }, + // Task Results + tasks: { type: 'array', description: 'List of tasks' }, + taskId: { type: 'string', description: 'Task identifier' }, + // Descendant Results + descendants: { type: 'array', description: 'List of descendant pages' }, + // Permission Results + permissions: { type: 'array', description: 'List of space permissions' }, + // Space Property Results + homepageId: { type: 'string', description: 'Space homepage ID' }, + description: { type: 'json', description: 'Space description' }, // Pagination nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' }, }, diff --git a/apps/sim/blocks/blocks/devin.ts b/apps/sim/blocks/blocks/devin.ts new file mode 100644 index 0000000000..8289fc3476 --- /dev/null +++ b/apps/sim/blocks/blocks/devin.ts @@ -0,0 +1,187 @@ +import { DevinIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const DevinBlock: BlockConfig = { + type: 'devin', + name: 'Devin', + description: 'Autonomous AI software engineer', + longDescription: + 'Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code.', + bestPractices: ` + - Write clear, specific prompts describing the task, expected outcome, and any constraints. + - Use playbook IDs to standardize recurring task patterns across sessions. + - Set ACU limits to control cost for long-running tasks. + - Use Get Session to poll for completion status before consuming structured output. + - Send Message auto-resumes suspended sessions — no need to resume separately. + `, + docsLink: 'https://docs.sim.ai/tools/devin', + category: 'tools', + bgColor: '#12141A', + icon: DevinIcon, + authMode: AuthMode.ApiKey, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Session', id: 'create_session' }, + { label: 'Get Session', id: 'get_session' }, + { label: 'List Sessions', id: 'list_sessions' }, + { label: 'Send Message', id: 'send_message' }, + ], + value: () => 'create_session', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Devin API key (cog_...)', + password: true, + required: true, + }, + { + id: 'prompt', + title: 'Prompt', + type: 'long-input', + placeholder: 'Describe the task for Devin...', + required: { field: 'operation', value: 'create_session' }, + condition: { field: 'operation', value: 'create_session' }, + wandConfig: { + enabled: true, + prompt: `You are an expert at writing clear, actionable prompts for Devin, an autonomous AI software engineer. Generate or refine a task prompt based on the user's request. + +Current prompt: {context} + +RULES: +1. Be specific about the expected outcome and deliverables +2. Include relevant technical context (languages, frameworks, repos) +3. Specify any constraints (don't modify certain files, follow certain patterns) +4. Break complex tasks into clear steps when helpful +5. Return ONLY the prompt text, no markdown formatting or explanations`, + placeholder: 'Describe what you want Devin to do...', + }, + }, + { + id: 'playbookId', + title: 'Playbook ID', + type: 'short-input', + placeholder: 'Optional playbook ID to guide the session', + condition: { field: 'operation', value: 'create_session' }, + mode: 'advanced', + }, + { + id: 'maxAcuLimit', + title: 'Max ACU Limit', + type: 'short-input', + placeholder: 'Maximum ACU budget for this session', + condition: { field: 'operation', value: 'create_session' }, + mode: 'advanced', + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'Comma-separated tags', + condition: { field: 'operation', value: 'create_session' }, + mode: 'advanced', + }, + { + id: 'sessionId', + title: 'Session ID', + type: 'short-input', + placeholder: 'Enter session ID', + required: { field: 'operation', value: ['get_session', 'send_message'] }, + condition: { field: 'operation', value: ['get_session', 'send_message'] }, + }, + { + id: 'message', + title: 'Message', + type: 'long-input', + placeholder: 'Enter message to send to Devin...', + required: { field: 'operation', value: 'send_message' }, + condition: { field: 'operation', value: 'send_message' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of sessions (1-200, default: 100)', + condition: { field: 'operation', value: 'list_sessions' }, + mode: 'advanced', + }, + ], + tools: { + access: [ + 'devin_create_session', + 'devin_get_session', + 'devin_list_sessions', + 'devin_send_message', + ], + config: { + tool: (params) => `devin_${params.operation}`, + params: (params) => { + if (params.maxAcuLimit != null && params.maxAcuLimit !== '') { + params.maxAcuLimit = Number(params.maxAcuLimit) + } + if (params.limit != null && params.limit !== '') { + params.limit = Number(params.limit) + } + return params + }, + }, + }, + inputs: { + prompt: { type: 'string', description: 'Task prompt for Devin' }, + sessionId: { type: 'string', description: 'Session ID' }, + message: { type: 'string', description: 'Message to send to the session' }, + apiKey: { type: 'string', description: 'Devin API key' }, + playbookId: { type: 'string', description: 'Playbook ID to guide the session' }, + maxAcuLimit: { type: 'number', description: 'Maximum ACU limit' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + limit: { type: 'number', description: 'Number of sessions to return' }, + }, + outputs: { + sessionId: { type: 'string', description: 'Session identifier' }, + url: { type: 'string', description: 'URL to view the session in Devin UI' }, + status: { + type: 'string', + description: 'Session status (new, claimed, running, exit, error, suspended, resuming)', + }, + statusDetail: { + type: 'string', + description: 'Detailed status (working, waiting_for_user, finished, etc.)', + condition: { field: 'operation', value: 'list_sessions', not: true }, + }, + title: { type: 'string', description: 'Session title' }, + createdAt: { type: 'number', description: 'Creation timestamp (Unix)' }, + updatedAt: { type: 'number', description: 'Last updated timestamp (Unix)' }, + acusConsumed: { + type: 'number', + description: 'ACUs consumed', + condition: { field: 'operation', value: 'list_sessions', not: true }, + }, + tags: { type: 'json', description: 'Session tags' }, + pullRequests: { + type: 'json', + description: 'Pull requests created during the session', + condition: { field: 'operation', value: 'list_sessions', not: true }, + }, + structuredOutput: { + type: 'json', + description: 'Structured output from the session', + condition: { field: 'operation', value: 'list_sessions', not: true }, + }, + playbookId: { + type: 'string', + description: 'Associated playbook ID', + condition: { field: 'operation', value: 'list_sessions', not: true }, + }, + sessions: { + type: 'json', + description: 'List of sessions', + condition: { field: 'operation', value: 'list_sessions' }, + }, + }, +} diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts new file mode 100644 index 0000000000..0ba15dfe56 --- /dev/null +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -0,0 +1,256 @@ +import { GoogleBigQueryIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const GoogleBigQueryBlock: BlockConfig = { + type: 'google_bigquery', + name: 'Google BigQuery', + description: 'Query, list, and insert data in Google BigQuery', + longDescription: + 'Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows.', + docsLink: 'https://docs.sim.ai/tools/google_bigquery', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleBigQueryIcon, + authMode: AuthMode.OAuth, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Run Query', id: 'query' }, + { label: 'List Datasets', id: 'list_datasets' }, + { label: 'List Tables', id: 'list_tables' }, + { label: 'Get Table', id: 'get_table' }, + { label: 'Insert Rows', id: 'insert_rows' }, + ], + value: () => 'query', + }, + + { + id: 'credential', + title: 'Google Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-bigquery', + requiredScopes: ['https://www.googleapis.com/auth/bigquery'], + placeholder: 'Select Google account', + }, + { + id: 'manualCredential', + title: 'Google Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + { + id: 'projectId', + title: 'Project ID', + type: 'short-input', + placeholder: 'Enter Google Cloud project ID', + required: true, + }, + + { + id: 'query', + title: 'SQL Query', + type: 'long-input', + placeholder: 'SELECT * FROM `project.dataset.table` LIMIT 100', + condition: { field: 'operation', value: 'query' }, + required: { field: 'operation', value: 'query' }, + wandConfig: { + enabled: true, + prompt: `Generate a BigQuery Standard SQL query based on the user's description. +The query should: +- Use Standard SQL syntax (not Legacy SQL) +- Be well-formatted and efficient +- Include appropriate LIMIT clauses when applicable + +Examples: +- "get all users" -> SELECT * FROM \`project.dataset.users\` LIMIT 1000 +- "count orders by status" -> SELECT status, COUNT(*) as count FROM \`project.dataset.orders\` GROUP BY status +- "recent events" -> SELECT * FROM \`project.dataset.events\` ORDER BY created_at DESC LIMIT 100 + +Return ONLY the SQL query - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the query you want to run...', + }, + }, + { + id: 'useLegacySql', + title: 'Use Legacy SQL', + type: 'switch', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum rows to return', + condition: { field: 'operation', value: ['query', 'list_datasets', 'list_tables'] }, + }, + { + id: 'defaultDatasetId', + title: 'Default Dataset', + type: 'short-input', + placeholder: 'Default dataset for unqualified table names', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'location', + title: 'Location', + type: 'short-input', + placeholder: 'Processing location (e.g., US, EU)', + condition: { field: 'operation', value: 'query' }, + }, + + { + id: 'datasetId', + title: 'Dataset ID', + type: 'short-input', + placeholder: 'Enter BigQuery dataset ID', + condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + }, + + { + id: 'tableId', + title: 'Table ID', + type: 'short-input', + placeholder: 'Enter BigQuery table ID', + condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, + required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + }, + + { + id: 'rows', + title: 'Rows', + type: 'long-input', + placeholder: '[{"column1": "value1", "column2": 42}]', + condition: { field: 'operation', value: 'insert_rows' }, + required: { field: 'operation', value: 'insert_rows' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of row objects for BigQuery insertion based on the user's description. +Each row should be a JSON object where keys are column names and values match the expected types. + +Examples: +- "3 users" -> [{"name": "Alice", "email": "alice@example.com"}, {"name": "Bob", "email": "bob@example.com"}, {"name": "Charlie", "email": "charlie@example.com"}] +- "order record" -> [{"order_id": "ORD-001", "amount": 99.99, "status": "pending"}] + +Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, + placeholder: 'Describe the rows to insert...', + generationType: 'json-object', + }, + }, + { + id: 'skipInvalidRows', + title: 'Skip Invalid Rows', + type: 'switch', + condition: { field: 'operation', value: 'insert_rows' }, + }, + { + id: 'ignoreUnknownValues', + title: 'Ignore Unknown Values', + type: 'switch', + condition: { field: 'operation', value: 'insert_rows' }, + }, + + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Pagination token', + condition: { field: 'operation', value: ['list_datasets', 'list_tables'] }, + }, + ], + tools: { + access: [ + 'google_bigquery_query', + 'google_bigquery_list_datasets', + 'google_bigquery_list_tables', + 'google_bigquery_get_table', + 'google_bigquery_insert_rows', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'query': + return 'google_bigquery_query' + case 'list_datasets': + return 'google_bigquery_list_datasets' + case 'list_tables': + return 'google_bigquery_list_tables' + case 'get_table': + return 'google_bigquery_get_table' + case 'insert_rows': + return 'google_bigquery_insert_rows' + default: + throw new Error(`Invalid Google BigQuery operation: ${params.operation}`) + } + }, + params: (params) => { + const { oauthCredential, rows, maxResults, ...rest } = params + return { + ...rest, + oauthCredential, + ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), + ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }), + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google BigQuery OAuth credential' }, + projectId: { type: 'string', description: 'Google Cloud project ID' }, + query: { type: 'string', description: 'SQL query to execute' }, + useLegacySql: { type: 'boolean', description: 'Whether to use legacy SQL syntax' }, + maxResults: { type: 'number', description: 'Maximum number of results to return' }, + defaultDatasetId: { + type: 'string', + description: 'Default dataset for unqualified table names', + }, + location: { type: 'string', description: 'Processing location' }, + datasetId: { type: 'string', description: 'BigQuery dataset ID' }, + tableId: { type: 'string', description: 'BigQuery table ID' }, + rows: { type: 'string', description: 'JSON array of row objects to insert' }, + skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' }, + ignoreUnknownValues: { + type: 'boolean', + description: 'Whether to ignore unknown column values', + }, + pageToken: { type: 'string', description: 'Pagination token' }, + }, + outputs: { + columns: { type: 'json', description: 'Array of column names (query)' }, + rows: { type: 'json', description: 'Array of row objects (query)' }, + totalRows: { type: 'string', description: 'Total number of rows (query)' }, + jobComplete: { type: 'boolean', description: 'Whether the query completed (query)' }, + totalBytesProcessed: { type: 'string', description: 'Bytes processed (query)' }, + cacheHit: { type: 'boolean', description: 'Whether result was cached (query)' }, + jobReference: { type: 'json', description: 'Job reference for incomplete queries (query)' }, + pageToken: { type: 'string', description: 'Token for additional result pages (query)' }, + datasets: { type: 'json', description: 'Array of dataset objects (list_datasets)' }, + tables: { type: 'json', description: 'Array of table objects (list_tables)' }, + totalItems: { type: 'number', description: 'Total items count (list_tables)' }, + tableId: { type: 'string', description: 'Table ID (get_table)' }, + datasetId: { type: 'string', description: 'Dataset ID (get_table)' }, + type: { type: 'string', description: 'Table type (get_table)' }, + description: { type: 'string', description: 'Table description (get_table)' }, + numRows: { type: 'string', description: 'Row count (get_table)' }, + numBytes: { type: 'string', description: 'Size in bytes (get_table)' }, + schema: { type: 'json', description: 'Column definitions (get_table)' }, + creationTime: { type: 'string', description: 'Creation time (get_table)' }, + lastModifiedTime: { type: 'string', description: 'Last modified time (get_table)' }, + location: { type: 'string', description: 'Data location (get_table)' }, + insertedRows: { type: 'number', description: 'Rows inserted (insert_rows)' }, + errors: { type: 'json', description: 'Insert errors (insert_rows)' }, + nextPageToken: { type: 'string', description: 'Token for next page of results' }, + }, +} diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index bde2bec45d..5c0232555b 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -440,6 +440,36 @@ Return ONLY the range string - no sheet name, no explanations, no quotes.`, placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...', }, }, + // Read Filter Fields (advanced mode only) + { + id: 'filterColumn', + title: 'Filter Column', + type: 'short-input', + placeholder: 'Column header name to filter on (e.g., Email, Status)', + condition: { field: 'operation', value: 'read' }, + mode: 'advanced', + }, + { + id: 'filterValue', + title: 'Filter Value', + type: 'short-input', + placeholder: 'Value to match against', + condition: { field: 'operation', value: 'read' }, + mode: 'advanced', + }, + { + id: 'filterMatchType', + title: 'Match Type', + type: 'dropdown', + options: [ + { label: 'Contains', id: 'contains' }, + { label: 'Exact Match', id: 'exact' }, + { label: 'Starts With', id: 'starts_with' }, + { label: 'Ends With', id: 'ends_with' }, + ], + condition: { field: 'operation', value: 'read' }, + mode: 'advanced', + }, // Write-specific Fields { id: 'values', @@ -748,6 +778,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, batchData, sheetId, destinationSpreadsheetId, + filterColumn, + filterValue, + filterMatchType, ...rest } = params @@ -836,6 +869,11 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, cellRange: cellRange ? (cellRange as string).trim() : undefined, values: parsedValues, oauthCredential, + ...(filterColumn ? { filterColumn: (filterColumn as string).trim() } : {}), + ...(filterValue !== undefined && filterValue !== '' + ? { filterValue: filterValue as string } + : {}), + ...(filterMatchType ? { filterMatchType: filterMatchType as string } : {}), } }, }, @@ -858,6 +896,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, type: 'string', description: 'Destination spreadsheet ID for copy', }, + filterColumn: { type: 'string', description: 'Column header name to filter on' }, + filterValue: { type: 'string', description: 'Value to match against the filter column' }, + filterMatchType: { + type: 'string', + description: 'Match type: contains, exact, starts_with, or ends_with', + }, }, outputs: { // Read outputs diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts new file mode 100644 index 0000000000..850f824d50 --- /dev/null +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -0,0 +1,262 @@ +import { GoogleTasksIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GoogleTasksResponse } from '@/tools/google_tasks/types' + +export const GoogleTasksBlock: BlockConfig = { + type: 'google_tasks', + name: 'Google Tasks', + description: 'Manage Google Tasks', + longDescription: + 'Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists.', + docsLink: 'https://docs.sim.ai/tools/google_tasks', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleTasksIcon, + authMode: AuthMode.OAuth, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Task', id: 'create' }, + { label: 'List Tasks', id: 'list' }, + { label: 'Get Task', id: 'get' }, + { label: 'Update Task', id: 'update' }, + { label: 'Delete Task', id: 'delete' }, + { label: 'List Task Lists', id: 'list_task_lists' }, + ], + value: () => 'create', + }, + { + id: 'credential', + title: 'Google Tasks Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-tasks', + requiredScopes: ['https://www.googleapis.com/auth/tasks'], + placeholder: 'Select Google Tasks account', + }, + { + id: 'manualCredential', + title: 'Google Tasks Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + // Task List ID - shown for all task operations (not list_task_lists) + { + id: 'taskListId', + title: 'Task List ID', + type: 'short-input', + placeholder: 'Task list ID (leave empty for default list)', + condition: { field: 'operation', value: 'list_task_lists', not: true }, + }, + + // Create Task Fields + { + id: 'title', + title: 'Title', + type: 'short-input', + placeholder: 'Buy groceries', + condition: { field: 'operation', value: 'create' }, + required: { field: 'operation', value: 'create' }, + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + placeholder: 'Task notes or description', + condition: { field: 'operation', value: 'create' }, + }, + { + id: 'due', + title: 'Due Date', + type: 'short-input', + placeholder: '2025-06-03T00:00:00.000Z', + condition: { field: 'operation', value: 'create' }, + wandConfig: { + enabled: true, + prompt: `Generate an RFC 3339 timestamp in UTC based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS.000Z (UTC timezone). +Examples: +- "tomorrow" -> Calculate tomorrow's date at 00:00:00.000Z +- "next Friday" -> Calculate the next Friday's date at 00:00:00.000Z +- "June 15" -> 2025-06-15T00:00:00.000Z + +Return ONLY the timestamp - no explanations, no extra text.`, + }, + }, + { + id: 'status', + title: 'Status', + type: 'dropdown', + condition: { field: 'operation', value: 'create' }, + options: [ + { label: 'Needs Action', id: 'needsAction' }, + { label: 'Completed', id: 'completed' }, + ], + }, + + // Get/Update/Delete Task Fields - Task ID + { + id: 'taskId', + title: 'Task ID', + type: 'short-input', + placeholder: 'Task ID', + condition: { field: 'operation', value: ['get', 'update', 'delete'] }, + required: { field: 'operation', value: ['get', 'update', 'delete'] }, + }, + + // Update Task Fields + { + id: 'title', + title: 'New Title', + type: 'short-input', + placeholder: 'Updated task title', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'notes', + title: 'New Notes', + type: 'long-input', + placeholder: 'Updated task notes', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'due', + title: 'New Due Date', + type: 'short-input', + placeholder: '2025-06-03T00:00:00.000Z', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate an RFC 3339 timestamp in UTC based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS.000Z (UTC timezone). +Examples: +- "tomorrow" -> Calculate tomorrow's date at 00:00:00.000Z +- "next Friday" -> Calculate the next Friday's date at 00:00:00.000Z +- "June 15" -> 2025-06-15T00:00:00.000Z + +Return ONLY the timestamp - no explanations, no extra text.`, + }, + }, + { + id: 'status', + title: 'New Status', + type: 'dropdown', + condition: { field: 'operation', value: 'update' }, + options: [ + { label: 'Needs Action', id: 'needsAction' }, + { label: 'Completed', id: 'completed' }, + ], + }, + + // List Tasks Fields + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: ['list', 'list_task_lists'] }, + }, + { + id: 'showCompleted', + title: 'Show Completed', + type: 'dropdown', + condition: { field: 'operation', value: 'list' }, + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + }, + ], + + tools: { + access: [ + 'google_tasks_create', + 'google_tasks_list', + 'google_tasks_get', + 'google_tasks_update', + 'google_tasks_delete', + 'google_tasks_list_task_lists', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'create': + return 'google_tasks_create' + case 'list': + return 'google_tasks_list' + case 'get': + return 'google_tasks_get' + case 'update': + return 'google_tasks_update' + case 'delete': + return 'google_tasks_delete' + case 'list_task_lists': + return 'google_tasks_list_task_lists' + default: + throw new Error(`Invalid Google Tasks operation: ${params.operation}`) + } + }, + params: (params) => { + const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params + + const processedParams: Record = { ...rest } + + if (maxResults && typeof maxResults === 'string') { + processedParams.maxResults = Number.parseInt(maxResults, 10) + } + + if (showCompleted !== undefined) { + processedParams.showCompleted = showCompleted === 'true' + } + + return { + oauthCredential, + ...processedParams, + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google Tasks access token' }, + taskListId: { type: 'string', description: 'Task list identifier' }, + title: { type: 'string', description: 'Task title' }, + notes: { type: 'string', description: 'Task notes' }, + due: { type: 'string', description: 'Task due date' }, + status: { type: 'string', description: 'Task status' }, + taskId: { type: 'string', description: 'Task identifier' }, + maxResults: { type: 'string', description: 'Maximum number of results' }, + showCompleted: { type: 'string', description: 'Whether to show completed tasks' }, + }, + + outputs: { + id: { type: 'string', description: 'Task ID' }, + title: { type: 'string', description: 'Task title' }, + notes: { type: 'string', description: 'Task notes' }, + status: { type: 'string', description: 'Task status' }, + due: { type: 'string', description: 'Due date' }, + updated: { type: 'string', description: 'Last modification time' }, + selfLink: { type: 'string', description: 'URL for the task' }, + webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI' }, + parent: { type: 'string', description: 'Parent task ID' }, + position: { type: 'string', description: 'Position among sibling tasks' }, + completed: { type: 'string', description: 'Completion date' }, + deleted: { type: 'boolean', description: 'Whether the task is deleted' }, + tasks: { type: 'json', description: 'Array of tasks (list operation)' }, + taskLists: { type: 'json', description: 'Array of task lists (list_task_lists operation)' }, + taskId: { type: 'string', description: 'Deleted task ID (delete operation)' }, + nextPageToken: { type: 'string', description: 'Token for next page of results' }, + }, +} diff --git a/apps/sim/blocks/blocks/google_translate.ts b/apps/sim/blocks/blocks/google_translate.ts new file mode 100644 index 0000000000..19c3236fb6 --- /dev/null +++ b/apps/sim/blocks/blocks/google_translate.ts @@ -0,0 +1,215 @@ +import { GoogleTranslateIcon } from '@/components/icons' +import { AuthMode, type BlockConfig } from '@/blocks/types' + +const SUPPORTED_LANGUAGES = [ + { label: 'Afrikaans', id: 'af' }, + { label: 'Albanian', id: 'sq' }, + { label: 'Amharic', id: 'am' }, + { label: 'Arabic', id: 'ar' }, + { label: 'Armenian', id: 'hy' }, + { label: 'Assamese', id: 'as' }, + { label: 'Aymara', id: 'ay' }, + { label: 'Azerbaijani', id: 'az' }, + { label: 'Bambara', id: 'bm' }, + { label: 'Basque', id: 'eu' }, + { label: 'Belarusian', id: 'be' }, + { label: 'Bengali', id: 'bn' }, + { label: 'Bhojpuri', id: 'bho' }, + { label: 'Bosnian', id: 'bs' }, + { label: 'Bulgarian', id: 'bg' }, + { label: 'Catalan', id: 'ca' }, + { label: 'Cebuano', id: 'ceb' }, + { label: 'Chinese (Simplified)', id: 'zh-CN' }, + { label: 'Chinese (Traditional)', id: 'zh-TW' }, + { label: 'Corsican', id: 'co' }, + { label: 'Croatian', id: 'hr' }, + { label: 'Czech', id: 'cs' }, + { label: 'Danish', id: 'da' }, + { label: 'Dhivehi', id: 'dv' }, + { label: 'Dogri', id: 'doi' }, + { label: 'Dutch', id: 'nl' }, + { label: 'English', id: 'en' }, + { label: 'Esperanto', id: 'eo' }, + { label: 'Estonian', id: 'et' }, + { label: 'Ewe', id: 'ee' }, + { label: 'Filipino', id: 'tl' }, + { label: 'Finnish', id: 'fi' }, + { label: 'French', id: 'fr' }, + { label: 'Frisian', id: 'fy' }, + { label: 'Galician', id: 'gl' }, + { label: 'Georgian', id: 'ka' }, + { label: 'German', id: 'de' }, + { label: 'Greek', id: 'el' }, + { label: 'Guarani', id: 'gn' }, + { label: 'Gujarati', id: 'gu' }, + { label: 'Haitian Creole', id: 'ht' }, + { label: 'Hausa', id: 'ha' }, + { label: 'Hawaiian', id: 'haw' }, + { label: 'Hebrew', id: 'he' }, + { label: 'Hindi', id: 'hi' }, + { label: 'Hmong', id: 'hmn' }, + { label: 'Hungarian', id: 'hu' }, + { label: 'Icelandic', id: 'is' }, + { label: 'Igbo', id: 'ig' }, + { label: 'Ilocano', id: 'ilo' }, + { label: 'Indonesian', id: 'id' }, + { label: 'Irish', id: 'ga' }, + { label: 'Italian', id: 'it' }, + { label: 'Japanese', id: 'ja' }, + { label: 'Javanese', id: 'jv' }, + { label: 'Kannada', id: 'kn' }, + { label: 'Kazakh', id: 'kk' }, + { label: 'Khmer', id: 'km' }, + { label: 'Kinyarwanda', id: 'rw' }, + { label: 'Konkani', id: 'gom' }, + { label: 'Korean', id: 'ko' }, + { label: 'Krio', id: 'kri' }, + { label: 'Kurdish', id: 'ku' }, + { label: 'Kurdish (Sorani)', id: 'ckb' }, + { label: 'Kyrgyz', id: 'ky' }, + { label: 'Lao', id: 'lo' }, + { label: 'Latin', id: 'la' }, + { label: 'Latvian', id: 'lv' }, + { label: 'Lingala', id: 'ln' }, + { label: 'Lithuanian', id: 'lt' }, + { label: 'Luganda', id: 'lg' }, + { label: 'Luxembourgish', id: 'lb' }, + { label: 'Macedonian', id: 'mk' }, + { label: 'Maithili', id: 'mai' }, + { label: 'Malagasy', id: 'mg' }, + { label: 'Malay', id: 'ms' }, + { label: 'Malayalam', id: 'ml' }, + { label: 'Maltese', id: 'mt' }, + { label: 'Maori', id: 'mi' }, + { label: 'Marathi', id: 'mr' }, + { label: 'Meiteilon (Manipuri)', id: 'mni-Mtei' }, + { label: 'Mizo', id: 'lus' }, + { label: 'Mongolian', id: 'mn' }, + { label: 'Myanmar (Burmese)', id: 'my' }, + { label: 'Nepali', id: 'ne' }, + { label: 'Norwegian', id: 'no' }, + { label: 'Nyanja (Chichewa)', id: 'ny' }, + { label: 'Odia (Oriya)', id: 'or' }, + { label: 'Oromo', id: 'om' }, + { label: 'Pashto', id: 'ps' }, + { label: 'Persian', id: 'fa' }, + { label: 'Polish', id: 'pl' }, + { label: 'Portuguese', id: 'pt' }, + { label: 'Punjabi', id: 'pa' }, + { label: 'Quechua', id: 'qu' }, + { label: 'Romanian', id: 'ro' }, + { label: 'Russian', id: 'ru' }, + { label: 'Samoan', id: 'sm' }, + { label: 'Sanskrit', id: 'sa' }, + { label: 'Scots Gaelic', id: 'gd' }, + { label: 'Sepedi', id: 'nso' }, + { label: 'Serbian', id: 'sr' }, + { label: 'Sesotho', id: 'st' }, + { label: 'Shona', id: 'sn' }, + { label: 'Sindhi', id: 'sd' }, + { label: 'Sinhala', id: 'si' }, + { label: 'Slovak', id: 'sk' }, + { label: 'Slovenian', id: 'sl' }, + { label: 'Somali', id: 'so' }, + { label: 'Spanish', id: 'es' }, + { label: 'Sundanese', id: 'su' }, + { label: 'Swahili', id: 'sw' }, + { label: 'Swedish', id: 'sv' }, + { label: 'Tajik', id: 'tg' }, + { label: 'Tamil', id: 'ta' }, + { label: 'Tatar', id: 'tt' }, + { label: 'Telugu', id: 'te' }, + { label: 'Thai', id: 'th' }, + { label: 'Tigrinya', id: 'ti' }, + { label: 'Tsonga', id: 'ts' }, + { label: 'Turkish', id: 'tr' }, + { label: 'Turkmen', id: 'tk' }, + { label: 'Twi (Akan)', id: 'ak' }, + { label: 'Ukrainian', id: 'uk' }, + { label: 'Urdu', id: 'ur' }, + { label: 'Uyghur', id: 'ug' }, + { label: 'Uzbek', id: 'uz' }, + { label: 'Vietnamese', id: 'vi' }, + { label: 'Welsh', id: 'cy' }, + { label: 'Xhosa', id: 'xh' }, + { label: 'Yiddish', id: 'yi' }, + { label: 'Yoruba', id: 'yo' }, + { label: 'Zulu', id: 'zu' }, +] satisfies { label: string; id: string }[] + +export const GoogleTranslateBlock: BlockConfig = { + type: 'google_translate', + name: 'Google Translate', + description: 'Translate text using Google Cloud Translation', + longDescription: + 'Translate and detect languages using the Google Cloud Translation API. Supports auto-detection of the source language.', + docsLink: 'https://docs.sim.ai/tools/google_translate', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleTranslateIcon, + authMode: AuthMode.ApiKey, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Translate Text', id: 'text' }, + { label: 'Detect Language', id: 'detect' }, + ], + value: () => 'text', + }, + { + id: 'text', + title: 'Text', + type: 'long-input', + placeholder: 'Enter text...', + required: true, + }, + { + id: 'target', + title: 'Target Language', + type: 'dropdown', + condition: { field: 'operation', value: 'text' }, + searchable: true, + options: SUPPORTED_LANGUAGES, + value: () => 'es', + required: { field: 'operation', value: 'text' }, + }, + { + id: 'source', + title: 'Source Language', + type: 'dropdown', + condition: { field: 'operation', value: 'text' }, + searchable: true, + options: [{ label: 'Auto-detect', id: '' }, ...SUPPORTED_LANGUAGES], + value: () => '', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Google Cloud API key', + password: true, + required: true, + }, + ], + tools: { + access: ['google_translate_text', 'google_translate_detect'], + config: { + tool: (params) => `google_translate_${params.operation}`, + }, + }, + inputs: { + text: { type: 'string', description: 'Text to translate or detect language of' }, + target: { type: 'string', description: 'Target language code' }, + source: { type: 'string', description: 'Source language code (optional, auto-detected)' }, + apiKey: { type: 'string', description: 'Google Cloud API key' }, + }, + outputs: { + translatedText: { type: 'string', description: 'Translated text' }, + detectedSourceLanguage: { type: 'string', description: 'Detected source language code' }, + language: { type: 'string', description: 'Detected language code' }, + confidence: { type: 'number', description: 'Detection confidence score' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 03b9827a77..eff25ffb1d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -23,6 +23,7 @@ import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence' import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor' import { DatadogBlock } from '@/blocks/blocks/datadog' +import { DevinBlock } from '@/blocks/blocks/devin' import { DiscordBlock } from '@/blocks/blocks/discord' import { DropboxBlock } from '@/blocks/blocks/dropbox' import { DSPyBlock } from '@/blocks/blocks/dspy' @@ -43,6 +44,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab' import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail' import { GongBlock } from '@/blocks/blocks/gong' import { GoogleSearchBlock } from '@/blocks/blocks/google' +import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' @@ -52,6 +54,8 @@ import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups' import { GoogleMapsBlock } from '@/blocks/blocks/google_maps' import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets' import { GoogleSlidesBlock, GoogleSlidesV2Block } from '@/blocks/blocks/google_slides' +import { GoogleTasksBlock } from '@/blocks/blocks/google_tasks' +import { GoogleTranslateBlock } from '@/blocks/blocks/google_translate' import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' import { GrafanaBlock } from '@/blocks/blocks/grafana' import { GrainBlock } from '@/blocks/blocks/grain' @@ -203,6 +207,7 @@ export const registry: Record = { cursor: CursorBlock, cursor_v2: CursorV2Block, datadog: DatadogBlock, + devin: DevinBlock, discord: DiscordBlock, dropbox: DropboxBlock, dspy: DSPyBlock, @@ -234,12 +239,15 @@ export const registry: Record = { google_forms: GoogleFormsBlock, google_groups: GoogleGroupsBlock, google_maps: GoogleMapsBlock, + google_tasks: GoogleTasksBlock, + google_translate: GoogleTranslateBlock, gong: GongBlock, google_search: GoogleSearchBlock, google_sheets: GoogleSheetsBlock, google_sheets_v2: GoogleSheetsV2Block, google_slides: GoogleSlidesBlock, google_slides_v2: GoogleSlidesV2Block, + google_bigquery: GoogleBigQueryBlock, google_vault: GoogleVaultBlock, grafana: GrafanaBlock, grain: GrainBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dcd5741f2b..9e68974089 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -939,6 +939,25 @@ export function GoogleIcon(props: SVGProps) { ) } +export function DevinIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function DiscordIcon(props: SVGProps) { return ( ) { ) } +export function GoogleTasksIcon(props: SVGProps) { + return ( + + + + + ) +} + export function SupabaseIcon(props: SVGProps) { const id = useId() const gradient0 = `supabase_paint0_${id}` @@ -3430,6 +3464,23 @@ export const ResendIcon = (props: SVGProps) => ( ) +export const GoogleBigQueryIcon = (props: SVGProps) => ( + + + + + +) + export const GoogleVaultIcon = (props: SVGProps) => ( ) { ) } +export function GoogleTranslateIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function DsPyIcon(props: SVGProps) { return ( diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index 7d3967732e..90c632fa5f 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -158,7 +158,6 @@ export const DEFAULTS = { MAX_LOOP_ITERATIONS: 1000, MAX_FOREACH_ITEMS: 1000, MAX_PARALLEL_BRANCHES: 20, - MAX_WORKFLOW_DEPTH: 10, MAX_SSE_CHILD_DEPTH: 3, EXECUTION_TIME: 0, TOKENS: { diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 6d7f746d4e..9325aa2861 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -80,7 +80,10 @@ export class BlockExecutor { const startTime = performance.now() let resolvedInputs: Record = {} - const nodeMetadata = this.buildNodeMetadata(node) + const nodeMetadata = { + ...this.buildNodeMetadata(node), + executionOrder: blockLog?.executionOrder, + } let cleanupSelfReference: (() => void) | undefined if (block.metadata?.id === BlockType.HUMAN_IN_THE_LOOP) { diff --git a/apps/sim/executor/execution/types.ts b/apps/sim/executor/execution/types.ts index d3ac877b89..bea082fe8a 100644 --- a/apps/sim/executor/execution/types.ts +++ b/apps/sim/executor/execution/types.ts @@ -89,7 +89,8 @@ export interface ExecutionCallbacks { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void } @@ -155,7 +156,8 @@ export interface ContextExtensions { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void /** diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 75c22e8be8..98560156e4 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -123,7 +123,6 @@ describe('AgentBlockHandler', () => { let handler: AgentBlockHandler let mockBlock: SerializedBlock let mockContext: ExecutionContext - let originalPromiseAll: any beforeEach(() => { handler = new AgentBlockHandler() @@ -135,8 +134,6 @@ describe('AgentBlockHandler', () => { configurable: true, }) - originalPromiseAll = Promise.all - mockBlock = { id: 'test-agent-block', metadata: { id: BlockType.AGENT, name: 'Test Agent' }, @@ -209,8 +206,6 @@ describe('AgentBlockHandler', () => { }) afterEach(() => { - Promise.all = originalPromiseAll - try { Object.defineProperty(global, 'window', { value: undefined, @@ -271,38 +266,7 @@ describe('AgentBlockHandler', () => { expect(result).toEqual(expectedOutput) }) - it('should preserve executeFunction for custom tools with different usageControl settings', async () => { - let capturedTools: any[] = [] - - Promise.all = vi.fn().mockImplementation((promises: Promise[]) => { - const result = originalPromiseAll.call(Promise, promises) - - result.then((tools: any[]) => { - if (tools?.length) { - capturedTools = tools.filter((t) => t !== null) - } - }) - - return result - }) - - mockExecuteProviderRequest.mockResolvedValueOnce({ - content: 'Using tools to respond', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - toolCalls: [ - { - name: 'auto_tool', - arguments: { input: 'test input for auto tool' }, - }, - { - name: 'force_tool', - arguments: { input: 'test input for force tool' }, - }, - ], - timing: { total: 100 }, - }) - + it('should preserve usageControl for custom tools and filter out "none"', async () => { const inputs = { model: 'gpt-4o', userPrompt: 'Test custom tools with different usageControl settings', @@ -372,13 +336,14 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(Promise.all).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools - expect(capturedTools.length).toBe(2) + expect(tools.length).toBe(2) - const autoTool = capturedTools.find((t) => t.name === 'auto_tool') - const forceTool = capturedTools.find((t) => t.name === 'force_tool') - const noneTool = capturedTools.find((t) => t.name === 'none_tool') + const autoTool = tools.find((t: any) => t.name === 'auto_tool') + const forceTool = tools.find((t: any) => t.name === 'force_tool') + const noneTool = tools.find((t: any) => t.name === 'none_tool') expect(autoTool).toBeDefined() expect(forceTool).toBeDefined() @@ -386,37 +351,6 @@ describe('AgentBlockHandler', () => { expect(autoTool.usageControl).toBe('auto') expect(forceTool.usageControl).toBe('force') - - expect(typeof autoTool.executeFunction).toBe('function') - expect(typeof forceTool.executeFunction).toBe('function') - - await autoTool.executeFunction({ input: 'test input' }) - expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', - expect.objectContaining({ - code: 'return { result: "auto tool executed", input }', - input: 'test input', - }), - false, // skipPostProcess - expect.any(Object) // execution context - ) - - await forceTool.executeFunction({ input: 'another test' }) - expect(mockExecuteTool).toHaveBeenNthCalledWith( - 2, // Check the 2nd call - 'function_execute', - expect.objectContaining({ - code: 'return { result: "force tool executed", input }', - input: 'another test', - }), - false, // skipPostProcess - expect.any(Object) // execution context - ) - - const providerCall = mockExecuteProviderRequest.mock.calls[0] - const requestBody = providerCall[1] - - expect(requestBody.tools.length).toBe(2) }) it('should filter out tools with usageControl set to "none"', async () => { @@ -1763,6 +1697,52 @@ describe('AgentBlockHandler', () => { expect(providerCallArgs[1].tools[0].name).toBe('search_files') }) + it('should pass callChain to executeProviderRequest for MCP cycle detection', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) + ) + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Search for files', + apiKey: 'test-api-key', + tools: [ + { + type: 'mcp', + title: 'search_files', + schema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + }, + required: ['query'], + }, + params: { + serverId: 'mcp-search-server', + toolName: 'search_files', + serverName: 'search', + }, + usageControl: 'auto' as const, + }, + ], + } + + const contextWithCallChain = { + ...mockContext, + workspaceId: 'test-workspace-123', + workflowId: 'test-workflow-456', + callChain: ['wf-parent', 'test-workflow-456'], + } + + mockGetProviderFromModel.mockReturnValue('openai') + + await handler.execute(contextWithCallChain, mockBlock, inputs) + + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCallArgs = mockExecuteProviderRequest.mock.calls[0][1] + expect(providerCallArgs.callChain).toEqual(['wf-parent', 'test-workflow-456']) + }) + it('should handle multiple MCP tools from the same server efficiently', async () => { const fetchCalls: any[] = [] @@ -2139,21 +2119,10 @@ describe('AgentBlockHandler', () => { expect(tools.length).toBe(0) }) - it('should use DB code for executeFunction when customToolId resolves', async () => { + it('should use DB schema when customToolId resolves', async () => { const toolId = 'custom-tool-123' mockFetchForCustomTool(toolId) - let capturedTools: any[] = [] - Promise.all = vi.fn().mockImplementation((promises: Promise[]) => { - const result = originalPromiseAll.call(Promise, promises) - result.then((tools: any[]) => { - if (tools?.length) { - capturedTools = tools.filter((t) => t !== null) - } - }) - return result - }) - const inputs = { model: 'gpt-4o', userPrompt: 'Format a report', @@ -2174,19 +2143,12 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(capturedTools.length).toBe(1) - expect(typeof capturedTools[0].executeFunction).toBe('function') - - await capturedTools[0].executeFunction({ title: 'Q1', format: 'pdf' }) + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const tools = providerCall[1].tools - expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', - expect.objectContaining({ - code: dbCode, - }), - false, - expect.any(Object) - ) + expect(tools.length).toBe(1) + expect(tools[0].name).toBe('formatReport') }) it('should not fetch from DB when no customToolId is present', async () => { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 506fe712f9..c76ab48c0d 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -1,9 +1,8 @@ import { db } from '@sim/db' -import { account, mcpServers } from '@sim/db/schema' +import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { createMcpToolId } from '@/lib/mcp/utils' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' import { @@ -30,10 +29,10 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu import { collectBlockData } from '@/executor/utils/block-data' import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { executeProviderRequest } from '@/providers' import { getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' -import { executeTool } from '@/tools' import { getTool, getToolAsync } from '@/tools/utils' const logger = createLogger('AgentBlockHandler') @@ -276,14 +275,12 @@ export class AgentBlockHandler implements BlockHandler { const userProvidedParams = tool.params || {} let schema = tool.schema - let code = tool.code let title = tool.title if (tool.customToolId) { const resolved = await this.fetchCustomToolById(ctx, tool.customToolId) if (resolved) { schema = resolved.schema - code = resolved.code title = resolved.title } else if (!schema) { logger.error(`Custom tool not found: ${tool.customToolId}`) @@ -296,7 +293,7 @@ export class AgentBlockHandler implements BlockHandler { return null } - const { filterSchemaForLLM, mergeToolParameters } = await import('@/tools/params') + const { filterSchemaForLLM } = await import('@/tools/params') const filteredSchema = filterSchemaForLLM(schema.function.parameters, userProvidedParams) @@ -313,43 +310,6 @@ export class AgentBlockHandler implements BlockHandler { usageControl: tool.usageControl || 'auto', } - if (code) { - base.executeFunction = async (callParams: Record) => { - const mergedParams = mergeToolParameters(userProvidedParams, callParams) - - const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) - - const result = await executeTool( - 'function_execute', - { - code, - ...mergedParams, - timeout: tool.timeout ?? AGENT.DEFAULT_FUNCTION_TIMEOUT, - envVars: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, - blockData, - blockNameMapping, - blockOutputSchemas, - isCustomTool: true, - _context: { - workflowId: ctx.workflowId, - workspaceId: ctx.workspaceId, - userId: ctx.userId, - isDeployedContext: ctx.isDeployedContext, - enforceCredentialAccess: ctx.enforceCredentialAccess, - }, - }, - false, - ctx - ) - - if (!result.success) { - throw new Error(result.error || 'Function execution failed') - } - return result.output - } - } - return base } @@ -359,7 +319,7 @@ export class AgentBlockHandler implements BlockHandler { private async fetchCustomToolById( ctx: ExecutionContext, customToolId: string - ): Promise<{ schema: any; code: string; title: string } | null> { + ): Promise<{ schema: any; title: string } | null> { if (typeof window !== 'undefined') { try { const { getCustomTool } = await import('@/hooks/queries/custom-tools') @@ -367,7 +327,6 @@ export class AgentBlockHandler implements BlockHandler { if (tool) { return { schema: tool.schema, - code: tool.code || '', title: tool.title, } } @@ -416,7 +375,6 @@ export class AgentBlockHandler implements BlockHandler { return { schema: tool.schema, - code: tool.code || '', title: tool.title, } } catch (error) { @@ -481,65 +439,15 @@ export class AgentBlockHandler implements BlockHandler { tool: ToolInput ): Promise { const { serverId, toolName, serverName, ...userProvidedParams } = tool.params || {} - - const { filterSchemaForLLM } = await import('@/tools/params') - const filteredSchema = filterSchemaForLLM( - tool.schema || { type: 'object', properties: {} }, - userProvidedParams - ) - - const toolId = createMcpToolId(serverId, toolName) - - return { - id: toolId, - name: toolName, + return this.buildMcpTool({ + serverId, + toolName, description: tool.schema?.description || `MCP tool ${toolName} from ${serverName || serverId}`, - parameters: filteredSchema, - params: userProvidedParams, - usageControl: tool.usageControl || 'auto', - executeFunction: async (callParams: Record) => { - const headers = await buildAuthHeaders() - const execParams: Record = {} - if (ctx.userId) execParams.userId = ctx.userId - const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams) - - const execResponse = await fetch(execUrl.toString(), { - method: 'POST', - headers, - body: stringifyJSON({ - serverId, - toolName, - arguments: callParams, - workspaceId: ctx.workspaceId, - workflowId: ctx.workflowId, - toolSchema: tool.schema, - }), - }) - - if (!execResponse.ok) { - throw new Error( - `MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}` - ) - } - - const result = await execResponse.json() - if (!result.success) { - throw new Error(result.error || 'MCP tool execution failed') - } - - return { - success: true, - output: result.data.output || {}, - metadata: { - source: 'mcp', - serverId, - serverName: serverName || serverId, - toolName, - }, - } - }, - } + schema: tool.schema || { type: 'object', properties: {} }, + userProvidedParams, + usageControl: tool.usageControl, + }) } /** @@ -668,63 +576,35 @@ export class AgentBlockHandler implements BlockHandler { serverId: string ): Promise { const { toolName, ...userProvidedParams } = tool.params || {} + return this.buildMcpTool({ + serverId, + toolName, + description: mcpTool.description || `MCP tool ${toolName} from ${mcpTool.serverName}`, + schema: mcpTool.inputSchema || { type: 'object', properties: {} }, + userProvidedParams, + usageControl: tool.usageControl, + }) + } + private async buildMcpTool(config: { + serverId: string + toolName: string + description: string + schema: any + userProvidedParams: Record + usageControl?: string + }): Promise { const { filterSchemaForLLM } = await import('@/tools/params') - const filteredSchema = filterSchemaForLLM( - mcpTool.inputSchema || { type: 'object', properties: {} }, - userProvidedParams - ) - - const toolId = createMcpToolId(serverId, toolName) + const filteredSchema = filterSchemaForLLM(config.schema, config.userProvidedParams) + const toolId = createMcpToolId(config.serverId, config.toolName) return { id: toolId, - name: toolName, - description: mcpTool.description || `MCP tool ${toolName} from ${mcpTool.serverName}`, + name: config.toolName, + description: config.description, parameters: filteredSchema, - params: userProvidedParams, - usageControl: tool.usageControl || 'auto', - executeFunction: async (callParams: Record) => { - const headers = await buildAuthHeaders() - const discoverExecParams: Record = {} - if (ctx.userId) discoverExecParams.userId = ctx.userId - const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams) - - const execResponse = await fetch(execUrl.toString(), { - method: 'POST', - headers, - body: stringifyJSON({ - serverId, - toolName, - arguments: callParams, - workspaceId: ctx.workspaceId, - workflowId: ctx.workflowId, - toolSchema: mcpTool.inputSchema, - }), - }) - - if (!execResponse.ok) { - throw new Error( - `MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}` - ) - } - - const result = await execResponse.json() - if (!result.success) { - throw new Error(result.error || 'MCP tool execution failed') - } - - return { - success: true, - output: result.data.output || {}, - metadata: { - source: 'mcp', - serverId, - serverName: mcpTool.serverName, - toolName, - }, - } - }, + params: config.userProvidedParams, + usageControl: config.usageControl || 'auto', } } @@ -1048,9 +928,9 @@ export class AgentBlockHandler implements BlockHandler { let finalApiKey: string | undefined = providerRequest.apiKey if (providerId === 'vertex' && providerRequest.vertexCredential) { - finalApiKey = await this.resolveVertexCredential( + finalApiKey = await resolveVertexCredential( providerRequest.vertexCredential, - ctx.workflowId + 'vertex-agent' ) } @@ -1082,6 +962,7 @@ export class AgentBlockHandler implements BlockHandler { blockData, blockNameMapping, isDeployedContext: ctx.isDeployedContext, + callChain: ctx.callChain, reasoningEffort: providerRequest.reasoningEffort, verbosity: providerRequest.verbosity, thinkingLevel: providerRequest.thinkingLevel, @@ -1096,37 +977,6 @@ export class AgentBlockHandler implements BlockHandler { } } - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string, workflowId: string): Promise { - const requestId = `vertex-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken - } - private handleExecutionError( error: any, startTime: number, @@ -1310,7 +1160,7 @@ export class AgentBlockHandler implements BlockHandler { }, toolCalls: { list: result.toolCalls?.map(this.formatToolCall.bind(this)) || [], - count: result.toolCalls?.length || DEFAULTS.EXECUTION_TIME, + count: result.toolCalls?.length ?? 0, }, providerTiming: result.timing, cost: result.cost, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index bd8fe04a24..710db01ee1 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -1,14 +1,11 @@ -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import type { BlockOutput } from '@/blocks/types' import { validateModelProvider } from '@/ee/access-control/utils/permission-check' import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -44,7 +41,10 @@ export class EvaluatorBlockHandler implements BlockHandler { let finalApiKey: string | undefined = evaluatorConfig.apiKey if (providerId === 'vertex' && evaluatorConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(evaluatorConfig.vertexCredential) + finalApiKey = await resolveVertexCredential( + evaluatorConfig.vertexCredential, + 'vertex-evaluator' + ) } const processedContent = this.processContent(inputs.content) @@ -234,7 +234,7 @@ export class EvaluatorBlockHandler implements BlockHandler { if (Object.keys(parsedContent).length === 0) { validMetrics.forEach((metric: any) => { if (metric?.name) { - metricScores[metric.name.toLowerCase()] = DEFAULTS.EXECUTION_TIME + metricScores[metric.name.toLowerCase()] = 0 } }) return metricScores @@ -273,37 +273,6 @@ export class EvaluatorBlockHandler implements BlockHandler { } logger.warn(`Metric "${metricName}" not found in LLM response`) - return DEFAULTS.EXECUTION_TIME - } - - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string): Promise { - const requestId = `vertex-evaluator-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken + return 0 } } diff --git a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts index 63b331754a..2208e9911d 100644 --- a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts +++ b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts @@ -9,7 +9,6 @@ import { HTTP, normalizeName, PAUSE_RESUME, - REFERENCE, } from '@/executor/constants' import { generatePauseContextId, @@ -17,6 +16,7 @@ import { } from '@/executor/human-in-the-loop/utils' import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' +import { convertBuilderDataToJson, convertPropertyValue } from '@/executor/utils/builder-data' import { parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -265,7 +265,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { } if (dataMode === 'structured' && inputs.builderData) { - const convertedData = this.convertBuilderDataToJson(inputs.builderData) + const convertedData = convertBuilderDataToJson(inputs.builderData) return parseObjectStrings(convertedData) } @@ -296,7 +296,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { } } - const value = this.convertPropertyValue(prop) + const value = convertPropertyValue(prop) entries.push({ name: path, @@ -352,140 +352,6 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { .filter((field): field is NormalizedInputField => field !== null) } - private convertBuilderDataToJson(builderData: JSONProperty[]): any { - if (!Array.isArray(builderData)) { - return {} - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - const value = this.convertPropertyValue(prop) - result[prop.name] = value - } - - return result - } - - static convertBuilderDataToJsonString(builderData: JSONProperty[]): string { - if (!Array.isArray(builderData) || builderData.length === 0) { - return '{\n \n}' - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - result[prop.name] = prop.value - } - - let jsonString = JSON.stringify(result, null, 2) - - jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') - - return jsonString - } - - private convertPropertyValue(prop: JSONProperty): any { - switch (prop.type) { - case 'object': - return this.convertObjectValue(prop.value) - case 'array': - return this.convertArrayValue(prop.value) - case 'number': - return this.convertNumberValue(prop.value) - case 'boolean': - return this.convertBooleanValue(prop.value) - case 'files': - return prop.value - default: - return prop.value - } - } - - private convertObjectValue(value: any): any { - if (Array.isArray(value)) { - return this.convertBuilderDataToJson(value) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - return this.tryParseJson(value, value) - } - - return value - } - - private convertArrayValue(value: any): any { - if (Array.isArray(value)) { - return value.map((item: any) => this.convertArrayItem(item)) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - const parsed = this.tryParseJson(value, value) - return Array.isArray(parsed) ? parsed : value - } - - return value - } - - private convertArrayItem(item: any): any { - if (typeof item !== 'object' || !item.type) { - return item - } - - if (item.type === 'object' && Array.isArray(item.value)) { - return this.convertBuilderDataToJson(item.value) - } - - if (item.type === 'array' && Array.isArray(item.value)) { - return item.value.map((subItem: any) => - typeof subItem === 'object' && subItem.type ? subItem.value : subItem - ) - } - - return item.value - } - - private convertNumberValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - const numValue = Number(value) - return Number.isNaN(numValue) ? value : numValue - } - - private convertBooleanValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - return value === 'true' || value === true - } - - private tryParseJson(jsonString: string, fallback: any): any { - try { - return JSON.parse(jsonString) - } catch { - return fallback - } - } - - private isVariableReference(value: any): boolean { - return ( - typeof value === 'string' && - value.trim().startsWith(REFERENCE.START) && - value.trim().includes(REFERENCE.END) - ) - } - private parseStatus(status?: string): number { if (!status) return HTTP.STATUS.OK const parsed = Number(status) diff --git a/apps/sim/executor/handlers/response/response-handler.ts b/apps/sim/executor/handlers/response/response-handler.ts index 8a3abb22fb..389ddda76b 100644 --- a/apps/sim/executor/handlers/response/response-handler.ts +++ b/apps/sim/executor/handlers/response/response-handler.ts @@ -1,19 +1,15 @@ import { createLogger } from '@sim/logger' -import { BlockType, HTTP, REFERENCE } from '@/executor/constants' +import { BlockType, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext, NormalizedBlockOutput } from '@/executor/types' +import { + convertBuilderDataToJson, + convertBuilderDataToJsonString, +} from '@/executor/utils/builder-data' import { parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' const logger = createLogger('ResponseBlockHandler') -interface JSONProperty { - id: string - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' - value: any - collapsed?: boolean -} - export class ResponseBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { return block.metadata?.id === BlockType.RESPONSE @@ -73,154 +69,15 @@ export class ResponseBlockHandler implements BlockHandler { } if (dataMode === 'structured' && inputs.builderData) { - const convertedData = this.convertBuilderDataToJson(inputs.builderData) + const convertedData = convertBuilderDataToJson(inputs.builderData) return parseObjectStrings(convertedData) } return inputs.data || {} } - private convertBuilderDataToJson(builderData: JSONProperty[]): any { - if (!Array.isArray(builderData)) { - return {} - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - const value = this.convertPropertyValue(prop) - result[prop.name] = value - } - - return result - } - - static convertBuilderDataToJsonString(builderData: JSONProperty[]): string { - if (!Array.isArray(builderData) || builderData.length === 0) { - return '{\n \n}' - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - result[prop.name] = prop.value - } - - let jsonString = JSON.stringify(result, null, 2) - - jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') - - return jsonString - } - - private convertPropertyValue(prop: JSONProperty): any { - switch (prop.type) { - case 'object': - return this.convertObjectValue(prop.value) - case 'array': - return this.convertArrayValue(prop.value) - case 'number': - return this.convertNumberValue(prop.value) - case 'boolean': - return this.convertBooleanValue(prop.value) - case 'files': - return prop.value - default: - return prop.value - } - } - - private convertObjectValue(value: any): any { - if (Array.isArray(value)) { - return this.convertBuilderDataToJson(value) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - return this.tryParseJson(value, value) - } - - return value - } - - private convertArrayValue(value: any): any { - if (Array.isArray(value)) { - return value.map((item: any) => this.convertArrayItem(item)) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - const parsed = this.tryParseJson(value, value) - if (Array.isArray(parsed)) { - return parsed - } - return value - } - - return value - } - - private convertArrayItem(item: any): any { - if (typeof item !== 'object' || !item.type) { - return item - } - - if (item.type === 'object' && Array.isArray(item.value)) { - return this.convertBuilderDataToJson(item.value) - } - - if (item.type === 'array' && Array.isArray(item.value)) { - return item.value.map((subItem: any) => { - if (typeof subItem === 'object' && subItem.type) { - return subItem.value - } - return subItem - }) - } - - return item.value - } - - private convertNumberValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - const numValue = Number(value) - if (Number.isNaN(numValue)) { - return value - } - return numValue - } - - private convertBooleanValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - return value === 'true' || value === true - } - - private tryParseJson(jsonString: string, fallback: any): any { - try { - return JSON.parse(jsonString) - } catch { - return fallback - } - } - - private isVariableReference(value: any): boolean { - return ( - typeof value === 'string' && - value.trim().startsWith(REFERENCE.START) && - value.trim().includes(REFERENCE.END) - ) + static convertBuilderDataToJsonString(builderData: any[]): string { + return convertBuilderDataToJsonString(builderData) } private parseStatus(status?: string): number { diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 723ba94393..c107f67937 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -1,9 +1,5 @@ -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' import { validateModelProvider } from '@/ee/access-control/utils/permission-check' @@ -16,6 +12,7 @@ import { } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAuthHeaders } from '@/executor/utils/http' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -87,7 +84,7 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(routerConfig.vertexCredential) + finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') } const providerRequest: Record = { @@ -217,7 +214,7 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(routerConfig.vertexCredential) + finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') } const providerRequest: Record = { @@ -416,35 +413,4 @@ export class RouterBlockHandler implements BlockHandler { } }) } - - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string): Promise { - const requestId = `vertex-router-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken - } } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 661796db90..bddc16e821 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -108,18 +108,16 @@ describe('WorkflowBlockHandler', () => { ) }) - it('should enforce maximum depth limit', async () => { + it('should enforce maximum call chain depth limit', async () => { const inputs = { workflowId: 'child-workflow-id' } - // Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10) const deepContext = { ...mockContext, - workflowId: - 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11', + callChain: Array.from({ length: 25 }, (_, i) => `wf-${i}`), } await expect(handler.execute(deepContext, mockBlock, inputs)).rejects.toThrow( - '"child-workflow-id" failed: Maximum workflow nesting depth of 10 exceeded' + 'Maximum workflow call chain depth (25) exceeded' ) }) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 1666b51f2e..b0123d7844 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -62,6 +62,7 @@ export class WorkflowBlockHandler implements BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ): Promise { return this._executeCore(ctx, block, inputs, nodeMetadata) @@ -79,6 +80,7 @@ export class WorkflowBlockHandler implements BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ): Promise { logger.info(`Executing workflow block: ${block.id}`) @@ -98,13 +100,17 @@ export class WorkflowBlockHandler implements BlockHandler { // workflow block execution, preventing cross-iteration child mixing in loop contexts. const instanceId = crypto.randomUUID() + const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId) + const depthError = validateCallChain(childCallChain) + if (depthError) { + throw new ChildWorkflowError({ + message: depthError, + childWorkflowName, + }) + } + let childWorkflowSnapshotId: string | undefined try { - const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1 - if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) { - throw new Error(`Maximum workflow nesting depth of ${DEFAULTS.MAX_WORKFLOW_DEPTH} exceeded`) - } - if (ctx.isDeployedContext) { const hasActiveDeployment = await this.checkChildDeployment(workflowId) if (!hasActiveDeployment) { @@ -126,7 +132,7 @@ export class WorkflowBlockHandler implements BlockHandler { childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow' logger.info( - `Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}` + `Executing child workflow: ${childWorkflowName} (${workflowId}), call chain depth ${ctx.callChain?.length || 0}` ) let childWorkflowInput: Record = {} @@ -165,16 +171,12 @@ export class WorkflowBlockHandler implements BlockHandler { const iterationContext = nodeMetadata ? this.getIterationContext(ctx, nodeMetadata) : undefined - ctx.onChildWorkflowInstanceReady?.(effectiveBlockId, instanceId, iterationContext) - } - - const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId) - const depthError = validateCallChain(childCallChain) - if (depthError) { - throw new ChildWorkflowError({ - message: depthError, - childWorkflowName, - }) + ctx.onChildWorkflowInstanceReady?.( + effectiveBlockId, + instanceId, + iterationContext, + nodeMetadata?.executionOrder + ) } const subExecutor = new Executor({ @@ -584,45 +586,6 @@ export class WorkflowBlockHandler implements BlockHandler { return processed } - private flattenChildWorkflowSpans(spans: TraceSpan[]): WorkflowTraceSpan[] { - const flattened: WorkflowTraceSpan[] = [] - - spans.forEach((span) => { - if (this.isSyntheticWorkflowWrapper(span)) { - if (span.children && Array.isArray(span.children)) { - flattened.push(...this.flattenChildWorkflowSpans(span.children)) - } - return - } - - const workflowSpan: WorkflowTraceSpan = { - ...span, - } - - if (Array.isArray(workflowSpan.children)) { - const childSpans = workflowSpan.children as TraceSpan[] - workflowSpan.children = this.flattenChildWorkflowSpans(childSpans) - } - - if (workflowSpan.output && typeof workflowSpan.output === 'object') { - const { childTraceSpans: nestedChildSpans, ...outputRest } = workflowSpan.output as { - childTraceSpans?: TraceSpan[] - } & Record - - if (Array.isArray(nestedChildSpans) && nestedChildSpans.length > 0) { - const flattenedNestedChildren = this.flattenChildWorkflowSpans(nestedChildSpans) - workflowSpan.children = [...(workflowSpan.children || []), ...flattenedNestedChildren] - } - - workflowSpan.output = outputRest - } - - flattened.push(workflowSpan) - }) - - return flattened - } - private toExecutionResult(result: ExecutionResult | StreamingExecution): ExecutionResult { return 'execution' in result ? result.execution : result } diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index b30dba2e1b..bf672dfd94 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -264,7 +264,8 @@ export interface ExecutionContext { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void /** @@ -377,6 +378,7 @@ export interface BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ) => Promise } diff --git a/apps/sim/executor/utils/builder-data.ts b/apps/sim/executor/utils/builder-data.ts new file mode 100644 index 0000000000..da471e5f4a --- /dev/null +++ b/apps/sim/executor/utils/builder-data.ts @@ -0,0 +1,149 @@ +import { REFERENCE } from '@/executor/constants' + +export interface JSONProperty { + id: string + name: string + type: string + value: any + collapsed?: boolean +} + +/** + * Converts builder data (structured JSON properties) into a plain JSON object. + */ +export function convertBuilderDataToJson(builderData: JSONProperty[]): any { + if (!Array.isArray(builderData)) { + return {} + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + const value = convertPropertyValue(prop) + result[prop.name] = value + } + + return result +} + +/** + * Converts builder data into a JSON string with variable references unquoted. + */ +export function convertBuilderDataToJsonString(builderData: JSONProperty[]): string { + if (!Array.isArray(builderData) || builderData.length === 0) { + return '{\n \n}' + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + result[prop.name] = prop.value + } + + let jsonString = JSON.stringify(result, null, 2) + + jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') + + return jsonString +} + +export function convertPropertyValue(prop: JSONProperty): any { + switch (prop.type) { + case 'object': + return convertObjectValue(prop.value) + case 'array': + return convertArrayValue(prop.value) + case 'number': + return convertNumberValue(prop.value) + case 'boolean': + return convertBooleanValue(prop.value) + case 'files': + return prop.value + default: + return prop.value + } +} + +function convertObjectValue(value: any): any { + if (Array.isArray(value)) { + return convertBuilderDataToJson(value) + } + + if (typeof value === 'string' && !isVariableReference(value)) { + return tryParseJson(value, value) + } + + return value +} + +function convertArrayValue(value: any): any { + if (Array.isArray(value)) { + return value.map((item: any) => convertArrayItem(item)) + } + + if (typeof value === 'string' && !isVariableReference(value)) { + const parsed = tryParseJson(value, value) + return Array.isArray(parsed) ? parsed : value + } + + return value +} + +function convertArrayItem(item: any): any { + if (typeof item !== 'object' || !item.type) { + return item + } + + if (item.type === 'object' && Array.isArray(item.value)) { + return convertBuilderDataToJson(item.value) + } + + if (item.type === 'array' && Array.isArray(item.value)) { + return item.value.map((subItem: any) => + typeof subItem === 'object' && subItem.type ? subItem.value : subItem + ) + } + + return item.value +} + +function convertNumberValue(value: any): any { + if (isVariableReference(value)) { + return value + } + + const numValue = Number(value) + return Number.isNaN(numValue) ? value : numValue +} + +function convertBooleanValue(value: any): any { + if (isVariableReference(value)) { + return value + } + + return value === 'true' || value === true +} + +function tryParseJson(jsonString: string, fallback: any): any { + try { + return JSON.parse(jsonString) + } catch { + return fallback + } +} + +function isVariableReference(value: any): boolean { + return ( + typeof value === 'string' && + value.trim().startsWith(REFERENCE.START) && + value.trim().includes(REFERENCE.END) + ) +} diff --git a/apps/sim/executor/utils/start-block.test.ts b/apps/sim/executor/utils/start-block.test.ts index 4c6fd708e0..9a3942cbee 100644 --- a/apps/sim/executor/utils/start-block.test.ts +++ b/apps/sim/executor/utils/start-block.test.ts @@ -215,5 +215,115 @@ describe('start-block utilities', () => { expect(output.customField).toBe('defaultValue') }) + + it.concurrent('preserves coerced types for unified start payload', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'conversation_id', type: 'number' }, + { name: 'sender', type: 'object' }, + { name: 'is_active', type: 'boolean' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + conversation_id: '149', + sender: '{"id":10,"email":"user@example.com"}', + is_active: 'true', + }, + }) + + expect(output.conversation_id).toBe(149) + expect(output.sender).toEqual({ id: 10, email: 'user@example.com' }) + expect(output.is_active).toBe(true) + }) + + it.concurrent( + 'prefers coerced inputFormat values over duplicated top-level workflowInput keys', + () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'conversation_id', type: 'number' }, + { name: 'sender', type: 'object' }, + { name: 'is_active', type: 'boolean' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + input: { + conversation_id: '149', + sender: '{"id":10,"email":"user@example.com"}', + is_active: 'false', + }, + conversation_id: '150', + sender: '{"id":99,"email":"wrong@example.com"}', + is_active: 'true', + extra: 'keep-me', + }, + }) + + expect(output.conversation_id).toBe(149) + expect(output.sender).toEqual({ id: 10, email: 'user@example.com' }) + expect(output.is_active).toBe(false) + expect(output.extra).toBe('keep-me') + } + ) + }) + + describe('EXTERNAL_TRIGGER path', () => { + it.concurrent('preserves coerced types for integration trigger payload', () => { + const block = createBlock('webhook', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'count', type: 'number' }, + { name: 'payload', type: 'object' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.EXTERNAL_TRIGGER, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + count: '5', + payload: '{"event":"push"}', + extra: 'untouched', + }, + }) + + expect(output.count).toBe(5) + expect(output.payload).toEqual({ event: 'push' }) + expect(output.extra).toBe('untouched') + }) }) }) diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index 3b7982f934..5ac0936e1b 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -262,6 +262,7 @@ function buildUnifiedStartOutput( hasStructured: boolean ): NormalizedBlockOutput { const output: NormalizedBlockOutput = {} + const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null if (hasStructured) { for (const [key, value] of Object.entries(structuredInput)) { @@ -272,6 +273,9 @@ function buildUnifiedStartOutput( if (isPlainObject(workflowInput)) { for (const [key, value] of Object.entries(workflowInput)) { if (key === 'onUploadError') continue + // Skip keys already set by schema-coerced structuredInput to + // prevent raw workflowInput strings from overwriting typed values. + if (structuredKeys?.has(key)) continue // Runtime values override defaults (except undefined/null which mean "not provided") if (value !== undefined && value !== null) { output[key] = value @@ -384,6 +388,7 @@ function buildIntegrationTriggerOutput( hasStructured: boolean ): NormalizedBlockOutput { const output: NormalizedBlockOutput = {} + const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null if (hasStructured) { for (const [key, value] of Object.entries(structuredInput)) { @@ -393,6 +398,7 @@ function buildIntegrationTriggerOutput( if (isPlainObject(workflowInput)) { for (const [key, value] of Object.entries(workflowInput)) { + if (structuredKeys?.has(key)) continue if (value !== undefined && value !== null) { output[key] = value } else if (!Object.hasOwn(output, key)) { diff --git a/apps/sim/executor/utils/vertex-credential.ts b/apps/sim/executor/utils/vertex-credential.ts new file mode 100644 index 0000000000..237262f406 --- /dev/null +++ b/apps/sim/executor/utils/vertex-credential.ts @@ -0,0 +1,42 @@ +import { db } from '@sim/db' +import { account } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('VertexCredential') + +/** + * Resolves a Vertex AI OAuth credential to an access token. + * Shared across agent, evaluator, and router handlers. + */ +export async function resolveVertexCredential( + credentialId: string, + callerLabel = 'vertex' +): Promise { + const requestId = `${callerLabel}-${Date.now()}` + + logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) + + const resolved = await resolveOAuthAccountId(credentialId) + if (!resolved) { + throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) + } + + const credential = await db.query.account.findFirst({ + where: eq(account.id, resolved.accountId), + }) + + if (!credential) { + throw new Error(`Vertex AI credential not found: ${credentialId}`) + } + + const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) + + if (!accessToken) { + throw new Error('Failed to get Vertex AI access token') + } + + logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) + return accessToken +} diff --git a/apps/sim/lib/audit/log.ts b/apps/sim/lib/audit/log.ts index a4300b82bd..af3cf23bcd 100644 --- a/apps/sim/lib/audit/log.ts +++ b/apps/sim/lib/audit/log.ts @@ -131,6 +131,8 @@ export const AuditAction = { WORKFLOW_DUPLICATED: 'workflow.duplicated', WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated', WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted', + WORKFLOW_LOCKED: 'workflow.locked', + WORKFLOW_UNLOCKED: 'workflow.unlocked', WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated', // Workspaces diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 91eb88b1d3..4812c275ad 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -484,8 +484,10 @@ export const auth = betterAuth({ 'google-docs', 'google-sheets', 'google-forms', + 'google-bigquery', 'google-vault', 'google-groups', + 'google-tasks', 'vertex-ai', 'github-repo', 'microsoft-dataverse', @@ -1068,6 +1070,46 @@ export const auth = betterAuth({ } }, }, + { + providerId: 'google-bigquery', + clientId: env.GOOGLE_CLIENT_ID as string, + clientSecret: env.GOOGLE_CLIENT_SECRET as string, + discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration', + accessType: 'offline', + scopes: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/bigquery', + ], + prompt: 'consent', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-bigquery`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, + { providerId: 'google-vault', clientId: env.GOOGLE_CLIENT_ID as string, @@ -1150,6 +1192,46 @@ export const auth = betterAuth({ }, }, + { + providerId: 'google-tasks', + clientId: env.GOOGLE_CLIENT_ID as string, + clientSecret: env.GOOGLE_CLIENT_SECRET as string, + discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration', + accessType: 'offline', + scopes: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/tasks', + ], + prompt: 'consent', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-tasks`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, + { providerId: 'vertex-ai', clientId: env.GOOGLE_CLIENT_ID as string, @@ -1846,6 +1928,15 @@ export const auth = betterAuth({ 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'delete:blogpost:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], responseType: 'code', pkce: true, diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 06bc41b069..cd005277ba 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1039,3 +1039,74 @@ export function validateGoogleCalendarId( return { isValid: true, sanitized: value } } + +/** + * Validates a pagination cursor token + * + * Pagination cursors are opaque tokens returned by APIs (e.g., Confluence, Jira) + * and passed back to get the next page. They are typically base64-encoded or + * URL-safe strings. This validator ensures the cursor cannot contain characters + * that could alter URL structure. + * + * @param value - The cursor token to validate + * @param paramName - Name of the parameter for error messages + * @param maxLength - Maximum length (default: 1024) + * @returns ValidationResult + * + * @example + * ```typescript + * if (cursor) { + * const result = validatePaginationCursor(cursor, 'cursor') + * if (!result.isValid) { + * return NextResponse.json({ error: result.error }, { status: 400 }) + * } + * } + * ``` + */ +export function validatePaginationCursor( + value: string | null | undefined, + paramName = 'cursor', + maxLength = 1024 +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + if (value.length > maxLength) { + logger.warn('Pagination cursor exceeds maximum length', { + paramName, + length: value.length, + maxLength, + }) + return { + isValid: false, + error: `${paramName} exceeds maximum length of ${maxLength} characters`, + } + } + + if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) { + logger.warn('Pagination cursor contains control characters', { paramName }) + return { + isValid: false, + error: `${paramName} contains invalid characters`, + } + } + + // Allow alphanumeric, base64 chars (+, /, =), and URL-safe chars (-, _, ., ~, %) + const cursorPattern = /^[A-Za-z0-9+/=\-_.~%]+$/ + if (!cursorPattern.test(value)) { + logger.warn('Pagination cursor contains disallowed characters', { + paramName, + value: value.substring(0, 100), + }) + return { + isValid: false, + error: `${paramName} contains invalid characters`, + } + } + + return { isValid: true, sanitized: value } +} diff --git a/apps/sim/lib/execution/__tests__/call-chain.test.ts b/apps/sim/lib/execution/__tests__/call-chain.test.ts index b793e1d531..fc937de142 100644 --- a/apps/sim/lib/execution/__tests__/call-chain.test.ts +++ b/apps/sim/lib/execution/__tests__/call-chain.test.ts @@ -19,8 +19,8 @@ describe('call-chain', () => { }) describe('MAX_CALL_CHAIN_DEPTH', () => { - it('equals 10', () => { - expect(MAX_CALL_CHAIN_DEPTH).toBe(10) + it('equals 25', () => { + expect(MAX_CALL_CHAIN_DEPTH).toBe(25) }) }) diff --git a/apps/sim/lib/execution/call-chain.ts b/apps/sim/lib/execution/call-chain.ts index 406274fa48..4e718c2352 100644 --- a/apps/sim/lib/execution/call-chain.ts +++ b/apps/sim/lib/execution/call-chain.ts @@ -7,7 +7,7 @@ */ export const SIM_VIA_HEADER = 'X-Sim-Via' -export const MAX_CALL_CHAIN_DEPTH = 10 +export const MAX_CALL_CHAIN_DEPTH = 25 /** * Parses the `X-Sim-Via` header value into an ordered list of workflow IDs. diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index 00ca59267b..69e7cc8117 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -170,7 +170,8 @@ class McpService { userId: string, serverId: string, toolCall: McpToolCall, - workspaceId: string + workspaceId: string, + extraHeaders?: Record ): Promise { const requestId = generateRequestId() const maxRetries = 2 @@ -187,6 +188,9 @@ class McpService { } const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId) + if (extraHeaders && Object.keys(extraHeaders).length > 0) { + resolvedConfig.headers = { ...resolvedConfig.headers, ...extraHeaders } + } const client = await this.createClient(resolvedConfig) try { diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index b890566334..2b9a96aca8 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -8,6 +8,7 @@ import { DropboxIcon, GithubIcon, GmailIcon, + GoogleBigQueryIcon, GoogleCalendarIcon, GoogleDocsIcon, GoogleDriveIcon, @@ -15,6 +16,7 @@ import { GoogleGroupsIcon, GoogleIcon, GoogleSheetsIcon, + GoogleTasksIcon, HubspotIcon, JiraIcon, LinearIcon, @@ -119,6 +121,22 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: GoogleIcon, scopes: ['https://www.googleapis.com/auth/calendar'], }, + 'google-bigquery': { + name: 'Google BigQuery', + description: 'Query, list, and insert data in Google BigQuery.', + providerId: 'google-bigquery', + icon: GoogleBigQueryIcon, + baseProviderIcon: GoogleIcon, + scopes: ['https://www.googleapis.com/auth/bigquery'], + }, + 'google-tasks': { + name: 'Google Tasks', + description: 'Create, manage, and organize tasks with Google Tasks.', + providerId: 'google-tasks', + icon: GoogleTasksIcon, + baseProviderIcon: GoogleIcon, + scopes: ['https://www.googleapis.com/auth/tasks'], + }, 'google-vault': { name: 'Google Vault', description: 'Search, export, and manage matters/holds via Google Vault.', @@ -330,6 +348,21 @@ export const OAUTH_PROVIDERS: Record = { 'search:confluence', 'read:me', 'offline_access', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'delete:blogpost:confluence', + 'read:content.property:confluence', + 'write:content.property:confluence', + 'read:hierarchical-content:confluence', + 'read:content.metadata:confluence', + 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], }, }, diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d5114a38bc..0da86f06fd 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -7,6 +7,8 @@ export type OAuthProvider = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-bigquery' + | 'google-tasks' | 'google-vault' | 'google-forms' | 'google-groups' @@ -52,6 +54,8 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-bigquery' + | 'google-tasks' | 'google-vault' | 'google-forms' | 'google-groups' diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index 09044acdc5..a9324d1087 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -155,6 +155,7 @@ export interface BlockChildWorkflowStartedEvent extends BaseExecutionEvent { childWorkflowInstanceId: string iterationCurrent?: number iterationContainerId?: string + executionOrder?: number } } @@ -396,7 +397,8 @@ export function createSSECallbacks(options: SSECallbackOptions) { const onChildWorkflowInstanceReady = ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => { sendEvent({ type: 'block:childWorkflowStarted', @@ -410,6 +412,7 @@ export function createSSECallbacks(options: SSECallbackOptions) { iterationCurrent: iterationContext.iterationCurrent, iterationContainerId: iterationContext.iterationContainerId, }), + ...(executionOrder !== undefined && { executionOrder }), }, }) } diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 5df279077e..af5362c3c7 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -174,6 +174,7 @@ export interface ProviderRequest { verbosity?: string thinkingLevel?: string isDeployedContext?: boolean + callChain?: string[] /** Previous interaction ID for multi-turn Interactions API requests (deep research follow-ups) */ previousInteractionId?: string abortSignal?: AbortSignal diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 03f664c074..1ee721e7b3 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -1110,6 +1110,7 @@ export function prepareToolExecution( blockData?: Record blockNameMapping?: Record isDeployedContext?: boolean + callChain?: string[] } ): { toolParams: Record @@ -1137,6 +1138,7 @@ export function prepareToolExecution( ...(request.isDeployedContext !== undefined ? { isDeployedContext: request.isDeployedContext } : {}), + ...(request.callChain ? { callChain: request.callChain } : {}), }, } : {}), diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index d677466cba..7fef6aab61 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, inArray, or, sql } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { env } from '@/lib/core/config/env' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -207,6 +208,17 @@ export async function persistWorkflowOperation(workflowId: string, operation: an } }) + // Audit workflow-level lock/unlock operations + if ( + target === OPERATION_TARGETS.BLOCKS && + op === BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED && + userId + ) { + auditWorkflowLockToggle(workflowId, userId).catch((error) => { + logger.error('Failed to audit workflow lock toggle', { error, workflowId }) + }) + } + const duration = Date.now() - startTime if (duration > 100) { logger.warn('Slow socket DB operation:', { @@ -226,6 +238,43 @@ export async function persistWorkflowOperation(workflowId: string, operation: an } } +/** + * Records an audit log entry when all blocks in a workflow are locked or unlocked. + * Only audits workflow-level transitions (all locked or all unlocked), not partial toggles. + */ +async function auditWorkflowLockToggle(workflowId: string, actorId: string): Promise { + const [wf] = await db + .select({ name: workflow.name, workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + + if (!wf) return + + const blocks = await db + .select({ locked: workflowBlocks.locked }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + if (blocks.length === 0) return + + const allLocked = blocks.every((b) => b.locked) + const allUnlocked = blocks.every((b) => !b.locked) + + // Only audit workflow-level transitions, not partial toggles + if (!allLocked && !allUnlocked) return + + recordAudit({ + workspaceId: wf.workspaceId, + actorId, + action: allLocked ? AuditAction.WORKFLOW_LOCKED : AuditAction.WORKFLOW_UNLOCKED, + resourceType: AuditResourceType.WORKFLOW, + resourceId: workflowId, + resourceName: wf.name, + description: allLocked ? `Locked workflow "${wf.name}"` : `Unlocked workflow "${wf.name}"`, + metadata: { blockCount: blocks.length }, + }) +} + async function handleBlockOperationTx( tx: any, workflowId: string, diff --git a/apps/sim/stores/notifications/types.ts b/apps/sim/stores/notifications/types.ts index 678f540ce8..627b09bb66 100644 --- a/apps/sim/stores/notifications/types.ts +++ b/apps/sim/stores/notifications/types.ts @@ -6,7 +6,7 @@ export interface NotificationAction { /** * Action type identifier for handler reconstruction */ - type: 'copilot' | 'refresh' + type: 'copilot' | 'refresh' | 'unlock-workflow' /** * Message or data to pass to the action handler. diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index e3b7f3ea10..3468e4d212 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -91,6 +91,13 @@ const matchesEntryForUpdate = ( return false } + if ( + update.childWorkflowBlockId !== undefined && + entry.childWorkflowBlockId !== update.childWorkflowBlockId + ) { + return false + } + return true } diff --git a/apps/sim/tools/confluence/create_space.ts b/apps/sim/tools/confluence/create_space.ts new file mode 100644 index 0000000000..f2d8b8a734 --- /dev/null +++ b/apps/sim/tools/confluence/create_space.ts @@ -0,0 +1,134 @@ +import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateSpaceParams { + accessToken: string + domain: string + name: string + key: string + description?: string + cloudId?: string +} + +export interface ConfluenceCreateSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + name: string + key: string + type: string + status: string + url: string + homepageId: string | null + description: { value: string; representation: string } | null + } +} + +export const confluenceCreateSpaceTool: ToolConfig< + ConfluenceCreateSpaceParams, + ConfluenceCreateSpaceResponse +> = { + id: 'confluence_create_space', + name: 'Confluence Create Space', + description: 'Create a new Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new space', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique key for the space (uppercase, no spaces)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description for the new space', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'POST', + headers: (params: ConfluenceCreateSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreateSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + name: params.name, + key: params.key, + description: params.description, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.id ?? '', + name: data.name ?? '', + key: data.key ?? '', + type: data.type ?? '', + status: data.status ?? '', + url: data._links?.webui ?? '', + homepageId: data.homepageId ?? null, + description: data.description ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Created space ID' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space type' }, + status: { type: 'string', description: 'Space status' }, + url: { type: 'string', description: 'URL to view the space' }, + homepageId: { type: 'string', description: 'Homepage ID', optional: true }, + description: { + type: 'object', + description: 'Space description', + properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES, + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/create_space_property.ts b/apps/sim/tools/confluence/create_space_property.ts new file mode 100644 index 0000000000..c702f63539 --- /dev/null +++ b/apps/sim/tools/confluence/create_space_property.ts @@ -0,0 +1,118 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateSpacePropertyParams { + accessToken: string + domain: string + spaceId: string + key: string + value?: unknown + cloudId?: string +} + +export interface ConfluenceCreateSpacePropertyResponse { + success: boolean + output: { + ts: string + propertyId: string + key: string + value: unknown + spaceId: string + } +} + +export const confluenceCreateSpacePropertyTool: ToolConfig< + ConfluenceCreateSpacePropertyParams, + ConfluenceCreateSpacePropertyResponse +> = { + id: 'confluence_create_space_property', + name: 'Confluence Create Space Property', + description: 'Create a property on a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to create the property on', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Property key/name', + }, + value: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Property value (JSON)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceCreateSpacePropertyParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreateSpacePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + action: 'create', + key: params.key, + value: params.value, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + propertyId: data.propertyId ?? '', + key: data.key ?? '', + value: data.value ?? null, + spaceId: data.spaceId ?? '', + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + propertyId: { type: 'string', description: 'Created property ID' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value' }, + spaceId: { type: 'string', description: 'Space ID' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_blogpost.ts b/apps/sim/tools/confluence/delete_blogpost.ts new file mode 100644 index 0000000000..c53562cf28 --- /dev/null +++ b/apps/sim/tools/confluence/delete_blogpost.ts @@ -0,0 +1,95 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteBlogPostParams { + accessToken: string + domain: string + blogPostId: string + cloudId?: string +} + +export interface ConfluenceDeleteBlogPostResponse { + success: boolean + output: { + ts: string + blogPostId: string + deleted: boolean + } +} + +export const confluenceDeleteBlogPostTool: ToolConfig< + ConfluenceDeleteBlogPostParams, + ConfluenceDeleteBlogPostResponse +> = { + id: 'confluence_delete_blogpost', + name: 'Confluence Delete Blog Post', + description: 'Delete a Confluence blog post.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + blogPostId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the blog post to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'DELETE', + headers: (params: ConfluenceDeleteBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + blogPostId: params.blogPostId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPostId: data.blogPostId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPostId: { type: 'string', description: 'Deleted blog post ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_space.ts b/apps/sim/tools/confluence/delete_space.ts new file mode 100644 index 0000000000..82442c66ad --- /dev/null +++ b/apps/sim/tools/confluence/delete_space.ts @@ -0,0 +1,95 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteSpaceParams { + accessToken: string + domain: string + spaceId: string + cloudId?: string +} + +export interface ConfluenceDeleteSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + deleted: boolean + } +} + +export const confluenceDeleteSpaceTool: ToolConfig< + ConfluenceDeleteSpaceParams, + ConfluenceDeleteSpaceResponse +> = { + id: 'confluence_delete_space', + name: 'Confluence Delete Space', + description: 'Delete a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'DELETE', + headers: (params: ConfluenceDeleteSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.spaceId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Deleted space ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_space_property.ts b/apps/sim/tools/confluence/delete_space_property.ts new file mode 100644 index 0000000000..9c69431aac --- /dev/null +++ b/apps/sim/tools/confluence/delete_space_property.ts @@ -0,0 +1,107 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteSpacePropertyParams { + accessToken: string + domain: string + spaceId: string + propertyId: string + cloudId?: string +} + +export interface ConfluenceDeleteSpacePropertyResponse { + success: boolean + output: { + ts: string + spaceId: string + propertyId: string + deleted: boolean + } +} + +export const confluenceDeleteSpacePropertyTool: ToolConfig< + ConfluenceDeleteSpacePropertyParams, + ConfluenceDeleteSpacePropertyResponse +> = { + id: 'confluence_delete_space_property', + name: 'Confluence Delete Space Property', + description: 'Delete a property from a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID the property belongs to', + }, + propertyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Property ID to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceDeleteSpacePropertyParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteSpacePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + action: 'delete', + propertyId: params.propertyId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.spaceId ?? '', + propertyId: data.propertyId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Space ID' }, + propertyId: { type: 'string', description: 'Deleted property ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_descendants.ts b/apps/sim/tools/confluence/get_page_descendants.ts new file mode 100644 index 0000000000..a9e0bc5a32 --- /dev/null +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -0,0 +1,147 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPageDescendantsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceGetPageDescendantsResponse { + success: boolean + output: { + ts: string + descendants: Array<{ + id: string + title: string + type: string | null + status: string | null + spaceId: string | null + parentId: string | null + childPosition: number | null + depth: number | null + }> + pageId: string + nextCursor: string | null + } +} + +export const confluenceGetPageDescendantsTool: ToolConfig< + ConfluenceGetPageDescendantsParams, + ConfluenceGetPageDescendantsResponse +> = { + id: 'confluence_get_page_descendants', + name: 'Confluence Get Page Descendants', + description: 'Get all descendants of a Confluence page recursively.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Page ID to get descendants for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of descendants to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-descendants', + method: 'POST', + headers: (params: ConfluenceGetPageDescendantsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetPageDescendantsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + descendants: data.descendants || [], + pageId: data.pageId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + descendants: { + type: 'array', + description: 'Array of descendant pages', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Page ID' }, + title: { type: 'string', description: 'Page title' }, + type: { + type: 'string', + description: 'Content type (page, whiteboard, database, etc.)', + optional: true, + }, + status: { type: 'string', description: 'Page status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + parentId: { type: 'string', description: 'Parent page ID', optional: true }, + childPosition: { type: 'number', description: 'Position among siblings', optional: true }, + depth: { type: 'number', description: 'Depth in the hierarchy', optional: true }, + }, + }, + }, + pageId: { type: 'string', description: 'Parent page ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_version.ts b/apps/sim/tools/confluence/get_page_version.ts index c162e25465..dc496b38a2 100644 --- a/apps/sim/tools/confluence/get_page_version.ts +++ b/apps/sim/tools/confluence/get_page_version.ts @@ -1,4 +1,8 @@ -import { DETAILED_VERSION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import { + BODY_FORMAT_PROPERTIES, + DETAILED_VERSION_OUTPUT_PROPERTIES, + TIMESTAMP_OUTPUT, +} from '@/tools/confluence/types' import type { ToolConfig } from '@/tools/types' export interface ConfluenceGetPageVersionParams { @@ -14,6 +18,8 @@ export interface ConfluenceGetPageVersionResponse { output: { ts: string pageId: string + title: string | null + content: string | null version: { number: number message: string | null @@ -25,6 +31,12 @@ export interface ConfluenceGetPageVersionResponse { prevVersion: number | null nextVersion: number | null } + body: { + storage?: { + value: string + representation: string + } + } | null } } @@ -100,6 +112,8 @@ export const confluenceGetPageVersionTool: ToolConfig< output: { ts: new Date().toISOString(), pageId: data.pageId ?? '', + title: data.title ?? null, + content: data.content ?? null, version: data.version ?? { number: 0, message: null, @@ -107,6 +121,7 @@ export const confluenceGetPageVersionTool: ToolConfig< authorId: null, createdAt: null, }, + body: data.body ?? null, }, } }, @@ -114,10 +129,29 @@ export const confluenceGetPageVersionTool: ToolConfig< outputs: { ts: TIMESTAMP_OUTPUT, pageId: { type: 'string', description: 'ID of the page' }, + title: { type: 'string', description: 'Page title at this version', optional: true }, + content: { + type: 'string', + description: 'Page content with HTML tags stripped at this version', + optional: true, + }, version: { type: 'object', description: 'Detailed version information', properties: DETAILED_VERSION_OUTPUT_PROPERTIES, }, + body: { + type: 'object', + description: 'Raw page body content in storage format at this version', + properties: { + storage: { + type: 'object', + description: 'Body in storage format (Confluence markup)', + properties: BODY_FORMAT_PROPERTIES, + optional: true, + }, + }, + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/get_task.ts b/apps/sim/tools/confluence/get_task.ts new file mode 100644 index 0000000000..cf0b617765 --- /dev/null +++ b/apps/sim/tools/confluence/get_task.ts @@ -0,0 +1,130 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetTaskParams { + accessToken: string + domain: string + taskId: string + cloudId?: string +} + +export interface ConfluenceGetTaskResponse { + success: boolean + output: { + ts: string + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + body: string | null + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + } +} + +export const confluenceGetTaskTool: ToolConfig = + { + id: 'confluence_get_task', + name: 'Confluence Get Task', + description: 'Get a specific Confluence inline task by ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to retrieve', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceGetTaskParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetTaskParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + taskId: params.taskId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const task = data.task || data + return { + success: true, + output: { + ts: new Date().toISOString(), + id: task.id ?? '', + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status ?? '', + body: task.body ?? null, + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Task status (complete or incomplete)' }, + body: { type: 'string', description: 'Task body content in storage format', optional: true }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, + } diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts new file mode 100644 index 0000000000..2304836135 --- /dev/null +++ b/apps/sim/tools/confluence/get_user.ts @@ -0,0 +1,113 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetUserParams { + accessToken: string + domain: string + accountId: string + cloudId?: string +} + +export interface ConfluenceGetUserResponse { + success: boolean + output: { + ts: string + accountId: string + displayName: string + email: string | null + accountType: string | null + profilePicture: string | null + publicName: string | null + } +} + +export const confluenceGetUserTool: ToolConfig = + { + id: 'confluence_get_user', + name: 'Confluence Get User', + description: 'Get display name and profile info for a Confluence user by account ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Atlassian account ID of the user to look up', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/user', + method: 'POST', + headers: (params: ConfluenceGetUserParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetUserParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + accountId: params.accountId?.trim(), + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + accountId: data.accountId ?? '', + displayName: data.displayName ?? '', + email: data.email ?? null, + accountType: data.accountType ?? null, + profilePicture: data.profilePicture?.path ?? null, + publicName: data.publicName ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + accountId: { type: 'string', description: 'Atlassian account ID of the user' }, + displayName: { type: 'string', description: 'Display name of the user' }, + email: { type: 'string', description: 'Email address of the user', optional: true }, + accountType: { + type: 'string', + description: 'Account type (e.g., atlassian, app, customer)', + optional: true, + }, + profilePicture: { + type: 'string', + description: 'Path to the user profile picture', + optional: true, + }, + publicName: { type: 'string', description: 'Public name of the user', optional: true }, + }, + } diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index 2494f32d04..0dbcf4024c 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -3,17 +3,25 @@ import { confluenceCreateBlogPostTool } from '@/tools/confluence/create_blogpost import { confluenceCreateCommentTool } from '@/tools/confluence/create_comment' import { confluenceCreatePageTool } from '@/tools/confluence/create_page' import { confluenceCreatePagePropertyTool } from '@/tools/confluence/create_page_property' +import { confluenceCreateSpaceTool } from '@/tools/confluence/create_space' +import { confluenceCreateSpacePropertyTool } from '@/tools/confluence/create_space_property' import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment' +import { confluenceDeleteBlogPostTool } from '@/tools/confluence/delete_blogpost' import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment' import { confluenceDeleteLabelTool } from '@/tools/confluence/delete_label' import { confluenceDeletePageTool } from '@/tools/confluence/delete_page' import { confluenceDeletePagePropertyTool } from '@/tools/confluence/delete_page_property' +import { confluenceDeleteSpaceTool } from '@/tools/confluence/delete_space' +import { confluenceDeleteSpacePropertyTool } from '@/tools/confluence/delete_space_property' import { confluenceGetBlogPostTool } from '@/tools/confluence/get_blogpost' import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ancestors' import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children' +import { confluenceGetPageDescendantsTool } from '@/tools/confluence/get_page_descendants' import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version' import { confluenceGetPagesByLabelTool } from '@/tools/confluence/get_pages_by_label' import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' +import { confluenceGetTaskTool } from '@/tools/confluence/get_task' +import { confluenceGetUserTool } from '@/tools/confluence/get_user' import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts' import { confluenceListBlogPostsInSpaceTool } from '@/tools/confluence/list_blogposts_in_space' @@ -23,7 +31,10 @@ import { confluenceListPagePropertiesTool } from '@/tools/confluence/list_page_p import { confluenceListPageVersionsTool } from '@/tools/confluence/list_page_versions' import { confluenceListPagesInSpaceTool } from '@/tools/confluence/list_pages_in_space' import { confluenceListSpaceLabelsTool } from '@/tools/confluence/list_space_labels' +import { confluenceListSpacePermissionsTool } from '@/tools/confluence/list_space_permissions' +import { confluenceListSpacePropertiesTool } from '@/tools/confluence/list_space_properties' import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces' +import { confluenceListTasksTool } from '@/tools/confluence/list_tasks' import { confluenceRetrieveTool } from '@/tools/confluence/retrieve' import { confluenceSearchTool } from '@/tools/confluence/search' import { confluenceSearchInSpaceTool } from '@/tools/confluence/search_in_space' @@ -64,7 +75,10 @@ import { VERSION_OUTPUT_PROPERTIES, } from '@/tools/confluence/types' import { confluenceUpdateTool } from '@/tools/confluence/update' +import { confluenceUpdateBlogPostTool } from '@/tools/confluence/update_blogpost' import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment' +import { confluenceUpdateSpaceTool } from '@/tools/confluence/update_space' +import { confluenceUpdateTaskTool } from '@/tools/confluence/update_task' import { confluenceUploadAttachmentTool } from '@/tools/confluence/upload_attachment' export { @@ -76,6 +90,7 @@ export { confluenceListPagesInSpaceTool, confluenceGetPageChildrenTool, confluenceGetPageAncestorsTool, + confluenceGetPageDescendantsTool, // Page Version Tools confluenceListPageVersionsTool, confluenceGetPageVersionTool, @@ -87,6 +102,8 @@ export { confluenceListBlogPostsTool, confluenceGetBlogPostTool, confluenceCreateBlogPostTool, + confluenceUpdateBlogPostTool, + confluenceDeleteBlogPostTool, confluenceListBlogPostsInSpaceTool, // Search Tools confluenceSearchTool, @@ -106,9 +123,24 @@ export { confluenceDeleteLabelTool, confluenceGetPagesByLabelTool, confluenceListSpaceLabelsTool, + // User Tools + confluenceGetUserTool, // Space Tools confluenceGetSpaceTool, + confluenceCreateSpaceTool, + confluenceUpdateSpaceTool, + confluenceDeleteSpaceTool, confluenceListSpacesTool, + // Space Property Tools + confluenceListSpacePropertiesTool, + confluenceCreateSpacePropertyTool, + confluenceDeleteSpacePropertyTool, + // Space Permission Tools + confluenceListSpacePermissionsTool, + // Task Tools + confluenceListTasksTool, + confluenceGetTaskTool, + confluenceUpdateTaskTool, // Item property constants (for use in outputs) ATTACHMENT_ITEM_PROPERTIES, COMMENT_ITEM_PROPERTIES, diff --git a/apps/sim/tools/confluence/list_space_permissions.ts b/apps/sim/tools/confluence/list_space_permissions.ts new file mode 100644 index 0000000000..3d8fe00f2b --- /dev/null +++ b/apps/sim/tools/confluence/list_space_permissions.ts @@ -0,0 +1,156 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpacePermissionsParams { + accessToken: string + domain: string + spaceId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListSpacePermissionsResponse { + success: boolean + output: { + ts: string + permissions: Array<{ + id: string + principalType: string | null + principalId: string | null + operationKey: string | null + operationTargetType: string | null + anonymousAccess: boolean + unlicensedAccess: boolean + }> + spaceId: string + nextCursor: string | null + } +} + +export const confluenceListSpacePermissionsTool: ToolConfig< + ConfluenceListSpacePermissionsParams, + ConfluenceListSpacePermissionsResponse +> = { + id: 'confluence_list_space_permissions', + name: 'Confluence List Space Permissions', + description: 'List permissions for a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to list permissions for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of permissions to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-permissions', + method: 'POST', + headers: (params: ConfluenceListSpacePermissionsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListSpacePermissionsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + permissions: data.permissions || [], + spaceId: data.spaceId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + permissions: { + type: 'array', + description: 'Array of space permissions', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Permission ID' }, + principalType: { + type: 'string', + description: 'Principal type (user, group, role)', + optional: true, + }, + principalId: { type: 'string', description: 'Principal ID', optional: true }, + operationKey: { + type: 'string', + description: 'Operation key (read, create, delete, etc.)', + optional: true, + }, + operationTargetType: { + type: 'string', + description: 'Target type (page, blogpost, space, etc.)', + optional: true, + }, + anonymousAccess: { type: 'boolean', description: 'Whether anonymous access is allowed' }, + unlicensedAccess: { + type: 'boolean', + description: 'Whether unlicensed access is allowed', + }, + }, + }, + }, + spaceId: { type: 'string', description: 'Space ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_space_properties.ts b/apps/sim/tools/confluence/list_space_properties.ts new file mode 100644 index 0000000000..d47c4570b0 --- /dev/null +++ b/apps/sim/tools/confluence/list_space_properties.ts @@ -0,0 +1,133 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpacePropertiesParams { + accessToken: string + domain: string + spaceId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListSpacePropertiesResponse { + success: boolean + output: { + ts: string + properties: Array<{ + id: string + key: string + value: unknown + }> + spaceId: string + nextCursor: string | null + } +} + +export const confluenceListSpacePropertiesTool: ToolConfig< + ConfluenceListSpacePropertiesParams, + ConfluenceListSpacePropertiesResponse +> = { + id: 'confluence_list_space_properties', + name: 'Confluence List Space Properties', + description: 'List properties on a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to list properties for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of properties to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceListSpacePropertiesParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListSpacePropertiesParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + properties: data.properties || [], + spaceId: data.spaceId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + properties: { + type: 'array', + description: 'Array of space properties', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Property ID' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value' }, + }, + }, + }, + spaceId: { type: 'string', description: 'Space ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_tasks.ts b/apps/sim/tools/confluence/list_tasks.ts new file mode 100644 index 0000000000..4f44678a89 --- /dev/null +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -0,0 +1,181 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListTasksParams { + accessToken: string + domain: string + pageId?: string + spaceId?: string + assignedTo?: string + status?: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListTasksResponse { + success: boolean + output: { + ts: string + tasks: Array<{ + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + body: string | null + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListTasksTool: ToolConfig< + ConfluenceListTasksParams, + ConfluenceListTasksResponse +> = { + id: 'confluence_list_tasks', + name: 'Confluence List Tasks', + description: + 'List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by page ID', + }, + spaceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by space ID', + }, + assignedTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by assignee account ID', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by status (complete or incomplete)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of tasks to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceListTasksParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListTasksParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + spaceId: params.spaceId, + assignedTo: params.assignedTo, + status: params.status, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + tasks: data.tasks || [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + tasks: { + type: 'array', + description: 'Array of Confluence tasks', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Task status (complete or incomplete)' }, + body: { + type: 'string', + description: 'Task body content in storage format', + optional: true, + }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/update_blogpost.ts b/apps/sim/tools/confluence/update_blogpost.ts new file mode 100644 index 0000000000..ea873cea17 --- /dev/null +++ b/apps/sim/tools/confluence/update_blogpost.ts @@ -0,0 +1,123 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateBlogPostParams { + accessToken: string + domain: string + blogPostId: string + title?: string + content?: string + cloudId?: string +} + +export interface ConfluenceUpdateBlogPostResponse { + success: boolean + output: { + ts: string + blogPostId: string + title: string + status: string | null + spaceId: string | null + version: Record | null + url: string + } +} + +export const confluenceUpdateBlogPostTool: ToolConfig< + ConfluenceUpdateBlogPostParams, + ConfluenceUpdateBlogPostResponse +> = { + id: 'confluence_update_blogpost', + name: 'Confluence Update Blog Post', + description: 'Update an existing Confluence blog post title and/or content.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + blogPostId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the blog post to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the blog post', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New content for the blog post in storage format', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'PUT', + headers: (params: ConfluenceUpdateBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + blogPostId: params.blogPostId, + title: params.title, + content: params.content, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPostId: data.id ?? '', + title: data.title ?? '', + status: data.status ?? null, + spaceId: data.spaceId ?? null, + version: data.version ?? null, + url: data._links?.webui ?? '', + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPostId: { type: 'string', description: 'Updated blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + version: { type: 'json', description: 'Version information', optional: true }, + url: { type: 'string', description: 'URL to view the blog post' }, + }, +} diff --git a/apps/sim/tools/confluence/update_space.ts b/apps/sim/tools/confluence/update_space.ts new file mode 100644 index 0000000000..c1cc6bd6db --- /dev/null +++ b/apps/sim/tools/confluence/update_space.ts @@ -0,0 +1,131 @@ +import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateSpaceParams { + accessToken: string + domain: string + spaceId: string + name?: string + description?: string + cloudId?: string +} + +export interface ConfluenceUpdateSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + name: string + key: string + type: string + status: string + url: string + description: { value: string; representation: string } | null + } +} + +export const confluenceUpdateSpaceTool: ToolConfig< + ConfluenceUpdateSpaceParams, + ConfluenceUpdateSpaceResponse +> = { + id: 'confluence_update_space', + name: 'Confluence Update Space', + description: 'Update a Confluence space name or description.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the space', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the space', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'PUT', + headers: (params: ConfluenceUpdateSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + name: params.name, + description: params.description, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.id ?? '', + name: data.name ?? '', + key: data.key ?? '', + type: data.type ?? '', + status: data.status ?? '', + url: data._links?.webui ?? '', + description: data.description ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Updated space ID' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space type' }, + status: { type: 'string', description: 'Space status' }, + url: { type: 'string', description: 'URL to view the space' }, + description: { + type: 'object', + description: 'Space description', + properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES, + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/update_task.ts b/apps/sim/tools/confluence/update_task.ts new file mode 100644 index 0000000000..d7d87387eb --- /dev/null +++ b/apps/sim/tools/confluence/update_task.ts @@ -0,0 +1,141 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateTaskParams { + accessToken: string + domain: string + taskId: string + status: string + cloudId?: string +} + +export interface ConfluenceUpdateTaskResponse { + success: boolean + output: { + ts: string + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + body: string | null + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + } +} + +export const confluenceUpdateTaskTool: ToolConfig< + ConfluenceUpdateTaskParams, + ConfluenceUpdateTaskResponse +> = { + id: 'confluence_update_task', + name: 'Confluence Update Task', + description: 'Update the status of a Confluence inline task (complete or incomplete).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to update', + }, + status: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New status for the task (complete or incomplete)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceUpdateTaskParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateTaskParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + action: 'update', + taskId: params.taskId, + status: params.status, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const task = data.task || data + return { + success: true, + output: { + ts: new Date().toISOString(), + id: task.id ?? '', + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status ?? '', + body: task.body ?? null, + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Updated task status' }, + body: { type: 'string', description: 'Task body content in storage format', optional: true }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/confluence/utils.ts b/apps/sim/tools/confluence/utils.ts index 2f57929654..d7e55a2c56 100644 --- a/apps/sim/tools/confluence/utils.ts +++ b/apps/sim/tools/confluence/utils.ts @@ -56,13 +56,21 @@ function stripHtmlTags(html: string): string { return text.trim() } +/** + * Strips HTML tags and decodes HTML entities from raw Confluence content. + */ +export function cleanHtmlContent(rawContent: string): string { + let content = stripHtmlTags(rawContent) + content = decodeHtmlEntities(content) + content = content.replace(/\s+/g, ' ').trim() + return content +} + export function transformPageData(data: any) { const rawContent = data.body?.storage?.value || data.body?.view?.value || data.body?.atlas_doc_format?.value || '' - let cleanContent = stripHtmlTags(rawContent) - cleanContent = decodeHtmlEntities(cleanContent) - cleanContent = cleanContent.replace(/\s+/g, ' ').trim() + const cleanContent = cleanHtmlContent(rawContent) return { success: true, diff --git a/apps/sim/tools/devin/create_session.ts b/apps/sim/tools/devin/create_session.ts new file mode 100644 index 0000000000..1c99bfba77 --- /dev/null +++ b/apps/sim/tools/devin/create_session.ts @@ -0,0 +1,105 @@ +import type { ToolConfig } from '@/tools/types' +import type { DevinCreateSessionParams, DevinCreateSessionResponse } from './types' +import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types' + +export const devinCreateSessionTool: ToolConfig< + DevinCreateSessionParams, + DevinCreateSessionResponse +> = { + id: 'devin_create_session', + name: 'create_session', + description: + 'Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Devin API key (service user credential starting with cog_)', + }, + prompt: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The task prompt for Devin to work on', + }, + playbookId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional playbook ID to guide the session', + }, + maxAcuLimit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum ACU limit for the session', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated tags for the session', + }, + }, + + request: { + url: 'https://api.devin.ai/v3/organizations/sessions', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + prompt: params.prompt, + } + if (params.playbookId) body.playbook_id = params.playbookId + if (params.maxAcuLimit != null) { + body.max_acu_limit = params.maxAcuLimit + } + if (params.tags) { + body.tags = params.tags.split(',').map((t: string) => t.trim()) + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + sessionId: data.session_id ?? null, + url: data.url ?? null, + status: data.status ?? null, + statusDetail: data.status_detail ?? null, + title: data.title ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + acusConsumed: data.acus_consumed ?? null, + tags: data.tags ?? null, + pullRequests: data.pull_requests ?? null, + structuredOutput: data.structured_output ?? null, + playbookId: data.playbook_id ?? null, + }, + } + }, + + outputs: { + sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId, + url: DEVIN_SESSION_OUTPUT_PROPERTIES.url, + status: DEVIN_SESSION_OUTPUT_PROPERTIES.status, + statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail, + title: DEVIN_SESSION_OUTPUT_PROPERTIES.title, + createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt, + updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt, + acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed, + tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags, + pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests, + structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput, + playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId, + }, +} diff --git a/apps/sim/tools/devin/get_session.ts b/apps/sim/tools/devin/get_session.ts new file mode 100644 index 0000000000..0af5b309bf --- /dev/null +++ b/apps/sim/tools/devin/get_session.ts @@ -0,0 +1,70 @@ +import type { ToolConfig } from '@/tools/types' +import type { DevinGetSessionParams, DevinGetSessionResponse } from './types' +import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types' + +export const devinGetSessionTool: ToolConfig = { + id: 'devin_get_session', + name: 'get_session', + description: + 'Retrieve details of an existing Devin session including status, tags, pull requests, and structured output.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Devin API key (service user credential starting with cog_)', + }, + sessionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The session ID to retrieve', + }, + }, + + request: { + url: (params) => `https://api.devin.ai/v3/organizations/sessions/${params.sessionId}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + sessionId: data.session_id ?? null, + url: data.url ?? null, + status: data.status ?? null, + statusDetail: data.status_detail ?? null, + title: data.title ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + acusConsumed: data.acus_consumed ?? null, + tags: data.tags ?? null, + pullRequests: data.pull_requests ?? null, + structuredOutput: data.structured_output ?? null, + playbookId: data.playbook_id ?? null, + }, + } + }, + + outputs: { + sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId, + url: DEVIN_SESSION_OUTPUT_PROPERTIES.url, + status: DEVIN_SESSION_OUTPUT_PROPERTIES.status, + statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail, + title: DEVIN_SESSION_OUTPUT_PROPERTIES.title, + createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt, + updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt, + acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed, + tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags, + pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests, + structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput, + playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId, + }, +} diff --git a/apps/sim/tools/devin/index.ts b/apps/sim/tools/devin/index.ts new file mode 100644 index 0000000000..be9d1f91db --- /dev/null +++ b/apps/sim/tools/devin/index.ts @@ -0,0 +1,4 @@ +export { devinCreateSessionTool } from './create_session' +export { devinGetSessionTool } from './get_session' +export { devinListSessionsTool } from './list_sessions' +export { devinSendMessageTool } from './send_message' diff --git a/apps/sim/tools/devin/list_sessions.ts b/apps/sim/tools/devin/list_sessions.ts new file mode 100644 index 0000000000..66391e4f34 --- /dev/null +++ b/apps/sim/tools/devin/list_sessions.ts @@ -0,0 +1,70 @@ +import type { ToolConfig } from '@/tools/types' +import type { DevinListSessionsParams, DevinListSessionsResponse } from './types' +import { DEVIN_SESSION_LIST_ITEM_PROPERTIES } from './types' + +export const devinListSessionsTool: ToolConfig = + { + id: 'devin_list_sessions', + name: 'list_sessions', + description: 'List Devin sessions in the organization. Returns up to 100 sessions by default.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Devin API key (service user credential starting with cog_)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of sessions to return (1-200, default: 100)', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.limit) searchParams.set('first', String(params.limit)) + const qs = searchParams.toString() + return `https://api.devin.ai/v3/organizations/sessions${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const items = data.items ?? [] + return { + success: true, + output: { + sessions: items.map((item: Record) => ({ + sessionId: item.session_id ?? null, + url: item.url ?? null, + status: item.status ?? null, + statusDetail: item.status_detail ?? null, + title: item.title ?? null, + createdAt: item.created_at ?? null, + updatedAt: item.updated_at ?? null, + tags: item.tags ?? null, + })), + }, + } + }, + + outputs: { + sessions: { + type: 'array', + description: 'List of Devin sessions', + items: { + type: 'object', + properties: DEVIN_SESSION_LIST_ITEM_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/devin/send_message.ts b/apps/sim/tools/devin/send_message.ts new file mode 100644 index 0000000000..13844c4703 --- /dev/null +++ b/apps/sim/tools/devin/send_message.ts @@ -0,0 +1,80 @@ +import type { ToolConfig } from '@/tools/types' +import type { DevinSendMessageParams, DevinSendMessageResponse } from './types' +import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types' + +export const devinSendMessageTool: ToolConfig = { + id: 'devin_send_message', + name: 'send_message', + description: + 'Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Devin API key (service user credential starting with cog_)', + }, + sessionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The session ID to send the message to', + }, + message: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The message to send to Devin', + }, + }, + + request: { + url: (params) => `https://api.devin.ai/v3/organizations/sessions/${params.sessionId}/messages`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + message: params.message, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + sessionId: data.session_id ?? null, + url: data.url ?? null, + status: data.status ?? null, + statusDetail: data.status_detail ?? null, + title: data.title ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + acusConsumed: data.acus_consumed ?? null, + tags: data.tags ?? null, + pullRequests: data.pull_requests ?? null, + structuredOutput: data.structured_output ?? null, + playbookId: data.playbook_id ?? null, + }, + } + }, + + outputs: { + sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId, + url: DEVIN_SESSION_OUTPUT_PROPERTIES.url, + status: DEVIN_SESSION_OUTPUT_PROPERTIES.status, + statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail, + title: DEVIN_SESSION_OUTPUT_PROPERTIES.title, + createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt, + updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt, + acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed, + tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags, + pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests, + structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput, + playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId, + }, +} diff --git a/apps/sim/tools/devin/types.ts b/apps/sim/tools/devin/types.ts new file mode 100644 index 0000000000..f38653819b --- /dev/null +++ b/apps/sim/tools/devin/types.ts @@ -0,0 +1,168 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +export interface DevinCreateSessionParams { + apiKey: string + prompt: string + playbookId?: string + maxAcuLimit?: number + tags?: string +} + +export interface DevinGetSessionParams { + apiKey: string + sessionId: string +} + +export interface DevinListSessionsParams { + apiKey: string + limit?: number +} + +export interface DevinSendMessageParams { + apiKey: string + sessionId: string + message: string +} + +export const DEVIN_SESSION_OUTPUT_PROPERTIES = { + sessionId: { + type: 'string', + description: 'Unique identifier for the session', + }, + url: { + type: 'string', + description: 'URL to view the session in the Devin UI', + }, + status: { + type: 'string', + description: 'Session status (new, claimed, running, exit, error, suspended, resuming)', + }, + statusDetail: { + type: 'string', + description: + 'Detailed status (working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.)', + optional: true, + }, + title: { + type: 'string', + description: 'Session title', + optional: true, + }, + createdAt: { + type: 'number', + description: 'Unix timestamp when the session was created', + optional: true, + }, + updatedAt: { + type: 'number', + description: 'Unix timestamp when the session was last updated', + optional: true, + }, + acusConsumed: { + type: 'number', + description: 'ACUs consumed by the session', + optional: true, + }, + tags: { + type: 'json', + description: 'Tags associated with the session', + optional: true, + }, + pullRequests: { + type: 'json', + description: 'Pull requests created during the session', + optional: true, + }, + structuredOutput: { + type: 'json', + description: 'Structured output from the session', + optional: true, + }, + playbookId: { + type: 'string', + description: 'Associated playbook ID', + optional: true, + }, +} as const satisfies Record + +export const DEVIN_SESSION_LIST_ITEM_PROPERTIES = { + sessionId: { + type: 'string', + description: 'Unique identifier for the session', + }, + url: { + type: 'string', + description: 'URL to view the session', + }, + status: { + type: 'string', + description: 'Session status', + }, + statusDetail: { + type: 'string', + description: 'Detailed status', + optional: true, + }, + title: { + type: 'string', + description: 'Session title', + optional: true, + }, + createdAt: { + type: 'number', + description: 'Creation timestamp (Unix)', + optional: true, + }, + updatedAt: { + type: 'number', + description: 'Last updated timestamp (Unix)', + optional: true, + }, + tags: { + type: 'json', + description: 'Session tags', + optional: true, + }, +} as const satisfies Record + +export interface DevinSessionOutput { + sessionId: string + url: string + status: string + statusDetail: string | null + title: string | null + createdAt: number | null + updatedAt: number | null + acusConsumed: number | null + tags: string[] | null + pullRequests: Array<{ pr_url: string; pr_state: string | null }> | null + structuredOutput: Record | null + playbookId: string | null +} + +export interface DevinCreateSessionResponse extends ToolResponse { + output: DevinSessionOutput +} + +export interface DevinGetSessionResponse extends ToolResponse { + output: DevinSessionOutput +} + +export interface DevinListSessionsResponse extends ToolResponse { + output: { + sessions: Array<{ + sessionId: string + url: string + status: string + statusDetail: string | null + title: string | null + createdAt: number | null + updatedAt: number | null + tags: string[] | null + }> + } +} + +export interface DevinSendMessageResponse extends ToolResponse { + output: DevinSessionOutput +} diff --git a/apps/sim/tools/gmail/create_label.ts b/apps/sim/tools/gmail/create_label.ts new file mode 100644 index 0000000000..98d9c4e447 --- /dev/null +++ b/apps/sim/tools/gmail/create_label.ts @@ -0,0 +1,119 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailCreateLabelParams { + accessToken: string + name: string + messageListVisibility?: string + labelListVisibility?: string +} + +interface GmailCreateLabelResponse { + success: boolean + output: { + id: string + name: string + messageListVisibility?: string + labelListVisibility?: string + type?: string + } +} + +export const gmailCreateLabelV2Tool: ToolConfig = + { + id: 'gmail_create_label_v2', + name: 'Gmail Create Label', + description: 'Create a new label in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Display name for the new label', + }, + messageListVisibility: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Visibility of messages with this label in the message list (show or hide)', + }, + labelListVisibility: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Visibility of the label in the label list (labelShow, labelShowIfUnread, or labelHide)', + }, + }, + + request: { + url: () => `${GMAIL_API_BASE}/labels`, + method: 'POST', + headers: (params: GmailCreateLabelParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GmailCreateLabelParams) => { + const body: Record = { name: params.name } + if (params.messageListVisibility) { + body.messageListVisibility = params.messageListVisibility + } + if (params.labelListVisibility) { + body.labelListVisibility = params.labelListVisibility + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', name: '' }, + error: data.error?.message || 'Failed to create label', + } + } + + return { + success: true, + output: { + id: data.id, + name: data.name, + messageListVisibility: data.messageListVisibility ?? null, + labelListVisibility: data.labelListVisibility ?? null, + type: data.type ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label display name' }, + messageListVisibility: { + type: 'string', + description: 'Visibility of messages with this label', + optional: true, + }, + labelListVisibility: { + type: 'string', + description: 'Visibility of the label in the label list', + optional: true, + }, + type: { type: 'string', description: 'Label type (system or user)', optional: true }, + }, + } diff --git a/apps/sim/tools/gmail/delete_draft.ts b/apps/sim/tools/gmail/delete_draft.ts new file mode 100644 index 0000000000..9597931542 --- /dev/null +++ b/apps/sim/tools/gmail/delete_draft.ts @@ -0,0 +1,76 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailDeleteDraftParams { + accessToken: string + draftId: string +} + +interface GmailDeleteDraftResponse { + success: boolean + output: { + deleted: boolean + draftId: string + } +} + +export const gmailDeleteDraftV2Tool: ToolConfig = + { + id: 'gmail_delete_draft_v2', + name: 'Gmail Delete Draft', + description: 'Delete a specific draft from Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + draftId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the draft to delete', + }, + }, + + request: { + url: (params: GmailDeleteDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}`, + method: 'DELETE', + headers: (params: GmailDeleteDraftParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: GmailDeleteDraftParams) => { + if (!response.ok) { + const data = await response.json() + return { + success: false, + output: { deleted: false, draftId: params?.draftId ?? '' }, + error: data.error?.message || 'Failed to delete draft', + } + } + + return { + success: true, + output: { + deleted: true, + draftId: params?.draftId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the draft was successfully deleted' }, + draftId: { type: 'string', description: 'ID of the deleted draft' }, + }, + } diff --git a/apps/sim/tools/gmail/delete_label.ts b/apps/sim/tools/gmail/delete_label.ts new file mode 100644 index 0000000000..2ecf1e76ad --- /dev/null +++ b/apps/sim/tools/gmail/delete_label.ts @@ -0,0 +1,76 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailDeleteLabelParams { + accessToken: string + labelId: string +} + +interface GmailDeleteLabelResponse { + success: boolean + output: { + deleted: boolean + labelId: string + } +} + +export const gmailDeleteLabelV2Tool: ToolConfig = + { + id: 'gmail_delete_label_v2', + name: 'Gmail Delete Label', + description: 'Delete a label from Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the label to delete', + }, + }, + + request: { + url: (params: GmailDeleteLabelParams) => `${GMAIL_API_BASE}/labels/${params.labelId}`, + method: 'DELETE', + headers: (params: GmailDeleteLabelParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: GmailDeleteLabelParams) => { + if (!response.ok) { + const data = await response.json() + return { + success: false, + output: { deleted: false, labelId: params?.labelId ?? '' }, + error: data.error?.message || 'Failed to delete label', + } + } + + return { + success: true, + output: { + deleted: true, + labelId: params?.labelId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the label was successfully deleted' }, + labelId: { type: 'string', description: 'ID of the deleted label' }, + }, + } diff --git a/apps/sim/tools/gmail/get_draft.ts b/apps/sim/tools/gmail/get_draft.ts new file mode 100644 index 0000000000..932f0733c7 --- /dev/null +++ b/apps/sim/tools/gmail/get_draft.ts @@ -0,0 +1,117 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailGetDraftParams { + accessToken: string + draftId: string +} + +interface GmailGetDraftResponse { + success: boolean + output: { + id: string + messageId?: string + threadId?: string + to?: string + from?: string + subject?: string + body?: string + labelIds?: string[] + } +} + +export const gmailGetDraftV2Tool: ToolConfig = { + id: 'gmail_get_draft_v2', + name: 'Gmail Get Draft', + description: 'Get a specific draft from Gmail by its ID', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + draftId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the draft to retrieve', + }, + }, + + request: { + url: (params: GmailGetDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}?format=full`, + method: 'GET', + headers: (params: GmailGetDraftParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '' }, + error: data.error?.message || 'Failed to get draft', + } + } + + const message = data.message || {} + const headers = message.payload?.headers || [] + const getHeader = (name: string): string | undefined => + headers.find((h: Record) => h.name.toLowerCase() === name.toLowerCase()) + ?.value + + let body = '' + if (message.payload?.body?.data) { + body = Buffer.from(message.payload.body.data, 'base64').toString() + } else if (message.payload?.parts) { + const textPart = message.payload.parts.find( + (part: Record) => part.mimeType === 'text/plain' + ) + if (textPart?.body?.data) { + body = Buffer.from(textPart.body.data, 'base64').toString() + } + } + + return { + success: true, + output: { + id: data.id, + messageId: message.id ?? undefined, + threadId: message.threadId ?? undefined, + to: getHeader('To'), + from: getHeader('From'), + subject: getHeader('Subject'), + body: body || undefined, + labelIds: message.labelIds ?? undefined, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Draft ID' }, + messageId: { type: 'string', description: 'Gmail message ID', optional: true }, + threadId: { type: 'string', description: 'Gmail thread ID', optional: true }, + to: { type: 'string', description: 'Recipient email address', optional: true }, + from: { type: 'string', description: 'Sender email address', optional: true }, + subject: { type: 'string', description: 'Draft subject', optional: true }, + body: { type: 'string', description: 'Draft body text', optional: true }, + labelIds: { + type: 'array', + items: { type: 'string' }, + description: 'Draft labels', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gmail/get_thread.ts b/apps/sim/tools/gmail/get_thread.ts new file mode 100644 index 0000000000..33641298ce --- /dev/null +++ b/apps/sim/tools/gmail/get_thread.ts @@ -0,0 +1,136 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailGetThreadParams { + accessToken: string + threadId: string + format?: string +} + +interface GmailGetThreadResponse { + success: boolean + output: { + id: string + historyId?: string + messages: Array<{ + id: string + threadId: string + labelIds?: string[] + snippet?: string + from?: string + to?: string + subject?: string + date?: string + body?: string + }> + } +} + +export const gmailGetThreadV2Tool: ToolConfig = { + id: 'gmail_get_thread_v2', + name: 'Gmail Get Thread', + description: 'Get a specific email thread from Gmail, including all messages in the thread', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to retrieve', + }, + format: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Format to return the messages in (full, metadata, or minimal). Defaults to full.', + }, + }, + + request: { + url: (params: GmailGetThreadParams) => { + const format = params.format || 'full' + return `${GMAIL_API_BASE}/threads/${params.threadId}?format=${format}` + }, + method: 'GET', + headers: (params: GmailGetThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', messages: [] }, + error: data.error?.message || 'Failed to get thread', + } + } + + const messages = (data.messages || []).map((message: Record) => { + const payload = message.payload as Record | undefined + const headers = (payload?.headers as Array>) || [] + const getHeader = (name: string): string | undefined => + headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value + + let body = '' + const payloadBody = payload?.body as Record | undefined + if (payloadBody?.data) { + body = Buffer.from(payloadBody.data as string, 'base64').toString() + } else if (payload?.parts) { + const parts = payload.parts as Array> + const textPart = parts.find((part) => part.mimeType === 'text/plain') + const textBody = textPart?.body as Record | undefined + if (textBody?.data) { + body = Buffer.from(textBody.data as string, 'base64').toString() + } + } + + return { + id: message.id, + threadId: message.threadId, + labelIds: message.labelIds ?? null, + snippet: message.snippet ?? null, + from: getHeader('From') ?? null, + to: getHeader('To') ?? null, + subject: getHeader('Subject') ?? null, + date: getHeader('Date') ?? null, + body: body || null, + } + }) + + return { + success: true, + output: { + id: data.id, + historyId: data.historyId ?? null, + messages, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + historyId: { type: 'string', description: 'History ID', optional: true }, + messages: { + type: 'json', + description: + 'Array of messages in the thread with id, from, to, subject, date, body, and labels', + }, + }, +} diff --git a/apps/sim/tools/gmail/index.ts b/apps/sim/tools/gmail/index.ts index 3dfa89e4eb..6869a2e5f6 100644 --- a/apps/sim/tools/gmail/index.ts +++ b/apps/sim/tools/gmail/index.ts @@ -1,7 +1,15 @@ import { gmailAddLabelTool, gmailAddLabelV2Tool } from '@/tools/gmail/add_label' import { gmailArchiveTool, gmailArchiveV2Tool } from '@/tools/gmail/archive' +import { gmailCreateLabelV2Tool } from '@/tools/gmail/create_label' import { gmailDeleteTool, gmailDeleteV2Tool } from '@/tools/gmail/delete' +import { gmailDeleteDraftV2Tool } from '@/tools/gmail/delete_draft' +import { gmailDeleteLabelV2Tool } from '@/tools/gmail/delete_label' import { gmailDraftTool, gmailDraftV2Tool } from '@/tools/gmail/draft' +import { gmailGetDraftV2Tool } from '@/tools/gmail/get_draft' +import { gmailGetThreadV2Tool } from '@/tools/gmail/get_thread' +import { gmailListDraftsV2Tool } from '@/tools/gmail/list_drafts' +import { gmailListLabelsV2Tool } from '@/tools/gmail/list_labels' +import { gmailListThreadsV2Tool } from '@/tools/gmail/list_threads' import { gmailMarkReadTool, gmailMarkReadV2Tool } from '@/tools/gmail/mark_read' import { gmailMarkUnreadTool, gmailMarkUnreadV2Tool } from '@/tools/gmail/mark_unread' import { gmailMoveTool, gmailMoveV2Tool } from '@/tools/gmail/move' @@ -9,7 +17,9 @@ import { gmailReadTool, gmailReadV2Tool } from '@/tools/gmail/read' import { gmailRemoveLabelTool, gmailRemoveLabelV2Tool } from '@/tools/gmail/remove_label' import { gmailSearchTool, gmailSearchV2Tool } from '@/tools/gmail/search' import { gmailSendTool, gmailSendV2Tool } from '@/tools/gmail/send' +import { gmailTrashThreadV2Tool } from '@/tools/gmail/trash_thread' import { gmailUnarchiveTool, gmailUnarchiveV2Tool } from '@/tools/gmail/unarchive' +import { gmailUntrashThreadV2Tool } from '@/tools/gmail/untrash_thread' export { gmailSendTool, @@ -36,4 +46,14 @@ export { gmailAddLabelV2Tool, gmailRemoveLabelTool, gmailRemoveLabelV2Tool, + gmailListDraftsV2Tool, + gmailGetDraftV2Tool, + gmailDeleteDraftV2Tool, + gmailCreateLabelV2Tool, + gmailDeleteLabelV2Tool, + gmailListLabelsV2Tool, + gmailGetThreadV2Tool, + gmailListThreadsV2Tool, + gmailTrashThreadV2Tool, + gmailUntrashThreadV2Tool, } diff --git a/apps/sim/tools/gmail/list_drafts.ts b/apps/sim/tools/gmail/list_drafts.ts new file mode 100644 index 0000000000..5ba31e5a23 --- /dev/null +++ b/apps/sim/tools/gmail/list_drafts.ts @@ -0,0 +1,126 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListDraftsParams { + accessToken: string + maxResults?: number + pageToken?: string + query?: string +} + +interface GmailListDraftsResponse { + success: boolean + output: { + drafts: Array<{ + id: string + messageId: string + threadId: string + }> + resultSizeEstimate: number + nextPageToken?: string + } +} + +export const gmailListDraftsV2Tool: ToolConfig = { + id: 'gmail_list_drafts_v2', + name: 'Gmail List Drafts', + description: 'List all drafts in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of drafts to return (default: 100, max: 500)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token for paginated results', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter drafts (same syntax as Gmail search)', + }, + }, + + request: { + url: (params: GmailListDraftsParams) => { + const searchParams = new URLSearchParams() + if (params.maxResults) { + searchParams.append('maxResults', Number(params.maxResults).toString()) + } + if (params.pageToken) { + searchParams.append('pageToken', params.pageToken) + } + if (params.query) { + searchParams.append('q', params.query) + } + const qs = searchParams.toString() + return `${GMAIL_API_BASE}/drafts${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: GmailListDraftsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { drafts: [], resultSizeEstimate: 0 }, + error: data.error?.message || 'Failed to list drafts', + } + } + + const drafts = (data.drafts || []).map((draft: Record) => ({ + id: draft.id, + messageId: (draft.message as Record)?.id ?? null, + threadId: (draft.message as Record)?.threadId ?? null, + })) + + return { + success: true, + output: { + drafts, + resultSizeEstimate: data.resultSizeEstimate ?? 0, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + drafts: { + type: 'json', + description: 'Array of draft objects with id, messageId, and threadId', + }, + resultSizeEstimate: { + type: 'number', + description: 'Estimated total number of drafts', + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gmail/list_labels.ts b/apps/sim/tools/gmail/list_labels.ts new file mode 100644 index 0000000000..159af96f34 --- /dev/null +++ b/apps/sim/tools/gmail/list_labels.ts @@ -0,0 +1,81 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListLabelsParams { + accessToken: string +} + +interface GmailListLabelsResponse { + success: boolean + output: { + labels: Array<{ + id: string + name: string + type: string + messageListVisibility?: string + labelListVisibility?: string + }> + } +} + +export const gmailListLabelsV2Tool: ToolConfig = { + id: 'gmail_list_labels_v2', + name: 'Gmail List Labels', + description: 'List all labels in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + }, + + request: { + url: () => `${GMAIL_API_BASE}/labels`, + method: 'GET', + headers: (params: GmailListLabelsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { labels: [] }, + error: data.error?.message || 'Failed to list labels', + } + } + + const labels = (data.labels || []).map((label: Record) => ({ + id: label.id, + name: label.name, + type: label.type ?? null, + messageListVisibility: label.messageListVisibility ?? null, + labelListVisibility: label.labelListVisibility ?? null, + })) + + return { + success: true, + output: { labels }, + } + }, + + outputs: { + labels: { + type: 'json', + description: 'Array of label objects with id, name, type, and visibility settings', + }, + }, +} diff --git a/apps/sim/tools/gmail/list_threads.ts b/apps/sim/tools/gmail/list_threads.ts new file mode 100644 index 0000000000..a2617cf9c5 --- /dev/null +++ b/apps/sim/tools/gmail/list_threads.ts @@ -0,0 +1,140 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailListThreadsParams { + accessToken: string + maxResults?: number + pageToken?: string + query?: string + labelIds?: string +} + +interface GmailListThreadsResponse { + success: boolean + output: { + threads: Array<{ + id: string + snippet: string + historyId: string + }> + resultSizeEstimate: number + nextPageToken?: string + } +} + +export const gmailListThreadsV2Tool: ToolConfig = + { + id: 'gmail_list_threads_v2', + name: 'Gmail List Threads', + description: 'List email threads in a Gmail account', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of threads to return (default: 100, max: 500)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token for paginated results', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter threads (same syntax as Gmail search)', + }, + labelIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated label IDs to filter threads by', + }, + }, + + request: { + url: (params: GmailListThreadsParams) => { + const searchParams = new URLSearchParams() + if (params.maxResults) { + searchParams.append('maxResults', Number(params.maxResults).toString()) + } + if (params.pageToken) { + searchParams.append('pageToken', params.pageToken) + } + if (params.query) { + searchParams.append('q', params.query) + } + if (params.labelIds) { + const labels = params.labelIds.split(',').map((l) => l.trim()) + for (const label of labels) { + searchParams.append('labelIds', label) + } + } + const qs = searchParams.toString() + return `${GMAIL_API_BASE}/threads${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: GmailListThreadsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { threads: [], resultSizeEstimate: 0 }, + error: data.error?.message || 'Failed to list threads', + } + } + + const threads = (data.threads || []).map((thread: Record) => ({ + id: thread.id, + snippet: thread.snippet ?? '', + historyId: thread.historyId ?? '', + })) + + return { + success: true, + output: { + threads, + resultSizeEstimate: data.resultSizeEstimate ?? 0, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + threads: { + type: 'json', + description: 'Array of thread objects with id, snippet, and historyId', + }, + resultSizeEstimate: { + type: 'number', + description: 'Estimated total number of threads', + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/gmail/trash_thread.ts b/apps/sim/tools/gmail/trash_thread.ts new file mode 100644 index 0000000000..4d13c32090 --- /dev/null +++ b/apps/sim/tools/gmail/trash_thread.ts @@ -0,0 +1,78 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailTrashThreadParams { + accessToken: string + threadId: string +} + +interface GmailTrashThreadResponse { + success: boolean + output: { + id: string + trashed: boolean + } +} + +export const gmailTrashThreadV2Tool: ToolConfig = + { + id: 'gmail_trash_thread_v2', + name: 'Gmail Trash Thread', + description: 'Move an email thread to trash in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to trash', + }, + }, + + request: { + url: (params: GmailTrashThreadParams) => `${GMAIL_API_BASE}/threads/${params.threadId}/trash`, + method: 'POST', + headers: (params: GmailTrashThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', trashed: false }, + error: data.error?.message || 'Failed to trash thread', + } + } + + return { + success: true, + output: { + id: data.id, + trashed: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + trashed: { type: 'boolean', description: 'Whether the thread was successfully trashed' }, + }, + } diff --git a/apps/sim/tools/gmail/untrash_thread.ts b/apps/sim/tools/gmail/untrash_thread.ts new file mode 100644 index 0000000000..ea88aac7c4 --- /dev/null +++ b/apps/sim/tools/gmail/untrash_thread.ts @@ -0,0 +1,84 @@ +import { GMAIL_API_BASE } from '@/tools/gmail/utils' +import type { ToolConfig } from '@/tools/types' + +interface GmailUntrashThreadParams { + accessToken: string + threadId: string +} + +interface GmailUntrashThreadResponse { + success: boolean + output: { + id: string + untrashed: boolean + } +} + +export const gmailUntrashThreadV2Tool: ToolConfig< + GmailUntrashThreadParams, + GmailUntrashThreadResponse +> = { + id: 'gmail_untrash_thread_v2', + name: 'Gmail Untrash Thread', + description: 'Remove an email thread from trash in Gmail', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-email', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Gmail API', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the thread to untrash', + }, + }, + + request: { + url: (params: GmailUntrashThreadParams) => + `${GMAIL_API_BASE}/threads/${params.threadId}/untrash`, + method: 'POST', + headers: (params: GmailUntrashThreadParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { id: '', untrashed: false }, + error: data.error?.message || 'Failed to untrash thread', + } + } + + return { + success: true, + output: { + id: data.id, + untrashed: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Thread ID' }, + untrashed: { + type: 'boolean', + description: 'Whether the thread was successfully removed from trash', + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/get_table.ts b/apps/sim/tools/google_bigquery/get_table.ts new file mode 100644 index 0000000000..50cf0437a8 --- /dev/null +++ b/apps/sim/tools/google_bigquery/get_table.ts @@ -0,0 +1,132 @@ +import type { + GoogleBigQueryGetTableParams, + GoogleBigQueryGetTableResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryGetTableTool: ToolConfig< + GoogleBigQueryGetTableParams, + GoogleBigQueryGetTableResponse +> = { + id: 'google_bigquery_get_table', + name: 'BigQuery Get Table', + description: 'Get metadata and schema for a Google BigQuery table', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables/${encodeURIComponent(params.tableId)}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to get BigQuery table' + throw new Error(errorMessage) + } + + const schema = (data.schema?.fields ?? []).map( + (f: { name: string; type: string; mode?: string; description?: string }) => ({ + name: f.name, + type: f.type, + mode: f.mode ?? null, + description: f.description ?? null, + }) + ) + + return { + success: true, + output: { + tableId: data.tableReference?.tableId ?? null, + datasetId: data.tableReference?.datasetId ?? null, + projectId: data.tableReference?.projectId ?? null, + type: data.type ?? null, + description: data.description ?? null, + numRows: data.numRows ?? null, + numBytes: data.numBytes ?? null, + schema, + creationTime: data.creationTime ?? null, + lastModifiedTime: data.lastModifiedTime ?? null, + location: data.location ?? null, + }, + } + }, + + outputs: { + tableId: { type: 'string', description: 'Table ID' }, + datasetId: { type: 'string', description: 'Dataset ID' }, + projectId: { type: 'string', description: 'Project ID' }, + type: { + type: 'string', + description: 'Table type (TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL)', + }, + description: { type: 'string', description: 'Table description', optional: true }, + numRows: { type: 'string', description: 'Total number of rows' }, + numBytes: { + type: 'string', + description: 'Total size in bytes, excluding data in streaming buffer', + }, + schema: { + type: 'array', + description: 'Array of column definitions', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Column name' }, + type: { + type: 'string', + description: 'Data type (STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.)', + }, + mode: { + type: 'string', + description: 'Column mode (NULLABLE, REQUIRED, or REPEATED)', + optional: true, + }, + description: { type: 'string', description: 'Column description', optional: true }, + }, + }, + }, + creationTime: { type: 'string', description: 'Table creation time (milliseconds since epoch)' }, + lastModifiedTime: { + type: 'string', + description: 'Last modification time (milliseconds since epoch)', + }, + location: { type: 'string', description: 'Geographic location where the table resides' }, + }, +} diff --git a/apps/sim/tools/google_bigquery/index.ts b/apps/sim/tools/google_bigquery/index.ts new file mode 100644 index 0000000000..ea1aa73371 --- /dev/null +++ b/apps/sim/tools/google_bigquery/index.ts @@ -0,0 +1,5 @@ +export { googleBigQueryGetTableTool } from '@/tools/google_bigquery/get_table' +export { googleBigQueryInsertRowsTool } from '@/tools/google_bigquery/insert_rows' +export { googleBigQueryListDatasetsTool } from '@/tools/google_bigquery/list_datasets' +export { googleBigQueryListTablesTool } from '@/tools/google_bigquery/list_tables' +export { googleBigQueryQueryTool } from '@/tools/google_bigquery/query' diff --git a/apps/sim/tools/google_bigquery/insert_rows.ts b/apps/sim/tools/google_bigquery/insert_rows.ts new file mode 100644 index 0000000000..8f7e03e839 --- /dev/null +++ b/apps/sim/tools/google_bigquery/insert_rows.ts @@ -0,0 +1,174 @@ +import type { + GoogleBigQueryInsertRowsParams, + GoogleBigQueryInsertRowsResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryInsertRowsTool: ToolConfig< + GoogleBigQueryInsertRowsParams, + GoogleBigQueryInsertRowsResponse +> = { + id: 'google_bigquery_insert_rows', + name: 'BigQuery Insert Rows', + description: 'Insert rows into a Google BigQuery table using streaming insert', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID', + }, + rows: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'JSON array of row objects to insert', + }, + skipInvalidRows: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to insert valid rows even if some are invalid', + }, + ignoreUnknownValues: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to ignore columns not in the table schema', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables/${encodeURIComponent(params.tableId)}/insertAll`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const parsedRows = typeof params.rows === 'string' ? JSON.parse(params.rows) : params.rows + const rows = (parsedRows as Record[]).map( + (row: Record) => ({ json: row }) + ) + + const body: Record = { rows } + if (params.skipInvalidRows !== undefined) body.skipInvalidRows = params.skipInvalidRows + if (params.ignoreUnknownValues !== undefined) + body.ignoreUnknownValues = params.ignoreUnknownValues + + return body + }, + }, + + transformResponse: async (response: Response, params?: GoogleBigQueryInsertRowsParams) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to insert rows into BigQuery table' + throw new Error(errorMessage) + } + + const insertErrors = data.insertErrors ?? [] + const errors = insertErrors.map( + (err: { + index: number + errors: Array<{ reason?: string; location?: string; message?: string }> + }) => ({ + index: err.index, + errors: err.errors.map((e) => ({ + reason: e.reason ?? null, + location: e.location ?? null, + message: e.message ?? null, + })), + }) + ) + + let totalRows = 0 + if (params?.rows) { + const parsed = typeof params.rows === 'string' ? JSON.parse(params.rows) : params.rows + totalRows = Array.isArray(parsed) ? parsed.length : 0 + } + + // When insertErrors is empty, all rows succeeded. + // When insertErrors is present and skipInvalidRows is false (default), + // the entire batch is rejected — no rows are inserted. + let insertedRows = 0 + if (insertErrors.length === 0) { + insertedRows = totalRows + } else if (params?.skipInvalidRows) { + const failedIndexes = new Set(insertErrors.map((e: { index: number }) => e.index)) + insertedRows = totalRows - failedIndexes.size + } + + return { + success: true, + output: { + insertedRows, + errors, + }, + } + }, + + outputs: { + insertedRows: { type: 'number', description: 'Number of rows successfully inserted' }, + errors: { + type: 'array', + description: 'Array of per-row insertion errors (empty if all succeeded)', + items: { + type: 'object', + properties: { + index: { type: 'number', description: 'Zero-based index of the row that failed' }, + errors: { + type: 'array', + description: 'Error details for this row', + items: { + type: 'object', + properties: { + reason: { + type: 'string', + description: 'Short error code summarizing the error', + optional: true, + }, + location: { + type: 'string', + description: 'Where the error occurred', + optional: true, + }, + message: { + type: 'string', + description: 'Human-readable error description', + optional: true, + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/list_datasets.ts b/apps/sim/tools/google_bigquery/list_datasets.ts new file mode 100644 index 0000000000..75da30bfe5 --- /dev/null +++ b/apps/sim/tools/google_bigquery/list_datasets.ts @@ -0,0 +1,121 @@ +import type { + GoogleBigQueryListDatasetsParams, + GoogleBigQueryListDatasetsResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryListDatasetsTool: ToolConfig< + GoogleBigQueryListDatasetsParams, + GoogleBigQueryListDatasetsResponse +> = { + id: 'google_bigquery_list_datasets', + name: 'BigQuery List Datasets', + description: 'List all datasets in a Google BigQuery project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of datasets to return', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets` + ) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list BigQuery datasets' + throw new Error(errorMessage) + } + + const datasets = (data.datasets ?? []).map( + (ds: { + datasetReference: { datasetId: string; projectId: string } + friendlyName?: string + location?: string + }) => ({ + datasetId: ds.datasetReference.datasetId, + projectId: ds.datasetReference.projectId, + friendlyName: ds.friendlyName ?? null, + location: ds.location ?? null, + }) + ) + + return { + success: true, + output: { + datasets, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + datasets: { + type: 'array', + description: 'Array of dataset objects', + items: { + type: 'object', + properties: { + datasetId: { type: 'string', description: 'Unique dataset identifier' }, + projectId: { type: 'string', description: 'Project ID containing this dataset' }, + friendlyName: { + type: 'string', + description: 'Descriptive name for the dataset', + optional: true, + }, + location: { type: 'string', description: 'Geographic location where the data resides' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/list_tables.ts b/apps/sim/tools/google_bigquery/list_tables.ts new file mode 100644 index 0000000000..1d116a5c1e --- /dev/null +++ b/apps/sim/tools/google_bigquery/list_tables.ts @@ -0,0 +1,142 @@ +import type { + GoogleBigQueryListTablesParams, + GoogleBigQueryListTablesResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryListTablesTool: ToolConfig< + GoogleBigQueryListTablesParams, + GoogleBigQueryListTablesResponse +> = { + id: 'google_bigquery_list_tables', + name: 'BigQuery List Tables', + description: 'List all tables in a Google BigQuery dataset', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of tables to return', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables` + ) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list BigQuery tables' + throw new Error(errorMessage) + } + + const tables = (data.tables ?? []).map( + (t: { + tableReference: { tableId: string; datasetId: string; projectId: string } + type?: string + friendlyName?: string + creationTime?: string + }) => ({ + tableId: t.tableReference.tableId, + datasetId: t.tableReference.datasetId, + projectId: t.tableReference.projectId, + type: t.type ?? null, + friendlyName: t.friendlyName ?? null, + creationTime: t.creationTime ?? null, + }) + ) + + return { + success: true, + output: { + tables, + totalItems: data.totalItems ?? null, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + tables: { + type: 'array', + description: 'Array of table objects', + items: { + type: 'object', + properties: { + tableId: { type: 'string', description: 'Table identifier' }, + datasetId: { type: 'string', description: 'Dataset ID containing this table' }, + projectId: { type: 'string', description: 'Project ID containing this table' }, + type: { type: 'string', description: 'Table type (TABLE, VIEW, EXTERNAL, etc.)' }, + friendlyName: { + type: 'string', + description: 'User-friendly name for the table', + optional: true, + }, + creationTime: { + type: 'string', + description: 'Time when created, in milliseconds since epoch', + optional: true, + }, + }, + }, + }, + totalItems: { + type: 'number', + description: 'Total number of tables in the dataset', + optional: true, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/query.ts b/apps/sim/tools/google_bigquery/query.ts new file mode 100644 index 0000000000..ad11adc901 --- /dev/null +++ b/apps/sim/tools/google_bigquery/query.ts @@ -0,0 +1,164 @@ +import type { + GoogleBigQueryQueryParams, + GoogleBigQueryQueryResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryQueryTool: ToolConfig< + GoogleBigQueryQueryParams, + GoogleBigQueryQueryResponse +> = { + id: 'google_bigquery_query', + name: 'BigQuery Run Query', + description: 'Run a SQL query against Google BigQuery and return the results', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'SQL query to execute', + }, + useLegacySql: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to use legacy SQL syntax (default: false)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + defaultDatasetId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Default dataset for unqualified table names', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Processing location (e.g., "US", "EU")', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/queries`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + query: params.query, + useLegacySql: params.useLegacySql ?? false, + } + if (params.maxResults !== undefined) body.maxResults = Number(params.maxResults) + if (params.defaultDatasetId) { + body.defaultDataset = { + projectId: params.projectId, + datasetId: params.defaultDatasetId, + } + } + if (params.location) body.location = params.location + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to execute BigQuery query' + throw new Error(errorMessage) + } + + const columns = (data.schema?.fields ?? []).map((f: { name: string }) => f.name) + const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => { + const obj: Record = {} + row.f.forEach((field, index) => { + obj[columns[index]] = field.v ?? null + }) + return obj + }) + + return { + success: true, + output: { + columns, + rows, + totalRows: data.totalRows ?? null, + jobComplete: data.jobComplete ?? false, + totalBytesProcessed: data.totalBytesProcessed ?? null, + cacheHit: data.cacheHit ?? null, + jobReference: data.jobReference ?? null, + pageToken: data.pageToken ?? null, + }, + } + }, + + outputs: { + columns: { + type: 'array', + description: 'Array of column names from the query result', + items: { type: 'string', description: 'Column name' }, + }, + rows: { + type: 'array', + description: 'Array of row objects keyed by column name', + items: { + type: 'object', + description: 'Row with column name/value pairs', + }, + }, + totalRows: { + type: 'string', + description: 'Total number of rows in the complete result set', + optional: true, + }, + jobComplete: { type: 'boolean', description: 'Whether the query completed within the timeout' }, + totalBytesProcessed: { type: 'string', description: 'Total bytes processed by the query' }, + cacheHit: { + type: 'boolean', + description: 'Whether the query result was served from cache', + optional: true, + }, + jobReference: { + type: 'object', + description: 'Job reference (useful when jobComplete is false)', + optional: true, + properties: { + projectId: { type: 'string', description: 'Project ID containing the job' }, + jobId: { type: 'string', description: 'Unique job identifier' }, + location: { type: 'string', description: 'Geographic location of the job' }, + }, + }, + pageToken: { + type: 'string', + description: 'Token for fetching additional result pages', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/types.ts b/apps/sim/tools/google_bigquery/types.ts new file mode 100644 index 0000000000..c7432e0dea --- /dev/null +++ b/apps/sim/tools/google_bigquery/types.ts @@ -0,0 +1,119 @@ +import type { ToolResponse } from '@/tools/types' + +export interface GoogleBigQueryBaseParams { + accessToken: string + projectId: string +} + +export interface GoogleBigQueryQueryParams extends GoogleBigQueryBaseParams { + query: string + useLegacySql?: boolean + maxResults?: number + defaultDatasetId?: string + location?: string +} + +export interface GoogleBigQueryListDatasetsParams extends GoogleBigQueryBaseParams { + maxResults?: number + pageToken?: string +} + +export interface GoogleBigQueryListTablesParams extends GoogleBigQueryBaseParams { + datasetId: string + maxResults?: number + pageToken?: string +} + +export interface GoogleBigQueryGetTableParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string +} + +export interface GoogleBigQueryInsertRowsParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string + rows: string + skipInvalidRows?: boolean + ignoreUnknownValues?: boolean +} + +export interface GoogleBigQueryJobReference { + projectId: string + jobId: string + location: string +} + +export interface GoogleBigQueryQueryResponse extends ToolResponse { + output: { + columns: string[] + rows: Record[] + totalRows: string | null + jobComplete: boolean + totalBytesProcessed: string | null + cacheHit: boolean | null + jobReference: GoogleBigQueryJobReference | null + pageToken: string | null + } +} + +export interface GoogleBigQueryListDatasetsResponse extends ToolResponse { + output: { + datasets: Array<{ + datasetId: string + projectId: string + friendlyName: string | null + location: string | null + }> + nextPageToken: string | null + } +} + +export interface GoogleBigQueryListTablesResponse extends ToolResponse { + output: { + tables: Array<{ + tableId: string + datasetId: string + projectId: string + type: string | null + friendlyName: string | null + creationTime: string | null + }> + totalItems: number | null + nextPageToken: string | null + } +} + +export interface GoogleBigQueryGetTableResponse extends ToolResponse { + output: { + tableId: string + datasetId: string + projectId: string + type: string | null + description: string | null + numRows: string | null + numBytes: string | null + schema: Array<{ + name: string + type: string + mode: string | null + description: string | null + }> + creationTime: string | null + lastModifiedTime: string | null + location: string | null + } +} + +export interface GoogleBigQueryInsertRowsResponse extends ToolResponse { + output: { + insertedRows: number + errors: Array<{ + index: number + errors: Array<{ + reason: string | null + location: string | null + message: string | null + }> + }> + } +} diff --git a/apps/sim/tools/google_calendar/freebusy.ts b/apps/sim/tools/google_calendar/freebusy.ts new file mode 100644 index 0000000000..00c68ec180 --- /dev/null +++ b/apps/sim/tools/google_calendar/freebusy.ts @@ -0,0 +1,155 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiFreeBusyResponse, + type GoogleCalendarFreeBusyParams, + type GoogleCalendarFreeBusyResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export const freebusyTool: ToolConfig< + GoogleCalendarFreeBusyParams, + GoogleCalendarFreeBusyResponse +> = { + id: 'google_calendar_freebusy', + name: 'Google Calendar Free/Busy', + description: 'Query free/busy information for one or more Google Calendars', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated calendar IDs to query (e.g., "primary,other@example.com")', + }, + timeMin: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start of the time range (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)', + }, + timeMax: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End of the time range (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)', + }, + timeZone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IANA time zone (e.g., "UTC", "America/New_York"). Defaults to UTC.', + }, + }, + + request: { + url: () => `${CALENDAR_API_BASE}/freeBusy`, + method: 'POST', + headers: (params: GoogleCalendarFreeBusyParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleCalendarFreeBusyParams) => { + const ids = params.calendarIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + + return { + timeMin: params.timeMin, + timeMax: params.timeMax, + timeZone: params.timeZone || 'UTC', + items: ids.map((id) => ({ id })), + } + }, + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiFreeBusyResponse = await response.json() + + const calendarIds = Object.keys(data.calendars || {}) + const totalBusy = calendarIds.reduce((sum, id) => { + return sum + (data.calendars[id]?.busy?.length || 0) + }, 0) + + return { + success: true, + output: { + content: `Found ${totalBusy} busy period${totalBusy !== 1 ? 's' : ''} across ${calendarIds.length} calendar${calendarIds.length !== 1 ? 's' : ''}`, + metadata: { + timeMin: data.timeMin, + timeMax: data.timeMax, + calendars: data.calendars, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of free/busy results' }, + metadata: { + type: 'json', + description: 'Free/busy data with time range and per-calendar busy periods', + }, + }, +} + +interface GoogleCalendarFreeBusyV2Response { + success: boolean + output: { + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > + } +} + +export const freebusyV2Tool: ToolConfig< + GoogleCalendarFreeBusyParams, + GoogleCalendarFreeBusyV2Response +> = { + id: 'google_calendar_freebusy_v2', + name: 'Google Calendar Free/Busy', + description: + 'Query free/busy information for one or more Google Calendars. Returns API-aligned fields only.', + version: '2.0.0', + oauth: freebusyTool.oauth, + params: freebusyTool.params, + request: freebusyTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiFreeBusyResponse = await response.json() + + return { + success: true, + output: { + timeMin: data.timeMin, + timeMax: data.timeMax, + calendars: data.calendars, + }, + } + }, + outputs: { + timeMin: { type: 'string', description: 'Start of the queried time range' }, + timeMax: { type: 'string', description: 'End of the queried time range' }, + calendars: { + type: 'json', + description: 'Per-calendar free/busy data with busy periods and any errors', + }, + }, +} diff --git a/apps/sim/tools/google_calendar/index.ts b/apps/sim/tools/google_calendar/index.ts index 2ddfcf55e5..4d55e57ad9 100644 --- a/apps/sim/tools/google_calendar/index.ts +++ b/apps/sim/tools/google_calendar/index.ts @@ -1,5 +1,6 @@ import { createTool, createV2Tool } from '@/tools/google_calendar/create' import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete' +import { freebusyTool, freebusyV2Tool } from '@/tools/google_calendar/freebusy' import { getTool, getV2Tool } from '@/tools/google_calendar/get' import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances' import { inviteTool, inviteV2Tool } from '@/tools/google_calendar/invite' @@ -11,6 +12,7 @@ import { updateTool, updateV2Tool } from '@/tools/google_calendar/update' export const googleCalendarCreateTool = createTool export const googleCalendarDeleteTool = deleteTool +export const googleCalendarFreeBusyTool = freebusyTool export const googleCalendarGetTool = getTool export const googleCalendarInstancesTool = instancesTool export const googleCalendarInviteTool = inviteTool @@ -22,6 +24,7 @@ export const googleCalendarUpdateTool = updateTool export const googleCalendarCreateV2Tool = createV2Tool export const googleCalendarDeleteV2Tool = deleteV2Tool +export const googleCalendarFreeBusyV2Tool = freebusyV2Tool export const googleCalendarGetV2Tool = getV2Tool export const googleCalendarInstancesV2Tool = instancesV2Tool export const googleCalendarInviteV2Tool = inviteV2Tool diff --git a/apps/sim/tools/google_calendar/types.ts b/apps/sim/tools/google_calendar/types.ts index 46b39f8a04..fc3cc83262 100644 --- a/apps/sim/tools/google_calendar/types.ts +++ b/apps/sim/tools/google_calendar/types.ts @@ -90,6 +90,14 @@ export interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams showDeleted?: boolean } +export interface GoogleCalendarFreeBusyParams { + accessToken: string + calendarIds: string // Comma-separated calendar IDs (e.g., "primary,other@example.com") + timeMin: string // RFC3339 timestamp (e.g., 2025-06-03T00:00:00Z) + timeMax: string // RFC3339 timestamp (e.g., 2025-06-04T00:00:00Z) + timeZone?: string // IANA time zone (e.g., "UTC", "America/New_York") +} + export interface GoogleCalendarListCalendarsParams { accessToken: string minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner' @@ -109,6 +117,7 @@ export type GoogleCalendarToolParams = | GoogleCalendarInviteParams | GoogleCalendarMoveParams | GoogleCalendarInstancesParams + | GoogleCalendarFreeBusyParams | GoogleCalendarListCalendarsParams interface EventMetadata { @@ -341,6 +350,36 @@ export interface GoogleCalendarInstancesResponse extends ToolResponse { } } +export interface GoogleCalendarFreeBusyResponse extends ToolResponse { + output: { + content: string + metadata: { + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > + } + } +} + +export interface GoogleCalendarApiFreeBusyResponse { + kind: string + timeMin: string + timeMax: string + calendars: Record< + string, + { + busy: Array<{ start: string; end: string }> + errors?: Array<{ domain: string; reason: string }> + } + > +} + export interface GoogleCalendarListCalendarsResponse extends ToolResponse { output: { content: string @@ -373,4 +412,5 @@ export type GoogleCalendarResponse = | GoogleCalendarDeleteResponse | GoogleCalendarMoveResponse | GoogleCalendarInstancesResponse + | GoogleCalendarFreeBusyResponse | GoogleCalendarListCalendarsResponse diff --git a/apps/sim/tools/google_drive/index.ts b/apps/sim/tools/google_drive/index.ts index 58dbdb03e7..7c81c6c2fc 100644 --- a/apps/sim/tools/google_drive/index.ts +++ b/apps/sim/tools/google_drive/index.ts @@ -7,6 +7,8 @@ import { getContentTool } from '@/tools/google_drive/get_content' import { getFileTool } from '@/tools/google_drive/get_file' import { listTool } from '@/tools/google_drive/list' import { listPermissionsTool } from '@/tools/google_drive/list_permissions' +import { moveTool } from '@/tools/google_drive/move' +import { searchTool } from '@/tools/google_drive/search' import { shareTool } from '@/tools/google_drive/share' import { trashTool } from '@/tools/google_drive/trash' import { unshareTool } from '@/tools/google_drive/unshare' @@ -23,6 +25,8 @@ export const googleDriveGetContentTool = getContentTool export const googleDriveGetFileTool = getFileTool export const googleDriveListTool = listTool export const googleDriveListPermissionsTool = listPermissionsTool +export const googleDriveMoveTool = moveTool +export const googleDriveSearchTool = searchTool export const googleDriveShareTool = shareTool export const googleDriveTrashTool = trashTool export const googleDriveUnshareTool = unshareTool diff --git a/apps/sim/tools/google_drive/move.ts b/apps/sim/tools/google_drive/move.ts new file mode 100644 index 0000000000..4358b7d29b --- /dev/null +++ b/apps/sim/tools/google_drive/move.ts @@ -0,0 +1,145 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveMoveParams extends GoogleDriveToolParams { + fileId: string + destinationFolderId: string + removeFromCurrent?: boolean +} + +interface GoogleDriveMoveResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const moveTool: ToolConfig = { + id: 'google_drive_move', + name: 'Move Google Drive File', + description: 'Move a file or folder to a different folder in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to move', + }, + destinationFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination folder', + }, + removeFromCurrent: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether to remove the file from its current parent folder (default: true). Set to false to add the file to the destination without removing it from the current location.', + }, + }, + + request: { + url: 'https://www.googleapis.com/drive/v3/files', + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + directExecution: async (params) => { + const fileId = params.fileId?.trim() + const destinationFolderId = params.destinationFolderId?.trim() + const removeFromCurrent = params.removeFromCurrent !== false + + if (!fileId) { + throw new Error('fileId is required') + } + if (!destinationFolderId) { + throw new Error('destinationFolderId is required') + } + + const headers = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + + // Build the PATCH URL with addParents + const url = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`) + url.searchParams.append('addParents', destinationFolderId) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + + if (removeFromCurrent) { + // Fetch current parents so we can remove them + const metadataUrl = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`) + metadataUrl.searchParams.append('fields', 'parents') + metadataUrl.searchParams.append('supportsAllDrives', 'true') + + const metadataResponse = await fetch(metadataUrl.toString(), { headers }) + + if (!metadataResponse.ok) { + const errorData = await metadataResponse.json() + throw new Error(errorData.error?.message || 'Failed to retrieve file metadata') + } + + const metadata = await metadataResponse.json() + if (metadata.parents && metadata.parents.length > 0) { + url.searchParams.append('removeParents', metadata.parents.join(',')) + } + } + + const response = await fetch(url.toString(), { + method: 'PATCH', + headers, + body: JSON.stringify({}), + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to move Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The moved file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + kind: { type: 'string', description: 'Resource type identifier' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + owners: { type: 'json', description: 'List of file owners' }, + size: { type: 'string', description: 'File size in bytes' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/search.ts b/apps/sim/tools/google_drive/search.ts new file mode 100644 index 0000000000..52eccb7ef6 --- /dev/null +++ b/apps/sim/tools/google_drive/search.ts @@ -0,0 +1,145 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveSearchParams extends GoogleDriveToolParams { + query: string + pageSize?: number + pageToken?: string +} + +interface GoogleDriveSearchResponse extends ToolResponse { + output: { + files: GoogleDriveFile[] + nextPageToken?: string + } +} + +export const searchTool: ToolConfig = { + id: 'google_drive_search', + name: 'Search Google Drive Files', + description: + 'Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Google Drive query string using advanced search syntax (e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime > \'2024-01-01\'")', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of files to return (default: 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Token for fetching the next page of results', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://www.googleapis.com/drive/v3/files') + url.searchParams.append('fields', `files(${ALL_FILE_FIELDS}),nextPageToken`) + url.searchParams.append('corpora', 'allDrives') + url.searchParams.append('supportsAllDrives', 'true') + url.searchParams.append('includeItemsFromAllDrives', 'true') + + // The query is passed directly as Google Drive query syntax + const conditions = ['trashed = false'] + if (params.query?.trim()) { + conditions.push(params.query.trim()) + } + url.searchParams.append('q', conditions.join(' and ')) + + if (params.pageSize) { + url.searchParams.append('pageSize', Number(params.pageSize).toString()) + } + if (params.pageToken) { + url.searchParams.append('pageToken', params.pageToken) + } + + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to search Google Drive files') + } + + return { + success: true, + output: { + files: data.files || [], + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + files: { + type: 'array', + description: 'Array of file metadata objects matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + kind: { type: 'string', description: 'Resource type identifier' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + description: { type: 'string', description: 'File description' }, + originalFilename: { type: 'string', description: 'Original uploaded filename' }, + fullFileExtension: { type: 'string', description: 'Full file extension' }, + fileExtension: { type: 'string', description: 'File extension' }, + owners: { type: 'json', description: 'List of file owners' }, + permissions: { type: 'json', description: 'File permissions' }, + shared: { type: 'boolean', description: 'Whether file is shared' }, + ownedByMe: { type: 'boolean', description: 'Whether owned by current user' }, + starred: { type: 'boolean', description: 'Whether file is starred' }, + trashed: { type: 'boolean', description: 'Whether file is in trash' }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + lastModifyingUser: { type: 'json', description: 'User who last modified the file' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + webContentLink: { type: 'string', description: 'Direct download URL' }, + iconLink: { type: 'string', description: 'URL to file icon' }, + thumbnailLink: { type: 'string', description: 'URL to thumbnail' }, + size: { type: 'string', description: 'File size in bytes' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + driveId: { type: 'string', description: 'Shared drive ID' }, + capabilities: { type: 'json', description: 'User capabilities on file' }, + version: { type: 'string', description: 'Version number' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_rows.ts b/apps/sim/tools/google_sheets/delete_rows.ts new file mode 100644 index 0000000000..9e146d3ad3 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_rows.ts @@ -0,0 +1,156 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteRowsParams { + accessToken: string + spreadsheetId: string + sheetId: number + startIndex: number + endIndex: number +} + +export interface GoogleSheetsV2DeleteRowsResponse extends ToolResponse { + output: { + spreadsheetId: string + sheetId: number + deletedRowRange: string + metadata: { + spreadsheetId: string + spreadsheetUrl: string + } + } +} + +export const deleteRowsV2Tool: ToolConfig< + GoogleSheetsV2DeleteRowsParams, + GoogleSheetsV2DeleteRowsResponse +> = { + id: 'google_sheets_delete_rows_v2', + name: 'Delete Rows from Google Sheets V2', + description: 'Delete rows from a sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID', + }, + sheetId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The numeric ID of the sheet/tab (not the sheet name). Use Get Spreadsheet to find sheet IDs.', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The start row index (0-based, inclusive) of the rows to delete', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end row index (0-based, exclusive) of the rows to delete', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (params.sheetId === undefined || params.sheetId === null) { + throw new Error('Sheet ID is required') + } + if (params.startIndex === undefined || params.startIndex === null) { + throw new Error('Start index is required') + } + if (params.endIndex === undefined || params.endIndex === null) { + throw new Error('End index is required') + } + + return { + requests: [ + { + deleteDimension: { + range: { + sheetId: params.sheetId, + dimension: 'ROWS', + startIndex: params.startIndex, + endIndex: params.endIndex, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteRowsParams) => { + await response.json() + + const spreadsheetId = params?.spreadsheetId ?? '' + const startIndex = params?.startIndex ?? 0 + const endIndex = params?.endIndex ?? 0 + + return { + success: true, + output: { + spreadsheetId, + sheetId: params?.sheetId ?? 0, + deletedRowRange: `rows ${startIndex} to ${endIndex}`, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + sheetId: { type: 'number', description: 'The numeric ID of the sheet' }, + deletedRowRange: { + type: 'string', + description: 'Description of the deleted row range', + }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_sheet.ts b/apps/sim/tools/google_sheets/delete_sheet.ts new file mode 100644 index 0000000000..b7a7246680 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_sheet.ts @@ -0,0 +1,123 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteSheetParams { + accessToken: string + spreadsheetId: string + sheetId: number +} + +export interface GoogleSheetsV2DeleteSheetResponse extends ToolResponse { + output: { + spreadsheetId: string + deletedSheetId: number + metadata: { + spreadsheetId: string + spreadsheetUrl: string + } + } +} + +export const deleteSheetV2Tool: ToolConfig< + GoogleSheetsV2DeleteSheetParams, + GoogleSheetsV2DeleteSheetResponse +> = { + id: 'google_sheets_delete_sheet_v2', + name: 'Delete Sheet V2', + description: 'Delete a sheet/tab from a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Sheets spreadsheet ID', + }, + sheetId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The numeric ID of the sheet/tab to delete (not the sheet name). Use Get Spreadsheet to find sheet IDs.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (params.sheetId === undefined || params.sheetId === null) { + throw new Error('Sheet ID is required') + } + + return { + requests: [ + { + deleteSheet: { + sheetId: params.sheetId, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSheetParams) => { + await response.json() + + const spreadsheetId = params?.spreadsheetId ?? '' + + return { + success: true, + output: { + spreadsheetId, + deletedSheetId: params?.sheetId ?? 0, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + deletedSheetId: { type: 'number', description: 'The numeric ID of the deleted sheet' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/delete_spreadsheet.ts b/apps/sim/tools/google_sheets/delete_spreadsheet.ts new file mode 100644 index 0000000000..70b7138c90 --- /dev/null +++ b/apps/sim/tools/google_sheets/delete_spreadsheet.ts @@ -0,0 +1,86 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface GoogleSheetsV2DeleteSpreadsheetParams { + accessToken: string + spreadsheetId: string +} + +export interface GoogleSheetsV2DeleteSpreadsheetResponse extends ToolResponse { + output: { + spreadsheetId: string + deleted: boolean + } +} + +export const deleteSpreadsheetV2Tool: ToolConfig< + GoogleSheetsV2DeleteSpreadsheetParams, + GoogleSheetsV2DeleteSpreadsheetResponse +> = { + id: 'google_sheets_delete_spreadsheet_v2', + name: 'Delete Spreadsheet V2', + description: 'Permanently delete a Google Sheets spreadsheet using the Google Drive API', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Google Sheets spreadsheet to delete', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://www.googleapis.com/drive/v3/files/${spreadsheetId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSpreadsheetParams) => { + const spreadsheetId = params?.spreadsheetId ?? '' + + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + spreadsheetId, + deleted: true, + }, + } + } + + const data = await response.json() + throw new Error(data.error?.message ?? 'Failed to delete spreadsheet') + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The ID of the deleted spreadsheet' }, + deleted: { type: 'boolean', description: 'Whether the spreadsheet was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/google_sheets/index.ts b/apps/sim/tools/google_sheets/index.ts index 750e0a3b26..47422712ff 100644 --- a/apps/sim/tools/google_sheets/index.ts +++ b/apps/sim/tools/google_sheets/index.ts @@ -5,6 +5,9 @@ import { batchUpdateV2Tool } from '@/tools/google_sheets/batch_update' import { clearV2Tool } from '@/tools/google_sheets/clear' import { copySheetV2Tool } from '@/tools/google_sheets/copy_sheet' import { createSpreadsheetV2Tool } from '@/tools/google_sheets/create_spreadsheet' +import { deleteRowsV2Tool } from '@/tools/google_sheets/delete_rows' +import { deleteSheetV2Tool } from '@/tools/google_sheets/delete_sheet' +import { deleteSpreadsheetV2Tool } from '@/tools/google_sheets/delete_spreadsheet' import { getSpreadsheetV2Tool } from '@/tools/google_sheets/get_spreadsheet' import { readTool, readV2Tool } from '@/tools/google_sheets/read' import { updateTool, updateV2Tool } from '@/tools/google_sheets/update' @@ -28,3 +31,6 @@ export const googleSheetsBatchGetV2Tool = batchGetV2Tool export const googleSheetsBatchUpdateV2Tool = batchUpdateV2Tool export const googleSheetsBatchClearV2Tool = batchClearV2Tool export const googleSheetsCopySheetV2Tool = copySheetV2Tool +export const googleSheetsDeleteRowsV2Tool = deleteRowsV2Tool +export const googleSheetsDeleteSheetV2Tool = deleteSheetV2Tool +export const googleSheetsDeleteSpreadsheetV2Tool = deleteSpreadsheetV2Tool diff --git a/apps/sim/tools/google_sheets/read.ts b/apps/sim/tools/google_sheets/read.ts index 2ee88b7c27..7494884727 100644 --- a/apps/sim/tools/google_sheets/read.ts +++ b/apps/sim/tools/google_sheets/read.ts @@ -154,6 +154,26 @@ export const readV2Tool: ToolConfig 1) { + const headers = values[0] as string[] + const columnIndex = headers.findIndex( + (h) => String(h).toLowerCase() === params.filterColumn!.toLowerCase() + ) + + if (columnIndex !== -1) { + const matchType = params.filterMatchType ?? 'contains' + const filterVal = params.filterValue.toLowerCase() + + const filteredRows = values.slice(1).filter((row) => { + const cellValue = String(row[columnIndex] ?? '').toLowerCase() + switch (matchType) { + case 'exact': + return cellValue === filterVal + case 'starts_with': + return cellValue.startsWith(filterVal) + case 'ends_with': + return cellValue.endsWith(filterVal) + default: + return cellValue.includes(filterVal) + } + }) + + // Return header row + matching rows + values = [values[0], ...filteredRows] + } + } + return { success: true, output: { sheetName: params?.sheetName ?? '', range: data.range ?? '', - values: data.values ?? [], + values, metadata: { spreadsheetId: metadata.spreadsheetId, spreadsheetUrl: metadata.spreadsheetUrl, diff --git a/apps/sim/tools/google_sheets/types.ts b/apps/sim/tools/google_sheets/types.ts index 74fdc84a1a..66a080d9df 100644 --- a/apps/sim/tools/google_sheets/types.ts +++ b/apps/sim/tools/google_sheets/types.ts @@ -1,3 +1,6 @@ +import type { GoogleSheetsV2DeleteRowsResponse } from '@/tools/google_sheets/delete_rows' +import type { GoogleSheetsV2DeleteSheetResponse } from '@/tools/google_sheets/delete_sheet' +import type { GoogleSheetsV2DeleteSpreadsheetResponse } from '@/tools/google_sheets/delete_spreadsheet' import type { ToolResponse } from '@/tools/types' export interface GoogleSheetsRange { @@ -129,6 +132,9 @@ export interface GoogleSheetsV2ToolParams { includeValuesInResponse?: boolean responseValueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA' majorDimension?: 'ROWS' | 'COLUMNS' + filterColumn?: string + filterValue?: string + filterMatchType?: 'contains' | 'exact' | 'starts_with' | 'ends_with' } export type GoogleSheetsV2Response = @@ -143,6 +149,9 @@ export type GoogleSheetsV2Response = | GoogleSheetsV2BatchUpdateResponse | GoogleSheetsV2BatchClearResponse | GoogleSheetsV2CopySheetResponse + | GoogleSheetsV2DeleteRowsResponse + | GoogleSheetsV2DeleteSheetResponse + | GoogleSheetsV2DeleteSpreadsheetResponse // V2 Clear Types export interface GoogleSheetsV2ClearParams { diff --git a/apps/sim/tools/google_tasks/create.ts b/apps/sim/tools/google_tasks/create.ts new file mode 100644 index 0000000000..d0d37eb494 --- /dev/null +++ b/apps/sim/tools/google_tasks/create.ts @@ -0,0 +1,133 @@ +import type { GoogleTasksCreateParams, GoogleTasksResponse } from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const createTool: ToolConfig = { + id: 'google_tasks_create', + name: 'Google Tasks Create Task', + description: 'Create a new task in a Google Tasks list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + taskListId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task list ID (defaults to primary task list "@default")', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Title of the task (max 1024 characters)', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Notes/description for the task (max 8192 characters)', + }, + due: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date in RFC 3339 format (e.g., 2025-06-03T00:00:00.000Z)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task status: "needsAction" or "completed"', + }, + parent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Parent task ID to create this task as a subtask. Omit for top-level tasks.', + }, + previous: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Previous sibling task ID to position after. Omit to place first among siblings.', + }, + }, + + request: { + url: (params) => { + const taskListId = params.taskListId || '@default' + const queryParams = new URLSearchParams() + if (params.parent) queryParams.set('parent', params.parent) + if (params.previous) queryParams.set('previous', params.previous) + const qs = queryParams.toString() + return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks${qs ? `?${qs}` : ''}` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + title: params.title, + } + if (params.notes) body.notes = params.notes + if (params.due) body.due = params.due + if (params.status) body.status = params.status + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message ?? 'Failed to create task') + } + + return { + success: true, + output: { + id: data.id ?? null, + title: data.title ?? null, + notes: data.notes ?? null, + status: data.status ?? null, + due: data.due ?? null, + updated: data.updated ?? null, + selfLink: data.selfLink ?? null, + webViewLink: data.webViewLink ?? null, + parent: data.parent ?? null, + position: data.position ?? null, + completed: data.completed ?? null, + deleted: data.deleted ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Task ID' }, + title: { type: 'string', description: 'Task title' }, + notes: { type: 'string', description: 'Task notes', optional: true }, + status: { type: 'string', description: 'Task status (needsAction or completed)' }, + due: { type: 'string', description: 'Due date', optional: true }, + updated: { type: 'string', description: 'Last modification time' }, + selfLink: { type: 'string', description: 'URL for the task' }, + webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true }, + parent: { type: 'string', description: 'Parent task ID', optional: true }, + position: { type: 'string', description: 'Position among sibling tasks' }, + completed: { type: 'string', description: 'Completion date', optional: true }, + deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true }, + }, +} diff --git a/apps/sim/tools/google_tasks/delete.ts b/apps/sim/tools/google_tasks/delete.ts new file mode 100644 index 0000000000..06a8559117 --- /dev/null +++ b/apps/sim/tools/google_tasks/delete.ts @@ -0,0 +1,67 @@ +import type { GoogleTasksDeleteParams, GoogleTasksDeleteResponse } from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'google_tasks_delete', + name: 'Google Tasks Delete Task', + description: 'Delete a task from a Google Tasks list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + taskListId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task list ID (defaults to primary task list "@default")', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to delete', + }, + }, + + request: { + url: (params) => { + const taskListId = params.taskListId || '@default' + return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params) => { + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + taskId: params?.taskId || '', + deleted: true, + }, + } + } + + const data = await response.json() + throw new Error(data.error?.message ?? 'Failed to delete task') + }, + + outputs: { + taskId: { type: 'string', description: 'Deleted task ID' }, + deleted: { type: 'boolean', description: 'Whether deletion was successful' }, + }, +} diff --git a/apps/sim/tools/google_tasks/get.ts b/apps/sim/tools/google_tasks/get.ts new file mode 100644 index 0000000000..60b5bf74d2 --- /dev/null +++ b/apps/sim/tools/google_tasks/get.ts @@ -0,0 +1,88 @@ +import type { GoogleTasksGetParams, GoogleTasksResponse } from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const getTool: ToolConfig = { + id: 'google_tasks_get', + name: 'Google Tasks Get Task', + description: 'Retrieve a specific task by ID from a Google Tasks list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + taskListId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task list ID (defaults to primary task list "@default")', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to retrieve', + }, + }, + + request: { + url: (params) => { + const taskListId = params.taskListId || '@default' + return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message ?? 'Failed to get task') + } + + return { + success: true, + output: { + id: data.id ?? null, + title: data.title ?? null, + notes: data.notes ?? null, + status: data.status ?? null, + due: data.due ?? null, + updated: data.updated ?? null, + selfLink: data.selfLink ?? null, + webViewLink: data.webViewLink ?? null, + parent: data.parent ?? null, + position: data.position ?? null, + completed: data.completed ?? null, + deleted: data.deleted ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Task ID' }, + title: { type: 'string', description: 'Task title' }, + notes: { type: 'string', description: 'Task notes', optional: true }, + status: { type: 'string', description: 'Task status (needsAction or completed)' }, + due: { type: 'string', description: 'Due date', optional: true }, + updated: { type: 'string', description: 'Last modification time' }, + selfLink: { type: 'string', description: 'URL for the task' }, + webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true }, + parent: { type: 'string', description: 'Parent task ID', optional: true }, + position: { type: 'string', description: 'Position among sibling tasks' }, + completed: { type: 'string', description: 'Completion date', optional: true }, + deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true }, + }, +} diff --git a/apps/sim/tools/google_tasks/index.ts b/apps/sim/tools/google_tasks/index.ts new file mode 100644 index 0000000000..07e0406abc --- /dev/null +++ b/apps/sim/tools/google_tasks/index.ts @@ -0,0 +1,6 @@ +export { createTool as googleTasksCreateTool } from '@/tools/google_tasks/create' +export { deleteTool as googleTasksDeleteTool } from '@/tools/google_tasks/delete' +export { getTool as googleTasksGetTool } from '@/tools/google_tasks/get' +export { listTool as googleTasksListTool } from '@/tools/google_tasks/list' +export { listTaskListsTool as googleTasksListTaskListsTool } from '@/tools/google_tasks/list_task_lists' +export { updateTool as googleTasksUpdateTool } from '@/tools/google_tasks/update' diff --git a/apps/sim/tools/google_tasks/list.ts b/apps/sim/tools/google_tasks/list.ts new file mode 100644 index 0000000000..1277b70ec2 --- /dev/null +++ b/apps/sim/tools/google_tasks/list.ts @@ -0,0 +1,214 @@ +import type { GoogleTasksListParams, GoogleTasksListResponse } from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const listTool: ToolConfig = { + id: 'google_tasks_list', + name: 'Google Tasks List Tasks', + description: 'List all tasks in a Google Tasks list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + taskListId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task list ID (defaults to primary task list "@default")', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of tasks to return (default 20, max 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + showCompleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to show completed tasks (default true)', + }, + showDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to show deleted tasks (default false)', + }, + showHidden: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to show hidden tasks (default false)', + }, + dueMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lower bound for due date filter (RFC 3339 timestamp)', + }, + dueMax: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Upper bound for due date filter (RFC 3339 timestamp)', + }, + completedMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lower bound for task completion date (RFC 3339 timestamp)', + }, + completedMax: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Upper bound for task completion date (RFC 3339 timestamp)', + }, + updatedMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lower bound for last modification time (RFC 3339 timestamp)', + }, + }, + + request: { + url: (params) => { + const taskListId = params.taskListId || '@default' + const queryParams = new URLSearchParams() + if (params.maxResults) queryParams.set('maxResults', String(params.maxResults)) + if (params.pageToken) queryParams.set('pageToken', params.pageToken) + if (params.showCompleted !== undefined) + queryParams.set('showCompleted', String(params.showCompleted)) + if (params.showDeleted !== undefined) + queryParams.set('showDeleted', String(params.showDeleted)) + if (params.showHidden !== undefined) queryParams.set('showHidden', String(params.showHidden)) + if (params.dueMin) queryParams.set('dueMin', params.dueMin) + if (params.dueMax) queryParams.set('dueMax', params.dueMax) + if (params.completedMin) queryParams.set('completedMin', params.completedMin) + if (params.completedMax) queryParams.set('completedMax', params.completedMax) + if (params.updatedMin) queryParams.set('updatedMin', params.updatedMin) + const qs = queryParams.toString() + return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message ?? 'Failed to list tasks') + } + + const items = data.items ?? [] + + return { + success: true, + output: { + tasks: items.map((item: Record) => ({ + id: (item.id as string) ?? null, + title: (item.title as string) ?? null, + notes: (item.notes as string) ?? null, + status: (item.status as string) ?? null, + due: (item.due as string) ?? null, + completed: (item.completed as string) ?? null, + updated: (item.updated as string) ?? null, + selfLink: (item.selfLink as string) ?? null, + webViewLink: (item.webViewLink as string) ?? null, + parent: (item.parent as string) ?? null, + position: (item.position as string) ?? null, + hidden: (item.hidden as boolean) ?? null, + deleted: (item.deleted as boolean) ?? null, + links: Array.isArray(item.links) + ? (item.links as Array>).map((link) => ({ + type: link.type ?? '', + description: link.description ?? '', + link: link.link ?? '', + })) + : [], + })), + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + tasks: { + type: 'array', + description: 'List of tasks', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task identifier' }, + title: { type: 'string', description: 'Title of the task' }, + notes: { type: 'string', description: 'Notes/description for the task', optional: true }, + status: { + type: 'string', + description: 'Task status: "needsAction" or "completed"', + }, + due: { type: 'string', description: 'Due date (RFC 3339 timestamp)', optional: true }, + completed: { + type: 'string', + description: 'Completion date (RFC 3339 timestamp)', + optional: true, + }, + updated: { type: 'string', description: 'Last modification time (RFC 3339 timestamp)' }, + selfLink: { type: 'string', description: 'URL pointing to this task' }, + webViewLink: { + type: 'string', + description: 'Link to task in Google Tasks UI', + optional: true, + }, + parent: { type: 'string', description: 'Parent task identifier', optional: true }, + position: { + type: 'string', + description: 'Position among sibling tasks (string-based ordering)', + }, + hidden: { type: 'boolean', description: 'Whether the task is hidden', optional: true }, + deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true }, + links: { + type: 'array', + description: 'Collection of links associated with the task', + optional: true, + items: { + type: 'object', + properties: { + type: { + type: 'string', + description: 'Link type (e.g., "email", "generic", "chat_message")', + }, + description: { type: 'string', description: 'Link description' }, + link: { type: 'string', description: 'The URL' }, + }, + }, + }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_tasks/list_task_lists.ts b/apps/sim/tools/google_tasks/list_task_lists.ts new file mode 100644 index 0000000000..9ab7faf24f --- /dev/null +++ b/apps/sim/tools/google_tasks/list_task_lists.ts @@ -0,0 +1,103 @@ +import type { + GoogleTasksListTaskListsParams, + GoogleTasksListTaskListsResponse, +} from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const listTaskListsTool: ToolConfig< + GoogleTasksListTaskListsParams, + GoogleTasksListTaskListsResponse +> = { + id: 'google_tasks_list_task_lists', + name: 'Google Tasks List Task Lists', + description: 'Retrieve all task lists for the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of task lists to return (default 20, max 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.maxResults) queryParams.set('maxResults', String(params.maxResults)) + if (params.pageToken) queryParams.set('pageToken', params.pageToken) + const qs = queryParams.toString() + return `${TASKS_API_BASE}/users/@me/lists${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message ?? 'Failed to list task lists') + } + + const items = data.items ?? [] + + return { + success: true, + output: { + taskLists: items.map((item: Record) => ({ + id: (item.id as string) ?? null, + title: (item.title as string) ?? null, + updated: (item.updated as string) ?? null, + selfLink: (item.selfLink as string) ?? null, + })), + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + taskLists: { + type: 'array', + description: 'List of task lists', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task list identifier' }, + title: { type: 'string', description: 'Title of the task list' }, + updated: { + type: 'string', + description: 'Last modification time (RFC 3339 timestamp)', + }, + selfLink: { type: 'string', description: 'URL pointing to this task list' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_tasks/types.ts b/apps/sim/tools/google_tasks/types.ts new file mode 100644 index 0000000000..2ef4daff88 --- /dev/null +++ b/apps/sim/tools/google_tasks/types.ts @@ -0,0 +1,113 @@ +import type { ToolResponse } from '@/tools/types' + +export const TASKS_API_BASE = 'https://tasks.googleapis.com/tasks/v1' + +export interface BaseGoogleTasksParams { + accessToken: string +} + +export interface GoogleTasksListTaskListsParams extends BaseGoogleTasksParams { + maxResults?: number + pageToken?: string +} + +export interface GoogleTasksCreateParams extends BaseGoogleTasksParams { + taskListId?: string + title: string + notes?: string + due?: string + status?: string + parent?: string + previous?: string +} + +export interface GoogleTasksListParams extends BaseGoogleTasksParams { + taskListId?: string + maxResults?: number + pageToken?: string + showCompleted?: boolean + showDeleted?: boolean + showHidden?: boolean + dueMin?: string + dueMax?: string + completedMin?: string + completedMax?: string + updatedMin?: string +} + +export interface GoogleTasksGetParams extends BaseGoogleTasksParams { + taskListId?: string + taskId: string +} + +export interface GoogleTasksUpdateParams extends BaseGoogleTasksParams { + taskListId?: string + taskId: string + title?: string + notes?: string + due?: string + status?: string +} + +export interface GoogleTasksDeleteParams extends BaseGoogleTasksParams { + taskListId?: string + taskId: string +} + +export interface GoogleTasksResponse extends ToolResponse { + output: { + id: string | null + title: string | null + notes: string | null + status: string | null + due: string | null + updated: string | null + selfLink: string | null + webViewLink: string | null + parent: string | null + position: string | null + completed: string | null + deleted: boolean | null + } +} + +export interface GoogleTasksListResponse extends ToolResponse { + output: { + tasks: Array<{ + id: string | null + title: string | null + notes: string | null + status: string | null + due: string | null + completed: string | null + updated: string | null + selfLink: string | null + webViewLink: string | null + parent: string | null + position: string | null + hidden: boolean | null + deleted: boolean | null + links: Array<{ type: string; description: string; link: string }> + }> + nextPageToken: string | null + } +} + +export interface GoogleTasksListTaskListsResponse extends ToolResponse { + output: { + taskLists: Array<{ + id: string | null + title: string | null + updated: string | null + selfLink: string | null + }> + nextPageToken: string | null + } +} + +export interface GoogleTasksDeleteResponse extends ToolResponse { + output: { + taskId: string + deleted: boolean + } +} diff --git a/apps/sim/tools/google_tasks/update.ts b/apps/sim/tools/google_tasks/update.ts new file mode 100644 index 0000000000..90d6d942eb --- /dev/null +++ b/apps/sim/tools/google_tasks/update.ts @@ -0,0 +1,121 @@ +import type { GoogleTasksResponse, GoogleTasksUpdateParams } from '@/tools/google_tasks/types' +import { TASKS_API_BASE } from '@/tools/google_tasks/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'google_tasks_update', + name: 'Google Tasks Update Task', + description: 'Update an existing task in a Google Tasks list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-tasks', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Google Tasks OAuth access token', + }, + taskListId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Task list ID (defaults to primary task list "@default")', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the task', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New notes for the task', + }, + due: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New due date in RFC 3339 format', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New status: "needsAction" or "completed"', + }, + }, + + request: { + url: (params) => { + const taskListId = params.taskListId || '@default' + return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.title !== undefined) body.title = params.title + if (params.notes !== undefined) body.notes = params.notes + if (params.due !== undefined) body.due = params.due + if (params.status !== undefined) body.status = params.status + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message ?? 'Failed to update task') + } + + return { + success: true, + output: { + id: data.id ?? null, + title: data.title ?? null, + notes: data.notes ?? null, + status: data.status ?? null, + due: data.due ?? null, + updated: data.updated ?? null, + selfLink: data.selfLink ?? null, + webViewLink: data.webViewLink ?? null, + parent: data.parent ?? null, + position: data.position ?? null, + completed: data.completed ?? null, + deleted: data.deleted ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Task ID' }, + title: { type: 'string', description: 'Task title' }, + notes: { type: 'string', description: 'Task notes', optional: true }, + status: { type: 'string', description: 'Task status (needsAction or completed)' }, + due: { type: 'string', description: 'Due date', optional: true }, + updated: { type: 'string', description: 'Last modification time' }, + selfLink: { type: 'string', description: 'URL for the task' }, + webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true }, + parent: { type: 'string', description: 'Parent task ID', optional: true }, + position: { type: 'string', description: 'Position among sibling tasks' }, + completed: { type: 'string', description: 'Completion date', optional: true }, + deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true }, + }, +} diff --git a/apps/sim/tools/google_translate/detect.ts b/apps/sim/tools/google_translate/detect.ts new file mode 100644 index 0000000000..48941276f3 --- /dev/null +++ b/apps/sim/tools/google_translate/detect.ts @@ -0,0 +1,82 @@ +import type { + GoogleTranslateDetectParams, + GoogleTranslateDetectResponse, +} from '@/tools/google_translate/types' +import type { ToolConfig } from '@/tools/types' + +export const googleTranslateDetectTool: ToolConfig< + GoogleTranslateDetectParams, + GoogleTranslateDetectResponse +> = { + id: 'google_translate_detect', + name: 'Google Translate Detect Language', + description: 'Detect the language of text using the Google Cloud Translation API.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Cloud API key with Cloud Translation API enabled', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text to detect the language of', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://translation.googleapis.com/language/translate/v2/detect') + url.searchParams.set('key', params.apiKey) + return url.toString() + }, + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + q: params.text, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + language: '', + confidence: null, + }, + error: data.error.message ?? 'Google Translate API error', + } + } + + const detection = data.data?.detections?.[0]?.[0] + + return { + success: true, + output: { + language: detection?.language ?? '', + confidence: detection?.confidence ?? null, + }, + } + }, + + outputs: { + language: { + type: 'string', + description: 'The detected language code (e.g., "en", "es", "fr")', + }, + confidence: { + type: 'number', + description: 'Confidence score of the detection', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_translate/index.ts b/apps/sim/tools/google_translate/index.ts new file mode 100644 index 0000000000..ddc89124f7 --- /dev/null +++ b/apps/sim/tools/google_translate/index.ts @@ -0,0 +1,4 @@ +import { googleTranslateDetectTool } from './detect' +import { googleTranslateTool } from './text' + +export { googleTranslateDetectTool, googleTranslateTool } diff --git a/apps/sim/tools/google_translate/text.ts b/apps/sim/tools/google_translate/text.ts new file mode 100644 index 0000000000..8f74df318c --- /dev/null +++ b/apps/sim/tools/google_translate/text.ts @@ -0,0 +1,102 @@ +import type { GoogleTranslateParams, GoogleTranslateResponse } from '@/tools/google_translate/types' +import type { ToolConfig } from '@/tools/types' + +export const googleTranslateTool: ToolConfig = { + id: 'google_translate_text', + name: 'Google Translate', + description: + 'Translate text between languages using the Google Cloud Translation API. Supports auto-detection of the source language.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Cloud API key with Cloud Translation API enabled', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text to translate', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Target language code (e.g., "es", "fr", "de", "ja")', + }, + source: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Source language code. If omitted, the API will auto-detect the source language.', + }, + format: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Format of the text: "text" for plain text, "html" for HTML content', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://translation.googleapis.com/language/translate/v2') + url.searchParams.set('key', params.apiKey) + return url.toString() + }, + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + q: params.text, + target: params.target, + } + if (params.source) body.source = params.source + if (params.format) body.format = params.format + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + translatedText: '', + detectedSourceLanguage: null, + }, + error: data.error.message ?? 'Google Translate API error', + } + } + + const translation = data.data?.translations?.[0] + + return { + success: true, + output: { + translatedText: translation?.translatedText ?? '', + detectedSourceLanguage: translation?.detectedSourceLanguage ?? null, + }, + } + }, + + outputs: { + translatedText: { + type: 'string', + description: 'The translated text', + }, + detectedSourceLanguage: { + type: 'string', + description: 'The detected source language code (if source was not specified)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_translate/types.ts b/apps/sim/tools/google_translate/types.ts new file mode 100644 index 0000000000..2ceffd18f5 --- /dev/null +++ b/apps/sim/tools/google_translate/types.ts @@ -0,0 +1,28 @@ +import type { ToolResponse } from '@/tools/types' + +export interface GoogleTranslateParams { + apiKey: string + text: string + target: string + source?: string + format?: 'text' | 'html' +} + +export interface GoogleTranslateResponse extends ToolResponse { + output: { + translatedText: string + detectedSourceLanguage: string | null + } +} + +export interface GoogleTranslateDetectParams { + apiKey: string + text: string +} + +export interface GoogleTranslateDetectResponse extends ToolResponse { + output: { + language: string + confidence: number | null + } +} diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 9426d73103..8184cf7064 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -578,6 +578,25 @@ function isErrorResponse( return { isError: false } } +/** + * Checks whether a fully resolved URL points back to this Sim instance. + * Used to propagate cycle-detection headers on API blocks that target + * the platform's own workflow execution endpoints via absolute URL. + */ +function isSelfOriginUrl(url: string): boolean { + try { + const targetOrigin = new URL(url).origin + const publicOrigin = new URL(getBaseUrl()).origin + if (targetOrigin === publicOrigin) return true + + const internalOrigin = new URL(getInternalApiBaseUrl()).origin + if (targetOrigin === internalOrigin) return true + } catch { + return false + } + return false +} + /** * Add internal authentication token to headers if running on server * @param headers - Headers object to modify @@ -737,7 +756,8 @@ async function executeToolRequest( const headers = new Headers(requestParams.headers) await addInternalAuthIfNeeded(headers, isInternalRoute, requestId, toolId) - if (isInternalRoute) { + const shouldPropagateCallChain = isInternalRoute || isSelfOriginUrl(fullUrl) + if (shouldPropagateCallChain) { const callChain = params._context?.callChain as string[] | undefined if (callChain && callChain.length > 0) { headers.set(SIM_VIA_HEADER, serializeCallChain(callChain)) @@ -1123,6 +1143,12 @@ async function executeMcpTool( const workspaceId = params._context?.workspaceId || executionContext?.workspaceId const workflowId = params._context?.workflowId || executionContext?.workflowId const userId = params._context?.userId || executionContext?.userId + const callChain = + (params._context?.callChain as string[] | undefined) || executionContext?.callChain + + if (callChain && callChain.length > 0) { + headers[SIM_VIA_HEADER] = serializeCallChain(callChain) + } if (!workspaceId) { return { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a2f5787c7..d8302770c3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -190,17 +190,25 @@ import { confluenceCreateCommentTool, confluenceCreatePagePropertyTool, confluenceCreatePageTool, + confluenceCreateSpacePropertyTool, + confluenceCreateSpaceTool, confluenceDeleteAttachmentTool, + confluenceDeleteBlogPostTool, confluenceDeleteCommentTool, confluenceDeleteLabelTool, confluenceDeletePagePropertyTool, confluenceDeletePageTool, + confluenceDeleteSpacePropertyTool, + confluenceDeleteSpaceTool, confluenceGetBlogPostTool, confluenceGetPageAncestorsTool, confluenceGetPageChildrenTool, + confluenceGetPageDescendantsTool, confluenceGetPagesByLabelTool, confluenceGetPageVersionTool, confluenceGetSpaceTool, + confluenceGetTaskTool, + confluenceGetUserTool, confluenceListAttachmentsTool, confluenceListBlogPostsInSpaceTool, confluenceListBlogPostsTool, @@ -210,11 +218,17 @@ import { confluenceListPagesInSpaceTool, confluenceListPageVersionsTool, confluenceListSpaceLabelsTool, + confluenceListSpacePermissionsTool, + confluenceListSpacePropertiesTool, confluenceListSpacesTool, + confluenceListTasksTool, confluenceRetrieveTool, confluenceSearchInSpaceTool, confluenceSearchTool, + confluenceUpdateBlogPostTool, confluenceUpdateCommentTool, + confluenceUpdateSpaceTool, + confluenceUpdateTaskTool, confluenceUpdateTool, confluenceUploadAttachmentTool, } from '@/tools/confluence' @@ -248,6 +262,12 @@ import { datadogSendLogsTool, datadogSubmitMetricsTool, } from '@/tools/datadog' +import { + devinCreateSessionTool, + devinGetSessionTool, + devinListSessionsTool, + devinSendMessageTool, +} from '@/tools/devin' import { discordAddReactionTool, discordArchiveThreadTool, @@ -578,10 +598,18 @@ import { gmailAddLabelV2Tool, gmailArchiveTool, gmailArchiveV2Tool, + gmailCreateLabelV2Tool, + gmailDeleteDraftV2Tool, + gmailDeleteLabelV2Tool, gmailDeleteTool, gmailDeleteV2Tool, gmailDraftTool, gmailDraftV2Tool, + gmailGetDraftV2Tool, + gmailGetThreadV2Tool, + gmailListDraftsV2Tool, + gmailListLabelsV2Tool, + gmailListThreadsV2Tool, gmailMarkReadTool, gmailMarkReadV2Tool, gmailMarkUnreadTool, @@ -596,8 +624,10 @@ import { gmailSearchV2Tool, gmailSendTool, gmailSendV2Tool, + gmailTrashThreadV2Tool, gmailUnarchiveTool, gmailUnarchiveV2Tool, + gmailUntrashThreadV2Tool, } from '@/tools/gmail' import { gongAggregateActivityTool, @@ -620,12 +650,21 @@ import { gongLookupPhoneTool, } from '@/tools/gong' import { googleSearchTool } from '@/tools/google' +import { + googleBigQueryGetTableTool, + googleBigQueryInsertRowsTool, + googleBigQueryListDatasetsTool, + googleBigQueryListTablesTool, + googleBigQueryQueryTool, +} from '@/tools/google_bigquery' import { googleBooksVolumeDetailsTool, googleBooksVolumeSearchTool } from '@/tools/google_books' import { googleCalendarCreateTool, googleCalendarCreateV2Tool, googleCalendarDeleteTool, googleCalendarDeleteV2Tool, + googleCalendarFreeBusyTool, + googleCalendarFreeBusyV2Tool, googleCalendarGetTool, googleCalendarGetV2Tool, googleCalendarInstancesTool, @@ -654,6 +693,8 @@ import { googleDriveGetFileTool, googleDriveListPermissionsTool, googleDriveListTool, + googleDriveMoveTool, + googleDriveSearchTool, googleDriveShareTool, googleDriveTrashTool, googleDriveUnshareTool, @@ -714,6 +755,9 @@ import { googleSheetsClearV2Tool, googleSheetsCopySheetV2Tool, googleSheetsCreateSpreadsheetV2Tool, + googleSheetsDeleteRowsV2Tool, + googleSheetsDeleteSheetV2Tool, + googleSheetsDeleteSpreadsheetV2Tool, googleSheetsGetSpreadsheetV2Tool, googleSheetsReadTool, googleSheetsReadV2Tool, @@ -738,6 +782,15 @@ import { googleSlidesUpdateSlidesPositionTool, googleSlidesWriteTool, } from '@/tools/google_slides' +import { + googleTasksCreateTool, + googleTasksDeleteTool, + googleTasksGetTool, + googleTasksListTaskListsTool, + googleTasksListTool, + googleTasksUpdateTool, +} from '@/tools/google_tasks' +import { googleTranslateDetectTool, googleTranslateTool } from '@/tools/google_translate' import { createMattersExportTool, createMattersHoldsTool, @@ -2471,6 +2524,16 @@ export const tools: Record = { gmail_add_label_v2: gmailAddLabelV2Tool, gmail_remove_label: gmailRemoveLabelTool, gmail_remove_label_v2: gmailRemoveLabelV2Tool, + gmail_create_label_v2: gmailCreateLabelV2Tool, + gmail_delete_draft_v2: gmailDeleteDraftV2Tool, + gmail_delete_label_v2: gmailDeleteLabelV2Tool, + gmail_get_draft_v2: gmailGetDraftV2Tool, + gmail_get_thread_v2: gmailGetThreadV2Tool, + gmail_list_drafts_v2: gmailListDraftsV2Tool, + gmail_list_labels_v2: gmailListLabelsV2Tool, + gmail_list_threads_v2: gmailListThreadsV2Tool, + gmail_trash_thread_v2: gmailTrashThreadV2Tool, + gmail_untrash_thread_v2: gmailUntrashThreadV2Tool, whatsapp_send_message: whatsappSendMessageTool, x_write: xWriteTool, x_read: xReadTool, @@ -2528,6 +2591,10 @@ export const tools: Record = { dropbox_get_metadata: dropboxGetMetadataTool, dropbox_create_shared_link: dropboxCreateSharedLinkTool, dropbox_search: dropboxSearchTool, + devin_create_session: devinCreateSessionTool, + devin_get_session: devinGetSessionTool, + devin_list_sessions: devinListSessionsTool, + devin_send_message: devinSendMessageTool, duckduckgo_search: duckduckgoSearchTool, dspy_predict: predictTool, dspy_chain_of_thought: chainOfThoughtTool, @@ -2867,6 +2934,8 @@ export const tools: Record = { google_drive_get_file: googleDriveGetFileTool, google_drive_list: googleDriveListTool, google_drive_list_permissions: googleDriveListPermissionsTool, + google_drive_move: googleDriveMoveTool, + google_drive_search: googleDriveSearchTool, google_drive_share: googleDriveShareTool, google_drive_trash: googleDriveTrashTool, google_drive_unshare: googleDriveUnshareTool, @@ -2891,6 +2960,14 @@ export const tools: Record = { google_maps_speed_limits: googleMapsSpeedLimitsTool, google_maps_timezone: googleMapsTimezoneTool, google_maps_validate_address: googleMapsValidateAddressTool, + google_tasks_create: googleTasksCreateTool, + google_tasks_delete: googleTasksDeleteTool, + google_tasks_get: googleTasksGetTool, + google_tasks_list: googleTasksListTool, + google_tasks_list_task_lists: googleTasksListTaskListsTool, + google_tasks_update: googleTasksUpdateTool, + google_translate_detect: googleTranslateDetectTool, + google_translate_text: googleTranslateTool, google_sheets_read: googleSheetsReadTool, google_sheets_write: googleSheetsWriteTool, google_sheets_update: googleSheetsUpdateTool, @@ -2906,6 +2983,9 @@ export const tools: Record = { google_sheets_batch_update_v2: googleSheetsBatchUpdateV2Tool, google_sheets_batch_clear_v2: googleSheetsBatchClearV2Tool, google_sheets_copy_sheet_v2: googleSheetsCopySheetV2Tool, + google_sheets_delete_rows_v2: googleSheetsDeleteRowsV2Tool, + google_sheets_delete_sheet_v2: googleSheetsDeleteSheetV2Tool, + google_sheets_delete_spreadsheet_v2: googleSheetsDeleteSpreadsheetV2Tool, google_slides_read: googleSlidesReadTool, google_slides_write: googleSlidesWriteTool, google_slides_create: googleSlidesCreateTool, @@ -2997,8 +3077,22 @@ export const tools: Record = { confluence_list_space_labels: confluenceListSpaceLabelsTool, confluence_delete_label: confluenceDeleteLabelTool, confluence_delete_page_property: confluenceDeletePagePropertyTool, + confluence_get_page_descendants: confluenceGetPageDescendantsTool, confluence_get_space: confluenceGetSpaceTool, + confluence_create_space: confluenceCreateSpaceTool, + confluence_update_space: confluenceUpdateSpaceTool, + confluence_delete_space: confluenceDeleteSpaceTool, + confluence_get_user: confluenceGetUserTool, confluence_list_spaces: confluenceListSpacesTool, + confluence_update_blogpost: confluenceUpdateBlogPostTool, + confluence_delete_blogpost: confluenceDeleteBlogPostTool, + confluence_list_tasks: confluenceListTasksTool, + confluence_get_task: confluenceGetTaskTool, + confluence_update_task: confluenceUpdateTaskTool, + confluence_list_space_permissions: confluenceListSpacePermissionsTool, + confluence_list_space_properties: confluenceListSpacePropertiesTool, + confluence_create_space_property: confluenceCreateSpacePropertyTool, + confluence_delete_space_property: confluenceDeleteSpacePropertyTool, cursor_list_agents: cursorListAgentsTool, cursor_list_agents_v2: cursorListAgentsV2Tool, cursor_get_agent: cursorGetAgentTool, @@ -3505,6 +3599,8 @@ export const tools: Record = { google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool, google_calendar_update: googleCalendarUpdateTool, google_calendar_update_v2: googleCalendarUpdateV2Tool, + google_calendar_freebusy: googleCalendarFreeBusyTool, + google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool, google_forms_get_responses: googleFormsGetResponsesTool, google_forms_get_form: googleFormsGetFormTool, google_forms_create_form: googleFormsCreateFormTool, @@ -3556,6 +3652,11 @@ export const tools: Record = { wordpress_list_users: wordpressListUsersTool, wordpress_get_user: wordpressGetUserTool, wordpress_search_content: wordpressSearchContentTool, + google_bigquery_query: googleBigQueryQueryTool, + google_bigquery_list_datasets: googleBigQueryListDatasetsTool, + google_bigquery_list_tables: googleBigQueryListTablesTool, + google_bigquery_get_table: googleBigQueryGetTableTool, + google_bigquery_insert_rows: googleBigQueryInsertRowsTool, google_vault_create_matters_export: createMattersExportTool, google_vault_list_matters_export: listMattersExportTool, google_vault_create_matters_holds: createMattersHoldsTool, diff --git a/bun.lock b/bun.lock index d0b2fce86d..3d4fc92887 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.10", + "turbo": "2.8.11", }, }, "apps/docs": { @@ -3437,19 +3437,19 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.8.10", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.10", "turbo-darwin-arm64": "2.8.10", "turbo-linux-64": "2.8.10", "turbo-linux-arm64": "2.8.10", "turbo-windows-64": "2.8.10", "turbo-windows-arm64": "2.8.10" }, "bin": { "turbo": "bin/turbo" } }, "sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ=="], + "turbo": ["turbo@2.8.11", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.11", "turbo-darwin-arm64": "2.8.11", "turbo-linux-64": "2.8.11", "turbo-linux-arm64": "2.8.11", "turbo-windows-64": "2.8.11", "turbo-windows-arm64": "2.8.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-H+rwSHHPLoyPOSoHdmI1zY0zy0GGj1Dmr7SeJW+nZiWLz2nex8EJ+fkdVabxXFMNEux+aywI4Sae8EqhmnOv4A=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-XKaCWaz4OCt77oYYvGCIRpvYD4c/aNaKjRkUpv+e8rN3RZb+5Xsyew4yRO+gaHdMIUhQznXNXfHlhs+/p7lIhA=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VvynLHGUNvQ9k7GZjRPSsRcK4VkioTfFb7O7liAk4nHKjEcMdls7GqxzjVWgJiKz3hWmQGaP9hRa9UUnhVWCxA=="], - "turbo-linux-64": ["turbo-linux-64@2.8.10", "", { "os": "linux", "cpu": "x64" }, "sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA=="], + "turbo-linux-64": ["turbo-linux-64@2.8.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cbSn37dcm+EmkQ7DD0euy7xV7o2el4GAOr1XujvkAyKjjNvQ+6QIUeDgQcwAx3D17zPpDvfDMJY2dLQadWnkmQ=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+trymp2s2aBrhS04l6qFxcExzZ8ffndevuUB9c5RCeqsVpZeiWuGQlWNm5XjOmzoMayxRARZ5ma7yiWbGMiLqQ=="], - "turbo-windows-64": ["turbo-windows-64@2.8.10", "", { "os": "win32", "cpu": "x64" }, "sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw=="], + "turbo-windows-64": ["turbo-windows-64@2.8.11", "", { "os": "win32", "cpu": "x64" }, "sha512-3kJjFSM4yw1n9Uzmi+XkAUgCae19l/bH6RJ442xo7mnZm0tpOjo33F+FYHoSVpIWVMd0HG0LDccyafPSdylQbA=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOM4uF2vuLsJUvibdR6X9QqdZr6BhC6Nhlrw4LKFPsXZZI/9HHLoqAiYRpE4MuzIwldCH/jVySnWXrI1SKto0g=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], diff --git a/package.json b/package.json index 55e9fe8fdc..d4a932a341 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.10" + "turbo": "2.8.11" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [ diff --git a/packages/db/migrations/0160_classy_tyger_tiger.sql b/packages/db/migrations/0160_classy_tyger_tiger.sql new file mode 100644 index 0000000000..42ac6a9eac --- /dev/null +++ b/packages/db/migrations/0160_classy_tyger_tiger.sql @@ -0,0 +1,9 @@ +DROP INDEX "a2a_agent_workspace_id_idx";--> statement-breakpoint +DROP INDEX "a2a_push_notification_config_task_id_idx";--> statement-breakpoint +DROP INDEX "credential_member_credential_id_idx";--> statement-breakpoint +DROP INDEX "credential_set_organization_id_idx";--> statement-breakpoint +DROP INDEX "credential_set_member_set_id_idx";--> statement-breakpoint +DROP INDEX "permission_group_organization_id_idx";--> statement-breakpoint +DROP INDEX "skill_workspace_id_idx";--> statement-breakpoint +DROP INDEX "user_table_rows_workspace_id_idx";--> statement-breakpoint +CREATE INDEX "workflow_execution_logs_running_started_at_idx" ON "workflow_execution_logs" USING btree ("started_at") WHERE status = 'running'; \ No newline at end of file diff --git a/packages/db/migrations/meta/0160_snapshot.json b/packages/db/migrations/meta/0160_snapshot.json new file mode 100644 index 0000000000..138aaa5675 --- /dev/null +++ b/packages/db/migrations/meta/0160_snapshot.json @@ -0,0 +1,12326 @@ +{ + "id": "f5c357ca-22f0-4813-892c-67840f87dc95", + "prevId": "4d7b5562-c797-4deb-9a19-7629437d701f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_attribution": { + "name": "referral_attribution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer_url": { + "name": "referrer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_attribution_user_id_idx": { + "name": "referral_attribution_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_org_unique_idx": { + "name": "referral_attribution_org_unique_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"referral_attribution\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_campaign_id_idx": { + "name": "referral_attribution_campaign_id_idx", + "columns": [ + { + "expression": "campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_campaign_idx": { + "name": "referral_attribution_utm_campaign_idx", + "columns": [ + { + "expression": "utm_campaign", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_content_idx": { + "name": "referral_attribution_utm_content_idx", + "columns": [ + { + "expression": "utm_content", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_created_at_idx": { + "name": "referral_attribution_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_attribution_user_id_user_id_fk": { + "name": "referral_attribution_user_id_user_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_attribution_organization_id_organization_id_fk": { + "name": "referral_attribution_organization_id_organization_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "referral_attribution_campaign_id_referral_campaigns_id_fk": { + "name": "referral_attribution_campaign_id_referral_campaigns_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "referral_campaigns", + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_attribution_user_id_unique": { + "name": "referral_attribution_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_campaigns": { + "name": "referral_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_campaigns_active_idx": { + "name": "referral_campaigns_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_campaigns_code_unique": { + "name": "referral_campaigns_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dark'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot", "mcp_copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 36fcc4f9c9..72367719e9 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1114,6 +1114,13 @@ "when": 1771897746619, "tag": "0159_magical_marten_broadcloak", "breakpoints": true + }, + { + "idx": 160, + "version": "7", + "when": 1772138479896, + "tag": "0160_classy_tyger_tiger", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index a64ef6a60b..3c699ad309 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -334,6 +334,9 @@ export const workflowExecutionLogs = pgTable( table.workspaceId, table.startedAt ), + runningStartedAtIdx: index('workflow_execution_logs_running_started_at_idx') + .on(table.startedAt) + .where(sql`status = 'running'`), }) ) @@ -812,7 +815,6 @@ export const skill = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - workspaceIdIdx: index('skill_workspace_id_idx').on(table.workspaceId), workspaceNameUnique: uniqueIndex('skill_workspace_name_unique').on( table.workspaceId, table.name @@ -1934,7 +1936,6 @@ export const a2aAgent = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - workspaceIdIdx: index('a2a_agent_workspace_id_idx').on(table.workspaceId), workflowIdIdx: index('a2a_agent_workflow_id_idx').on(table.workflowId), createdByIdx: index('a2a_agent_created_by_idx').on(table.createdBy), workspaceWorkflowUnique: uniqueIndex('a2a_agent_workspace_workflow_unique').on( @@ -2018,7 +2019,6 @@ export const a2aPushNotificationConfig = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - taskIdIdx: index('a2a_push_notification_config_task_id_idx').on(table.taskId), taskIdUnique: uniqueIndex('a2a_push_notification_config_task_unique').on(table.taskId), }) ) @@ -2173,7 +2173,6 @@ export const credentialMember = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - credentialIdIdx: index('credential_member_credential_id_idx').on(table.credentialId), userIdIdx: index('credential_member_user_id_idx').on(table.userId), roleIdx: index('credential_member_role_idx').on(table.role), statusIdx: index('credential_member_status_idx').on(table.status), @@ -2224,7 +2223,6 @@ export const credentialSet = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - organizationIdIdx: index('credential_set_organization_id_idx').on(table.organizationId), createdByIdx: index('credential_set_created_by_idx').on(table.createdBy), orgNameUnique: uniqueIndex('credential_set_org_name_unique').on( table.organizationId, @@ -2257,7 +2255,6 @@ export const credentialSetMember = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - credentialSetIdIdx: index('credential_set_member_set_id_idx').on(table.credentialSetId), userIdIdx: index('credential_set_member_user_id_idx').on(table.userId), uniqueMembership: uniqueIndex('credential_set_member_unique').on( table.credentialSetId, @@ -2320,7 +2317,6 @@ export const permissionGroup = pgTable( autoAddNewMembers: boolean('auto_add_new_members').notNull().default(false), }, (table) => ({ - organizationIdIdx: index('permission_group_organization_id_idx').on(table.organizationId), createdByIdx: index('permission_group_created_by_idx').on(table.createdBy), orgNameUnique: uniqueIndex('permission_group_org_name_unique').on( table.organizationId, @@ -2438,7 +2434,6 @@ export const userTableRows = pgTable( }, (table) => ({ tableIdIdx: index('user_table_rows_table_id_idx').on(table.tableId), - workspaceIdIdx: index('user_table_rows_workspace_id_idx').on(table.workspaceId), dataGinIdx: index('user_table_rows_data_gin_idx').using('gin', table.data), workspaceTableIdx: index('user_table_rows_workspace_table_idx').on( table.workspaceId, diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index d0f913c7fc..d31b812b9e 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -86,6 +86,8 @@ export const auditMock = { WORKFLOW_DEPLOYED: 'workflow.deployed', WORKFLOW_UNDEPLOYED: 'workflow.undeployed', WORKFLOW_DUPLICATED: 'workflow.duplicated', + WORKFLOW_LOCKED: 'workflow.locked', + WORKFLOW_UNLOCKED: 'workflow.unlocked', WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated', WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted', WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',