diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index c213134b0e1..b11ef7e3835 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1274,6 +1274,31 @@ export function xAIIcon(props: SVGProps) { ) } +export function TikTokIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function xIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 38effba830e..20bda3ccd47 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -213,6 +213,7 @@ import { TemporalIcon, TextractIcon, ThriveIcon, + TikTokIcon, TinybirdIcon, TrelloIcon, TriggerDevIcon, @@ -475,6 +476,7 @@ export const blockTypeToIconMap: Record = { textract: TextractIcon, textract_v2: TextractIcon, thrive: ThriveIcon, + tiktok: TikTokIcon, tinybird: TinybirdIcon, trello: TrelloIcon, trigger_dev: TriggerDevIcon, diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 85ab2e676bc..8e84c83439d 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -215,6 +215,7 @@ "temporal", "textract", "thrive", + "tiktok", "tinybird", "trello", "trigger_dev", diff --git a/apps/docs/content/docs/en/integrations/tiktok.mdx b/apps/docs/content/docs/en/integrations/tiktok.mdx new file mode 100644 index 00000000000..c90e7ae195a --- /dev/null +++ b/apps/docs/content/docs/en/integrations/tiktok.mdx @@ -0,0 +1,384 @@ +--- +title: TikTok +description: Access TikTok profiles and videos, and publish content +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +TikTok webhooks use a single app-level Callback URL configured in the [TikTok Developer Portal](https://developers.tiktok.com/): + +`https://www.sim.ai/api/webhooks/tiktok` + +(Use your environment’s `{APP_URL}/api/webhooks/tiktok` for staging or local ngrok.) You do not paste a unique per-workflow URL into TikTok. In Sim, connect a TikTok account on the trigger and deploy the workflow; events for that `user_openid` fan out to matching workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata. Publish videos and photos directly to TikTok (or send them to the inbox as drafts) from a public URL or a file uploaded in the workflow, then track post status. + + + +## Actions + +### `tiktok_get_user` + +Get the authenticated TikTok user profile information including display name, avatar, bio, follower count, and video statistics. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fields` | string | No | Comma-separated list of fields to return. Available: open_id, union_id, avatar_url, avatar_large_url, display_name, bio_description, profile_deep_link, is_verified, username, follower_count, following_count, likes_count, video_count. Include avatar_url and avatar_large_url to receive the avatarFile output. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `openId` | string | Unique TikTok user ID for this application | +| `unionId` | string | Unique TikTok user ID across all apps from the same developer | +| `displayName` | string | User display name | +| `bioDescription` | string | User bio description | +| `profileDeepLink` | string | Deep link to user TikTok profile | +| `isVerified` | boolean | Whether the account is verified | +| `username` | string | TikTok username | +| `followerCount` | number | Number of followers | +| `followingCount` | number | Number of accounts the user follows | +| `likesCount` | number | Total likes received across all videos | +| `videoCount` | number | Total number of public videos | +| `avatarFile` | file | Downloadable copy of the profile avatar image \(largest available variant\), stored as a workflow file so it can be chained into file-consuming blocks \(e.g. attached to an email\). | + +### `tiktok_list_videos` + +Get a list of the authenticated user's TikTok videos with cover images, titles, and metadata. Supports pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `maxCount` | number | No | Maximum number of videos to return \(1-20\) | +| `cursor` | number | No | Cursor for pagination \(from previous response\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `videos` | array | List of TikTok videos | +| ↳ `id` | string | Video ID | +| ↳ `title` | string | Video title | +| ↳ `coverImageUrl` | string | Signed cover image URL from TikTok CDN. Publicly fetchable without auth, but time-limited \(embeds an x-expires param\) — use it right away rather than storing it for later. | +| ↳ `embedLink` | string | Embeddable video URL | +| ↳ `duration` | number | Video duration in seconds | +| ↳ `createTime` | number | Unix timestamp when video was created | +| ↳ `shareUrl` | string | Shareable video URL | +| ↳ `videoDescription` | string | Video description/caption | +| ↳ `width` | number | Video width in pixels | +| ↳ `height` | number | Video height in pixels | +| ↳ `viewCount` | number | Number of views | +| ↳ `likeCount` | number | Number of likes | +| ↳ `commentCount` | number | Number of comments | +| ↳ `shareCount` | number | Number of shares | +| `cursor` | number | Cursor for fetching the next page of results | +| `hasMore` | boolean | Whether there are more videos to fetch | + +### `tiktok_query_videos` + +Query specific TikTok videos by their IDs to get fresh metadata including cover images, embed links, and video details. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `videoIds` | array | Yes | Array of video IDs to query \(maximum 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `videos` | array | List of queried TikTok videos | +| ↳ `id` | string | Video ID | +| ↳ `title` | string | Video title | +| ↳ `coverImageUrl` | string | Signed cover image URL from TikTok CDN. Publicly fetchable without auth, but time-limited \(embeds an x-expires param\) — use it right away rather than storing it for later. | +| ↳ `embedLink` | string | Embeddable video URL | +| ↳ `duration` | number | Video duration in seconds | +| ↳ `createTime` | number | Unix timestamp when video was created | +| ↳ `shareUrl` | string | Shareable video URL | +| ↳ `videoDescription` | string | Video description/caption | +| ↳ `width` | number | Video width in pixels | +| ↳ `height` | number | Video height in pixels | +| ↳ `viewCount` | number | Number of views | +| ↳ `likeCount` | number | Number of likes | +| ↳ `commentCount` | number | Number of comments | +| ↳ `shareCount` | number | Number of shares | + +### `tiktok_query_creator_info` + +Check if the authenticated TikTok user can post content and retrieve their available privacy options, interaction settings, and maximum video duration. Required before any post per TikTok UX guidelines. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `creatorAvatarUrl` | string | URL of the creator avatar | +| `creatorUsername` | string | TikTok username of the creator | +| `creatorNickname` | string | Display name/nickname of the creator | +| `privacyLevelOptions` | array | Available privacy levels for posting \(e.g., PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY\) | +| `commentDisabled` | boolean | Whether the creator has disabled comments by default | +| `duetDisabled` | boolean | Whether the creator has disabled duets by default | +| `stitchDisabled` | boolean | Whether the creator has disabled stitches by default | +| `maxVideoPostDurationSec` | number | Maximum allowed video duration in seconds | + +### `tiktok_direct_post_video` + +Publish a video to TikTok, either by public URL (TikTok fetches it) or by uploading a file from the workflow. Rate limit: 6 requests per minute per user. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `source` | string | Yes | Media transfer method: 'PULL_FROM_URL' or 'FILE_UPLOAD'. | +| `videoUrl` | string | No | Public URL of the video to post \(used when source is PULL_FROM_URL\). The domain/URL prefix must be verified in the TikTok developer portal. | +| `file` | file | No | Video file to upload from the workflow \(used when source is FILE_UPLOAD\). | +| `title` | string | No | Video caption/description. Maximum 2200 characters. | +| `privacyLevel` | string | Yes | Privacy level for the video. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Must match one of the privacyLevelOptions returned by Query Creator Info. Note: unaudited apps \(including sandbox apps\) are restricted to SELF_ONLY. | +| `disableDuet` | boolean | No | Disable duet for this video. Defaults to false. | +| `disableStitch` | boolean | No | Disable stitch for this video. Defaults to false. | +| `disableComment` | boolean | No | Disable comments for this video. Defaults to false. | +| `videoCoverTimestampMs` | number | No | Timestamp in milliseconds to use as the video cover image. | +| `isAigc` | boolean | No | Set to true if the video is AI-generated content \(AIGC\). | +| `brandContentToggle` | boolean | No | Set to true if the video is a paid partnership promoting a third-party business. Branded content cannot be posted with Only Me privacy. | +| `brandOrganicToggle` | boolean | No | Set to true if the video is promoting the creator's own business. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `publishId` | string | Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the video was successfully published. | + +### `tiktok_upload_video_draft` + +Send a video to the authenticated user's TikTok inbox (by public URL or uploaded file) for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Rate limit: 6 requests per minute per user. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `source` | string | Yes | Media transfer method: 'PULL_FROM_URL' or 'FILE_UPLOAD'. | +| `videoUrl` | string | No | Public URL of the video to upload \(used when source is PULL_FROM_URL\). The domain/URL prefix must be verified in the TikTok developer portal. | +| `file` | file | No | Video file to upload from the workflow \(used when source is FILE_UPLOAD\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `publishId` | string | Unique identifier for tracking the draft status. Use this with the Get Post Status tool to check when the user has completed posting from their inbox. | + +### `tiktok_direct_post_photo` + +Publish one or more photos to TikTok from public URLs. TikTok will fetch each image and post them as a photo carousel. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `photoImages` | array | Yes | Public, verified-domain URLs of the images to post \(up to 35\). Must be JPEG or WEBP — TikTok rejects PNG. | +| `photoCoverIndex` | number | No | Index \(starting from 0\) of the photo to use as the cover. | +| `title` | string | No | Post title. Maximum 90 characters. | +| `description` | string | No | Post description. Maximum 4000 characters. | +| `privacyLevel` | string | Yes | Privacy level for the post. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Must match one of the privacyLevelOptions returned by Query Creator Info. Note: unaudited apps \(including sandbox apps\) are restricted to SELF_ONLY. | +| `disableComment` | boolean | No | Disable comments for this post. Defaults to false. | +| `autoAddMusic` | boolean | No | Automatically add recommended music to the photo post. Defaults to false. | +| `brandContentToggle` | boolean | No | Set to true if the post is a paid partnership promoting a third-party business. Branded content cannot be posted with Only Me privacy. | +| `brandOrganicToggle` | boolean | No | Set to true if the post is promoting the creator's own business. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `publishId` | string | Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the photos were successfully published. | + +### `tiktok_upload_photo_draft` + +Send one or more photos (from public URLs) to the authenticated user's TikTok inbox for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `photoImages` | array | Yes | Public, verified-domain URLs of the images to upload \(up to 35\). Must be JPEG or WEBP — TikTok rejects PNG. | +| `photoCoverIndex` | number | No | Index \(starting from 0\) of the photo to use as the cover. | +| `title` | string | No | Post title. Maximum 90 characters. | +| `description` | string | No | Post description. Maximum 4000 characters. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `publishId` | string | Unique identifier for tracking the draft status. Use this with the Get Post Status tool to check when the user has completed posting from their inbox. | + +### `tiktok_get_post_status` + +Check the status of a post initiated with Direct Post Video, Upload Video Draft, Direct Post Photo, or Upload Photo Draft. Use the publishId returned from the post request to track progress. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `publishId` | string | Yes | The publish ID returned from a post/upload tool. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Current status of the post. Values: PROCESSING_UPLOAD/PROCESSING_DOWNLOAD \(TikTok is processing the media\), SEND_TO_USER_INBOX \(draft delivered, awaiting user action\), PUBLISH_COMPLETE \(successfully posted\), FAILED \(check failReason\). | +| `failReason` | string | Reason for failure if status is FAILED. Null otherwise. | +| `publiclyAvailablePostId` | array | Array of public post IDs \(as strings\) once the content is published and publicly viewable. Can be used to construct the TikTok post URL. | + + + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### TikTok Authorization Removed + +Trigger when a user deauthorizes your TikTok app + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event` | string | TikTok webhook event name | +| `createTime` | number | UTC epoch seconds when the event occurred | +| `userOpenId` | string | TikTok user open_id for the connected account | +| `clientKey` | string | TikTok app client_key that received the event | +| `reason` | number | Revocation reason \(0 unknown, 1 user disconnect, 2 account deleted, 3 age change, 4 banned, 5 developer revoke\) | + + +--- + +### TikTok Post Inbox Delivered + +Trigger when a draft notification is delivered to the creator inbox + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + + +--- + +### TikTok Post No Longer Public + +Trigger when a post is no longer publicly viewable on TikTok + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + + +--- + +### TikTok Post Publicly Available + +Trigger when a published post becomes publicly viewable on TikTok + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + + +--- + +### TikTok Post Publish Complete + +Trigger when a TikTok Content Posting publish completes + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + + +--- + +### TikTok Post Publish Failed + +Trigger when a TikTok Content Posting publish fails + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + + +--- + +### TikTok Video Publish Completed + +Trigger when a Share Kit / Video Kit upload is published by the user + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event` | string | TikTok webhook event name | +| `createTime` | number | UTC epoch seconds when the event occurred | +| `userOpenId` | string | TikTok user open_id for the connected account | +| `clientKey` | string | TikTok app client_key that received the event | +| `shareId` | string | Share Kit / Video Kit share_id | + + +--- + +### TikTok Video Upload Failed + +Trigger when a Share Kit / Video Kit upload fails in TikTok + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerCredentials` | string | Yes | TikTok Account | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event` | string | TikTok webhook event name | +| `createTime` | number | UTC epoch seconds when the event occurred | +| `userOpenId` | string | TikTok user open_id for the connected account | +| `clientKey` | string | TikTok app client_key that received the event | +| `shareId` | string | Share Kit / Video Kit share_id | + diff --git a/apps/sim/app/api/tools/jupyter/upload/route.ts b/apps/sim/app/api/tools/jupyter/upload/route.ts index 3c12cb21bbf..ff0c38862d9 100644 --- a/apps/sim/app/api/tools/jupyter/upload/route.ts +++ b/apps/sim/app/api/tools/jupyter/upload/route.ts @@ -18,6 +18,7 @@ import { buildJupyterAuthHeaders, encodeJupyterPath, normalizeJupyterServerUrl, + parseJupyterContentModel, UnsafeJupyterPathError, } from '@/tools/jupyter/utils' @@ -128,17 +129,29 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const uploaded = await response.json() + const uploaded = parseJupyterContentModel(await response.json()) + if (!uploaded) { + logger.error(`[${requestId}] Jupyter returned an invalid upload response`) + return NextResponse.json( + { success: false, error: 'Jupyter returned an invalid upload response' }, + { status: 502 } + ) + } + + const uploadedName = uploaded.name ?? fileName + const uploadedPath = uploaded.path ?? destinationPath + const uploadedSize = uploaded.size ?? fileBuffer.length + const lastModified = uploaded.lastModified ?? null - logger.info(`[${requestId}] File uploaded to Jupyter: ${uploaded.path}`) + logger.info(`[${requestId}] File uploaded to Jupyter: ${uploadedPath}`) return NextResponse.json({ success: true, output: { - name: uploaded.name ?? fileName, - path: uploaded.path ?? destinationPath, - size: uploaded.size ?? fileBuffer.length, - lastModified: uploaded.last_modified ?? null, + name: uploadedName, + path: uploadedPath, + size: uploadedSize, + lastModified, }, }) } catch (error) { diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.ts b/apps/sim/app/api/tools/tiktok/publish-video/route.ts new file mode 100644 index 00000000000..5de5d53bee5 --- /dev/null +++ b/apps/sim/app/api/tools/tiktok/publish-video/route.ts @@ -0,0 +1,220 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { tiktokPublishVideoContract } from '@/lib/api/contracts/tiktok-tools' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + getFileExtension, + getMimeTypeFromExtension, + processSingleFileToUserFile, +} from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' +import type { UserFile } from '@/executor/types' +import { tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' +import { readTikTokApiResponse } from '@/tools/tiktok/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TikTokPublishVideoAPI') + +const TIKTOK_VIDEO_MIME_TYPES = new Set(['video/mp4', 'video/quicktime', 'video/webm']) + +/** TikTok requires each chunk between 5MB and 64MB; the final chunk absorbs the remainder (up to ~2x this size, well under the 128MB cap). Capped at 1000 chunks total, which this default comfortably satisfies up to TikTok's 4GB video size limit. */ +const DEFAULT_CHUNK_SIZE = 10_000_000 + +/** Maximum size this route will buffer in memory for a single file-upload request. TikTok's + * own limit is 4GB, but relaying that much through this server's memory per request isn't + * safe under concurrent load. Enforced before downloading the file so an oversized upload + * fails fast with a clean 413 instead of materializing hundreds of MB to multiple GB + * in-process. Users with larger files can use the "Public URL" source instead, since + * PULL_FROM_URL never routes bytes through this process. */ +const TIKTOK_MAX_VIDEO_BYTES = 250 * 1024 * 1024 + +function computeChunkPlan(totalBytes: number): { chunkSize: number; totalChunkCount: number } { + if (totalBytes <= DEFAULT_CHUNK_SIZE) { + return { chunkSize: totalBytes, totalChunkCount: 1 } + } + const totalChunkCount = Math.floor(totalBytes / DEFAULT_CHUNK_SIZE) + return { chunkSize: DEFAULT_CHUNK_SIZE, totalChunkCount } +} + +function resolveVideoMimeType(fileName: string, fileType: string | undefined): string { + if (fileType && TIKTOK_VIDEO_MIME_TYPES.has(fileType)) return fileType + const fromExtension = getMimeTypeFromExtension(getFileExtension(fileName)) + return TIKTOK_VIDEO_MIME_TYPES.has(fromExtension) ? fromExtension : 'video/mp4' +} + +async function uploadChunks( + uploadUrl: string, + buffer: Buffer, + mimeType: string, + requestId: string +): Promise { + const totalBytes = buffer.length + const { chunkSize, totalChunkCount } = computeChunkPlan(totalBytes) + + for (let i = 0; i < totalChunkCount; i++) { + const start = i * chunkSize + const isLastChunk = i === totalChunkCount - 1 + const end = isLastChunk ? totalBytes - 1 : start + chunkSize - 1 + const chunk = buffer.subarray(start, end + 1) + + const response = await fetch(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Type': mimeType, + 'Content-Length': String(chunk.length), + 'Content-Range': `bytes ${start}-${end}/${totalBytes}`, + }, + body: new Uint8Array(chunk), + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + logger.error(`[${requestId}] TikTok chunk upload failed`, { + chunkIndex: i, + status: response.status, + errorText, + }) + throw new Error( + `TikTok rejected video chunk ${i + 1}/${totalChunkCount}: ${response.status} ${errorText || response.statusText}` + ) + } + } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized TikTok publish-video attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest(tiktokPublishVideoContract, request, {}) + if (!parsed.success) return parsed.response + const data = parsed.data.body + + let userFile: UserFile + try { + userFile = processSingleFileToUserFile(data.file, requestId, logger) + } catch (error) { + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Failed to process file') }, + { status: 400 } + ) + } + + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + + logger.info(`[${requestId}] Downloading video from storage`, { + fileName: userFile.name, + size: userFile.size, + }) + + const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger, { + maxBytes: TIKTOK_MAX_VIDEO_BYTES, + }) + const mimeType = resolveVideoMimeType(userFile.name, userFile.type) + const { chunkSize, totalChunkCount } = computeChunkPlan(fileBuffer.length) + + const initUrl = + data.mode === 'draft' + ? 'https://open.tiktokapis.com/v2/post/publish/inbox/video/init/' + : 'https://open.tiktokapis.com/v2/post/publish/video/init/' + + const initBody: Record = { + source_info: { + source: 'FILE_UPLOAD', + video_size: fileBuffer.length, + chunk_size: chunkSize, + total_chunk_count: totalChunkCount, + }, + } + if (data.mode === 'direct') { + initBody.post_info = data.postInfo ?? {} + } + + logger.info(`[${requestId}] Initializing TikTok video ${data.mode}`, { + videoSize: fileBuffer.length, + chunkSize, + totalChunkCount, + }) + + const initResponse = await fetch(initUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${data.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify(initBody), + }) + + const { data: initData, error: initError } = await readTikTokApiResponse( + initResponse, + tiktokPublishInitApiDataSchema + ) + + if (initError) { + logger.error(`[${requestId}] TikTok init failed`, { error: initError }) + return NextResponse.json( + { success: false, error: initError.message || 'Failed to initialize TikTok upload' }, + { status: initResponse.status >= 400 ? initResponse.status : 502 } + ) + } + + const publishId = initData?.publish_id + const uploadUrl = initData?.upload_url + + if (!publishId || !uploadUrl) { + return NextResponse.json( + { success: false, error: 'TikTok did not return a publish ID and upload URL' }, + { status: 502 } + ) + } + + try { + await uploadChunks(uploadUrl, fileBuffer, mimeType, requestId) + } catch (error) { + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Failed to upload video to TikTok') }, + { status: 502 } + ) + } + + logger.info(`[${requestId}] TikTok video upload complete`, { publishId }) + + return NextResponse.json({ success: true, output: { publishId } }) + } catch (error) { + if (isPayloadSizeLimitError(error)) { + logger.warn(`[${requestId}] Rejected oversized TikTok video upload`, { + maxBytes: error.maxBytes, + observedBytes: error.observedBytes, + }) + const maxMb = Math.floor(TIKTOK_MAX_VIDEO_BYTES / (1024 * 1024)) + return NextResponse.json( + { + success: false, + error: `Video exceeds the ${maxMb}MB limit for file uploads. Use a public video URL instead to post larger files.`, + }, + { status: 413 } + ) + } + logger.error(`[${requestId}] Error publishing video to TikTok:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Internal server error') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/webhooks/tiktok/route.test.ts b/apps/sim/app/api/webhooks/tiktok/route.test.ts new file mode 100644 index 00000000000..71d07947cab --- /dev/null +++ b/apps/sim/app/api/webhooks/tiktok/route.test.ts @@ -0,0 +1,116 @@ +/** + * @vitest-environment node + */ + +import crypto from 'node:crypto' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockEnqueue, mockExecuteTikTokWebhookIngress, mockRelease } = vi.hoisted(() => ({ + mockEnqueue: vi.fn(), + mockExecuteTikTokWebhookIngress: vi.fn(), + mockRelease: vi.fn(), +})) + +vi.mock('@/background/tiktok-webhook-ingress', () => ({ + executeTikTokWebhookIngress: mockExecuteTikTokWebhookIngress, + TIKTOK_WEBHOOK_INGRESS_CONCURRENCY_LIMIT: 50, + TIKTOK_WEBHOOK_INGRESS_MAX_ATTEMPTS: 3, +})) + +vi.mock('@/lib/core/admission/gate', () => ({ + admissionRejectedResponse: vi.fn(() => new Response(null, { status: 503 })), + tryAdmit: vi.fn(() => ({ release: mockRelease })), +})) + +vi.mock('@/lib/core/async-jobs', () => ({ + getJobQueue: vi.fn(async () => ({ enqueue: mockEnqueue })), +})) + +vi.mock('@/lib/core/config/env', () => ({ + env: { + TIKTOK_CLIENT_ID: 'client-key', + TIKTOK_CLIENT_SECRET: 'client-secret', + }, +})) + +vi.mock('@/lib/core/utils/request', () => ({ + generateRequestId: vi.fn(() => 'request-1'), +})) + +vi.mock('@/lib/core/utils/with-route-handler', () => ({ + withRouteHandler: + (handler: (request: NextRequest) => Promise) => (request: NextRequest) => + handler(request), +})) + +import { POST } from '@/app/api/webhooks/tiktok/route' + +function signedRequest(overrides?: { clientKey?: string }): NextRequest { + const body = JSON.stringify({ + client_key: overrides?.clientKey ?? 'client-key', + event: 'post.publish.complete', + create_time: 1_725_000_000, + user_openid: 'act.user', + content: '{"publish_id":"publish-1"}', + }) + const timestamp = String(Math.floor(Date.now() / 1000)) + const signature = crypto + .createHmac('sha256', 'client-secret') + .update(`${timestamp}.${body}`, 'utf8') + .digest('hex') + + return new NextRequest('http://localhost/api/webhooks/tiktok', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'TikTok-Signature': `t=${timestamp},s=${signature}`, + }, + body, + }) +} + +describe('TikTok webhook ingress route', () => { + beforeEach(() => { + vi.clearAllMocks() + mockEnqueue.mockResolvedValue('ingress-job-1') + }) + + it('returns 200 only after the verified delivery is accepted by the job queue', async () => { + const response = await POST(signedRequest()) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ ok: true }) + expect(mockEnqueue).toHaveBeenCalledWith( + 'tiktok-webhook-ingress', + expect.objectContaining({ + envelope: expect.objectContaining({ + client_key: 'client-key', + user_openid: 'act.user', + }), + requestId: 'request-1', + }), + expect.objectContaining({ + maxAttempts: 3, + runner: expect.any(Function), + }) + ) + expect(mockRelease).toHaveBeenCalledOnce() + }) + + it('returns 503 when durable acceptance fails so TikTok retries', async () => { + mockEnqueue.mockRejectedValue(new Error('queue unavailable')) + + const response = await POST(signedRequest()) + + expect(response.status).toBe(503) + expect(mockRelease).toHaveBeenCalledOnce() + }) + + it('rejects a signed delivery for a different TikTok app', async () => { + const response = await POST(signedRequest({ clientKey: 'other-client-key' })) + + expect(response.status).toBe(401) + expect(mockEnqueue).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/webhooks/tiktok/route.ts b/apps/sim/app/api/webhooks/tiktok/route.ts new file mode 100644 index 00000000000..5fecb9134d0 --- /dev/null +++ b/apps/sim/app/api/webhooks/tiktok/route.ts @@ -0,0 +1,135 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { tiktokWebhookEnvelopeSchema } from '@/lib/api/contracts/webhooks' +import { admissionRejectedResponse, tryAdmit } from '@/lib/core/admission/gate' +import { getJobQueue } from '@/lib/core/async-jobs' +import { env } from '@/lib/core/config/env' +import { generateRequestId } from '@/lib/core/utils/request' +import { + assertContentLengthWithinLimit, + isPayloadSizeLimitError, + readStreamToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { WEBHOOK_MAX_BODY_BYTES } from '@/lib/webhooks/constants' +import { verifyTikTokSignature } from '@/lib/webhooks/providers/tiktok' +import { + executeTikTokWebhookIngress, + TIKTOK_WEBHOOK_INGRESS_CONCURRENCY_LIMIT, + TIKTOK_WEBHOOK_INGRESS_MAX_ATTEMPTS, + type TikTokWebhookIngressPayload, +} from '@/background/tiktok-webhook-ingress' + +const logger = createLogger('TikTokWebhookIngress') + +const TIKTOK_BODY_LABEL = 'TikTok webhook body' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' +export const maxDuration = 60 + +async function readTikTokBody(req: Request): Promise { + assertContentLengthWithinLimit(req.headers, WEBHOOK_MAX_BODY_BYTES, TIKTOK_BODY_LABEL) + const buffer = await readStreamToBufferWithLimit(req.body, { + maxBytes: WEBHOOK_MAX_BODY_BYTES, + label: TIKTOK_BODY_LABEL, + }) + return new TextDecoder().decode(buffer) +} + +/** + * App-level TikTok webhook Callback URL. + * Portal: `{APP_URL}/api/webhooks/tiktok` (e.g. https://www.sim.ai/api/webhooks/tiktok). + * Verifies TikTok-Signature and durably accepts the delivery before background target fanout. + */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const ticket = tryAdmit() + if (!ticket) { + return admissionRejectedResponse() + } + + const requestId = generateRequestId() + const receivedAt = Date.now() + + try { + let rawBody: string + try { + rawBody = await readTikTokBody(request) + } catch (bodyError) { + if (isPayloadSizeLimitError(bodyError)) { + logger.warn(`[${requestId}] Rejected oversized TikTok webhook body`, { + maxBytes: WEBHOOK_MAX_BODY_BYTES, + observedBytes: bodyError.observedBytes, + }) + return NextResponse.json({ error: 'Request body too large' }, { status: 413 }) + } + throw bodyError + } + + const authError = verifyTikTokSignature( + rawBody, + request.headers.get('TikTok-Signature'), + requestId + ) + if (authError) { + return authError + } + + let parsedJson: unknown + try { + parsedJson = rawBody ? JSON.parse(rawBody) : {} + } catch { + logger.warn(`[${requestId}] TikTok webhook body is not valid JSON`) + // Ack to avoid retry storms on malformed payloads after a valid signature. + return NextResponse.json({ ok: true }) + } + + const envelopeResult = tiktokWebhookEnvelopeSchema.safeParse(parsedJson) + if (!envelopeResult.success) { + logger.warn(`[${requestId}] Invalid TikTok webhook envelope`, { + issues: envelopeResult.error.issues, + }) + return NextResponse.json({ ok: true }) + } + + const envelope = envelopeResult.data + if (!env.TIKTOK_CLIENT_ID || envelope.client_key !== env.TIKTOK_CLIENT_ID) { + logger.warn(`[${requestId}] TikTok webhook client_key does not match configured app`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const payload: TikTokWebhookIngressPayload = { + envelope, + headers: { + 'content-type': request.headers.get('content-type') ?? 'application/json', + }, + requestId, + receivedAt, + } + const jobQueue = await getJobQueue() + const jobId = await jobQueue.enqueue('tiktok-webhook-ingress', payload, { + maxAttempts: TIKTOK_WEBHOOK_INGRESS_MAX_ATTEMPTS, + concurrencyKey: 'tiktok-webhook-ingress', + concurrencyLimit: TIKTOK_WEBHOOK_INGRESS_CONCURRENCY_LIMIT, + runner: async () => { + await executeTikTokWebhookIngress(payload) + }, + }) + + logger.info(`[${requestId}] Accepted TikTok webhook delivery`, { + event: envelope.event, + jobId, + userOpenIdPrefix: envelope.user_openid.slice(0, 12), + }) + + return NextResponse.json({ ok: true }) + } catch (error) { + logger.error(`[${requestId}] TikTok webhook ingress error`, { + error: getErrorMessage(error, 'Unknown error'), + }) + return NextResponse.json({ error: 'Temporarily unable to accept webhook' }, { status: 503 }) + } finally { + ticket.release() + } +}) diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index df0e11e35dd..395882f0c8b 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -15,63 +15,52 @@ import { } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -/** Mock execution dependencies for webhook tests */ -function mockExecutionDependencies() { - vi.mock('@/lib/core/security/encryption', () => encryptionMock) - - vi.mock('@/lib/logs/execution/trace-spans/trace-spans', () => ({ - buildTraceSpans: vi.fn().mockReturnValue({ traceSpans: [], totalDuration: 100 }), - })) - - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) - - vi.mock('@/serializer', () => ({ - Serializer: vi.fn().mockImplementation(() => ({ - serializeWorkflow: vi.fn().mockReturnValue({ - version: '1.0', - blocks: [ - { - id: 'starter-id', - metadata: { id: 'starter', name: 'Start' }, - config: {}, - inputs: {}, - outputs: {}, - position: { x: 100, y: 100 }, - enabled: true, - }, - { - id: 'agent-id', - metadata: { id: 'agent', name: 'Agent 1' }, - config: {}, - inputs: {}, - outputs: {}, - position: { x: 634, y: -167 }, - enabled: true, - }, - ], - edges: [ - { - id: 'edge-1', - source: 'starter-id', - target: 'agent-id', - sourceHandle: 'source', - targetHandle: 'target', - }, - ], - loops: {}, - parallels: {}, - }), - })), - })) -} +vi.mock('@/lib/core/security/encryption', () => encryptionMock) -/** Mock Trigger.dev SDK */ -function mockTriggerDevSdk() { - vi.mock('@trigger.dev/sdk', () => ({ - tasks: { trigger: vi.fn().mockResolvedValue({ id: 'mock-task-id' }) }, - task: vi.fn().mockReturnValue({}), - })) -} +vi.mock('@/lib/logs/execution/trace-spans/trace-spans', () => ({ + buildTraceSpans: vi.fn().mockReturnValue({ traceSpans: [], totalDuration: 100 }), +})) + +vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) + +vi.mock('@/serializer', () => ({ + Serializer: vi.fn().mockImplementation(() => ({ + serializeWorkflow: vi.fn().mockReturnValue({ + version: '1.0', + blocks: [ + { + id: 'starter-id', + metadata: { id: 'starter', name: 'Start' }, + config: {}, + inputs: {}, + outputs: {}, + position: { x: 100, y: 100 }, + enabled: true, + }, + { + id: 'agent-id', + metadata: { id: 'agent', name: 'Agent 1' }, + config: {}, + inputs: {}, + outputs: {}, + position: { x: 634, y: -167 }, + enabled: true, + }, + ], + edges: [ + { + id: 'edge-1', + source: 'starter-id', + target: 'agent-id', + sourceHandle: 'source', + targetHandle: 'target', + }, + ], + loops: {}, + parallels: {}, + }), + })), +})) /** * Test data store - isolated per test via beforeEach reset @@ -106,6 +95,7 @@ const { executeMock, getWorkspaceBilledAccountUserIdMock, queueWebhookExecutionMock, + dispatchResolvedWebhookTargetMock, isWorkspaceApiExecutionEntitledMock, } = vi.hoisted(() => ({ generateRequestHashMock: vi.fn().mockResolvedValue('test-hash-123'), @@ -134,6 +124,7 @@ const { const { NextResponse } = await import('next/server') return NextResponse.json({ message: 'Webhook processed' }) }), + dispatchResolvedWebhookTargetMock: vi.fn(), isWorkspaceApiExecutionEntitledMock: vi.fn().mockResolvedValue(true), })) @@ -274,6 +265,25 @@ vi.mock('@/lib/webhooks/processor', () => ({ } ), handleProviderReachabilityTest: vi.fn().mockReturnValue(null), + dispatchResolvedWebhookTarget: dispatchResolvedWebhookTargetMock.mockImplementation( + async ( + foundWebhook: unknown, + foundWorkflow: unknown, + body: unknown, + request: unknown, + options: unknown + ) => ({ + outcome: 'queued', + reason: 'queued', + response: await queueWebhookExecutionMock( + foundWebhook, + foundWorkflow, + body, + request, + options + ), + }) + ), verifyProviderAuth: vi .fn() .mockImplementation( @@ -350,7 +360,6 @@ vi.mock('@/lib/webhooks/processor', () => ({ }), shouldSkipWebhookEvent: vi.fn().mockReturnValue(false), handlePreDeploymentVerification: vi.fn().mockReturnValue(null), - queueWebhookExecution: queueWebhookExecutionMock, })) vi.mock('drizzle-orm/postgres-js', () => ({ @@ -401,9 +410,6 @@ describe('Webhook Trigger API Route', () => { workflowsPersistenceUtilsMockFns.mockBlockExistsInDeployment.mockResolvedValue(true) isWorkspaceApiExecutionEntitledMock.mockResolvedValue(true) - mockExecutionDependencies() - mockTriggerDevSdk() - // Set up default workflow for tests testData.workflows.push({ id: 'test-workflow-id', @@ -495,8 +501,8 @@ describe('Webhook Trigger API Route', () => { expect(text).toMatch(/not found/i) }) - describe('Internal trigger providers', () => { - it.each(['sim', 'table'])( + describe('Non-path trigger providers', () => { + it.each(['sim', 'table', 'tiktok'])( 'rejects HTTP deliveries to %s trigger paths with 404', async (provider) => { testData.webhooks.push({ @@ -539,7 +545,7 @@ describe('Webhook Trigger API Route', () => { }) describe('Generic Webhook Authentication', () => { - it('passes correlation-bearing request context into webhook queueing', async () => { + it('passes request context into shared webhook dispatch', async () => { testData.webhooks.push({ id: 'generic-webhook-id', provider: 'generic', @@ -556,7 +562,7 @@ describe('Webhook Trigger API Route', () => { expect(response.status).toBe(200) expect(queueWebhookExecutionMock).toHaveBeenCalledOnce() - const call = queueWebhookExecutionMock.mock.calls[0] + const call = dispatchResolvedWebhookTargetMock.mock.calls[0] expect(call[0]).toEqual(expect.objectContaining({ id: 'generic-webhook-id' })) expect(call[1]).toEqual(expect.objectContaining({ id: 'test-workflow-id' })) expect(call[2]).toEqual(expect.objectContaining({ event: 'test', id: 'test-123' })) @@ -564,18 +570,6 @@ describe('Webhook Trigger API Route', () => { expect.objectContaining({ requestId: 'mock-request-id', path: 'test-path', - actorUserId: 'test-user-id', - executionId: 'preprocess-execution-id', - correlation: { - executionId: 'preprocess-execution-id', - requestId: 'mock-request-id', - source: 'webhook', - workflowId: 'test-workflow-id', - webhookId: 'generic-webhook-id', - path: 'test-path', - provider: 'generic', - triggerType: 'webhook', - }, }) ) }) diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 1c103ddad72..af9ebe7c134 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -10,18 +10,15 @@ import { admissionRejectedResponse, tryAdmit } from '@/lib/core/admission/gate' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { - checkWebhookPreprocessing, + dispatchResolvedWebhookTarget, findAllWebhooksForPath, - handlePreDeploymentVerification, handlePreLookupWebhookVerification, handleProviderChallenges, handleProviderReachabilityTest, parseWebhookBody, - queueWebhookExecution, - shouldSkipWebhookEvent, verifyProviderAuth, } from '@/lib/webhooks/processor' -import { blockExistsInDeployment } from '@/lib/workflows/persistence/utils' +import { acceptsPathWebhookDelivery } from '@/lib/webhooks/providers' import { isInternalTriggerProvider } from '@/triggers/constants' const logger = createLogger('WebhookTriggerAPI') @@ -107,15 +104,15 @@ async function handleWebhookPost( // Find all webhooks for this path (multiple webhooks in one workflow may share a path) const allWebhooksForPath = await findAllWebhooksForPath({ requestId, path }) - // Internal trigger providers (sim, table) are fired in-process, never over - // HTTP. Their rows still register a path, so reject deliveries here to keep - // forged events out. + /** Exclude in-process triggers and providers that own an app-level ingress route. */ const webhooksForPath = allWebhooksForPath.filter( - ({ webhook: foundWebhook }) => !isInternalTriggerProvider(foundWebhook.provider) + ({ webhook: foundWebhook }) => + !isInternalTriggerProvider(foundWebhook.provider) && + acceptsPathWebhookDelivery(foundWebhook.provider) ) if (allWebhooksForPath.length > 0 && webhooksForPath.length === 0) { - logger.warn(`[${requestId}] Rejected HTTP delivery to internal trigger path: ${path}`) + logger.warn(`[${requestId}] Rejected HTTP delivery to non-path trigger: ${path}`) return new NextResponse('Not Found', { status: 404 }) } @@ -172,48 +169,35 @@ async function handleWebhookPost( return reachabilityResponse } - const preprocessResult = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId) - if (preprocessResult.error) { - if (webhooksForPath.length > 1) { - logger.warn( - `[${requestId}] Preprocessing failed for webhook ${foundWebhook.id}, continuing to next` - ) - continue + const dispatchResult = await dispatchResolvedWebhookTarget( + foundWebhook, + foundWorkflow, + body, + request, + { + requestId, + path, + receivedAt, + triggerTimestampMs: Number.isFinite(triggerTimestampMs) ? triggerTimestampMs : undefined, } - return preprocessResult.error - } + ) - if (foundWebhook.blockId) { - const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId) - if (!blockExists) { - const preDeploymentResponse = handlePreDeploymentVerification(foundWebhook, requestId) - if (preDeploymentResponse) { - return preDeploymentResponse - } + if (dispatchResult.reason === 'filtered') { + continue + } - logger.info( - `[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}` + if (dispatchResult.outcome === 'failed' || dispatchResult.reason === 'block-missing') { + if (webhooksForPath.length > 1) { + logger.warn( + `[${requestId}] Webhook dispatch failed for ${foundWebhook.id}, continuing to next`, + { reason: dispatchResult.reason, status: dispatchResult.response.status } ) - if (webhooksForPath.length > 1) { - continue - } - return new NextResponse('Trigger block not found in deployment', { status: 404 }) + continue } + return dispatchResult.response } - if (shouldSkipWebhookEvent(foundWebhook, body, requestId)) { - continue - } - const response = await queueWebhookExecution(foundWebhook, foundWorkflow, body, request, { - requestId, - path, - actorUserId: preprocessResult.actorUserId, - executionId: preprocessResult.executionId, - correlation: preprocessResult.correlation, - receivedAt, - triggerTimestampMs: Number.isFinite(triggerTimestampMs) ? triggerTimestampMs : undefined, - }) - responses.push(response) + responses.push(dispatchResult.response) } if (responses.length === 0) { diff --git a/apps/sim/background/tiktok-webhook-ingress.test.ts b/apps/sim/background/tiktok-webhook-ingress.test.ts new file mode 100644 index 00000000000..7673053bb95 --- /dev/null +++ b/apps/sim/background/tiktok-webhook-ingress.test.ts @@ -0,0 +1,109 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDispatchResolvedWebhookTarget, mockFindTikTokWebhookTargets } = vi.hoisted(() => ({ + mockDispatchResolvedWebhookTarget: vi.fn(), + mockFindTikTokWebhookTargets: vi.fn(), +})) + +vi.mock('@trigger.dev/sdk', () => ({ + task: vi.fn((config: unknown) => config), +})) + +vi.mock('@/lib/webhooks/processor', () => ({ + dispatchResolvedWebhookTarget: mockDispatchResolvedWebhookTarget, +})) + +vi.mock('@/lib/webhooks/providers/tiktok-targets', () => ({ + findTikTokWebhookTargets: mockFindTikTokWebhookTargets, +})) + +import { + executeTikTokWebhookIngress, + type TikTokWebhookIngressPayload, +} from '@/background/tiktok-webhook-ingress' + +const payload: TikTokWebhookIngressPayload = { + envelope: { + client_key: 'client-key', + event: 'post.publish.complete', + create_time: 1_725_000_000, + user_openid: 'act.user', + content: '{"publish_id":"publish-1"}', + }, + headers: { 'content-type': 'application/json' }, + requestId: 'request-1', + receivedAt: 1_725_000_000_000, +} + +const targets = [ + { + webhook: { id: 'webhook-1', path: 'tiktok', provider: 'tiktok' }, + workflow: { id: 'workflow-1' }, + }, + { + webhook: { id: 'webhook-2', path: 'tiktok', provider: 'tiktok' }, + workflow: { id: 'workflow-2' }, + }, +] + +describe('executeTikTokWebhookIngress', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('acknowledges deliveries without active targets', async () => { + mockFindTikTokWebhookTargets.mockResolvedValue([]) + + await expect(executeTikTokWebhookIngress(payload)).resolves.toEqual({ + ignored: 0, + processed: 0, + targetCount: 0, + }) + expect(mockDispatchResolvedWebhookTarget).not.toHaveBeenCalled() + }) + + it('dispatches every resolved target and reports typed outcomes', async () => { + mockFindTikTokWebhookTargets.mockResolvedValue(targets) + mockDispatchResolvedWebhookTarget + .mockResolvedValueOnce({ + outcome: 'queued', + reason: 'queued', + response: new Response(null, { status: 200 }), + }) + .mockResolvedValueOnce({ + outcome: 'ignored', + reason: 'event-mismatch', + response: new Response(null, { status: 200 }), + }) + + await expect(executeTikTokWebhookIngress(payload)).resolves.toEqual({ + ignored: 1, + processed: 1, + targetCount: 2, + }) + expect(mockDispatchResolvedWebhookTarget).toHaveBeenCalledTimes(2) + }) + + it('throws after a target failure so the durable ingress job retries', async () => { + mockFindTikTokWebhookTargets.mockResolvedValue(targets) + mockDispatchResolvedWebhookTarget + .mockResolvedValueOnce({ + outcome: 'queued', + reason: 'queued', + response: new Response(null, { status: 200 }), + }) + .mockResolvedValueOnce({ + outcome: 'failed', + reason: 'queue-failed', + response: new Response(null, { status: 500 }), + }) + + await expect(executeTikTokWebhookIngress(payload)).rejects.toThrow( + 'Failed to dispatch 1 of 2 TikTok webhook targets' + ) + }) +}) diff --git a/apps/sim/background/tiktok-webhook-ingress.ts b/apps/sim/background/tiktok-webhook-ingress.ts new file mode 100644 index 00000000000..a55cbb3783e --- /dev/null +++ b/apps/sim/background/tiktok-webhook-ingress.ts @@ -0,0 +1,105 @@ +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { NextRequest } from 'next/server' +import type { TikTokWebhookEnvelope } from '@/lib/api/contracts/webhooks' +import { dispatchResolvedWebhookTarget } from '@/lib/webhooks/processor' +import { findTikTokWebhookTargets } from '@/lib/webhooks/providers/tiktok-targets' + +const logger = createLogger('TikTokWebhookIngressTask') + +export const TIKTOK_WEBHOOK_INGRESS_CONCURRENCY_LIMIT = 50 +export const TIKTOK_WEBHOOK_INGRESS_MAX_ATTEMPTS = 3 + +export interface TikTokWebhookIngressPayload { + envelope: TikTokWebhookEnvelope + headers: { + 'content-type': string + } + requestId: string + receivedAt: number +} + +export interface TikTokWebhookIngressResult { + ignored: number + processed: number + targetCount: number +} + +/** + * Resolves and dispatches all active workflow targets for one verified TikTok delivery. Throwing + * after any retryable target failure lets Trigger.dev replay the fanout; workflow-level + * idempotency prevents already-queued targets from executing twice. + */ +export async function executeTikTokWebhookIngress( + payload: TikTokWebhookIngressPayload +): Promise { + const targets = await findTikTokWebhookTargets(payload.envelope.user_openid, payload.requestId) + if (targets.length === 0) { + logger.info(`[${payload.requestId}] No TikTok webhook targets found`, { + event: payload.envelope.event, + userOpenIdPrefix: payload.envelope.user_openid.slice(0, 12), + }) + return { ignored: 0, processed: 0, targetCount: 0 } + } + + const request = new NextRequest('http://internal/api/webhooks/tiktok', { + method: 'POST', + headers: payload.headers, + body: JSON.stringify(payload.envelope), + }) + + let ignored = 0 + let processed = 0 + let failed = 0 + + for (const { webhook, workflow } of targets) { + const result = await dispatchResolvedWebhookTarget( + webhook, + workflow, + payload.envelope, + request, + { + requestId: payload.requestId, + path: webhook.path, + receivedAt: payload.receivedAt, + triggerTimestampMs: payload.envelope.create_time * 1000, + } + ) + + if (result.outcome === 'queued') { + processed += 1 + } else if (result.outcome === 'ignored') { + ignored += 1 + } else { + failed += 1 + } + } + + if (failed > 0) { + throw new Error(`Failed to dispatch ${failed} of ${targets.length} TikTok webhook targets`) + } + + logger.info(`[${payload.requestId}] TikTok webhook fanout completed`, { + event: payload.envelope.event, + ignored, + processed, + targetCount: targets.length, + }) + + return { ignored, processed, targetCount: targets.length } +} + +export const tiktokWebhookIngressTask = task({ + id: 'tiktok-webhook-ingress', + machine: 'small-1x', + retry: { + maxAttempts: TIKTOK_WEBHOOK_INGRESS_MAX_ATTEMPTS, + factor: 2, + minTimeoutInMs: 1000, + maxTimeoutInMs: 10_000, + }, + queue: { + concurrencyLimit: TIKTOK_WEBHOOK_INGRESS_CONCURRENCY_LIMIT, + }, + run: async (payload: TikTokWebhookIngressPayload) => executeTikTokWebhookIngress(payload), +}) diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts new file mode 100644 index 00000000000..1b087f6ba2d --- /dev/null +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -0,0 +1,810 @@ +import { TikTokIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { TikTokResponse } from '@/tools/tiktok/types' +import { getTrigger } from '@/triggers' + +const VIDEO_POST_OPERATIONS = ['tiktok_direct_post_video', 'tiktok_upload_video_draft'] +const PHOTO_POST_OPERATIONS = ['tiktok_direct_post_photo', 'tiktok_upload_photo_draft'] + +export const TikTokBlock: BlockConfig = { + type: 'tiktok', + name: 'TikTok', + description: 'Access TikTok profiles and videos, and publish content', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata. Publish videos and photos directly to TikTok (or send them to the inbox as drafts) from a public URL or a file uploaded in the workflow, then track post status.', + docsLink: 'https://docs.sim.ai/integrations/tiktok', + category: 'tools', + integrationType: IntegrationType.Communication, + bgColor: '#000000', + icon: TikTokIcon, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get User Info', id: 'tiktok_get_user' }, + { label: 'List Videos', id: 'tiktok_list_videos' }, + { label: 'Query Videos', id: 'tiktok_query_videos' }, + { label: 'Query Creator Info', id: 'tiktok_query_creator_info' }, + { label: 'Direct Post Video', id: 'tiktok_direct_post_video' }, + { label: 'Upload Video Draft', id: 'tiktok_upload_video_draft' }, + { label: 'Direct Post Photo', id: 'tiktok_direct_post_photo' }, + { label: 'Upload Photo Draft', id: 'tiktok_upload_photo_draft' }, + { label: 'Get Post Status', id: 'tiktok_get_post_status' }, + ], + value: () => 'tiktok_get_user', + }, + + // --- OAuth Credential --- + { + id: 'credential', + title: 'TikTok Account', + type: 'oauth-input', + serviceId: 'tiktok', + canonicalParamId: 'oauthCredential', + mode: 'basic', + requiredScopes: getScopesForService('tiktok'), + placeholder: 'Select TikTok account', + required: true, + }, + { + id: 'manualCredential', + title: 'TikTok Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + // --- Get User Info --- + { + id: 'fields', + title: 'Fields', + type: 'short-input', + placeholder: 'open_id,display_name,avatar_url,follower_count,video_count', + description: 'Comma-separated list of user fields to return. Leave empty for all fields.', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_get_user' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of TikTok user info fields based on the user request, choosing only from: open_id, union_id, avatar_url, avatar_large_url, display_name, bio_description, profile_deep_link, is_verified, username, follower_count, following_count, likes_count, video_count. Return ONLY the comma-separated field names - no explanations, no extra text.', + placeholder: 'Describe which profile fields you need', + }, + }, + + // --- List Videos --- + { + id: 'maxCount', + title: 'Max Count', + type: 'short-input', + placeholder: '20', + description: 'Maximum number of videos to return (1-20).', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_list_videos' }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous response', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_list_videos' }, + }, + + // --- Query Videos --- + { + id: 'videoIds', + title: 'Video IDs', + type: 'long-input', + placeholder: 'One video ID per line or comma-separated (e.g., 7077642457847994444)', + condition: { field: 'operation', value: 'tiktok_query_videos' }, + required: { field: 'operation', value: 'tiktok_query_videos' }, + }, + + // --- Video source (Direct Post Video / Upload Video Draft) --- + { + id: 'videoSource', + title: 'Video Source', + type: 'dropdown', + options: [ + { label: 'Public URL', id: 'PULL_FROM_URL' }, + { label: 'Upload File', id: 'FILE_UPLOAD' }, + ], + value: () => 'PULL_FROM_URL', + condition: { field: 'operation', value: VIDEO_POST_OPERATIONS }, + }, + { + id: 'videoUrl', + title: 'Video URL', + type: 'short-input', + placeholder: 'https://example.com/video.mp4', + description: + 'Public URL of the video. The domain/URL prefix must be verified in the TikTok developer portal.', + condition: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'PULL_FROM_URL' }, + }, + required: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'PULL_FROM_URL' }, + }, + }, + { + id: 'videoFile', + title: 'Video File', + type: 'file-upload', + canonicalParamId: 'file', + mode: 'basic', + placeholder: 'Upload video', + acceptedTypes: '.mp4,.mov,.webm', + multiple: false, + condition: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'FILE_UPLOAD' }, + }, + required: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'FILE_UPLOAD' }, + }, + }, + { + id: 'videoFileRef', + title: 'Video File', + type: 'short-input', + canonicalParamId: 'file', + mode: 'advanced', + placeholder: 'Reference a video from a previous block (e.g., )', + condition: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'FILE_UPLOAD' }, + }, + required: { + field: 'operation', + value: VIDEO_POST_OPERATIONS, + and: { field: 'videoSource', value: 'FILE_UPLOAD' }, + }, + }, + + // --- Photo images (Direct Post Photo / Upload Photo Draft) --- + { + id: 'photoImages', + title: 'Photo URLs', + type: 'long-input', + placeholder: 'One public image URL per line (up to 35, JPEG or WEBP only — no PNG)', + description: + 'Public, verified-domain URLs of the images to post. Photos only support public URLs, not file upload.', + condition: { field: 'operation', value: PHOTO_POST_OPERATIONS }, + required: { field: 'operation', value: PHOTO_POST_OPERATIONS }, + }, + { + id: 'photoCoverIndex', + title: 'Cover Photo Index', + type: 'short-input', + placeholder: '0', + description: 'Index (starting from 0) of the photo to use as the cover.', + mode: 'advanced', + condition: { field: 'operation', value: PHOTO_POST_OPERATIONS }, + }, + + // --- Shared caption/title (video + photo posts) --- + { + id: 'title', + title: 'Title / Caption', + type: 'long-input', + placeholder: 'Caption with #hashtags and @mentions', + description: + 'Caption or title. Max 2200 characters for videos, 90 characters for photo posts.', + condition: { + field: 'operation', + value: [ + 'tiktok_direct_post_video', + 'tiktok_direct_post_photo', + 'tiktok_upload_photo_draft', + ], + }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Post description', + description: 'Post description. Max 4000 characters. Photo posts only.', + condition: { field: 'operation', value: PHOTO_POST_OPERATIONS }, + }, + + // --- Privacy & interaction settings (Direct Post Video / Direct Post Photo) --- + { + id: 'privacyLevel', + title: 'Privacy Level', + type: 'dropdown', + options: [ + { label: 'Public', id: 'PUBLIC_TO_EVERYONE' }, + { label: 'Friends', id: 'MUTUAL_FOLLOW_FRIENDS' }, + { label: 'Followers', id: 'FOLLOWER_OF_CREATOR' }, + { label: 'Only Me', id: 'SELF_ONLY' }, + ], + value: () => 'SELF_ONLY', + description: + 'Must match one of the privacyLevelOptions returned by Query Creator Info. Unaudited apps (including sandbox apps) are restricted to Only Me.', + condition: { + field: 'operation', + value: ['tiktok_direct_post_video', 'tiktok_direct_post_photo'], + }, + required: { + field: 'operation', + value: ['tiktok_direct_post_video', 'tiktok_direct_post_photo'], + }, + }, + { + id: 'disableComment', + title: 'Disable Comments', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { + field: 'operation', + value: ['tiktok_direct_post_video', 'tiktok_direct_post_photo'], + }, + }, + { + id: 'disableDuet', + title: 'Disable Duet', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_direct_post_video' }, + }, + { + id: 'disableStitch', + title: 'Disable Stitch', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_direct_post_video' }, + }, + { + id: 'videoCoverTimestampMs', + title: 'Cover Timestamp (ms)', + type: 'short-input', + placeholder: '1000', + description: 'Timestamp in milliseconds to use as the video cover image.', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_direct_post_video' }, + }, + { + id: 'isAigc', + title: 'AI-Generated Content', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_direct_post_video' }, + }, + { + id: 'autoAddMusic', + title: 'Auto-Add Music', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'tiktok_direct_post_photo' }, + }, + { + id: 'brandContentToggle', + title: 'Paid Partnership', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + description: + 'Disclose this post as a paid partnership promoting a third-party business. Branded content cannot be posted with Only Me privacy.', + mode: 'advanced', + condition: { + field: 'operation', + value: ['tiktok_direct_post_video', 'tiktok_direct_post_photo'], + }, + }, + { + id: 'brandOrganicToggle', + title: 'Promotes Own Business', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + description: "Disclose this post as promoting the creator's own business.", + mode: 'advanced', + condition: { + field: 'operation', + value: ['tiktok_direct_post_video', 'tiktok_direct_post_photo'], + }, + }, + + // --- Get Post Status --- + { + id: 'publishId', + title: 'Publish ID', + type: 'short-input', + placeholder: 'v_pub_file~v2-1.123456789', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + required: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + + ...getTrigger('tiktok_post_publish_complete').subBlocks, + ...getTrigger('tiktok_post_publish_failed').subBlocks, + ...getTrigger('tiktok_post_inbox_delivered').subBlocks, + ...getTrigger('tiktok_post_publicly_available').subBlocks, + ...getTrigger('tiktok_post_no_longer_public').subBlocks, + ...getTrigger('tiktok_video_publish_completed').subBlocks, + ...getTrigger('tiktok_video_upload_failed').subBlocks, + ...getTrigger('tiktok_authorization_removed').subBlocks, + ], + + triggers: { + enabled: true, + available: [ + 'tiktok_post_publish_complete', + 'tiktok_post_publish_failed', + 'tiktok_post_inbox_delivered', + 'tiktok_post_publicly_available', + 'tiktok_post_no_longer_public', + 'tiktok_video_publish_completed', + 'tiktok_video_upload_failed', + 'tiktok_authorization_removed', + ], + }, + + tools: { + access: [ + 'tiktok_get_user', + 'tiktok_list_videos', + 'tiktok_query_videos', + 'tiktok_query_creator_info', + 'tiktok_direct_post_video', + 'tiktok_upload_video_draft', + 'tiktok_direct_post_photo', + 'tiktok_upload_photo_draft', + 'tiktok_get_post_status', + ], + config: { + tool: (params) => params.operation || 'tiktok_get_user', + params: (params) => { + const operation = params.operation || 'tiktok_get_user' + const toBoolean = (value: unknown): boolean | undefined => + value === undefined || value === '' ? undefined : String(value).toLowerCase() === 'true' + + switch (operation) { + case 'tiktok_get_user': + return { + ...(params.fields && { fields: params.fields }), + } + case 'tiktok_list_videos': + return { + ...(params.maxCount && { maxCount: Number(params.maxCount) }), + ...(params.cursor !== undefined && + params.cursor !== '' && { cursor: Number(params.cursor) }), + } + case 'tiktok_query_videos': + return { + videoIds: (params.videoIds || '') + .split(/[,\n]+/) + .map((id: string) => id.trim()) + .filter(Boolean), + } + case 'tiktok_query_creator_info': + return {} + case 'tiktok_direct_post_video': { + const file = normalizeFileInput(params.file, { single: true }) + return { + source: params.videoSource || 'PULL_FROM_URL', + videoUrl: params.videoUrl, + file, + title: params.title, + privacyLevel: params.privacyLevel || 'SELF_ONLY', + disableDuet: toBoolean(params.disableDuet), + disableStitch: toBoolean(params.disableStitch), + disableComment: toBoolean(params.disableComment), + ...(params.videoCoverTimestampMs !== undefined && + params.videoCoverTimestampMs !== '' && { + videoCoverTimestampMs: Number(params.videoCoverTimestampMs), + }), + isAigc: toBoolean(params.isAigc), + brandContentToggle: toBoolean(params.brandContentToggle), + brandOrganicToggle: toBoolean(params.brandOrganicToggle), + } + } + case 'tiktok_upload_video_draft': { + const file = normalizeFileInput(params.file, { single: true }) + return { + source: params.videoSource || 'PULL_FROM_URL', + videoUrl: params.videoUrl, + file, + } + } + case 'tiktok_direct_post_photo': + return { + photoImages: (params.photoImages || '') + .split('\n') + .map((url: string) => url.trim()) + .filter(Boolean), + ...(params.photoCoverIndex !== undefined && + params.photoCoverIndex !== '' && { + photoCoverIndex: Number(params.photoCoverIndex), + }), + title: params.title, + description: params.description, + privacyLevel: params.privacyLevel || 'SELF_ONLY', + disableComment: toBoolean(params.disableComment), + autoAddMusic: toBoolean(params.autoAddMusic), + brandContentToggle: toBoolean(params.brandContentToggle), + brandOrganicToggle: toBoolean(params.brandOrganicToggle), + } + case 'tiktok_upload_photo_draft': + return { + photoImages: (params.photoImages || '') + .split('\n') + .map((url: string) => url.trim()) + .filter(Boolean), + ...(params.photoCoverIndex !== undefined && + params.photoCoverIndex !== '' && { + photoCoverIndex: Number(params.photoCoverIndex), + }), + title: params.title, + description: params.description, + } + case 'tiktok_get_post_status': + return { + publishId: params.publishId, + } + default: + return {} + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'TikTok account credential' }, + fields: { type: 'string', description: 'Comma-separated list of user fields to return' }, + maxCount: { type: 'number', description: 'Maximum number of videos to return (1-20)' }, + cursor: { type: 'number', description: 'Pagination cursor from previous response' }, + videoIds: { + type: 'string', + description: 'List of video IDs to query, one per line or comma-separated', + }, + videoSource: { + type: 'string', + description: 'Video transfer method (PULL_FROM_URL/FILE_UPLOAD)', + }, + videoUrl: { type: 'string', description: 'Public URL of the video' }, + file: { + type: 'json', + description: 'Video file to upload (uploaded file or reference from a previous block)', + }, + photoImages: { type: 'string', description: 'Newline-separated public photo URLs' }, + photoCoverIndex: { type: 'number', description: 'Index of the photo to use as cover' }, + title: { type: 'string', description: 'Video/photo caption or title' }, + description: { type: 'string', description: 'Photo post description' }, + privacyLevel: { type: 'string', description: 'Privacy level for the post' }, + disableComment: { type: 'string', description: 'Whether to disable comments' }, + disableDuet: { type: 'string', description: 'Whether to disable duet' }, + disableStitch: { type: 'string', description: 'Whether to disable stitch' }, + videoCoverTimestampMs: { type: 'number', description: 'Video cover timestamp in ms' }, + isAigc: { type: 'string', description: 'Whether the video is AI-generated content' }, + autoAddMusic: { type: 'string', description: 'Whether to auto-add recommended music' }, + brandContentToggle: { + type: 'string', + description: 'Whether the post is a paid partnership promoting a third-party business', + }, + brandOrganicToggle: { + type: 'string', + description: "Whether the post promotes the creator's own business", + }, + publishId: { type: 'string', description: 'Publish ID to check status for' }, + }, + + outputs: { + // Get User Info + openId: { + type: 'string', + description: 'Unique TikTok user ID for this application', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + unionId: { + type: 'string', + description: 'Unique TikTok user ID across all apps from the developer', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + displayName: { + type: 'string', + description: 'User display name', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + bioDescription: { + type: 'string', + description: 'User bio description', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + profileDeepLink: { + type: 'string', + description: 'Deep link to the user TikTok profile', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + isVerified: { + type: 'boolean', + description: 'Whether the account is verified', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + username: { + type: 'string', + description: 'TikTok username', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + followerCount: { + type: 'number', + description: 'Number of followers', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + followingCount: { + type: 'number', + description: 'Number of accounts followed', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + likesCount: { + type: 'number', + description: 'Total likes received across all videos', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + videoCount: { + type: 'number', + description: 'Total number of public videos', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + avatarFile: { + type: 'file', + description: + 'Downloadable copy of the profile avatar image, stored as a workflow file so it can be chained into file-consuming blocks (e.g. attached to an email).', + condition: { field: 'operation', value: 'tiktok_get_user' }, + }, + + // List/Query Videos + videos: { + type: 'json', + description: + 'Array of video objects (id, title, coverImageUrl, embedLink, embedHtml, duration, createTime, shareUrl, videoDescription, width, height, viewCount, likeCount, commentCount, shareCount)', + condition: { field: 'operation', value: ['tiktok_list_videos', 'tiktok_query_videos'] }, + }, + cursor: { + type: 'number', + description: 'Pagination cursor for fetching the next page', + condition: { field: 'operation', value: 'tiktok_list_videos' }, + }, + hasMore: { + type: 'boolean', + description: 'Whether more videos are available', + condition: { field: 'operation', value: 'tiktok_list_videos' }, + }, + + // Query Creator Info + creatorAvatarUrl: { + type: 'string', + description: 'URL of the creator avatar', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + creatorUsername: { + type: 'string', + description: 'TikTok username of the creator', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + creatorNickname: { + type: 'string', + description: 'Display name/nickname of the creator', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + privacyLevelOptions: { + type: 'json', + description: 'Available privacy levels for posting (array of strings)', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + commentDisabled: { + type: 'boolean', + description: 'Whether the creator disabled comments by default', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + duetDisabled: { + type: 'boolean', + description: 'Whether the creator disabled duets by default', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + stitchDisabled: { + type: 'boolean', + description: 'Whether the creator disabled stitches by default', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + maxVideoPostDurationSec: { + type: 'number', + description: 'Maximum allowed video duration in seconds', + condition: { field: 'operation', value: 'tiktok_query_creator_info' }, + }, + + // Direct Post / Upload Draft (video and photo) + publishId: { + type: 'string', + description: 'Publish ID for tracking post status', + condition: { + field: 'operation', + value: [...VIDEO_POST_OPERATIONS, ...PHOTO_POST_OPERATIONS], + }, + }, + + // Get Post Status + status: { + type: 'string', + description: + 'Post status: PROCESSING_UPLOAD/PROCESSING_DOWNLOAD, SEND_TO_USER_INBOX, PUBLISH_COMPLETE, or FAILED', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + failReason: { + type: 'string', + description: 'Reason for failure if status is FAILED', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + publiclyAvailablePostId: { + type: 'json', + description: 'Array of public post IDs once the content is published', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + uploadedBytes: { + type: 'number', + description: 'Bytes uploaded to TikTok for a file upload', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + downloadedBytes: { + type: 'number', + description: 'Bytes TikTok downloaded from a public URL', + condition: { field: 'operation', value: 'tiktok_get_post_status' }, + }, + }, +} + +export const TikTokBlockMeta = { + tags: ['marketing', 'content-management'], + url: 'https://www.tiktok.com', + templates: [ + { + icon: TikTokIcon, + title: 'AI video auto-publisher', + prompt: + 'Build a workflow that takes a generated video file and publishes it directly to TikTok with an AI-written caption, then checks the post status until it completes.', + modules: ['agent', 'workflows'], + category: 'marketing', + tags: ['marketing', 'automation'], + }, + { + icon: TikTokIcon, + title: 'TikTok content calendar scheduler', + prompt: + 'Create a scheduled workflow that reads the next due row from a Tables-based content calendar and publishes the matching video or photo to TikTok at the right time.', + modules: ['scheduled', 'tables', 'agent', 'workflows'], + category: 'marketing', + tags: ['marketing', 'automation'], + }, + { + icon: TikTokIcon, + title: 'TikTok performance reporter', + prompt: + 'Build a workflow that lists recent TikTok videos, summarizes view and engagement trends with an agent, and posts the digest to Slack every week.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'marketing', + tags: ['marketing', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: TikTokIcon, + title: 'TikTok draft review pipeline', + prompt: + 'Create a workflow that uploads a new video to a TikTok inbox draft for review, then notifies the marketing team on Slack to approve and post it from the TikTok app.', + modules: ['agent', 'workflows'], + category: 'operations', + tags: ['marketing', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: TikTokIcon, + title: 'TikTok carousel campaign launcher', + prompt: + 'Build a campaign workflow that turns approved product images into a TikTok photo carousel, publishes it, and sends the resulting post link to Slack.', + modules: ['tables', 'agent', 'workflows'], + category: 'marketing', + tags: ['marketing', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: TikTokIcon, + title: 'TikTok publishing failure alerts', + prompt: + 'Create a workflow triggered when a TikTok post fails to publish, summarize the failure reason, and alert the content team in Slack with the publish ID.', + modules: ['agent', 'workflows'], + category: 'operations', + tags: ['monitoring', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: TikTokIcon, + title: 'TikTok authorization monitor', + prompt: + 'Create a workflow triggered when a TikTok user removes app authorization, record the affected account, and notify the operations team to reconnect it.', + modules: ['tables', 'workflows'], + category: 'operations', + tags: ['monitoring', 'automation'], + alsoIntegrations: ['slack'], + }, + ], + skills: [ + { + name: 'publish-video-to-tiktok', + description: 'Publish a video directly to TikTok from a public URL or an uploaded file.', + content: + '# Publish a Video to TikTok\n\nPost a video straight to a connected TikTok account.\n\n## Steps\n1. Use the Direct Post Video operation with a connected TikTok Account.\n2. Before the first post, run Query Creator Info to confirm posting permissions and see the allowed privacy levels — sandbox/unaudited apps are restricted to Only Me.\n3. Choose the Video Source: Public URL (the domain must be verified in the TikTok developer portal) or Upload File (a file produced earlier in the workflow).\n4. Set the Title/Caption with hashtags and mentions, and pick a Privacy Level from the options Query Creator Info returned.\n5. Use the Get Post Status operation with the returned Publish ID to confirm the post completed.\n\n## Output\nReturn the Publish ID and the final status (PUBLISH_COMPLETE or FAILED with a reason).', + }, + { + name: 'send-video-draft-to-inbox', + description: "Send a video to the user's TikTok inbox for manual review before posting.", + content: + "# Send a TikTok Video Draft\n\nDeliver a video to the connected account's TikTok inbox so a human can review, edit, and publish it from the app.\n\n## Steps\n1. Use the Upload Video Draft operation with a connected TikTok Account.\n2. Choose the Video Source: Public URL or Upload File.\n3. Submit the draft — no caption or privacy level is set here, since the user finishes the post manually in the TikTok app.\n4. Use Get Post Status with the returned Publish ID to see when the user has acted on the inbox notification (SEND_TO_USER_INBOX until they do).\n\n## Output\nReturn the Publish ID so the draft's status can be tracked or referenced later.", + }, + { + name: 'publish-photo-carousel', + description: 'Publish a set of photos to TikTok as a photo carousel post.', + content: + '# Publish a Photo Carousel to TikTok\n\nPost a carousel of images to a connected TikTok account. Photos only support public URLs (no file upload).\n\n## Steps\n1. Run Query Creator Info first to confirm the account can post and to read the allowed privacy levels.\n2. Use the Direct Post Photo operation with a connected TikTok Account.\n3. Provide Photo URLs, one public JPEG or WEBP URL per line (PNG is rejected), up to 35 images.\n4. Set a Title (max 90 characters), an optional Description (max 4000 characters), the Cover Photo Index, and a Privacy Level.\n5. Check progress with Get Post Status using the returned Publish ID.\n\n## Output\nReturn the Publish ID and the final status of the photo post.', + }, + { + name: 'check-tiktok-post-status', + description: 'Poll the status of a TikTok post or draft until it completes or fails.', + content: + '# Check TikTok Post Status\n\nTrack the outcome of a post or draft submitted with any TikTok publish operation.\n\n## Steps\n1. Capture the Publish ID returned by Direct Post Video, Upload Video Draft, Direct Post Photo, or Upload Photo Draft.\n2. Call Get Post Status with that Publish ID.\n3. Branch on the returned status: PROCESSING_UPLOAD/PROCESSING_DOWNLOAD means still in progress, SEND_TO_USER_INBOX means a draft is waiting on the user, PUBLISH_COMPLETE means it succeeded, and FAILED means it did not (read failReason for why).\n4. Repeat on a delay for in-progress statuses until a terminal state is reached.\n\n## Output\nReturn the final status, failReason (if any), and the publiclyAvailablePostId once published.', + }, + { + name: 'summarize-tiktok-video-performance', + description: "List a creator's recent TikTok videos and summarize engagement for reporting.", + content: + "# Summarize TikTok Video Performance\n\nPull a creator's recent videos and turn the metadata into a readable report.\n\n## Steps\n1. Use List Videos with a connected TikTok Account to fetch recent videos (paginate with the Cursor if more than one page is needed).\n2. For specific videos already known by ID, use Query Videos instead to refresh their metadata.\n3. Ask an agent to summarize the results — highlight top performers by duration/engagement signals available in the metadata and note any patterns.\n4. Optionally use Get User Info alongside this to report overall follower and like counts.\n\n## Output\nA structured summary or table of videos with their titles, share URLs, and key metadata, suitable for posting to a report or chat.", + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index 89919671fc9..f8c74a4e986 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -294,6 +294,7 @@ import { TemporalBlock, TemporalBlockMeta } from '@/blocks/blocks/temporal' import { TextractBlock, TextractBlockMeta, TextractV2Block } from '@/blocks/blocks/textract' import { ThinkingBlock } from '@/blocks/blocks/thinking' import { ThriveBlock, ThriveBlockMeta } from '@/blocks/blocks/thrive' +import { TikTokBlock, TikTokBlockMeta } from '@/blocks/blocks/tiktok' import { TinybirdBlock, TinybirdBlockMeta } from '@/blocks/blocks/tinybird' import { TranslateBlock } from '@/blocks/blocks/translate' import { TrelloBlock, TrelloBlockMeta } from '@/blocks/blocks/trello' @@ -604,6 +605,7 @@ export const BLOCK_REGISTRY: Record = { textract_v2: TextractV2Block, thinking: ThinkingBlock, thrive: ThriveBlock, + tiktok: TikTokBlock, tinybird: TinybirdBlock, translate: TranslateBlock, trello: TrelloBlock, @@ -871,6 +873,7 @@ export const BLOCK_META_REGISTRY: Record = { temporal: TemporalBlockMeta, textract: TextractBlockMeta, thrive: ThriveBlockMeta, + tiktok: TikTokBlockMeta, tinybird: TinybirdBlockMeta, trello: TrelloBlockMeta, trigger_dev: TriggerDevBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c213134b0e1..b11ef7e3835 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1274,6 +1274,31 @@ export function xAIIcon(props: SVGProps) { ) } +export function TikTokIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function xIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/api/contracts/tiktok-tools.ts b/apps/sim/lib/api/contracts/tiktok-tools.ts new file mode 100644 index 00000000000..569871c7258 --- /dev/null +++ b/apps/sim/lib/api/contracts/tiktok-tools.ts @@ -0,0 +1,42 @@ +import { z } from 'zod' +import { + type ContractBodyInput, + type ContractJsonResponse, + defineRouteContract, +} from '@/lib/api/contracts/types' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' + +const tiktokPublishVideoPostInfoSchema = z.object({ + title: z.string().optional(), + privacy_level: z.string().optional(), + disable_duet: z.boolean().optional(), + disable_stitch: z.boolean().optional(), + disable_comment: z.boolean().optional(), + video_cover_timestamp_ms: z.number().optional(), + is_aigc: z.boolean().optional(), + brand_content_toggle: z.boolean().optional(), + brand_organic_toggle: z.boolean().optional(), +}) + +export const tiktokPublishVideoBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + mode: z.enum(['direct', 'draft']), + file: RawFileInputSchema, + postInfo: tiktokPublishVideoPostInfoSchema.optional().nullable(), +}) + +export const tiktokPublishVideoResponseSchema = z.object({ + success: z.boolean(), + output: z.object({ publishId: z.string() }).optional(), + error: z.string().optional(), +}) + +export const tiktokPublishVideoContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/tiktok/publish-video', + body: tiktokPublishVideoBodySchema, + response: { mode: 'json', schema: tiktokPublishVideoResponseSchema }, +}) + +export type TikTokPublishVideoBody = ContractBodyInput +export type TikTokPublishVideoResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/webhooks.ts b/apps/sim/lib/api/contracts/webhooks.ts index 4903d558614..04c2d3ad5ba 100644 --- a/apps/sim/lib/api/contracts/webhooks.ts +++ b/apps/sim/lib/api/contracts/webhooks.ts @@ -258,3 +258,38 @@ export const webhookTriggerPostContract = defineRouteContract({ schema: z.unknown(), }, }) + +/** + * TikTok app-level webhook ingress. Signature is verified from the raw body + * before this schema runs; `content` remains a JSON string per TikTok docs. + */ +export const tiktokWebhookEnvelopeSchema = z.object({ + client_key: z.string().min(1).max(255), + event: z.string().min(1).max(255), + create_time: z.number().int().nonnegative(), + user_openid: z.string().min(1).max(255), + content: z.string().max(1_000_000), +}) + +export type TikTokWebhookEnvelope = z.input + +export const tiktokWebhookHeadersSchema = z.object({ + 'tiktok-signature': z.string().min(1), +}) + +export const tiktokWebhookResponseSchema = z.union([ + z.object({ ok: z.literal(true) }), + z.object({ error: z.string().min(1) }), +]) + +export const tiktokWebhookContract = defineRouteContract({ + method: 'POST', + path: '/api/webhooks/tiktok', + headers: tiktokWebhookHeadersSchema, + // Body is validated after HMAC verification against the raw payload. + body: tiktokWebhookEnvelopeSchema, + response: { + mode: 'json', + schema: tiktokWebhookResponseSchema, + }, +}) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 96c86881aa9..d9d6443fdb4 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1938,6 +1938,77 @@ export const auth = betterAuth({ }, }, + { + providerId: 'tiktok', + clientId: env.TIKTOK_CLIENT_ID as string, + clientSecret: env.TIKTOK_CLIENT_SECRET as string, + authorizationUrl: 'https://www.tiktok.com/v2/auth/authorize/', + tokenUrl: 'https://open.tiktokapis.com/v2/oauth/token/', + scopes: getCanonicalScopesForProvider('tiktok'), + responseType: 'code', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`, + // TikTok requires the app identifier under `client_key`, not the standard + // `client_id`. The library always sends `client_id` too, but TikTok ignores + // unrecognized params, so adding `client_key` here (for both the authorize + // redirect and the token exchange) is sufficient without patching the library. + // + // TikTok also requires a comma-separated `scope` list, but the library always + // joins scopes with a space (no configurable separator in the generic-oauth + // plugin's authorize route). `additionalParams` is applied via + // `url.searchParams.set(key, value)` after the default scope is set, so + // overriding `scope` here replaces the space-joined value with a comma-joined + // one that TikTok can actually parse. + authorizationUrlParams: { + client_key: env.TIKTOK_CLIENT_ID as string, + scope: getCanonicalScopesForProvider('tiktok').join(','), + }, + tokenUrlParams: { client_key: env.TIKTOK_CLIENT_ID as string }, + getUserInfo: async (tokens) => { + try { + const response = await fetch( + 'https://open.tiktokapis.com/v2/user/info/?fields=open_id,display_name,avatar_url', + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + } + ) + + if (!response.ok) { + await response.text().catch(() => {}) + logger.error('Error fetching TikTok user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const profile = await response.json() + const user = profile.data?.user + + if (!user?.open_id) { + logger.error('Invalid TikTok profile response:', profile) + return null + } + + const now = new Date() + + return { + id: `${user.open_id}-${generateId()}`, + name: user.display_name || 'TikTok User', + email: `${user.open_id}@tiktok.user`, + image: user.avatar_url || undefined, + emailVerified: false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in TikTok getUserInfo:', { error }) + return null + } + }, + }, + { providerId: 'confluence', clientId: env.CONFLUENCE_CLIENT_ID as string, diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index 233be772e99..eecf53ff772 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -21,6 +21,7 @@ const JOB_TYPE_TO_TASK_ID: Record = { 'workflow-execution': 'workflow-execution', 'schedule-execution': 'schedule-execution', 'webhook-execution': 'webhook-execution', + 'tiktok-webhook-ingress': 'tiktok-webhook-ingress', 'resume-execution': 'resume-execution', 'workflow-group-cell': 'workflow-group-cell', 'cleanup-logs': 'cleanup-logs', diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index 3acb9927c25..95d6c83dbba 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -24,6 +24,7 @@ export type JobType = | 'workflow-execution' | 'schedule-execution' | 'webhook-execution' + | 'tiktok-webhook-ingress' | 'resume-execution' | 'workflow-group-cell' | 'cleanup-logs' diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index d7903b9a380..67259e2cba6 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -343,6 +343,8 @@ export const env = createEnv({ X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret + TIKTOK_CLIENT_ID: z.string().optional(), // TikTok OAuth client key (TikTok calls this "client_key") + TIKTOK_CLIENT_SECRET: z.string().optional(), // TikTok OAuth client secret CONFLUENCE_CLIENT_ID: z.string().optional(), // Atlassian Confluence OAuth client ID CONFLUENCE_CLIENT_SECRET: z.string().optional(), // Atlassian Confluence OAuth client secret JIRA_CLIENT_ID: z.string().optional(), // Atlassian Jira OAuth client ID diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index f7c5ddf8d19..c8bc3a5b104 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -212,6 +212,7 @@ import { TemporalIcon, TextractIcon, ThriveIcon, + TikTokIcon, TinybirdIcon, TrelloIcon, TriggerDevIcon, @@ -449,6 +450,7 @@ export const blockTypeToIconMap: Record = { temporal: TemporalIcon, textract_v2: TextractIcon, thrive: ThriveIcon, + tiktok: TikTokIcon, tinybird: TinybirdIcon, trello: TrelloIcon, trigger_dev: TriggerDevIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 8e741d71e57..ad32e0234ab 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -18691,6 +18691,103 @@ "integrationType": "hr", "tags": ["content-management", "knowledge-base", "automation"] }, + { + "type": "tiktok", + "slug": "tiktok", + "name": "TikTok", + "description": "Access TikTok profiles and videos, and publish content", + "longDescription": "Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata. Publish videos and photos directly to TikTok (or send them to the inbox as drafts) from a public URL or a file uploaded in the workflow, then track post status.", + "bgColor": "#000000", + "iconName": "TikTokIcon", + "docsUrl": "https://docs.sim.ai/integrations/tiktok", + "operations": [ + { + "name": "Get User Info", + "description": "Get the authenticated TikTok user profile information including display name, avatar, bio, follower count, and video statistics." + }, + { + "name": "List Videos", + "description": "Get a list of the authenticated user's TikTok videos with cover images, titles, and metadata. Supports pagination." + }, + { + "name": "Query Videos", + "description": "Query specific TikTok videos by their IDs to get fresh metadata including cover images, embed links, and video details." + }, + { + "name": "Query Creator Info", + "description": "Check if the authenticated TikTok user can post content and retrieve their available privacy options, interaction settings, and maximum video duration. Required before any post per TikTok UX guidelines." + }, + { + "name": "Direct Post Video", + "description": "Publish a video to TikTok, either by public URL (TikTok fetches it) or by uploading a file from the workflow. Rate limit: 6 requests per minute per user." + }, + { + "name": "Upload Video Draft", + "description": "Send a video to the authenticated user's TikTok inbox (by public URL or uploaded file) for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Rate limit: 6 requests per minute per user." + }, + { + "name": "Direct Post Photo", + "description": "Publish one or more photos to TikTok from public URLs. TikTok will fetch each image and post them as a photo carousel. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user." + }, + { + "name": "Upload Photo Draft", + "description": "Send one or more photos (from public URLs) to the authenticated user's TikTok inbox for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user." + }, + { + "name": "Get Post Status", + "description": "Check the status of a post initiated with Direct Post Video, Upload Video Draft, Direct Post Photo, or Upload Photo Draft. Use the publishId returned from the post request to track progress." + } + ], + "operationCount": 9, + "triggers": [ + { + "id": "tiktok_post_publish_complete", + "name": "TikTok Post Publish Complete", + "description": "Trigger when a TikTok Content Posting publish completes" + }, + { + "id": "tiktok_post_publish_failed", + "name": "TikTok Post Publish Failed", + "description": "Trigger when a TikTok Content Posting publish fails" + }, + { + "id": "tiktok_post_inbox_delivered", + "name": "TikTok Post Inbox Delivered", + "description": "Trigger when a draft notification is delivered to the creator inbox" + }, + { + "id": "tiktok_post_publicly_available", + "name": "TikTok Post Publicly Available", + "description": "Trigger when a published post becomes publicly viewable on TikTok" + }, + { + "id": "tiktok_post_no_longer_public", + "name": "TikTok Post No Longer Public", + "description": "Trigger when a post is no longer publicly viewable on TikTok" + }, + { + "id": "tiktok_video_publish_completed", + "name": "TikTok Video Publish Completed", + "description": "Trigger when a Share Kit / Video Kit upload is published by the user" + }, + { + "id": "tiktok_video_upload_failed", + "name": "TikTok Video Upload Failed", + "description": "Trigger when a Share Kit / Video Kit upload fails in TikTok" + }, + { + "id": "tiktok_authorization_removed", + "name": "TikTok Authorization Removed", + "description": "Trigger when a user deauthorizes your TikTok app" + } + ], + "triggerCount": 8, + "authType": "oauth", + "oauthServiceId": "tiktok", + "category": "tools", + "integrationType": "communication", + "tags": ["marketing", "content-management"] + }, { "type": "tinybird", "slug": "tinybird", diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 8f6b17100fe..578271fe85b 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -43,6 +43,7 @@ import { ShopifyIcon, SlackIcon, SpotifyIcon, + TikTokIcon, TrelloIcon, VertexIcon, WealthboxIcon, @@ -427,6 +428,28 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'x', }, + tiktok: { + name: 'TikTok', + icon: TikTokIcon, + services: { + tiktok: { + name: 'TikTok', + description: 'Read profile info and videos, and publish content to TikTok.', + providerId: 'tiktok', + icon: TikTokIcon, + baseProviderIcon: TikTokIcon, + scopes: [ + 'user.info.basic', + 'user.info.profile', + 'user.info.stats', + 'video.publish', + 'video.upload', + 'video.list', + ], + }, + }, + defaultService: 'tiktok', + }, atlassian: { name: 'Atlassian', icon: JiraIcon, @@ -1057,6 +1080,11 @@ interface ProviderAuthConfig { * instead of the default application/x-www-form-urlencoded. Used by Notion. */ useJsonBody?: boolean + /** + * Body param name to use for the client identifier instead of the standard `client_id`. + * TikTok requires `client_key` instead. + */ + clientIdParamName?: string } /** @@ -1093,6 +1121,21 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: true, } } + case 'tiktok': { + const { clientId, clientSecret } = getCredentials( + env.TIKTOK_CLIENT_ID, + env.TIKTOK_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://open.tiktokapis.com/v2/oauth/token/', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: true, + // TikTok requires `client_key` in the token request body instead of `client_id`. + clientIdParamName: 'client_key', + } + } case 'confluence': { const { clientId, clientSecret } = getCredentials( env.CONFLUENCE_CLIENT_ID, @@ -1455,7 +1498,7 @@ function buildAuthRequest( headers.Authorization = `Basic ${basicAuth}` } else { // Use body credentials - include client credentials in request body - bodyParams.client_id = config.clientId + bodyParams[config.clientIdParamName || 'client_id'] = config.clientId if (config.clientSecret) { bodyParams.client_secret = config.clientSecret } diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index bd1de917bf2..4e25f6ff1a2 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -29,6 +29,7 @@ export type OAuthProvider = | 'google-meet' | 'vertex-ai' | 'x' + | 'tiktok' | 'confluence' | 'airtable' | 'notion' @@ -81,6 +82,7 @@ export type OAuthService = | 'google-meet' | 'vertex-ai' | 'x' + | 'tiktok' | 'confluence' | 'airtable' | 'notion' diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 6d903a6cd1c..1ca1655e1f3 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -122,6 +122,14 @@ export const SCOPE_DESCRIPTIONS: Record = { 'mute.write': 'Mute and unmute users', 'offline.access': 'Access account when not using the application', + // TikTok scopes + 'user.info.basic': "Read a user's profile info (open id, avatar, display name)", + 'user.info.profile': "Read a user's profile info (bio, verification status, username)", + 'user.info.stats': "Read a user's stats (follower, following, likes, and video counts)", + 'video.publish': "Directly post content to a user's TikTok profile", + 'video.upload': "Share content to a creator's account as a draft for further edit and post", + 'video.list': "Read a user's public TikTok videos", + // Airtable scopes 'data.records:read': 'Read records', 'data.records:write': 'Write to records', diff --git a/apps/sim/lib/webhooks/deploy.test.ts b/apps/sim/lib/webhooks/deploy.test.ts index f5c111ed68a..3958b74ab21 100644 --- a/apps/sim/lib/webhooks/deploy.test.ts +++ b/apps/sim/lib/webhooks/deploy.test.ts @@ -13,7 +13,7 @@ vi.mock('@/lib/webhooks/providers', () => ({ getProviderHandler: vi.fn() })) vi.mock('@/lib/webhooks/provider-subscriptions', () => ({ cleanupExternalWebhook: vi.fn(), createExternalWebhookSubscription: vi.fn(), - shouldRecreateExternalWebhookSubscription: vi.fn(), + hasWebhookConfigChanged: vi.fn(), })) vi.mock('@/lib/webhooks/utils.server', () => ({ findConflictingWebhookPathOwner: vi.fn(), @@ -29,7 +29,12 @@ const trigger = (subBlocks: Partial[]): { subBlocks: SubBlockCon }) const driveTrigger = trigger([ - { id: 'triggerCredentials', mode: 'trigger', canonicalParamId: 'oauthCredential' }, + { + id: 'triggerCredentials', + mode: 'trigger', + canonicalParamId: 'oauthCredential', + serviceId: 'google-drive', + }, { id: 'folderId', mode: 'trigger', canonicalParamId: 'folderId', required: false }, { id: 'manualFolderId', mode: 'trigger-advanced', canonicalParamId: 'folderId', required: false }, ]) @@ -63,6 +68,15 @@ describe('buildProviderConfig canonical collapse', () => { expect(providerConfig.folderId).toBe('BASIC') }) + it('returns the credential reference and canonical OAuth service for deploy validation', () => { + const block = makeBlock('google_drive_poller', { triggerCredentials: 'credential-1' }) + const result = buildProviderConfig(block, 'google_drive_poller', driveTrigger) + + expect(result.credentialReference).toBe('credential-1') + expect(result.credentialServiceId).toBe('google-drive') + expect(result.providerConfig.credentialId).toBeUndefined() + }) + it('writes the active (advanced) value under the canonical key when only advanced is set', () => { const block = makeBlock('google_drive_poller', { manualFolderId: 'ADVANCED' }) const { providerConfig } = buildProviderConfig(block, 'google_drive_poller', driveTrigger) diff --git a/apps/sim/lib/webhooks/deploy.ts b/apps/sim/lib/webhooks/deploy.ts index 93e21eb6b87..825adb147f7 100644 --- a/apps/sim/lib/webhooks/deploy.ts +++ b/apps/sim/lib/webhooks/deploy.ts @@ -1,14 +1,14 @@ import { db } from '@sim/db' -import { webhook, workflowDeploymentVersion } from '@sim/db/schema' +import { credential, webhook, workflowDeploymentVersion } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateShortId } from '@sim/utils/id' -import { and, eq, inArray, isNull } from 'drizzle-orm' +import { and, eq, inArray, isNull, or } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { PendingWebhookVerificationTracker } from '@/lib/webhooks/pending-verification' import { cleanupExternalWebhook, createExternalWebhookSubscription, - shouldRecreateExternalWebhookSubscription, + hasWebhookConfigChanged, } from '@/lib/webhooks/provider-subscriptions' import { getProviderHandler } from '@/lib/webhooks/providers' import { findConflictingWebhookPathOwner } from '@/lib/webhooks/utils.server' @@ -35,6 +35,14 @@ interface TriggerSaveResult { error?: TriggerSaveError } +interface BuiltProviderConfig { + providerConfig: Record + missingFields: string[] + credentialReference?: string + credentialServiceId?: string + triggerPath: string +} + export async function validateTriggerWebhookConfigForDeploy( blocks: Record ): Promise { @@ -198,12 +206,7 @@ export function buildProviderConfig( block: BlockState, triggerId: string, triggerDef: { subBlocks: SubBlockConfig[] } -): { - providerConfig: Record - missingFields: string[] - credentialId?: string - triggerPath: string -} { +): BuiltProviderConfig { const triggerConfigValue = getSubBlockValue(block, 'triggerConfig') const baseConfig = triggerConfigValue && typeof triggerConfigValue === 'object' @@ -282,11 +285,10 @@ export function buildProviderConfig( missingFields.push(credentialConfig.title || 'Credentials') } - let credentialId: string | undefined - if (typeof triggerCredentials === 'string' && triggerCredentials.length > 0) { - credentialId = triggerCredentials - providerConfig.credentialId = credentialId - } + const credentialReference = + typeof triggerCredentials === 'string' && triggerCredentials.length > 0 + ? triggerCredentials + : undefined providerConfig.triggerId = triggerId @@ -296,7 +298,38 @@ export function buildProviderConfig( ? triggerPathValue : block.id - return { providerConfig, missingFields, credentialId, triggerPath } + return { + providerConfig, + missingFields, + credentialReference, + credentialServiceId: credentialConfig?.serviceId, + triggerPath, + } +} + +/** + * Resolves a trigger credential reference to its canonical platform credential ID while enforcing + * that the credential belongs to the deployed workflow's workspace and OAuth service. + */ +async function resolveTriggerCredentialId( + credentialReference: string, + workspaceId: string, + serviceId: string +): Promise { + const [resolvedCredential] = await db + .select({ id: credential.id }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'oauth'), + eq(credential.providerId, serviceId), + or(eq(credential.id, credentialReference), eq(credential.accountId, credentialReference)) + ) + ) + .limit(1) + + return resolvedCredential?.id ?? null } async function configurePollingIfNeeded( @@ -387,11 +420,8 @@ export async function saveTriggerWebhooksForDeploy({ const triggerDef = getTrigger(triggerId) const provider = triggerDef.provider - const { providerConfig, missingFields, triggerPath } = buildProviderConfig( - block, - triggerId, - triggerDef - ) + const { providerConfig, missingFields, credentialReference, credentialServiceId, triggerPath } = + buildProviderConfig(block, triggerId, triggerDef) if (missingFields.length > 0) { return { @@ -414,6 +444,38 @@ export async function saveTriggerWebhooksForDeploy({ } } + let credentialId: string | undefined + if (credentialReference && credentialServiceId) { + const workflowWorkspaceId = + typeof workflow.workspaceId === 'string' ? workflow.workspaceId : undefined + if (!workflowWorkspaceId) { + return { + success: false, + error: { + message: `Cannot validate credentials for ${triggerDef.name || triggerId} without a workflow workspace`, + status: 400, + }, + } + } + + credentialId = + (await resolveTriggerCredentialId( + credentialReference, + workflowWorkspaceId, + credentialServiceId + )) ?? undefined + if (!credentialId) { + return { + success: false, + error: { + message: `The selected ${credentialServiceId} credential is not available in this workspace`, + status: 400, + }, + } + } + providerConfig.credentialId = credentialId + } + const pathConflict = await findConflictingWebhookPathOwner({ path: triggerPath, workflowId, @@ -450,12 +512,8 @@ export async function saveTriggerWebhooksForDeploy({ const existingConfig = (existingWh.providerConfig as Record) || {} const needsRecreation = forceRecreateSubscriptions || - shouldRecreateExternalWebhookSubscription({ - previousProvider: existingWh.provider as string, - nextProvider: provider, - previousConfig: existingConfig, - nextConfig: providerConfig, - }) + existingWh.provider !== provider || + hasWebhookConfigChanged(existingConfig, providerConfig) if (needsRecreation) { webhooksToDelete.push(existingWh) diff --git a/apps/sim/lib/webhooks/processor.test.ts b/apps/sim/lib/webhooks/processor.test.ts index 1e823ed8af5..cbb7f5cdcc4 100644 --- a/apps/sim/lib/webhooks/processor.test.ts +++ b/apps/sim/lib/webhooks/processor.test.ts @@ -2,24 +2,28 @@ * @vitest-environment node */ +import type { webhook, workflow } from '@sim/db/schema' import { createMockRequest, envFlagsMock, executionPreprocessingMock, executionPreprocessingMockFns, } from '@sim/testing' +import type { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockGenerateId, mockEnqueue, mockGetJobQueue, + mockProviderHandler, mockShouldExecuteInline, mockWebhookLookupResult, } = vi.hoisted(() => ({ mockGenerateId: vi.fn(), mockEnqueue: vi.fn(), mockGetJobQueue: vi.fn(), + mockProviderHandler: { current: {} as Record }, mockShouldExecuteInline: vi.fn(), mockWebhookLookupResult: { rows: [] as Array<{ webhook: any; workflow: any }> }, })) @@ -95,7 +99,7 @@ vi.mock('@/lib/webhooks/utils.server', () => ({ })) vi.mock('@/lib/webhooks/providers', () => ({ - getProviderHandler: vi.fn().mockReturnValue({}), + getProviderHandler: vi.fn(() => mockProviderHandler.current), })) vi.mock('@/background/webhook-execution', () => ({ @@ -122,11 +126,55 @@ vi.mock('@/triggers/jira/utils', () => ({ isJiraEventMatch: vi.fn().mockReturnValue(true), })) -import { - checkWebhookPreprocessing, - findAllWebhooksForPath, - queueWebhookExecution, -} from '@/lib/webhooks/processor' +import { dispatchResolvedWebhookTarget, findAllWebhooksForPath } from '@/lib/webhooks/processor' + +type WebhookRecord = typeof webhook.$inferSelect +type WorkflowRecord = typeof workflow.$inferSelect + +function makeWebhookRecord(overrides: Partial): WebhookRecord { + const now = new Date('2026-01-01T00:00:00.000Z') + return { + id: 'webhook-1', + workflowId: 'workflow-1', + deploymentVersionId: null, + blockId: null, + path: 'incoming/test', + provider: 'generic', + providerConfig: {}, + isActive: true, + failedCount: 0, + lastFailedAt: null, + archivedAt: null, + createdAt: now, + updatedAt: now, + ...overrides, + } +} + +function makeWorkflowRecord(overrides: Partial): WorkflowRecord { + const now = new Date('2026-01-01T00:00:00.000Z') + return { + id: 'workflow-1', + userId: 'owner-1', + workspaceId: 'workspace-1', + folderId: null, + sortOrder: 0, + name: 'Webhook workflow', + description: null, + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: true, + deployedAt: now, + isPublicApi: false, + locked: false, + runCount: 0, + lastRunAt: null, + variables: {}, + archivedAt: null, + ...overrides, + } +} describe('findAllWebhooksForPath cross-tenant collision', () => { beforeEach(() => { @@ -200,64 +248,38 @@ describe('webhook processor execution identity', () => { }) mockEnqueue.mockResolvedValue('job-1') mockGetJobQueue.mockResolvedValue({ enqueue: mockEnqueue }) + mockProviderHandler.current = {} mockShouldExecuteInline.mockReturnValue(false) mockGenerateId.mockReturnValue('generated-execution-id') }) it('reuses preprocessing execution identity when queueing a polling webhook', async () => { - const preprocessingResult = await checkWebhookPreprocessing( - { - id: 'workflow-1', - userId: 'owner-1', - workspaceId: 'workspace-1', - }, - { - id: 'webhook-1', - path: 'incoming/gmail', - provider: 'gmail', - }, - 'request-1' - ) - - expect(preprocessingResult).toMatchObject({ - error: null, - actorUserId: 'actor-user-1', + const expectedCorrelation = { executionId: 'generated-execution-id', - correlation: { - executionId: 'generated-execution-id', - requestId: 'request-1', - source: 'webhook', - workflowId: 'workflow-1', - webhookId: 'webhook-1', - path: 'incoming/gmail', - provider: 'gmail', - triggerType: 'webhook', - }, - }) - - await queueWebhookExecution( - { - id: 'webhook-1', + requestId: 'request-1', + source: 'webhook', + workflowId: 'workflow-1', + webhookId: 'webhook-1', + path: 'incoming/gmail', + provider: 'gmail', + triggerType: 'webhook', + } + + const result = await dispatchResolvedWebhookTarget( + makeWebhookRecord({ path: 'incoming/gmail', provider: 'gmail', - providerConfig: {}, - blockId: 'block-1', - }, - { - id: 'workflow-1', - workspaceId: 'workspace-1', - }, + }), + makeWorkflowRecord({}), { event: 'message.received' }, - createMockRequest('POST', { event: 'message.received' }) as any, + createMockRequest('POST', { event: 'message.received' }) as NextRequest, { requestId: 'request-1', path: 'incoming/gmail', - actorUserId: preprocessingResult.actorUserId, - executionId: preprocessingResult.executionId, - correlation: preprocessingResult.correlation, } ) + expect(result.outcome).toBe('queued') expect(mockGenerateId).toHaveBeenCalledTimes(1) expect(mockEnqueue).toHaveBeenCalledWith( 'webhook-execution', @@ -270,9 +292,41 @@ describe('webhook processor execution identity', () => { workflowId: 'workflow-1', workspaceId: 'workspace-1', userId: 'actor-user-1', - correlation: preprocessingResult.correlation, + correlation: expectedCorrelation, }), }) ) }) + + it('routes queue-mode providers through the durable job backend', async () => { + mockProviderHandler.current = { executionMode: 'queue' } + + const result = await dispatchResolvedWebhookTarget( + makeWebhookRecord({ + id: 'webhook-2', + path: 'tiktok', + provider: 'tiktok', + }), + makeWorkflowRecord({ + id: 'workflow-2', + workspaceId: 'workspace-2', + }), + { event: 'post.publish.complete' }, + createMockRequest('POST', { event: 'post.publish.complete' }) as NextRequest, + { + requestId: 'request-2', + } + ) + + expect(result.outcome).toBe('queued') + expect(result.response.status).toBe(200) + expect(mockEnqueue).toHaveBeenCalledWith( + 'webhook-execution', + expect.objectContaining({ + provider: 'tiktok', + workflowId: 'workflow-2', + }), + expect.any(Object) + ) + }) }) diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 80fb7b488f5..f674e816c64 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -22,6 +22,7 @@ import { requiresPendingWebhookVerification, } from '@/lib/webhooks/pending-verification' import { getProviderHandler } from '@/lib/webhooks/providers' +import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { blockExistsInDeployment } from '@/lib/workflows/persistence/utils' import { SIM_TRIGGER_PROVIDER } from '@/lib/workspace-events/constants' import { executeWebhookJob } from '@/background/webhook-execution' @@ -514,14 +515,51 @@ export async function checkWebhookPreprocessing( } } -export async function queueWebhookExecution( - foundWebhook: any, - foundWorkflow: any, - body: any, +export type WebhookDispatchOutcome = 'queued' | 'ignored' | 'failed' + +export interface WebhookDispatchResult { + outcome: WebhookDispatchOutcome + response: NextResponse + reason: + | 'queued' + | 'event-mismatch' + | 'filtered' + | 'preprocessing' + | 'block-missing' + | 'queue-failed' +} + +type ResolvedWebhookRecord = Omit & { + provider: string + providerConfig: Record +} + +function parseProviderConfig(value: unknown): Record { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} +} + +function getCredentialId(providerConfig: Record): string | undefined { + return typeof providerConfig.credentialId === 'string' ? providerConfig.credentialId : undefined +} + +function shouldUseDurableQueue(provider: string, handler: WebhookProviderHandler): boolean { + return ( + isPollingWebhookProvider(provider) || + provider === SIM_TRIGGER_PROVIDER || + handler.executionMode === 'queue' + ) +} + +async function queueWebhookExecutionWithResult( + foundWebhook: ResolvedWebhookRecord, + foundWorkflow: typeof workflow.$inferSelect, + body: unknown, request: NextRequest, options: WebhookProcessorOptions -): Promise { - const providerConfig = (foundWebhook.providerConfig as Record) || {} +): Promise { + const providerConfig = foundWebhook.providerConfig ?? {} const handler = getProviderHandler(foundWebhook.provider) try { @@ -536,11 +574,19 @@ export async function queueWebhookExecution( }) if (result !== true) { if (result instanceof NextResponse) { - return result + return { + outcome: result.ok ? 'ignored' : 'failed', + response: result, + reason: 'event-mismatch', + } + } + return { + outcome: 'ignored', + response: NextResponse.json({ + message: 'Event type does not match trigger configuration. Ignoring.', + }), + reason: 'event-mismatch', } - return NextResponse.json({ - message: 'Event type does not match trigger configuration. Ignoring.', - }) } } @@ -553,12 +599,19 @@ export async function queueWebhookExecution( ) } - const credentialId = providerConfig.credentialId as string | undefined + const credentialId = getCredentialId(providerConfig) const actorUserId = options.actorUserId if (!actorUserId) { logger.error(`[${options.requestId}] No actorUserId provided for webhook ${foundWebhook.id}`) - return NextResponse.json({ error: 'Unable to resolve billing account' }, { status: 500 }) + return { + outcome: 'failed', + response: NextResponse.json( + { error: 'Unable to resolve billing account' }, + { status: 500 } + ), + reason: 'queue-failed', + } } const executionId = options.executionId ?? generateId() @@ -574,6 +627,7 @@ export async function queueWebhookExecution( provider: foundWebhook.provider, triggerType: 'webhook', } satisfies AsyncExecutionCorrelation) + const workspaceId = foundWorkflow.workspaceId ?? undefined const payload = { webhookId: foundWebhook.id, @@ -586,8 +640,8 @@ export async function queueWebhookExecution( body, headers, path: options.path || foundWebhook.path, - blockId: foundWebhook.blockId, - workspaceId: foundWorkflow.workspaceId, + blockId: foundWebhook.blockId ?? undefined, + workspaceId, ...(credentialId ? { credentialId } : {}), ...(options.receivedAt !== undefined ? { webhookReceivedAt: options.receivedAt } : {}), ...(options.triggerTimestampMs !== undefined @@ -595,26 +649,26 @@ export async function queueWebhookExecution( : {}), } - const isPolling = isPollingWebhookProvider(payload.provider) + const shouldUseQueue = shouldUseDurableQueue(payload.provider, handler) - if (isPolling && !shouldExecuteInline()) { + if (shouldUseQueue && !shouldExecuteInline()) { const jobId = await (await getJobQueue()).enqueue('webhook-execution', payload, { metadata: { workflowId: foundWorkflow.id, - workspaceId: foundWorkflow.workspaceId, + workspaceId, userId: actorUserId, correlation, }, }) logger.info( - `[${options.requestId}] Queued polling webhook execution task ${jobId} for ${foundWebhook.provider} webhook via job queue` + `[${options.requestId}] Queued webhook execution task ${jobId} for ${foundWebhook.provider} webhook via job queue` ) } else { const jobQueue = await getInlineJobQueue() const jobId = await jobQueue.enqueue('webhook-execution', payload, { metadata: { workflowId: foundWorkflow.id, - workspaceId: foundWorkflow.workspaceId, + workspaceId, userId: actorUserId, correlation, }, @@ -659,20 +713,95 @@ export async function queueWebhookExecution( const successResponse = handler.formatSuccessResponse?.(providerConfig) ?? null if (successResponse) { - return successResponse + return { outcome: 'queued', response: successResponse, reason: 'queued' } } - return NextResponse.json({ message: 'Webhook processed' }) + return { + outcome: 'queued', + response: NextResponse.json({ message: 'Webhook processed' }), + reason: 'queued', + } } catch (error: unknown) { logger.error(`[${options.requestId}] Failed to queue webhook execution:`, error) const errorResponse = handler.formatQueueErrorResponse?.() ?? null if (errorResponse) { - return errorResponse + return { outcome: 'failed', response: errorResponse, reason: 'queue-failed' } + } + + return { + outcome: 'failed', + response: NextResponse.json({ error: 'Internal server error' }, { status: 500 }), + reason: 'queue-failed', + } + } +} + +/** + * Runs the common post-authentication lifecycle for a resolved webhook target and returns a typed + * outcome so app-level fanout workers do not infer queue state from HTTP response bodies. + */ +export async function dispatchResolvedWebhookTarget( + foundWebhook: typeof webhook.$inferSelect, + foundWorkflow: typeof workflow.$inferSelect, + body: unknown, + request: NextRequest, + options: WebhookProcessorOptions +): Promise { + if (!foundWebhook.provider) { + return { + outcome: 'failed', + response: NextResponse.json({ error: 'Webhook provider is missing' }, { status: 500 }), + reason: 'queue-failed', + } + } + + const webhookRecord = { + ...foundWebhook, + provider: foundWebhook.provider, + providerConfig: parseProviderConfig(foundWebhook.providerConfig), + } + const preprocessResult = await checkWebhookPreprocessing( + foundWorkflow, + webhookRecord, + options.requestId + ) + if (preprocessResult.error) { + return { + outcome: 'failed', + response: preprocessResult.error, + reason: 'preprocessing', + } + } + + if (webhookRecord.blockId) { + const blockExists = await blockExistsInDeployment(foundWorkflow.id, webhookRecord.blockId) + if (!blockExists) { + const verificationResponse = handlePreDeploymentVerification(webhookRecord, options.requestId) + return { + outcome: 'ignored', + response: + verificationResponse ?? + new NextResponse('Trigger block not found in deployment', { status: 404 }), + reason: 'block-missing', + } } + } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + if (shouldSkipWebhookEvent(webhookRecord, body, options.requestId)) { + return { + outcome: 'ignored', + response: NextResponse.json({ message: 'Webhook event ignored' }), + reason: 'filtered', + } } + + return queueWebhookExecutionWithResult(webhookRecord, foundWorkflow, body, request, { + ...options, + actorUserId: preprocessResult.actorUserId, + executionId: preprocessResult.executionId, + correlation: preprocessResult.correlation, + }) } export interface PolledWebhookEventResult { @@ -734,8 +863,8 @@ export async function processPolledWebhookEvent( } } - const providerConfig = (foundWebhook.providerConfig as Record) || {} - const credentialId = providerConfig.credentialId as string | undefined + const providerConfig = parseProviderConfig(foundWebhook.providerConfig) + const credentialId = getCredentialId(providerConfig) const actorUserId = preprocessResult.actorUserId if (!actorUserId) { @@ -774,8 +903,7 @@ export async function processPolledWebhookEvent( ...(credentialId ? { credentialId } : {}), } - const isQueueRoutedProvider = - isPollingWebhookProvider(payload.provider) || payload.provider === SIM_TRIGGER_PROVIDER + const isQueueRoutedProvider = shouldUseDurableQueue(provider, getProviderHandler(provider)) if (isQueueRoutedProvider && !shouldExecuteInline()) { const jobId = await (await getJobQueue()).enqueue('webhook-execution', payload, { metadata: { diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts index 05c3eab64c6..ae8b160279e 100644 --- a/apps/sim/lib/webhooks/provider-subscriptions.ts +++ b/apps/sim/lib/webhooks/provider-subscriptions.ts @@ -17,7 +17,7 @@ type RecreateCheckInput = { nextConfig: Record } -/** System-managed fields that shouldn't trigger recreation */ +/** System-managed fields that should not trigger recreation. */ const SYSTEM_MANAGED_FIELDS = new Set([ 'externalId', 'externalSubscriptionId', @@ -32,6 +32,29 @@ const SYSTEM_MANAGED_FIELDS = new Set([ 'userId', ]) +/** Returns true when user-controlled persisted webhook configuration changed. */ +export function hasWebhookConfigChanged( + previousConfig: Record, + nextConfig: Record +): boolean { + const allKeys = new Set([...Object.keys(previousConfig), ...Object.keys(nextConfig)]) + + for (const key of allKeys) { + if (SYSTEM_MANAGED_FIELDS.has(key)) continue + + const previousValue = previousConfig[key] + const nextValue = nextConfig[key] + const previousComparable = + typeof previousValue === 'object' ? JSON.stringify(previousValue ?? null) : previousValue + const nextComparable = + typeof nextValue === 'object' ? JSON.stringify(nextValue ?? null) : nextValue + + if (previousComparable !== nextComparable) return true + } + + return false +} + /** * Determine whether a webhook with provider-managed registration should be * recreated after its persisted provider config changes. @@ -58,23 +81,7 @@ export function shouldRecreateExternalWebhookSubscription({ return false } - const allKeys = new Set([...Object.keys(previousConfig), ...Object.keys(nextConfig)]) - - for (const key of allKeys) { - if (SYSTEM_MANAGED_FIELDS.has(key)) continue - - const prevVal = previousConfig[key] - const nextVal = nextConfig[key] - - const prevStr = typeof prevVal === 'object' ? JSON.stringify(prevVal ?? null) : prevVal - const nextStr = typeof nextVal === 'object' ? JSON.stringify(nextVal ?? null) : nextVal - - if (prevStr !== nextStr) { - return true - } - } - - return false + return hasWebhookConfigChanged(previousConfig, nextConfig) } /** diff --git a/apps/sim/lib/webhooks/providers/index.ts b/apps/sim/lib/webhooks/providers/index.ts index a73a597c73b..59bc422f913 100644 --- a/apps/sim/lib/webhooks/providers/index.ts +++ b/apps/sim/lib/webhooks/providers/index.ts @@ -13,3 +13,9 @@ export function extractProviderIdentifierFromBody(provider: string, body: unknow const handler = getProviderHandler(provider) return handler.extractIdempotencyId?.(body) ?? null } + +/** Returns whether a provider accepts deliveries through the generic per-webhook path route. */ +export function acceptsPathWebhookDelivery(provider: string | null): boolean { + if (!provider) return true + return getProviderHandler(provider).ingressMode !== 'provider' +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 1965a9663c7..dc0f2bffcc2 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -47,6 +47,7 @@ import { slackHandler } from '@/lib/webhooks/providers/slack' import { stripeHandler } from '@/lib/webhooks/providers/stripe' import { tableProviderHandler } from '@/lib/webhooks/providers/table' import { telegramHandler } from '@/lib/webhooks/providers/telegram' +import { tiktokHandler } from '@/lib/webhooks/providers/tiktok' import { twilioHandler } from '@/lib/webhooks/providers/twilio' import { twilioVoiceHandler } from '@/lib/webhooks/providers/twilio-voice' import { typeformHandler } from '@/lib/webhooks/providers/typeform' @@ -108,6 +109,7 @@ const PROVIDER_HANDLERS: Record = { stripe: stripeHandler, table: tableProviderHandler, telegram: telegramHandler, + tiktok: tiktokHandler, twilio: twilioHandler, twilio_voice: twilioVoiceHandler, typeform: typeformHandler, diff --git a/apps/sim/lib/webhooks/providers/tiktok-targets.test.ts b/apps/sim/lib/webhooks/providers/tiktok-targets.test.ts new file mode 100644 index 00000000000..e0ac0d04b84 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/tiktok-targets.test.ts @@ -0,0 +1,119 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCredentialExpression, mockEq, mockSelect, queryRows } = vi.hoisted(() => ({ + mockCredentialExpression: vi.fn(() => 'webhook.credentialId'), + mockEq: vi.fn((left: unknown, right: unknown) => ({ left, right })), + mockSelect: vi.fn(), + queryRows: { + rows: [] as Array<{ + accountId: string + webhook: Record + workflow: Record + }>, + }, +})) + +vi.mock('@sim/db', () => { + const chain = { + from: vi.fn(() => chain), + innerJoin: vi.fn(() => chain), + leftJoin: vi.fn(() => chain), + where: vi.fn(() => Promise.resolve(queryRows.rows)), + } + mockSelect.mockImplementation(() => chain) + + return { + account: { + id: 'account.id', + accountId: 'account.accountId', + providerId: 'account.providerId', + }, + credential: { + id: 'credential.id', + accountId: 'credential.accountId', + providerId: 'credential.providerId', + type: 'credential.type', + workspaceId: 'credential.workspaceId', + }, + db: { select: mockSelect }, + webhookCredentialIdExpression: mockCredentialExpression, + webhook: { + deploymentVersionId: 'webhook.deploymentVersionId', + isActive: 'webhook.isActive', + archivedAt: 'webhook.archivedAt', + provider: 'webhook.provider', + providerConfig: 'webhook.providerConfig', + workflowId: 'webhook.workflowId', + }, + workflow: { + id: 'workflow.id', + workspaceId: 'workflow.workspaceId', + archivedAt: 'workflow.archivedAt', + }, + workflowDeploymentVersion: { + id: 'workflowDeploymentVersion.id', + workflowId: 'workflowDeploymentVersion.workflowId', + isActive: 'workflowDeploymentVersion.isActive', + }, + } +}) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...conditions: unknown[]) => conditions), + eq: mockEq, + isNull: vi.fn((value: unknown) => ({ isNull: value })), + like: vi.fn((left: unknown, right: unknown) => ({ left, right })), + or: vi.fn((...conditions: unknown[]) => conditions), +})) + +import { findTikTokWebhookTargets } from '@/lib/webhooks/providers/tiktok-targets' + +describe('findTikTokWebhookTargets', () => { + beforeEach(() => { + vi.clearAllMocks() + queryRows.rows = [] + }) + + it('returns only rows whose stored account ID exactly matches user_openid', async () => { + queryRows.rows = [ + { + accountId: 'act.user-11111111-2222-3333-4444-555555555555', + webhook: { id: 'webhook-1' }, + workflow: { id: 'workflow-1' }, + }, + { + accountId: 'act.user-other-11111111-2222-3333-4444-555555555555', + webhook: { id: 'webhook-2' }, + workflow: { id: 'workflow-2' }, + }, + ] + + const targets = await findTikTokWebhookTargets('act.user', 'request-1') + + expect(targets).toEqual([ + { + webhook: { id: 'webhook-1' }, + workflow: { id: 'workflow-1' }, + }, + ]) + }) + + it('enforces provider and workspace bindings in the database query', async () => { + await findTikTokWebhookTargets('act.user', 'request-2') + + expect(mockEq).toHaveBeenCalledWith('credential.providerId', 'tiktok') + expect(mockEq).toHaveBeenCalledWith('webhook.provider', 'tiktok') + expect(mockEq).toHaveBeenCalledWith('workflow.workspaceId', 'credential.workspaceId') + expect(mockCredentialExpression).toHaveBeenCalledWith('webhook.providerConfig') + expect(mockEq).toHaveBeenCalledWith('webhook.credentialId', 'credential.id') + }) + + it('does not query for an empty user_openid', async () => { + expect(await findTikTokWebhookTargets('', 'request-3')).toEqual([]) + expect(mockSelect).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/webhooks/providers/tiktok-targets.ts b/apps/sim/lib/webhooks/providers/tiktok-targets.ts new file mode 100644 index 00000000000..8fc4dc2aeaf --- /dev/null +++ b/apps/sim/lib/webhooks/providers/tiktok-targets.ts @@ -0,0 +1,102 @@ +import { + account, + credential, + db, + webhook, + webhookCredentialIdExpression, + workflow, + workflowDeploymentVersion, +} from '@sim/db' +import { createLogger } from '@sim/logger' +import { and, eq, isNull, like, or } from 'drizzle-orm' + +const logger = createLogger('TikTokWebhookTargets') +const ACCOUNT_ID_UUID_SUFFIX = /-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +export interface TikTokWebhookTarget { + webhook: typeof webhook.$inferSelect + workflow: typeof workflow.$inferSelect +} + +function escapeLikePattern(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_') +} + +function openIdFromAccountId(accountId: string): string { + return accountId.replace(ACCOUNT_ID_UUID_SUFFIX, '') +} + +/** + * Resolves a TikTok user_openid to active webhook targets through the credential ID persisted in + * providerConfig. The workflow-workspace equality prevents cross-tenant event routing. + */ +export async function findTikTokWebhookTargets( + userOpenId: string, + requestId: string +): Promise { + if (!userOpenId) return [] + + const rows = await db + .select({ + accountId: account.accountId, + webhook, + workflow, + }) + .from(account) + .innerJoin( + credential, + and( + eq(credential.accountId, account.id), + eq(credential.type, 'oauth'), + eq(credential.providerId, 'tiktok') + ) + ) + .innerJoin( + webhook, + and( + eq(webhookCredentialIdExpression(webhook.providerConfig), credential.id), + eq(webhook.provider, 'tiktok'), + eq(webhook.isActive, true), + isNull(webhook.archivedAt) + ) + ) + .innerJoin( + workflow, + and( + eq(workflow.id, webhook.workflowId), + eq(workflow.workspaceId, credential.workspaceId), + isNull(workflow.archivedAt) + ) + ) + .leftJoin( + workflowDeploymentVersion, + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .where( + and( + eq(account.providerId, 'tiktok'), + like(account.accountId, `${escapeLikePattern(userOpenId)}-%`), + or( + eq(webhook.deploymentVersionId, workflowDeploymentVersion.id), + and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId)) + ) + ) + ) + + const targets = rows + .filter((row) => openIdFromAccountId(row.accountId) === userOpenId) + .map(({ webhook: webhookRecord, workflow: workflowRecord }) => ({ + webhook: webhookRecord, + workflow: workflowRecord, + })) + + logger.info(`[${requestId}] Resolved TikTok webhook targets`, { + userOpenIdPrefix: userOpenId.slice(0, 12), + webhookCount: targets.length, + }) + + return targets +} diff --git a/apps/sim/lib/webhooks/providers/tiktok.test.ts b/apps/sim/lib/webhooks/providers/tiktok.test.ts new file mode 100644 index 00000000000..3d40d8060e1 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/tiktok.test.ts @@ -0,0 +1,298 @@ +import crypto from 'node:crypto' +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { + parseTikTokContent, + parseTikTokSignatureHeader, + tiktokHandler, + verifyTikTokSignature, +} from '@/lib/webhooks/providers/tiktok' +import { isTikTokEventMatch } from '@/triggers/tiktok/utils' + +function signTikTokBody(secret: string, timestamp: string, rawBody: string): string { + return crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`, 'utf8').digest('hex') +} + +function requestWithTikTokSignature(signatureHeader: string): NextRequest { + return new NextRequest('http://localhost/api/webhooks/tiktok', { + headers: { + 'TikTok-Signature': signatureHeader, + }, + }) +} + +describe('parseTikTokSignatureHeader', () => { + it('parses t and s from the header', () => { + expect( + parseTikTokSignatureHeader( + 't=1633174587,s=18494715036ac4416a1d0a673871a2edbcfc94d94bd88ccd2c5ec9b3425afe66' + ) + ).toEqual({ + timestamp: '1633174587', + signature: '18494715036ac4416a1d0a673871a2edbcfc94d94bd88ccd2c5ec9b3425afe66', + }) + }) + + it('returns null for missing or malformed headers', () => { + expect(parseTikTokSignatureHeader(null)).toBeNull() + expect(parseTikTokSignatureHeader('')).toBeNull() + expect(parseTikTokSignatureHeader('t=123')).toBeNull() + expect(parseTikTokSignatureHeader('s=abc')).toBeNull() + }) +}) + +describe('verifyTikTokSignature', () => { + const secret = 'tiktok-client-secret' + const rawBody = JSON.stringify({ + client_key: 'key', + event: 'post.publish.complete', + create_time: 1615338610, + user_openid: 'act.example', + content: '{"publish_id":"p1","publish_type":"DIRECT_POST"}', + }) + + it('accepts a valid signature within the skew window', () => { + const now = Math.floor(Date.now() / 1000) + const timestamp = String(now) + const signature = signTikTokBody(secret, timestamp, rawBody) + const result = verifyTikTokSignature( + rawBody, + `t=${timestamp},s=${signature}`, + 'tt-1', + secret, + now + ) + expect(result).toBeNull() + }) + + it('rejects an invalid signature', () => { + const now = Math.floor(Date.now() / 1000) + const result = verifyTikTokSignature( + rawBody, + `t=${now},s=${'0'.repeat(64)}`, + 'tt-2', + secret, + now + ) + expect(result?.status).toBe(401) + }) + + it('rejects when the client secret is missing', () => { + const now = Math.floor(Date.now() / 1000) + const result = verifyTikTokSignature(rawBody, `t=${now},s=abc`, 'tt-3', undefined, now) + expect(result?.status).toBe(401) + }) + + it('rejects a stale timestamp', () => { + const now = Math.floor(Date.now() / 1000) + const stale = String(now - 600) + const signature = signTikTokBody(secret, stale, rawBody) + const result = verifyTikTokSignature(rawBody, `t=${stale},s=${signature}`, 'tt-4', secret, now) + expect(result?.status).toBe(401) + }) + + it('rejects a missing signature header', () => { + const result = verifyTikTokSignature(rawBody, null, 'tt-5', secret) + expect(result?.status).toBe(401) + }) +}) + +describe('parseTikTokContent', () => { + it('parses a JSON string content field', () => { + expect(parseTikTokContent('{"publish_id":"p1","publish_type":"DIRECT_POST"}')).toEqual({ + publish_id: 'p1', + publish_type: 'DIRECT_POST', + }) + }) + + it('returns an empty object for invalid JSON', () => { + expect(parseTikTokContent('{not-json')).toEqual({}) + }) +}) + +describe('isTikTokEventMatch', () => { + it('matches documented event names including TikTok typo', () => { + expect(isTikTokEventMatch('tiktok_post_publish_complete', 'post.publish.complete')).toBe(true) + expect( + isTikTokEventMatch( + 'tiktok_post_no_longer_public', + 'post.publish.no_longer_publicaly_available' + ) + ).toBe(true) + expect(isTikTokEventMatch('tiktok_post_publish_complete', 'post.publish.failed')).toBe(false) + }) +}) + +describe('tiktokHandler', () => { + it('verifyAuth delegates to signature verification', async () => { + const secret = 'tiktok-client-secret' + const rawBody = '{"event":"authorization.removed"}' + const now = Math.floor(Date.now() / 1000) + const timestamp = String(now) + const signature = signTikTokBody(secret, timestamp, rawBody) + + // verifyAuth uses env.TIKTOK_CLIENT_SECRET; exercise via verifyTikTokSignature path above. + // Handler still requires a request with the header for the dedicated ingress. + const res = await tiktokHandler.verifyAuth!({ + request: requestWithTikTokSignature(`t=${timestamp},s=${signature}`), + rawBody, + requestId: 'tt-handler', + providerConfig: {}, + webhook: {}, + workflow: {}, + }) + // Without env secret this may 401; signature path is covered above. + expect(res === null || res.status === 401).toBe(true) + }) + + it('matchEvent filters by trigger id', async () => { + const match = await tiktokHandler.matchEvent!({ + body: { event: 'post.publish.complete' }, + request: new NextRequest('http://localhost'), + requestId: 'tt-match', + providerConfig: { triggerId: 'tiktok_post_publish_complete' }, + webhook: {}, + workflow: {}, + }) + expect(match).toBe(true) + + const skip = await tiktokHandler.matchEvent!({ + body: { event: 'post.publish.failed' }, + request: new NextRequest('http://localhost'), + requestId: 'tt-skip', + providerConfig: { triggerId: 'tiktok_post_publish_complete' }, + webhook: {}, + workflow: {}, + }) + expect(skip).toBe(false) + }) + + it('formatInput flattens envelope and content fields', async () => { + const { input } = await tiktokHandler.formatInput!({ + body: { + client_key: 'ck', + event: 'post.publish.failed', + create_time: 1615338610, + user_openid: 'act.user', + content: '{"publish_id":"pub-1","publish_type":"DIRECT_POST","reason":"spam_risk"}', + }, + webhook: {}, + workflow: { id: 'w1', userId: 'u1' }, + headers: {}, + requestId: 'tt-fmt', + }) + + expect(input).toEqual({ + event: 'post.publish.failed', + createTime: 1615338610, + userOpenId: 'act.user', + clientKey: 'ck', + publishId: 'pub-1', + publishType: 'DIRECT_POST', + failReason: 'spam_risk', + }) + }) + + it('formatInput maps authorization.removed reason', async () => { + const { input } = await tiktokHandler.formatInput!({ + body: { + client_key: 'ck', + event: 'authorization.removed', + create_time: 1615338610, + user_openid: 'act.user', + content: '{"reason": 1 }', + }, + webhook: {}, + workflow: { id: 'w1', userId: 'u1' }, + headers: {}, + requestId: 'tt-auth', + }) + + expect(input).toEqual({ + event: 'authorization.removed', + createTime: 1615338610, + userOpenId: 'act.user', + clientKey: 'ck', + reason: 1, + }) + }) + + it('formatInput emits only the selected event output shape with null optional values', async () => { + const { input } = await tiktokHandler.formatInput!({ + body: { + client_key: 'ck', + event: 'post.publish.publicly_available', + create_time: 1615338610, + user_openid: 'act.user', + content: '{"publish_id":"pub-1"}', + }, + webhook: {}, + workflow: { id: 'w1', userId: 'u1' }, + headers: {}, + requestId: 'tt-public', + }) + + expect(input).toEqual({ + event: 'post.publish.publicly_available', + createTime: 1615338610, + userOpenId: 'act.user', + clientKey: 'ck', + publishId: 'pub-1', + publishType: null, + postId: null, + }) + expect(input).not.toHaveProperty('shareId') + expect(input).not.toHaveProperty('failReason') + }) + + it('distinguishes multiple completed posts created from one publish_id', () => { + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'post.publish.complete', + user_openid: 'act.user', + create_time: 1, + content: '{"publish_id":"pub-1"}', + }) + ).toBe('post.publish.complete:act.user:pub-1:1') + + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'post.publish.complete', + user_openid: 'act.user', + create_time: 2, + content: '{"publish_id":"pub-1"}', + }) + ).toBe('post.publish.complete:act.user:pub-1:2') + }) + + it('uses post_id to distinguish public availability events for the same publish_id', () => { + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'post.publish.publicly_available', + user_openid: 'act.user', + create_time: 3, + content: '{"publish_id":"pub-1","post_id":"post-1"}', + }) + ).toBe('post.publish.publicly_available:act.user:pub-1:post-1') + }) + + it('extractIdempotencyId falls back to share_id then create_time', () => { + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'video.publish.completed', + user_openid: 'act.user', + create_time: 99, + content: '{"share_id":"share-1"}', + }) + ).toBe('video.publish.completed:act.user:share-1') + + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'authorization.removed', + user_openid: 'act.user', + create_time: 42, + content: '{"reason":1}', + }) + ).toBe('authorization.removed:act.user:42') + }) +}) diff --git a/apps/sim/lib/webhooks/providers/tiktok.ts b/apps/sim/lib/webhooks/providers/tiktok.ts new file mode 100644 index 00000000000..0abedaa7e03 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/tiktok.ts @@ -0,0 +1,245 @@ +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' +import { NextResponse } from 'next/server' +import { env } from '@/lib/core/config/env' +import type { + AuthContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:TikTok') + +/** TikTok recommends rejecting replayed signatures; 5 minutes matches Linear/common practice. */ +export const TIKTOK_WEBHOOK_TIMESTAMP_SKEW_SECONDS = 5 * 60 + +export interface TikTokSignatureParts { + timestamp: string + signature: string +} + +/** + * Parse `TikTok-Signature: t=,s=` (comma-separated prefix=value pairs). + */ +export function parseTikTokSignatureHeader(header: string | null): TikTokSignatureParts | null { + if (!header) return null + + let timestamp: string | undefined + let signature: string | undefined + + for (const part of header.split(',')) { + const trimmed = part.trim() + const eq = trimmed.indexOf('=') + if (eq <= 0) continue + const prefix = trimmed.slice(0, eq) + const value = trimmed.slice(eq + 1) + if (prefix === 't') timestamp = value + if (prefix === 's') signature = value + } + + if (!timestamp || !signature) return null + return { timestamp, signature } +} + +/** + * Verify TikTok webhook HMAC-SHA256 of `${t}.${rawBody}` with the app client secret. + * Returns null on success, or a 401 NextResponse on failure. + */ +export function verifyTikTokSignature( + rawBody: string, + signatureHeader: string | null, + requestId: string, + clientSecret: string | undefined = env.TIKTOK_CLIENT_SECRET, + nowSeconds: number = Math.floor(Date.now() / 1000) +): NextResponse | null { + if (!clientSecret) { + logger.warn(`[${requestId}] TikTok webhook missing TIKTOK_CLIENT_SECRET`) + return new NextResponse('Unauthorized - TikTok client secret not configured', { status: 401 }) + } + + const parts = parseTikTokSignatureHeader(signatureHeader) + if (!parts) { + logger.warn(`[${requestId}] TikTok webhook missing or malformed TikTok-Signature header`) + return new NextResponse('Unauthorized - Missing TikTok signature', { status: 401 }) + } + + const timestampSeconds = Number(parts.timestamp) + if (!Number.isFinite(timestampSeconds)) { + logger.warn(`[${requestId}] TikTok webhook signature timestamp is not a number`) + return new NextResponse('Unauthorized - Invalid TikTok signature timestamp', { status: 401 }) + } + + if (Math.abs(nowSeconds - timestampSeconds) > TIKTOK_WEBHOOK_TIMESTAMP_SKEW_SECONDS) { + logger.warn(`[${requestId}] TikTok webhook signature timestamp outside allowed skew`, { + skewSeconds: TIKTOK_WEBHOOK_TIMESTAMP_SKEW_SECONDS, + timestampSeconds, + nowSeconds, + }) + return new NextResponse('Unauthorized - TikTok signature timestamp skew too large', { + status: 401, + }) + } + + const signedPayload = `${parts.timestamp}.${rawBody}` + const computed = hmacSha256Hex(signedPayload, clientSecret) + if (!safeCompare(computed, parts.signature)) { + logger.warn(`[${requestId}] TikTok signature verification failed`) + return new NextResponse('Unauthorized - Invalid TikTok signature', { status: 401 }) + } + + return null +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null + return value as Record +} + +/** + * Parse the TikTok envelope `content` field (a JSON string) into an object. + */ +export function parseTikTokContent(content: unknown): Record { + if (typeof content !== 'string' || content.length === 0) { + return asRecord(content) ?? {} + } + try { + return asRecord(JSON.parse(content)) ?? {} + } catch { + logger.warn('Failed to parse TikTok webhook content JSON string') + return {} + } +} + +function stringField(obj: Record, ...keys: string[]): string | undefined { + for (const key of keys) { + const value = obj[key] + if (typeof value === 'string' && value.length > 0) return value + if (typeof value === 'number' && Number.isFinite(value)) return String(value) + } + return undefined +} + +function numberField(value: unknown): number | null { + const number = typeof value === 'number' ? value : Number(value) + return Number.isFinite(number) ? number : null +} + +export const tiktokHandler: WebhookProviderHandler = { + ingressMode: 'provider', + executionMode: 'queue', + + async verifyAuth({ request, rawBody, requestId }: AuthContext): Promise { + return verifyTikTokSignature(rawBody, request.headers.get('TikTok-Signature'), requestId) + }, + + async matchEvent({ body, requestId, providerConfig }: EventMatchContext) { + const triggerId = + typeof providerConfig.triggerId === 'string' ? providerConfig.triggerId : undefined + if (!triggerId) return true + + const { isTikTokEventMatch } = await import('@/triggers/tiktok/utils') + const event = stringField(asRecord(body) ?? {}, 'event') + if (!isTikTokEventMatch(triggerId, event)) { + logger.debug( + `[${requestId}] TikTok event mismatch for trigger ${triggerId}. Event: ${event}. Skipping.` + ) + return false + } + return true + }, + + async formatInput({ body }: FormatInputContext): Promise { + const envelope = asRecord(body) ?? {} + const content = parseTikTokContent(envelope.content) + const event = typeof envelope.event === 'string' ? envelope.event : '' + const commonInput: Record = { + event, + createTime: numberField(envelope.create_time), + userOpenId: typeof envelope.user_openid === 'string' ? envelope.user_openid : null, + clientKey: typeof envelope.client_key === 'string' ? envelope.client_key : null, + } + const postingInput = { + ...commonInput, + publishId: stringField(content, 'publish_id') ?? null, + publishType: stringField(content, 'publish_type') ?? null, + } + + if (event === 'post.publish.failed') { + return { + input: { + ...postingInput, + failReason: stringField(content, 'fail_reason', 'reason') ?? null, + }, + } + } + + if (event === 'post.publish.complete' || event === 'post.publish.inbox_delivered') { + return { input: postingInput } + } + + if ( + event === 'post.publish.publicly_available' || + event === 'post.publish.no_longer_publicaly_available' + ) { + return { + input: { + ...postingInput, + postId: stringField(content, 'post_id') ?? null, + }, + } + } + + if (event === 'authorization.removed') { + return { + input: { + ...commonInput, + reason: numberField(content.reason), + }, + } + } + + if (event === 'video.publish.completed' || event === 'video.upload.failed') { + return { + input: { + ...commonInput, + shareId: stringField(content, 'share_id') ?? null, + }, + } + } + + return { input: commonInput } + }, + + extractIdempotencyId(body: unknown) { + const envelope = asRecord(body) + if (!envelope) return null + + const event = typeof envelope.event === 'string' ? envelope.event : null + const userOpenId = typeof envelope.user_openid === 'string' ? envelope.user_openid : null + if (!event || !userOpenId) return null + + const content = parseTikTokContent(envelope.content) + const publishId = stringField(content, 'publish_id') + const postId = stringField(content, 'post_id') + const shareId = stringField(content, 'share_id') + const createTime = + typeof envelope.create_time === 'number' || typeof envelope.create_time === 'string' + ? String(envelope.create_time) + : null + + let unique: string | null = null + if (publishId && postId) { + unique = `${publishId}:${postId}` + } else if (event === 'post.publish.complete' && publishId && createTime) { + unique = `${publishId}:${createTime}` + } else { + unique = publishId ?? shareId ?? createTime + } + if (!unique) return null + + return `${event}:${userOpenId}:${unique}` + }, +} diff --git a/apps/sim/lib/webhooks/providers/types.ts b/apps/sim/lib/webhooks/providers/types.ts index d25037117d9..b773b1e3fd1 100644 --- a/apps/sim/lib/webhooks/providers/types.ts +++ b/apps/sim/lib/webhooks/providers/types.ts @@ -89,6 +89,18 @@ export interface PollingConfigContext { * Each provider implements only the methods it needs — all methods are optional. */ export interface WebhookProviderHandler { + /** + * Restrict deliveries to a provider-owned ingress route instead of the generic per-webhook + * path route. Use when the provider sends all app events to one callback before target lookup. + */ + ingressMode?: 'path' | 'provider' + + /** + * Queue workflow execution through the configured durable backend instead of the low-latency + * in-process path. Use for providers whose ingress is acknowledged before target processing. + */ + executionMode?: 'inline' | 'queue' + /** Verify signature/auth. Return NextResponse(401/403) on failure, null on success. */ verifyAuth?(ctx: AuthContext): Promise | NextResponse | null diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 741e30081db..5bc9afa6e7f 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -196,7 +196,10 @@ function handleInvitationRedirects( function handleSecurityFiltering(request: NextRequest): NextResponse | null { const userAgent = request.headers.get('user-agent') || '' const { pathname } = request.nextUrl - const isWebhookEndpoint = pathname.startsWith('/api/webhooks/trigger/') + const isWebhookEndpoint = + pathname.startsWith('/api/webhooks/trigger/') || + pathname.startsWith('/api/webhooks/tiktok') || + pathname.startsWith('/api/webhooks/agentmail') const isMcpEndpoint = pathname.startsWith('/api/mcp/') const isMcpOauthDiscoveryEndpoint = pathname.startsWith('/.well-known/oauth-authorization-server') || diff --git a/apps/sim/tools/jupyter/content-transforms.test.ts b/apps/sim/tools/jupyter/content-transforms.test.ts new file mode 100644 index 00000000000..22d99e69917 --- /dev/null +++ b/apps/sim/tools/jupyter/content-transforms.test.ts @@ -0,0 +1,119 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { jupyterCopyContentTool } from '@/tools/jupyter/copy_content' +import { jupyterCreateFileTool } from '@/tools/jupyter/create_file' +import { jupyterGetContentTool } from '@/tools/jupyter/get_content' +import { jupyterListContentsTool } from '@/tools/jupyter/list_contents' +import { jupyterRenameContentTool } from '@/tools/jupyter/rename_content' + +const AUTH = { + serverUrl: 'http://localhost:8888', + token: 'token', +} + +const CONTENT_MODEL = { + name: 'notes.txt', + path: 'docs/notes.txt', + type: 'file', + writable: true, + created: '2026-07-09T10:00:00Z', + last_modified: '2026-07-09T11:00:00Z', + size: 5, + mimetype: 'text/plain', + format: 'text', + content: 'hello', +} + +describe('Jupyter content transforms', () => { + it('preserves existing output shapes for valid Contents API models', async () => { + await expect( + jupyterCreateFileTool.transformResponse?.(Response.json(CONTENT_MODEL), { + ...AUTH, + path: CONTENT_MODEL.path, + type: 'file', + }) + ).resolves.toMatchObject({ + success: true, + output: { + name: CONTENT_MODEL.name, + path: CONTENT_MODEL.path, + type: 'file', + createdAt: CONTENT_MODEL.created, + lastModified: CONTENT_MODEL.last_modified, + }, + }) + + await expect( + jupyterCopyContentTool.transformResponse?.(Response.json(CONTENT_MODEL), { + ...AUTH, + path: CONTENT_MODEL.path, + copyFromPath: 'notes.txt', + }) + ).resolves.toMatchObject({ + success: true, + output: { + name: CONTENT_MODEL.name, + path: CONTENT_MODEL.path, + createdAt: CONTENT_MODEL.created, + }, + }) + + await expect( + jupyterRenameContentTool.transformResponse?.(Response.json(CONTENT_MODEL), { + ...AUTH, + path: 'notes.txt', + newPath: CONTENT_MODEL.path, + }) + ).resolves.toMatchObject({ + success: true, + output: { + name: CONTENT_MODEL.name, + path: CONTENT_MODEL.path, + lastModified: CONTENT_MODEL.last_modified, + }, + }) + + await expect( + jupyterListContentsTool.transformResponse?.( + Response.json({ ...CONTENT_MODEL, path: 'docs', content: [CONTENT_MODEL] }), + { ...AUTH, path: 'docs' } + ) + ).resolves.toMatchObject({ + success: true, + output: { + path: 'docs', + items: [ + { + name: CONTENT_MODEL.name, + path: CONTENT_MODEL.path, + type: 'file', + writable: true, + created: CONTENT_MODEL.created, + lastModified: CONTENT_MODEL.last_modified, + size: CONTENT_MODEL.size, + mimetype: CONTENT_MODEL.mimetype, + format: CONTENT_MODEL.format, + }, + ], + }, + }) + + await expect( + jupyterGetContentTool.transformResponse?.(Response.json(CONTENT_MODEL), { + ...AUTH, + path: CONTENT_MODEL.path, + }) + ).resolves.toMatchObject({ + success: true, + output: { + name: CONTENT_MODEL.name, + path: CONTENT_MODEL.path, + mimetype: CONTENT_MODEL.mimetype, + text: CONTENT_MODEL.content, + file: null, + }, + }) + }) +}) diff --git a/apps/sim/tools/jupyter/copy_content.ts b/apps/sim/tools/jupyter/copy_content.ts index 87abbdbd0d3..797aad0761d 100644 --- a/apps/sim/tools/jupyter/copy_content.ts +++ b/apps/sim/tools/jupyter/copy_content.ts @@ -1,5 +1,9 @@ import type { JupyterCopyContentParams, JupyterCopyContentResponse } from '@/tools/jupyter/types' -import { assertSafeJupyterPath, encodeJupyterPath } from '@/tools/jupyter/utils' +import { + assertSafeJupyterPath, + encodeJupyterPath, + parseJupyterContentModel, +} from '@/tools/jupyter/utils' import type { ToolConfig } from '@/tools/types' export const jupyterCopyContentTool: ToolConfig< @@ -61,14 +65,14 @@ export const jupyterCopyContentTool: ToolConfig< } } - const data = await response.json() + const data = parseJupyterContentModel(await response.json()) ?? {} return { success: true, output: { - name: (data.name as string | undefined) ?? '', - path: (data.path as string | undefined) ?? params?.path ?? '', - createdAt: (data.created as string | undefined) ?? null, + name: data.name ?? '', + path: data.path ?? params?.path ?? '', + createdAt: data.created ?? null, }, } }, diff --git a/apps/sim/tools/jupyter/create_file.ts b/apps/sim/tools/jupyter/create_file.ts index 5d59dfcff20..6eb45e5f1d8 100644 --- a/apps/sim/tools/jupyter/create_file.ts +++ b/apps/sim/tools/jupyter/create_file.ts @@ -1,5 +1,5 @@ import type { JupyterCreateFileParams, JupyterCreateFileResponse } from '@/tools/jupyter/types' -import { encodeJupyterPath } from '@/tools/jupyter/utils' +import { encodeJupyterPath, parseJupyterContentModel } from '@/tools/jupyter/utils' import type { ToolConfig } from '@/tools/types' const EMPTY_NOTEBOOK = { cells: [], metadata: {}, nbformat: 4, nbformat_minor: 5 } @@ -95,16 +95,16 @@ export const jupyterCreateFileTool: ToolConfig = @@ -58,10 +58,10 @@ export const jupyterGetContentTool: ToolConfig) => ({ - name: item.name, - path: item.path, - type: item.type, - writable: Boolean(item.writable), - created: (item.created as string | undefined) ?? null, - lastModified: (item.last_modified as string | undefined) ?? null, - size: (item.size as number | undefined) ?? null, - mimetype: (item.mimetype as string | undefined) ?? null, - format: (item.format as string | undefined) ?? null, - })), - path: (data.path as string | undefined) ?? params?.path ?? '', + items: items.map((item) => { + const content = parseJupyterContentModel(item) ?? {} + return { + name: content.name ?? '', + path: content.path ?? '', + type: content.type ?? 'file', + writable: content.writable ?? false, + created: content.created ?? null, + lastModified: content.lastModified ?? null, + size: content.size ?? null, + mimetype: content.mimetype ?? null, + format: content.format ?? null, + } + }), + path: data.path ?? params?.path ?? '', }, } }, diff --git a/apps/sim/tools/jupyter/rename_content.ts b/apps/sim/tools/jupyter/rename_content.ts index 2e32900b1e6..3f6598cb54c 100644 --- a/apps/sim/tools/jupyter/rename_content.ts +++ b/apps/sim/tools/jupyter/rename_content.ts @@ -2,7 +2,11 @@ import type { JupyterRenameContentParams, JupyterRenameContentResponse, } from '@/tools/jupyter/types' -import { assertSafeJupyterPath, encodeJupyterPath } from '@/tools/jupyter/utils' +import { + assertSafeJupyterPath, + encodeJupyterPath, + parseJupyterContentModel, +} from '@/tools/jupyter/utils' import type { ToolConfig } from '@/tools/types' export const jupyterRenameContentTool: ToolConfig< @@ -64,14 +68,14 @@ export const jupyterRenameContentTool: ToolConfig< } } - const data = await response.json() + const data = parseJupyterContentModel(await response.json()) ?? {} return { success: true, output: { - name: (data.name as string | undefined) ?? '', - path: (data.path as string | undefined) ?? params?.newPath ?? '', - lastModified: (data.last_modified as string | undefined) ?? null, + name: data.name ?? '', + path: data.path ?? params?.newPath ?? '', + lastModified: data.lastModified ?? null, }, } }, diff --git a/apps/sim/tools/jupyter/utils.test.ts b/apps/sim/tools/jupyter/utils.test.ts new file mode 100644 index 00000000000..037aa2b4e02 --- /dev/null +++ b/apps/sim/tools/jupyter/utils.test.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { parseJupyterContentModel } from '@/tools/jupyter/utils' + +describe('parseJupyterContentModel', () => { + it('normalizes a Jupyter Contents API model without changing its values', () => { + const content = { cells: [] } + + expect( + parseJupyterContentModel({ + name: 'analysis.ipynb', + path: 'notebooks/analysis.ipynb', + type: 'notebook', + writable: true, + created: '2026-07-09T10:00:00Z', + last_modified: '2026-07-09T11:00:00Z', + size: 42, + mimetype: 'application/x-ipynb+json', + format: 'json', + content, + }) + ).toEqual({ + name: 'analysis.ipynb', + path: 'notebooks/analysis.ipynb', + type: 'notebook', + writable: true, + created: '2026-07-09T10:00:00Z', + lastModified: '2026-07-09T11:00:00Z', + size: 42, + mimetype: 'application/x-ipynb+json', + format: 'json', + content, + }) + }) + + it('rejects non-object models and omits fields with invalid types', () => { + expect(parseJupyterContentModel(null)).toBeNull() + expect( + parseJupyterContentModel({ + name: 42, + path: 'valid/path', + size: '42', + content: null, + }) + ).toEqual({ + path: 'valid/path', + content: null, + }) + }) +}) diff --git a/apps/sim/tools/jupyter/utils.ts b/apps/sim/tools/jupyter/utils.ts index 517ec545c95..30c14f3cf5d 100644 --- a/apps/sim/tools/jupyter/utils.ts +++ b/apps/sim/tools/jupyter/utils.ts @@ -1,3 +1,5 @@ +import { isPlainRecord } from '@sim/utils/object' + const PROTOCOL_PATTERN = /^https?:\/\//i /** @@ -131,6 +133,46 @@ export function assertSafeJupyterProxyPath(rawPath: string): void { assertNoJupyterPathTraversal(pathname) } +interface JupyterContentModel { + name?: string + path?: string + type?: 'directory' | 'file' | 'notebook' + writable?: boolean + created?: string + lastModified?: string + size?: number + mimetype?: string + format?: 'json' | 'text' | 'base64' + content?: unknown +} + +/** Parses the shared model returned by Jupyter's Contents API. */ +export function parseJupyterContentModel(value: unknown): JupyterContentModel | null { + if (!isPlainRecord(value)) return null + + const type = + value.type === 'directory' || value.type === 'file' || value.type === 'notebook' + ? value.type + : undefined + const format = + value.format === 'json' || value.format === 'text' || value.format === 'base64' + ? value.format + : undefined + + return { + ...(typeof value.name === 'string' ? { name: value.name } : {}), + ...(typeof value.path === 'string' ? { path: value.path } : {}), + ...(type ? { type } : {}), + ...(typeof value.writable === 'boolean' ? { writable: value.writable } : {}), + ...(typeof value.created === 'string' ? { created: value.created } : {}), + ...(typeof value.last_modified === 'string' ? { lastModified: value.last_modified } : {}), + ...(typeof value.size === 'number' ? { size: value.size } : {}), + ...(typeof value.mimetype === 'string' ? { mimetype: value.mimetype } : {}), + ...(format ? { format } : {}), + ...('content' in value ? { content: value.content } : {}), + } +} + interface RawJupyterKernel { id?: string name?: string diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4c2ce2bf0d9..86c37112082 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4068,6 +4068,17 @@ import { thriveUpdateUserSkillsTool, thriveUpdateUserTool, } from '@/tools/thrive' +import { + tiktokDirectPostPhotoTool, + tiktokDirectPostVideoTool, + tiktokGetPostStatusTool, + tiktokGetUserTool, + tiktokListVideosTool, + tiktokQueryCreatorInfoTool, + tiktokQueryVideosTool, + tiktokUploadPhotoDraftTool, + tiktokUploadVideoDraftTool, +} from '@/tools/tiktok' import { tinybirdAppendDatasourceTool, tinybirdDeleteDatasourceRowsTool, @@ -7177,6 +7188,15 @@ export const tools: Record = { thrive_remove_user_tags: thriveRemoveUserTagsTool, thrive_update_user_skills: thriveUpdateUserSkillsTool, thrive_get_skill_levels: thriveGetSkillLevelsTool, + tiktok_direct_post_photo: tiktokDirectPostPhotoTool, + tiktok_direct_post_video: tiktokDirectPostVideoTool, + tiktok_get_post_status: tiktokGetPostStatusTool, + tiktok_get_user: tiktokGetUserTool, + tiktok_list_videos: tiktokListVideosTool, + tiktok_query_creator_info: tiktokQueryCreatorInfoTool, + tiktok_query_videos: tiktokQueryVideosTool, + tiktok_upload_photo_draft: tiktokUploadPhotoDraftTool, + tiktok_upload_video_draft: tiktokUploadVideoDraftTool, tinybird_events: tinybirdEventsTool, tinybird_query: tinybirdQueryTool, tinybird_query_pipe: tinybirdQueryPipeTool, diff --git a/apps/sim/tools/tiktok/api-schemas.ts b/apps/sim/tools/tiktok/api-schemas.ts new file mode 100644 index 00000000000..82d1606c0f0 --- /dev/null +++ b/apps/sim/tools/tiktok/api-schemas.ts @@ -0,0 +1,77 @@ +import { z } from 'zod' + +/** Raw user object returned by TikTok's user info API. */ +export const tiktokApiUserSchema = z.object({ + open_id: z.string().optional(), + union_id: z.string().optional(), + avatar_url: z.string().optional(), + avatar_large_url: z.string().optional(), + display_name: z.string().optional(), + bio_description: z.string().optional(), + profile_deep_link: z.string().optional(), + is_verified: z.boolean().optional(), + username: z.string().optional(), + follower_count: z.number().optional(), + following_count: z.number().optional(), + likes_count: z.number().optional(), + video_count: z.number().optional(), +}) + +/** Data payload returned by TikTok's user info API. */ +export const tiktokGetUserApiDataSchema = z.object({ + user: tiktokApiUserSchema.optional(), +}) + +/** Raw video object returned by TikTok's video APIs. */ +export const tiktokApiVideoSchema = z.object({ + id: z.string().optional(), + title: z.string().optional(), + cover_image_url: z.string().optional(), + embed_link: z.string().optional(), + embed_html: z.string().optional(), + duration: z.number().optional(), + create_time: z.number().optional(), + share_url: z.string().optional(), + video_description: z.string().optional(), + width: z.number().optional(), + height: z.number().optional(), + view_count: z.number().optional(), + like_count: z.number().optional(), + comment_count: z.number().optional(), + share_count: z.number().optional(), +}) + +/** Data payload returned by TikTok's list/query video APIs. */ +export const tiktokVideosApiDataSchema = z.object({ + videos: z.array(tiktokApiVideoSchema).optional(), + cursor: z.number().optional(), + has_more: z.boolean().optional(), +}) + +/** Data payload returned by TikTok's creator info API. */ +export const tiktokCreatorInfoApiDataSchema = z.object({ + creator_avatar_url: z.string().optional(), + creator_username: z.string().optional(), + creator_nickname: z.string().optional(), + privacy_level_options: z.array(z.string()).optional(), + comment_disabled: z.boolean().optional(), + duet_disabled: z.boolean().optional(), + stitch_disabled: z.boolean().optional(), + max_video_post_duration_sec: z.number().optional(), +}) + +/** Data payload returned by TikTok's publish initialization APIs. */ +export const tiktokPublishInitApiDataSchema = z.object({ + publish_id: z.string().optional(), + upload_url: z.string().optional(), +}) + +/** Data payload returned by TikTok's post status API. */ +export const tiktokPostStatusApiDataSchema = z.object({ + status: z.string().optional(), + fail_reason: z.string().optional(), + uploaded_bytes: z.number().optional(), + downloaded_bytes: z.number().optional(), +}) + +export type TikTokApiVideo = z.infer diff --git a/apps/sim/tools/tiktok/direct_post_photo.ts b/apps/sim/tools/tiktok/direct_post_photo.ts new file mode 100644 index 00000000000..0a6531462bc --- /dev/null +++ b/apps/sim/tools/tiktok/direct_post_photo.ts @@ -0,0 +1,147 @@ +import type { + TikTokDirectPostPhotoParams, + TikTokDirectPostPhotoResponse, +} from '@/tools/tiktok/types' +import { + readTikTokPublishInitResponse, + resolveTikTokPhotoCoverIndex, + toTikTokPublishToolResponse, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +export const tiktokDirectPostPhotoTool: ToolConfig< + TikTokDirectPostPhotoParams, + TikTokDirectPostPhotoResponse +> = { + id: 'tiktok_direct_post_photo', + name: 'TikTok Direct Post Photo', + description: + 'Publish one or more photos to TikTok from public URLs. TikTok will fetch each image and post them as a photo carousel. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + photoImages: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'Public, verified-domain URLs of the images to post (up to 35). Must be JPEG or WEBP — TikTok rejects PNG.', + items: { + type: 'string', + description: 'Public image URL', + }, + }, + photoCoverIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 0, + description: 'Index (starting from 0) of the photo to use as the cover.', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post title. Maximum 90 characters.', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post description. Maximum 4000 characters.', + }, + privacyLevel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Privacy level for the post. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Must match one of the privacyLevelOptions returned by Query Creator Info. Note: unaudited apps (including sandbox apps) are restricted to SELF_ONLY.', + }, + disableComment: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable comments for this post. Defaults to false.', + }, + autoAddMusic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Automatically add recommended music to the photo post. Defaults to false.', + }, + brandContentToggle: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Set to true if the post is a paid partnership promoting a third-party business. Branded content cannot be posted with Only Me privacy.', + }, + brandOrganicToggle: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: "Set to true if the post is promoting the creator's own business.", + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/content/init/', + method: 'POST', + headers: (params: TikTokDirectPostPhotoParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: (params: TikTokDirectPostPhotoParams) => { + const photoCoverIndex = resolveTikTokPhotoCoverIndex( + params.photoImages, + params.photoCoverIndex + ) + + const postInfo: Record = { + brand_content_toggle: params.brandContentToggle ?? false, + brand_organic_toggle: params.brandOrganicToggle ?? false, + privacy_level: params.privacyLevel, + } + + if (params.title) postInfo.title = params.title + if (params.description) postInfo.description = params.description + if (params.disableComment !== undefined) postInfo.disable_comment = params.disableComment + if (params.autoAddMusic !== undefined) postInfo.auto_add_music = params.autoAddMusic + return { + media_type: 'PHOTO', + post_mode: 'DIRECT_POST', + post_info: postInfo, + source_info: { + source: 'PULL_FROM_URL', + photo_images: params.photoImages, + photo_cover_index: photoCoverIndex, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const result = await readTikTokPublishInitResponse(response) + return toTikTokPublishToolResponse(result) + }, + + outputs: { + publishId: { + type: 'string', + description: + 'Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the photos were successfully published.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/direct_post_video.ts b/apps/sim/tools/tiktok/direct_post_video.ts new file mode 100644 index 00000000000..cc5e3d6a779 --- /dev/null +++ b/apps/sim/tools/tiktok/direct_post_video.ts @@ -0,0 +1,176 @@ +import type { + TikTokDirectPostVideoParams, + TikTokDirectPostVideoResponse, +} from '@/tools/tiktok/types' +import { + assertTikTokVideoSourceInput, + readTikTokPublishInitResponse, + toTikTokPublishToolResponse, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +function buildPostInfo(params: TikTokDirectPostVideoParams): Record { + const postInfo: Record = { + brand_content_toggle: params.brandContentToggle ?? false, + brand_organic_toggle: params.brandOrganicToggle ?? false, + privacy_level: params.privacyLevel, + } + + if (params.title) postInfo.title = params.title + if (params.disableDuet !== undefined) postInfo.disable_duet = params.disableDuet + if (params.disableStitch !== undefined) postInfo.disable_stitch = params.disableStitch + if (params.disableComment !== undefined) postInfo.disable_comment = params.disableComment + if (params.videoCoverTimestampMs !== undefined) { + postInfo.video_cover_timestamp_ms = params.videoCoverTimestampMs + } + if (params.isAigc !== undefined) postInfo.is_aigc = params.isAigc + return postInfo +} + +export const tiktokDirectPostVideoTool: ToolConfig< + TikTokDirectPostVideoParams, + TikTokDirectPostVideoResponse +> = { + id: 'tiktok_direct_post_video', + name: 'TikTok Direct Post Video', + description: + 'Publish a video to TikTok, either by public URL (TikTok fetches it) or by uploading a file from the workflow. Rate limit: 6 requests per minute per user.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + source: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Media transfer method: 'PULL_FROM_URL' or 'FILE_UPLOAD'.", + }, + videoUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Public URL of the video to post (used when source is PULL_FROM_URL). The domain/URL prefix must be verified in the TikTok developer portal.', + }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Video file to upload from the workflow (used when source is FILE_UPLOAD).', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Video caption/description. Maximum 2200 characters.', + }, + privacyLevel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Privacy level for the video. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Must match one of the privacyLevelOptions returned by Query Creator Info. Note: unaudited apps (including sandbox apps) are restricted to SELF_ONLY.', + }, + disableDuet: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable duet for this video. Defaults to false.', + }, + disableStitch: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable stitch for this video. Defaults to false.', + }, + disableComment: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable comments for this video. Defaults to false.', + }, + videoCoverTimestampMs: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timestamp in milliseconds to use as the video cover image.', + }, + isAigc: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Set to true if the video is AI-generated content (AIGC).', + }, + brandContentToggle: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Set to true if the video is a paid partnership promoting a third-party business. Branded content cannot be posted with Only Me privacy.', + }, + brandOrganicToggle: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: "Set to true if the video is promoting the creator's own business.", + }, + }, + + request: { + url: (params: TikTokDirectPostVideoParams) => + params.source === 'FILE_UPLOAD' + ? '/api/tools/tiktok/publish-video' + : 'https://open.tiktokapis.com/v2/post/publish/video/init/', + method: 'POST', + headers: (params: TikTokDirectPostVideoParams) => ({ + ...(params.source !== 'FILE_UPLOAD' && { Authorization: `Bearer ${params.accessToken}` }), + 'Content-Type': + params.source === 'FILE_UPLOAD' ? 'application/json' : 'application/json; charset=UTF-8', + }), + body: (params: TikTokDirectPostVideoParams) => { + assertTikTokVideoSourceInput(params.source, params.videoUrl, params.file) + const postInfo = buildPostInfo(params) + + if (params.source === 'FILE_UPLOAD') { + return { + accessToken: params.accessToken, + mode: 'direct', + file: params.file, + postInfo, + } + } + + return { + post_info: postInfo, + source_info: { + source: 'PULL_FROM_URL', + video_url: params.videoUrl, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const result = await readTikTokPublishInitResponse(response) + return toTikTokPublishToolResponse(result) + }, + + outputs: { + publishId: { + type: 'string', + description: + 'Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the video was successfully published.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/get_post_status.ts b/apps/sim/tools/tiktok/get_post_status.ts new file mode 100644 index 00000000000..f58aca0e233 --- /dev/null +++ b/apps/sim/tools/tiktok/get_post_status.ts @@ -0,0 +1,133 @@ +import { tiktokPostStatusApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { TikTokGetPostStatusParams, TikTokGetPostStatusResponse } from '@/tools/tiktok/types' +import { readTikTokApiResponse } from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +function emptyPostStatusOutput(): TikTokGetPostStatusResponse['output'] { + return { + status: '', + failReason: null, + publiclyAvailablePostId: [], + uploadedBytes: null, + downloadedBytes: null, + } +} + +export const tiktokGetPostStatusTool: ToolConfig< + TikTokGetPostStatusParams, + TikTokGetPostStatusResponse +> = { + id: 'tiktok_get_post_status', + name: 'TikTok Get Post Status', + description: + 'Check the status of a post initiated with Direct Post Video, Upload Video Draft, Direct Post Photo, or Upload Photo Draft. Use the publishId returned from the post request to track progress.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish', 'video.upload'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + publishId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The publish ID returned from a post/upload tool.', + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', + method: 'POST', + headers: (params: TikTokGetPostStatusParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: (params: TikTokGetPostStatusParams) => ({ + publish_id: params.publishId.trim(), + }), + }, + + transformResponse: async (response: Response): Promise => { + /** TikTok's int64 post IDs must be extracted before JSON parsing rounds them. */ + const { + data: statusData, + error, + rawBody, + } = await readTikTokApiResponse(response, tiktokPostStatusApiDataSchema) + const postIdsMatch = rawBody.match(/"publicaly_available_post_id"\s*:\s*\[([^\]]*)\]/) + const publiclyAvailablePostId = postIdsMatch + ? postIdsMatch[1] + .split(',') + .map((id: string) => id.trim().replace(/^"|"$/g, '')) + .filter(Boolean) + : [] + + if (error) { + return { + success: false, + output: emptyPostStatusOutput(), + error: error.message || 'Failed to fetch post status', + } + } + + if (!statusData) { + return { + success: false, + output: emptyPostStatusOutput(), + error: 'No status data returned', + } + } + + return { + success: true, + output: { + status: statusData.status ?? '', + failReason: statusData.fail_reason ?? null, + publiclyAvailablePostId, + uploadedBytes: statusData.uploaded_bytes ?? null, + downloadedBytes: statusData.downloaded_bytes ?? null, + }, + } + }, + + outputs: { + status: { + type: 'string', + description: + 'Current status of the post. Values: PROCESSING_UPLOAD/PROCESSING_DOWNLOAD (TikTok is processing the media), SEND_TO_USER_INBOX (draft delivered, awaiting user action), PUBLISH_COMPLETE (successfully posted), FAILED (check failReason).', + }, + failReason: { + type: 'string', + description: 'Reason for failure if status is FAILED. Null otherwise.', + optional: true, + }, + publiclyAvailablePostId: { + type: 'array', + description: + 'Array of public post IDs (as strings) once the content is published and publicly viewable. Can be used to construct the TikTok post URL.', + items: { + type: 'string', + description: 'Public TikTok post ID', + }, + }, + uploadedBytes: { + type: 'number', + description: 'Number of bytes uploaded to TikTok for FILE_UPLOAD posts', + optional: true, + }, + downloadedBytes: { + type: 'number', + description: 'Number of bytes TikTok downloaded for PULL_FROM_URL posts', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tiktok/get_user.ts b/apps/sim/tools/tiktok/get_user.ts new file mode 100644 index 00000000000..384196a152d --- /dev/null +++ b/apps/sim/tools/tiktok/get_user.ts @@ -0,0 +1,173 @@ +import { tiktokGetUserApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { TikTokGetUserParams, TikTokGetUserResponse } from '@/tools/tiktok/types' +import { readTikTokApiResponse, TIKTOK_USER_FIELDS } from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +function emptyUserOutput(): TikTokGetUserResponse['output'] { + return { + openId: '', + unionId: null, + displayName: '', + bioDescription: null, + profileDeepLink: null, + isVerified: null, + username: null, + followerCount: null, + followingCount: null, + likesCount: null, + videoCount: null, + } +} + +export const tiktokGetUserTool: ToolConfig = { + id: 'tiktok_get_user', + name: 'TikTok Get User', + description: + 'Get the authenticated TikTok user profile information including display name, avatar, bio, follower count, and video statistics.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['user.info.basic', 'user.info.profile', 'user.info.stats'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + default: TIKTOK_USER_FIELDS, + description: + 'Comma-separated list of fields to return. Available: open_id, union_id, avatar_url, avatar_large_url, display_name, bio_description, profile_deep_link, is_verified, username, follower_count, following_count, likes_count, video_count. Include avatar_url and avatar_large_url to receive the avatarFile output.', + }, + }, + + request: { + url: (params: TikTokGetUserParams) => { + const fields = params.fields || TIKTOK_USER_FIELDS + return `https://open.tiktokapis.com/v2/user/info/?fields=${encodeURIComponent(fields)}` + }, + method: 'GET', + headers: (params: TikTokGetUserParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response): Promise => { + const { data, error } = await readTikTokApiResponse(response, tiktokGetUserApiDataSchema) + + if (error) { + return { + success: false, + output: emptyUserOutput(), + error: error.message || 'Failed to fetch user info', + } + } + + const user = data?.user + + if (!user) { + return { + success: false, + output: emptyUserOutput(), + error: 'No user data returned', + } + } + + const avatarSourceUrl = user.avatar_large_url ?? user.avatar_url + const avatarFileName = `${user.username || user.open_id || 'tiktok-user'}-avatar.jpg` + + return { + success: true, + output: { + openId: user.open_id ?? '', + unionId: user.union_id ?? null, + displayName: user.display_name ?? '', + bioDescription: user.bio_description ?? null, + profileDeepLink: user.profile_deep_link ?? null, + isVerified: user.is_verified ?? null, + username: user.username ?? null, + followerCount: user.follower_count ?? null, + followingCount: user.following_count ?? null, + likesCount: user.likes_count ?? null, + videoCount: user.video_count ?? null, + ...(avatarSourceUrl && { + avatarFile: { + name: avatarFileName, + mimeType: 'image/jpeg', + url: avatarSourceUrl, + }, + }), + }, + } + }, + + outputs: { + openId: { + type: 'string', + description: 'Unique TikTok user ID for this application', + }, + unionId: { + type: 'string', + description: 'Unique TikTok user ID across all apps from the same developer', + optional: true, + }, + displayName: { + type: 'string', + description: 'User display name', + }, + bioDescription: { + type: 'string', + description: 'User bio description', + optional: true, + }, + profileDeepLink: { + type: 'string', + description: 'Deep link to user TikTok profile', + optional: true, + }, + isVerified: { + type: 'boolean', + description: 'Whether the account is verified', + optional: true, + }, + username: { + type: 'string', + description: 'TikTok username', + optional: true, + }, + followerCount: { + type: 'number', + description: 'Number of followers', + optional: true, + }, + followingCount: { + type: 'number', + description: 'Number of accounts the user follows', + optional: true, + }, + likesCount: { + type: 'number', + description: 'Total likes received across all videos', + optional: true, + }, + videoCount: { + type: 'number', + description: 'Total number of public videos', + optional: true, + }, + avatarFile: { + type: 'file', + description: + 'Downloadable copy of the profile avatar image (largest available variant), stored as a workflow file so it can be chained into file-consuming blocks (e.g. attached to an email).', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tiktok/index.ts b/apps/sim/tools/tiktok/index.ts new file mode 100644 index 00000000000..a87fd762726 --- /dev/null +++ b/apps/sim/tools/tiktok/index.ts @@ -0,0 +1,21 @@ +import { tiktokDirectPostPhotoTool } from '@/tools/tiktok/direct_post_photo' +import { tiktokDirectPostVideoTool } from '@/tools/tiktok/direct_post_video' +import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status' +import { tiktokGetUserTool } from '@/tools/tiktok/get_user' +import { tiktokListVideosTool } from '@/tools/tiktok/list_videos' +import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info' +import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos' +import { tiktokUploadPhotoDraftTool } from '@/tools/tiktok/upload_photo_draft' +import { tiktokUploadVideoDraftTool } from '@/tools/tiktok/upload_video_draft' + +export * from '@/tools/tiktok/types' + +export { tiktokGetUserTool } +export { tiktokListVideosTool } +export { tiktokQueryVideosTool } +export { tiktokQueryCreatorInfoTool } +export { tiktokDirectPostVideoTool } +export { tiktokUploadVideoDraftTool } +export { tiktokDirectPostPhotoTool } +export { tiktokUploadPhotoDraftTool } +export { tiktokGetPostStatusTool } diff --git a/apps/sim/tools/tiktok/list_videos.ts b/apps/sim/tools/tiktok/list_videos.ts new file mode 100644 index 00000000000..d1d8e0bf7f6 --- /dev/null +++ b/apps/sim/tools/tiktok/list_videos.ts @@ -0,0 +1,115 @@ +import { tiktokVideosApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { + TikTokListVideosParams, + TikTokListVideosResponse, + TikTokVideo, +} from '@/tools/tiktok/types' +import { + mapTikTokVideo, + readTikTokApiResponse, + TIKTOK_VIDEO_FIELDS, + TIKTOK_VIDEO_OUTPUT_PROPERTIES, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +export const tiktokListVideosTool: ToolConfig = { + id: 'tiktok_list_videos', + name: 'TikTok List Videos', + description: + "Get a list of the authenticated user's TikTok videos with cover images, titles, and metadata. Supports pagination.", + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.list'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + maxCount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 10, + description: 'Maximum number of videos to return (1-20)', + }, + cursor: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination (from previous response)', + }, + }, + + request: { + url: () => `https://open.tiktokapis.com/v2/video/list/?fields=${TIKTOK_VIDEO_FIELDS}`, + method: 'POST', + headers: (params: TikTokListVideosParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: TikTokListVideosParams) => { + const maxCount = params.maxCount ?? 10 + if (!Number.isInteger(maxCount) || maxCount < 1 || maxCount > 20) { + throw new Error('maxCount must be an integer between 1 and 20') + } + return { + max_count: maxCount, + ...(params.cursor !== undefined && { cursor: params.cursor }), + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const { data, error } = await readTikTokApiResponse(response, tiktokVideosApiDataSchema) + + if (error) { + return { + success: false, + output: { + videos: [], + cursor: null, + hasMore: false, + }, + error: error.message || 'Failed to fetch videos', + } + } + + const videos: TikTokVideo[] = (data?.videos ?? []).map(mapTikTokVideo) + + return { + success: true, + output: { + videos, + cursor: data?.cursor ?? null, + hasMore: data?.has_more ?? false, + }, + } + }, + + outputs: { + videos: { + type: 'array', + description: 'List of TikTok videos', + items: { + type: 'object', + properties: TIKTOK_VIDEO_OUTPUT_PROPERTIES, + }, + }, + cursor: { + type: 'number', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more videos to fetch', + }, + }, +} diff --git a/apps/sim/tools/tiktok/query_creator_info.ts b/apps/sim/tools/tiktok/query_creator_info.ts new file mode 100644 index 00000000000..f39f767daa4 --- /dev/null +++ b/apps/sim/tools/tiktok/query_creator_info.ts @@ -0,0 +1,133 @@ +import { tiktokCreatorInfoApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { + TikTokQueryCreatorInfoParams, + TikTokQueryCreatorInfoResponse, +} from '@/tools/tiktok/types' +import { readTikTokApiResponse } from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +function emptyCreatorInfoOutput(): TikTokQueryCreatorInfoResponse['output'] { + return { + creatorAvatarUrl: null, + creatorUsername: null, + creatorNickname: null, + privacyLevelOptions: [], + commentDisabled: false, + duetDisabled: false, + stitchDisabled: false, + maxVideoPostDurationSec: null, + } +} + +export const tiktokQueryCreatorInfoTool: ToolConfig< + TikTokQueryCreatorInfoParams, + TikTokQueryCreatorInfoResponse +> = { + id: 'tiktok_query_creator_info', + name: 'TikTok Query Creator Info', + description: + 'Check if the authenticated TikTok user can post content and retrieve their available privacy options, interaction settings, and maximum video duration. Required before any post per TikTok UX guidelines.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/creator_info/query/', + method: 'POST', + headers: (params: TikTokQueryCreatorInfoParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response): Promise => { + const { data: creatorInfo, error } = await readTikTokApiResponse( + response, + tiktokCreatorInfoApiDataSchema + ) + + if (error) { + return { + success: false, + output: emptyCreatorInfoOutput(), + error: error.message || 'Failed to query creator info', + } + } + + if (!creatorInfo) { + return { + success: false, + output: emptyCreatorInfoOutput(), + error: 'No creator info returned', + } + } + + return { + success: true, + output: { + creatorAvatarUrl: creatorInfo.creator_avatar_url ?? null, + creatorUsername: creatorInfo.creator_username ?? null, + creatorNickname: creatorInfo.creator_nickname ?? null, + privacyLevelOptions: creatorInfo.privacy_level_options ?? [], + commentDisabled: creatorInfo.comment_disabled ?? false, + duetDisabled: creatorInfo.duet_disabled ?? false, + stitchDisabled: creatorInfo.stitch_disabled ?? false, + maxVideoPostDurationSec: creatorInfo.max_video_post_duration_sec ?? null, + }, + } + }, + + outputs: { + creatorAvatarUrl: { + type: 'string', + description: 'URL of the creator avatar', + optional: true, + }, + creatorUsername: { + type: 'string', + description: 'TikTok username of the creator', + optional: true, + }, + creatorNickname: { + type: 'string', + description: 'Display name/nickname of the creator', + optional: true, + }, + privacyLevelOptions: { + type: 'array', + description: + 'Available privacy levels for posting (e.g., PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)', + }, + commentDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled comments by default', + }, + duetDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled duets by default', + }, + stitchDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled stitches by default', + }, + maxVideoPostDurationSec: { + type: 'number', + description: 'Maximum allowed video duration in seconds', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tiktok/query_videos.ts b/apps/sim/tools/tiktok/query_videos.ts new file mode 100644 index 00000000000..88fc0a915a3 --- /dev/null +++ b/apps/sim/tools/tiktok/query_videos.ts @@ -0,0 +1,99 @@ +import { tiktokVideosApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { + TikTokQueryVideosParams, + TikTokQueryVideosResponse, + TikTokVideo, +} from '@/tools/tiktok/types' +import { + assertTikTokArrayLength, + mapTikTokVideo, + readTikTokApiResponse, + TIKTOK_VIDEO_FIELDS, + TIKTOK_VIDEO_OUTPUT_PROPERTIES, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +export const tiktokQueryVideosTool: ToolConfig = + { + id: 'tiktok_query_videos', + name: 'TikTok Query Videos', + description: + 'Query specific TikTok videos by their IDs to get fresh metadata including cover images, embed links, and video details.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.list'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + videoIds: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Array of video IDs to query (maximum 20)', + items: { + type: 'string', + description: 'TikTok video ID', + }, + }, + }, + + request: { + url: () => `https://open.tiktokapis.com/v2/video/query/?fields=${TIKTOK_VIDEO_FIELDS}`, + method: 'POST', + headers: (params: TikTokQueryVideosParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: TikTokQueryVideosParams) => { + assertTikTokArrayLength(params.videoIds, 'videoIds', 20) + return { + filters: { + video_ids: params.videoIds, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const { data, error } = await readTikTokApiResponse(response, tiktokVideosApiDataSchema) + + if (error) { + return { + success: false, + output: { + videos: [], + }, + error: error.message || 'Failed to query videos', + } + } + + const videos: TikTokVideo[] = (data?.videos ?? []).map(mapTikTokVideo) + + return { + success: true, + output: { + videos, + }, + } + }, + + outputs: { + videos: { + type: 'array', + description: 'List of queried TikTok videos', + items: { + type: 'object', + properties: TIKTOK_VIDEO_OUTPUT_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/tiktok/types.ts b/apps/sim/tools/tiktok/types.ts new file mode 100644 index 00000000000..ee2c1e41a34 --- /dev/null +++ b/apps/sim/tools/tiktok/types.ts @@ -0,0 +1,212 @@ +import type { UserFile } from '@/executor/types' +import type { ToolResponse } from '@/tools/types' + +/** + * Base params that include OAuth access token + */ +export interface TikTokBaseParams { + accessToken: string +} + +/** Error envelope returned by TikTok APIs. */ +export interface TikTokApiError { + code: string + message?: string + logId?: string +} + +/** + * Get User Info + */ +export interface TikTokGetUserParams extends TikTokBaseParams { + fields?: string +} + +export interface TikTokGetUserResponse extends ToolResponse { + output: { + openId: string + unionId: string | null + displayName: string + bioDescription: string | null + profileDeepLink: string | null + isVerified: boolean | null + username: string | null + followerCount: number | null + followingCount: number | null + likesCount: number | null + videoCount: number | null + avatarFile?: { + name: string + mimeType: string + url: string + } + } +} + +/** + * List Videos + */ +export interface TikTokListVideosParams extends TikTokBaseParams { + maxCount?: number + cursor?: number +} + +export interface TikTokVideo { + id: string + title: string | null + coverImageUrl: string | null + embedLink: string | null + embedHtml: string | null + duration: number | null + createTime: number | null + shareUrl: string | null + videoDescription: string | null + width: number | null + height: number | null + viewCount: number | null + likeCount: number | null + commentCount: number | null + shareCount: number | null +} + +export interface TikTokListVideosResponse extends ToolResponse { + output: { + videos: TikTokVideo[] + cursor: number | null + hasMore: boolean + } +} + +/** + * Query Videos + */ +export interface TikTokQueryVideosParams extends TikTokBaseParams { + videoIds: string[] +} + +export interface TikTokQueryVideosResponse extends ToolResponse { + output: { + videos: TikTokVideo[] + } +} + +/** + * Query Creator Info - Check posting permissions and get privacy options + */ +export interface TikTokQueryCreatorInfoParams extends TikTokBaseParams {} + +export interface TikTokQueryCreatorInfoResponse extends ToolResponse { + output: { + creatorAvatarUrl: string | null + creatorUsername: string | null + creatorNickname: string | null + privacyLevelOptions: string[] + commentDisabled: boolean + duetDisabled: boolean + stitchDisabled: boolean + maxVideoPostDurationSec: number | null + } +} + +/** + * Shared media transfer source for video operations + */ +export type TikTokVideoSource = 'PULL_FROM_URL' | 'FILE_UPLOAD' + +/** + * Direct Post Video - Publish video (by URL or uploaded file) to TikTok + */ +export interface TikTokDirectPostVideoParams extends TikTokBaseParams { + source: TikTokVideoSource + videoUrl?: string + file?: UserFile + title?: string + privacyLevel: string + disableDuet?: boolean + disableStitch?: boolean + disableComment?: boolean + videoCoverTimestampMs?: number + isAigc?: boolean + brandContentToggle?: boolean + brandOrganicToggle?: boolean +} + +/** Shared response shape for TikTok publish and draft initialization tools. */ +export interface TikTokPublishResponse extends ToolResponse { + output: { + publishId: string + } +} + +export type TikTokDirectPostVideoResponse = TikTokPublishResponse + +/** + * Upload Video Draft - Send a video to the user's TikTok inbox for manual editing/posting + */ +export interface TikTokUploadVideoDraftParams extends TikTokBaseParams { + source: TikTokVideoSource + videoUrl?: string + file?: UserFile +} + +export type TikTokUploadVideoDraftResponse = TikTokPublishResponse + +/** + * Direct Post Photo - Publish photo(s) from public URLs to TikTok + */ +export interface TikTokDirectPostPhotoParams extends TikTokBaseParams { + photoImages: string[] + photoCoverIndex?: number + title?: string + description?: string + privacyLevel: string + disableComment?: boolean + autoAddMusic?: boolean + brandContentToggle?: boolean + brandOrganicToggle?: boolean +} + +export type TikTokDirectPostPhotoResponse = TikTokPublishResponse + +/** + * Upload Photo Draft - Send photo(s) to the user's TikTok inbox for manual editing/posting + */ +export interface TikTokUploadPhotoDraftParams extends TikTokBaseParams { + photoImages: string[] + photoCoverIndex?: number + title?: string + description?: string +} + +export type TikTokUploadPhotoDraftResponse = TikTokPublishResponse + +/** + * Get Post Status - Check status of a published/uploaded post + */ +export interface TikTokGetPostStatusParams extends TikTokBaseParams { + publishId: string +} + +export interface TikTokGetPostStatusResponse extends ToolResponse { + output: { + status: string + failReason: string | null + publiclyAvailablePostId: string[] + uploadedBytes: number | null + downloadedBytes: number | null + } +} + +/** + * Union type of all TikTok responses + */ +export type TikTokResponse = + | TikTokGetUserResponse + | TikTokListVideosResponse + | TikTokQueryVideosResponse + | TikTokQueryCreatorInfoResponse + | TikTokDirectPostVideoResponse + | TikTokUploadVideoDraftResponse + | TikTokDirectPostPhotoResponse + | TikTokUploadPhotoDraftResponse + | TikTokGetPostStatusResponse diff --git a/apps/sim/tools/tiktok/upload_photo_draft.ts b/apps/sim/tools/tiktok/upload_photo_draft.ts new file mode 100644 index 00000000000..e8ac6f6f164 --- /dev/null +++ b/apps/sim/tools/tiktok/upload_photo_draft.ts @@ -0,0 +1,110 @@ +import type { + TikTokUploadPhotoDraftParams, + TikTokUploadPhotoDraftResponse, +} from '@/tools/tiktok/types' +import { + readTikTokPublishInitResponse, + resolveTikTokPhotoCoverIndex, + toTikTokPublishToolResponse, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +export const tiktokUploadPhotoDraftTool: ToolConfig< + TikTokUploadPhotoDraftParams, + TikTokUploadPhotoDraftResponse +> = { + id: 'tiktok_upload_photo_draft', + name: 'TikTok Upload Photo Draft', + description: + "Send one or more photos (from public URLs) to the authenticated user's TikTok inbox for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Photos only support PULL_FROM_URL (no file upload). Rate limit: 6 requests per minute per user.", + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.upload'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + photoImages: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'Public, verified-domain URLs of the images to upload (up to 35). Must be JPEG or WEBP — TikTok rejects PNG.', + items: { + type: 'string', + description: 'Public image URL', + }, + }, + photoCoverIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 0, + description: 'Index (starting from 0) of the photo to use as the cover.', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post title. Maximum 90 characters.', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post description. Maximum 4000 characters.', + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/content/init/', + method: 'POST', + headers: (params: TikTokUploadPhotoDraftParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: (params: TikTokUploadPhotoDraftParams) => { + const photoCoverIndex = resolveTikTokPhotoCoverIndex( + params.photoImages, + params.photoCoverIndex + ) + + const postInfo: Record = {} + + if (params.title) postInfo.title = params.title + if (params.description) postInfo.description = params.description + + return { + media_type: 'PHOTO', + post_mode: 'MEDIA_UPLOAD', + post_info: postInfo, + source_info: { + source: 'PULL_FROM_URL', + photo_images: params.photoImages, + photo_cover_index: photoCoverIndex, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const result = await readTikTokPublishInitResponse(response) + return toTikTokPublishToolResponse(result) + }, + + outputs: { + publishId: { + type: 'string', + description: + 'Unique identifier for tracking the draft status. Use this with the Get Post Status tool to check when the user has completed posting from their inbox.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/upload_video_draft.ts b/apps/sim/tools/tiktok/upload_video_draft.ts new file mode 100644 index 00000000000..f038c0425c0 --- /dev/null +++ b/apps/sim/tools/tiktok/upload_video_draft.ts @@ -0,0 +1,98 @@ +import type { + TikTokUploadVideoDraftParams, + TikTokUploadVideoDraftResponse, +} from '@/tools/tiktok/types' +import { + assertTikTokVideoSourceInput, + readTikTokPublishInitResponse, + toTikTokPublishToolResponse, +} from '@/tools/tiktok/utils' +import type { ToolConfig } from '@/tools/types' + +export const tiktokUploadVideoDraftTool: ToolConfig< + TikTokUploadVideoDraftParams, + TikTokUploadVideoDraftResponse +> = { + id: 'tiktok_upload_video_draft', + name: 'TikTok Upload Video Draft', + description: + "Send a video to the authenticated user's TikTok inbox (by public URL or uploaded file) for manual editing and posting. The user must open TikTok and tap the inbox notification to complete the post. Rate limit: 6 requests per minute per user.", + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.upload'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'TikTok OAuth access token', + }, + source: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Media transfer method: 'PULL_FROM_URL' or 'FILE_UPLOAD'.", + }, + videoUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Public URL of the video to upload (used when source is PULL_FROM_URL). The domain/URL prefix must be verified in the TikTok developer portal.', + }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Video file to upload from the workflow (used when source is FILE_UPLOAD).', + }, + }, + + request: { + url: (params: TikTokUploadVideoDraftParams) => + params.source === 'FILE_UPLOAD' + ? '/api/tools/tiktok/publish-video' + : 'https://open.tiktokapis.com/v2/post/publish/inbox/video/init/', + method: 'POST', + headers: (params: TikTokUploadVideoDraftParams) => ({ + ...(params.source !== 'FILE_UPLOAD' && { Authorization: `Bearer ${params.accessToken}` }), + 'Content-Type': + params.source === 'FILE_UPLOAD' ? 'application/json' : 'application/json; charset=UTF-8', + }), + body: (params: TikTokUploadVideoDraftParams) => { + assertTikTokVideoSourceInput(params.source, params.videoUrl, params.file) + if (params.source === 'FILE_UPLOAD') { + return { + accessToken: params.accessToken, + mode: 'draft', + file: params.file, + } + } + + return { + source_info: { + source: 'PULL_FROM_URL', + video_url: params.videoUrl, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const result = await readTikTokPublishInitResponse(response) + return toTikTokPublishToolResponse(result) + }, + + outputs: { + publishId: { + type: 'string', + description: + 'Unique identifier for tracking the draft status. Use this with the Get Post Status tool to check when the user has completed posting from their inbox.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/utils.test.ts b/apps/sim/tools/tiktok/utils.test.ts new file mode 100644 index 00000000000..f075c2eb077 --- /dev/null +++ b/apps/sim/tools/tiktok/utils.test.ts @@ -0,0 +1,104 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' +import { + assertTikTokArrayLength, + assertTikTokVideoSourceInput, + mapTikTokVideo, + readTikTokApiResponse, + readTikTokPublishInitResponse, + resolveTikTokPhotoCoverIndex, +} from '@/tools/tiktok/utils' + +describe('TikTok tool utilities', () => { + it('turns non-JSON HTTP failures into a structured TikTok error', async () => { + const response = new Response('Bad gateway', { status: 502 }) + + await expect(readTikTokApiResponse(response, tiktokPublishInitApiDataSchema)).resolves.toEqual({ + data: null, + error: { + code: 'http_502', + message: 'TikTok request failed with HTTP 502: Bad gateway', + }, + rawBody: 'Bad gateway', + }) + }) + + it('preserves internal publish route failures', async () => { + const response = Response.json( + { success: false, output: { publishId: '' }, error: 'Upload failed' }, + { status: 400 } + ) + + await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + success: false, + publishId: '', + error: 'Upload failed', + }) + }) + + it('validates TikTok response data before exposing it to tools', async () => { + const response = Response.json({ + data: { publish_id: 123 }, + error: { code: 'ok' }, + }) + + await expect(readTikTokApiResponse(response, tiktokPublishInitApiDataSchema)).resolves.toEqual({ + data: null, + error: { + code: 'invalid_response', + message: 'TikTok returned an unexpected data shape', + }, + rawBody: '{"data":{"publish_id":123},"error":{"code":"ok"}}', + }) + }) + + it('normalizes direct TikTok publish responses', async () => { + const response = Response.json({ + data: { publish_id: 'publish-1' }, + error: { code: 'ok' }, + }) + + await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + success: true, + publishId: 'publish-1', + }) + }) + + it('enforces TikTok array limits', () => { + expect(() => assertTikTokArrayLength([], 'videoIds', 20)).toThrow( + 'videoIds must contain at least one item' + ) + expect(() => assertTikTokArrayLength(Array.from({ length: 21 }), 'videoIds', 20)).toThrow( + 'videoIds supports at most 20 items' + ) + expect(() => assertTikTokArrayLength(['video-1'], 'videoIds', 20)).not.toThrow() + }) + + it('requires the input matching the selected video source', () => { + expect(() => assertTikTokVideoSourceInput('PULL_FROM_URL', undefined, undefined)).toThrow( + 'videoUrl is required when source is PULL_FROM_URL' + ) + expect(() => assertTikTokVideoSourceInput('FILE_UPLOAD', undefined, undefined)).toThrow( + 'file is required when source is FILE_UPLOAD' + ) + expect(() => + assertTikTokVideoSourceInput('PULL_FROM_URL', 'https://example.com/video.mp4', undefined) + ).not.toThrow() + }) + + it('validates photo cover indexes against the image list', () => { + expect(resolveTikTokPhotoCoverIndex(['photo-1'])).toBe(0) + expect(() => resolveTikTokPhotoCoverIndex(['photo-1'], 1)).toThrow( + 'photoCoverIndex must refer to an item in photoImages' + ) + }) + + it('maps TikTok embed HTML', () => { + expect(mapTikTokVideo({ id: 'video-1', embed_html: '
' }).embedHtml).toBe( + '
' + ) + }) +}) diff --git a/apps/sim/tools/tiktok/utils.ts b/apps/sim/tools/tiktok/utils.ts new file mode 100644 index 00000000000..126e4ee1ef9 --- /dev/null +++ b/apps/sim/tools/tiktok/utils.ts @@ -0,0 +1,302 @@ +import { truncate } from '@sim/utils/string' +import type { ZodType } from 'zod' +import { type TikTokApiVideo, tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' +import type { TikTokApiError, TikTokPublishResponse, TikTokVideo } from '@/tools/tiktok/types' +import type { OutputProperty } from '@/tools/types' + +/** + * Default fields requested from TikTok's `/v2/user/info/` endpoint, covering the + * `user.info.basic`, `user.info.profile`, and `user.info.stats` scopes. + * `avatar_url` and `avatar_large_url` feed the file-typed `avatarFile` output. + */ +export const TIKTOK_USER_FIELDS = + 'open_id,union_id,avatar_url,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count' + +/** + * Fields requested from TikTok's `/v2/video/list/` and `/v2/video/query/` endpoints. + * All are available under the `video.list` scope. + */ +export const TIKTOK_VIDEO_FIELDS = + 'id,title,cover_image_url,embed_link,embed_html,duration,create_time,share_url,video_description,width,height,view_count,like_count,comment_count,share_count' + +/** Shared output schema for video objects returned by list and query tools. */ +export const TIKTOK_VIDEO_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'Video ID' }, + title: { type: 'string', description: 'Video title', optional: true }, + coverImageUrl: { + type: 'string', + description: + 'Signed TikTok CDN cover URL. It is public but time-limited, so consume it immediately.', + optional: true, + }, + embedLink: { type: 'string', description: 'Embeddable video URL', optional: true }, + embedHtml: { type: 'string', description: 'HTML embed markup for the video', optional: true }, + duration: { type: 'number', description: 'Video duration in seconds', optional: true }, + createTime: { + type: 'number', + description: 'Unix timestamp when the video was created', + optional: true, + }, + shareUrl: { type: 'string', description: 'Shareable video URL', optional: true }, + videoDescription: { + type: 'string', + description: 'Video description or caption', + optional: true, + }, + width: { type: 'number', description: 'Video width in pixels', optional: true }, + height: { type: 'number', description: 'Video height in pixels', optional: true }, + viewCount: { type: 'number', description: 'Number of views', optional: true }, + likeCount: { type: 'number', description: 'Number of likes', optional: true }, + commentCount: { type: 'number', description: 'Number of comments', optional: true }, + shareCount: { type: 'number', description: 'Number of shares', optional: true }, +} satisfies Record + +export function mapTikTokVideo(video: TikTokApiVideo): TikTokVideo { + return { + id: video.id ?? '', + title: video.title ?? null, + coverImageUrl: video.cover_image_url ?? null, + embedLink: video.embed_link ?? null, + embedHtml: video.embed_html ?? null, + duration: video.duration ?? null, + createTime: video.create_time ?? null, + shareUrl: video.share_url ?? null, + videoDescription: video.video_description ?? null, + width: video.width ?? null, + height: video.height ?? null, + viewCount: video.view_count ?? null, + likeCount: video.like_count ?? null, + commentCount: video.comment_count ?? null, + shareCount: video.share_count ?? null, + } +} + +interface ParsedTikTokApiResponse { + data: TData | null + error: TikTokApiError | null + rawBody: string +} + +interface ParsedJsonObject { + body: Record | null + error: TikTokApiError | null + rawBody: string +} + +interface TikTokPublishInitResult { + success: boolean + publishId: string + error?: string +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} + +function asRecord(value: unknown): Record | null { + return isRecord(value) ? value : null +} + +function parseTikTokError(value: unknown): TikTokApiError | null { + const error = asRecord(value) + if (!error) return null + + const code = typeof error.code === 'string' ? error.code : null + if (!code) return null + + return { + code, + ...(typeof error.message === 'string' ? { message: error.message } : {}), + ...(typeof error.log_id === 'string' ? { logId: error.log_id } : {}), + } +} + +function httpError(response: Response, rawBody: string, message?: string): TikTokApiError { + return { + code: `http_${response.status}`, + message: + message ?? + `TikTok request failed with HTTP ${response.status}: ${truncate(rawBody.trim(), 300)}`, + } +} + +async function readJsonObject(response: Response): Promise { + const rawBody = await response.text() + let parsed: unknown + + try { + parsed = JSON.parse(rawBody) + } catch { + return { + body: null, + error: response.ok + ? { code: 'invalid_response', message: 'TikTok returned an invalid JSON response' } + : httpError(response, rawBody), + rawBody, + } + } + + const body = asRecord(parsed) + if (!body) { + return { + body: null, + error: { code: 'invalid_response', message: 'TikTok returned an unexpected response shape' }, + rawBody, + } + } + + return { body, error: null, rawBody } +} + +function parseApiEnvelope( + response: Response, + parsed: ParsedJsonObject, + dataSchema: ZodType +): ParsedTikTokApiResponse { + if (!parsed.body) { + return { data: null, error: parsed.error, rawBody: parsed.rawBody } + } + + const providerError = parseTikTokError(parsed.body.error) + if (providerError?.code && providerError.code !== 'ok') { + return { data: null, error: providerError, rawBody: parsed.rawBody } + } + + if (!response.ok) { + return { + data: null, + error: httpError(response, parsed.rawBody, providerError?.message), + rawBody: parsed.rawBody, + } + } + + if (parsed.body.data === null || parsed.body.data === undefined) { + return { data: null, error: null, rawBody: parsed.rawBody } + } + + const dataResult = dataSchema.safeParse(parsed.body.data) + if (!dataResult.success) { + return { + data: null, + error: { + code: 'invalid_response', + message: 'TikTok returned an unexpected data shape', + }, + rawBody: parsed.rawBody, + } + } + + return { data: dataResult.data, error: null, rawBody: parsed.rawBody } +} + +/** Reads and normalizes a typed TikTok API envelope. */ +export async function readTikTokApiResponse( + response: Response, + dataSchema: ZodType +): Promise> { + return parseApiEnvelope(response, await readJsonObject(response), dataSchema) +} + +/** Enforces TikTok's bounded array request limits before making a network request. */ +export function assertTikTokArrayLength(values: unknown[], label: string, maximum: number): void { + if (values.length === 0) { + throw new Error(`${label} must contain at least one item`) + } + if (values.length > maximum) { + throw new Error(`${label} supports at most ${maximum} items`) + } +} + +/** Validates photo count and returns a cover index that points into the image list. */ +export function resolveTikTokPhotoCoverIndex( + photoImages: string[], + requestedIndex?: number +): number { + assertTikTokArrayLength(photoImages, 'photoImages', 35) + const coverIndex = requestedIndex ?? 0 + if (!Number.isInteger(coverIndex) || coverIndex < 0) { + throw new Error('photoCoverIndex must be a non-negative integer') + } + if (coverIndex >= photoImages.length) { + throw new Error('photoCoverIndex must refer to an item in photoImages') + } + return coverIndex +} + +/** Validates that the selected video transfer source has its corresponding input. */ +export function assertTikTokVideoSourceInput( + source: string, + videoUrl: string | undefined, + file: unknown +): void { + if (source === 'PULL_FROM_URL') { + if (!videoUrl?.trim()) throw new Error('videoUrl is required when source is PULL_FROM_URL') + return + } + if (source === 'FILE_UPLOAD') { + if (!file) throw new Error('file is required when source is FILE_UPLOAD') + return + } + throw new Error('source must be PULL_FROM_URL or FILE_UPLOAD') +} + +/** + * Video/photo publish-init tools can hit TikTok directly (PULL_FROM_URL, calling + * TikTok's own `/init/` endpoints) or an internal Sim route (FILE_UPLOAD, which + * downloads the workflow file and performs the chunked PUT before responding). + * The two paths return different envelopes, so reading and normalization share one boundary. + */ +export async function readTikTokPublishInitResponse( + response: Response +): Promise { + const parsed = await readJsonObject(response) + if (!parsed.body) { + return { + success: false, + publishId: '', + error: parsed.error?.message ?? 'Failed to read publish response', + } + } + + if ('success' in parsed.body) { + if (parsed.body.success !== true) { + return { + success: false, + publishId: '', + error: typeof parsed.body.error === 'string' ? parsed.body.error : 'Failed to publish', + } + } + + const output = asRecord(parsed.body.output) + const publishId = typeof output?.publishId === 'string' ? output.publishId : '' + return publishId + ? { success: true, publishId } + : { success: false, publishId: '', error: 'No publish ID returned' } + } + + const result = parseApiEnvelope(response, parsed, tiktokPublishInitApiDataSchema) + if (result.error) { + return { + success: false, + publishId: '', + error: result.error.message ?? 'Failed to initiate post', + } + } + + return result.data?.publish_id + ? { success: true, publishId: result.data.publish_id } + : { success: false, publishId: '', error: 'No publish ID returned' } +} + +/** Converts a normalized publish result into the shared tool response shape. */ +export function toTikTokPublishToolResponse( + result: TikTokPublishInitResult +): TikTokPublishResponse { + return result.success + ? { success: true, output: { publishId: result.publishId } } + : { + success: false, + output: { publishId: '' }, + error: result.error ?? 'Failed to initiate TikTok publish', + } +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 67416d77974..e8dd16ccbd0 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -383,6 +383,16 @@ import { slackWebhookTrigger } from '@/triggers/slack' import { stripeWebhookTrigger } from '@/triggers/stripe' import { tableNewRowTrigger } from '@/triggers/table' import { telegramWebhookTrigger } from '@/triggers/telegram' +import { + tiktokAuthorizationRemovedTrigger, + tiktokPostInboxDeliveredTrigger, + tiktokPostNoLongerPublicTrigger, + tiktokPostPubliclyAvailableTrigger, + tiktokPostPublishCompleteTrigger, + tiktokPostPublishFailedTrigger, + tiktokVideoPublishCompletedTrigger, + tiktokVideoUploadFailedTrigger, +} from '@/triggers/tiktok' import { twilioSmsReceivedTrigger, twilioSmsStatusTrigger } from '@/triggers/twilio' import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice' import { typeformWebhookTrigger } from '@/triggers/typeform' @@ -672,6 +682,14 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { stripe_webhook: stripeWebhookTrigger, table_new_row: tableNewRowTrigger, telegram_webhook: telegramWebhookTrigger, + tiktok_post_publish_complete: tiktokPostPublishCompleteTrigger, + tiktok_post_publish_failed: tiktokPostPublishFailedTrigger, + tiktok_post_inbox_delivered: tiktokPostInboxDeliveredTrigger, + tiktok_post_publicly_available: tiktokPostPubliclyAvailableTrigger, + tiktok_post_no_longer_public: tiktokPostNoLongerPublicTrigger, + tiktok_video_publish_completed: tiktokVideoPublishCompletedTrigger, + tiktok_video_upload_failed: tiktokVideoUploadFailedTrigger, + tiktok_authorization_removed: tiktokAuthorizationRemovedTrigger, typeform_webhook: typeformWebhookTrigger, whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, diff --git a/apps/sim/triggers/tiktok/authorization_removed.ts b/apps/sim/triggers/tiktok/authorization_removed.ts new file mode 100644 index 00000000000..9845b6e13a1 --- /dev/null +++ b/apps/sim/triggers/tiktok/authorization_removed.ts @@ -0,0 +1,25 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokAuthorizationRemovedOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokAuthorizationRemovedTrigger: TriggerConfig = { + id: 'tiktok_authorization_removed', + name: 'TikTok Authorization Removed', + provider: 'tiktok', + description: 'Trigger when a user deauthorizes your TikTok app', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks('tiktok_authorization_removed', 'authorization.removed'), + + outputs: buildTikTokAuthorizationRemovedOutputs(), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/index.ts b/apps/sim/triggers/tiktok/index.ts new file mode 100644 index 00000000000..87d4219c68c --- /dev/null +++ b/apps/sim/triggers/tiktok/index.ts @@ -0,0 +1,8 @@ +export { tiktokAuthorizationRemovedTrigger } from './authorization_removed' +export { tiktokPostInboxDeliveredTrigger } from './post_inbox_delivered' +export { tiktokPostNoLongerPublicTrigger } from './post_no_longer_public' +export { tiktokPostPubliclyAvailableTrigger } from './post_publicly_available' +export { tiktokPostPublishCompleteTrigger } from './post_publish_complete' +export { tiktokPostPublishFailedTrigger } from './post_publish_failed' +export { tiktokVideoPublishCompletedTrigger } from './video_publish_completed' +export { tiktokVideoUploadFailedTrigger } from './video_upload_failed' diff --git a/apps/sim/triggers/tiktok/post_inbox_delivered.ts b/apps/sim/triggers/tiktok/post_inbox_delivered.ts new file mode 100644 index 00000000000..ab729fdaa3d --- /dev/null +++ b/apps/sim/triggers/tiktok/post_inbox_delivered.ts @@ -0,0 +1,28 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokPostingOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokPostInboxDeliveredTrigger: TriggerConfig = { + id: 'tiktok_post_inbox_delivered', + name: 'TikTok Post Inbox Delivered', + provider: 'tiktok', + description: 'Trigger when a draft notification is delivered to the creator inbox', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks( + 'tiktok_post_inbox_delivered', + 'post.publish.inbox_delivered' + ), + + outputs: buildTikTokPostingOutputs(), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/post_no_longer_public.ts b/apps/sim/triggers/tiktok/post_no_longer_public.ts new file mode 100644 index 00000000000..2adc1796b9c --- /dev/null +++ b/apps/sim/triggers/tiktok/post_no_longer_public.ts @@ -0,0 +1,28 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokPostingOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokPostNoLongerPublicTrigger: TriggerConfig = { + id: 'tiktok_post_no_longer_public', + name: 'TikTok Post No Longer Public', + provider: 'tiktok', + description: 'Trigger when a post is no longer publicly viewable on TikTok', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks( + 'tiktok_post_no_longer_public', + 'post.publish.no_longer_publicaly_available' + ), + + outputs: buildTikTokPostingOutputs({ includePostId: true }), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/post_publicly_available.ts b/apps/sim/triggers/tiktok/post_publicly_available.ts new file mode 100644 index 00000000000..54fc9caca42 --- /dev/null +++ b/apps/sim/triggers/tiktok/post_publicly_available.ts @@ -0,0 +1,28 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokPostingOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokPostPubliclyAvailableTrigger: TriggerConfig = { + id: 'tiktok_post_publicly_available', + name: 'TikTok Post Publicly Available', + provider: 'tiktok', + description: 'Trigger when a published post becomes publicly viewable on TikTok', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks( + 'tiktok_post_publicly_available', + 'post.publish.publicly_available' + ), + + outputs: buildTikTokPostingOutputs({ includePostId: true }), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/post_publish_complete.ts b/apps/sim/triggers/tiktok/post_publish_complete.ts new file mode 100644 index 00000000000..e77eb2d91bc --- /dev/null +++ b/apps/sim/triggers/tiktok/post_publish_complete.ts @@ -0,0 +1,41 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokPostingOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, + tiktokTriggerOptions, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Primary TikTok trigger — includes the trigger-type dropdown. + * Fires when Content Posting completes (direct post or inbox draft published). + */ +export const tiktokPostPublishCompleteTrigger: TriggerConfig = { + id: 'tiktok_post_publish_complete', + name: 'TikTok Post Publish Complete', + provider: 'tiktok', + description: 'Trigger when a TikTok Content Posting publish completes', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: [ + { + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + mode: 'trigger', + options: tiktokTriggerOptions, + value: () => 'tiktok_post_publish_complete', + required: true, + }, + ...buildTikTokTriggerSubBlocks('tiktok_post_publish_complete', 'post.publish.complete'), + ], + + outputs: buildTikTokPostingOutputs(), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/post_publish_failed.ts b/apps/sim/triggers/tiktok/post_publish_failed.ts new file mode 100644 index 00000000000..a60d0251668 --- /dev/null +++ b/apps/sim/triggers/tiktok/post_publish_failed.ts @@ -0,0 +1,25 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokPostingOutputs, + buildTikTokTriggerSubBlocks, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokPostPublishFailedTrigger: TriggerConfig = { + id: 'tiktok_post_publish_failed', + name: 'TikTok Post Publish Failed', + provider: 'tiktok', + description: 'Trigger when a TikTok Content Posting publish fails', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks('tiktok_post_publish_failed', 'post.publish.failed'), + + outputs: buildTikTokPostingOutputs({ includeFailReason: true }), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/utils.ts b/apps/sim/triggers/tiktok/utils.ts new file mode 100644 index 00000000000..398ed9fd284 --- /dev/null +++ b/apps/sim/triggers/tiktok/utils.ts @@ -0,0 +1,145 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +export const tiktokTriggerOptions = [ + { label: 'Post Publish Complete', id: 'tiktok_post_publish_complete' }, + { label: 'Post Publish Failed', id: 'tiktok_post_publish_failed' }, + { label: 'Post Inbox Delivered', id: 'tiktok_post_inbox_delivered' }, + { label: 'Post Publicly Available', id: 'tiktok_post_publicly_available' }, + { label: 'Post No Longer Public', id: 'tiktok_post_no_longer_public' }, + { label: 'Video Publish Completed', id: 'tiktok_video_publish_completed' }, + { label: 'Video Upload Failed', id: 'tiktok_video_upload_failed' }, + { label: 'Authorization Removed', id: 'tiktok_authorization_removed' }, +] + +/** Map Sim trigger ids to TikTok `event` strings (documented spelling preserved). */ +export const TIKTOK_TRIGGER_EVENT_MAP: Record = { + tiktok_post_publish_complete: 'post.publish.complete', + tiktok_post_publish_failed: 'post.publish.failed', + tiktok_post_inbox_delivered: 'post.publish.inbox_delivered', + tiktok_post_publicly_available: 'post.publish.publicly_available', + tiktok_post_no_longer_public: 'post.publish.no_longer_publicaly_available', + tiktok_video_publish_completed: 'video.publish.completed', + tiktok_video_upload_failed: 'video.upload.failed', + tiktok_authorization_removed: 'authorization.removed', +} + +export function isTikTokEventMatch(triggerId: string, event: string | undefined): boolean { + if (!event) return false + const expected = TIKTOK_TRIGGER_EVENT_MAP[triggerId] + return expected === event +} + +export function tiktokSetupInstructions(eventLabel: string): string { + const instructions = [ + 'App setup: A Sim operator must register the full app-level Callback URL https://<your-sim-domain>/api/webhooks/tiktok once in the TikTok Developer Portal. TikTok allows one Callback URL per app, so workflow builders do not paste a unique webhook URL.', + 'Connect the TikTok account that should receive this event using the credential selector above.', + `This trigger listens for ${eventLabel} events for that connected account.`, + 'Deploy the workflow to activate the trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index === 0 ? instruction : `${index}. ${instruction}`}
` + ) + .join('') +} + +/** + * Builds subBlocks for a TikTok trigger: OAuth credential + setup instructions. + * Omits the per-workflow Webhook URL field — TikTok uses a fixed app-level Callback URL. + */ +export function buildTikTokTriggerSubBlocks( + triggerId: string, + eventLabel: string +): SubBlockConfig[] { + return [ + { + id: 'triggerCredentials', + title: 'TikTok Account', + type: 'oauth-input', + serviceId: 'tiktok', + mode: 'trigger', + required: true, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: tiktokSetupInstructions(eventLabel), + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +function buildCommonOutputs(): Record { + return { + event: { type: 'string', description: 'TikTok webhook event name' }, + createTime: { + type: 'number', + description: 'UTC epoch seconds when the event occurred', + }, + userOpenId: { + type: 'string', + description: 'TikTok user open_id for the connected account', + }, + clientKey: { + type: 'string', + description: 'TikTok app client_key that received the event', + }, + } +} + +export function buildTikTokPostingOutputs(options?: { + includeFailReason?: boolean + includePostId?: boolean +}): Record { + const outputs: Record = { + ...buildCommonOutputs(), + publishId: { type: 'string', description: 'Content Posting API publish_id' }, + publishType: { + type: 'string', + description: 'Publish type (e.g. DIRECT_POST, INBOX_SHARE)', + }, + } + if (options?.includePostId) { + outputs.postId = { + type: 'string', + description: 'TikTok post_id when the post is publicly available', + } + } + if (options?.includeFailReason) { + outputs.failReason = { + type: 'string', + description: 'Failure reason enum from TikTok when publishing fails', + } + } + return outputs +} + +export function buildTikTokVideoKitOutputs(): Record { + return { + ...buildCommonOutputs(), + shareId: { type: 'string', description: 'Share Kit / Video Kit share_id' }, + } +} + +export function buildTikTokAuthorizationRemovedOutputs(): Record { + return { + ...buildCommonOutputs(), + reason: { + type: 'number', + description: + 'Revocation reason (0 unknown, 1 user disconnect, 2 account deleted, 3 age change, 4 banned, 5 developer revoke)', + }, + } +} + +export const TIKTOK_WEBHOOK_HEADERS = { + 'Content-Type': 'application/json', + 'TikTok-Signature': 't=,s=', +} as const diff --git a/apps/sim/triggers/tiktok/video_publish_completed.ts b/apps/sim/triggers/tiktok/video_publish_completed.ts new file mode 100644 index 00000000000..9cd74477337 --- /dev/null +++ b/apps/sim/triggers/tiktok/video_publish_completed.ts @@ -0,0 +1,28 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokTriggerSubBlocks, + buildTikTokVideoKitOutputs, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokVideoPublishCompletedTrigger: TriggerConfig = { + id: 'tiktok_video_publish_completed', + name: 'TikTok Video Publish Completed', + provider: 'tiktok', + description: 'Trigger when a Share Kit / Video Kit upload is published by the user', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks( + 'tiktok_video_publish_completed', + 'video.publish.completed' + ), + + outputs: buildTikTokVideoKitOutputs(), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/video_upload_failed.ts b/apps/sim/triggers/tiktok/video_upload_failed.ts new file mode 100644 index 00000000000..767d3b7ff1d --- /dev/null +++ b/apps/sim/triggers/tiktok/video_upload_failed.ts @@ -0,0 +1,25 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokTriggerSubBlocks, + buildTikTokVideoKitOutputs, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const tiktokVideoUploadFailedTrigger: TriggerConfig = { + id: 'tiktok_video_upload_failed', + name: 'TikTok Video Upload Failed', + provider: 'tiktok', + description: 'Trigger when a Share Kit / Video Kit upload fails in TikTok', + version: '1.0.0', + icon: TikTokIcon, + + subBlocks: buildTikTokTriggerSubBlocks('tiktok_video_upload_failed', 'video.upload.failed'), + + outputs: buildTikTokVideoKitOutputs(), + + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/packages/db/migrations/0258_gigantic_lady_mastermind.sql b/packages/db/migrations/0258_gigantic_lady_mastermind.sql new file mode 100644 index 00000000000..e35e5dc07ff --- /dev/null +++ b/packages/db/migrations/0258_gigantic_lady_mastermind.sql @@ -0,0 +1,4 @@ +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "webhook_tiktok_credential_id_idx" ON "webhook" USING btree ((("provider_config")::jsonb ->> 'credentialId')) WHERE "webhook"."provider" = 'tiktok' AND "webhook"."is_active" = true AND "webhook"."archived_at" IS NULL;--> statement-breakpoint +SET lock_timeout = '5s'; \ No newline at end of file diff --git a/packages/db/migrations/meta/0258_snapshot.json b/packages/db/migrations/meta/0258_snapshot.json new file mode 100644 index 00000000000..70ac5ac0184 --- /dev/null +++ b/packages/db/migrations/meta/0258_snapshot.json @@ -0,0 +1,16734 @@ +{ + "id": "4fa65eec-f5c0-4d5c-8d9c-a3b6577228d0", + "prevId": "c405fc48-c6bb-4786-863f-3d3e686095b5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "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 + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "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_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "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.background_work_status": { + "name": "background_work_status", + "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": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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": "'[]'" + }, + "archived_at": { + "name": "archived_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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_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": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_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": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "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": { + "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_user_workspace_idx": { + "name": "copilot_chats_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": {} + }, + "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": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "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" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_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.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "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": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "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": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_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()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "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": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_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 + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "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.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_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.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "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 + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "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": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_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": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "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 + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "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_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "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" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "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": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "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": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "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()" + }, + "updated_at": { + "name": "updated_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": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "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": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "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": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_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": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "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": "cascade", + "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.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_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_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "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_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "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": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "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.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "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 + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_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": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "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 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_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": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "next_resume_at": { + "name": "next_resume_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": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "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()" + }, + "is_default": { + "name": "is_default", + "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_organization_name_unique": { + "name": "permission_group_organization_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_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = 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 + }, + "organization_id": { + "name": "organization_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_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_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.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": "'[]'" + }, + "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": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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.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 + }, + "impersonated_by": { + "name": "impersonated_by", + "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": "'system'" + }, + "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 + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "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 + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "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 + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "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 + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "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.table_jobs": { + "name": "table_jobs", + "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 + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_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": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "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 + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_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 + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_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": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_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": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "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 + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_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_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "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": "'5'" + }, + "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'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "bigint", + "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 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "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 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "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": { + "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, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "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 + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": 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": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "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 + }, + "archived_at": { + "name": "archived_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": { + "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, + "where": "\"webhook\".\"archived_at\" IS NULL", + "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_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "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_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "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" + } + }, + "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 + }, + "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 + }, + "locked": { + "name": "locked", + "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": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "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": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "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 + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "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_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "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 + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "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 + }, + "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": { + "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": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "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": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_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_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, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "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": false + }, + "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 + }, + "infra_retry_count": { + "name": "infra_retry_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 + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "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": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "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" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "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 + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_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": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_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 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "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_file_folders": { + "name": "workspace_file_folders", + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_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": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_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": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_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": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "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" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "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_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "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_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "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()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "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_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "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 b457b88600c..bc28ddf7be3 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1800,6 +1800,13 @@ "when": 1783531236677, "tag": "0257_majestic_chat", "breakpoints": true + }, + { + "idx": 258, + "version": "7", + "when": 1783620533559, + "tag": "0258_gigantic_lady_mastermind", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 62bf0b5d806..64ee9a71061 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -726,6 +726,11 @@ export const jobExecutionLogs = pgTable( }) ) +/** Extracts the canonical credential ID persisted in webhook provider configuration. */ +export function webhookCredentialIdExpression(column: AnyPgColumn): SQL { + return sql`((${column})::jsonb ->> 'credentialId')` +} + export const webhook = pgTable( 'webhook', { @@ -764,6 +769,11 @@ export const webhook = pgTable( providerActiveWorkflowDeploymentIdx: index( 'idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468' ).on(table.provider, table.isActive, table.workflowId, table.deploymentVersionId), + tiktokCredentialIdIdx: index('webhook_tiktok_credential_id_idx') + .on(webhookCredentialIdExpression(table.providerConfig)) + .where( + sql`${table.provider} = 'tiktok' AND ${table.isActive} = true AND ${table.archivedAt} IS NULL` + ), workflowBlockUpdatedDescIdx: index('idx_webhook_on_workflow_id_block_id_updated_at_desc').on( table.workflowId, table.blockId, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 23c532a1dea..402ddce6221 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 921, - zodRoutes: 921, + totalRoutes: 923, + zodRoutes: 923, nonZodRoutes: 0, } as const diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 3ad1c9e3938..1abf4e2d3af 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -119,6 +119,7 @@ const TRIGGER_PROVIDER_DISPLAY_NAMES: Record = { slack: 'Slack', stripe: 'Stripe', telegram: 'Telegram', + tiktok: 'TikTok', twilio_voice: 'Twilio Voice', typeform: 'Typeform', vercel: 'Vercel',