From c864a928bf30288247124fd38861ca43f1fde8d8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 11:21:21 -0700 Subject: [PATCH 01/26] improvement(models): sort model dropdown by latest release date within each provider (#5099) * improvement(models): sort model dropdown by latest release date within each provider * fix(models): preserve input provider order and build catalog index once --- apps/sim/blocks/utils.ts | 3 +- apps/sim/providers/models.test.ts | 104 ++++++++++++++++++++++++++++++ apps/sim/providers/models.ts | 67 +++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 apps/sim/providers/models.test.ts diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 861c0b12de..8fc80b1009 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -13,6 +13,7 @@ import { getHostedModels, getProviderIcon, getProviderModels, + orderModelIdsByReleaseDate, } from '@/providers/models' import { useProvidersStore } from '@/stores/providers/store' @@ -48,7 +49,7 @@ export const SERVICE_ACCOUNT_SUBBLOCKS: SubBlockConfig[] = [ */ export function getModelOptions() { const providersState = useProvidersStore.getState() - const baseModels = providersState.providers.base.models + const baseModels = orderModelIdsByReleaseDate(providersState.providers.base.models) const ollamaModels = providersState.providers.ollama.models const ollamaCloudModels = providersState.providers['ollama-cloud'].models const vllmModels = providersState.providers.vllm.models diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts new file mode 100644 index 0000000000..ca9af8a07c --- /dev/null +++ b/apps/sim/providers/models.test.ts @@ -0,0 +1,104 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + getBaseModelProviders, + orderModelIdsByReleaseDate, + PROVIDER_DEFINITIONS, +} from '@/providers/models' + +/** Maps a lowercased model ID to its provider's index in the catalog. */ +const PROVIDER_INDEX_BY_MODEL = new Map() +/** Maps a lowercased model ID to its release time (ms), or null when undated. */ +const RELEASE_TIME_BY_MODEL = new Map() +for (const [providerIndex, provider] of Object.values(PROVIDER_DEFINITIONS).entries()) { + for (const model of provider.models) { + const id = model.id.toLowerCase() + PROVIDER_INDEX_BY_MODEL.set(id, providerIndex) + RELEASE_TIME_BY_MODEL.set(id, model.releaseDate ? Date.parse(model.releaseDate) : null) + } +} + +describe('orderModelIdsByReleaseDate', () => { + it('keeps provider grouping order intact', () => { + const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders())) + let lastProviderIndex = -1 + const seenProviders = new Set() + for (const id of ordered) { + const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase()) + expect(providerIndex).toBeDefined() + // A provider's models must form one contiguous run: once we leave a provider + // we never return to it. + if (providerIndex !== lastProviderIndex) { + expect(seenProviders.has(providerIndex as number)).toBe(false) + seenProviders.add(providerIndex as number) + lastProviderIndex = providerIndex as number + } + } + }) + + it('sorts models within a provider newest-first by release date', () => { + const ordered = orderModelIdsByReleaseDate(Object.keys(getBaseModelProviders())) + for (let i = 1; i < ordered.length; i++) { + const prev = ordered[i - 1].toLowerCase() + const curr = ordered[i].toLowerCase() + if (PROVIDER_INDEX_BY_MODEL.get(prev) !== PROVIDER_INDEX_BY_MODEL.get(curr)) continue + + const prevTime = RELEASE_TIME_BY_MODEL.get(prev) + const currTime = RELEASE_TIME_BY_MODEL.get(curr) + // Dated models precede undated ones; among dated models, newer precedes older. + if (prevTime == null) { + expect(currTime).toBeNull() + } else if (currTime != null) { + expect(prevTime).toBeGreaterThanOrEqual(currTime) + } + } + }) + + it('preserves the cross-provider grouping order given in the input', () => { + // Pick the first model of two different providers and feed the second provider + // first; the helper must keep that provider's group ahead of the other. + const byProvider = new Map() + for (const id of Object.keys(getBaseModelProviders())) { + const providerIndex = PROVIDER_INDEX_BY_MODEL.get(id.toLowerCase()) as number + const bucket = byProvider.get(providerIndex) ?? [] + bucket.push(id) + byProvider.set(providerIndex, bucket) + } + const providerIndexes = [...byProvider.keys()] + expect(providerIndexes.length).toBeGreaterThanOrEqual(2) + const [firstProvider, secondProvider] = providerIndexes + const fromFirst = byProvider.get(firstProvider) as string[] + const fromSecond = byProvider.get(secondProvider) as string[] + + // Input order intentionally leads with the second provider. + const input = [fromSecond[0], fromFirst[0]] + const ordered = orderModelIdsByReleaseDate(input) + expect(PROVIDER_INDEX_BY_MODEL.get(ordered[0].toLowerCase())).toBe(secondProvider) + expect(PROVIDER_INDEX_BY_MODEL.get(ordered[1].toLowerCase())).toBe(firstProvider) + }) + + it('places unknown model IDs last, preserving their input order', () => { + const known = Object.keys(getBaseModelProviders())[0] + const ordered = orderModelIdsByReleaseDate(['mystery-a', known, 'mystery-b']) + expect(ordered[0]).toBe(known) + expect(ordered.slice(1)).toEqual(['mystery-a', 'mystery-b']) + }) + + it('is case-insensitive when matching catalog IDs', () => { + const id = Object.keys(getBaseModelProviders())[0] + const ordered = orderModelIdsByReleaseDate([id.toUpperCase()]) + expect(ordered).toEqual([id.toUpperCase()]) + }) + + it('returns an empty array for empty input', () => { + expect(orderModelIdsByReleaseDate([])).toEqual([]) + }) + + it('does not add or drop any IDs', () => { + const input = Object.keys(getBaseModelProviders()) + const ordered = orderModelIdsByReleaseDate(input) + expect([...ordered].sort()).toEqual([...input].sort()) + }) +}) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index a827c32fe8..b8fa2a2bae 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3047,6 +3047,73 @@ export function getProviderModels(providerId: string): string[] { return PROVIDER_DEFINITIONS[providerId]?.models.map((m) => m.id) || [] } +interface ModelCatalogEntry { + providerId: string + declIndex: number + releaseTime: number +} + +/** + * Lowercased model ID → catalog position metadata, built once from the static + * provider catalog. Dynamic providers contribute nothing here because their model + * lists are populated at runtime (not at module load), and only catalog models are + * ever reordered by release date. + */ +const MODEL_CATALOG_INDEX: Map = new Map( + Object.entries(PROVIDER_DEFINITIONS).flatMap(([providerId, provider]) => + provider.models.map((model, declIndex): [string, ModelCatalogEntry] => { + const parsed = model.releaseDate ? Date.parse(model.releaseDate) : Number.NaN + return [ + model.id.toLowerCase(), + { + providerId, + declIndex, + releaseTime: Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed, + }, + ] + }) + ) +) + +/** + * Reorders model IDs so that, within each provider, newer models (by release date) + * come first — while preserving the caller's existing provider grouping order. The + * relative order of providers is taken from the order they first appear in `modelIds`, + * so the cross-provider layout the user already sees is never reshuffled. + * + * Models without a known release date keep their declaration order and sort after + * dated models within the same provider. IDs not found in the catalog (e.g. + * dynamically-discovered provider models) are left in their original order at the end. + */ +export function orderModelIdsByReleaseDate(modelIds: string[]): string[] { + const groups = new Map() + const unknown: string[] = [] + + for (const id of modelIds) { + const meta = MODEL_CATALOG_INDEX.get(id.toLowerCase()) + if (!meta) { + unknown.push(id) + continue + } + const bucket = groups.get(meta.providerId) + if (bucket) bucket.push(id) + else groups.set(meta.providerId, [id]) + } + + const ordered: string[] = [] + for (const bucket of groups.values()) { + bucket.sort((a, b) => { + const ma = MODEL_CATALOG_INDEX.get(a.toLowerCase())! + const mb = MODEL_CATALOG_INDEX.get(b.toLowerCase())! + if (ma.releaseTime !== mb.releaseTime) return mb.releaseTime - ma.releaseTime + return ma.declIndex - mb.declIndex + }) + ordered.push(...bucket) + } + ordered.push(...unknown) + return ordered +} + export const DYNAMIC_MODEL_PROVIDERS = [ 'ollama', 'ollama-cloud', From f238184fa032fb72b136526f059233b683c26608 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 12:25:33 -0700 Subject: [PATCH 02/26] feat(file): add Compress and Decompress operations to the File block (#5100) * feat(file): add Compress operation to bundle files into a .zip archive * feat(file): add Decompress operation to extract .zip archives Adds the inbound half of the archive pair: extracts a .zip back into the workspace with zip-slip path sanitization, symlink skipping, and entry/ size caps to bound zip-bomb expansion. Extracted files are returned in the files output, ready to chain downstream. * fix(file): align archive ops with v5 output surface and zip mime - Drop the single 'file' output reintroduced for compress/decompress; v5 intentionally exposes only 'files' (plus id/name/size/url scalars), so compress/decompress reuse the existing surface with no new block output - Add zip/gz to EXTENSION_TO_MIME (previously only in the reverse map), so archive extensions resolve to a real mime instead of octet-stream - Update File v5 block test for the two new operations * fix(file): harden compress naming per review - Flatten zip entry names to a safe basename so untrusted fileInput names with .. or / cannot produce zip-slip entry paths (cursor) - Treat archiveName as a flat name landing at the workspace root instead of passing it through splitWorkspaceFilePath, which silently created folders for names with separators (greptile) - Add the upfront empty-input guard before any DB calls, matching the read and content operations (greptile) * fix(file): make decompress extraction atomic and bound per-entry size - Read and validate every entry before writing any file, so hitting a size cap no longer leaves partially-extracted files in the workspace (cursor) - Enforce the per-entry cap on the materialized buffer in addition to the declared size, covering entries that omit an uncompressed size (cursor) - Pre-check declared sizes up front to reject standard zip bombs before materializing, and return 422 when no files could be extracted (cursor) * fix(file): exclude skipped entries from caps and reject multi-archive decompress - Resolve safe (sanitized) zip entries up front so unsafe/skipped entries no longer count toward the per-entry and total uncompressed-size caps (cursor) - Reject decompress input that resolves to more than one archive with a clear error instead of silently extracting only the first (cursor) * fix(file): enforce single-archive decompress at the API boundary The block already rejects multiple archives, but the manage route is the real boundary (callable directly and by the LLM tool) and still took the first of multiple resolved inputs. Add the empty-input and >1-archive guards in the route so extra archives are rejected with a clear error rather than silently ignored (cursor). * docs(file): correct compress description and stale file-output references - Drop the misleading 'under provider upload limits' claim from the compress tool description (models cannot read zip archives) - Fix bestPractices to reference the 'files' output, not a non-existent 'file' - Remove the stale 'file' property from the compress test fixture so it matches the real API response (greptile) --- apps/sim/app/api/tools/file/manage/route.ts | 386 ++++++++++++++++++++ apps/sim/blocks/blocks.test.ts | 4 + apps/sim/blocks/blocks/file.ts | 144 +++++++- apps/sim/lib/api/contracts/tools/file.ts | 25 ++ apps/sim/lib/uploads/utils/file-utils.ts | 4 + apps/sim/tools/file/compress.test.ts | 120 ++++++ apps/sim/tools/file/compress.ts | 125 +++++++ apps/sim/tools/file/index.ts | 1 + apps/sim/tools/registry.ts | 4 + 9 files changed, 809 insertions(+), 4 deletions(-) create mode 100644 apps/sim/tools/file/compress.test.ts create mode 100644 apps/sim/tools/file/compress.ts diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts index 22cf2cbe20..1f19ed29f9 100644 --- a/apps/sim/app/api/tools/file/manage/route.ts +++ b/apps/sim/app/api/tools/file/manage/route.ts @@ -2,6 +2,7 @@ import { Buffer, isUtf8 } from 'buffer' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' +import JSZip from 'jszip' import { type NextRequest, NextResponse } from 'next/server' import { fileManageContract } from '@/lib/api/contracts/tools/file' import { parseRequest } from '@/lib/api/server' @@ -133,6 +134,98 @@ const MAX_GET_CONTENT_FILE_BYTES = 64 * 1024 * 1024 /** Combined extracted-text cap so the content array stays within the large-value-ref ceiling. */ const MAX_GET_CONTENT_TOTAL_BYTES = 64 * 1024 * 1024 +/** Per-file download cap for the compress operation. */ +const MAX_COMPRESS_FILE_BYTES = 100 * 1024 * 1024 +/** Combined input cap for the compress operation to bound in-memory archiving. */ +const MAX_COMPRESS_TOTAL_BYTES = 100 * 1024 * 1024 + +/** Ensure an archive name ends with a single `.zip` extension. */ +const ensureZipExtension = (name: string): string => + name.toLowerCase().endsWith('.zip') ? name : `${name}.zip` + +/** Strip the trailing extension from a file name (e.g., "report.pdf" -> "report"). */ +const stripExtension = (name: string): string => { + const dot = name.lastIndexOf('.') + return dot > 0 ? name.slice(0, dot) : name +} + +/** + * Reduce an arbitrary name to a safe, flat file name: takes the final path + * segment, drops directory and traversal components, and falls back when the + * result would be empty or a dot segment. Used for zip entry names and the + * compress archive name so untrusted input cannot introduce nested or + * zip-slip-style paths. + */ +const toFlatFileName = (name: string, fallback: string): string => { + const leaf = name.replace(/\\/g, '/').split('/').pop()?.trim() + if (!leaf || leaf === '.' || leaf === '..') return fallback + return leaf +} + +/** + * Return a zip entry name unique within `usedNames`, appending a numeric suffix + * before the extension on collision (e.g., "data.csv" -> "data (1).csv"). + */ +const uniqueZipEntryName = (name: string, usedNames: Set): string => { + if (!usedNames.has(name)) { + usedNames.add(name) + return name + } + + const dot = name.lastIndexOf('.') + const base = dot > 0 ? name.slice(0, dot) : name + const ext = dot > 0 ? name.slice(dot) : '' + let counter = 1 + let candidate = `${base} (${counter})${ext}` + while (usedNames.has(candidate)) { + counter += 1 + candidate = `${base} (${counter})${ext}` + } + usedNames.add(candidate) + return candidate +} + +/** Input archive download cap for the decompress operation. */ +const MAX_DECOMPRESS_ARCHIVE_BYTES = 100 * 1024 * 1024 +/** Maximum number of entries extracted from a single archive. */ +const MAX_DECOMPRESS_ENTRIES = 1000 +/** Maximum uncompressed size for any single archive entry. */ +const MAX_DECOMPRESS_ENTRY_BYTES = 100 * 1024 * 1024 +/** Maximum total uncompressed size across all entries, to bound zip-bomb expansion. */ +const MAX_DECOMPRESS_TOTAL_BYTES = 200 * 1024 * 1024 + +const S_IFMT = 0o170000 +const S_IFLNK = 0o120000 + +/** Read a zip entry's declared uncompressed size without materializing it (zip-bomb pre-check). */ +const readEntryUncompressedSize = (entry: JSZip.JSZipObject): number | undefined => { + const data = (entry as JSZip.JSZipObject & { _data?: { uncompressedSize?: number } })._data + const size = data?.uncompressedSize + return typeof size === 'number' && Number.isFinite(size) ? size : undefined +} + +/** True when a zip entry's unix mode marks it as a symlink (never extracted). */ +const isSymlinkEntry = (entry: JSZip.JSZipObject): boolean => { + const mode = (entry as JSZip.JSZipObject & { unixPermissions?: number | null }).unixPermissions + return typeof mode === 'number' && (mode & S_IFMT) === S_IFLNK +} + +/** + * Normalize a zip entry path into safe workspace folder segments, guarding against + * zip-slip. Returns null for traversal (`..`), so the entry is skipped rather than + * written outside its intended location. + */ +const sanitizeArchiveEntryPath = (rawPath: string): string[] | null => { + const segments = rawPath + .replace(/\\/g, '/') + .split('/') + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0 && segment !== '.') + + if (segments.length === 0 || segments.includes('..')) return null + return segments +} + const isLikelyTextBuffer = (buffer: Buffer): boolean => isUtf8(buffer) && !buffer.includes(0) /** @@ -462,6 +555,299 @@ export const POST = withRouteHandler(async (request: NextRequest) => { await releaseLock(lockKey, lockValue) } } + + case 'compress': { + const { fileId, fileInput, archiveName } = body + const requestId = generateRequestId() + + const selectedFileIds = Array.isArray(fileId) + ? fileId.map((id) => id.trim()).filter(Boolean) + : fileId + ? normalizeFileIdList(fileId) + : extractFileIdsFromInput(fileInput) + const selectedInputFiles = fileId ? [] : extractUserFilesFromInput(fileInput) + + if (selectedFileIds.length === 0 && selectedInputFiles.length === 0) { + return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 }) + } + + const workspaceFiles = await Promise.all( + selectedFileIds.map((id) => getWorkspaceFile(workspaceId, id)) + ) + const missingFileId = selectedFileIds.find((_, index) => !workspaceFiles[index]) + if (missingFileId) { + return NextResponse.json( + { success: false, error: `File not found: "${missingFileId}"` }, + { status: 404 } + ) + } + + const userFiles: UserFile[] = workspaceFiles + .map((file) => workspaceFileToUserFile(file)) + .filter((file): file is NonNullable> => + Boolean(file) + ) + .concat(selectedInputFiles) + + const zip = new JSZip() + const usedNames = new Set() + let totalBytes = 0 + for (const userFile of userFiles) { + const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger) + if (denied) return denied + + const buffer = await downloadFileFromStorage(userFile, requestId, logger, { + maxBytes: MAX_COMPRESS_FILE_BYTES, + }) + totalBytes += buffer.length + if (totalBytes > MAX_COMPRESS_TOTAL_BYTES) { + return NextResponse.json( + { + success: false, + error: `Combined input is too large to compress. Maximum is ${ + MAX_COMPRESS_TOTAL_BYTES / (1024 * 1024) + } MB.`, + }, + { status: 413 } + ) + } + zip.file(uniqueZipEntryName(toFlatFileName(userFile.name, 'file'), usedNames), buffer) + } + + const zipBuffer = await zip.generateAsync({ + type: 'nodebuffer', + compression: 'DEFLATE', + compressionOptions: { level: 6 }, + }) + + const requestedName = typeof archiveName === 'string' ? archiveName.trim() : '' + const baseName = requestedName + ? toFlatFileName(requestedName, 'archive') + : userFiles.length === 1 + ? stripExtension(toFlatFileName(userFiles[0].name, 'archive')) + : 'archive' + const leafName = ensureZipExtension(baseName) + const folderId = await ensureWorkspaceFileFolderPath({ + workspaceId, + userId, + pathSegments: [], + }) + const result = await uploadWorkspaceFile( + workspaceId, + userId, + zipBuffer, + leafName, + 'application/zip', + { folderId } + ) + + const compressedFile: UserFile = { + ...result, + url: ensureAbsoluteUrl(result.url), + size: zipBuffer.length, + } + + logger.info('Files compressed', { + fileId: result.id, + name: result.name, + fileCount: userFiles.length, + size: zipBuffer.length, + }) + + return NextResponse.json({ + success: true, + data: { + id: compressedFile.id, + name: compressedFile.name, + size: compressedFile.size, + url: compressedFile.url, + files: [compressedFile], + }, + }) + } + + case 'decompress': { + const { fileId, fileInput } = body + const requestId = generateRequestId() + + const selectedFileIds = fileId ? [fileId] : extractFileIdsFromInput(fileInput) + const selectedInputFiles = fileId ? [] : extractUserFilesFromInput(fileInput) + + if (selectedFileIds.length === 0 && selectedInputFiles.length === 0) { + return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 }) + } + if (selectedFileIds.length + selectedInputFiles.length > 1) { + return NextResponse.json( + { success: false, error: 'Decompress accepts a single .zip archive at a time' }, + { status: 400 } + ) + } + + const workspaceFiles = await Promise.all( + selectedFileIds.map((id) => getWorkspaceFile(workspaceId, id)) + ) + const missingFileId = selectedFileIds.find((_, index) => !workspaceFiles[index]) + if (missingFileId) { + return NextResponse.json( + { success: false, error: `File not found: "${missingFileId}"` }, + { status: 404 } + ) + } + + const archive = workspaceFiles + .map((file) => workspaceFileToUserFile(file)) + .filter((file): file is NonNullable> => + Boolean(file) + ) + .concat(selectedInputFiles)[0] + + if (!archive) { + return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 }) + } + + const denied = await assertToolFileAccess(archive.key, userId, requestId, logger) + if (denied) return denied + + const archiveBuffer = await downloadFileFromStorage(archive, requestId, logger, { + maxBytes: MAX_DECOMPRESS_ARCHIVE_BYTES, + }) + + let zip: JSZip + try { + zip = await JSZip.loadAsync(archiveBuffer) + } catch { + return NextResponse.json( + { success: false, error: `"${archive.name}" is not a valid .zip archive` }, + { status: 400 } + ) + } + + const entries = Object.values(zip.files).filter( + (entry) => !entry.dir && !isSymlinkEntry(entry) + ) + if (entries.length > MAX_DECOMPRESS_ENTRIES) { + return NextResponse.json( + { + success: false, + error: `Archive has too many entries to extract. Maximum is ${MAX_DECOMPRESS_ENTRIES}.`, + }, + { status: 413 } + ) + } + + const entryTooLargeResponse = (name: string) => + NextResponse.json( + { + success: false, + error: `Archive entry "${name}" is too large to extract. Maximum is ${ + MAX_DECOMPRESS_ENTRY_BYTES / (1024 * 1024) + } MB per file.`, + }, + { status: 413 } + ) + const totalTooLargeResponse = () => + NextResponse.json( + { + success: false, + error: `Archive expands to more than the ${ + MAX_DECOMPRESS_TOTAL_BYTES / (1024 * 1024) + } MB extraction limit.`, + }, + { status: 413 } + ) + + // Resolve which entries are safe to extract first, so unsafe entries + // (skipped below) never count toward the size caps. + const safeEntries: Array<{ entry: JSZip.JSZipObject; segments: string[] }> = [] + let skippedCount = 0 + for (const entry of entries) { + const segments = sanitizeArchiveEntryPath(entry.name) + if (!segments) { + skippedCount += 1 + logger.warn('Skipping unsafe archive entry', { name: entry.name }) + continue + } + safeEntries.push({ entry, segments }) + } + + // Reject standard zip bombs up front using the declared uncompressed sizes, + // before materializing any entry into memory. + let declaredTotal = 0 + for (const { entry } of safeEntries) { + const declaredSize = readEntryUncompressedSize(entry) + if (declaredSize === undefined) continue + if (declaredSize > MAX_DECOMPRESS_ENTRY_BYTES) return entryTooLargeResponse(entry.name) + declaredTotal += declaredSize + if (declaredTotal > MAX_DECOMPRESS_TOTAL_BYTES) return totalTooLargeResponse() + } + + // Read and validate every safe entry before writing anything, so a cap + // breach never leaves partially-extracted files behind in the workspace. + const pending: Array<{ segments: string[]; buffer: Buffer }> = [] + let totalBytes = 0 + for (const { entry, segments } of safeEntries) { + const buffer = await entry.async('nodebuffer') + // Enforce the per-entry cap on the materialized size too, covering + // entries that omit a declared uncompressed size. + if (buffer.length > MAX_DECOMPRESS_ENTRY_BYTES) return entryTooLargeResponse(entry.name) + totalBytes += buffer.length + if (totalBytes > MAX_DECOMPRESS_TOTAL_BYTES) return totalTooLargeResponse() + + pending.push({ segments, buffer }) + } + + if (pending.length === 0) { + return NextResponse.json( + { + success: false, + error: `No files could be extracted from "${archive.name}".`, + }, + { status: 422 } + ) + } + + const folderIdCache = new Map() + const extractedFiles: UserFile[] = [] + for (const { segments, buffer } of pending) { + const leafName = segments[segments.length - 1] + const folderSegments = segments.slice(0, -1) + const folderKey = folderSegments.join('/') + let folderId = folderIdCache.get(folderKey) + if (folderId === undefined) { + folderId = await ensureWorkspaceFileFolderPath({ + workspaceId, + userId, + pathSegments: folderSegments, + }) + folderIdCache.set(folderKey, folderId) + } + + const mimeType = getMimeTypeFromExtension(getFileExtension(leafName)) + const uploaded = await uploadWorkspaceFile( + workspaceId, + userId, + buffer, + leafName, + mimeType, + { folderId } + ) + extractedFiles.push({ ...uploaded, url: ensureAbsoluteUrl(uploaded.url) }) + } + + logger.info('Archive decompressed', { + fileId: archive.id, + name: archive.name, + extractedCount: extractedFiles.length, + skippedCount, + }) + + return NextResponse.json({ + success: true, + data: { + files: extractedFiles, + }, + }) + } } } catch (error) { if (isWorkspaceAccessDeniedError(error)) { diff --git a/apps/sim/blocks/blocks.test.ts b/apps/sim/blocks/blocks.test.ts index 79dce03022..9799ef72eb 100644 --- a/apps/sim/blocks/blocks.test.ts +++ b/apps/sim/blocks/blocks.test.ts @@ -172,7 +172,11 @@ describe.concurrent('Blocks Module', () => { 'file_fetch', 'file_write', 'file_append', + 'file_compress', + 'file_decompress', ]) + expect(block?.tools.config?.tool({ operation: 'file_compress' })).toBe('file_compress') + expect(block?.tools.config?.tool({ operation: 'file_decompress' })).toBe('file_decompress') expect(block?.subBlocks.find((subBlock) => subBlock.id === 'readFile')?.multiple).toBe(true) expect(block?.tools.config?.tool({ operation: 'file_read' })).toBe('file_read') expect(block?.tools.config?.tool({ operation: 'file_get_content' })).toBe('file_get_content') diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index b315411c61..541192bfe2 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -822,9 +822,9 @@ export const FileV5Block: BlockConfig = { ...FileV4Block, type: 'file_v5', name: 'File', - description: 'Read, get content, fetch, write, and append files', + description: 'Read, get content, fetch, write, append, compress, and decompress files', longDescription: - 'Read workspace file objects, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files.', + 'Read workspace file objects, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, append content to existing files, compress files into a .zip archive, or extract a .zip archive into the workspace.', hideFromToolbar: false, bestPractices: ` - Read returns workspace file objects in the "files" output and does NOT include their text. Use it to pick files or pass file references downstream (e.g. as attachments). @@ -833,6 +833,8 @@ export const FileV5Block: BlockConfig = { - Get Content's "contents" can be large; it is persisted through the execution large-value system automatically, so prefer it over inlining file text any other way. - Use Fetch for external file URLs. Add headers for authenticated downloads, for example Slack private file URLs require an Authorization Bearer token. - Use Write to create a new workspace file and Append to add content to an existing one. + - Use Compress to bundle one or more files into a single .zip archive stored in the workspace. The new archive is returned in the "files" output. + - Use Decompress to extract a .zip archive back into the workspace; the extracted files are returned in the "files" output, ready to chain into Get Content or downstream blocks. `, subBlocks: [ { @@ -845,6 +847,8 @@ export const FileV5Block: BlockConfig = { { label: 'Fetch', id: 'file_fetch' }, { label: 'Write', id: 'file_write' }, { label: 'Append', id: 'file_append' }, + { label: 'Compress', id: 'file_compress' }, + { label: 'Decompress', id: 'file_decompress' }, ], value: () => 'file_read', }, @@ -962,9 +966,67 @@ export const FileV5Block: BlockConfig = { condition: { field: 'operation', value: 'file_append' }, required: { field: 'operation', value: 'file_append' }, }, + { + id: 'compressFile', + title: 'Files', + type: 'file-upload' as SubBlockType, + canonicalParamId: 'compressInput', + acceptedTypes: '*', + placeholder: 'Select workspace files', + multiple: true, + mode: 'basic', + condition: { field: 'operation', value: 'file_compress' }, + required: { field: 'operation', value: 'file_compress' }, + }, + { + id: 'compressFileId', + title: 'File ID', + type: 'short-input' as SubBlockType, + canonicalParamId: 'compressInput', + placeholder: 'Workspace file ID or JSON array of IDs', + mode: 'advanced', + condition: { field: 'operation', value: 'file_compress' }, + required: { field: 'operation', value: 'file_compress' }, + }, + { + id: 'archiveName', + title: 'Archive Name', + type: 'short-input' as SubBlockType, + placeholder: 'archive.zip (auto-named from source if omitted)', + condition: { field: 'operation', value: 'file_compress' }, + }, + { + id: 'decompressFile', + title: 'Archive', + type: 'file-upload' as SubBlockType, + canonicalParamId: 'decompressInput', + acceptedTypes: '.zip', + placeholder: 'Select a .zip archive', + mode: 'basic', + condition: { field: 'operation', value: 'file_decompress' }, + required: { field: 'operation', value: 'file_decompress' }, + }, + { + id: 'decompressFileId', + title: 'File ID', + type: 'short-input' as SubBlockType, + canonicalParamId: 'decompressInput', + placeholder: 'Workspace file ID of the .zip archive', + mode: 'advanced', + condition: { field: 'operation', value: 'file_decompress' }, + required: { field: 'operation', value: 'file_decompress' }, + }, ], tools: { - access: ['file_read', 'file_get_content', 'file_fetch', 'file_write', 'file_append'], + access: [ + 'file_read', + 'file_get_content', + 'file_fetch', + 'file_write', + 'file_append', + 'file_compress', + 'file_decompress', + ], config: { tool: (params) => params.operation || 'file_read', params: (params) => { @@ -1005,6 +1067,70 @@ export const FileV5Block: BlockConfig = { } } + if (operation === 'file_compress') { + const compressInput = params.compressInput + if (!compressInput) { + throw new Error('File is required for compress') + } + + const archiveName = + typeof params.archiveName === 'string' && params.archiveName.trim() + ? params.archiveName.trim() + : undefined + + const fileIds = parseReadFileIds(compressInput) + if (fileIds) { + return { + fileId: fileIds, + archiveName, + workspaceId: params._context?.workspaceId, + } + } + + const normalized = normalizeFileInput(compressInput) + if (!normalized || normalized.length === 0) { + throw new Error('File is required for compress') + } + + return { + fileInput: normalized, + archiveName, + workspaceId: params._context?.workspaceId, + } + } + + if (operation === 'file_decompress') { + const decompressInput = params.decompressInput + if (!decompressInput) { + throw new Error('File is required for decompress') + } + + const fileIds = parseReadFileIds(decompressInput) + if (fileIds) { + const ids = Array.isArray(fileIds) ? fileIds : [fileIds] + if (ids.length > 1) { + throw new Error('Decompress accepts a single .zip archive at a time') + } + return { + fileId: ids[0], + workspaceId: params._context?.workspaceId, + } + } + + const normalized = normalizeFileInput(decompressInput) + if (!normalized || normalized.length === 0) { + throw new Error('File is required for decompress') + } + if (normalized.length > 1) { + throw new Error('Decompress accepts a single .zip archive at a time') + } + + return { + fileInput: normalized[0], + workspaceId: params._context?.workspaceId, + } + } + if (operation === 'file_fetch') { const fileUrl = resolveHttpFileUrl(params.fileUrl) @@ -1089,11 +1215,21 @@ export const FileV5Block: BlockConfig = { contentType: { type: 'string', description: 'MIME content type for write' }, appendFileInput: { type: 'json', description: 'File to append to' }, appendContent: { type: 'string', description: 'Content to append to file' }, + compressInput: { + type: 'json', + description: 'Selected workspace files or canonical file IDs to compress', + }, + archiveName: { type: 'string', description: 'Name for the compressed .zip archive' }, + decompressInput: { + type: 'json', + description: 'Selected .zip archive or canonical file ID to extract', + }, }, outputs: { files: { type: 'file[]', - description: 'Workspace file objects (read) or fetched file objects (fetch)', + description: + 'Workspace file objects (read), fetched file objects (fetch), the compressed archive (compress), or extracted files (decompress)', }, contents: { type: 'array', diff --git a/apps/sim/lib/api/contracts/tools/file.ts b/apps/sim/lib/api/contracts/tools/file.ts index 1a151342e3..0b7a439615 100644 --- a/apps/sim/lib/api/contracts/tools/file.ts +++ b/apps/sim/lib/api/contracts/tools/file.ts @@ -64,6 +64,29 @@ export const fileManageContentBodySchema = z message: 'Either fileId or fileInput is required for content operation', }) +export const fileManageCompressBodySchema = z + .object({ + operation: z.literal('compress'), + workspaceId: z.string().min(1).optional(), + fileId: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).optional(), + fileInput: z.unknown().optional(), + archiveName: z.string().min(1).max(255).optional(), + }) + .refine((data) => data.fileId !== undefined || data.fileInput !== undefined, { + message: 'Either fileId or fileInput is required for compress operation', + }) + +export const fileManageDecompressBodySchema = z + .object({ + operation: z.literal('decompress'), + workspaceId: z.string().min(1).optional(), + fileId: z.string().min(1).optional(), + fileInput: z.unknown().optional(), + }) + .refine((data) => data.fileId !== undefined || data.fileInput !== undefined, { + message: 'Either fileId or fileInput is required for decompress operation', + }) + export const fileManageBodySchema = z.union([ fileManageWriteBodySchema, fileManageAppendBodySchema, @@ -71,6 +94,8 @@ export const fileManageBodySchema = z.union([ fileManageMoveBodySchema, fileManageReadBodySchema, fileManageContentBodySchema, + fileManageCompressBodySchema, + fileManageDecompressBodySchema, ]) export const fileManageContract = defineRouteContract({ diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index b275845928..0fd254f2e2 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -241,6 +241,10 @@ const EXTENSION_TO_MIME: Record = { yml: 'application/x-yaml', rtf: 'application/rtf', + // Archives + zip: 'application/zip', + gz: 'application/gzip', + // Code / plain-text source py: 'text/x-python', js: 'text/javascript', diff --git a/apps/sim/tools/file/compress.test.ts b/apps/sim/tools/file/compress.test.ts new file mode 100644 index 0000000000..318b85bccf --- /dev/null +++ b/apps/sim/tools/file/compress.test.ts @@ -0,0 +1,120 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { fileCompressTool, fileDecompressTool } from '@/tools/file/compress' + +describe('fileCompressTool', () => { + it('builds a compress request body from file IDs and archive name', () => { + const body = fileCompressTool.request.body?.({ + fileId: ['wf_a', 'wf_b'], + archiveName: 'documents.zip', + _context: { workspaceId: 'ws_1' }, + } as Parameters>[0]) + + expect(body).toMatchObject({ + operation: 'compress', + fileId: ['wf_a', 'wf_b'], + archiveName: 'documents.zip', + workspaceId: 'ws_1', + }) + }) + + it('forwards a selected file object when no IDs are provided', () => { + const fileInput = { id: 'wf_c', name: 'report.pdf' } + const body = fileCompressTool.request.body?.({ + fileInput, + workspaceId: 'ws_2', + } as Parameters>[0]) + + expect(body).toMatchObject({ + operation: 'compress', + fileInput, + workspaceId: 'ws_2', + }) + }) + + it('returns the compressed archive on success', async () => { + const archive = { + id: 'wf_zip', + name: 'archive.zip', + size: 1024, + url: 'https://example.com/archive.zip', + type: 'application/zip', + key: 'workspace/ws_1/archive.zip', + } + + const result = await fileCompressTool.transformResponse?.( + Response.json({ + success: true, + data: { + id: archive.id, + name: archive.name, + size: archive.size, + url: archive.url, + files: [archive], + }, + }) + ) + + expect(result).toMatchObject({ + success: true, + output: { id: 'wf_zip', name: 'archive.zip', size: 1024, files: [archive] }, + }) + }) + + it('propagates route failures as tool failures', async () => { + const result = await fileCompressTool.transformResponse?.( + Response.json({ success: false, error: 'Combined input is too large to compress.' }) + ) + + expect(result).toMatchObject({ + success: false, + error: 'Combined input is too large to compress.', + output: {}, + }) + }) +}) + +describe('fileDecompressTool', () => { + it('builds a decompress request body from a file ID', () => { + const body = fileDecompressTool.request.body?.({ + fileId: 'wf_zip', + _context: { workspaceId: 'ws_1' }, + } as Parameters>[0]) + + expect(body).toMatchObject({ + operation: 'decompress', + fileId: 'wf_zip', + workspaceId: 'ws_1', + }) + }) + + it('returns the extracted files on success', async () => { + const extracted = [ + { id: 'wf_a', name: 'a.txt', url: 'https://example.com/a.txt', key: 'k/a.txt' }, + { id: 'wf_b', name: 'b.txt', url: 'https://example.com/b.txt', key: 'k/b.txt' }, + ] + + const result = await fileDecompressTool.transformResponse?.( + Response.json({ success: true, data: { files: extracted } }) + ) + + expect(result).toMatchObject({ + success: true, + output: { files: extracted }, + }) + }) + + it('propagates route failures as tool failures', async () => { + const result = await fileDecompressTool.transformResponse?.( + Response.json({ success: false, error: '"data.txt" is not a valid .zip archive' }) + ) + + expect(result).toMatchObject({ + success: false, + error: '"data.txt" is not a valid .zip archive', + output: {}, + }) + }) +}) diff --git a/apps/sim/tools/file/compress.ts b/apps/sim/tools/file/compress.ts new file mode 100644 index 0000000000..3dc28ef58d --- /dev/null +++ b/apps/sim/tools/file/compress.ts @@ -0,0 +1,125 @@ +import type { ToolConfig, ToolResponse, WorkflowToolExecutionContext } from '@/tools/types' + +interface FileCompressParams { + fileId?: string | string[] + fileInput?: unknown + archiveName?: string + workspaceId?: string + _context?: WorkflowToolExecutionContext +} + +export const fileCompressTool: ToolConfig = { + id: 'file_compress', + name: 'File Compress', + description: + 'Compress one or more workspace files into a single .zip archive stored in the workspace, for bundling files to download, transfer, or store.', + version: '1.0.0', + + params: { + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Canonical workspace file ID, or an array of canonical workspace file IDs.', + }, + fileInput: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Selected workspace file object, or an array of file objects.', + }, + archiveName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Name for the .zip archive (e.g., "documents.zip"). Defaults to the source file name when compressing a single file, otherwise "archive.zip".', + }, + }, + + request: { + url: '/api/tools/file/manage', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + operation: 'compress', + fileId: params.fileId, + fileInput: params.fileInput, + archiveName: params.archiveName, + workspaceId: params.workspaceId || params._context?.workspaceId, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok || !data.success) { + return { success: false, output: {}, error: data.error || 'Failed to compress files' } + } + return { success: true, output: data.data } + }, + + outputs: { + id: { type: 'string', description: 'Compressed archive file ID' }, + name: { type: 'string', description: 'Compressed archive file name' }, + size: { type: 'number', description: 'Compressed archive size in bytes' }, + url: { type: 'string', description: 'URL to access the compressed archive', optional: true }, + files: { + type: 'file[]', + description: 'Compressed archive file object, as a single-item array', + }, + }, +} + +interface FileDecompressParams { + fileId?: string + fileInput?: unknown + workspaceId?: string + _context?: WorkflowToolExecutionContext +} + +export const fileDecompressTool: ToolConfig = { + id: 'file_decompress', + name: 'File Decompress', + description: + 'Extract the contents of a .zip archive into the workspace, preserving the archive folder structure.', + version: '1.0.0', + + params: { + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Canonical workspace file ID of the .zip archive to extract.', + }, + fileInput: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Selected .zip archive file object.', + }, + }, + + request: { + url: '/api/tools/file/manage', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + operation: 'decompress', + fileId: params.fileId, + fileInput: params.fileInput, + workspaceId: params.workspaceId || params._context?.workspaceId, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok || !data.success) { + return { success: false, output: {}, error: data.error || 'Failed to decompress archive' } + } + return { success: true, output: data.data } + }, + + outputs: { + files: { type: 'file[]', description: 'Extracted workspace file objects' }, + }, +} diff --git a/apps/sim/tools/file/index.ts b/apps/sim/tools/file/index.ts index c05a3a1995..853b0c8669 100644 --- a/apps/sim/tools/file/index.ts +++ b/apps/sim/tools/file/index.ts @@ -6,6 +6,7 @@ import { } from '@/tools/file/parser' export { fileAppendTool } from '@/tools/file/append' +export { fileCompressTool, fileDecompressTool } from '@/tools/file/compress' export { fileGetContentTool, fileGetTool, fileReadTool } from '@/tools/file/get' export { fileWriteTool } from '@/tools/file/write' diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index e7c8f70198..bb80d09d21 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -850,6 +850,8 @@ import { } from '@/tools/fathom' import { fileAppendTool, + fileCompressTool, + fileDecompressTool, fileFetchTool, fileGetContentTool, fileGetTool, @@ -3958,6 +3960,8 @@ export const tools: Record = { file_parser_v2: fileParserV2Tool, file_parser_v3: fileParserV3Tool, file_append: fileAppendTool, + file_compress: fileCompressTool, + file_decompress: fileDecompressTool, file_fetch: fileFetchTool, file_get: fileGetTool, file_get_content: fileGetContentTool, From cc56408be3224402775c40ee7e30bc35178fa833 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 13:49:03 -0700 Subject: [PATCH 03/26] perf(execution): parallelize preflight gates, cache deployed state, memoize Anthropic client (#5098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(execution): parallelize preflight gates, cache deployed state, memoize Anthropic client - Memoize Anthropic + Azure-Anthropic SDK clients (new client-cache.ts) keyed by apiKey (+beta header; +baseURL/version/pinnedIP for Azure) so HTTP keep-alive connections are reused instead of a fresh TLS handshake per call. apiKey is the tenant boundary. - Parallelize the read-only preflight gates in preprocessing.ts (ban + subscription, then usage + org-member + rate-limit) while preserving exact error precedence (ban 403 -> usage 402 -> rate 429) and keeping the sole write (admission reservation) last. - Parallelize the independent workflow-state and env-var loads in execution-core. - Cache deployed workflow state by immutable deploymentVersionId with deep-clone-on-read, oldest-first eviction, and a 5-min TTL bounding the credential-mapping edge across ECS tasks. - Parallelize the independent personal-subscription + membership queries in getHighestPrioritySubscription. - BYOK: drop the redundant getWorkspaceById existence check (auth already validates the workspace); read the key list fresh every call for zero cross-instance staleness. Billing/usage/ban/permission reads stay fresh on the primary (no cache, no replica). Adds tests for every new mechanism and fixes a pre-existing vitest class-mock incompatibility that had execution-core.test.ts fully red on staging. * fix(execution): run rate-limit gate only after ban/usage pass The rate-limit gate is not read-only — checkRateLimitWithSubscription consumes a token — so running it in parallel with the read-only gates debited rate-limit quota for requests that the ban (403) or usage (402) gates reject, which the original sequential flow never did. Move the rate-limit gate to run sequentially after the ban and usage gates pass, preserving the read-only gates' parallelism (ban + subscription + usage) and the exact ban -> usage -> rate precedence. Add regression tests asserting the rate limiter is not consumed when an earlier gate rejects, and is consumed once when they pass. Caught by Cursor Bugbot review. * chore(execution): trim redundant preflight comments Tighten the gate overview to match the sequential rate-limit gate and drop inline notes that duplicated it or the runRateLimitGate doc. * refactor(cache): address review — idle TTL for client cache, LRUCache for deployed state - client-cache: add updateAgeOnGet so the TTL is genuinely idle-based (active clients keep their warm keep-alive connections; the JSDoc now matches behavior). - deployed-state: replace the hand-rolled Map + manual FIFO eviction/TTL with LRUCache (real LRU eviction, built-in TTL), matching the effectiveDecryptedEnv and integration-tool-schema caches. TTL stays absolute (not reset on read) so the credential-migration remap still propagates across ECS tasks. Both per review feedback from Greptile. * test(execution): isolate rate-limit gate test from STEP 7 reservation The 'consumes the rate-limit gate once' test reached the STEP 7 admission reservation, which depends on Redis — it passed locally (reserve throws and is swallowed) but failed in CI (reserve returns not-reserved -> 429). Pass skipConcurrencyReservation so the test isolates the rate gate deterministically. * perf(providers): memoize SDK clients where the pool is per-client (bedrock, vllm) Generalize the Anthropic client cache into one shared memoizer (providers/client-cache.ts) and apply it only where each new client owns its own connection pool — so reuse actually keeps connections warm: - bedrock: AWS SDK clients hold a per-client connection pool (reuse is the AWS best practice). Keyed by region + credential identity. - vllm: a pinned endpoint creates its own undici Agent per call; key by the resolved IP so DNS re-validation still runs each request. - anthropic + azure-anthropic: migrated onto the shared memoizer. Deliberately NOT applied to the OpenAI-compatible providers, groq, cerebras, or google: their SDKs share a process-global keep-alive pool (Node openai-sdk module singleton agent; anthropic/global undici), so a fresh client per request already reuses connections and memoization would add complexity with ~no benefit. litellm uses a plain shared-agent client (no pinning) and is likewise skipped. Bounded LRU (max 1000, 30m idle TTL) with no close-on-eviction, avoiding the unbounded-growth and eviction-closes-in-use-client failure modes seen in similar client caches. * chore(perf): trim verbose comments to terse why-notes * chore(perf): drop obvious inline comments, keep nuance as TSDoc * fix(bedrock): key client cache on full credential, not just access key id A corrected secret under the same access key id would otherwise keep serving the stale cached client until TTL/eviction. Caught by Cursor Bugbot. * test(execution,providers): fix preflight mock reset + isolate provider client cache in tests - preprocessing.test: re-establish the checkOrgMemberUsageLimit mock in beforeEach (the only gate mock not re-set). In the full suite its implementation was reset so the success-path test got undefined -> threw -> 500 -> success:false. Mirrors how checkServerSideUsageLimits is handled. - client-cache: add clearProviderClientCacheForTests; call it in the bedrock and vllm test beforeEach so construction assertions always start from a cache miss now that those providers memoize their client. * test(execution): make RateLimiter mock constructable under vitest 4.x The RateLimiter mock used an arrow factory (vi.fn(() => ({...}))). vitest 4.x (CI) rejects `new` on an arrow-implemented mock ("not a constructor"); 3.2.4 allowed it. The new rate-gate test is the first to actually `new RateLimiter()`, so it surfaced the failure only in CI. Switch the mock to a regular function and drop the speculative beforeEach re-establishments that didn't address it. --- apps/sim/lib/api-key/byok.test.ts | 26 +- apps/sim/lib/api-key/byok.ts | 9 +- .../lib/billing/calculations/usage-monitor.ts | 3 + apps/sim/lib/billing/core/plan.test.ts | 193 +++++++++ apps/sim/lib/billing/core/plan.ts | 29 +- apps/sim/lib/execution/preprocessing.test.ts | 84 +++- apps/sim/lib/execution/preprocessing.ts | 407 +++++++++++------- .../workflows/executor/execution-core.test.ts | 116 ++++- .../lib/workflows/executor/execution-core.ts | 106 +++-- .../lib/workflows/persistence/utils.test.ts | 170 ++++++++ apps/sim/lib/workflows/persistence/utils.ts | 35 +- apps/sim/providers/anthropic/index.ts | 21 +- apps/sim/providers/azure-anthropic/index.ts | 42 +- apps/sim/providers/bedrock/index.test.ts | 2 + apps/sim/providers/bedrock/index.ts | 12 +- apps/sim/providers/client-cache.test.ts | 107 +++++ apps/sim/providers/client-cache.ts | 36 ++ apps/sim/providers/vllm/index.test.ts | 2 + apps/sim/providers/vllm/index.ts | 19 +- 19 files changed, 1128 insertions(+), 291 deletions(-) create mode 100644 apps/sim/lib/billing/core/plan.test.ts create mode 100644 apps/sim/providers/client-cache.test.ts create mode 100644 apps/sim/providers/client-cache.ts diff --git a/apps/sim/lib/api-key/byok.test.ts b/apps/sim/lib/api-key/byok.test.ts index 439c392d94..6b288b6213 100644 --- a/apps/sim/lib/api-key/byok.test.ts +++ b/apps/sim/lib/api-key/byok.test.ts @@ -3,9 +3,8 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockOrderBy, mockGetWorkspaceById, mockDecryptSecret } = vi.hoisted(() => ({ +const { mockOrderBy, mockDecryptSecret } = vi.hoisted(() => ({ mockOrderBy: vi.fn(), - mockGetWorkspaceById: vi.fn(), mockDecryptSecret: vi.fn(), })) @@ -19,10 +18,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@/lib/workspaces/permissions/utils', () => ({ - getWorkspaceById: mockGetWorkspaceById, -})) - vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: mockDecryptSecret, })) @@ -70,7 +65,6 @@ const storedKey = (id: string) => ({ id, encryptedApiKey: `encrypted-${id}` }) describe('getBYOKKey', () => { beforeEach(() => { vi.clearAllMocks() - mockGetWorkspaceById.mockResolvedValue({ id: 'workspace' }) mockOrderBy.mockResolvedValue([]) mockDecryptSecret.mockImplementation(async (encrypted: string) => ({ decrypted: encrypted.replace('encrypted-', 'decrypted-'), @@ -80,13 +74,6 @@ describe('getBYOKKey', () => { it('returns null when no workspaceId is provided', async () => { expect(await getBYOKKey(undefined, 'openai')).toBeNull() expect(await getBYOKKey(null, 'openai')).toBeNull() - expect(mockGetWorkspaceById).not.toHaveBeenCalled() - }) - - it('returns null when the workspace does not exist', async () => { - mockGetWorkspaceById.mockResolvedValue(null) - - expect(await getBYOKKey(uniqueWorkspaceId(), 'openai')).toBeNull() }) it('returns null when the workspace has no keys for the provider', async () => { @@ -123,6 +110,17 @@ describe('getBYOKKey', () => { ]) }) + it('reads the key list fresh from the database on every call', async () => { + const workspaceId = uniqueWorkspaceId() + mockOrderBy.mockResolvedValue([storedKey('key-1')]) + + await getBYOKKey(workspaceId, 'openai') + await getBYOKKey(workspaceId, 'openai') + await getBYOKKey(workspaceId, 'openai') + + expect(mockOrderBy).toHaveBeenCalledTimes(3) + }) + it('tracks rotation independently per provider within a workspace', async () => { const workspaceId = uniqueWorkspaceId() mockOrderBy.mockResolvedValue([storedKey('key-1'), storedKey('key-2')]) diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index b131d2f742..73df4a3a1b 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -6,7 +6,6 @@ import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { env } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/env-flags' import { decryptSecret } from '@/lib/core/security/encryption' -import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { getHostedModels } from '@/providers/models' import { PROVIDER_PLACEHOLDER_KEY } from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' @@ -37,6 +36,9 @@ function nextRotationIndex(poolKey: string, poolSize: number): number { * multiple keys stored for the provider, requests round-robin across them in * creation order. A key that fails to decrypt is skipped in favor of the next * one in the pool. + * + * The key list is read fresh every call (not cached): BYOK is not a hot query, + * and reading fresh keeps revocation immediate across ECS tasks. */ export async function getBYOKKey( workspaceId: string | undefined | null, @@ -47,11 +49,6 @@ export async function getBYOKKey( } try { - const activeWorkspace = await getWorkspaceById(workspaceId) - if (!activeWorkspace) { - return null - } - const keys = await db .select({ id: workspaceBYOKKeys.id, encryptedApiKey: workspaceBYOKKeys.encryptedApiKey }) .from(workspaceBYOKKeys) diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index 906007689f..4e259f2036 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -467,6 +467,9 @@ export async function checkOrgMemberUsageLimit( return { isExceeded: false, currentUsage: 0, limit: null } } + // Resolve the cap first and short-circuit when unset (the common case); only + // then is computing usage worthwhile. Kept sequential, not raced, to avoid a + // usage query on every uncapped member's execution. const limit = await getOrgMemberUsageLimit(organizationId, userId) if (limit === null) { return { isExceeded: false, currentUsage: 0, limit: null } diff --git a/apps/sim/lib/billing/core/plan.test.ts b/apps/sim/lib/billing/core/plan.test.ts new file mode 100644 index 0000000000..cf223952b0 --- /dev/null +++ b/apps/sim/lib/billing/core/plan.test.ts @@ -0,0 +1,193 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +/** + * Drizzle mock for `getHighestPrioritySubscription`. It issues up to four + * queries keyed by table: + * - `subscription` for the user's personal subs (parallelized with members) + * - `member` for the user's org memberships (parallelized with subs) + * - `organization` for the org-existence follow-up + * - `subscription` again for the org-scoped subs follow-up + * + * The mock routes results by the table object passed to `.from()`, serving the + * (twice-read) `subscription` table from a FIFO queue (first read = personal, + * second = org). It records which tables were queried so we can assert the + * parallelized pair both run and that follow-ups are skipped when appropriate. + * + * Table sentinels and shared mock state live inside `vi.hoisted` so the + * `vi.mock` factories (hoisted to the top of the file) can reference them. + */ +const { SUBSCRIPTION_TABLE, MEMBER_TABLE, ORGANIZATION_TABLE, resultsByTable, fromCalls, select } = + vi.hoisted(() => { + const SUBSCRIPTION_TABLE = { __table: 'subscription' } + const MEMBER_TABLE = { __table: 'member' } + const ORGANIZATION_TABLE = { __table: 'organization' } + + const resultsByTable: Record = { + subscription: [], + member: [], + organization: [], + } + const fromCalls: string[] = [] + + const select = vi.fn(() => ({ + from: (table: { __table: string }) => { + fromCalls.push(table.__table) + const where = () => { + const queue = resultsByTable[table.__table] + const next = queue.length > 0 ? queue.shift() : [] + return Promise.resolve(next ?? []) + } + return { where } + }, + })) + + return { + SUBSCRIPTION_TABLE, + MEMBER_TABLE, + ORGANIZATION_TABLE, + resultsByTable, + fromCalls, + select, + } + }) + +vi.mock('@sim/db', () => ({ + db: { select }, +})) + +vi.mock('@sim/db/schema', () => ({ + subscription: SUBSCRIPTION_TABLE, + member: MEMBER_TABLE, + organization: ORGANIZATION_TABLE, +})) + +/** + * Realistic plan-check predicates so `pickHighestPrioritySubscription` exercises + * the real Enterprise > Team > Pro priority ordering over the rows we feed it. + */ +vi.mock('@/lib/billing/subscriptions/utils', () => ({ + ENTITLED_SUBSCRIPTION_STATUSES: ['active', 'past_due'], + checkEnterprisePlan: (s: any) => + s?.plan === 'enterprise' && ['active', 'past_due'].includes(s?.status), + checkTeamPlan: (s: any) => s?.plan === 'team' && ['active', 'past_due'].includes(s?.status), + checkProPlan: (s: any) => s?.plan === 'pro' && ['active', 'past_due'].includes(s?.status), +})) + +import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' + +interface SubRow { + id: string + referenceId: string + plan: string + status: string +} + +function personalPro(userId: string): SubRow { + return { id: 'sub-personal-pro', referenceId: userId, plan: 'pro', status: 'active' } +} + +function orgEnterprise(orgId: string): SubRow { + return { id: 'sub-org-enterprise', referenceId: orgId, plan: 'enterprise', status: 'active' } +} + +function queue(table: 'subscription' | 'member' | 'organization', rows: unknown[]) { + resultsByTable[table].push(rows) +} + +describe('getHighestPrioritySubscription', () => { + beforeEach(() => { + vi.clearAllMocks() + resultsByTable.subscription = [] + resultsByTable.member = [] + resultsByTable.organization = [] + fromCalls.length = 0 + }) + + it('picks the org Enterprise sub over a personal Pro sub (priority order)', async () => { + queue('subscription', [personalPro('user-1')]) // personalSubs query + queue('member', [{ organizationId: 'org-1' }]) // memberships query + queue('organization', [{ id: 'org-1' }]) // org-existence query + queue('subscription', [orgEnterprise('org-1')]) // org-subscriptions query + + const result = await getHighestPrioritySubscription('user-1') + + expect(result).not.toBeNull() + expect(result?.id).toBe('sub-org-enterprise') + expect(result?.plan).toBe('enterprise') + }) + + it('selection is deterministic regardless of which parallelized query resolves first', async () => { + queue('subscription', [personalPro('user-1')]) + queue('member', [{ organizationId: 'org-1' }]) + queue('organization', [{ id: 'org-1' }]) + queue('subscription', [orgEnterprise('org-1')]) + + const result = await getHighestPrioritySubscription('user-1') + + expect(result?.id).toBe('sub-org-enterprise') + }) + + it('issues BOTH the personal-subscriptions and memberships queries (parallelized pair)', async () => { + queue('subscription', [personalPro('user-1')]) + queue('member', [{ organizationId: 'org-1' }]) + queue('organization', [{ id: 'org-1' }]) + queue('subscription', [orgEnterprise('org-1')]) + + await getHighestPrioritySubscription('user-1') + + expect(fromCalls).toContain('subscription') + expect(fromCalls).toContain('member') + // First two queries are exactly the parallelized pair (in either order). + expect(fromCalls.slice(0, 2).sort()).toEqual(['member', 'subscription']) + }) + + it('returns the personal sub and skips org follow-ups when there are no memberships', async () => { + queue('subscription', [personalPro('user-1')]) + queue('member', []) + + const result = await getHighestPrioritySubscription('user-1') + + expect(result?.id).toBe('sub-personal-pro') + expect(result?.plan).toBe('pro') + // org-existence + org-subscription follow-ups are NOT issued. + expect(fromCalls).not.toContain('organization') + expect(fromCalls.filter((t) => t === 'subscription')).toHaveLength(1) + }) + + it('returns null when neither personal nor org subscriptions exist', async () => { + queue('subscription', []) + queue('member', []) + + const result = await getHighestPrioritySubscription('user-1') + + expect(result).toBeNull() + }) + + it('excludes orphaned org memberships whose organization row no longer exists', async () => { + queue('subscription', []) + queue('member', [{ organizationId: 'ghost-org' }]) // membership points at a deleted org + queue('organization', []) + + const result = await getHighestPrioritySubscription('user-1') + + // Org subs are never fetched (no valid org ids) -> falls back to null. + expect(result).toBeNull() + expect(fromCalls).toContain('organization') + // Only the initial personal-subs read on `subscription`; org-subs query skipped. + expect(fromCalls.filter((t) => t === 'subscription')).toHaveLength(1) + }) + + it('falls back to the personal sub when the only org is orphaned', async () => { + queue('subscription', [personalPro('user-1')]) + queue('member', [{ organizationId: 'ghost-org' }]) + queue('organization', []) + + const result = await getHighestPrioritySubscription('user-1') + + expect(result?.id).toBe('sub-personal-pro') + expect(fromCalls.filter((t) => t === 'subscription')).toHaveLength(1) + }) +}) diff --git a/apps/sim/lib/billing/core/plan.ts b/apps/sim/lib/billing/core/plan.ts index b4a56dab13..633d2e55c8 100644 --- a/apps/sim/lib/billing/core/plan.ts +++ b/apps/sim/lib/billing/core/plan.ts @@ -82,20 +82,21 @@ export async function getHighestPrioritySubscription( ) { const { onError = 'return-null', executor = db } = options try { - const personalSubs = await executor - .select() - .from(subscription) - .where( - and( - eq(subscription.referenceId, userId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES) - ) - ) - - const memberships = await executor - .select({ organizationId: member.organizationId }) - .from(member) - .where(eq(member.userId, userId)) + const [personalSubs, memberships] = await Promise.all([ + executor + .select() + .from(subscription) + .where( + and( + eq(subscription.referenceId, userId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES) + ) + ), + executor + .select({ organizationId: member.organizationId }) + .from(member) + .where(eq(member.userId, userId)), + ]) const orgIds = memberships.map((m: { organizationId: string }) => m.organizationId) diff --git a/apps/sim/lib/execution/preprocessing.test.ts b/apps/sim/lib/execution/preprocessing.test.ts index c00e5653ec..e72c44a567 100644 --- a/apps/sim/lib/execution/preprocessing.test.ts +++ b/apps/sim/lib/execution/preprocessing.test.ts @@ -28,7 +28,11 @@ vi.mock('@/lib/core/execution-limits', () => ({ getExecutionTimeout: vi.fn(() => 0), })) vi.mock('@/lib/core/rate-limiter/rate-limiter', () => ({ - RateLimiter: vi.fn(() => ({ checkRateLimitWithSubscription: mockCheckRateLimit })), + // Regular function (not an arrow) so `new RateLimiter()` is constructable under + // vitest 4.x, which rejects `new` on an arrow-implemented mock. + RateLimiter: vi.fn(function (this: unknown) { + return { checkRateLimitWithSubscription: mockCheckRateLimit } + }), })) vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock) vi.mock('@/lib/workspaces/utils', () => ({ @@ -176,7 +180,7 @@ describe('preprocessExecution ban gate', () => { } as any) }) - it('blocks execution with 403 when the actor is banned, before any billing queries', async () => { + it('blocks execution with 403 when the actor is banned (ban wins over the parallel gates)', async () => { mockGetActivelyBannedUserIds.mockResolvedValue(['billed-account-1']) const loggingSession = { @@ -194,8 +198,79 @@ describe('preprocessExecution ban gate', () => { error: { statusCode: 403, logCreated: true, message: 'Account suspended' }, }) expect(loggingSession.safeStart).toHaveBeenCalled() - expect(getHighestPrioritySubscription).not.toHaveBeenCalled() - expect(checkServerSideUsageLimits).not.toHaveBeenCalled() + }) + + it('returns 403 (ban precedence) when ban, usage, and rate limit all fail simultaneously', async () => { + mockGetActivelyBannedUserIds.mockResolvedValue(['billed-account-1']) + vi.mocked(checkServerSideUsageLimits).mockResolvedValue({ + isExceeded: true, + currentUsage: 20, + limit: 10, + message: 'Usage limit exceeded. Please upgrade your plan to continue.', + } as any) + mockCheckRateLimit.mockResolvedValue({ + allowed: false, + remaining: 0, + resetAt: new Date(), + }) + + const loggingSession = { + safeStart: vi.fn().mockResolvedValue(true), + safeCompleteWithError: vi.fn().mockResolvedValue(undefined), + } + + const result = await preprocessExecution({ + ...baseOptions, + checkRateLimit: true, + loggingSession: loggingSession as any, + }) + + // Ban (403) takes precedence over usage (402) and rate limit (429), + // independent of which parallel gate's promise settled first. + expect(result).toMatchObject({ + success: false, + error: { statusCode: 403, logCreated: true, message: 'Account suspended' }, + }) + }) + + it('does not debit rate-limit quota when the ban gate rejects', async () => { + // The rate-limit gate consumes a token, so it must not run for a request + // an earlier gate (ban) already rejects. + mockGetActivelyBannedUserIds.mockResolvedValue(['billed-account-1']) + + const result = await preprocessExecution({ ...baseOptions, checkRateLimit: true }) + + expect(result).toMatchObject({ success: false, error: { statusCode: 403 } }) + expect(mockCheckRateLimit).not.toHaveBeenCalled() + }) + + it('does not debit rate-limit quota when the usage gate rejects', async () => { + vi.mocked(checkServerSideUsageLimits).mockResolvedValue({ + isExceeded: true, + currentUsage: 20, + limit: 10, + message: 'Usage limit exceeded. Please upgrade your plan to continue.', + } as any) + + const result = await preprocessExecution({ ...baseOptions, checkRateLimit: true }) + + expect(result).toMatchObject({ success: false, error: { statusCode: 402 } }) + expect(mockCheckRateLimit).not.toHaveBeenCalled() + }) + + it('consumes the rate-limit gate exactly once when the ban and usage gates pass', async () => { + mockCheckRateLimit.mockResolvedValue({ allowed: true, remaining: 5, resetAt: new Date() }) + + // skipConcurrencyReservation bypasses the STEP 7 admission reservation so the + // assertion isolates the rate gate and does not depend on Redis availability. + const result = await preprocessExecution({ + ...baseOptions, + checkRateLimit: true, + skipConcurrencyReservation: true, + }) + + expect(result.success).toBe(true) + expect(mockCheckRateLimit).toHaveBeenCalledTimes(1) }) it('checks the billing actor, caller-provided userId, and workflow owner in one call', async () => { @@ -234,6 +309,5 @@ describe('preprocessExecution ban gate', () => { success: false, error: { statusCode: 500, logCreated: true }, }) - expect(checkServerSideUsageLimits).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/execution/preprocessing.ts b/apps/sim/lib/execution/preprocessing.ts index 075ee57af9..a41d3dbbbc 100644 --- a/apps/sim/lib/execution/preprocessing.ts +++ b/apps/sim/lib/execution/preprocessing.ts @@ -322,85 +322,118 @@ export async function preprocessExecution( } } - // ========== STEP 3.5: Reject Banned Accounts ========== - // Blocks executions when the billing actor, the workflow owner, or the - // caller-provided userId (chat deployer, authenticated caller) has an - // active ban or a blocked email domain. The owner comes from the workflow - // record so schedules — which pass the 'unknown' sentinel — are covered. - const banCandidateIds = [actorUserId] - if (userId && userId !== 'unknown' && userId !== actorUserId) { - banCandidateIds.push(userId) + // ========== STEPS 3.5–6: Preflight Gates ========== + // Read-only gates (ban, subscription, usage) run concurrently; the stateful + // rate-limit gate runs after they pass. Precedence: ban 403 → usage 402 → rate 429. + + /** + * A failing gate's deferred outcome: the response to return, plus an optional + * error-log write to flush before returning. Evaluated in precedence order. + */ + interface GateFailure { + response: PreprocessExecutionResult + recordError?: Parameters[0] } - if (workflowRecord.userId && !banCandidateIds.includes(workflowRecord.userId)) { - banCandidateIds.push(workflowRecord.userId) + + /** Usage figures captured by STEP 5 and reused by the STEP 7 reservation. */ + interface UsageSnapshot { + currentUsage: number + limit: number } - try { - const bannedUserIds = await getActivelyBannedUserIds(banCandidateIds) - if (bannedUserIds.length > 0) { - logger.warn(`[${requestId}] Execution blocked: banned account`, { - workflowId, - bannedUserIds, - triggerType, - }) - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: 'This account has been suspended. Workflow executions are blocked.', - loggingSession: providedLoggingSession, - triggerData, - }) + const banCheck = (async (): Promise => { + // Blocks executions when the billing actor, the workflow owner, or the + // caller-provided userId (chat deployer, authenticated caller) has an + // active ban or a blocked email domain. The owner comes from the workflow + // record so schedules — which pass the 'unknown' sentinel — are covered. + const banCandidateIds = [actorUserId] + if (userId && userId !== 'unknown' && userId !== actorUserId) { + banCandidateIds.push(userId) + } + if (workflowRecord.userId && !banCandidateIds.includes(workflowRecord.userId)) { + banCandidateIds.push(workflowRecord.userId) + } + try { + const bannedUserIds = await getActivelyBannedUserIds(banCandidateIds) + if (bannedUserIds.length > 0) { + logger.warn(`[${requestId}] Execution blocked: banned account`, { + workflowId, + bannedUserIds, + triggerType, + }) + + return { + response: { + success: false, + error: { + message: 'Account suspended', + statusCode: 403, + logCreated: true, + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: 'This account has been suspended. Workflow executions are blocked.', + loggingSession: providedLoggingSession, + triggerData, + }, + } + } + return null + } catch (error) { + logger.error(`[${requestId}] Error checking account ban status`, { error, actorUserId }) return { - success: false, - error: { - message: 'Account suspended', - statusCode: 403, - logCreated: true, + response: { + success: false, + error: { + message: 'Unable to verify account status. Execution blocked for security.', + statusCode: 500, + logCreated: true, + retryable: isRetryableInfrastructureError(error), + cause: describeRetryableInfrastructureError(error), + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: 'Unable to verify account status. Execution blocked for security.', + loggingSession: providedLoggingSession, + triggerData, }, } } - } catch (error) { - logger.error(`[${requestId}] Error checking account ban status`, { error, actorUserId }) - - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: 'Unable to verify account status. Execution blocked for security.', - loggingSession: providedLoggingSession, - triggerData, - }) - - return { - success: false, - error: { - message: 'Unable to verify account status. Execution blocked for security.', - statusCode: 500, - logCreated: true, - retryable: isRetryableInfrastructureError(error), - cause: describeRetryableInfrastructureError(error), - }, - } - } + })() // ========== STEP 4: Get Subscription ========== - const userSubscription = await getHighestPrioritySubscription(actorUserId) + const subscriptionFetch = getHighestPrioritySubscription(actorUserId) + + const [banFailure, userSubscription] = await Promise.all([banCheck, subscriptionFetch]) - // ========== STEP 5: Check Usage Limits ========== - // Snapshot reused by the STEP 7 admission reservation. - let usageSnapshot: { currentUsage: number; limit: number } | null = null - if (!skipUsageLimits) { + /** + * STEP 5: usage + per-member org usage gate. Returns the failure outcome (or + * `null` on pass/skip) plus the usage snapshot reused by the STEP 7 admission + * reservation. The snapshot is returned rather than written to an outer + * variable so concurrent gate tasks share no mutable state. + */ + const usageCheckTask = (async (): Promise<{ + failure: GateFailure | null + snapshot: UsageSnapshot | null + }> => { + if (skipUsageLimits) return { failure: null, snapshot: null } + let snapshot: UsageSnapshot | null = null try { const usageCheck = await checkServerSideUsageLimits(actorUserId, userSubscription) - usageSnapshot = { currentUsage: usageCheck.currentUsage, limit: usageCheck.limit } + snapshot = { currentUsage: usageCheck.currentUsage, limit: usageCheck.limit } if (usageCheck.isExceeded) { logger.warn( `[${requestId}] User ${actorUserId} has exceeded usage limits. Blocking execution.`, @@ -412,28 +445,33 @@ export async function preprocessExecution( } ) - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: - usageCheck.message || - `Usage limit exceeded: $${usageCheck.currentUsage?.toFixed(2)} used of $${usageCheck.limit?.toFixed(2)} limit. Please upgrade your plan to continue.`, - loggingSession: providedLoggingSession, - triggerData, - }) - return { - success: false, - error: { - message: - usageCheck.message || 'Usage limit exceeded. Please upgrade your plan to continue.', - statusCode: 402, - logCreated: true, + failure: { + response: { + success: false, + error: { + message: + usageCheck.message || + 'Usage limit exceeded. Please upgrade your plan to continue.', + statusCode: 402, + logCreated: true, + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: + usageCheck.message || + `Usage limit exceeded: $${usageCheck.currentUsage?.toFixed(2)} used of $${usageCheck.limit?.toFixed(2)} limit. Please upgrade your plan to continue.`, + loggingSession: providedLoggingSession, + triggerData, + }, }, + snapshot, } } @@ -457,126 +495,167 @@ export async function preprocessExecution( } ) - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: memberLimitMessage, - loggingSession: providedLoggingSession, - triggerData, - }) - return { - success: false, - error: { - message: memberLimitMessage, - statusCode: 402, - logCreated: true, + failure: { + response: { + success: false, + error: { + message: memberLimitMessage, + statusCode: 402, + logCreated: true, + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: memberLimitMessage, + loggingSession: providedLoggingSession, + triggerData, + }, }, + snapshot, } } + return { failure: null, snapshot } } catch (error) { logger.error(`[${requestId}] Error checking usage limits`, { error, actorUserId, }) - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: - 'Unable to determine usage limits. Execution blocked for security. Please contact support.', - loggingSession: providedLoggingSession, - triggerData, - }) - return { - success: false, - error: { - message: 'Unable to determine usage limits. Execution blocked for security.', - statusCode: 500, - logCreated: true, - retryable: isRetryableInfrastructureError(error), - cause: describeRetryableInfrastructureError(error), + failure: { + response: { + success: false, + error: { + message: 'Unable to determine usage limits. Execution blocked for security.', + statusCode: 500, + logCreated: true, + retryable: isRetryableInfrastructureError(error), + cause: describeRetryableInfrastructureError(error), + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: + 'Unable to determine usage limits. Execution blocked for security. Please contact support.', + loggingSession: providedLoggingSession, + triggerData, + }, }, + snapshot, } } - } + })() // ========== STEP 6: Check Rate Limits ========== let rateLimitInfo: { allowed: boolean; remaining: number; resetAt: Date } | undefined - if (checkRateLimit) { + /** + * STEP 6: rate-limit gate. Unlike the other gates this one is NOT read-only — + * `checkRateLimitWithSubscription` consumes a token — so it is invoked + * sequentially only after the ban and usage gates pass, matching the original + * order. Running it eagerly or in parallel would debit rate-limit quota for + * requests that ban or usage rejects. Returns the failure outcome, or `null` + * on pass/skip; on a non-error outcome it populates `rateLimitInfo`. + */ + const runRateLimitGate = async (): Promise => { + if (!checkRateLimit) return null try { const rateLimiter = new RateLimiter() - rateLimitInfo = await rateLimiter.checkRateLimitWithSubscription( + const info = await rateLimiter.checkRateLimitWithSubscription( actorUserId, userSubscription, triggerType, false // not async ) + rateLimitInfo = info - if (!rateLimitInfo.allowed) { + if (!info.allowed) { logger.warn(`[${requestId}] Rate limit exceeded for user ${actorUserId}`, { triggerType, - remaining: rateLimitInfo.remaining, - resetAt: rateLimitInfo.resetAt, + remaining: info.remaining, + resetAt: info.resetAt, }) - await recordPreprocessingError({ + return { + response: { + success: false, + error: { + message: `Rate limit exceeded. Please try again later.`, + statusCode: 429, + logCreated: true, + }, + }, + recordError: { + workflowId, + executionId, + triggerType, + requestId, + userId: actorUserId, + workspaceId, + errorMessage: `Rate limit exceeded. ${info.remaining} requests remaining. Resets at ${info.resetAt.toISOString()}.`, + loggingSession: providedLoggingSession, + triggerData, + }, + } + } + return null + } catch (error) { + logger.error(`[${requestId}] Error checking rate limits`, { error, actorUserId }) + + return { + response: { + success: false, + error: { + message: 'Error checking rate limits', + statusCode: 500, + logCreated: true, + retryable: isRetryableInfrastructureError(error), + cause: describeRetryableInfrastructureError(error), + }, + }, + recordError: { workflowId, executionId, triggerType, requestId, userId: actorUserId, workspaceId, - errorMessage: `Rate limit exceeded. ${rateLimitInfo.remaining} requests remaining. Resets at ${rateLimitInfo.resetAt.toISOString()}.`, + errorMessage: 'Error checking rate limits. Execution blocked for safety.', loggingSession: providedLoggingSession, triggerData, - }) - - return { - success: false, - error: { - message: `Rate limit exceeded. Please try again later.`, - statusCode: 429, - logCreated: true, - }, - } + }, } - } catch (error) { - logger.error(`[${requestId}] Error checking rate limits`, { error, actorUserId }) + } + } - await recordPreprocessingError({ - workflowId, - executionId, - triggerType, - requestId, - userId: actorUserId, - workspaceId, - errorMessage: 'Error checking rate limits. Execution blocked for safety.', - loggingSession: providedLoggingSession, - triggerData, - }) + const usageResult = await usageCheckTask + const usageSnapshot = usageResult.snapshot - return { - success: false, - error: { - message: 'Error checking rate limits', - statusCode: 500, - logCreated: true, - retryable: isRetryableInfrastructureError(error), - cause: describeRetryableInfrastructureError(error), - }, - } + const readGateFailure = banFailure ?? usageResult.failure + if (readGateFailure) { + if (readGateFailure.recordError) { + await recordPreprocessingError(readGateFailure.recordError) + } + return readGateFailure.response + } + + const rateLimitFailure = await runRateLimitGate() + if (rateLimitFailure) { + if (rateLimitFailure.recordError) { + await recordPreprocessingError(rateLimitFailure.recordError) } + return rateLimitFailure.response } /** diff --git a/apps/sim/lib/workflows/executor/execution-core.test.ts b/apps/sim/lib/workflows/executor/execution-core.test.ts index eba2011484..58fbca16d5 100644 --- a/apps/sim/lib/workflows/executor/execution-core.test.ts +++ b/apps/sim/lib/workflows/executor/execution-core.test.ts @@ -72,26 +72,22 @@ vi.mock('@/lib/workflows/triggers/triggers', () => ({ vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/executor', () => ({ - Executor: vi.fn().mockImplementation( - class { - constructor(args: unknown) { - executorConstructorMock(args) - // biome-ignore lint/correctness/noConstructorReturn: vitest 4 constructs mocks via Reflect.construct; returning the instance overrides `new Executor(...)` - return { - execute: executorExecuteMock, - executeFromBlock: executorExecuteMock, - } + Executor: class { + constructor(args: unknown) { + executorConstructorMock(args) + // biome-ignore lint/correctness/noConstructorReturn: returning the instance overrides `new Executor(...)` so consumers get the mocked methods + return { + execute: executorExecuteMock, + executeFromBlock: executorExecuteMock, } } - ), + }, })) vi.mock('@/serializer', () => ({ - Serializer: vi.fn().mockImplementation( - class { - serializeWorkflow = serializeWorkflowMock - } - ), + Serializer: class { + serializeWorkflow = serializeWorkflowMock + }, })) import { @@ -192,6 +188,96 @@ describe('executeWorkflowCore terminal finalization sequencing', () => { clearExecutionCancellationMock.mockResolvedValue(undefined) }) + it('loads workflow state and env vars concurrently, then starts logging before constructing the executor', async () => { + const callOrder: string[] = [] + + let releaseWorkflowLoad: (() => void) | undefined + let releaseEnvLoad: (() => void) | undefined + const workflowLoadGate = new Promise((resolve) => { + releaseWorkflowLoad = resolve + }) + const envLoadGate = new Promise((resolve) => { + releaseEnvLoad = resolve + }) + + loadWorkflowFromNormalizedTablesMock.mockImplementation(async () => { + callOrder.push('load-workflow:start') + await workflowLoadGate + callOrder.push('load-workflow:end') + return { + blocks: { + 'start-block': { + id: 'start-block', + type: 'start_trigger', + subBlocks: {}, + name: 'Start', + }, + }, + edges: [], + loops: {}, + parallels: {}, + } + }) + + getPersonalAndWorkspaceEnvMock.mockImplementation(async () => { + callOrder.push('load-env:start') + await envLoadGate + callOrder.push('load-env:end') + return { + personalEncrypted: {}, + workspaceEncrypted: {}, + personalDecrypted: {}, + workspaceDecrypted: {}, + } + }) + + safeStartMock.mockImplementation(async () => { + callOrder.push('safeStart') + return true + }) + + executorConstructorMock.mockImplementation(() => { + callOrder.push('executor-construct') + }) + + executorExecuteMock.mockResolvedValue({ + success: true, + status: 'completed', + output: { done: true }, + logs: [], + metadata: { duration: 123, startTime: 'start', endTime: 'end' }, + }) + + const executionPromise = executeWorkflowCore({ + snapshot: createSnapshot() as any, + callbacks: {}, + loggingSession: loggingSession as any, + }) + + await Promise.resolve() + + expect(callOrder).toContain('load-workflow:start') + expect(callOrder).toContain('load-env:start') + expect(callOrder).not.toContain('safeStart') + expect(callOrder).not.toContain('executor-construct') + + releaseWorkflowLoad?.() + releaseEnvLoad?.() + + await executionPromise + + expect(callOrder).toEqual([ + 'load-workflow:start', + 'load-env:start', + 'load-workflow:end', + 'load-env:end', + 'safeStart', + 'executor-construct', + ]) + expect(safeStartMock).toHaveBeenCalledTimes(1) + expect(executorConstructorMock).toHaveBeenCalledTimes(1) + }) + it('routes onBlockStart through logging session persistence path', async () => { executorExecuteMock.mockResolvedValue({ success: true, diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 3763203aaa..068e4bf9e3 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -349,62 +349,78 @@ export async function executeWorkflowCore( } try { - let blocks - let edges: Edge[] - let loops - let parallels - - // Use workflowStateOverride if provided (for diff workflows) - if (metadata.workflowStateOverride) { - blocks = metadata.workflowStateOverride.blocks - edges = metadata.workflowStateOverride.edges - loops = metadata.workflowStateOverride.loops || {} - parallels = metadata.workflowStateOverride.parallels || {} - deploymentVersionId = metadata.workflowStateOverride.deploymentVersionId - - logger.info(`[${requestId}] Using workflow state override (diff workflow execution)`, { - blocksCount: Object.keys(blocks).length, - edgesCount: edges.length, - }) - } else if (useDraftState) { - const draftData = await loadWorkflowFromNormalizedTables(workflowId) + const personalEnvUserId = + metadata.isClientSession && metadata.sessionUserId + ? metadata.sessionUserId + : metadata.workflowUserId - if (!draftData) { - throw new Error('Workflow not found or not yet saved') + if (!personalEnvUserId) { + throw new Error('Missing workflowUserId in execution metadata') + } + + /** + * Resolves the workflow state from the override, the draft tables, or the + * deployed snapshot. The async load (draft/deployed) has no data dependency + * on the environment load, so the two are awaited concurrently below. + */ + const loadWorkflowState = async () => { + if (metadata.workflowStateOverride) { + const override = metadata.workflowStateOverride + logger.info(`[${requestId}] Using workflow state override (diff workflow execution)`, { + blocksCount: Object.keys(override.blocks).length, + edgesCount: override.edges.length, + }) + return { + blocks: override.blocks, + edges: override.edges, + loops: override.loops || {}, + parallels: override.parallels || {}, + deploymentVersionId: override.deploymentVersionId, + } } - blocks = draftData.blocks - edges = draftData.edges - loops = draftData.loops - parallels = draftData.parallels + if (useDraftState) { + const draftData = await loadWorkflowFromNormalizedTables(workflowId) - logger.info( - `[${requestId}] Using draft workflow state from normalized tables (client execution)` - ) - } else { - const deployedData = await loadDeployedWorkflowState(workflowId) - blocks = deployedData.blocks - edges = deployedData.edges - loops = deployedData.loops - parallels = deployedData.parallels - deploymentVersionId = deployedData.deploymentVersionId + if (!draftData) { + throw new Error('Workflow not found or not yet saved') + } + logger.info( + `[${requestId}] Using draft workflow state from normalized tables (client execution)` + ) + return { + blocks: draftData.blocks, + edges: draftData.edges, + loops: draftData.loops, + parallels: draftData.parallels, + deploymentVersionId: undefined, + } + } + + const deployedData = await loadDeployedWorkflowState(workflowId) logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`) + return { + blocks: deployedData.blocks, + edges: deployedData.edges, + loops: deployedData.loops, + parallels: deployedData.parallels, + deploymentVersionId: deployedData.deploymentVersionId, + } } - const mergedStates = mergeSubblockStateWithValues(blocks) + const [workflowState, env] = await Promise.all([ + loadWorkflowState(), + getPersonalAndWorkspaceEnv(personalEnvUserId, providedWorkspaceId), + ]) - const personalEnvUserId = - metadata.isClientSession && metadata.sessionUserId - ? metadata.sessionUserId - : metadata.workflowUserId + const { blocks, loops, parallels } = workflowState + const edges: Edge[] = workflowState.edges + deploymentVersionId = workflowState.deploymentVersionId - if (!personalEnvUserId) { - throw new Error('Missing workflowUserId in execution metadata') - } + const mergedStates = mergeSubblockStateWithValues(blocks) - const { personalEncrypted, workspaceEncrypted, personalDecrypted, workspaceDecrypted } = - await getPersonalAndWorkspaceEnv(personalEnvUserId, providedWorkspaceId) + const { personalEncrypted, workspaceEncrypted, personalDecrypted, workspaceDecrypted } = env // Use encrypted values for logging (don't log decrypted secrets) const variables = EnvVarsSchema.parse({ ...personalEncrypted, ...workspaceEncrypted }) diff --git a/apps/sim/lib/workflows/persistence/utils.test.ts b/apps/sim/lib/workflows/persistence/utils.test.ts index afcc4081fa..964577f9cf 100644 --- a/apps/sim/lib/workflows/persistence/utils.test.ts +++ b/apps/sim/lib/workflows/persistence/utils.test.ts @@ -113,6 +113,22 @@ vi.mock('@sim/db', () => ({ webhook: {}, })) +const { mockSanitizeAgentToolsInBlocks } = vi.hoisted(() => ({ + mockSanitizeAgentToolsInBlocks: vi.fn(), +})) + +/** + * Default identity behavior for the mocked migration step. Re-applied in the + * cache describe block's `beforeEach` because the outer `afterEach` calls + * `vi.resetAllMocks()`, which clears implementations. + */ +const sanitizeIdentity = (blocks: unknown) => ({ blocks }) +mockSanitizeAgentToolsInBlocks.mockImplementation(sanitizeIdentity) + +vi.mock('@/lib/workflows/sanitization/validation', () => ({ + sanitizeAgentToolsInBlocks: mockSanitizeAgentToolsInBlocks, +})) + import * as dbHelpers from '@/lib/workflows/persistence/utils' const mockWorkflowId = 'test-workflow-123' @@ -307,6 +323,7 @@ const mockWorkflowState = createWorkflowState({ describe('Database Helpers', () => { beforeEach(() => { vi.clearAllMocks() + mockSanitizeAgentToolsInBlocks.mockImplementation(sanitizeIdentity) }) afterEach(() => { @@ -1550,4 +1567,157 @@ describe('Database Helpers', () => { expect(messages2).toEqual([{ role: 'system', content: 'System' }]) }) }) + + describe('loadDeployedWorkflowState deployed-state cache', () => { + /** + * Minimal but realistic deployed state: a couple of plain (non-agent, + * credential-free) blocks plus an edge. Plain blocks make the real + * downstream migration steps (agent-message, subblock-id, credential, + * canonical-mode) no-ops, so the only observable "heavy work" is the + * mocked `sanitizeAgentToolsInBlocks` first step, which we use as the + * migration call counter. + */ + function buildDeployedState() { + return { + blocks: { + 'block-1': { + id: 'block-1', + type: 'api', + name: 'API Block', + position: { x: 0, y: 0 }, + enabled: true, + subBlocks: { url: { id: 'url', type: 'short-input', value: 'https://example.com' } }, + outputs: {}, + data: {}, + }, + 'block-2': { + id: 'block-2', + type: 'function', + name: 'Function Block', + position: { x: 100, y: 0 }, + enabled: true, + subBlocks: { code: { id: 'code', type: 'code', value: 'return 1' } }, + outputs: {}, + data: {}, + }, + }, + edges: [ + { + id: 'edge-1', + source: 'block-1', + target: 'block-2', + sourceHandle: 'output', + targetHandle: 'input', + }, + ], + loops: {}, + parallels: {}, + variables: { threshold: 5 }, + } + } + + /** + * Wires `db.select` to return a single active deployment-version row for the + * given id. Returns the inner `where` spy so tests can assert how many times + * the active-version SELECT ran. + */ + function mockActiveVersionSelect(versionId: string, state: unknown) { + const where = vi.fn().mockReturnValue({ + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ id: versionId, state, createdAt: new Date() }]), + }), + }) + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ where }), + }) + return where + } + + beforeEach(() => { + vi.clearAllMocks() + mockSanitizeAgentToolsInBlocks.mockImplementation(sanitizeIdentity) + dbHelpers.invalidateDeployedStateCache() + }) + + it('serves a cache HIT, skipping migrations on the second call for the same active version', async () => { + const where = mockActiveVersionSelect('dv-hit', buildDeployedState()) + + const first = await dbHelpers.loadDeployedWorkflowState('wf-1', 'workspace-1') + const second = await dbHelpers.loadDeployedWorkflowState('wf-1', 'workspace-1') + + expect(first).toBeDefined() + expect(second).toBeDefined() + expect(mockSanitizeAgentToolsInBlocks).toHaveBeenCalledTimes(1) + expect(where).toHaveBeenCalledTimes(2) + }) + + it('still runs the active-version SELECT on every call so rollback/redeploy stays observable', async () => { + const where = mockActiveVersionSelect('dv-active', buildDeployedState()) + + await dbHelpers.loadDeployedWorkflowState('wf-2', 'workspace-1') + await dbHelpers.loadDeployedWorkflowState('wf-2', 'workspace-1') + + expect(where).toHaveBeenCalledTimes(2) + }) + + it('deep-clones on read: mutating the first result does not corrupt the cached copy', async () => { + mockActiveVersionSelect('dv-clone', buildDeployedState()) + + const first = await dbHelpers.loadDeployedWorkflowState('wf-3', 'workspace-1') + ;(first.blocks['block-1'] as any).name = 'MUTATED' + ;(first.blocks['block-1'].subBlocks.url as any).value = 'https://hacked.example' + first.edges.push({ + id: 'edge-injected', + source: 'block-2', + target: 'block-1', + } as any) + + const second = await dbHelpers.loadDeployedWorkflowState('wf-3', 'workspace-1') + + expect(second.blocks['block-1'].name).toBe('API Block') + expect(second.blocks['block-1'].subBlocks.url.value).toBe('https://example.com') + expect(second.edges).toHaveLength(1) + expect(second.blocks).toEqual(buildDeployedState().blocks) + }) + + it('keys the cache by deploymentVersionId: a different active id triggers a fresh build', async () => { + mockActiveVersionSelect('dv-old', buildDeployedState()) + await dbHelpers.loadDeployedWorkflowState('wf-4', 'workspace-1') + expect(mockSanitizeAgentToolsInBlocks).toHaveBeenCalledTimes(1) + + mockActiveVersionSelect('dv-new', buildDeployedState()) + await dbHelpers.loadDeployedWorkflowState('wf-4', 'workspace-1') + expect(mockSanitizeAgentToolsInBlocks).toHaveBeenCalledTimes(2) + }) + + it('invalidateDeployedStateCache(id) forces a rebuild on the next call', async () => { + mockActiveVersionSelect('dv-inv', buildDeployedState()) + + await dbHelpers.loadDeployedWorkflowState('wf-5', 'workspace-1') + await dbHelpers.loadDeployedWorkflowState('wf-5', 'workspace-1') + expect(mockSanitizeAgentToolsInBlocks).toHaveBeenCalledTimes(1) + + dbHelpers.invalidateDeployedStateCache('dv-inv') + + await dbHelpers.loadDeployedWorkflowState('wf-5', 'workspace-1') + expect(mockSanitizeAgentToolsInBlocks).toHaveBeenCalledTimes(2) + }) + + it('throws when there is no active deployment and does not cache the failure', async () => { + const where = vi.fn().mockReturnValue({ + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }) + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ where }), + }) + + await expect(dbHelpers.loadDeployedWorkflowState('wf-6', 'workspace-1')).rejects.toThrow( + 'Workflow wf-6 has no active deployment' + ) + + expect(mockSanitizeAgentToolsInBlocks).not.toHaveBeenCalled() + }) + }) }) diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index 5663d0f3fd..725e7a1347 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -13,6 +13,7 @@ import type { DbOrTx, NormalizedWorkflowData } from '@sim/workflow-persistence/t import type { BlockState, Loop, Parallel, WorkflowState } from '@sim/workflow-types/workflow' import type { InferSelectModel } from 'drizzle-orm' import { and, desc, eq, inArray, lt, sql } from 'drizzle-orm' +import { LRUCache } from 'lru-cache' import type { Edge } from 'reactflow' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' import { @@ -99,6 +100,29 @@ export async function blockExistsInDeployment( } } +const DEPLOYED_STATE_CACHE_MAX_ENTRIES = 500 +const DEPLOYED_STATE_CACHE_TTL_MS = 5 * 60 * 1000 + +/** + * Caches post-migration deployed state by the immutable `deploymentVersionId`, so + * a redeploy/rollback (which changes the active id) self-invalidates. The TTL is + * absolute on purpose — it bounds the one non-immutable part, the live credential + * remap in `applyBlockMigrations` — so credential changes still propagate. + */ +const deployedStateCache = new LRUCache({ + max: DEPLOYED_STATE_CACHE_MAX_ENTRIES, + ttl: DEPLOYED_STATE_CACHE_TTL_MS, +}) + +/** Evicts one deployed-state entry, or clears the cache when no id is given. */ +export function invalidateDeployedStateCache(deploymentVersionId?: string): void { + if (deploymentVersionId) { + deployedStateCache.delete(deploymentVersionId) + return + } + deployedStateCache.clear() +} + export async function loadDeployedWorkflowState( workflowId: string, providedWorkspaceId?: string @@ -124,6 +148,11 @@ export async function loadDeployedWorkflowState( throw new Error(`Workflow ${workflowId} has no active deployment`) } + const cached = deployedStateCache.get(active.id) + if (cached) { + return structuredClone(cached) + } + const state = active.state as WorkflowState & { variables?: Record } let resolvedWorkspaceId = providedWorkspaceId @@ -141,7 +170,7 @@ export async function loadDeployedWorkflowState( resolvedWorkspaceId ) - return { + const deployedState: DeployedWorkflowData = { blocks: migratedBlocks, edges: state.edges || [], loops: state.loops || {}, @@ -150,6 +179,10 @@ export async function loadDeployedWorkflowState( isFromNormalizedTables: false, deploymentVersionId: active.id, } + + deployedStateCache.set(active.id, deployedState) + + return structuredClone(deployedState) } catch (error) { logger.error(`Error loading deployed workflow state ${workflowId}:`, error) throw error diff --git a/apps/sim/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts index 543c328fb1..043ae4b0f0 100644 --- a/apps/sim/providers/anthropic/index.ts +++ b/apps/sim/providers/anthropic/index.ts @@ -2,6 +2,7 @@ import Anthropic from '@anthropic-ai/sdk' import { createLogger } from '@sim/logger' import type { StreamingExecution } from '@/executor/types' import { executeAnthropicProviderRequest } from '@/providers/anthropic/core' +import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types' @@ -21,13 +22,19 @@ export const anthropicProvider: ProviderConfig = { return executeAnthropicProviderRequest(request, { providerId: 'anthropic', providerLabel: 'Anthropic', - createClient: (apiKey, useNativeStructuredOutputs) => - new Anthropic({ - apiKey, - defaultHeaders: useNativeStructuredOutputs - ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } - : undefined, - }), + createClient: (apiKey, useNativeStructuredOutputs) => { + const cacheKey = `anthropic::${apiKey}::${useNativeStructuredOutputs ? 'beta' : 'default'}` + return getCachedProviderClient( + cacheKey, + () => + new Anthropic({ + apiKey, + defaultHeaders: useNativeStructuredOutputs + ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } + : undefined, + }) + ) + }, logger, }) }, diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts index 39980d77c2..2f0498992a 100644 --- a/apps/sim/providers/azure-anthropic/index.ts +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -4,6 +4,7 @@ import { env } from '@/lib/core/config/env' import { createPinnedFetch, validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import type { StreamingExecution } from '@/executor/types' import { executeAnthropicProviderRequest } from '@/providers/anthropic/core' +import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import type { ProviderConfig, ProviderRequest, ProviderResponse } from '@/providers/types' @@ -29,6 +30,7 @@ export const azureAnthropicProvider: ProviderConfig = { } let pinnedFetch: typeof fetch | undefined + let pinnedIP: string | undefined if (userProvidedEndpoint) { const validation = await validateUrlWithDNS(userProvidedEndpoint, 'azureEndpoint') if (!validation.isValid) { @@ -41,7 +43,8 @@ export const azureAnthropicProvider: ProviderConfig = { if (!validation.resolvedIP) { throw new Error('Invalid Azure Anthropic endpoint: could not resolve a pinnable IP address') } - pinnedFetch = createPinnedFetch(validation.resolvedIP) + pinnedIP = validation.resolvedIP + pinnedFetch = createPinnedFetch(pinnedIP) } const apiKey = request.apiKey @@ -68,19 +71,32 @@ export const azureAnthropicProvider: ProviderConfig = { { providerId: 'azure-anthropic', providerLabel: 'Azure Anthropic', - createClient: (apiKey, useNativeStructuredOutputs) => - new Anthropic({ - baseURL, + createClient: (apiKey, useNativeStructuredOutputs) => { + const cacheKey = [ + 'azure-anthropic', apiKey, - ...(pinnedFetch ? { fetch: pinnedFetch } : {}), - defaultHeaders: { - 'api-key': apiKey, - 'anthropic-version': anthropicVersion, - ...(useNativeStructuredOutputs - ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } - : {}), - }, - }), + baseURL, + anthropicVersion, + pinnedIP ?? 'no-pin', + useNativeStructuredOutputs ? 'beta' : 'default', + ].join('::') + return getCachedProviderClient( + cacheKey, + () => + new Anthropic({ + baseURL, + apiKey, + ...(pinnedFetch ? { fetch: pinnedFetch } : {}), + defaultHeaders: { + 'api-key': apiKey, + 'anthropic-version': anthropicVersion, + ...(useNativeStructuredOutputs + ? { 'anthropic-beta': 'structured-outputs-2025-11-13' } + : {}), + }, + }) + ) + }, logger, } ) diff --git a/apps/sim/providers/bedrock/index.test.ts b/apps/sim/providers/bedrock/index.test.ts index 38cb857425..3a9abceefb 100644 --- a/apps/sim/providers/bedrock/index.test.ts +++ b/apps/sim/providers/bedrock/index.test.ts @@ -50,10 +50,12 @@ vi.mock('@/tools', () => ({ import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime' import { bedrockProvider } from '@/providers/bedrock/index' +import { clearProviderClientCacheForTests } from '@/providers/client-cache' describe('bedrockProvider credential handling', () => { beforeEach(() => { vi.clearAllMocks() + clearProviderClientCacheForTests() mockSend.mockResolvedValue({ output: { message: { content: [{ text: 'response' }] } }, usage: { inputTokens: 10, outputTokens: 5 }, diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index 32be407867..4e512d15b4 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -24,6 +24,7 @@ import { generateToolUseId, getBedrockInferenceProfileId, } from '@/providers/bedrock/utils' +import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { createStreamingExecution } from '@/providers/streaming-execution' import { enrichLastModelSegment } from '@/providers/trace-enrichment' @@ -138,7 +139,16 @@ export const bedrockProvider: ProviderConfig = { } } - const client = new BedrockRuntimeClient(clientConfig) + // Key on the full credential (access key id + secret) so a corrected secret + // under the same access key id yields a fresh client rather than a stale one. + const credentialKey = + request.bedrockAccessKeyId && request.bedrockSecretKey + ? `${request.bedrockAccessKeyId}:${request.bedrockSecretKey}` + : 'default-chain' + const client = getCachedProviderClient( + `bedrock::${region}::${credentialKey}`, + () => new BedrockRuntimeClient(clientConfig) + ) const messages: BedrockMessage[] = [] const systemContent: SystemContentBlock[] = [] diff --git a/apps/sim/providers/client-cache.test.ts b/apps/sim/providers/client-cache.test.ts new file mode 100644 index 0000000000..6fd03ee97b --- /dev/null +++ b/apps/sim/providers/client-cache.test.ts @@ -0,0 +1,107 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { getCachedProviderClient } from '@/providers/client-cache' + +/** + * Builds a fresh fake "client" object on every call so identity comparisons + * (`toBe`) tell us whether the cache returned the memoized instance or a new one + * from the factory. We never construct a real SDK client — these tests exercise + * the cache, not any provider SDK. + */ +function makeFactory() { + return vi.fn(() => ({}) as object) +} + +/** + * Generates a unique suffix per test so distinct tests never collide on cache + * keys. The cache util exposes no reset hook, so isolation is achieved by + * namespacing keys rather than clearing shared state. + */ +let keyCounter = 0 +function uniqueNs(): string { + keyCounter += 1 + return `ns-${keyCounter}-${Date.now()}` +} + +describe('getCachedProviderClient', () => { + it('returns the SAME instance for an identical key and runs the factory once (memoized)', () => { + const key = `anthropic::${uniqueNs()}::default` + const factory = makeFactory() + + const first = getCachedProviderClient(key, factory) + const second = getCachedProviderClient(key, factory) + + expect(second).toBe(first) + expect(factory).toHaveBeenCalledTimes(1) + }) + + it('returns a DIFFERENT instance for a different apiKey (tenant isolation)', () => { + const ns = uniqueNs() + const factoryA = makeFactory() + const factoryB = makeFactory() + + const tenantA = getCachedProviderClient(`anthropic::${ns}-tenant-a::default`, factoryA) + const tenantB = getCachedProviderClient(`anthropic::${ns}-tenant-b::default`, factoryB) + + expect(tenantB).not.toBe(tenantA) + expect(factoryA).toHaveBeenCalledTimes(1) + expect(factoryB).toHaveBeenCalledTimes(1) + }) + + it('namespaces by provider: the same apiKey under different provider prefixes does not collide', () => { + const ns = uniqueNs() + const apiKey = `${ns}-shared-key` + const anthropicFactory = makeFactory() + const bedrockFactory = makeFactory() + + const anthropicClient = getCachedProviderClient(`anthropic::${apiKey}`, anthropicFactory) + const bedrockClient = getCachedProviderClient(`bedrock::${apiKey}`, bedrockFactory) + + expect(bedrockClient).not.toBe(anthropicClient) + }) + + it('treats every distinct key dimension as a distinct client', () => { + const ns = uniqueNs() + const base = `azure-anthropic::${ns}-key::https://a.example.com::2023-06-01::10.0.0.1::default` + const baseFactory = makeFactory() + const baseClient = getCachedProviderClient(base, baseFactory) + + const variants = [ + `azure-anthropic::${ns}-key::https://b.example.com::2023-06-01::10.0.0.1::default`, + `azure-anthropic::${ns}-key::https://a.example.com::2024-10-22::10.0.0.1::default`, + `azure-anthropic::${ns}-key::https://a.example.com::2023-06-01::10.0.0.2::default`, + `azure-anthropic::${ns}-key::https://a.example.com::2023-06-01::no-pin::default`, + `azure-anthropic::${ns}-key::https://a.example.com::2023-06-01::10.0.0.1::beta`, + ] + + for (const key of variants) { + const factory = makeFactory() + const client = getCachedProviderClient(key, factory) + expect(client).not.toBe(baseClient) + expect(factory).toHaveBeenCalledTimes(1) + } + }) + + it('evicts the least-recently-used entry once the cache cap is exceeded', () => { + const ns = uniqueNs() + const CAP = 1_000 + + const oldestKey = `evict::${ns}::0` + const oldestFactory = makeFactory() + getCachedProviderClient(oldestKey, oldestFactory) + expect(oldestFactory).toHaveBeenCalledTimes(1) + + // Fill the remaining capacity, then push one past the cap. The oldest key has + // not been touched since insertion, so it is the LRU eviction victim. + for (let i = 1; i <= CAP; i += 1) { + getCachedProviderClient(`evict::${ns}::${i}`, makeFactory()) + } + + const reFactory = makeFactory() + getCachedProviderClient(oldestKey, reFactory) + expect(reFactory).toHaveBeenCalledTimes(1) + expect(oldestFactory).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/providers/client-cache.ts b/apps/sim/providers/client-cache.ts new file mode 100644 index 0000000000..7908a94d21 --- /dev/null +++ b/apps/sim/providers/client-cache.ts @@ -0,0 +1,36 @@ +import { LRUCache } from 'lru-cache' + +const CLIENT_CACHE_MAX_ENTRIES = 1_000 +const CLIENT_CACHE_TTL_MS = 30 * 60 * 1_000 + +/** + * `updateAgeOnGet` makes the TTL idle-based: a continuously-used client keeps its + * warm keep-alive connections, while idle keys age out. + */ +const clientCache = new LRUCache({ + max: CLIENT_CACHE_MAX_ENTRIES, + ttl: CLIENT_CACHE_TTL_MS, + updateAgeOnGet: true, +}) + +/** + * Memoizes provider SDK clients so connections stay warm across requests rather + * than re-handshaking per call. The key must be namespaced per provider and + * encode every input that varies the client; the API key is always part of it, + * making it the tenant boundary (clients are never shared across keys). + */ +export function getCachedProviderClient(key: string, factory: () => T): T { + const existing = clientCache.get(key) + if (existing) { + return existing as T + } + + const client = factory() + clientCache.set(key, client) + return client +} + +/** Clears the cache so tests asserting client construction start from a miss. */ +export function clearProviderClientCacheForTests(): void { + clientCache.clear() +} diff --git a/apps/sim/providers/vllm/index.test.ts b/apps/sim/providers/vllm/index.test.ts index 8739c95f98..925c61ca2d 100644 --- a/apps/sim/providers/vllm/index.test.ts +++ b/apps/sim/providers/vllm/index.test.ts @@ -79,6 +79,7 @@ vi.mock('@/stores/providers', () => ({ useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, })) +import { clearProviderClientCacheForTests } from '@/providers/client-cache' import type { ProviderToolConfig } from '@/providers/types' import { vllmProvider } from '@/providers/vllm/index' @@ -117,6 +118,7 @@ const createPayload = (callIndex: number) => mockCreate.mock.calls[callIndex][0] describe('vllmProvider', () => { beforeEach(() => { vi.clearAllMocks() + clearProviderClientCacheForTests() openAIArgs.length = 0 envState.VLLM_BASE_URL = 'http://localhost:8000' envState.VLLM_API_KEY = undefined diff --git a/apps/sim/providers/vllm/index.ts b/apps/sim/providers/vllm/index.ts index 572b5df51c..936bf3af63 100644 --- a/apps/sim/providers/vllm/index.ts +++ b/apps/sim/providers/vllm/index.ts @@ -7,6 +7,7 @@ import { createPinnedFetch, validateUrlWithDNS } from '@/lib/core/security/input import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { formatMessagesForProvider } from '@/providers/attachments' +import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { createStreamingExecution } from '@/providers/streaming-execution' import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' @@ -114,6 +115,7 @@ export const vllmProvider: ProviderConfig = { * IP blocklist and blocked-port checks still apply, so SSRF protection is intact. */ let pinnedFetch: typeof fetch | undefined + let pinnedIP: string | undefined if (userProvidedEndpoint) { const validation = await validateUrlWithDNS(userProvidedEndpoint, 'vLLM endpoint', { allowHttp: true, @@ -128,15 +130,20 @@ export const vllmProvider: ProviderConfig = { if (!validation.resolvedIP) { throw new Error('Invalid vLLM endpoint: could not resolve a pinnable IP address') } - pinnedFetch = createPinnedFetch(validation.resolvedIP) + pinnedIP = validation.resolvedIP + pinnedFetch = createPinnedFetch(pinnedIP) } const apiKey = request.apiKey || env.VLLM_API_KEY || 'empty' - const vllm = new OpenAI({ - apiKey, - baseURL: `${baseUrl}/v1`, - ...(pinnedFetch ? { fetch: pinnedFetch } : {}), - }) + const vllm = getCachedProviderClient( + `vllm::${apiKey}::${baseUrl}::${pinnedIP ?? 'no-pin'}`, + () => + new OpenAI({ + apiKey, + baseURL: `${baseUrl}/v1`, + ...(pinnedFetch ? { fetch: pinnedFetch } : {}), + }) + ) const allMessages: Message[] = [] From feca5fa6f63cbd3d102870c300aee663182afa57 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Jun 2026 14:22:59 -0700 Subject: [PATCH 04/26] improvement(execution, connectors): offload large function inputs, increase connector limits + better error propagation (#5089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(execution,connectors): offload large function inputs; harden KB connector size limits Addresses a class of 10 MB limit failures: - executor/variables: offload over-budget function block-output context values to durable large-value refs (lazy `sim.values.read`) so JS function blocks can merge medium files without exceeding the 10 MB inter-block request-body cap. - connectors: stream downloads via `readBodyWithLimit` (memory-safe), and surface oversized files as visible `failed` KB documents instead of silently dropping them — listing-time for github/s3/dropbox/onedrive/sharepoint, fetch-time for gitlab/azure/google-drive via a shared `ConnectorFileTooLargeError`. Raise the per-file cap from a hardcoded 10 MB to the canonical 100 MB KB document limit (`CONNECTOR_MAX_FILE_BYTES`), except Google Drive's export path (Google's hard 10 MB export-API limit). - sync-engine: `classifyExternalDoc` + bulk `skipDocuments` (failed rows with a reason, excluded from retry), byte-bounded batch concurrency to cap peak worker memory at the raised cap, and a `metadata.fileSize ?? size` fallback. * fix zoom * update skill * address comments + fix terminal event in sse stream * fix accounting issue --- .agents/skills/memory-load-check/SKILL.md | 26 ++ .../app/api/workflows/[id]/execute/route.ts | 28 +- .../connectors/azure-devops/azure-devops.ts | 34 ++- apps/sim/connectors/dropbox/dropbox.ts | 85 ++++-- apps/sim/connectors/github/github.ts | 51 +++- apps/sim/connectors/gitlab/gitlab.ts | 35 ++- .../connectors/google-drive/google-drive.ts | 48 +++- apps/sim/connectors/onedrive/onedrive.ts | 54 ++-- apps/sim/connectors/s3/s3.ts | 56 ++-- apps/sim/connectors/sharepoint/sharepoint.ts | 64 +++-- apps/sim/connectors/types.ts | 7 + apps/sim/connectors/utils.test.ts | 176 ++++++++++++ apps/sim/connectors/utils.ts | 108 +++++++ apps/sim/connectors/zoom/zoom.ts | 49 +++- apps/sim/executor/variables/resolver.test.ts | 151 +++++++++- apps/sim/executor/variables/resolver.ts | 157 ++++++++++- .../lib/execution/payloads/large-value-ref.ts | 2 +- .../knowledge/connectors/sync-engine.test.ts | 108 +++++++ .../lib/knowledge/connectors/sync-engine.ts | 264 ++++++++++++++++-- 19 files changed, 1340 insertions(+), 163 deletions(-) diff --git a/.agents/skills/memory-load-check/SKILL.md b/.agents/skills/memory-load-check/SKILL.md index 5a46fce78c..340f6b9757 100644 --- a/.agents/skills/memory-load-check/SKILL.md +++ b/.agents/skills/memory-load-check/SKILL.md @@ -49,10 +49,35 @@ Read these when doing a deeper pass: - cap downloads and parsed output separately - preserve partial results when a later item exceeds the cap - never read untrusted response bodies without a byte cap +- KB connector file downloads in `apps/sim/connectors/utils.ts` + - `CONNECTOR_MAX_FILE_BYTES`: shared per-file cap (aligned with the manual KB upload limit) + - `readBodyWithLimit`: stream a download body to a Buffer with a hard byte cap (null on overflow) + - `stubOrSkipBySize`: listing-time skip when the reported size exceeds the cap + - `markSkipped` / `sizeLimitSkipReason`: surface oversized files as failed (skipped) KB rows + - `ConnectorFileTooLargeError`: thrown mid-download when the listing under-reported size - Large workflow value payloads - prefer durable references/manifests over inlining large arrays or files - materialize refs only behind an explicit byte budget +## KB Connector File Size Handling + +The connector size pattern in `apps/sim/connectors/utils.ts` (`CONNECTOR_MAX_FILE_BYTES` + `readBodyWithLimit` + `stubOrSkipBySize`/`markSkipped`) exists for one risk: a knowledge-base connector downloading **arbitrary, user-controlled file bytes** that the source does not hard-cap. Apply it by that risk, not by the connector's name. + +Use the pattern when the connector downloads file content via a stream/`download_url` where the user controls the size: +- file-storage connectors: Dropbox, OneDrive, SharePoint, Google Drive, S3, GitHub, GitLab, Azure DevOps +- any connector that fetches a file via a download URL even if it is not a "storage" service (e.g. the Zoom transcript `.vtt`) + +For those, require all three: +- stream the body with `readBodyWithLimit(resp, CONNECTOR_MAX_FILE_BYTES)` — never raw `response.text()`/`response.arrayBuffer()` +- skip oversize at listing (`stubOrSkipBySize` with the reported size) and again at fetch time (overflow -> `markSkipped`), since the listing size can be missing or under-reported +- never drop/truncate silently — oversized files become content-less failed rows carrying `skippedReason`, so they stay visible in the KB UI instead of vanishing from the index + +Skip the pattern when the source already bounds the payload: +- pure API/structured-data connectors (Jira, Linear, Notion, Confluence, Sentry, Slack, Zendesk, Gmail, ...) — paginated JSON/text; apply normal pagination + concurrency bounds instead of a per-file byte cap +- native-document connectors capped by the platform (Google Docs ~50 MB, Google Sheets via `MAX_ROWS`, Evernote ~25 MB/note) — a 100 MB cap can never fire, and wrapping a `response.json()`/Thrift parse in `readBodyWithLimit` is cargo-culting + +Litmus test: "Can a user make this one fetch arbitrarily large, with nothing upstream stopping it?" Yes -> use the pattern. No (platform hard-cap, or already paginated) -> a per-file byte cap adds noise, not safety. Borderline: a user-configured/self-hosted endpoint with no platform cap (e.g. Obsidian) — bound it only if the content is genuinely unbounded. + ## Review Workflow 1. Identify every changed data source: @@ -96,6 +121,7 @@ Read these when doing a deeper pass: - fetches all pages from an external API before processing - reads an entire file, HTTP response, or stream without a max byte budget - checks size only after `Buffer.concat`, `arrayBuffer`, `text`, `JSON.parse`, or parse expansion +- a KB connector silently drops or truncates an oversized file instead of recording it as a failed (skipped) row - chunks only after loading the complete dataset - paginates with unbounded/deep `OFFSET` on a mutable or large table - creates one queue job per row without batching or a queue-level concurrency key diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 0ac748e12e..8f3007b5c4 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -1235,12 +1235,28 @@ async function handleExecutePost( const isBuffered = event.type !== 'stream:chunk' && event.type !== 'stream:done' let eventToSend = event if (isBuffered) { - const entry = terminalStatus - ? await eventWriter.writeTerminal(event, terminalStatus) - : await eventWriter.write(event) - eventToSend = entry.event - eventToSend.eventId = entry.eventId - terminalEventPublished ||= Boolean(terminalStatus) + try { + const entry = terminalStatus + ? await eventWriter.writeTerminal(event, terminalStatus) + : await eventWriter.write(event) + eventToSend = entry.event + eventToSend.eventId = entry.eventId + terminalEventPublished ||= Boolean(terminalStatus) + } catch (e) { + // The event buffer (Redis replay store) rejected this event — e.g. the flush + // batch exceeds the per-write byte cap for large block outputs. The buffer only + // backs reconnect/replay; the live SSE stream is the primary delivery. Fall + // through to enqueue the event live (below) instead of throwing, so terminal + // events still reach the active client and the UI doesn't hang on "running". + // Marking a terminal event delivered-live as published lets finalization close + // the stream cleanly instead of aborting it with controller.error(). + reqLogger.warn('Event buffer write failed; delivering event over live stream only', { + eventType: event.type, + terminal: Boolean(terminalStatus), + error: toError(e).message, + }) + terminalEventPublished ||= Boolean(terminalStatus) + } } if (!isStreamClosed) { try { diff --git a/apps/sim/connectors/azure-devops/azure-devops.ts b/apps/sim/connectors/azure-devops/azure-devops.ts index 6f9eae611c..7351709a90 100644 --- a/apps/sim/connectors/azure-devops/azure-devops.ts +++ b/apps/sim/connectors/azure-devops/azure-devops.ts @@ -3,7 +3,15 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { azureDevopsConnectorMeta } from '@/connectors/azure-devops/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, joinTagArray, parseTagDate, readBodyWithLimit } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + htmlToPlainText, + joinTagArray, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, +} from '@/connectors/utils' const logger = createLogger('AzureDevOpsConnector') @@ -30,7 +38,7 @@ const FILE_BATCH_SIZE = 100 * and aborts (returning null) the moment the cap is exceeded. Larger files are * skipped without being fully buffered. */ -const MAX_FILE_SIZE = 10 * 1024 * 1024 +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES /** Bytes sniffed for a NUL byte when detecting binary files (matches git's heuristic). */ const BINARY_SNIFF_BYTES = 8000 /** @@ -1090,7 +1098,27 @@ async function getFileDocument( const buffer = await readBodyWithLimit(contentResponse, MAX_FILE_SIZE) if (buffer === null) { logger.info('Skipping oversized Azure DevOps file', { path }) - return null + const skippedTitle = path.split('/').filter(Boolean).pop() || path + return markSkipped( + { + externalId, + title: skippedTitle, + content: '', + mimeType: 'text/plain', + sourceUrl: buildFileSourceUrl(repo?.webUrl, branch, path), + contentHash: buildFileContentHash(repoId, item.objectId), + metadata: { + kind: 'file', + organization, + project, + repository: repo?.name ?? '', + repositoryId: repoId, + branch, + path, + }, + }, + sizeLimitSkipReason(MAX_FILE_SIZE) + ) } if (isBinaryBuffer(buffer)) { logger.info('Skipping binary Azure DevOps file', { path }) diff --git a/apps/sim/connectors/dropbox/dropbox.ts b/apps/sim/connectors/dropbox/dropbox.ts index 4b25be7948..e4bad7b40d 100644 --- a/apps/sim/connectors/dropbox/dropbox.ts +++ b/apps/sim/connectors/dropbox/dropbox.ts @@ -3,7 +3,18 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { dropboxConnectorMeta } from '@/connectors/dropbox/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + ConnectorFileTooLargeError, + htmlToPlainText, + isSkippedDocument, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' const logger = createLogger('DropboxConnector') @@ -23,7 +34,7 @@ const SUPPORTED_EXTENSIONS = new Set([ '.tsv', ]) -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES interface DropboxFileEntry { '.tag': 'file' | 'folder' | 'deleted' @@ -44,16 +55,18 @@ interface DropboxListFolderResponse { has_more: boolean } -function isSupportedFile(entry: DropboxFileEntry): boolean { - if (entry['.tag'] !== 'file') return false - if (entry.is_downloadable === false) return false - if (entry.size && entry.size > MAX_FILE_SIZE) return false - - const name = entry.name.toLowerCase() - const dotIndex = name.lastIndexOf('.') +function hasSupportedExtension(name: string): boolean { + const lower = name.toLowerCase() + const dotIndex = lower.lastIndexOf('.') if (dotIndex === -1) return false + return SUPPORTED_EXTENSIONS.has(lower.slice(dotIndex)) +} - return SUPPORTED_EXTENSIONS.has(name.slice(dotIndex)) +/** A downloadable file with a supported extension, regardless of size. */ +function isDownloadableFile(entry: DropboxFileEntry): boolean { + return ( + entry['.tag'] === 'file' && entry.is_downloadable !== false && hasSupportedExtension(entry.name) + ) } async function downloadFileContent(accessToken: string, filePath: string): Promise { @@ -69,7 +82,15 @@ async function downloadFileContent(accessToken: string, filePath: string): Promi throw new Error(`Failed to download file ${filePath}: ${response.status}`) } - const text = await response.text() + // Stream with a hard byte cap so a file whose listing metadata under-reported + // (or omitted) its size can never be fully buffered into memory. Oversize raises + // so getDocument can surface it as a skipped (failed) row rather than dropping it. + const buffer = await readBodyWithLimit(response, MAX_FILE_SIZE) + if (!buffer) { + throw new ConnectorFileTooLargeError(MAX_FILE_SIZE) + } + + const text = buffer.toString('utf8') if (filePath.endsWith('.html') || filePath.endsWith('.htm')) { return htmlToPlainText(text) @@ -162,23 +183,27 @@ export const dropboxConnector: ConnectorConfig = { data = await response.json() } - const supportedFiles = data.entries.filter(isSupportedFile) + // Keep oversized files and surface them as skipped (failed) documents instead + // of dropping them silently at listing time. + const candidateFiles = data.entries.filter(isDownloadableFile) const maxFiles = sourceConfig.maxFiles ? Number(sourceConfig.maxFiles) : 0 const previouslyFetched = (syncContext?.totalDocsFetched as number) ?? 0 - let documents = supportedFiles.map(fileToStub) + const stubs = candidateFiles.map((entry) => + stubOrSkipBySize(fileToStub(entry), entry.size, MAX_FILE_SIZE) + ) - if (maxFiles > 0) { - const remaining = maxFiles - previouslyFetched - if (documents.length > remaining) { - documents = documents.slice(0, remaining) - } - } + const { documents, indexableCount, capReached } = takeIndexableWithinCap( + stubs, + isSkippedDocument, + maxFiles, + previouslyFetched + ) - const totalFetched = previouslyFetched + documents.length + const totalFetched = previouslyFetched + indexableCount if (syncContext) syncContext.totalDocsFetched = totalFetched - const hitLimit = maxFiles > 0 && totalFetched >= maxFiles + const hitLimit = capReached if (hitLimit && syncContext) syncContext.listingCapped = true return { @@ -210,12 +235,24 @@ export const dropboxConnector: ConnectorConfig = { const entry = (await response.json()) as DropboxFileEntry - if (!isSupportedFile(entry)) return null + if (!isDownloadableFile(entry)) return null - const content = await downloadFileContent(accessToken, entry.path_lower) + const stub = fileToStub(entry) + if (entry.size && entry.size > MAX_FILE_SIZE) { + return markSkipped(stub, sizeLimitSkipReason(MAX_FILE_SIZE)) + } + + let content: string + try { + content = await downloadFileContent(accessToken, entry.path_lower) + } catch (error) { + if (error instanceof ConnectorFileTooLargeError) { + return markSkipped(stub, sizeLimitSkipReason(error.limitBytes)) + } + throw error + } if (!content.trim()) return null - const stub = fileToStub(entry) return { ...stub, content, contentDeferred: false } } catch (error) { logger.warn(`Failed to fetch document ${externalId}`, { diff --git a/apps/sim/connectors/github/github.ts b/apps/sim/connectors/github/github.ts index e90b51a932..2509ecb0df 100644 --- a/apps/sim/connectors/github/github.ts +++ b/apps/sim/connectors/github/github.ts @@ -3,14 +3,21 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { githubConnectorMeta } from '@/connectors/github/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + markSkipped, + parseTagDate, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' const logger = createLogger('GitHubConnector') const GITHUB_API_URL = 'https://api.github.com' const BATCH_SIZE = 30 const GIT_SHA_PREFIX = 'git-sha:' -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES const BINARY_SNIFF_BYTES = 8000 /** @@ -197,16 +204,25 @@ export const githubConnector: ConnectorConfig = { } else { const tree = await fetchTree(accessToken, owner, repo, branch) - // Filter by path prefix, extensions, and size + // Filter by path prefix and extensions. Oversized files are kept here and + // surfaced as skipped (failed) documents at stub time so they stay visible. const filtered = tree.filter((item) => { if (pathPrefix && !item.path.startsWith(pathPrefix)) return false if (!matchesExtension(item.path, extSet)) return false - if (typeof item.size === 'number' && item.size > MAX_FILE_SIZE) return false return true }) - // Apply max files limit - capped = maxFiles > 0 ? filtered.slice(0, maxFiles) : filtered + // Apply the max-files limit to indexable files only; oversized files within + // the capped window are kept (and surfaced as skipped) but never consume the cap. + capped = + maxFiles > 0 + ? takeIndexableWithinCap( + filtered, + (item) => Boolean(item.size && item.size > MAX_FILE_SIZE), + maxFiles, + 0 + ).documents + : filtered if (syncContext) syncContext.filteredTree = capped } @@ -223,7 +239,9 @@ export const githubConnector: ConnectorConfig = { batchSize: batch.length, }) - const documents = batch.map((item) => treeItemToStub(owner, repo, branch, item)) + const documents = batch.map((item) => + stubOrSkipBySize(treeItemToStub(owner, repo, branch, item), item.size, MAX_FILE_SIZE) + ) const nextOffset = offset + BATCH_SIZE const hasMore = nextOffset < capped.length @@ -281,7 +299,24 @@ export const githubConnector: ConnectorConfig = { size, limit: MAX_FILE_SIZE, }) - return null + return markSkipped( + { + externalId, + title: path.split('/').pop() || path, + content: '', + mimeType: 'text/plain', + sourceUrl: `https://github.com/${owner}/${repo}/blob/${branch.split('/').map(encodeURIComponent).join('/')}/${path.split('/').map(encodeURIComponent).join('/')}`, + contentHash: `${GIT_SHA_PREFIX}${data.sha as string}`, + metadata: { + path, + sha: data.sha as string, + size, + branch, + repository: `${owner}/${repo}`, + }, + }, + sizeLimitSkipReason(MAX_FILE_SIZE) + ) } const rawContent = (data.content as string) || '' diff --git a/apps/sim/connectors/gitlab/gitlab.ts b/apps/sim/connectors/gitlab/gitlab.ts index ed22010ad6..1824721400 100644 --- a/apps/sim/connectors/gitlab/gitlab.ts +++ b/apps/sim/connectors/gitlab/gitlab.ts @@ -6,14 +6,21 @@ import { secureFetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.ser import { VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { gitlabConnectorMeta } from '@/connectors/gitlab/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { computeContentHash, joinTagArray, parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + computeContentHash, + joinTagArray, + markSkipped, + parseTagDate, + sizeLimitSkipReason, +} from '@/connectors/utils' const logger = createLogger('GitLabConnector') const DEFAULT_HOST = 'gitlab.com' const PAGE_SIZE = 100 /** Max repository file size to index. Larger blobs are skipped. */ -const MAX_FILE_SIZE = 10 * 1024 * 1024 +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES /** Bytes sniffed for NUL when detecting binary files (matches git's heuristic). */ const BINARY_SNIFF_BYTES = 8000 @@ -324,9 +331,25 @@ function fileToDocument( const blobSha = file.blob_id?.trim() if (!blobSha) return null + const title = path.split('/').pop() || path + const skippedForSize = (size: number): ExternalDocument => { + logger.info('Skipping oversized GitLab file', { path, size }) + return markSkipped( + { + externalId: `${FILE_PREFIX}${path}`, + title, + content: '', + mimeType: 'text/plain', + sourceUrl: buildFileSourceUrl(apiBase, encodedProject, host, projectPath, ref, path), + contentHash: buildFileContentHash(encodedProject, path, blobSha), + metadata: { contentType: 'file', title, path, size }, + }, + sizeLimitSkipReason(MAX_FILE_SIZE) + ) + } + if (typeof file.size === 'number' && file.size > MAX_FILE_SIZE) { - logger.info('Skipping oversized GitLab file', { path, size: file.size }) - return null + return skippedForSize(file.size) } const raw = typeof file.content === 'string' ? file.content : '' @@ -336,12 +359,10 @@ function fileToDocument( return null } if (buffer.byteLength > MAX_FILE_SIZE) { - logger.info('Skipping oversized GitLab file', { path, size: buffer.byteLength }) - return null + return skippedForSize(buffer.byteLength) } const content = buffer.toString('utf8') - const title = path.split('/').pop() || path const body = composeBody(title, content) if (!body.trim()) return null diff --git a/apps/sim/connectors/google-drive/google-drive.ts b/apps/sim/connectors/google-drive/google-drive.ts index 775bebe142..14d98bc413 100644 --- a/apps/sim/connectors/google-drive/google-drive.ts +++ b/apps/sim/connectors/google-drive/google-drive.ts @@ -3,7 +3,16 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + ConnectorFileTooLargeError, + htmlToPlainText, + joinTagArray, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, +} from '@/connectors/utils' const logger = createLogger('GoogleDriveConnector') @@ -22,7 +31,9 @@ const SUPPORTED_TEXT_MIME_TYPES = [ 'application/xml', ] -const MAX_EXPORT_SIZE = 10 * 1024 * 1024 // 10 MB (Google export limit) +// Google Drive's `files.export` API rejects exports over 10 MB (exportSizeLimitExceeded), +// so this is a hard external limit for Google Workspace docs — not the connector cap. +const MAX_EXPORT_SIZE = 10 * 1024 * 1024 function isGoogleWorkspaceFile(mimeType: string): boolean { return mimeType in GOOGLE_WORKSPACE_MIME_TYPES @@ -50,10 +61,22 @@ async function exportGoogleWorkspaceFile( }) if (!response.ok) { + // Google rejects exports over its 10 MB limit with a 403 exportSizeLimitExceeded + // before streaming any bytes — surface that as an oversize skip, not a hard error. + if (response.status === 403) { + const body = await response.text().catch(() => '') + if (body.includes('exportSizeLimitExceeded')) { + throw new ConnectorFileTooLargeError(MAX_EXPORT_SIZE) + } + } throw new Error(`Failed to export file ${fileId}: ${response.status}`) } - return response.text() + const buffer = await readBodyWithLimit(response, MAX_EXPORT_SIZE) + if (!buffer) { + throw new ConnectorFileTooLargeError(MAX_EXPORT_SIZE) + } + return buffer.toString('utf8') } async function downloadTextFile(accessToken: string, fileId: string): Promise { @@ -68,15 +91,14 @@ async function downloadTextFile(accessToken: string, fileId: string): Promise MAX_EXPORT_SIZE) { - logger.warn(`File exceeds ${MAX_EXPORT_SIZE} bytes, truncating`) - const buf = Buffer.from(text, 'utf8') - let end = MAX_EXPORT_SIZE - while (end > 0 && (buf[end] & 0xc0) === 0x80) end-- - return buf.subarray(0, end).toString('utf8') + // Stream with a hard byte cap so a file with missing/under-reported listing + // size metadata is never fully buffered into memory. Oversized files raise + // DriveFileTooLargeError so getDocument can surface them as skipped (failed) rows. + const buffer = await readBodyWithLimit(response, CONNECTOR_MAX_FILE_BYTES) + if (!buffer) { + throw new ConnectorFileTooLargeError(CONNECTOR_MAX_FILE_BYTES) } - return text + return buffer.toString('utf8') } async function fetchFileContent( @@ -287,6 +309,10 @@ export const googleDriveConnector: ConnectorConfig = { const stub = fileToStub(file) return { ...stub, content, contentDeferred: false } } catch (error) { + if (error instanceof ConnectorFileTooLargeError) { + logger.info('Skipping oversized Google Drive file', { fileId: file.id, name: file.name }) + return markSkipped(fileToStub(file), sizeLimitSkipReason(error.limitBytes)) + } logger.warn(`Failed to fetch content for file: ${file.name} (${file.id})`, { error: toError(error).message, }) diff --git a/apps/sim/connectors/onedrive/onedrive.ts b/apps/sim/connectors/onedrive/onedrive.ts index 63dfb0d2fa..5cb5316a72 100644 --- a/apps/sim/connectors/onedrive/onedrive.ts +++ b/apps/sim/connectors/onedrive/onedrive.ts @@ -3,7 +3,18 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { onedriveConnectorMeta } from '@/connectors/onedrive/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + ConnectorFileTooLargeError, + htmlToPlainText, + isSkippedDocument, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' const logger = createLogger('OneDriveConnector') @@ -22,7 +33,7 @@ const SUPPORTED_EXTENSIONS = new Set([ '.tsv', ]) -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES const GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0' @@ -71,11 +82,11 @@ async function downloadFileContent(accessToken: string, fileId: string): Promise throw new Error(`Failed to download file ${fileId}: ${response.status}`) } - const text = await response.text() - if (Buffer.byteLength(text, 'utf8') > MAX_FILE_SIZE) { - return Buffer.from(text, 'utf8').subarray(0, MAX_FILE_SIZE).toString('utf8') + const buffer = await readBodyWithLimit(response, MAX_FILE_SIZE) + if (!buffer) { + throw new ConnectorFileTooLargeError(MAX_FILE_SIZE) } - return text + return buffer.toString('utf8') } /** @@ -197,26 +208,27 @@ export const onedriveConnector: ConnectorConfig = { } } - const textFiles = items.filter( - (item) => - item.file && isSupportedTextFile(item.name) && (!item.size || item.size <= MAX_FILE_SIZE) - ) + // Keep oversized files and surface them as skipped (failed) documents instead + // of filtering them out silently. + const supportedFiles = items.filter((item) => item.file && isSupportedTextFile(item.name)) const maxFiles = sourceConfig.maxFiles ? Number(sourceConfig.maxFiles) : 0 const previouslyFetched = (syncContext?.totalDocsFetched as number) ?? 0 - let documents = textFiles.map(fileToStub) + const stubs = supportedFiles.map((item) => + stubOrSkipBySize(fileToStub(item), item.size, MAX_FILE_SIZE) + ) - if (maxFiles > 0) { - const remaining = maxFiles - previouslyFetched - if (documents.length > remaining) { - documents = documents.slice(0, remaining) - } - } + const { documents, indexableCount, capReached } = takeIndexableWithinCap( + stubs, + isSkippedDocument, + maxFiles, + previouslyFetched + ) - const totalFetched = previouslyFetched + documents.length + const totalFetched = previouslyFetched + indexableCount if (syncContext) syncContext.totalDocsFetched = totalFetched - const hitLimit = maxFiles > 0 && totalFetched >= maxFiles + const hitLimit = capReached if (hitLimit && syncContext) syncContext.listingCapped = true const nextLink = data['@odata.nextLink'] @@ -275,6 +287,10 @@ export const onedriveConnector: ConnectorConfig = { const stub = fileToStub(item) return { ...stub, content, contentDeferred: false } } catch (error) { + if (error instanceof ConnectorFileTooLargeError) { + logger.info('Skipping oversized OneDrive file', { fileId: item.id, name: item.name }) + return markSkipped(fileToStub(item), sizeLimitSkipReason(error.limitBytes)) + } logger.warn(`Failed to fetch content for file: ${item.name} (${item.id})`, { error: toError(error).message, }) diff --git a/apps/sim/connectors/s3/s3.ts b/apps/sim/connectors/s3/s3.ts index 42e2766752..8f92d0687d 100644 --- a/apps/sim/connectors/s3/s3.ts +++ b/apps/sim/connectors/s3/s3.ts @@ -6,13 +6,22 @@ import { secureFetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.ser import { VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { s3ConnectorMeta } from '@/connectors/s3/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { parseTagDate, readBodyWithLimit } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + isSkippedDocument, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' import { encodeS3PathComponent, getSignatureKey } from '@/tools/s3/utils' const logger = createLogger('S3Connector') /** Maximum object size to sync. Larger objects are skipped during listing. */ -const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB +const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES /** Number of objects requested per ListObjectsV2 page (S3 caps at 1000). */ const LIST_MAX_KEYS = 1000 @@ -509,23 +518,21 @@ export const s3Connector: ConnectorConfig = { cursor ) - let documents = objects - .filter((entry) => isSupportedKey(entry.key, allowedExtensions)) - .filter((entry) => entry.size > 0 && entry.size <= MAX_FILE_SIZE) - .map((entry) => objectToStub(ctx, entry)) - - let slicedSome = false - if (maxObjects > 0) { - const remaining = maxObjects - previouslyFetched - if (documents.length > remaining) { - slicedSome = true - documents = documents.slice(0, remaining) - } - } + const stubs = objects + .filter((entry) => isSupportedKey(entry.key, allowedExtensions) && entry.size > 0) + .map((entry) => stubOrSkipBySize(objectToStub(ctx, entry), entry.size, MAX_FILE_SIZE)) + + const { documents, indexableCount, capReached } = takeIndexableWithinCap( + stubs, + isSkippedDocument, + maxObjects, + previouslyFetched + ) + const slicedSome = documents.length < stubs.length - const totalFetched = previouslyFetched + documents.length + const totalFetched = previouslyFetched + indexableCount if (syncContext) syncContext.totalDocsFetched = totalFetched - const hitLimit = maxObjects > 0 && totalFetched >= maxObjects + const hitLimit = capReached const moreAvailable = slicedSome || (isTruncated && Boolean(nextContinuationToken)) if (hitLimit && moreAvailable && syncContext) syncContext.listingCapped = true @@ -563,13 +570,24 @@ export const s3Connector: ConnectorConfig = { if (declaredLength > MAX_FILE_SIZE) { logger.warn('Skipping oversized S3 object', { key, size: declaredLength }) - return null + return markSkipped( + objectToStub(ctx, { key, etag, lastModified, size: declaredLength }), + sizeLimitSkipReason(MAX_FILE_SIZE) + ) } const body = await readBodyWithLimit(response, MAX_FILE_SIZE) if (body === null) { logger.warn('Skipping oversized S3 object (size cap exceeded while streaming)', { key }) - return null + return markSkipped( + objectToStub(ctx, { + key, + etag, + lastModified, + size: Number.isFinite(declaredLength) ? declaredLength : 0, + }), + sizeLimitSkipReason(MAX_FILE_SIZE) + ) } const content = body.toString('utf-8') if (!content.trim()) return null diff --git a/apps/sim/connectors/sharepoint/sharepoint.ts b/apps/sim/connectors/sharepoint/sharepoint.ts index 04b64b9a90..8d476c9b28 100644 --- a/apps/sim/connectors/sharepoint/sharepoint.ts +++ b/apps/sim/connectors/sharepoint/sharepoint.ts @@ -3,7 +3,18 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { sharepointConnectorMeta } from '@/connectors/sharepoint/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + ConnectorFileTooLargeError, + htmlToPlainText, + isSkippedDocument, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' const logger = createLogger('SharePointConnector') @@ -24,7 +35,7 @@ const SUPPORTED_TEXT_EXTENSIONS = new Set([ '.tsv', ]) -const MAX_DOWNLOAD_SIZE = 10 * 1024 * 1024 // 10 MB +const MAX_DOWNLOAD_SIZE = CONNECTOR_MAX_FILE_BYTES /** Microsoft Graph drive item shape (subset of fields we use). */ interface DriveItem { @@ -133,15 +144,14 @@ async function downloadFileContent( throw new Error(`Failed to download file "${fileName}" (${itemId}): ${response.status}`) } - const text = await response.text() - if (Buffer.byteLength(text, 'utf8') > MAX_DOWNLOAD_SIZE) { - logger.warn(`File "${fileName}" exceeds ${MAX_DOWNLOAD_SIZE} bytes, truncating`) - const buf = Buffer.from(text, 'utf8') - let end = MAX_DOWNLOAD_SIZE - while (end > 0 && (buf[end] & 0xc0) === 0x80) end-- - return buf.subarray(0, end).toString('utf8') + // Stream with a hard byte cap so a file with missing/under-reported listing + // size metadata is never fully buffered into memory. Oversized files are + // skipped (returned empty) rather than indexed as truncated partial content. + const buffer = await readBodyWithLimit(response, MAX_DOWNLOAD_SIZE) + if (!buffer) { + throw new ConnectorFileTooLargeError(MAX_DOWNLOAD_SIZE) } - return text + return buffer.toString('utf8') } /** @@ -341,11 +351,8 @@ export const sharepointConnector: ConnectorConfig = { for (const item of data.value) { if (item.folder) { subfolders.push(item.id) - } else if ( - item.file && - isSupportedTextFile(item.name) && - (!item.size || item.size <= MAX_DOWNLOAD_SIZE) - ) { + } else if (item.file && isSupportedTextFile(item.name)) { + // Keep oversized files; they are surfaced as skipped (failed) docs below. files.push(item) } } @@ -353,17 +360,23 @@ export const sharepointConnector: ConnectorConfig = { // Push subfolders onto the stack for depth-first traversal state.folderStack.push(...subfolders) - // Convert files to lightweight stubs (no content download) + // Convert files to lightweight stubs (no content download). Oversized files are + // kept as skipped stubs but do not consume the max-files cap. const previouslyFetched = totalFetched - for (const file of files) { - if (maxFiles > 0 && previouslyFetched + documents.length >= maxFiles) break - documents.push(itemToStub(file, siteName)) - } + const stubs = files.map((file) => + stubOrSkipBySize(itemToStub(file, siteName), file.size, MAX_DOWNLOAD_SIZE) + ) + const { + documents: pageDocuments, + indexableCount, + capReached, + } = takeIndexableWithinCap(stubs, isSkippedDocument, maxFiles, previouslyFetched) + documents.push(...pageDocuments) - totalFetched += documents.length + totalFetched += indexableCount if (syncContext) syncContext.totalDocsFetched = totalFetched - const hitLimit = maxFiles > 0 && totalFetched >= maxFiles + const hitLimit = capReached if (hitLimit && syncContext) syncContext.listingCapped = true if (hitLimit) { @@ -443,6 +456,13 @@ export const sharepointConnector: ConnectorConfig = { const stub = itemToStub(item, siteName ?? siteUrl) return { ...stub, content, contentDeferred: false } } catch (error) { + if (error instanceof ConnectorFileTooLargeError) { + logger.info('Skipping oversized SharePoint file', { fileId: item.id, name: item.name }) + return markSkipped( + itemToStub(item, siteName ?? siteUrl), + sizeLimitSkipReason(error.limitBytes) + ) + } logger.warn(`Failed to fetch content for file: ${item.name} (${item.id})`, { error: toError(error).message, }) diff --git a/apps/sim/connectors/types.ts b/apps/sim/connectors/types.ts index f25359d63f..c012a7ae56 100644 --- a/apps/sim/connectors/types.ts +++ b/apps/sim/connectors/types.ts @@ -28,6 +28,13 @@ export interface ExternalDocument { contentHash: string /** When true, content is empty and will be fetched via getDocument for new/changed docs only */ contentDeferred?: boolean + /** + * When set, the document was intentionally not indexed (e.g. it exceeds the + * connector's size limit). The sync engine records it as a `failed` document + * carrying this reason so it is visible in the knowledge base UI instead of + * being silently dropped. + */ + skippedReason?: string /** Additional source-specific metadata */ metadata?: Record } diff --git a/apps/sim/connectors/utils.test.ts b/apps/sim/connectors/utils.test.ts index 956f796cbd..3dca925df1 100644 --- a/apps/sim/connectors/utils.test.ts +++ b/apps/sim/connectors/utils.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { describe, expect, it, vi } from 'vitest' +import type { ExternalDocument } from '@/connectors/types' vi.mock('@/components/icons', () => ({ JiraIcon: () => null, @@ -59,6 +60,14 @@ import { rootlyConnector } from '@/connectors/rootly/rootly' import { s3Connector } from '@/connectors/s3/s3' import { sentryConnector } from '@/connectors/sentry/sentry' import { typeformConnector } from '@/connectors/typeform/typeform' +import { + ConnectorFileTooLargeError, + isSkippedDocument, + markSkipped, + readBodyWithLimit, + sizeLimitSkipReason, + takeIndexableWithinCap, +} from '@/connectors/utils' import { xConnector } from '@/connectors/x/x' import { youtubeConnector } from '@/connectors/youtube/youtube' @@ -1134,3 +1143,170 @@ describe('Rootly mapTags', () => { expect(result).toEqual({}) }) }) + +function streamResponse(chunks: Uint8Array[], onCancel?: () => void): Response { + let index = 0 + const stream = new ReadableStream({ + pull(controller) { + if (index < chunks.length) { + controller.enqueue(chunks[index++]) + } else { + controller.close() + } + }, + cancel() { + onCancel?.() + }, + }) + return new Response(stream) +} + +describe('readBodyWithLimit', () => { + it('returns the full buffer when the streamed body is within the cap', async () => { + const chunk = new Uint8Array(1024).fill(65) + const result = await readBodyWithLimit(streamResponse([chunk, chunk]), 4096) + expect(result).not.toBeNull() + expect(result?.byteLength).toBe(2048) + }) + + it('returns the buffer when the body is exactly at the cap', async () => { + const chunk = new Uint8Array(1024).fill(65) + const result = await readBodyWithLimit(streamResponse([chunk, chunk]), 2048) + expect(result?.byteLength).toBe(2048) + }) + + it('returns null and cancels the stream once the cap is exceeded', async () => { + const onCancel = vi.fn() + const chunk = new Uint8Array(1024).fill(65) + // Cap is 2048; the third 1KB chunk pushes the total to 3072 and trips the cap, + // so the remaining body is never buffered into memory. + const result = await readBodyWithLimit(streamResponse([chunk, chunk, chunk], onCancel), 2048) + expect(result).toBeNull() + expect(onCancel).toHaveBeenCalled() + }) + + it('enforces the cap on bodyless responses via the arrayBuffer fallback', async () => { + // double-cast-allowed: minimal response stub exercising the no-stream branch + const oversized = { + body: null, + arrayBuffer: async () => new Uint8Array(5000).buffer, + } as unknown as Response + expect(await readBodyWithLimit(oversized, 4096)).toBeNull() + + // double-cast-allowed: minimal response stub exercising the no-stream branch + const within = { + body: null, + arrayBuffer: async () => new Uint8Array(100).buffer, + } as unknown as Response + expect((await readBodyWithLimit(within, 4096))?.byteLength).toBe(100) + }) +}) + +describe('markSkipped', () => { + const stub: ExternalDocument = { + externalId: 'file-1', + title: 'big.csv', + content: 'should be cleared', + contentDeferred: true, + mimeType: 'text/csv', + sourceUrl: 'https://example.com/big.csv', + contentHash: 'hash-1', + metadata: { fileSize: 20_000_000, path: '/big.csv' }, + } + + it('clears content and flags the stub as skipped while preserving identity', () => { + const skipped = markSkipped(stub, sizeLimitSkipReason(10 * 1024 * 1024)) + expect(skipped.content).toBe('') + expect(skipped.contentDeferred).toBe(false) + expect(skipped.skippedReason).toBe('File exceeds the 10MB size limit and was not indexed') + // Identity/metadata preserved so change detection + tags still work. + expect(skipped.externalId).toBe('file-1') + expect(skipped.contentHash).toBe('hash-1') + expect(skipped.sourceUrl).toBe('https://example.com/big.csv') + expect(skipped.metadata).toEqual({ fileSize: 20_000_000, path: '/big.csv' }) + }) + + it('does not mutate the original stub', () => { + markSkipped(stub, 'too big') + expect(stub.content).toBe('should be cleared') + expect(stub.skippedReason).toBeUndefined() + }) +}) + +describe('ConnectorFileTooLargeError', () => { + it('carries the limit and is catchable by type', () => { + const error = new ConnectorFileTooLargeError(100 * 1024 * 1024) + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(ConnectorFileTooLargeError) + expect(error.limitBytes).toBe(100 * 1024 * 1024) + expect(error.message).toContain('100MB') + }) +}) + +describe('isSkippedDocument', () => { + const base: ExternalDocument = { + externalId: 'f1', + title: 'f1', + content: '', + contentDeferred: false, + mimeType: 'text/plain', + contentHash: 'h', + metadata: {}, + } + + it('is true only for a markSkipped stub', () => { + expect(isSkippedDocument(base)).toBe(false) + expect(isSkippedDocument(markSkipped(base, 'too big'))).toBe(true) + }) +}) + +describe('takeIndexableWithinCap', () => { + const skip = (id: number) => ({ id, skip: true }) + const file = (id: number) => ({ id, skip: false }) + const isSkip = (i: { skip: boolean }) => i.skip + + it('passes everything through when the cap is unlimited', () => { + const res = takeIndexableWithinCap([file(1), skip(2), file(3)], isSkip, 0, 0) + expect(res.documents).toHaveLength(3) + expect(res.indexableCount).toBe(2) + expect(res.capReached).toBe(false) + }) + + it('does not count skipped items against the cap', () => { + const res = takeIndexableWithinCap([skip(1), skip(2), file(3), file(4), file(5)], isSkip, 2, 0) + // both skips + the first two files emitted; the third file is beyond the cap + expect(res.documents.map((i) => i.id)).toEqual([1, 2, 3, 4]) + expect(res.indexableCount).toBe(2) + expect(res.capReached).toBe(true) + }) + + it('keeps emitting indexable docs even when oversized files crowd the front', () => { + // Regression guard: an oversized prefix must not starve the indexable budget. + const res = takeIndexableWithinCap([skip(1), skip(2), skip(3), file(4), file(5)], isSkip, 2, 0) + expect(res.documents.map((i) => i.id)).toEqual([1, 2, 3, 4, 5]) + expect(res.indexableCount).toBe(2) + expect(res.capReached).toBe(true) + }) + + it('stops once the indexable quota is met, dropping trailing items', () => { + const res = takeIndexableWithinCap([file(1), file(2), file(3), file(4)], isSkip, 2, 0) + expect(res.documents.map((i) => i.id)).toEqual([1, 2]) + expect(res.indexableCount).toBe(2) + expect(res.capReached).toBe(true) + }) + + it('accounts for indexable docs already counted on previous pages', () => { + const res = takeIndexableWithinCap([file(1), file(2), file(3)], isSkip, 5, 4) + // only one indexable slot remains (5 - 4) + expect(res.documents.map((i) => i.id)).toEqual([1]) + expect(res.indexableCount).toBe(1) + expect(res.capReached).toBe(true) + }) + + it('emits nothing once the cap is already reached', () => { + const res = takeIndexableWithinCap([skip(1), file(2)], isSkip, 3, 3) + expect(res.documents).toHaveLength(0) + expect(res.indexableCount).toBe(0) + expect(res.capReached).toBe(true) + }) +}) diff --git a/apps/sim/connectors/utils.ts b/apps/sim/connectors/utils.ts index d30bc67017..242edbae72 100644 --- a/apps/sim/connectors/utils.ts +++ b/apps/sim/connectors/utils.ts @@ -1,4 +1,18 @@ import type { SecureFetchResponse } from '@/lib/core/security/input-validation.server' +import { MAX_FILE_SIZE as KB_DOCUMENT_MAX_BYTES } from '@/lib/uploads/utils/validation' +import type { ExternalDocument } from '@/connectors/types' + +/** + * Per-file size cap for knowledge base connector syncs. Aligned with the limit for + * manually uploaded KB documents (`MAX_FILE_SIZE` in `uploads/validation`) so a + * connector indexes the same files a user could add by hand — rather than the much + * lower proxy-derived 10 MB number that previously (and arbitrarily) applied here. + * + * Connector downloads are streamed against this cap via `readBodyWithLimit`, and + * files above it are surfaced as skipped (failed) documents instead of being dropped + * silently, so raising the limit stays memory-safe and visible. + */ +export const CONNECTOR_MAX_FILE_BYTES = KB_DOCUMENT_MAX_BYTES /** * Strips HTML tags from content and decodes common HTML entities. @@ -113,3 +127,97 @@ export async function readBodyWithLimit( } return Buffer.concat(chunks) } + +/** + * Marks a listed document stub as intentionally skipped — for example because it + * exceeds the connector's size limit. The sync engine records these as `failed` + * documents carrying `skippedReason`, so oversized files stay visible in the + * knowledge base UI instead of vanishing from the index silently. Reuses the + * connector's own stub so the externalId, contentHash, sourceUrl, and metadata + * (including fileSize) are preserved. + */ +export function markSkipped(stub: ExternalDocument, reason: string): ExternalDocument { + return { ...stub, content: '', contentDeferred: false, skippedReason: reason } +} + +/** Human-readable size-limit skip reason, e.g. "File exceeds the 10MB size limit". */ +export function sizeLimitSkipReason(maxBytes: number): string { + return `File exceeds the ${Math.round(maxBytes / (1024 * 1024))}MB size limit and was not indexed` +} + +/** + * Returns the listing stub as-is, or a skipped marker when its size exceeds the cap. + * Lets each connector express the listing-time size decision once instead of + * repeating the `size > max ? markSkipped(...) : stub` ternary (and building the stub + * twice). A missing/zero size is treated as within the cap (oversize is then caught + * at fetch time via `ConnectorFileTooLargeError`). + */ +export function stubOrSkipBySize( + stub: ExternalDocument, + size: number | undefined, + maxBytes: number +): ExternalDocument { + return size && size > maxBytes ? markSkipped(stub, sizeLimitSkipReason(maxBytes)) : stub +} + +/** True when a stub has been flagged as skipped (e.g. oversized) via `markSkipped`. */ +export function isSkippedDocument(doc: ExternalDocument): boolean { + return doc.skippedReason !== undefined +} + +/** + * Applies a document cap (`maxFiles`/`maxObjects`/`maxRecordings`) to a page of + * listing items so that only **indexable** items consume the cap. Skipped + * (oversized) items still ride along and surface as failed rows, but they no longer + * count toward the budget — otherwise a run of oversized files at the front of a + * listing could exhaust the cap before any indexable file is listed, silently + * shrinking real sync coverage. + * + * Items are walked in listing order: every item (indexable or skipped) is emitted + * until the indexable quota is reached, then iteration stops. A cap of `0` (or less) + * means unlimited and all items pass through. + * + * @param items page items in listing order + * @param isSkipped predicate identifying non-indexable (skipped) items + * @param max configured cap; `0` or less means no cap + * @param alreadyIndexed indexable items already counted on previous pages + * @returns the emitted items, the number of indexable items emitted (the only ones + * that count toward the cap), and whether the cap is now reached + */ +export function takeIndexableWithinCap( + items: T[], + isSkipped: (item: T) => boolean, + max: number, + alreadyIndexed: number +): { documents: T[]; indexableCount: number; capReached: boolean } { + if (max <= 0) { + let indexableCount = 0 + for (const item of items) { + if (!isSkipped(item)) indexableCount += 1 + } + return { documents: items, indexableCount, capReached: false } + } + + const remaining = max - alreadyIndexed + const documents: T[] = [] + let indexableCount = 0 + for (const item of items) { + if (indexableCount >= remaining) break + documents.push(item) + if (!isSkipped(item)) indexableCount += 1 + } + return { documents, indexableCount, capReached: alreadyIndexed + indexableCount >= max } +} + +/** + * Raised by a connector when a file exceeds its size cap mid-download — i.e. the + * listing did not report a size, so the limit is only discovered while streaming. + * `getDocument` catches it and returns a `markSkipped` document so the file surfaces + * as a failed row instead of being dropped silently. + */ +export class ConnectorFileTooLargeError extends Error { + constructor(readonly limitBytes: number) { + super(`File exceeds the ${Math.round(limitBytes / (1024 * 1024))}MB size limit`) + this.name = 'ConnectorFileTooLargeError' + } +} diff --git a/apps/sim/connectors/zoom/zoom.ts b/apps/sim/connectors/zoom/zoom.ts index 68dd44761c..77122bd3fa 100644 --- a/apps/sim/connectors/zoom/zoom.ts +++ b/apps/sim/connectors/zoom/zoom.ts @@ -2,7 +2,16 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { parseTagDate } from '@/connectors/utils' +import { + CONNECTOR_MAX_FILE_BYTES, + isSkippedDocument, + markSkipped, + parseTagDate, + readBodyWithLimit, + sizeLimitSkipReason, + stubOrSkipBySize, + takeIndexableWithinCap, +} from '@/connectors/utils' import { zoomConnectorMeta } from '@/connectors/zoom/meta' const logger = createLogger('ZoomConnector') @@ -208,6 +217,7 @@ function recordingToStub( duration: recording.duration, meetingDate: recording.start_time, topic: recording.topic, + fileSize: transcriptFile.file_size, }, } } @@ -309,21 +319,26 @@ export const zoomConnector: ConnectorConfig = { if (!meeting.uuid) continue const transcript = findTranscriptFile(meeting.recording_files) if (!transcript) continue - allDocuments.push(recordingToStub(meeting, transcript)) + allDocuments.push( + stubOrSkipBySize( + recordingToStub(meeting, transcript), + transcript.file_size, + CONNECTOR_MAX_FILE_BYTES + ) + ) } const prevFetched = (syncContext?.totalDocsFetched as number) ?? 0 - let documents = allDocuments - if (maxRecordings > 0) { - const remaining = Math.max(0, maxRecordings - prevFetched) - if (allDocuments.length > remaining) { - documents = allDocuments.slice(0, remaining) - } - } - - const totalFetched = prevFetched + documents.length + const { documents, indexableCount, capReached } = takeIndexableWithinCap( + allDocuments, + isSkippedDocument, + maxRecordings, + prevFetched + ) + + const totalFetched = prevFetched + indexableCount if (syncContext) syncContext.totalDocsFetched = totalFetched - const hitLimit = maxRecordings > 0 && totalFetched >= maxRecordings + const hitLimit = capReached if (hitLimit && syncContext) syncContext.listingCapped = true let nextCursor: string | undefined @@ -386,7 +401,15 @@ export const zoomConnector: ConnectorConfig = { return null } - const vttText = await vttResponse.text() + const vttBuffer = await readBodyWithLimit(vttResponse, CONNECTOR_MAX_FILE_BYTES) + if (!vttBuffer) { + return markSkipped( + recordingToStub(recording, transcript), + sizeLimitSkipReason(CONNECTOR_MAX_FILE_BYTES) + ) + } + + const vttText = vttBuffer.toString('utf8') const transcriptText = parseVtt(vttText).trim() if (!transcriptText) return null diff --git a/apps/sim/executor/variables/resolver.test.ts b/apps/sim/executor/variables/resolver.test.ts index 84b3bf92d6..e7672297f4 100644 --- a/apps/sim/executor/variables/resolver.test.ts +++ b/apps/sim/executor/variables/resolver.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { LARGE_ARRAY_MANIFEST_VERSION, type LargeArrayManifest, @@ -13,6 +13,13 @@ import { VariableResolver } from '@/executor/variables/resolver' import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +const { mockStoreLargeValue } = vi.hoisted(() => ({ mockStoreLargeValue: vi.fn() })) + +vi.mock('@/lib/execution/payloads/store', () => ({ + storeLargeValue: mockStoreLargeValue, + materializeLargeValueRef: vi.fn(), +})) + function createBlock(id: string, name: string, type: string, params = {}): SerializedBlock { return { id, @@ -1170,3 +1177,145 @@ describe('VariableResolver function block inputs', () => { expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) }) }) + +describe('VariableResolver function context overflow offload', () => { + const REF_KEY = 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json' + + function createOffloadEnv(language: string, producerOutput: Record) { + const { block, ctx } = createResolver(language) + const producer = createBlock('producer', 'Producer', BlockType.API) + const state = new ExecutionState() + state.setBlockOutput('producer', producerOutput) + const workflow: SerializedWorkflow = { + version: '1', + blocks: [producer, block], + connections: [], + loops: {}, + parallels: {}, + } + const resolver = new VariableResolver(workflow, {}, state) + const durableCtx = { + ...ctx, + blockStates: state.getBlockStates(), + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + executionId: 'execution-1', + largeValueKeys: [] as string[], + } as ExecutionContext + return { block, resolver, durableCtx } + } + + beforeEach(() => { + mockStoreLargeValue.mockReset() + mockStoreLargeValue.mockResolvedValue({ + __simLargeValueRef: true, + version: 1, + id: 'lv_ABCDEFGHIJKL', + kind: 'string', + size: 4 * 1024 * 1024, + key: REF_KEY, + executionId: 'execution-1', + }) + }) + + it('offloads an oversized inline value to a lazily-read large-value ref', async () => { + const big = 'x'.repeat(4 * 1024 * 1024) + const { block, resolver, durableCtx } = createOffloadEnv('javascript', { result: big }) + + const result = await resolver.resolveInputsForFunctionBlock( + durableCtx, + 'function', + { code: 'return ' }, + block + ) + + expect(mockStoreLargeValue).toHaveBeenCalledTimes(1) + expect(result.resolvedInputs.code).toBe( + 'return (await sim.values.read(globalThis["__blockRef_0"]))' + ) + expect(result.contextVariables.__blockRef_0).toMatchObject({ + __simLargeValueRef: true, + id: 'lv_ABCDEFGHIJKL', + }) + // The bulky value must not be inlined into either the request data or display source, + // and the Input view shows a readable placeholder instead of the raw ref object. + expect(result.displayInputs.code.length).toBeLessThan(1024) + expect(result.displayInputs.code).not.toContain('__simLargeValueRef') + expect(result.displayInputs.code).toContain('large string') + // The route must be authorized to materialize the ref it is about to receive. + expect(durableCtx.largeValueKeys).toContain(REF_KEY) + }) + + it('offloads only the values that overflow the budget when several are merged', async () => { + // Each value's inline footprint (data + display ~= 2x) is ~4 MB. The first fits the + // ~6 MB budget and stays inline; the second overflows and is offloaded. + const half = 'y'.repeat(2 * 1024 * 1024) + const { block, resolver, durableCtx } = createOffloadEnv('javascript', { + first: half, + second: half, + }) + + const result = await resolver.resolveInputsForFunctionBlock( + durableCtx, + 'function', + { code: 'return [, ]' }, + block + ) + + // First value fits the budget and stays inline; the second overflows and is offloaded. + expect(mockStoreLargeValue).toHaveBeenCalledTimes(1) + expect(result.resolvedInputs.code).toBe( + 'return [globalThis["__blockRef_0"], (await sim.values.read(globalThis["__blockRef_1"]))]' + ) + expect(result.contextVariables.__blockRef_0).toBe(half) + expect(result.contextVariables.__blockRef_1).toMatchObject({ __simLargeValueRef: true }) + }) + + it('keeps small inline values inline without offloading', async () => { + const { block, resolver, durableCtx } = createOffloadEnv('javascript', { + result: 'hello world', + }) + + const result = await resolver.resolveInputsForFunctionBlock( + durableCtx, + 'function', + { code: 'return ' }, + block + ) + + expect(mockStoreLargeValue).not.toHaveBeenCalled() + expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]') + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) + + it('does not offload when the execution context cannot persist durably', async () => { + const big = 'x'.repeat(4 * 1024 * 1024) + const { block, resolver, durableCtx } = createOffloadEnv('javascript', { result: big }) + durableCtx.executionId = undefined + + const result = await resolver.resolveInputsForFunctionBlock( + durableCtx, + 'function', + { code: 'return ' }, + block + ) + + expect(mockStoreLargeValue).not.toHaveBeenCalled() + expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]') + }) + + it('does not offload for non-JavaScript runtimes that lack the read broker', async () => { + const big = 'x'.repeat(4 * 1024 * 1024) + const { block, resolver, durableCtx } = createOffloadEnv('python', { result: big }) + + const result = await resolver.resolveInputsForFunctionBlock( + durableCtx, + 'function', + { code: 'return ' }, + block + ) + + expect(mockStoreLargeValue).not.toHaveBeenCalled() + expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]') + }) +}) diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 22739fc5bd..851358cd2c 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -1,11 +1,14 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { isUserFileWithMetadata } from '@/lib/core/utils/user-file' +import { mergeLargeValueKeys } from '@/lib/execution/payloads/access-keys' import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata' import { containsLargeValueRef, + formatLargeValueSize, getLargeValueMaterializationError, isLargeValueRef, + type LargeValueRef, } from '@/lib/execution/payloads/large-value-ref' import { isLikelyReferenceSegment } from '@/lib/workflows/sanitization/references' import { BlockType, parseReferencePath, REFERENCE } from '@/executor/constants' @@ -32,6 +35,38 @@ export const FUNCTION_BLOCK_DISPLAY_CODE_KEY = '_runtimeDisplayCode' const logger = createLogger('VariableResolver') +/** + * Combined inline budget (data + display source) for a function block's resolved + * block-output context values. Internal routes cap request bodies at ~10 MB, and a + * resolved block reference is serialized into the function request both as data + * (`contextVariables`) and as a literal in the display source, so it costs roughly + * twice its size. Keeping the inline footprint under this budget leaves headroom for + * the code, params, and environment variables in the same body. Values that would + * overflow the budget are offloaded to durable large-value refs and lazily re-read in + * the sandbox via the `sim.values.read` broker. + */ +const FUNCTION_CONTEXT_INLINE_BUDGET_BYTES = 6 * 1024 * 1024 + +interface FunctionContextOffloadState { + inlineFootprintRemaining: number +} + +function createFunctionContextOffloadState(): FunctionContextOffloadState { + return { inlineFootprintRemaining: FUNCTION_CONTEXT_INLINE_BUDGET_BYTES } +} + +function measureJson(value: unknown): { json: string; size: number } | null { + try { + const json = JSON.stringify(value) + if (json === undefined) { + return null + } + return { json, size: Buffer.byteLength(json, 'utf8') } + } catch { + return null + } +} + function getNestedLargeValueMaterializationError(): Error { return new Error( 'This execution value contains nested large values. Reference the nested field directly so it can be lazy-loaded.' @@ -129,6 +164,7 @@ export class VariableResolver { const contextVariables: Record = {} const resolved: Record = {} const display: Record = {} + const offloadState = createFunctionContextOffloadState() if (!params) { return { resolvedInputs: resolved, displayInputs: display, contextVariables } @@ -143,7 +179,8 @@ export class VariableResolver { value, undefined, block, - contextVariables + contextVariables, + offloadState ) resolved[key] = code.resolvedCode display[key] = code.displayCode @@ -158,7 +195,8 @@ export class VariableResolver { item.content, undefined, block, - contextVariables + contextVariables, + offloadState ) resolvedItems.push({ ...item, @@ -339,7 +377,8 @@ export class VariableResolver { template: string, loopScope: LoopScope | undefined, block: SerializedBlock, - contextVarAccumulator: Record + contextVarAccumulator: Record, + offloadState: FunctionContextOffloadState = createFunctionContextOffloadState() ): Promise<{ resolvedCode: string; displayCode: string }> { const resolutionContext: ResolutionContext = { executionContext: ctx, @@ -388,8 +427,12 @@ export class VariableResolver { // Block output: store in contextVarAccumulator and replace the reference // with language-specific runtime access to that stored value. const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` - contextVarAccumulator[varName] = effectiveValue let replacement: string + // `storedValue` is placed in contextVarAccumulator and `displayValue` is + // rendered into the display source. They diverge only when an oversized + // inline value is offloaded to a ref (both then carry the small ref). + let storedValue: unknown = effectiveValue + let displayValue: unknown = effectiveValue if (isLargeValueRef(effectiveValue)) { const lazyReplacement = this.formatLazyLargeValueReference( varName, @@ -415,16 +458,40 @@ export class VariableResolver { } else if (containsLargeValueRef(effectiveValue)) { throw getNestedLargeValueMaterializationError() } else { - replacement = this.formatContextVariableReference( - varName, + const offloadedRef = await this.maybeOffloadInlineFunctionContextValue( + ctx, + effectiveValue, language, template, - index, - effectiveValue + offloadState ) + if (offloadedRef) { + storedValue = offloadedRef + displayValue = offloadedRef + // maybeOffload only returns a ref when the JS runtime helpers are usable — + // the same guard formatLazyLargeValueReference needs — so it is non-null here. + replacement = + this.formatLazyLargeValueReference(varName, language, template, index) ?? + this.formatContextVariableReference( + varName, + language, + template, + index, + effectiveValue + ) + } else { + replacement = this.formatContextVariableReference( + varName, + language, + template, + index, + effectiveValue + ) + } } + contextVarAccumulator[varName] = storedValue displayResult += this.formatDisplayValueForCodeContext( - effectiveValue, + displayValue, language, template, index @@ -577,6 +644,68 @@ export class VariableResolver { } } + /** + * Offloads an inline function block-output value to a durable large-value ref when + * keeping it inline would push the function execution request body past its budget. + * + * A few medium values merged in one function block (for example two fetched images) + * can exceed the ~10 MB internal-route body cap even though no single value crosses + * the per-value large-value threshold. Offloading the overflowing values lets the + * function runtime lazily re-read them via the `sim.values.read` broker instead of + * inlining their bytes into the request. + * + * Returns the stored reference when offloaded, or `null` to keep the value inline. + */ + private async maybeOffloadInlineFunctionContextValue( + ctx: ExecutionContext, + value: unknown, + language: string | undefined, + template: string, + offloadState: FunctionContextOffloadState + ): Promise { + // Lazy re-reading is only available in the JavaScript isolated-vm runtime; other + // runtimes have no broker to materialize a ref, so the value must stay inline. + if (!this.canUseJavaScriptRuntimeHelpers(language, template)) { + return null + } + if (!ctx.workspaceId || !ctx.workflowId || !ctx.executionId) { + return null + } + + const measured = measureJson(value) + if (measured === null) { + return null + } + + // Inline values are serialized into both the request data and the display source. + const footprint = measured.size * 2 + if (footprint <= offloadState.inlineFootprintRemaining) { + offloadState.inlineFootprintRemaining -= footprint + return null + } + + try { + const { storeLargeValue } = await import('@/lib/execution/payloads/store') + const ref = await storeLargeValue(value, measured.json, measured.size, { + workspaceId: ctx.workspaceId, + workflowId: ctx.workflowId, + executionId: ctx.executionId, + userId: ctx.userId, + requireDurable: true, + }) + // Authorize the function route to materialize the ref it is about to receive. + if (ref.key) { + mergeLargeValueKeys(ctx, [ref.key]) + } + return ref + } catch (error) { + logger.warn('Failed to offload oversized function context value; keeping inline', { + error: toError(error).message, + }) + return null + } + } + private formatLazyLargeValueReference( varName: string, language: string | undefined, @@ -846,6 +975,16 @@ export class VariableResolver { template: string, matchIndex: number ): string { + // Offloaded large values carry only a storage ref (or array manifest), never the + // data itself. Render a concise placeholder for the Input view instead of leaking + // the internal ref object, which is unreadable and meaningless to a user. + if (isLargeValueRef(value)) { + return `/* large ${value.kind} · ${formatLargeValueSize(value.size)}, fetched at runtime */` + } + if (isLargeArrayManifest(value)) { + return `/* large array · ${value.totalCount} items · ${formatLargeValueSize(value.byteSize)}, fetched at runtime */` + } + if (language === 'shell') { return this.formatShellDisplayValue(value, template, matchIndex) } diff --git a/apps/sim/lib/execution/payloads/large-value-ref.ts b/apps/sim/lib/execution/payloads/large-value-ref.ts index d770f6ed37..6397504de9 100644 --- a/apps/sim/lib/execution/payloads/large-value-ref.ts +++ b/apps/sim/lib/execution/payloads/large-value-ref.ts @@ -84,7 +84,7 @@ export function getLargeValueMaterializationError(ref: LargeValueRef): Error { ) } -function formatLargeValueSize(bytes: number): string { +export function formatLargeValueSize(bytes: number): string { const megabytes = bytes / (1024 * 1024) return `${megabytes.toFixed(1)} MB` } diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.test.ts b/apps/sim/lib/knowledge/connectors/sync-engine.test.ts index b0de83c82b..56baf974ef 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.test.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.test.ts @@ -179,3 +179,111 @@ describe('resolveTagMapping', () => { expect(result).toBeUndefined() }) }) + +describe('classifyExternalDoc', () => { + const base = { content: 'hello', contentDeferred: false, contentHash: 'h1' } + + it('records a new skipped file as a failed row', async () => { + const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-engine') + expect( + classifyExternalDoc({ ...base, content: '', skippedReason: 'too big' }, undefined) + ).toEqual({ type: 'skip' }) + }) + + it('keeps an already-indexed file as-is when it becomes skipped (last-known-good)', async () => { + const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-engine') + expect( + classifyExternalDoc( + { ...base, content: '', skippedReason: 'too big' }, + { + id: 'doc-1', + contentHash: 'old', + } + ) + ).toEqual({ type: 'unchanged' }) + }) + + it('drops empty non-deferred content', async () => { + const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-engine') + expect(classifyExternalDoc({ ...base, content: ' ' }, undefined)).toEqual({ type: 'drop' }) + }) + + it('adds new content and deferred stubs', async () => { + const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-engine') + expect(classifyExternalDoc(base, undefined)).toEqual({ type: 'add' }) + expect(classifyExternalDoc({ ...base, content: '', contentDeferred: true }, undefined)).toEqual( + { type: 'add' } + ) + }) + + it('updates when the content hash changed and is unchanged otherwise', async () => { + const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-engine') + expect(classifyExternalDoc(base, { id: 'doc-1', contentHash: 'old' })).toEqual({ + type: 'update', + existingId: 'doc-1', + }) + expect(classifyExternalDoc(base, { id: 'doc-1', contentHash: 'h1' })).toEqual({ + type: 'unchanged', + }) + }) +}) + +describe('chunkOpsByByteBudget', () => { + const MB = 1024 * 1024 + const addOp = (sizeBytes?: number) => ({ + type: 'add' as const, + extDoc: { + externalId: `e-${Math.random()}`, + title: 'f', + content: 'x', + contentHash: 'h', + mimeType: 'text/plain', + ...(sizeBytes != null ? { metadata: { fileSize: sizeBytes } } : {}), + }, + }) + const skipOp = (sizeBytes: number) => ({ + type: 'skip' as const, + extDoc: { + externalId: `s-${Math.random()}`, + title: 'f', + content: '', + contentHash: 'h', + mimeType: 'text/plain', + skippedReason: 'too big', + metadata: { fileSize: sizeBytes }, + }, + }) + + it('batches small ops up to the count cap', async () => { + const { chunkOpsByByteBudget } = await import('@/lib/knowledge/connectors/sync-engine') + const chunks = chunkOpsByByteBudget( + Array.from({ length: 7 }, () => addOp(1024)), + 64 * MB, + 5 + ) + expect(chunks.map((c) => c.length)).toEqual([5, 2]) + }) + + it('isolates a file larger than the budget into its own chunk', async () => { + const { chunkOpsByByteBudget } = await import('@/lib/knowledge/connectors/sync-engine') + const chunks = chunkOpsByByteBudget([addOp(100 * MB), addOp(1024)], 64 * MB, 5) + expect(chunks.map((c) => c.length)).toEqual([1, 1]) + }) + + it('caps summed bytes per chunk for medium files', async () => { + const { chunkOpsByByteBudget } = await import('@/lib/knowledge/connectors/sync-engine') + // 40 + 40 = 80 MB exceeds the 64 MB budget, so they split. + const chunks = chunkOpsByByteBudget([addOp(40 * MB), addOp(40 * MB)], 64 * MB, 5) + expect(chunks.map((c) => c.length)).toEqual([1, 1]) + }) + + it('treats skip ops as zero bytes so they do not consume the budget', async () => { + const { chunkOpsByByteBudget } = await import('@/lib/knowledge/connectors/sync-engine') + const chunks = chunkOpsByByteBudget( + [skipOp(100 * MB), skipOp(100 * MB), addOp(1024)], + 64 * MB, + 5 + ) + expect(chunks).toHaveLength(1) + }) +}) diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts index f74f21c679..c3423b455d 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -10,7 +10,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { randomInt } from '@sim/utils/random' -import { and, eq, gt, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' +import { and, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from 'drizzle-orm' import { decryptApiKey } from '@/lib/api-key/crypto' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import type { DocumentData } from '@/lib/knowledge/documents/service' @@ -43,6 +43,16 @@ class ConnectorDeletedException extends Error { } const SYNC_BATCH_SIZE = 5 +/** Estimated source bytes for a doc whose listing did not report a size. */ +const DEFAULT_OP_SIZE_BYTES = 4 * 1024 * 1024 +/** + * Max summed source bytes hydrated/uploaded concurrently within a batch. Each + * in-flight file materializes as a content string plus an upload buffer, so this + * bounds peak worker memory: a few large files near the per-file cap are processed + * in smaller sub-chunks instead of all at once, while small files still process up + * to SYNC_BATCH_SIZE at a time. + */ +const CONTENT_INFLIGHT_BUDGET_BYTES = 64 * 1024 * 1024 const MAX_PAGES = 500 const MAX_SAFE_TITLE_LENGTH = 200 const STALE_PROCESSING_MINUTES = 45 @@ -58,6 +68,82 @@ type KnowledgeBaseLockingTx = Pick type DocOp = | { type: 'add'; extDoc: ExternalDocument } | { type: 'update'; existingId: string; extDoc: ExternalDocument } + | { type: 'skip'; extDoc: ExternalDocument } + +type DocClassification = + | { type: 'add' } + | { type: 'update'; existingId: string } + | { type: 'skip' } + | { type: 'unchanged' } + | { type: 'drop' } + +/** + * Decides what a listed external document becomes during reconciliation. + * + * - `skip`: connector flagged it (e.g. too large) and it is not already indexed — + * record a visible `failed` document instead of dropping it silently. A file that + * is already indexed is kept as-is (last-known-good) rather than downgraded. + * - `drop`: empty, non-deferred content that cannot be indexed. + * - `add` / `update` / `unchanged`: normal content reconciliation by content hash. + */ +export function classifyExternalDoc( + extDoc: Pick, + existing: { id: string; contentHash: string | null } | undefined +): DocClassification { + if (extDoc.skippedReason) { + return existing ? { type: 'unchanged' } : { type: 'skip' } + } + if (!extDoc.content.trim() && !extDoc.contentDeferred) { + return { type: 'drop' } + } + if (!existing) { + return { type: 'add' } + } + if (existing.contentHash !== extDoc.contentHash) { + return { type: 'update', existingId: existing.id } + } + return { type: 'unchanged' } +} + +/** Estimated source bytes for a pending op, taken from its listing metadata. */ +function estimateOpSizeBytes(op: DocOp): number { + // Skip ops load no content (just a row insert), so they do not count against the + // in-flight content budget. + if (op.type === 'skip') return 0 + const size = op.extDoc.metadata?.fileSize ?? op.extDoc.metadata?.size + return typeof size === 'number' && Number.isFinite(size) && size > 0 + ? size + : DEFAULT_OP_SIZE_BYTES +} + +/** + * Splits content ops into sub-chunks bounded by both a count (maxCount) and a summed + * byte budget, so large files are hydrated/uploaded a few at a time. A single op + * larger than the budget still forms its own chunk (always >= 1 op per chunk). + */ +export function chunkOpsByByteBudget( + ops: DocOp[], + budgetBytes: number, + maxCount: number +): DocOp[][] { + const chunks: DocOp[][] = [] + let current: DocOp[] = [] + let currentBytes = 0 + for (const op of ops) { + const bytes = estimateOpSizeBytes(op) + if (current.length > 0 && (current.length >= maxCount || currentBytes + bytes > budgetBytes)) { + chunks.push(current) + current = [] + currentBytes = 0 + } + current.push(op) + currentBytes += bytes + } + if (current.length > 0) { + chunks.push(current) + } + return chunks +} /** Single-roundtrip liveness check used between batches. */ async function checkSyncLiveness( @@ -532,25 +618,34 @@ export async function executeSync( continue } - if (!extDoc.content.trim() && !extDoc.contentDeferred) { - logger.info(`Skipping empty document: ${extDoc.title}`, { - externalId: extDoc.externalId, - }) - continue - } - const existing = existingByExternalId.get(extDoc.externalId) - - if (!existing) { - pendingOps.push({ type: 'add', extDoc }) - } else if (existing.contentHash !== extDoc.contentHash) { - pendingOps.push({ type: 'update', existingId: existing.id, extDoc }) - } else { - result.docsUnchanged++ + const classification = classifyExternalDoc(extDoc, existing) + + switch (classification.type) { + case 'skip': + pendingOps.push({ type: 'skip', extDoc }) + break + case 'drop': + logger.info(`Skipping empty document: ${extDoc.title}`, { + externalId: extDoc.externalId, + }) + break + case 'add': + pendingOps.push({ type: 'add', extDoc }) + break + case 'update': + pendingOps.push({ type: 'update', existingId: classification.existingId, extDoc }) + break + case 'unchanged': + result.docsUnchanged++ + break } } - for (let i = 0; i < pendingOps.length; i += SYNC_BATCH_SIZE) { + // Batch by both count and summed content bytes so a few large files near the + // per-file cap never hydrate/upload together and exhaust the worker heap. + const batches = chunkOpsByByteBudget(pendingOps, CONTENT_INFLIGHT_BUDGET_BYTES, SYNC_BATCH_SIZE) + for (const rawBatch of batches) { const liveness = await checkSyncLiveness(connectorId, connector.knowledgeBaseId) if (liveness.connectorDeleted) { throw new ConnectorDeletedException(connectorId) @@ -559,10 +654,16 @@ export async function executeSync( throw new Error(`Knowledge base ${connector.knowledgeBaseId} was deleted during sync`) } - const rawBatch = pendingOps.slice(i, i + SYNC_BATCH_SIZE) + // Oversized/skipped docs become visible `failed` rows (never silent). They are + // flagged either at listing time (skip ops here) or discovered only at fetch + // time during hydration below; both are collected and persisted after hydration. + const skipExtDocs: ExternalDocument[] = rawBatch + .filter((op) => op.type === 'skip') + .map((op) => op.extDoc) - const deferredOps = rawBatch.filter((op) => op.extDoc.contentDeferred) - const readyOps = rawBatch.filter((op) => !op.extDoc.contentDeferred) + const contentOps = rawBatch.filter((op) => op.type !== 'skip') + const deferredOps = contentOps.filter((op) => op.extDoc.contentDeferred) + const readyOps = contentOps.filter((op) => !op.extDoc.contentDeferred) if (deferredOps.length > 0) { if (connectorConfig.auth.mode === 'oauth') { @@ -577,7 +678,30 @@ export async function executeSync( op.extDoc.externalId, syncContext ) - if (!fullDoc?.content.trim()) return null + // A connector may only learn a file is too large at fetch time (its + // listing has no size). Surface that as a failed row for new files; keep + // already-indexed files as last-known-good rather than downgrading them. + if (fullDoc?.skippedReason) { + if (op.type === 'add') { + skipExtDocs.push({ + ...op.extDoc, + skippedReason: fullDoc.skippedReason, + contentHash: fullDoc.contentHash ?? op.extDoc.contentHash, + metadata: { ...op.extDoc.metadata, ...fullDoc.metadata }, + }) + } else if (op.type === 'update') { + // Already-indexed file is kept as last-known-good (not downgraded), so it + // counts as unchanged rather than slipping past every result counter. + result.docsUnchanged++ + } + return null + } + if (!fullDoc?.content.trim()) { + // An empty re-fetch leaves an already-indexed update as last-known-good; count + // it as unchanged so the totals still reconcile with documents seen. + if (op.type === 'update') result.docsUnchanged++ + return null + } const hydratedHash = fullDoc.contentHash ?? op.extDoc.contentHash if ( op.type === 'update' && @@ -615,6 +739,27 @@ export async function executeSync( } } + // Record all skipped (oversized) docs in this batch in one bulk insert. + if (skipExtDocs.length > 0) { + try { + const recorded = await skipDocuments( + connector.knowledgeBaseId, + connectorId, + connector.connectorType, + skipExtDocs, + sourceConfig + ) + result.docsFailed += recorded + } catch (error) { + result.docsFailed += skipExtDocs.length + logger.error('Failed to record skipped documents', { + connectorId, + count: skipExtDocs.length, + error: toError(error).message, + }) + } + } + const batch = readyOps const settled = await Promise.allSettled( @@ -734,6 +879,9 @@ export async function executeSync( lt(document.uploadedAt, syncStartedAt), gt(document.uploadedAt, retryCutoff), eq(document.userExcluded, false), + // Skipped (oversized) docs are recorded as content-less failed rows with no + // storage key; they cannot be reprocessed, so exclude them from retry. + isNotNull(document.storageKey), isNull(document.archivedAt), isNull(document.deletedAt) ) @@ -941,6 +1089,82 @@ function kbOwnershipMetadata( : undefined } +/** Builds a content-less `failed` document row for a skipped (e.g. oversized) file. */ +function buildSkippedDocumentRow( + knowledgeBaseId: string, + connectorId: string, + connectorType: string, + extDoc: ExternalDocument, + sourceConfig?: Record +) { + const reason = extDoc.skippedReason ?? 'Document was skipped during sync' + const tagValues = extDoc.metadata + ? resolveTagMapping(connectorType, extDoc.metadata, sourceConfig) + : undefined + // Connectors put the source size under either `fileSize` or `size`; accept both + // so the skipped failed row shows the real size instead of 0. + const rawSize = extDoc.metadata?.fileSize ?? extDoc.metadata?.size + const fileSize = + typeof rawSize === 'number' && Number.isFinite(rawSize) ? Math.max(0, Math.trunc(rawSize)) : 0 + + return { + id: generateId(), + knowledgeBaseId, + filename: extDoc.title, + fileUrl: '', + storageKey: null, + fileSize, + mimeType: 'text/plain', + processingStatus: 'failed', + processingError: reason, + enabled: true, + connectorId, + externalId: extDoc.externalId, + contentHash: extDoc.contentHash, + sourceUrl: extDoc.sourceUrl ?? null, + ...tagValues, + uploadedAt: new Date(), + } +} + +/** + * Records source files that were intentionally not indexed (e.g. they exceed the + * connector's size limit) as content-less `failed` documents in a single bulk insert. + * This keeps the files visible in the knowledge base UI — with `processingError` + * explaining why — instead of silently dropping them. The rows have no storage key, + * so they are excluded from the stuck-document retry sweep (nothing to reprocess). + * + * Only called for files not already indexed; previously-indexed files that later + * exceed the limit are kept as-is (last-known-good) by `classifyExternalDoc`. + * + * Returns the number of rows recorded. + */ +async function skipDocuments( + knowledgeBaseId: string, + connectorId: string, + connectorType: string, + extDocs: ExternalDocument[], + sourceConfig?: Record +): Promise { + if (extDocs.length === 0) { + return 0 + } + const rows = extDocs.map((extDoc) => + buildSkippedDocumentRow(knowledgeBaseId, connectorId, connectorType, extDoc, sourceConfig) + ) + + await db.transaction(async (tx) => { + const isActive = await isKnowledgeBaseActiveInTx(tx, knowledgeBaseId) + if (!isActive) { + throw new Error(`Knowledge base ${knowledgeBaseId} is deleted`) + } + + await tx.insert(document).values(rows) + }) + + return rows.length +} + /** * Upload content to storage as a .txt file, create a document record, * and trigger processing via the existing pipeline. From 15a970d8051d2e7adb2b4e27699be4ee1efd0ad0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 16 Jun 2026 16:12:59 -0700 Subject: [PATCH 05/26] feat(integrations): hosted email-enrichment providers + cascade wiring (#5087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(integrations): hosted email-enrichment providers + cascade wiring Add Datagma, Dropcontact, LeadMagic, Icypeas, and Enrow integrations — tools, blocks, brand icons, and BYOK + metered hosted-key support — and register each in the tool/block registries and BYOK provider list. Wire the new finders/verifiers into the enrichment cascades: - work-email: Datagma, LeadMagic, Dropcontact, Icypeas, Enrow - phone-number: LeadMagic, Datagma, Dropcontact - email-verification: Icypeas, Enrow - company-info: Datagma, LeadMagic - company-domain: Datagma Add hosting tests for all five providers and cascade tests covering the new providers (incl. new test files for email-verification, company-info, and company-domain). Co-Authored-By: Claude Opus 4.8 * fix(enrichment): address PR review on Icypeas success + Datagma billing - Icypeas find_email/verify_email postProcess return success:true for all terminal statuses (NOT_FOUND/DEBITED_NOT_FOUND included) so the cascade runner calls mapOutput and records invalid/not-found verdicts instead of throwing and inflating the error count - Bill Icypeas verify FOUND (not just DEBITED*) per the documented 0.1-credit charge - Datagma enrich_person only applies the 30-credit phone surcharge when a phone lookup (phoneFull) was requested - Note Datagma's URL-param (apiId) auth in the hosted-key doc comment - Update hosting tests to match Co-Authored-By: Claude Opus 4.8 * fix(enrichment): only bill Enrow verify on a completed verification getCost returned a flat 0.25 credits regardless of output, so a job that fell back to the initial submit response (poll never completed, no qualification) was still metered. Charge 0.25 only when a qualification is present; 0 otherwise. Add a no-qualification test case. Co-Authored-By: Claude Opus 4.8 * chore(enrichment): peg hosted credit cost to each provider's lowest paid plan Align *_CREDIT_USD to the entry tier Sim will provision: - Datagma: Regular $49/3,000 emails → $0.0163 (was Popular $0.0132) - LeadMagic: Basic $49/2,000 → $0.0245 (was Growth $0.0104) Icypeas (Basic $0.019), Enrow (Starter $0.012), and Dropcontact (Starter ~$0.17) already reflect their lowest plan. Tests derive from the constants, so values stay consistent. Co-Authored-By: Claude Opus 4.8 * fix(enrichment): address PR review on mononyms + Icypeas verify email map - work-email LeadMagic: pass full_name + domain so single-token (mononym) names are no longer skipped - work-email Icypeas: firstname/lastname are optional on the API, so run a mononym with firstname alone instead of self-skipping - icypeas_verify_email mapItem reads item.email (verify payload shape) with a fallback to the nested results.emails[0].email Co-Authored-By: Claude Opus 4.8 * fix(enrichment): case-insensitive Enrow find billing getCost compared qualification to exactly 'valid' while the cascade normalizes with toLowerCase(), so a differently-cased API qualifier could zero out billing on a valid email. Lowercase before comparing; add a test. Co-Authored-By: Claude Opus 4.8 * fix(enrichment): drop Dropcontact from the phone-number cascade Dropcontact is an email/company-data enrichment service, not a phone-discovery provider — its phone/mobile_phone fields are unreliable and were surfacing firmographic data (an employee-count range like "5000-20077") as the phone. Keep the two purpose-built phone finders (LeadMagic find_mobile, Datagma find_phone); Dropcontact stays in the work-email and company cascades where its data is reliable. Co-Authored-By: Claude Opus 4.8 * fix(enrichment): only accept valid-qualified Enrow emails in work-email Enrow's finder qualifies each email valid/invalid. The work-email mapOutput accepted any non-empty email, so an invalid-qualified address could fill the cell while hosted billing (which only charges on valid) charged zero. Gate the cell on qualification === 'valid', consistent with billing. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../settings/components/byok/byok.tsx | 45 ++ apps/sim/blocks/blocks/datagma.ts | 325 +++++++++++++++ apps/sim/blocks/blocks/dropcontact.ts | 224 ++++++++++ apps/sim/blocks/blocks/enrow.ts | 128 ++++++ apps/sim/blocks/blocks/icypeas.ts | 163 ++++++++ apps/sim/blocks/blocks/leadmagic.ts | 390 ++++++++++++++++++ apps/sim/blocks/registry.ts | 15 + apps/sim/components/icons.tsx | 111 +++++ .../company-domain/company-domain.test.ts | 44 ++ .../company-domain/company-domain.ts | 17 +- .../company-info/company-info.test.ts | 67 +++ .../enrichments/company-info/company-info.ts | 43 +- .../email-verification.test.ts | 85 ++++ .../email-verification/email-verification.ts | 46 ++- .../phone-number/phone-number.test.ts | 26 ++ .../enrichments/phone-number/phone-number.ts | 37 +- .../enrichments/work-email/work-email.test.ts | 84 ++++ apps/sim/enrichments/work-email/work-email.ts | 94 ++++- apps/sim/lib/api/contracts/byok-keys.ts | 5 + apps/sim/tools/datagma-hosting.test.ts | 114 +++++ apps/sim/tools/datagma/enrich_company.ts | 132 ++++++ apps/sim/tools/datagma/enrich_person.ts | 175 ++++++++ apps/sim/tools/datagma/find_email.ts | 130 ++++++ apps/sim/tools/datagma/find_phone.ts | 111 +++++ apps/sim/tools/datagma/get_credits.ts | 61 +++ apps/sim/tools/datagma/hosting.ts | 49 +++ apps/sim/tools/datagma/index.ts | 13 + apps/sim/tools/datagma/types.ts | 151 +++++++ apps/sim/tools/dropcontact-hosting.test.ts | 181 ++++++++ apps/sim/tools/dropcontact/enrich_contact.ts | 368 +++++++++++++++++ apps/sim/tools/dropcontact/hosting.ts | 49 +++ apps/sim/tools/dropcontact/index.ts | 5 + apps/sim/tools/dropcontact/types.ts | 140 +++++++ apps/sim/tools/enrow-hosting.test.ts | 170 ++++++++ apps/sim/tools/enrow/find_email.ts | 193 +++++++++ apps/sim/tools/enrow/hosting.ts | 44 ++ apps/sim/tools/enrow/index.ts | 6 + apps/sim/tools/enrow/types.ts | 82 ++++ apps/sim/tools/enrow/verify_email.ts | 151 +++++++ apps/sim/tools/icypeas-hosting.test.ts | 219 ++++++++++ apps/sim/tools/icypeas/find_email.ts | 195 +++++++++ apps/sim/tools/icypeas/hosting.ts | 50 +++ apps/sim/tools/icypeas/index.ts | 6 + apps/sim/tools/icypeas/types.ts | 89 ++++ apps/sim/tools/icypeas/verify_email.ts | 183 ++++++++ apps/sim/tools/leadmagic-hosting.test.ts | 129 ++++++ apps/sim/tools/leadmagic/company_search.ts | 134 ++++++ apps/sim/tools/leadmagic/email_to_profile.ts | 89 ++++ apps/sim/tools/leadmagic/find_email.ts | 130 ++++++ apps/sim/tools/leadmagic/find_mobile.ts | 101 +++++ apps/sim/tools/leadmagic/get_credits.ts | 51 +++ apps/sim/tools/leadmagic/hosting.ts | 48 +++ apps/sim/tools/leadmagic/index.ts | 21 + apps/sim/tools/leadmagic/profile_search.ts | 128 ++++++ apps/sim/tools/leadmagic/profile_to_email.ts | 84 ++++ apps/sim/tools/leadmagic/role_finder.ts | 100 +++++ apps/sim/tools/leadmagic/types.ts | 249 +++++++++++ apps/sim/tools/leadmagic/validate_email.ts | 119 ++++++ apps/sim/tools/registry.ts | 40 ++ apps/sim/tools/types.ts | 5 + 60 files changed, 6430 insertions(+), 14 deletions(-) create mode 100644 apps/sim/blocks/blocks/datagma.ts create mode 100644 apps/sim/blocks/blocks/dropcontact.ts create mode 100644 apps/sim/blocks/blocks/enrow.ts create mode 100644 apps/sim/blocks/blocks/icypeas.ts create mode 100644 apps/sim/blocks/blocks/leadmagic.ts create mode 100644 apps/sim/enrichments/company-domain/company-domain.test.ts create mode 100644 apps/sim/enrichments/company-info/company-info.test.ts create mode 100644 apps/sim/enrichments/email-verification/email-verification.test.ts create mode 100644 apps/sim/tools/datagma-hosting.test.ts create mode 100644 apps/sim/tools/datagma/enrich_company.ts create mode 100644 apps/sim/tools/datagma/enrich_person.ts create mode 100644 apps/sim/tools/datagma/find_email.ts create mode 100644 apps/sim/tools/datagma/find_phone.ts create mode 100644 apps/sim/tools/datagma/get_credits.ts create mode 100644 apps/sim/tools/datagma/hosting.ts create mode 100644 apps/sim/tools/datagma/index.ts create mode 100644 apps/sim/tools/datagma/types.ts create mode 100644 apps/sim/tools/dropcontact-hosting.test.ts create mode 100644 apps/sim/tools/dropcontact/enrich_contact.ts create mode 100644 apps/sim/tools/dropcontact/hosting.ts create mode 100644 apps/sim/tools/dropcontact/index.ts create mode 100644 apps/sim/tools/dropcontact/types.ts create mode 100644 apps/sim/tools/enrow-hosting.test.ts create mode 100644 apps/sim/tools/enrow/find_email.ts create mode 100644 apps/sim/tools/enrow/hosting.ts create mode 100644 apps/sim/tools/enrow/index.ts create mode 100644 apps/sim/tools/enrow/types.ts create mode 100644 apps/sim/tools/enrow/verify_email.ts create mode 100644 apps/sim/tools/icypeas-hosting.test.ts create mode 100644 apps/sim/tools/icypeas/find_email.ts create mode 100644 apps/sim/tools/icypeas/hosting.ts create mode 100644 apps/sim/tools/icypeas/index.ts create mode 100644 apps/sim/tools/icypeas/types.ts create mode 100644 apps/sim/tools/icypeas/verify_email.ts create mode 100644 apps/sim/tools/leadmagic-hosting.test.ts create mode 100644 apps/sim/tools/leadmagic/company_search.ts create mode 100644 apps/sim/tools/leadmagic/email_to_profile.ts create mode 100644 apps/sim/tools/leadmagic/find_email.ts create mode 100644 apps/sim/tools/leadmagic/find_mobile.ts create mode 100644 apps/sim/tools/leadmagic/get_credits.ts create mode 100644 apps/sim/tools/leadmagic/hosting.ts create mode 100644 apps/sim/tools/leadmagic/index.ts create mode 100644 apps/sim/tools/leadmagic/profile_search.ts create mode 100644 apps/sim/tools/leadmagic/profile_to_email.ts create mode 100644 apps/sim/tools/leadmagic/role_finder.ts create mode 100644 apps/sim/tools/leadmagic/types.ts create mode 100644 apps/sim/tools/leadmagic/validate_email.ts diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index 99f25b48bf..8d4b5e527f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -6,6 +6,9 @@ import { AnthropicIcon, BasetenIcon, BrandfetchIcon, + DatagmaIcon, + DropcontactIcon, + EnrowIcon, ExaAIIcon, FalIcon, FindymailIcon, @@ -14,7 +17,9 @@ import { GeminiIcon, GoogleIcon, HunterIOIcon, + IcypeasIcon, JinaAIIcon, + LeadMagicIcon, LinkupIcon, MillionVerifierIcon, MistralIcon, @@ -202,6 +207,41 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [ description: 'Prospect search, individual reveal, and company enrichment', placeholder: 'Enter your Wiza API key', }, + { + id: 'datagma', + name: 'Datagma', + icon: DatagmaIcon, + description: 'Email, phone, person, and company enrichment', + placeholder: 'Enter your Datagma API key', + }, + { + id: 'dropcontact', + name: 'Dropcontact', + icon: DropcontactIcon, + description: 'GDPR-compliant contact enrichment and email finding', + placeholder: 'Enter your Dropcontact API key', + }, + { + id: 'leadmagic', + name: 'LeadMagic', + icon: LeadMagicIcon, + description: 'Email finding, validation, and B2B profile enrichment', + placeholder: 'Enter your LeadMagic API key', + }, + { + id: 'icypeas', + name: 'Icypeas', + icon: IcypeasIcon, + description: 'Email finding and verification', + placeholder: 'Enter your Icypeas API key', + }, + { + id: 'enrow', + name: 'Enrow', + icon: EnrowIcon, + description: 'Email finding and verification', + placeholder: 'Enter your Enrow API key', + }, { id: 'zerobounce', name: 'ZeroBounce', @@ -267,6 +307,11 @@ const PROVIDER_SECTIONS: BYOKProviderSection[] = [ 'findymail', 'prospeo', 'wiza', + 'datagma', + 'dropcontact', + 'leadmagic', + 'icypeas', + 'enrow', 'zerobounce', 'neverbounce', 'millionverifier', diff --git a/apps/sim/blocks/blocks/datagma.ts b/apps/sim/blocks/blocks/datagma.ts new file mode 100644 index 0000000000..9abe2a5868 --- /dev/null +++ b/apps/sim/blocks/blocks/datagma.ts @@ -0,0 +1,325 @@ +import { DatagmaIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import type { DatagmaResponse } from '@/tools/datagma/types' + +export const DatagmaBlock: BlockConfig = { + type: 'datagma', + name: 'Datagma', + description: 'Find verified B2B emails, mobile phones, and enrich person or company profiles', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Datagma to find verified work emails from a name and company, enrich person profiles via email or LinkedIn URL, enrich company data from a domain or name, look up mobile phone numbers from LinkedIn, and check your credit balance.', + docsLink: 'https://docs.sim.ai/tools/datagma', + category: 'tools', + integrationType: IntegrationType.Sales, + bgColor: '#FFFFFF', + icon: DatagmaIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Find Email', id: 'datagma_find_email' }, + { label: 'Enrich Person', id: 'datagma_enrich_person' }, + { label: 'Enrich Company', id: 'datagma_enrich_company' }, + { label: 'Find Phone', id: 'datagma_find_phone' }, + { label: 'Get Remaining Credits', id: 'datagma_get_credits' }, + ], + value: () => 'datagma_find_email', + }, + + // ------------------------------------------------------------------------- + // Find Email + // ------------------------------------------------------------------------- + { + id: 'fe_fullName', + title: 'Full Name', + type: 'short-input', + required: true, + placeholder: 'John Doe', + condition: { field: 'operation', value: 'datagma_find_email' }, + }, + { + id: 'fe_company', + title: 'Company Name or Domain', + type: 'short-input', + required: true, + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'datagma_find_email' }, + }, + { + id: 'fe_linkedInSlug', + title: 'LinkedIn Company Slug', + type: 'short-input', + placeholder: 'https://linkedin.com/company/stripe', + condition: { field: 'operation', value: 'datagma_find_email' }, + mode: 'advanced', + }, + + // ------------------------------------------------------------------------- + // Enrich Person + // ------------------------------------------------------------------------- + { + id: 'ep_data', + title: 'Email, LinkedIn URL, or Full Name', + type: 'short-input', + required: true, + placeholder: 'john@stripe.com or https://linkedin.com/in/johndoe or John Doe', + condition: { field: 'operation', value: 'datagma_enrich_person' }, + }, + { + id: 'ep_companyKeyword', + title: 'Company (when using full name)', + type: 'short-input', + placeholder: 'Stripe', + condition: { field: 'operation', value: 'datagma_enrich_person' }, + }, + { + id: 'ep_phoneFull', + title: 'Find Phone Number', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes (costs 30 extra credits if found)', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'datagma_enrich_person' }, + }, + { + id: 'ep_countryCode', + title: 'Country Code', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'datagma_enrich_person' }, + mode: 'advanced', + }, + { + id: 'ep_personFull', + title: 'Include Full Profile', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes (education + work history)', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'datagma_enrich_person' }, + mode: 'advanced', + }, + + // ------------------------------------------------------------------------- + // Enrich Company + // ------------------------------------------------------------------------- + { + id: 'ec_data', + title: 'Company Domain, Name, or SIREN', + type: 'short-input', + required: true, + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'datagma_enrich_company' }, + }, + { + id: 'ec_companyPremium', + title: 'Include LinkedIn Data', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'datagma_enrich_company' }, + mode: 'advanced', + }, + { + id: 'ec_companyFull', + title: 'Include Financial Data', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'datagma_enrich_company' }, + mode: 'advanced', + }, + + // ------------------------------------------------------------------------- + // Find Phone + // ------------------------------------------------------------------------- + { + id: 'fp_username', + title: 'LinkedIn URL', + type: 'short-input', + required: true, + placeholder: 'https://linkedin.com/in/johndoe', + condition: { field: 'operation', value: 'datagma_find_phone' }, + }, + { + id: 'fp_email', + title: 'Email (improves accuracy)', + type: 'short-input', + placeholder: 'john@stripe.com', + condition: { field: 'operation', value: 'datagma_find_phone' }, + }, + + // ------------------------------------------------------------------------- + // API Key — hidden on hosted Sim for operations with hosted-key support + // ------------------------------------------------------------------------- + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Datagma API key', + password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'datagma_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Datagma API key', + password: true, + condition: { field: 'operation', value: 'datagma_get_credits' }, + }, + ], + + tools: { + access: [ + 'datagma_find_email', + 'datagma_enrich_person', + 'datagma_enrich_company', + 'datagma_find_phone', + 'datagma_get_credits', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'datagma_find_email': + case 'datagma_enrich_person': + case 'datagma_enrich_company': + case 'datagma_find_phone': + case 'datagma_get_credits': + return params.operation + default: + return 'datagma_find_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + + // Map unique subBlock IDs back to tool param names + const idToParam: Record = { + fe_fullName: 'fullName', + fe_company: 'company', + fe_linkedInSlug: 'linkedInSlug', + ep_data: 'data', + ep_companyKeyword: 'companyKeyword', + ep_phoneFull: 'phoneFull', + ep_countryCode: 'countryCode', + ep_personFull: 'personFull', + ec_data: 'data', + ec_companyPremium: 'companyPremium', + ec_companyFull: 'companyFull', + fp_username: 'username', + fp_email: 'email', + } + + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + + // Coerce boolean-like dropdown values at execution time + if ( + mappedKey === 'phoneFull' || + mappedKey === 'personFull' || + mappedKey === 'companyPremium' || + mappedKey === 'companyFull' + ) { + result[mappedKey] = value === true || value === 'true' + } else { + result[mappedKey] = value + } + } + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Datagma API key' }, + // Find Email + fe_fullName: { type: 'string', description: "Person's full name (find email)" }, + fe_company: { type: 'string', description: 'Company name or domain (find email)' }, + fe_linkedInSlug: { type: 'string', description: 'LinkedIn company slug (find email)' }, + // Enrich Person + ep_data: { + type: 'string', + description: 'Email, LinkedIn URL, or full name (enrich person)', + }, + ep_companyKeyword: { type: 'string', description: 'Company keyword (enrich person)' }, + ep_phoneFull: { type: 'boolean', description: 'Find phone number (enrich person)' }, + ep_countryCode: { type: 'string', description: 'Country code (enrich person)' }, + ep_personFull: { type: 'boolean', description: 'Include full profile (enrich person)' }, + // Enrich Company + ec_data: { type: 'string', description: 'Company domain, name, or SIREN (enrich company)' }, + ec_companyPremium: { type: 'boolean', description: 'Include LinkedIn data (enrich company)' }, + ec_companyFull: { type: 'boolean', description: 'Include financial data (enrich company)' }, + // Find Phone + fp_username: { type: 'string', description: 'LinkedIn URL (find phone)' }, + fp_email: { type: 'string', description: 'Email address (find phone)' }, + }, + + outputs: { + // Find Email + email: { type: 'string', description: 'Verified work email address' }, + emailStatus: { type: 'string', description: 'Email verification status' }, + emailDomain: { type: 'string', description: 'Email domain' }, + mxfound: { type: 'boolean', description: 'Whether MX records were found' }, + smtpCheck: { type: 'boolean', description: 'Whether SMTP validation succeeded' }, + catchAll: { type: 'boolean', description: 'Whether the domain is catch-all' }, + // Enrich Person + name: { type: 'string', description: 'Full name' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + jobTitle: { type: 'string', description: 'Current job title' }, + company: { type: 'string', description: 'Current company name' }, + linkedInUrl: { type: 'string', description: 'LinkedIn profile URL' }, + location: { type: 'string', description: 'Location string' }, + country: { type: 'string', description: 'Country' }, + region: { type: 'string', description: 'Region/state' }, + city: { type: 'string', description: 'City' }, + extractedRole: { type: 'string', description: 'Extracted role category' }, + extractedSeniority: { type: 'string', description: 'Extracted seniority level' }, + twitter: { type: 'string', description: 'Twitter handle' }, + personConfidenceScore: { + type: 'number', + description: 'Confidence score for the person match', + }, + // Enrich Company + website: { type: 'string', description: 'Company website' }, + industries: { type: 'string', description: 'Industry classification' }, + companySize: { type: 'string', description: 'Employee headcount range' }, + type: { type: 'string', description: 'Company type (e.g., Private, Public)' }, + founded: { type: 'string', description: 'Year founded' }, + shortDescription: { type: 'string', description: 'Short company description' }, + revenueRange: { type: 'string', description: 'Estimated annual revenue range' }, + headquarters: { type: 'string', description: 'Headquarters location' }, + // Find Phone + phone: { type: 'string', description: 'Mobile phone number' }, + countryCode: { type: 'string', description: 'Country code prefix' }, + isWhatsapp: { type: 'boolean', description: 'Whether the number is linked to WhatsApp' }, + // Get Credits + credits: { type: 'number', description: 'Remaining Datagma credits' }, + }, +} + +export const DatagmaBlockMeta = { + tags: ['enrichment', 'sales-engagement'], + url: 'https://datagma.com', +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/dropcontact.ts b/apps/sim/blocks/blocks/dropcontact.ts new file mode 100644 index 0000000000..be4ca04a2f --- /dev/null +++ b/apps/sim/blocks/blocks/dropcontact.ts @@ -0,0 +1,224 @@ +import { DropcontactIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import type { DropcontactResponse } from '@/tools/dropcontact/types' + +export const DropcontactBlock: BlockConfig = { + type: 'dropcontact', + name: 'Dropcontact', + description: 'Enrich B2B contacts with verified email, phone, and company data', + longDescription: + 'Use Dropcontact to verify and enrich B2B contacts. Submit a contact with their name, company, website, or LinkedIn URL and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. Enrichment is async: Dropcontact processes the request, then Sim polls until the result is ready. Credits are only charged when a verified email is returned.', + docsLink: 'https://docs.sim.ai/tools/dropcontact', + category: 'tools', + bgColor: '#0066FF', + icon: DropcontactIcon, + authMode: AuthMode.ApiKey, + integrationType: IntegrationType.Sales, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [{ label: 'Enrich Contact', id: 'dropcontact_enrich_contact' }], + value: () => 'dropcontact_enrich_contact', + }, + + // Enrich Contact fields + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'john.doe@acme.com', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + }, + { + id: 'first_name', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + }, + { + id: 'last_name', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + }, + { + id: 'full_name', + title: 'Full Name', + type: 'short-input', + placeholder: 'John Doe (alternative to first + last name)', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'company', + title: 'Company Name', + type: 'short-input', + placeholder: 'Acme Corp', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + }, + { + id: 'website', + title: 'Company Website', + type: 'short-input', + placeholder: 'acme.com', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + }, + { + id: 'linkedin', + title: 'LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/in/johndoe', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'num_siren', + title: 'SIREN Number', + type: 'short-input', + placeholder: 'French company SIREN (optional)', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1 555 555 5555', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'country', + title: 'Country Code', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'siren', + title: 'Include SIREN Enrichment', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + { + id: 'language', + title: 'Language', + type: 'short-input', + placeholder: 'en', + condition: { field: 'operation', value: 'dropcontact_enrich_contact' }, + mode: 'advanced', + }, + + // API Key — hidden on hosted Sim (hosted key handles it) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Dropcontact API key', + password: true, + hideWhenHosted: true, + }, + ], + tools: { + access: ['dropcontact_enrich_contact'], + config: { + tool: (_params) => 'dropcontact_enrich_contact', + params: (params) => { + const { operation: _operation, ...rest } = params + const result: Record = {} + + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + if (key === 'siren') { + result[key] = value === true || value === 'true' + } else { + result[key] = value + } + } + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Dropcontact API key' }, + email: { type: 'string', description: 'Contact email address' }, + first_name: { type: 'string', description: 'Contact first name' }, + last_name: { type: 'string', description: 'Contact last name' }, + full_name: { type: 'string', description: 'Contact full name' }, + company: { type: 'string', description: 'Company name' }, + website: { type: 'string', description: 'Company website' }, + linkedin: { type: 'string', description: 'LinkedIn profile URL' }, + num_siren: { type: 'string', description: 'French company SIREN number' }, + phone: { type: 'string', description: 'Phone number' }, + country: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)' }, + siren: { type: 'boolean', description: 'Include SIREN/SIRET enrichment (France only)' }, + language: { type: 'string', description: 'Language for returned data' }, + }, + outputs: { + request_id: { type: 'string', description: 'Dropcontact async request ID' }, + email_found: { type: 'boolean', description: 'Whether a verified email was found' }, + email: { type: 'string', description: 'Primary verified email address' }, + emails: { + type: 'array', + description: 'All email addresses returned (each with email and qualification)', + }, + qualification: { + type: 'string', + description: 'Email qualification (e.g. nominative@pro)', + }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + full_name: { type: 'string', description: 'Full name' }, + civility: { type: 'string', description: 'Civility (Mr, Mrs, etc.)' }, + phone: { type: 'string', description: 'Phone number' }, + mobile_phone: { type: 'string', description: 'Mobile phone number' }, + company: { type: 'string', description: 'Company name' }, + website: { type: 'string', description: 'Company website' }, + company_linkedin: { type: 'string', description: 'Company LinkedIn URL' }, + linkedin: { type: 'string', description: 'Personal LinkedIn URL' }, + country: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)' }, + siren: { type: 'string', description: 'French SIREN number' }, + siret: { type: 'string', description: 'French SIRET number' }, + siret_address: { type: 'string', description: 'SIRET registered address' }, + siret_zip: { type: 'string', description: 'SIRET registered postal code' }, + siret_city: { type: 'string', description: 'SIRET registered city' }, + vat: { type: 'string', description: 'VAT number' }, + nb_employees: { type: 'string', description: 'Employee count range' }, + employee_count: { + type: 'number', + description: 'Exact employee count (Growth plan and above)', + }, + naf5_code: { type: 'string', description: 'NAF/APE code (France)' }, + naf5_des: { + type: 'string', + description: 'NAF/APE code description (France)', + }, + industry: { type: 'string', description: 'Industry classification' }, + job: { type: 'string', description: 'Job title' }, + job_level: { type: 'string', description: 'Job seniority level' }, + job_function: { type: 'string', description: 'Job function' }, + company_turnover: { + type: 'string', + description: 'Company revenue/turnover range', + }, + company_results: { type: 'string', description: 'Company net results' }, + }, +} + +export const DropcontactBlockMeta = { + tags: ['enrichment', 'sales-engagement'], + url: 'https://www.dropcontact.com', +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/enrow.ts b/apps/sim/blocks/blocks/enrow.ts new file mode 100644 index 0000000000..7d49e030b5 --- /dev/null +++ b/apps/sim/blocks/blocks/enrow.ts @@ -0,0 +1,128 @@ +import { EnrowIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import type { EnrowResponse } from '@/tools/enrow/types' + +export const EnrowBlock: BlockConfig = { + type: 'enrow', + name: 'Enrow', + description: 'Find and verify B2B emails with triple-verified accuracy', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Enrow to find verified B2B email addresses from a full name and company, or verify the deliverability of an existing email. Enrow performs deterministic verifications including catch-all emails — no additional verifier needed.', + docsLink: 'https://enrow.readme.io', + category: 'tools', + integrationType: IntegrationType.Sales, + bgColor: '#FFFFFF', + icon: EnrowIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Find Email', id: 'enrow_find_email' }, + { label: 'Verify Email', id: 'enrow_verify_email' }, + ], + value: () => 'enrow_find_email', + }, + + // --- Find Email --- + { + id: 'fullname', + title: 'Full Name', + type: 'short-input', + required: true, + placeholder: 'John Doe', + condition: { field: 'operation', value: 'enrow_find_email' }, + }, + { + id: 'company_domain', + title: 'Company Domain', + type: 'short-input', + required: true, + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'enrow_find_email' }, + }, + { + id: 'company_name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Stripe (used when domain is unavailable)', + condition: { field: 'operation', value: 'enrow_find_email' }, + mode: 'advanced', + }, + + // --- Verify Email --- + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@example.com', + condition: { field: 'operation', value: 'enrow_verify_email' }, + }, + + // --- API Key (hidden on hosted Sim for operations with hosted-key support) --- + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Enrow API key', + password: true, + hideWhenHosted: true, + }, + ], + tools: { + access: ['enrow_find_email', 'enrow_verify_email'], + config: { + tool: (params) => { + switch (params.operation) { + case 'enrow_find_email': + case 'enrow_verify_email': + return params.operation + default: + return 'enrow_find_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + + // Map unique subBlock IDs back to tool param names + const idToParam: Record = { + ve_email: 'email', + } + + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + result[mappedKey] = value + } + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Enrow API key' }, + fullname: { type: 'string', description: 'Full name for email search' }, + company_domain: { type: 'string', description: 'Company domain for email search' }, + company_name: { type: 'string', description: 'Company name for email search' }, + ve_email: { type: 'string', description: 'Email address to verify' }, + }, + outputs: { + id: { type: 'string', description: 'Enrow job identifier' }, + email: { type: 'string', description: 'Email address found or verified' }, + qualification: { type: 'string', description: '"valid" or "invalid"' }, + fullname: { type: 'string', description: 'Full name of the person (find only)' }, + company_name: { type: 'string', description: 'Company name (find only)' }, + company_domain: { type: 'string', description: 'Company domain (find only)' }, + linkedin_url: { type: 'string', description: 'LinkedIn URL of the person (find only)' }, + }, +} + +export const EnrowBlockMeta = { + tags: ['enrichment', 'sales-engagement'], + url: 'https://enrow.io', +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/icypeas.ts b/apps/sim/blocks/blocks/icypeas.ts new file mode 100644 index 0000000000..45fdb69150 --- /dev/null +++ b/apps/sim/blocks/blocks/icypeas.ts @@ -0,0 +1,163 @@ +import { IcypeasIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import type { IcypeasResponse } from '@/tools/icypeas/types' + +export const IcypeasBlock: BlockConfig = { + type: 'icypeas', + name: 'Icypeas', + description: 'Find and verify professional email addresses', + longDescription: + 'Integrate Icypeas to find a professional email address from a name and company domain, or verify whether an existing email is valid and deliverable. Results are returned asynchronously via polling.', + docsLink: 'https://docs.sim.ai/tools/icypeas', + category: 'tools', + integrationType: IntegrationType.Sales, + bgColor: '#0EA5E9', + icon: IcypeasIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Find Email', id: 'icypeas_find_email' }, + { label: 'Verify Email', id: 'icypeas_verify_email' }, + ], + value: () => 'icypeas_find_email', + }, + + // ----------------------------------------------------------------------- + // Find Email + // ----------------------------------------------------------------------- + { + id: 'fe_firstname', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: 'icypeas_find_email' }, + }, + { + id: 'fe_lastname', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: 'icypeas_find_email' }, + }, + { + id: 'fe_domainOrCompany', + title: 'Company Domain or Name', + type: 'short-input', + required: true, + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'icypeas_find_email' }, + }, + + // ----------------------------------------------------------------------- + // Verify Email + // ----------------------------------------------------------------------- + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@stripe.com', + condition: { field: 'operation', value: 'icypeas_verify_email' }, + }, + + // ----------------------------------------------------------------------- + // API Key — hidden on hosted Sim for all operations (hosted-key supported) + // ----------------------------------------------------------------------- + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Icypeas API key', + password: true, + hideWhenHosted: true, + }, + ], + + tools: { + access: ['icypeas_find_email', 'icypeas_verify_email'], + config: { + tool: (params) => { + switch (params.operation) { + case 'icypeas_find_email': + case 'icypeas_verify_email': + return params.operation + default: + return 'icypeas_find_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + + // Map unique subBlock IDs back to tool param names. + const idToParam: Record = { + fe_firstname: 'firstname', + fe_lastname: 'lastname', + fe_domainOrCompany: 'domainOrCompany', + ve_email: 'email', + } + + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + result[idToParam[key] ?? key] = value + } + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Icypeas API key' }, + fe_firstname: { type: 'string', description: "Person's first name (find email)" }, + fe_lastname: { type: 'string', description: "Person's last name (find email)" }, + fe_domainOrCompany: { + type: 'string', + description: 'Company domain or name (find email)', + }, + ve_email: { type: 'string', description: 'Email address to verify' }, + }, + + outputs: { + searchId: { + type: 'string', + description: 'Icypeas internal search ID', + }, + status: { + type: 'string', + description: + 'Terminal search status (FOUND, DEBITED, NOT_FOUND, DEBITED_NOT_FOUND, BAD_INPUT, INSUFFICIENT_FUNDS, ABORTED)', + }, + email: { + type: 'string', + description: 'Email address found or verified', + }, + firstname: { + type: 'string', + description: "Found person's first name (find-email only)", + }, + lastname: { + type: 'string', + description: "Found person's last name (find-email only)", + }, + valid: { + type: 'boolean', + description: 'Whether the email is valid/deliverable (verify-email only)', + }, + item: { + type: 'json', + description: 'Full raw item object from the Icypeas results endpoint', + }, + }, +} + +export const IcypeasBlockMeta = { + tags: ['enrichment', 'sales-engagement'], + url: 'https://www.icypeas.com', +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/leadmagic.ts b/apps/sim/blocks/blocks/leadmagic.ts new file mode 100644 index 0000000000..eaac36043d --- /dev/null +++ b/apps/sim/blocks/blocks/leadmagic.ts @@ -0,0 +1,390 @@ +import { LeadMagicIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import type { LeadMagicResponse } from '@/tools/leadmagic/types' + +export const LeadMagicBlock: BlockConfig = { + type: 'leadmagic', + name: 'LeadMagic', + description: 'Find and enrich B2B contacts, emails, mobile numbers, and company data', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate LeadMagic to find verified work emails by name or company, validate email deliverability, find direct mobile numbers, enrich LinkedIn profiles, reverse-lookup profiles from emails, search companies by domain, identify role holders at accounts, and check account credit balance.', + docsLink: 'https://docs.sim.ai/tools/leadmagic', + category: 'tools', + integrationType: IntegrationType.Sales, + bgColor: '#FFFFFF', + icon: LeadMagicIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Find Email', id: 'leadmagic_find_email' }, + { label: 'Validate Email', id: 'leadmagic_validate_email' }, + { label: 'Find Mobile', id: 'leadmagic_find_mobile' }, + { label: 'Profile Search', id: 'leadmagic_profile_search' }, + { label: 'Profile to Email', id: 'leadmagic_profile_to_email' }, + { label: 'Email to Profile', id: 'leadmagic_email_to_profile' }, + { label: 'Company Search', id: 'leadmagic_company_search' }, + { label: 'Role Finder', id: 'leadmagic_role_finder' }, + { label: 'Get Credits', id: 'leadmagic_get_credits' }, + ], + value: () => 'leadmagic_find_email', + }, + + // --- Find Email --- + { + id: 'fe_full_name', + title: 'Full Name', + type: 'short-input', + placeholder: 'John Doe', + condition: { field: 'operation', value: 'leadmagic_find_email' }, + }, + { + id: 'fe_domain', + title: 'Company Domain', + type: 'short-input', + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'leadmagic_find_email' }, + }, + { + id: 'fe_company_name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Stripe (if domain unavailable)', + condition: { field: 'operation', value: 'leadmagic_find_email' }, + mode: 'advanced', + }, + + // --- Validate Email --- + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@example.com', + condition: { field: 'operation', value: 'leadmagic_validate_email' }, + }, + + // --- Find Mobile --- + { + id: 'fm_profile_url', + title: 'LinkedIn Profile URL', + type: 'short-input', + placeholder: 'https://linkedin.com/in/johndoe', + condition: { field: 'operation', value: 'leadmagic_find_mobile' }, + }, + { + id: 'fm_work_email', + title: 'Work Email', + type: 'short-input', + placeholder: 'john@company.com (alternative to profile URL)', + condition: { field: 'operation', value: 'leadmagic_find_mobile' }, + mode: 'advanced', + }, + + // --- Profile Search --- + { + id: 'ps_profile_url', + title: 'LinkedIn Profile URL', + type: 'short-input', + required: true, + placeholder: 'https://linkedin.com/in/johndoe', + condition: { field: 'operation', value: 'leadmagic_profile_search' }, + }, + { + id: 'extended_response', + title: 'Include Profile Image', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'leadmagic_profile_search' }, + mode: 'advanced', + }, + + // --- Profile to Email --- + { + id: 'pte_profile_url', + title: 'LinkedIn Profile URL', + type: 'short-input', + required: true, + placeholder: 'https://linkedin.com/in/johndoe', + condition: { field: 'operation', value: 'leadmagic_profile_to_email' }, + }, + + // --- Email to Profile --- + { + id: 'etp_work_email', + title: 'Work Email', + type: 'short-input', + placeholder: 'john@company.com', + condition: { field: 'operation', value: 'leadmagic_email_to_profile' }, + }, + { + id: 'etp_personal_email', + title: 'Personal Email', + type: 'short-input', + placeholder: 'john@gmail.com (alternative to work email)', + condition: { field: 'operation', value: 'leadmagic_email_to_profile' }, + mode: 'advanced', + }, + + // --- Company Search --- + { + id: 'cs_company_domain', + title: 'Company Domain', + type: 'short-input', + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'leadmagic_company_search' }, + }, + { + id: 'cs_profile_url', + title: 'LinkedIn Company URL', + type: 'short-input', + placeholder: 'https://linkedin.com/company/stripe', + condition: { field: 'operation', value: 'leadmagic_company_search' }, + mode: 'advanced', + }, + { + id: 'cs_company_name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Stripe', + condition: { field: 'operation', value: 'leadmagic_company_search' }, + mode: 'advanced', + }, + + // --- Role Finder --- + { + id: 'rf_job_title', + title: 'Job Title', + type: 'short-input', + required: true, + placeholder: 'Head of Sales', + condition: { field: 'operation', value: 'leadmagic_role_finder' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a specific job title to search for at the company. Return ONLY the job title — no explanations or extra text.', + placeholder: 'e.g. Head of Sales, CTO, VP Engineering', + }, + }, + { + id: 'rf_company_domain', + title: 'Company Domain', + type: 'short-input', + placeholder: 'stripe.com', + condition: { field: 'operation', value: 'leadmagic_role_finder' }, + }, + { + id: 'rf_company_name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Stripe (if domain unavailable)', + condition: { field: 'operation', value: 'leadmagic_role_finder' }, + mode: 'advanced', + }, + + // API Key — hidden on hosted Sim for operations with hosted-key support + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your LeadMagic API key', + password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'leadmagic_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your LeadMagic API key', + password: true, + condition: { field: 'operation', value: 'leadmagic_get_credits' }, + }, + ], + + tools: { + access: [ + 'leadmagic_validate_email', + 'leadmagic_find_email', + 'leadmagic_find_mobile', + 'leadmagic_profile_search', + 'leadmagic_profile_to_email', + 'leadmagic_email_to_profile', + 'leadmagic_company_search', + 'leadmagic_role_finder', + 'leadmagic_get_credits', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'leadmagic_validate_email': + case 'leadmagic_find_email': + case 'leadmagic_find_mobile': + case 'leadmagic_profile_search': + case 'leadmagic_profile_to_email': + case 'leadmagic_email_to_profile': + case 'leadmagic_company_search': + case 'leadmagic_role_finder': + case 'leadmagic_get_credits': + return params.operation + default: + return 'leadmagic_find_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + + const idToParam: Record = { + // Find Email + fe_full_name: 'full_name', + fe_domain: 'domain', + fe_company_name: 'company_name', + // Validate Email + ve_email: 'email', + // Find Mobile + fm_profile_url: 'profile_url', + fm_work_email: 'work_email', + // Profile Search + ps_profile_url: 'profile_url', + // Profile to Email + pte_profile_url: 'profile_url', + // Email to Profile + etp_work_email: 'work_email', + etp_personal_email: 'personal_email', + // Company Search + cs_company_domain: 'company_domain', + cs_profile_url: 'profile_url', + cs_company_name: 'company_name', + // Role Finder + rf_job_title: 'job_title', + rf_company_domain: 'company_domain', + rf_company_name: 'company_name', + } + + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + if (mappedKey === 'extended_response') { + result[mappedKey] = value === true || value === 'true' + } else { + result[mappedKey] = value + } + } + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'LeadMagic API key' }, + // Find Email + fe_full_name: { type: 'string', description: 'Full name (find email)' }, + fe_domain: { type: 'string', description: 'Company domain (find email)' }, + fe_company_name: { type: 'string', description: 'Company name (find email)' }, + // Validate Email + ve_email: { type: 'string', description: 'Email address to validate' }, + // Find Mobile + fm_profile_url: { type: 'string', description: 'LinkedIn profile URL (find mobile)' }, + fm_work_email: { type: 'string', description: 'Work email (find mobile)' }, + // Profile Search + ps_profile_url: { type: 'string', description: 'LinkedIn profile URL (profile search)' }, + extended_response: { type: 'boolean', description: 'Include profile image URL' }, + // Profile to Email + pte_profile_url: { type: 'string', description: 'LinkedIn profile URL (profile to email)' }, + // Email to Profile + etp_work_email: { type: 'string', description: 'Work email (email to profile)' }, + etp_personal_email: { type: 'string', description: 'Personal email (email to profile)' }, + // Company Search + cs_company_domain: { type: 'string', description: 'Company domain (company search)' }, + cs_profile_url: { type: 'string', description: 'LinkedIn company URL (company search)' }, + cs_company_name: { type: 'string', description: 'Company name (company search)' }, + // Role Finder + rf_job_title: { type: 'string', description: 'Job title to find (role finder)' }, + rf_company_domain: { type: 'string', description: 'Company domain (role finder)' }, + rf_company_name: { type: 'string', description: 'Company name (role finder)' }, + }, + + outputs: { + // Shared + credits_consumed: { type: 'number', description: 'Credits charged for this request' }, + message: { type: 'string', description: 'Human-readable status message' }, + // Validate Email + email_status: { + type: 'string', + description: 'Validation result: valid, invalid, or unknown', + }, + is_domain_catch_all: { type: 'boolean', description: 'Whether the domain is a catch-all' }, + mx_record: { type: 'string', description: 'MX record for the domain' }, + mx_provider: { type: 'string', description: 'Email provider (Google, Microsoft, etc.)' }, + mx_gateway: { type: 'string', description: 'MX gateway for the domain' }, + mx_security_gateway: { + type: 'boolean', + description: 'Whether the domain uses a security gateway', + }, + // Find Email / Profile To Email / Validate + email: { type: 'string', description: 'Email address' }, + employment_verified: { type: 'boolean', description: 'Whether employment was verified' }, + has_mx: { type: 'boolean', description: 'Whether the domain has a valid MX record' }, + company_profile_url: { type: 'string', description: 'Company B2B profile URL' }, + // Find Mobile + mobile_number: { type: 'string', description: 'Direct mobile phone number' }, + // Profile Search + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + full_name: { type: 'string', description: 'Full name' }, + professional_title: { type: 'string', description: 'Current job title' }, + bio: { type: 'string', description: 'Profile bio / summary' }, + location: { type: 'string', description: 'Location' }, + country: { type: 'string', description: 'Country' }, + followers_range: { type: 'string', description: 'LinkedIn follower range' }, + company_name: { type: 'string', description: 'Current employer name' }, + company_industry: { type: 'string', description: 'Company industry' }, + company_website: { type: 'string', description: 'Company website' }, + total_tenure_years: { type: 'string', description: 'Total career tenure in years' }, + total_tenure_months: { type: 'string', description: 'Total career tenure in months' }, + work_experience: { type: 'array', description: 'Work history entries' }, + education: { type: 'array', description: 'Education history entries' }, + certifications: { type: 'array', description: 'Professional certifications' }, + // Email to Profile + profile_url: { type: 'string', description: 'LinkedIn profile URL' }, + // Company Search + companyName: { type: 'string', description: 'Company name' }, + companyId: { type: 'number', description: 'Internal company ID' }, + industry: { type: 'string', description: 'Industry classification' }, + employeeCount: { type: 'number', description: 'Number of employees' }, + employeeRange: { type: 'string', description: 'Headcount range' }, + founded: { type: 'number', description: 'Year founded' }, + headquarters: { type: 'json', description: 'Headquarters location' }, + revenue: { type: 'string', description: 'Revenue range' }, + funding: { type: 'string', description: 'Total funding' }, + description: { type: 'string', description: 'Company description' }, + specialties: { type: 'array', description: 'Company specialties' }, + competitors: { type: 'array', description: 'Competitor companies' }, + followerCount: { type: 'number', description: 'LinkedIn follower count' }, + twitter_url: { type: 'string', description: 'Twitter/X profile URL' }, + facebook_url: { type: 'string', description: 'Facebook page URL' }, + b2b_profile_url: { type: 'string', description: 'LinkedIn company profile URL' }, + logo_url: { type: 'string', description: 'Company logo URL' }, + // Role Finder + job_title: { type: 'string', description: 'Verified job title at the company' }, + // Get Credits + credits: { type: 'number', description: 'Remaining credit balance' }, + }, +} + +export const LeadMagicBlockMeta = { + tags: ['enrichment', 'sales-engagement'], + url: 'https://leadmagic.io', +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 1d2066d265..96d2f7f2f6 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -46,12 +46,14 @@ import { CursorBlock, CursorBlockMeta, CursorV2Block } from '@/blocks/blocks/cur import { DagsterBlock, DagsterBlockMeta } from '@/blocks/blocks/dagster' import { DatabricksBlock, DatabricksBlockMeta } from '@/blocks/blocks/databricks' import { DatadogBlock, DatadogBlockMeta } from '@/blocks/blocks/datadog' +import { DatagmaBlock, DatagmaBlockMeta } from '@/blocks/blocks/datagma' import { DaytonaBlock, DaytonaBlockMeta } from '@/blocks/blocks/daytona' import { DeploymentsBlock } from '@/blocks/blocks/deployments' import { DevinBlock, DevinBlockMeta } from '@/blocks/blocks/devin' import { DiscordBlock, DiscordBlockMeta } from '@/blocks/blocks/discord' import { DocuSignBlock, DocuSignBlockMeta } from '@/blocks/blocks/docusign' import { DropboxBlock, DropboxBlockMeta } from '@/blocks/blocks/dropbox' +import { DropcontactBlock, DropcontactBlockMeta } from '@/blocks/blocks/dropcontact' import { DSPyBlock, DSPyBlockMeta } from '@/blocks/blocks/dspy' import { DubBlock, DubBlockMeta } from '@/blocks/blocks/dub' import { DuckDuckGoBlock, DuckDuckGoBlockMeta } from '@/blocks/blocks/duckduckgo' @@ -61,6 +63,7 @@ import { ElevenLabsBlock, ElevenLabsBlockMeta } from '@/blocks/blocks/elevenlabs import { EmailBisonBlock, EmailBisonBlockMeta } from '@/blocks/blocks/emailbison' import { EnrichBlock, EnrichBlockMeta } from '@/blocks/blocks/enrich' import { EnrichmentBlock, EnrichmentBlockMeta } from '@/blocks/blocks/enrichment' +import { EnrowBlock, EnrowBlockMeta } from '@/blocks/blocks/enrow' import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { EvernoteBlock, EvernoteBlockMeta } from '@/blocks/blocks/evernote' import { ExaBlock, ExaBlockMeta } from '@/blocks/blocks/exa' @@ -132,6 +135,7 @@ import { HuggingFaceBlock, HuggingFaceBlockMeta } from '@/blocks/blocks/huggingf import { HumanInTheLoopBlock } from '@/blocks/blocks/human_in_the_loop' import { HunterBlock, HunterBlockMeta } from '@/blocks/blocks/hunter' import { IAMBlock, IAMBlockMeta } from '@/blocks/blocks/iam' +import { IcypeasBlock, IcypeasBlockMeta } from '@/blocks/blocks/icypeas' import { IdentityCenterBlock, IdentityCenterBlockMeta } from '@/blocks/blocks/identity_center' import { ImageGeneratorBlock, ImageGeneratorV2Block } from '@/blocks/blocks/image_generator' import { ImapBlock, ImapBlockMeta } from '@/blocks/blocks/imap' @@ -162,6 +166,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LangsmithBlock, LangsmithBlockMeta } from '@/blocks/blocks/langsmith' import { LatexBlock, LatexBlockMeta } from '@/blocks/blocks/latex' import { LaunchDarklyBlock, LaunchDarklyBlockMeta } from '@/blocks/blocks/launchdarkly' +import { LeadMagicBlock, LeadMagicBlockMeta } from '@/blocks/blocks/leadmagic' import { LemlistBlock, LemlistBlockMeta } from '@/blocks/blocks/lemlist' import { LinearBlock, LinearBlockMeta, LinearV2Block } from '@/blocks/blocks/linear' import { LinkedInBlock, LinkedInBlockMeta } from '@/blocks/blocks/linkedin' @@ -379,12 +384,14 @@ const BLOCK_REGISTRY: Record = { dagster: DagsterBlock, databricks: DatabricksBlock, datadog: DatadogBlock, + datagma: DatagmaBlock, daytona: DaytonaBlock, deployments: DeploymentsBlock, devin: DevinBlock, discord: DiscordBlock, docusign: DocuSignBlock, dropbox: DropboxBlock, + dropcontact: DropcontactBlock, dspy: DSPyBlock, dub: DubBlock, duckduckgo: DuckDuckGoBlock, @@ -394,6 +401,7 @@ const BLOCK_REGISTRY: Record = { emailbison: EmailBisonBlock, enrich: EnrichBlock, enrichment: EnrichmentBlock, + enrow: EnrowBlock, evaluator: EvaluatorBlock, evernote: EvernoteBlock, exa: ExaBlock, @@ -454,6 +462,7 @@ const BLOCK_REGISTRY: Record = { human_in_the_loop: HumanInTheLoopBlock, hunter: HunterBlock, iam: IAMBlock, + icypeas: IcypeasBlock, identity_center: IdentityCenterBlock, image_generator: ImageGeneratorBlock, image_generator_v2: ImageGeneratorV2Block, @@ -474,6 +483,7 @@ const BLOCK_REGISTRY: Record = { langsmith: LangsmithBlock, latex: LatexBlock, launchdarkly: LaunchDarklyBlock, + leadmagic: LeadMagicBlock, lemlist: LemlistBlock, linear: LinearBlock, linear_v2: LinearV2Block, @@ -678,11 +688,13 @@ const BLOCK_META_REGISTRY: Record = { dagster: DagsterBlockMeta, databricks: DatabricksBlockMeta, datadog: DatadogBlockMeta, + datagma: DatagmaBlockMeta, daytona: DaytonaBlockMeta, devin: DevinBlockMeta, discord: DiscordBlockMeta, docusign: DocuSignBlockMeta, dropbox: DropboxBlockMeta, + dropcontact: DropcontactBlockMeta, dspy: DSPyBlockMeta, dub: DubBlockMeta, duckduckgo: DuckDuckGoBlockMeta, @@ -692,6 +704,7 @@ const BLOCK_META_REGISTRY: Record = { emailbison: EmailBisonBlockMeta, enrich: EnrichBlockMeta, enrichment: EnrichmentBlockMeta, + enrow: EnrowBlockMeta, evernote: EvernoteBlockMeta, exa: ExaBlockMeta, extend: ExtendBlockMeta, @@ -738,6 +751,7 @@ const BLOCK_META_REGISTRY: Record = { huggingface: HuggingFaceBlockMeta, hunter: HunterBlockMeta, iam: IAMBlockMeta, + icypeas: IcypeasBlockMeta, identity_center: IdentityCenterBlockMeta, imap: ImapBlockMeta, incidentio: IncidentioBlockMeta, @@ -754,6 +768,7 @@ const BLOCK_META_REGISTRY: Record = { langsmith: LangsmithBlockMeta, latex: LatexBlockMeta, launchdarkly: LaunchDarklyBlockMeta, + leadmagic: LeadMagicBlockMeta, lemlist: LemlistBlockMeta, linear: LinearBlockMeta, linkedin: LinkedInBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index cd31b9713c..13fa62588b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -7870,3 +7870,114 @@ export function TriggerDevIcon(props: SVGProps) { ) } + +/** Datagma brand icon: navy square with the white Datagma "D" mark. */ +export function DatagmaIcon(props: SVGProps) { + return ( + + + + + ) +} + +/** LeadMagic brand icon: purple gradient tile with the white spark mark. */ +export function LeadMagicIcon(props: SVGProps) { + const id = useId() + const gradient = `leadmagic_grad_${id}` + return ( + + + + + + + + + + + + + + + + + + ) +} + +/** Dropcontact brand icon: teal disc with the white open-"d" contact mark. */ +export function DropcontactIcon(props: SVGProps) { + return ( + + + + + + ) +} + +/** Icypeas brand icon: dark tile with the teal ring + rising-chart mark. */ +export function IcypeasIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +/** Enrow brand icon: blue tile with the three white stacked rows. */ +export function EnrowIcon(props: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/sim/enrichments/company-domain/company-domain.test.ts b/apps/sim/enrichments/company-domain/company-domain.test.ts new file mode 100644 index 0000000000..3c4fd0fe6f --- /dev/null +++ b/apps/sim/enrichments/company-domain/company-domain.test.ts @@ -0,0 +1,44 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { companyDomainEnrichment } from '@/enrichments/company-domain/company-domain' +import type { EnrichmentProvider } from '@/enrichments/types' + +function provider(id: string): EnrichmentProvider { + const p = companyDomainEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in company-domain cascade`) + return p +} + +const nameInput = { companyName: 'Acme Inc' } + +describe('company-domain enrichment cascade', () => { + it('chains PDL then Datagma', () => { + expect(companyDomainEnrichment.providers.map((p) => p.id)).toEqual(['pdl', 'datagma']) + }) + + describe('pdl', () => { + const p = provider('pdl') + it('matches by name and normalizes the returned website', () => { + expect(p.toolId).toBe('pdl_company_enrich') + expect(p.buildParams(nameInput)).toEqual({ name: 'Acme Inc' }) + expect(p.buildParams({ companyName: '' })).toBeNull() + expect(p.mapOutput({ company: { website: 'https://www.acme.com' } })).toEqual({ + domain: 'acme.com', + }) + expect(p.mapOutput({ company: {} })).toBeNull() + }) + }) + + describe('datagma', () => { + const p = provider('datagma') + it('enriches by company name and normalizes the returned website', () => { + expect(p.toolId).toBe('datagma_enrich_company') + expect(p.buildParams(nameInput)).toEqual({ data: 'Acme Inc' }) + expect(p.buildParams({ companyName: '' })).toBeNull() + expect(p.mapOutput({ website: 'https://www.acme.com/' })).toEqual({ domain: 'acme.com' }) + expect(p.mapOutput({})).toBeNull() + }) + }) +}) diff --git a/apps/sim/enrichments/company-domain/company-domain.ts b/apps/sim/enrichments/company-domain/company-domain.ts index 7d6fc95860..7739d46df7 100644 --- a/apps/sim/enrichments/company-domain/company-domain.ts +++ b/apps/sim/enrichments/company-domain/company-domain.ts @@ -4,7 +4,7 @@ import type { EnrichmentConfig } from '@/enrichments/types' /** * Company Domain enrichment. Resolves a company's website domain from its name - * via a People Data Labs company match. + * via a People Data Labs company match, falling back to Datagma's company enrich. */ export const companyDomainEnrichment: EnrichmentConfig = { id: 'company-domain', @@ -29,5 +29,20 @@ export const companyDomainEnrichment: EnrichmentConfig = { return domain ? { domain } : null }, }), + toolProvider({ + id: 'datagma', + label: 'Datagma', + toolId: 'datagma_enrich_company', + buildParams: (inputs) => { + // Datagma's `data` accepts a company name and returns its website. + const data = str(inputs.companyName) + if (!data) return null + return { data } + }, + mapOutput: (output) => { + const domain = normalizeDomain(output.website) + return domain ? { domain } : null + }, + }), ], } diff --git a/apps/sim/enrichments/company-info/company-info.test.ts b/apps/sim/enrichments/company-info/company-info.test.ts new file mode 100644 index 0000000000..d799b2ddb2 --- /dev/null +++ b/apps/sim/enrichments/company-info/company-info.test.ts @@ -0,0 +1,67 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { companyInfoEnrichment } from '@/enrichments/company-info/company-info' +import type { EnrichmentProvider } from '@/enrichments/types' + +function provider(id: string): EnrichmentProvider { + const p = companyInfoEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in company-info cascade`) + return p +} + +const domainInput = { domain: 'https://www.acme.com/about' } + +describe('company-info enrichment cascade', () => { + it('chains the company-info providers in waterfall order', () => { + expect(companyInfoEnrichment.providers.map((p) => p.id)).toEqual([ + 'hunter', + 'pdl', + 'datagma', + 'leadmagic', + ]) + }) + + describe('hunter', () => { + const p = provider('hunter') + it('normalizes the domain and maps size/description', () => { + expect(p.toolId).toBe('hunter_companies_find') + expect(p.buildParams(domainInput)).toEqual({ domain: 'acme.com' }) + expect(p.buildParams({ domain: '' })).toBeNull() + expect(p.mapOutput({ size: '11-50', description: 'Payments' })).toEqual({ + employeeCount: '11-50', + description: 'Payments', + }) + expect(p.mapOutput({})).toEqual({}) + }) + }) + + describe('datagma', () => { + const p = provider('datagma') + it('passes the normalized domain as data and maps companySize/shortDescription', () => { + expect(p.toolId).toBe('datagma_enrich_company') + expect(p.buildParams(domainInput)).toEqual({ data: 'acme.com' }) + expect(p.buildParams({ domain: '' })).toBeNull() + expect(p.mapOutput({ companySize: '11-50', shortDescription: 'Payments' })).toEqual({ + employeeCount: '11-50', + description: 'Payments', + }) + expect(p.mapOutput({})).toEqual({}) + }) + }) + + describe('leadmagic', () => { + const p = provider('leadmagic') + it('searches by domain and prefers the headcount range', () => { + expect(p.toolId).toBe('leadmagic_company_search') + expect(p.buildParams(domainInput)).toEqual({ company_domain: 'acme.com' }) + expect(p.buildParams({ domain: '' })).toBeNull() + expect( + p.mapOutput({ employeeRange: '11-50', employeeCount: 42, description: 'Pay' }) + ).toEqual({ employeeCount: '11-50', description: 'Pay' }) + expect(p.mapOutput({ employeeCount: 42 })).toEqual({ employeeCount: '42' }) + expect(p.mapOutput({})).toEqual({}) + }) + }) +}) diff --git a/apps/sim/enrichments/company-info/company-info.ts b/apps/sim/enrichments/company-info/company-info.ts index b3fdc541ec..b67a8ed821 100644 --- a/apps/sim/enrichments/company-info/company-info.ts +++ b/apps/sim/enrichments/company-info/company-info.ts @@ -5,11 +5,11 @@ import type { EnrichmentConfig } from '@/enrichments/types' /** * Company Info enrichment. Looks up a company by domain, trying Hunter first - * (free) then People Data Labs as a fallback. Outputs are limited to the fields - * both providers reliably return — employee count and description — so the - * result stays consistent regardless of which provider fills the cell. - * `employeeCount` is a string so Hunter's range bucket (e.g. `"11-50"`) and - * PDL's exact count map onto the same column. + * (free) then People Data Labs, then Datagma and LeadMagic as fallbacks. Outputs + * are limited to the fields the providers reliably return — employee count and + * description — so the result stays consistent regardless of which provider fills + * the cell. `employeeCount` is a string so Hunter's range bucket (e.g. `"11-50"`), + * PDL's exact count, and LeadMagic's range all map onto the same column. */ export const companyInfoEnrichment: EnrichmentConfig = { id: 'company-info', @@ -55,5 +55,38 @@ export const companyInfoEnrichment: EnrichmentConfig = { }) }, }), + toolProvider({ + id: 'datagma', + label: 'Datagma', + toolId: 'datagma_enrich_company', + buildParams: (inputs) => { + const data = normalizeDomain(inputs.domain) + if (!data) return null + return { data } + }, + mapOutput: (output) => { + return filterUndefined({ + employeeCount: str(output.companySize) || undefined, + description: str(output.shortDescription) || undefined, + }) + }, + }), + toolProvider({ + id: 'leadmagic', + label: 'LeadMagic', + toolId: 'leadmagic_company_search', + buildParams: (inputs) => { + const companyDomain = normalizeDomain(inputs.domain) + if (!companyDomain) return null + return { company_domain: companyDomain } + }, + mapOutput: (output) => { + // Prefer the headcount range to match Hunter's bucket style; fall back to the exact count. + return filterUndefined({ + employeeCount: str(output.employeeRange) || str(output.employeeCount) || undefined, + description: str(output.description) || undefined, + }) + }, + }), ], } diff --git a/apps/sim/enrichments/email-verification/email-verification.test.ts b/apps/sim/enrichments/email-verification/email-verification.test.ts new file mode 100644 index 0000000000..694c8dd034 --- /dev/null +++ b/apps/sim/enrichments/email-verification/email-verification.test.ts @@ -0,0 +1,85 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { emailVerificationEnrichment } from '@/enrichments/email-verification/email-verification' +import type { EnrichmentProvider } from '@/enrichments/types' + +function provider(id: string): EnrichmentProvider { + const p = emailVerificationEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in email-verification cascade`) + return p +} + +const emailInput = { email: ' john@acme.com ' } + +describe('email-verification enrichment cascade', () => { + it('chains the hosted verifiers in waterfall order', () => { + expect(emailVerificationEnrichment.providers.map((p) => p.id)).toEqual([ + 'zerobounce', + 'neverbounce', + 'millionverifier', + 'icypeas', + 'enrow', + ]) + }) + + describe('zerobounce', () => { + const p = provider('zerobounce') + it('trims the email and falls through on missing/unknown verdict', () => { + expect(p.toolId).toBe('zerobounce_verify_email') + expect(p.buildParams(emailInput)).toEqual({ email: 'john@acme.com' }) + expect(p.buildParams({ email: '' })).toBeNull() + expect(p.mapOutput({ status: 'valid', deliverable: true })).toEqual({ + status: 'valid', + deliverable: true, + }) + expect(p.mapOutput({ status: 'unknown', deliverable: false })).toBeNull() + expect(p.mapOutput({})).toBeNull() + }) + }) + + describe('icypeas', () => { + const p = provider('icypeas') + it('maps FOUND/DEBITED to deliverable and NOT_FOUND to undeliverable', () => { + expect(p.toolId).toBe('icypeas_verify_email') + expect(p.buildParams(emailInput)).toEqual({ email: 'john@acme.com' }) + expect(p.buildParams({ email: '' })).toBeNull() + expect(p.mapOutput({ status: 'FOUND' })).toEqual({ status: 'valid', deliverable: true }) + expect(p.mapOutput({ status: 'DEBITED' })).toEqual({ status: 'valid', deliverable: true }) + expect(p.mapOutput({ status: 'NOT_FOUND' })).toEqual({ + status: 'invalid', + deliverable: false, + }) + expect(p.mapOutput({ status: 'DEBITED_NOT_FOUND' })).toEqual({ + status: 'invalid', + deliverable: false, + }) + }) + it('falls through on inconclusive statuses', () => { + expect(p.mapOutput({ status: 'BAD_INPUT' })).toBeNull() + expect(p.mapOutput({ status: 'INSUFFICIENT_FUNDS' })).toBeNull() + expect(p.mapOutput({ status: 'ABORTED' })).toBeNull() + expect(p.mapOutput({})).toBeNull() + }) + }) + + describe('enrow', () => { + const p = provider('enrow') + it('maps the valid/invalid qualifier and falls through otherwise', () => { + expect(p.toolId).toBe('enrow_verify_email') + expect(p.buildParams(emailInput)).toEqual({ email: 'john@acme.com' }) + expect(p.buildParams({ email: '' })).toBeNull() + expect(p.mapOutput({ qualification: 'valid' })).toEqual({ + status: 'valid', + deliverable: true, + }) + expect(p.mapOutput({ qualification: 'invalid' })).toEqual({ + status: 'invalid', + deliverable: false, + }) + expect(p.mapOutput({ qualification: null })).toBeNull() + expect(p.mapOutput({})).toBeNull() + }) + }) +}) diff --git a/apps/sim/enrichments/email-verification/email-verification.ts b/apps/sim/enrichments/email-verification/email-verification.ts index 71cb3f9b32..d6d7417c51 100644 --- a/apps/sim/enrichments/email-verification/email-verification.ts +++ b/apps/sim/enrichments/email-verification/email-verification.ts @@ -5,10 +5,11 @@ import type { EnrichmentConfig } from '@/enrichments/types' /** * Email Verification enrichment. Checks an email address's deliverability via a * verifier waterfall — ZeroBounce first (highest coverage), then NeverBounce, - * then MillionVerifier. A provider that returns a definitive verdict - * (valid / invalid / catch_all / disposable / etc.) fills the cell; a provider - * that can only return `unknown` falls through to the next so the row gets the - * most confident answer available. All providers support hosted keys. + * then MillionVerifier, then Icypeas, then Enrow. A provider that returns a + * definitive verdict (valid / invalid / catch_all / disposable / etc.) fills the + * cell; a provider that can only return `unknown` falls through to the next so + * the row gets the most confident answer available. All providers support hosted + * keys. */ export const emailVerificationEnrichment: EnrichmentConfig = { id: 'email-verification', @@ -67,5 +68,42 @@ export const emailVerificationEnrichment: EnrichmentConfig = { return { status, deliverable: output.deliverable === true } }, }), + toolProvider({ + id: 'icypeas', + label: 'Icypeas', + toolId: 'icypeas_verify_email', + buildParams: (inputs) => { + const email = str(inputs.email) + if (!email) return null + return { email } + }, + mapOutput: (output) => { + // FOUND/DEBITED → deliverable, NOT_FOUND/DEBITED_NOT_FOUND → undeliverable. + // Bad input / insufficient funds / aborted are inconclusive → fall through. + const status = str(output.status) + if (status === 'FOUND' || status === 'DEBITED') + return { status: 'valid', deliverable: true } + if (status === 'NOT_FOUND' || status === 'DEBITED_NOT_FOUND') + return { status: 'invalid', deliverable: false } + return null + }, + }), + toolProvider({ + id: 'enrow', + label: 'Enrow', + toolId: 'enrow_verify_email', + buildParams: (inputs) => { + const email = str(inputs.email) + if (!email) return null + return { email } + }, + mapOutput: (output) => { + // Enrow returns a "valid" / "invalid" qualifier; anything else is inconclusive. + const qualification = str(output.qualification).toLowerCase() + if (qualification === 'valid') return { status: 'valid', deliverable: true } + if (qualification === 'invalid') return { status: 'invalid', deliverable: false } + return null + }, + }), ], } diff --git a/apps/sim/enrichments/phone-number/phone-number.test.ts b/apps/sim/enrichments/phone-number/phone-number.test.ts index 2aa82c33ee..c8b998929c 100644 --- a/apps/sim/enrichments/phone-number/phone-number.test.ts +++ b/apps/sim/enrichments/phone-number/phone-number.test.ts @@ -21,6 +21,8 @@ describe('phone-number enrichment cascade', () => { 'wiza', 'findymail', 'prospeo', + 'leadmagic', + 'datagma', ]) }) @@ -75,4 +77,28 @@ describe('phone-number enrichment cascade', () => { expect(p.mapOutput({ person: { mobile: { mobile: '+1555' } } })).toEqual({ phone: '+1555' }) }) }) + + describe('leadmagic', () => { + const p = provider('leadmagic') + it('keys off the LinkedIn URL and skips without one', () => { + expect(p.toolId).toBe('leadmagic_find_mobile') + expect(p.buildParams(linkedinOnly)).toEqual({ + profile_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ mobile_number: '+1555' })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ mobile_number: null })).toBeNull() + }) + }) + + describe('datagma', () => { + const p = provider('datagma') + it('passes the LinkedIn URL as username and skips without one', () => { + expect(p.toolId).toBe('datagma_find_phone') + expect(p.buildParams(linkedinOnly)).toEqual({ username: 'https://linkedin.com/in/johndoe' }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ phone: '+1555' })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ phone: null })).toBeNull() + }) + }) }) diff --git a/apps/sim/enrichments/phone-number/phone-number.ts b/apps/sim/enrichments/phone-number/phone-number.ts index 6a7ad186cd..3680b677ac 100644 --- a/apps/sim/enrichments/phone-number/phone-number.ts +++ b/apps/sim/enrichments/phone-number/phone-number.ts @@ -7,9 +7,10 @@ import type { EnrichmentConfig } from '@/enrichments/types' * Phone Number enrichment. Finds a contact's phone number from a full name plus * any available identifiers (company domain, LinkedIn URL) via a waterfall: * People Data Labs (name match) → Wiza reveal → Findymail (LinkedIn) → Prospeo - * mobile. Each provider opportunistically uses whatever identifiers the row - * provides and self-skips when it has none usable, so adding more inputs widens - * coverage without reordering. First phone wins; all providers support hosted keys. + * mobile → LeadMagic (LinkedIn) → Datagma (LinkedIn). Each provider + * opportunistically uses whatever identifiers the row provides and self-skips + * when it has none usable, so adding more inputs widens coverage without + * reordering. First phone wins; all providers support hosted keys. */ export const phoneNumberEnrichment: EnrichmentConfig = { id: 'phone-number', @@ -106,5 +107,35 @@ export const phoneNumberEnrichment: EnrichmentConfig = { return phone ? { phone } : null }, }), + toolProvider({ + id: 'leadmagic', + label: 'LeadMagic', + toolId: 'leadmagic_find_mobile', + buildParams: (inputs) => { + // LeadMagic's mobile finder keys off a LinkedIn URL. + const profileUrl = str(inputs.linkedinUrl) + if (!profileUrl) return null + return { profile_url: profileUrl } + }, + mapOutput: (output) => { + const phone = str(output.mobile_number) + return phone ? { phone } : null + }, + }), + toolProvider({ + id: 'datagma', + label: 'Datagma', + toolId: 'datagma_find_phone', + buildParams: (inputs) => { + // Datagma's phone finder takes the full LinkedIn URL as `username`. + const username = str(inputs.linkedinUrl) + if (!username) return null + return { username } + }, + mapOutput: (output) => { + const phone = str(output.phone) + return phone ? { phone } : null + }, + }), ], } diff --git a/apps/sim/enrichments/work-email/work-email.test.ts b/apps/sim/enrichments/work-email/work-email.test.ts index 41bf50bc23..9f3f6801f1 100644 --- a/apps/sim/enrichments/work-email/work-email.test.ts +++ b/apps/sim/enrichments/work-email/work-email.test.ts @@ -23,6 +23,11 @@ describe('work-email enrichment cascade', () => { 'prospeo', 'wiza', 'pdl', + 'datagma', + 'leadmagic', + 'dropcontact', + 'icypeas', + 'enrow', ]) }) @@ -83,4 +88,83 @@ describe('work-email enrichment cascade', () => { expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) }) }) + + describe('datagma', () => { + const p = provider('datagma') + it('maps name + normalized company domain', () => { + expect(p.toolId).toBe('datagma_find_email') + expect(p.buildParams(nameDomain)).toEqual({ fullName: 'John Doe', company: 'acme.com' }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + expect(p.mapOutput({})).toBeNull() + }) + }) + + describe('leadmagic', () => { + const p = provider('leadmagic') + it('passes full_name + domain and keeps mononym rows', () => { + expect(p.toolId).toBe('leadmagic_find_email') + expect(p.buildParams(nameDomain)).toEqual({ full_name: 'John Doe', domain: 'acme.com' }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + // single-token name still runs (no longer skipped) + expect(p.buildParams({ fullName: 'Cher', companyDomain: 'acme.com' })).toEqual({ + full_name: 'Cher', + domain: 'acme.com', + }) + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + }) + }) + + describe('dropcontact', () => { + const p = provider('dropcontact') + it('enriches from name plus company or LinkedIn', () => { + expect(p.toolId).toBe('dropcontact_enrich_contact') + expect(p.buildParams(nameDomain)).toEqual({ full_name: 'John Doe', website: 'acme.com' }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + linkedin: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams({ companyDomain: 'acme.com' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + expect(p.mapOutput({})).toBeNull() + }) + }) + + describe('icypeas', () => { + const p = provider('icypeas') + it('splits the name when possible and keeps mononym rows', () => { + expect(p.toolId).toBe('icypeas_find_email') + expect(p.buildParams(nameDomain)).toEqual({ + firstname: 'John', + lastname: 'Doe', + domainOrCompany: 'acme.com', + }) + // single-token name runs with firstname alone (lastname is optional) + expect(p.buildParams({ fullName: 'Cher', companyDomain: 'acme.com' })).toEqual({ + firstname: 'Cher', + domainOrCompany: 'acme.com', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + }) + }) + + describe('enrow', () => { + const p = provider('enrow') + it('maps full name + company domain', () => { + expect(p.toolId).toBe('enrow_find_email') + expect(p.buildParams(nameDomain)).toEqual({ + fullname: 'John Doe', + company_domain: 'acme.com', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + // only a valid-qualified email fills the cell + expect(p.mapOutput({ email: 'j@acme.com', qualification: 'valid' })).toEqual({ + email: 'j@acme.com', + }) + expect(p.mapOutput({ email: 'j@acme.com', qualification: 'invalid' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toBeNull() + expect(p.mapOutput({})).toBeNull() + }) + }) }) diff --git a/apps/sim/enrichments/work-email/work-email.ts b/apps/sim/enrichments/work-email/work-email.ts index 2076e18eb4..6b5efc513c 100644 --- a/apps/sim/enrichments/work-email/work-email.ts +++ b/apps/sim/enrichments/work-email/work-email.ts @@ -8,7 +8,8 @@ import type { EnrichmentConfig } from '@/enrichments/types' * available identifiers (company domain, LinkedIn URL) via a provider waterfall: * deterministic finders first (Hunter, Findymail by name then by LinkedIn), then * enrichment/reveal providers (Prospeo, Wiza), then People Data Labs as a broad - * record-match fallback. Each provider opportunistically uses whatever + * record-match fallback, then Datagma, LeadMagic, Dropcontact, Icypeas, and Enrow + * as additional finders. Each provider opportunistically uses whatever * identifiers the row provides and self-skips when it has none usable, so adding * more inputs widens coverage. First email wins; all providers support hosted keys. */ @@ -133,5 +134,96 @@ export const workEmailEnrichment: EnrichmentConfig = { return email ? { email } : null }, }), + toolProvider({ + id: 'datagma', + label: 'Datagma', + toolId: 'datagma_find_email', + buildParams: (inputs) => { + const fullName = str(inputs.fullName) + const company = normalizeDomain(inputs.companyDomain) + if (!fullName || !company) return null + return { fullName, company } + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'leadmagic', + label: 'LeadMagic', + toolId: 'leadmagic_find_email', + buildParams: (inputs) => { + // LeadMagic accepts full_name + domain, so pass the whole name and let it + // split — this keeps single-token (mononym) rows in play. + const fullName = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + if (!fullName || !domain) return null + return { full_name: fullName, domain } + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'dropcontact', + label: 'Dropcontact', + toolId: 'dropcontact_enrich_contact', + buildParams: (inputs) => { + const fullName = str(inputs.fullName) + const website = normalizeDomain(inputs.companyDomain) + const linkedin = str(inputs.linkedinUrl) + if (!fullName || (!website && !linkedin)) return null + return filterUndefined({ + full_name: fullName, + website: website || undefined, + linkedin: linkedin || undefined, + }) + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'icypeas', + label: 'Icypeas', + toolId: 'icypeas_find_email', + buildParams: (inputs) => { + // Icypeas only requires domainOrCompany; firstname/lastname are optional, + // so a mononym still runs with firstname alone rather than self-skipping. + const fullName = str(inputs.fullName) + const domainOrCompany = normalizeDomain(inputs.companyDomain) + if (!fullName || !domainOrCompany) return null + const name = splitName(inputs.fullName) + return name + ? { firstname: name.firstName, lastname: name.lastName, domainOrCompany } + : { firstname: fullName, domainOrCompany } + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'enrow', + label: 'Enrow', + toolId: 'enrow_find_email', + buildParams: (inputs) => { + const fullname = str(inputs.fullName) + const company_domain = normalizeDomain(inputs.companyDomain) + if (!fullname || !company_domain) return null + return { fullname, company_domain } + }, + mapOutput: (output) => { + // Enrow qualifies each found email valid/invalid; only accept verified-valid + // results so the cell isn't filled with an address Enrow itself rejected + // (and which hosted billing correctly charges zero for). + const email = str(output.email) + const qualification = str(output.qualification).toLowerCase() + return email && qualification === 'valid' ? { email } : null + }, + }), ], } diff --git a/apps/sim/lib/api/contracts/byok-keys.ts b/apps/sim/lib/api/contracts/byok-keys.ts index 0c682458b8..0c4f10f708 100644 --- a/apps/sim/lib/api/contracts/byok-keys.ts +++ b/apps/sim/lib/api/contracts/byok-keys.ts @@ -29,6 +29,11 @@ export const byokProviderIdSchema = z.enum([ 'zerobounce', 'neverbounce', 'millionverifier', + 'datagma', + 'dropcontact', + 'leadmagic', + 'icypeas', + 'enrow', ]) /** Maximum number of keys a workspace may store per provider. */ diff --git a/apps/sim/tools/datagma-hosting.test.ts b/apps/sim/tools/datagma-hosting.test.ts new file mode 100644 index 0000000000..96b34a231e --- /dev/null +++ b/apps/sim/tools/datagma-hosting.test.ts @@ -0,0 +1,114 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { enrichCompanyTool } from '@/tools/datagma/enrich_company' +import { enrichPersonTool } from '@/tools/datagma/enrich_person' +import { findEmailTool } from '@/tools/datagma/find_email' +import { findPhoneTool } from '@/tools/datagma/find_phone' +import { getCreditsTool } from '@/tools/datagma/get_credits' +import { DATAGMA_CREDIT_USD } from '@/tools/datagma/hosting' +import type { ToolConfig } from '@/tools/types' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Datagma hosted key config', () => { + it('declares the shared env prefix and BYOK provider on all credit-consuming tools', () => { + for (const tool of [findEmailTool, enrichPersonTool, enrichCompanyTool, findPhoneTool]) { + expect(tool.hosting?.envKeyPrefix).toBe('DATAGMA_API_KEY') + expect(tool.hosting?.byokProviderId).toBe('datagma') + } + }) + + it('get_credits tool has no hosting config (always BYOK)', () => { + expect(getCreditsTool.hosting).toBeUndefined() + }) +}) + +describe('Datagma find email pricing', () => { + it('charges 1 credit when a verified email is found', () => { + expect(cost(findEmailTool, {}, { email: 'john@stripe.com' }).cost).toBeCloseTo( + DATAGMA_CREDIT_USD + ) + }) + + it('charges 0 credits when no email is returned', () => { + expect(cost(findEmailTool, {}, { email: null }).cost).toBe(0) + expect(cost(findEmailTool, {}, {}).cost).toBe(0) + }) +}) + +describe('Datagma enrich person pricing', () => { + it('charges 2 credits on a match without phone', () => { + expect( + cost(enrichPersonTool, {}, { name: 'John Doe', email: 'john@stripe.com', phone: null }).cost + ).toBeCloseTo(2 * DATAGMA_CREDIT_USD) + }) + + it('charges 32 credits (2 + 30) when a phone lookup was requested and found', () => { + expect( + cost( + enrichPersonTool, + { phoneFull: true }, + { name: 'John Doe', email: 'john@stripe.com', phone: '+14155551234' } + ).cost + ).toBeCloseTo(32 * DATAGMA_CREDIT_USD) + }) + + it('does not charge the phone surcharge when phoneFull was not requested', () => { + expect( + cost( + enrichPersonTool, + {}, + { name: 'John Doe', email: 'john@stripe.com', phone: '+14155551234' } + ).cost + ).toBeCloseTo(2 * DATAGMA_CREDIT_USD) + }) + + it('charges 0 credits on no match', () => { + expect(cost(enrichPersonTool, {}, { name: null, email: null }).cost).toBe(0) + expect(cost(enrichPersonTool, {}, {}).cost).toBe(0) + }) +}) + +describe('Datagma enrich company pricing', () => { + it('charges 2 credits on a match', () => { + expect(cost(enrichCompanyTool, {}, { name: 'Stripe', website: 'stripe.com' }).cost).toBeCloseTo( + 2 * DATAGMA_CREDIT_USD + ) + }) + + it('charges 2 credits when only website is present', () => { + expect(cost(enrichCompanyTool, {}, { name: null, website: 'stripe.com' }).cost).toBeCloseTo( + 2 * DATAGMA_CREDIT_USD + ) + }) + + it('charges 0 credits on no match', () => { + expect(cost(enrichCompanyTool, {}, { name: null, website: null }).cost).toBe(0) + expect(cost(enrichCompanyTool, {}, {}).cost).toBe(0) + }) +}) + +describe('Datagma find phone pricing', () => { + it('charges 30 credits when a phone number is found', () => { + expect(cost(findPhoneTool, {}, { phone: '+14155551234' }).cost).toBeCloseTo( + 30 * DATAGMA_CREDIT_USD + ) + }) + + it('charges 0 credits when no phone is returned', () => { + expect(cost(findPhoneTool, {}, { phone: null }).cost).toBe(0) + expect(cost(findPhoneTool, {}, {}).cost).toBe(0) + }) +}) diff --git a/apps/sim/tools/datagma/enrich_company.ts b/apps/sim/tools/datagma/enrich_company.ts new file mode 100644 index 0000000000..81391e61b3 --- /dev/null +++ b/apps/sim/tools/datagma/enrich_company.ts @@ -0,0 +1,132 @@ +import { datagmaHosting } from '@/tools/datagma/hosting' +import type { + DatagmaEnrichCompanyParams, + DatagmaEnrichCompanyResponse, +} from '@/tools/datagma/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Enrich a company profile from a company domain, name, or SIREN number. + * + * Endpoint: GET https://gateway.datagma.net/api/ingress/v2/full + * Auth: apiId query param + * Docs: https://datagmaapi.readme.io/reference/ingressservice_fullapiv2 + * Pricing: 2 credits per successful response + */ +export const enrichCompanyTool: ToolConfig< + DatagmaEnrichCompanyParams, + DatagmaEnrichCompanyResponse +> = { + id: 'datagma_enrich_company', + name: 'Datagma Enrich Company', + description: + 'Enrich a company profile using a domain, company name, or SIREN number (France). Returns size, industry, revenue, and description. Uses 2 credits per match.', + version: '1.0.0', + + hosting: datagmaHosting((_params, output) => { + const name = output.name as string | null + const website = output.website as string | null + return name || website ? 2 : 0 + }), + + params: { + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + "Company domain (e.g., 'stripe.com'), company name, or French SIREN number to enrich", + }, + companyPremium: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include LinkedIn company data in the response', + }, + companyFull: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include financial information in the response', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datagma API key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://gateway.datagma.net/api/ingress/v2/full') + url.searchParams.set('apiId', params.apiKey) + url.searchParams.set('data', params.data) + if (params.companyPremium != null) + url.searchParams.set('companyPremium', String(params.companyPremium)) + if (params.companyFull != null) + url.searchParams.set('companyFull', String(params.companyFull)) + return url.toString() + }, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).message || + `Datagma API error: ${response.status} ${response.statusText}`, + output: { + name: null, + website: null, + industries: null, + companySize: null, + type: null, + founded: null, + shortDescription: null, + revenueRange: null, + headquarters: null, + }, + } + } + const data = (await response.json()) as Record + + // Company data may be nested under a `company` key or returned at the top level + const company = (data.company ?? data) as Record + + return { + success: true, + output: { + name: (company.name as string | null) ?? null, + website: (company.website as string | null) ?? null, + industries: (company.industries as string | null) ?? null, + companySize: (company.companySize as string | null) ?? null, + type: (company.type as string | null) ?? null, + founded: (company.founded as string | null) ?? null, + shortDescription: (company.shortDescription as string | null) ?? null, + revenueRange: (company.revenueRange as string | null) ?? null, + headquarters: (company.headquarters as string | null) ?? null, + }, + } + }, + + outputs: { + name: { type: 'string', description: 'Company name', optional: true }, + website: { type: 'string', description: 'Company website', optional: true }, + industries: { type: 'string', description: 'Industry classification', optional: true }, + companySize: { type: 'string', description: 'Employee headcount range', optional: true }, + type: { type: 'string', description: 'Company type (e.g., Private, Public)', optional: true }, + founded: { type: 'string', description: 'Year founded', optional: true }, + shortDescription: { type: 'string', description: 'Short company description', optional: true }, + revenueRange: { + type: 'string', + description: 'Estimated annual revenue range', + optional: true, + }, + headquarters: { type: 'string', description: 'Headquarters location', optional: true }, + }, +} diff --git a/apps/sim/tools/datagma/enrich_person.ts b/apps/sim/tools/datagma/enrich_person.ts new file mode 100644 index 0000000000..2f1aaf0287 --- /dev/null +++ b/apps/sim/tools/datagma/enrich_person.ts @@ -0,0 +1,175 @@ +import { datagmaHosting } from '@/tools/datagma/hosting' +import type { DatagmaEnrichPersonParams, DatagmaEnrichPersonResponse } from '@/tools/datagma/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Enrich a person's profile from an email, LinkedIn URL, or full name + company. + * + * Endpoint: GET https://gateway.datagma.net/api/ingress/v2/full + * Auth: apiId query param + * Docs: https://datagmaapi.readme.io/reference/ingressservice_fullapiv2 + * Pricing: 2 credits per successful response; 30 additional credits when phone is found + */ +export const enrichPersonTool: ToolConfig = + { + id: 'datagma_enrich_person', + name: 'Datagma Enrich Person', + description: + "Enrich a person's profile using their email, LinkedIn URL, or full name and company. Returns job title, company, location, and social data. Uses 2 credits per match; add 30 credits when a phone number is found.", + version: '1.0.0', + + hosting: datagmaHosting((params, output) => { + const name = output.name as string | null + const email = output.email as string | null + if (!name && !email) return 0 + // The 30-credit phone surcharge applies only when the caller requested a + // phone lookup (phoneFull); a phone that rides along otherwise isn't charged. + const phoneCredits = params.phoneFull && output.phone ? 30 : 0 + return 2 + phoneCredits + }), + + params: { + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Email address, LinkedIn URL, or full name (use companyKeyword when providing a name)', + }, + companyKeyword: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name or keyword to disambiguate when data is a full name', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Two-letter country code to improve match accuracy (e.g., 'US', 'GB')", + }, + personFull: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include education and work history in the response', + }, + phoneFull: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Attempt to find a mobile phone number (costs 30 additional credits if found)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datagma API key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://gateway.datagma.net/api/ingress/v2/full') + url.searchParams.set('apiId', params.apiKey) + url.searchParams.set('data', params.data) + if (params.companyKeyword) url.searchParams.set('companyKeyword', params.companyKeyword) + if (params.countryCode) url.searchParams.set('countryCode', params.countryCode) + if (params.personFull != null) url.searchParams.set('personFull', String(params.personFull)) + if (params.phoneFull != null) url.searchParams.set('phoneFull', String(params.phoneFull)) + return url.toString() + }, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).message || + `Datagma API error: ${response.status} ${response.statusText}`, + output: { + name: null, + firstName: null, + lastName: null, + email: null, + emailStatus: null, + jobTitle: null, + company: null, + linkedInUrl: null, + location: null, + country: null, + region: null, + city: null, + extractedRole: null, + extractedSeniority: null, + twitter: null, + phone: null, + personConfidenceScore: null, + }, + } + } + const data = (await response.json()) as Record + + // Datagma nests phone numbers in an array; surface the first number's raw value + const phones = data.phones as Array> | null | undefined + const firstPhone = + Array.isArray(phones) && phones.length > 0 + ? ((phones[0].number as string | null) ?? null) + : null + + return { + success: true, + output: { + name: (data.name as string | null) ?? null, + firstName: (data.firstName as string | null) ?? null, + lastName: (data.lastName as string | null) ?? null, + email: (data.email as string | null) ?? null, + emailStatus: (data.emailStatus as string | null) ?? null, + jobTitle: (data.jobTitle as string | null) ?? null, + company: (data.company as string | null) ?? null, + linkedInUrl: (data.linkedInUrl as string | null) ?? null, + location: (data.location as string | null) ?? null, + country: (data.country as string | null) ?? null, + region: (data.region as string | null) ?? null, + city: (data.city as string | null) ?? null, + extractedRole: (data.extractedRole as string | null) ?? null, + extractedSeniority: (data.extractedSeniority as string | null) ?? null, + twitter: (data.twitter as string | null) ?? null, + phone: firstPhone, + personConfidenceScore: (data.personConfidenceScore as number | null) ?? null, + }, + } + }, + + outputs: { + name: { type: 'string', description: 'Full name', optional: true }, + firstName: { type: 'string', description: 'First name', optional: true }, + lastName: { type: 'string', description: 'Last name', optional: true }, + email: { type: 'string', description: 'Work email address', optional: true }, + emailStatus: { type: 'string', description: 'Email verification status', optional: true }, + jobTitle: { type: 'string', description: 'Current job title', optional: true }, + company: { type: 'string', description: 'Current company name', optional: true }, + linkedInUrl: { type: 'string', description: 'LinkedIn profile URL', optional: true }, + location: { type: 'string', description: 'Location string', optional: true }, + country: { type: 'string', description: 'Country', optional: true }, + region: { type: 'string', description: 'Region/state', optional: true }, + city: { type: 'string', description: 'City', optional: true }, + extractedRole: { type: 'string', description: 'Extracted role category', optional: true }, + extractedSeniority: { + type: 'string', + description: 'Extracted seniority level', + optional: true, + }, + twitter: { type: 'string', description: 'Twitter handle', optional: true }, + phone: { type: 'string', description: 'Mobile phone number', optional: true }, + personConfidenceScore: { + type: 'number', + description: 'Confidence score for the person match (0–1)', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/datagma/find_email.ts b/apps/sim/tools/datagma/find_email.ts new file mode 100644 index 0000000000..22bc38a019 --- /dev/null +++ b/apps/sim/tools/datagma/find_email.ts @@ -0,0 +1,130 @@ +import { datagmaHosting } from '@/tools/datagma/hosting' +import type { DatagmaFindEmailParams, DatagmaFindEmailResponse } from '@/tools/datagma/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Find a verified work email address from a full name and company. + * + * Endpoint: GET https://gateway.datagma.net/api/ingress/v6/findEmail + * Auth: apiId query param + * Docs: https://datagmaapi.readme.io/reference/find-work-email-address + * Pricing: 1 credit per verified email found (no charge for unverified/not found) + */ +export const findEmailTool: ToolConfig = { + id: 'datagma_find_email', + name: 'Datagma Find Email', + description: + "Find a verified work email from a person's full name and company. Uses 1 credit when a verified email is found.", + version: '1.0.0', + + hosting: datagmaHosting((_params, output) => { + const email = output.email as string | null + return email ? 1 : 0 + }), + + params: { + fullName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Person's full name (e.g., 'John Doe')", + }, + company: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Company name or domain (e.g., 'Stripe' or 'stripe.com')", + }, + linkedInSlug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn company URL slug to improve match accuracy by 20%+', + }, + findEmailV2Step: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Lookup depth: 3 = full email (default), 2 = domain only', + }, + findEmailV2Country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "User's location to improve accuracy (e.g., 'General', 'Japan', 'France')", + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datagma API key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://gateway.datagma.net/api/ingress/v6/findEmail') + url.searchParams.set('apiId', params.apiKey) + url.searchParams.set('fullName', params.fullName) + url.searchParams.set('company', params.company) + if (params.linkedInSlug) url.searchParams.set('linkedInSlug', params.linkedInSlug) + if (params.findEmailV2Step != null) + url.searchParams.set('findEmailV2Step', String(params.findEmailV2Step)) + if (params.findEmailV2Country) + url.searchParams.set('findEmailV2Country', params.findEmailV2Country) + return url.toString() + }, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).message || + `Datagma API error: ${response.status} ${response.statusText}`, + output: { + email: null, + emailStatus: null, + emailDomain: null, + mxfound: null, + smtpCheck: null, + catchAll: null, + }, + } + } + const data = (await response.json()) as Record + return { + success: true, + output: { + email: (data.email as string | null) ?? null, + emailStatus: (data.status as string | null) ?? null, + emailDomain: (data.emailDomain as string | null) ?? null, + mxfound: (data.mxfound as boolean | null) ?? null, + smtpCheck: (data.smtpCheck as boolean | null) ?? null, + // Datagma API spells this field "cachAll" (their documented typo); read both to be safe + catchAll: (data.cachAll as boolean | null) ?? (data.catchAll as boolean | null) ?? null, + }, + } + }, + + outputs: { + email: { type: 'string', description: 'Verified work email address', optional: true }, + emailStatus: { + type: 'string', + description: 'Email verification status (e.g., valid, invalid)', + optional: true, + }, + emailDomain: { type: 'string', description: 'Email domain', optional: true }, + mxfound: { type: 'boolean', description: 'Whether MX records were found', optional: true }, + smtpCheck: { + type: 'boolean', + description: 'Whether SMTP validation succeeded', + optional: true, + }, + catchAll: { type: 'boolean', description: 'Whether the domain is catch-all', optional: true }, + }, +} diff --git a/apps/sim/tools/datagma/find_phone.ts b/apps/sim/tools/datagma/find_phone.ts new file mode 100644 index 0000000000..7d3b019f0e --- /dev/null +++ b/apps/sim/tools/datagma/find_phone.ts @@ -0,0 +1,111 @@ +import { datagmaHosting } from '@/tools/datagma/hosting' +import type { DatagmaFindPhoneParams, DatagmaFindPhoneResponse } from '@/tools/datagma/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Find a mobile phone number from a LinkedIn URL (and optional email). + * + * Endpoint: GET https://gateway.datagma.net/api/ingress/v1/search + * Auth: apiId query param + * Docs: https://datagmaapi.readme.io/reference/find-a-phone-number + * Pricing: 30 credits per phone number found (same credit unit as email; 1 email = 1 credit) + * Pricing source: https://datagma.com/pricing ("30 credits = 1 mobile phone number") + */ +export const findPhoneTool: ToolConfig = { + id: 'datagma_find_phone', + name: 'Datagma Find Phone', + description: + "Find a mobile phone number from a person's LinkedIn URL. Optionally supply an email to improve match accuracy. Uses 30 credits when a number is found.", + version: '1.0.0', + + hosting: datagmaHosting((_params, output) => { + const phone = output.phone as string | null + return phone ? 30 : 0 + }), + + params: { + username: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "LinkedIn URL of the person (e.g., 'https://linkedin.com/in/johndoe')", + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address to improve phone match accuracy', + }, + minimumMatch: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum match confidence threshold (0–1; default 1 for highest precision)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datagma API key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://gateway.datagma.net/api/ingress/v1/search') + url.searchParams.set('apiId', params.apiKey) + url.searchParams.set('username', params.username) + if (params.email) url.searchParams.set('email', params.email) + if (params.minimumMatch != null) + url.searchParams.set('minimumMatch', String(params.minimumMatch)) + // Always request WhatsApp verification since we surface isWhatsapp in the output + url.searchParams.set('whatsappCheck', 'true') + return url.toString() + }, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).message || + `Datagma API error: ${response.status} ${response.statusText}`, + output: { phone: null, countryCode: null, isWhatsapp: null }, + } + } + const data = (await response.json()) as Record + + // Phone data may be nested under a `phones` array or returned at top level + const phones = data.phones as Array> | null | undefined + const firstPhone = Array.isArray(phones) && phones.length > 0 ? phones[0] : null + + return { + success: true, + output: { + phone: firstPhone + ? ((firstPhone.number as string | null) ?? null) + : ((data.phone as string | null) ?? null), + countryCode: firstPhone + ? ((firstPhone.countryCode as string | null) ?? null) + : ((data.countryCode as string | null) ?? null), + isWhatsapp: firstPhone + ? ((firstPhone.isWhatsapp as boolean | null) ?? null) + : ((data.isWhatsapp as boolean | null) ?? null), + }, + } + }, + + outputs: { + phone: { type: 'string', description: 'Mobile phone number', optional: true }, + countryCode: { type: 'string', description: 'Country code prefix (e.g., +1)', optional: true }, + isWhatsapp: { + type: 'boolean', + description: 'Whether the number is linked to WhatsApp', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/datagma/get_credits.ts b/apps/sim/tools/datagma/get_credits.ts new file mode 100644 index 0000000000..fecedc68aa --- /dev/null +++ b/apps/sim/tools/datagma/get_credits.ts @@ -0,0 +1,61 @@ +import type { DatagmaGetCreditsParams, DatagmaGetCreditsResponse } from '@/tools/datagma/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Check the remaining credit balance on a Datagma account. + * + * Endpoint: GET https://gateway.datagma.net/api/ingress/v1/mine + * Auth: apiId query param + * Docs: https://datagmaapi.readme.io/reference/ingressservice_getcredit + * Pricing: free (no credits consumed) + */ +export const getCreditsTool: ToolConfig = { + id: 'datagma_get_credits', + name: 'Datagma Get Credits', + description: 'Check remaining credit balance on a Datagma account. Free — no credits consumed.', + version: '1.0.0', + + // No hosting config — credit-balance lookup is free and should always use BYOK + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datagma API key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://gateway.datagma.net/api/ingress/v1/mine') + url.searchParams.set('apiId', params.apiKey) + return url.toString() + }, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).message || + `Datagma API error: ${response.status} ${response.statusText}`, + output: { credits: null }, + } + } + const data = (await response.json()) as Record + return { + success: true, + output: { + credits: (data.credit as number | null) ?? (data.credits as number | null) ?? null, + }, + } + }, + + outputs: { + credits: { type: 'number', description: 'Remaining Datagma credits', optional: true }, + }, +} diff --git a/apps/sim/tools/datagma/hosting.ts b/apps/sim/tools/datagma/hosting.ts new file mode 100644 index 0000000000..18dec37c7f --- /dev/null +++ b/apps/sim/tools/datagma/hosting.ts @@ -0,0 +1,49 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Datagma hosted keys. Provide keys as + * `DATAGMA_API_KEY_COUNT` plus `DATAGMA_API_KEY_1..N`. + * + * Note: Datagma authenticates via an `apiId` URL query parameter (its only + * documented scheme), so the key appears verbatim in every request URL and may + * be captured by Datagma's and any intermediary's access logs. Treat a leaked + * key accordingly and rotate via the env vars above. + */ +export const DATAGMA_API_KEY_PREFIX = 'DATAGMA_API_KEY' + +/** + * Dollar cost of a single Datagma credit. + * + * Based on the entry Regular plan ($49/month, 3,000 emails ≈ $0.0163/credit); + * per-credit drops at higher tiers (Popular/Expert) and on annual billing. + * Email finder: 1 credit per verified email. Phone finder: 30 credits per mobile. + * Enrichment: 2 credits per successful response. + * Pricing source: https://datagma.com/pricing + */ +export const DATAGMA_CREDIT_USD = 0.0163 + +/** + * Build a Datagma `hosting` config. `getCredits` returns the number of Datagma + * credits the call consumed, derived from the tool's output (per the documented + * per-endpoint credit model at https://datagmaapi.readme.io/reference/getting-started-with-your-api). + */ +export function datagmaHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: DATAGMA_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'datagma', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * DATAGMA_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/datagma/index.ts b/apps/sim/tools/datagma/index.ts new file mode 100644 index 0000000000..47c4abb90e --- /dev/null +++ b/apps/sim/tools/datagma/index.ts @@ -0,0 +1,13 @@ +export * from './types' + +import { enrichCompanyTool } from '@/tools/datagma/enrich_company' +import { enrichPersonTool } from '@/tools/datagma/enrich_person' +import { findEmailTool } from '@/tools/datagma/find_email' +import { findPhoneTool } from '@/tools/datagma/find_phone' +import { getCreditsTool } from '@/tools/datagma/get_credits' + +export const datagmaEnrichCompanyTool = enrichCompanyTool +export const datagmaEnrichPersonTool = enrichPersonTool +export const datagmaFindEmailTool = findEmailTool +export const datagmaFindPhoneTool = findPhoneTool +export const datagmaGetCreditsTool = getCreditsTool diff --git a/apps/sim/tools/datagma/types.ts b/apps/sim/tools/datagma/types.ts new file mode 100644 index 0000000000..c018eb3e4e --- /dev/null +++ b/apps/sim/tools/datagma/types.ts @@ -0,0 +1,151 @@ +import type { ToolResponse } from '@/tools/types' + +interface DatagmaBaseParams { + apiKey: string +} + +// --------------------------------------------------------------------------- +// Find Email (findEmail) +// Endpoint: GET https://gateway.datagma.net/api/ingress/v6/findEmail +// Auth: apiId query param +// Docs: https://datagmaapi.readme.io/reference/find-work-email-address +// --------------------------------------------------------------------------- + +export interface DatagmaFindEmailParams extends DatagmaBaseParams { + fullName: string + company: string + linkedInSlug?: string + findEmailV2Step?: number + findEmailV2Country?: string +} + +export interface DatagmaFindEmailResponse extends ToolResponse { + output: { + email: string | null + emailStatus: string | null + emailDomain: string | null + mxfound: boolean | null + smtpCheck: boolean | null + catchAll: boolean | null + } +} + +// --------------------------------------------------------------------------- +// Enrich Person +// Endpoint: GET https://gateway.datagma.net/api/ingress/v2/full +// Auth: apiId query param +// Docs: https://datagmaapi.readme.io/reference/ingressservice_fullapiv2 +// Pricing: 2 credits per successful response +// --------------------------------------------------------------------------- + +export interface DatagmaEnrichPersonParams extends DatagmaBaseParams { + /** Email address, LinkedIn URL, or full name (use with companyKeyword) */ + data: string + companyKeyword?: string + countryCode?: string + personFull?: boolean + phoneFull?: boolean +} + +export interface DatagmaEnrichPersonResponse extends ToolResponse { + output: { + name: string | null + firstName: string | null + lastName: string | null + email: string | null + emailStatus: string | null + jobTitle: string | null + company: string | null + linkedInUrl: string | null + location: string | null + country: string | null + region: string | null + city: string | null + extractedRole: string | null + extractedSeniority: string | null + twitter: string | null + phone: string | null + personConfidenceScore: number | null + } +} + +// --------------------------------------------------------------------------- +// Enrich Company (via full endpoint with company domain/name) +// Endpoint: GET https://gateway.datagma.net/api/ingress/v2/full +// Auth: apiId query param +// Docs: https://datagmaapi.readme.io/reference/ingressservice_fullapiv2 +// Pricing: 2 credits per successful response +// --------------------------------------------------------------------------- + +export interface DatagmaEnrichCompanyParams extends DatagmaBaseParams { + /** Company domain, name, or SIREN number */ + data: string + companyPremium?: boolean + companyFull?: boolean +} + +export interface DatagmaEnrichCompanyResponse extends ToolResponse { + output: { + name: string | null + website: string | null + industries: string | null + companySize: string | null + type: string | null + founded: string | null + shortDescription: string | null + revenueRange: string | null + headquarters: string | null + } +} + +// --------------------------------------------------------------------------- +// Find Phone (via search endpoint or enrich with phoneFull) +// Endpoint: GET https://gateway.datagma.net/api/ingress/v1/search +// Auth: apiId query param +// Docs: https://datagmaapi.readme.io/reference/find-a-phone-number +// Pricing: 30 credits per phone number found (1 credit = 1 email) +// --------------------------------------------------------------------------- + +export interface DatagmaFindPhoneParams extends DatagmaBaseParams { + /** LinkedIn URL of the person */ + username: string + /** Email address to improve match accuracy */ + email?: string + /** Minimum match confidence (0–1, default 1) */ + minimumMatch?: number +} + +export interface DatagmaFindPhoneResponse extends ToolResponse { + output: { + phone: string | null + countryCode: string | null + isWhatsapp: boolean | null + } +} + +// --------------------------------------------------------------------------- +// Get Credits +// Endpoint: GET https://gateway.datagma.net/api/ingress/v1/mine +// Auth: apiId query param +// Docs: https://datagmaapi.readme.io/reference/ingressservice_getcredit +// Pricing: free (no credit consumed) +// --------------------------------------------------------------------------- + +export interface DatagmaGetCreditsParams extends DatagmaBaseParams {} + +export interface DatagmaGetCreditsResponse extends ToolResponse { + output: { + credits: number | null + } +} + +// --------------------------------------------------------------------------- +// Union of all response types +// --------------------------------------------------------------------------- + +export type DatagmaResponse = + | DatagmaFindEmailResponse + | DatagmaEnrichPersonResponse + | DatagmaEnrichCompanyResponse + | DatagmaFindPhoneResponse + | DatagmaGetCreditsResponse diff --git a/apps/sim/tools/dropcontact-hosting.test.ts b/apps/sim/tools/dropcontact-hosting.test.ts new file mode 100644 index 0000000000..02d92b02c3 --- /dev/null +++ b/apps/sim/tools/dropcontact-hosting.test.ts @@ -0,0 +1,181 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { dropcontactEnrichContactTool } from '@/tools/dropcontact/enrich_contact' +import { DROPCONTACT_CREDIT_USD } from '@/tools/dropcontact/hosting' +import type { ToolConfig } from '@/tools/types' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Dropcontact hosted key config', () => { + it('declares hosting with the correct env prefix and BYOK provider ID', () => { + expect(dropcontactEnrichContactTool.hosting?.envKeyPrefix).toBe('DROPCONTACT_API_KEY') + expect(dropcontactEnrichContactTool.hosting?.byokProviderId).toBe('dropcontact') + }) +}) + +describe('Dropcontact hosted key pricing', () => { + it('charges 1 credit when email_found is true', () => { + expect( + cost(dropcontactEnrichContactTool, {}, { email_found: true, email: 'a@b.com' }).cost + ).toBeCloseTo(DROPCONTACT_CREDIT_USD) + }) + + it('charges 0 credits when email_found is false', () => { + expect(cost(dropcontactEnrichContactTool, {}, { email_found: false, email: null }).cost).toBe(0) + }) + + it('charges 0 credits when email_found is undefined/missing', () => { + expect(cost(dropcontactEnrichContactTool, {}, {}).cost).toBe(0) + }) +}) + +describe('Dropcontact postProcess polls to completion', () => { + it('polls the enrich endpoint until success:true and resolves the final output', async () => { + vi.useFakeTimers() + + // Mock: first call returns success:false (pending), second returns success:true (ready) + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + error: false, + success: false, + reason: 'Request not ready yet, try again in 30 seconds', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + error: false, + success: true, + data: [ + { + civility: 'Mr', + first_name: 'John', + last_name: 'Doe', + full_name: 'John Doe', + email: [{ email: 'john.doe@acme.com', qualification: 'nominative@pro' }], + phone: null, + mobile_phone: null, + company: 'Acme Corp', + website: 'acme.com', + company_linkedin: null, + linkedin: 'https://linkedin.com/in/johndoe', + siren: null, + siret: null, + siret_address: null, + vat: null, + nb_employees: '50-100', + naf5_code: null, + naf5_des: null, + industry: 'Software', + job: 'Software Engineer', + job_level: 'Senior', + job_function: 'Engineering', + company_turnover: null, + company_results: null, + }, + ], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { request_id: 'req_abc123' } as any, + } + const promise = dropcontactEnrichContactTool.postProcess!( + initial as any, + { apiKey: 'test-key' } as any, + vi.fn() + ) + + // Advance past two poll intervals + await vi.advanceTimersByTimeAsync(5000) + await vi.advanceTimersByTimeAsync(5000) + + const result = await promise + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.dropcontact.com/v1/enrich/all/req_abc123', + expect.objectContaining({ + headers: expect.objectContaining({ 'X-Access-Token': 'test-key' }), + }) + ) + expect(fetchMock).toHaveBeenCalledTimes(2) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('john.doe@acme.com') + expect((result.output as any).email_found).toBe(true) + expect((result.output as any).qualification).toBe('nominative@pro') + expect((result.output as any).first_name).toBe('John') + expect((result.output as any).company).toBe('Acme Corp') + expect((result.output as any).request_id).toBe('req_abc123') + }) + + it('throws if no request_id is present in the initial result', async () => { + const initial = { + success: true as const, + output: { request_id: null } as any, + } + await expect( + dropcontactEnrichContactTool.postProcess!(initial as any, { apiKey: 'k' } as any, vi.fn()) + ).rejects.toThrow('request_id') + }) + + it('throws if enrichment does not complete within the polling window', async () => { + vi.useFakeTimers() + + // Always returns pending — use a factory so each call gets a fresh Response body + const fetchMock = vi.fn().mockImplementation(() => + Promise.resolve( + new Response( + JSON.stringify({ + error: false, + success: false, + reason: 'Request not ready yet, try again in 30 seconds', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { request_id: 'req_timeout' } as any, + } + + let rejection: unknown + const promise = dropcontactEnrichContactTool.postProcess!( + initial as any, + { apiKey: 'k' } as any, + vi.fn() + ).catch((err) => { + rejection = err + }) + + // Advance past MAX_POLL_TIME_MS (120000ms) + await vi.advanceTimersByTimeAsync(125000) + await promise + + expect(rejection).toBeInstanceOf(Error) + expect((rejection as Error).message).toMatch(/polling window/) + }) +}) diff --git a/apps/sim/tools/dropcontact/enrich_contact.ts b/apps/sim/tools/dropcontact/enrich_contact.ts new file mode 100644 index 0000000000..a82322a0ad --- /dev/null +++ b/apps/sim/tools/dropcontact/enrich_contact.ts @@ -0,0 +1,368 @@ +import { sleep } from '@sim/utils/helpers' +import { dropcontactHosting } from '@/tools/dropcontact/hosting' +import type { + DropcontactEmailEntry, + DropcontactEnrichContactParams, + DropcontactEnrichContactResponse, + DropcontactEnrichedContact, +} from '@/tools/dropcontact/types' +import type { ToolConfig } from '@/tools/types' + +const POLL_INTERVAL_MS = 5000 +const MAX_POLL_TIME_MS = 120000 + +/** + * Map the first contact from the Dropcontact poll result data array to the + * flat tool output shape. + * + * @param contact - Raw contact object from the Dropcontact API poll response + * @returns Structured output matching `DropcontactEnrichContactResponse.output` + */ +function mapContactData( + requestId: string | null, + contact: DropcontactEnrichedContact +): DropcontactEnrichContactResponse['output'] { + const emailEntries = Array.isArray(contact.email) + ? (contact.email as DropcontactEmailEntry[]) + : null + const firstEmail = emailEntries?.[0] ?? null + + return { + request_id: requestId, + email_found: Boolean(firstEmail?.email), + email: firstEmail?.email ?? null, + emails: emailEntries, + qualification: firstEmail?.qualification ?? null, + first_name: contact.first_name ?? null, + last_name: contact.last_name ?? null, + full_name: contact.full_name ?? null, + civility: contact.civility ?? null, + phone: contact.phone ?? null, + mobile_phone: contact.mobile_phone ?? null, + company: contact.company ?? null, + website: contact.website ?? null, + company_linkedin: contact.company_linkedin ?? null, + linkedin: contact.linkedin ?? null, + country: contact.country ?? null, + siren: contact.siren ?? null, + siret: contact.siret ?? null, + siret_address: contact.siret_address ?? null, + siret_zip: contact.siret_zip ?? null, + siret_city: contact.siret_city ?? null, + vat: contact.vat ?? null, + nb_employees: contact.nb_employees ?? null, + employee_count: contact.employee_count ?? null, + naf5_code: contact.naf5_code ?? null, + naf5_des: contact.naf5_des ?? null, + industry: contact.industry ?? null, + job: contact.job ?? null, + job_level: contact.job_level ?? null, + job_function: contact.job_function ?? null, + company_turnover: contact.company_turnover ?? null, + company_results: contact.company_results ?? null, + } +} + +export const dropcontactEnrichContactTool: ToolConfig< + DropcontactEnrichContactParams, + DropcontactEnrichContactResponse +> = { + id: 'dropcontact_enrich_contact', + name: 'Dropcontact Enrich Contact', + description: + 'Enrich a contact with verified B2B email, phone, company data, and LinkedIn info via Dropcontact. Submits an async enrichment request, then polls until the result is ready (up to 2 minutes). Charges 1 credit only when a verified email is returned. Provide at least one of: email, first_name+last_name+company, full_name+company, or linkedin URL.', + version: '1.0.0', + + hosting: dropcontactHosting((_params, output) => { + // 1 credit per contact when a verified email is found. + // Source: https://developer.dropcontact.com (retrieved 2026-05) + return output.email_found === true ? 1 : 0 + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dropcontact API key (X-Access-Token)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the contact to enrich', + }, + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name of the contact', + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name of the contact', + }, + full_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name (alternative to first_name + last_name)', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website (e.g. acme.com)', + }, + num_siren: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'French company SIREN number', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number', + }, + linkedin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country code (ISO 3166-1 alpha-2, e.g. "US", "FR")', + }, + siren: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include SIREN/SIRET enrichment (France only)', + }, + language: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Language for returned data (e.g. "en", "fr")', + }, + }, + + request: { + // Submit endpoint: POST https://api.dropcontact.com/v1/enrich/all + // Source: https://developer.dropcontact.com (retrieved 2026-05) + url: 'https://api.dropcontact.com/v1/enrich/all', + method: 'POST', + headers: (params: DropcontactEnrichContactParams) => ({ + 'X-Access-Token': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params: DropcontactEnrichContactParams) => { + const contact: Record = {} + if (params.email) contact.email = params.email + if (params.first_name) contact.first_name = params.first_name + if (params.last_name) contact.last_name = params.last_name + if (params.full_name) contact.full_name = params.full_name + if (params.company) contact.company = params.company + if (params.website) contact.website = params.website + if (params.num_siren) contact.num_siren = params.num_siren + if (params.phone) contact.phone = params.phone + if (params.linkedin) contact.linkedin = params.linkedin + if (params.country) contact.country = params.country + + const body: Record = { data: [contact] } + if (params.siren !== undefined) body.siren = params.siren + if (params.language) body.language = params.language + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Dropcontact API error: ${response.status} - ${errorText}`) + } + const json = await response.json() + if (json.error) { + throw new Error(`Dropcontact API error: ${String(json.reason ?? json.error)}`) + } + // Submit response includes request_id; enrichment is async + return { + success: true, + output: { + request_id: (json.request_id as string) ?? null, + email_found: false, + email: null, + emails: null, + qualification: null, + first_name: null, + last_name: null, + full_name: null, + civility: null, + phone: null, + mobile_phone: null, + company: null, + website: null, + company_linkedin: null, + linkedin: null, + country: null, + siren: null, + siret: null, + siret_address: null, + siret_zip: null, + siret_city: null, + vat: null, + nb_employees: null, + employee_count: null, + naf5_code: null, + naf5_des: null, + industry: null, + job: null, + job_level: null, + job_function: null, + company_turnover: null, + company_results: null, + }, + } + }, + + postProcess: async (result, params) => { + if (!result.success) return result + + const requestId = result.output.request_id + if (!requestId) { + throw new Error('Dropcontact enrichment did not return a request_id') + } + + let elapsedTime = 0 + while (elapsedTime < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsedTime += POLL_INTERVAL_MS + + // Poll endpoint: GET https://api.dropcontact.com/v1/enrich/all/{request_id} + // Source: https://developer.dropcontact.com (retrieved 2026-05) + const pollResponse = await fetch( + `https://api.dropcontact.com/v1/enrich/all/${encodeURIComponent(requestId)}`, + { + headers: { + 'X-Access-Token': params.apiKey, + 'Content-Type': 'application/json', + }, + } + ) + + if (!pollResponse.ok) { + const errorText = await pollResponse.text() + throw new Error(`Dropcontact poll error: ${pollResponse.status} - ${errorText}`) + } + + const json = await pollResponse.json() + + // Error state: { error: true|string, reason?: string } + if (json.error) { + throw new Error(`Dropcontact enrichment failed: ${String(json.reason ?? json.error)}`) + } + + // Pending: { success: false, error: false, reason: "Request not ready yet..." } + if (!json.success) continue + + // Ready: { success: true, data: [...], error: false } + + const contacts = Array.isArray(json.data) ? json.data : [] + const contact = (contacts[0] ?? {}) as DropcontactEnrichedContact + + return { + success: true, + output: mapContactData(requestId, contact), + } + } + + throw new Error('Dropcontact enrichment did not complete within the polling window') + }, + + outputs: { + request_id: { type: 'string', description: 'Dropcontact async request ID', optional: true }, + email_found: { type: 'boolean', description: 'Whether a verified email was found' }, + email: { type: 'string', description: 'Primary verified email address', optional: true }, + emails: { + type: 'array', + description: 'All email addresses returned (each with email and qualification)', + optional: true, + items: { + type: 'object', + properties: { + email: { type: 'string', description: 'Email address' }, + qualification: { + type: 'string', + description: 'Email qualification (e.g. nominative@pro)', + }, + }, + }, + }, + qualification: { + type: 'string', + description: 'Primary email qualification (e.g. nominative@pro, catch_all@pro)', + optional: true, + }, + first_name: { type: 'string', description: 'First name', optional: true }, + last_name: { type: 'string', description: 'Last name', optional: true }, + full_name: { type: 'string', description: 'Full name', optional: true }, + civility: { type: 'string', description: 'Civility (Mr, Mrs, etc.)', optional: true }, + phone: { type: 'string', description: 'Phone number', optional: true }, + mobile_phone: { type: 'string', description: 'Mobile phone number', optional: true }, + company: { type: 'string', description: 'Company name', optional: true }, + website: { type: 'string', description: 'Company website', optional: true }, + company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, + linkedin: { type: 'string', description: 'Personal LinkedIn URL', optional: true }, + country: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)', optional: true }, + siren: { type: 'string', description: 'French SIREN number', optional: true }, + siret: { type: 'string', description: 'French SIRET number', optional: true }, + siret_address: { type: 'string', description: 'SIRET registered address', optional: true }, + siret_zip: { type: 'string', description: 'SIRET registered postal code', optional: true }, + siret_city: { type: 'string', description: 'SIRET registered city', optional: true }, + vat: { type: 'string', description: 'VAT number', optional: true }, + nb_employees: { type: 'string', description: 'Employee count range', optional: true }, + employee_count: { + type: 'number', + description: 'Exact employee count (Growth plan and above)', + optional: true, + }, + naf5_code: { type: 'string', description: 'NAF/APE code (France)', optional: true }, + naf5_des: { + type: 'string', + description: 'NAF/APE code description (France)', + optional: true, + }, + industry: { type: 'string', description: 'Industry classification', optional: true }, + job: { type: 'string', description: 'Job title', optional: true }, + job_level: { + type: 'string', + description: 'Job seniority level (e.g. C-level, Director)', + optional: true, + }, + job_function: { + type: 'string', + description: 'Job function (e.g. Sales, Engineering)', + optional: true, + }, + company_turnover: { + type: 'string', + description: 'Company revenue/turnover range', + optional: true, + }, + company_results: { type: 'string', description: 'Company net results', optional: true }, + }, +} diff --git a/apps/sim/tools/dropcontact/hosting.ts b/apps/sim/tools/dropcontact/hosting.ts new file mode 100644 index 0000000000..6ba68ad91d --- /dev/null +++ b/apps/sim/tools/dropcontact/hosting.ts @@ -0,0 +1,49 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Dropcontact hosted keys. Provide keys as + * `DROPCONTACT_API_KEY_COUNT` plus `DROPCONTACT_API_KEY_1..N`. + */ +export const DROPCONTACT_API_KEY_PREFIX = 'DROPCONTACT_API_KEY' + +/** + * Dollar cost of a single Dropcontact credit. + * + * Dropcontact's Starter plan is €79/month for 500 credits (≈ $0.158/credit at + * parity). Credits are only deducted when a verified business email is + * successfully returned; no charge if no email is found. + * + * Pricing source: https://www.dropcontact.com/pricing (retrieved 2026-05) + * + * NOTE: This is an approximation based on the Starter plan rate. Actual + * per-credit cost varies by plan tier and currency. A human should verify + * before deploying hosted-key billing. + */ +export const DROPCONTACT_CREDIT_USD = 0.17 + +/** + * Build a Dropcontact `hosting` config. `getCredits` returns the number of + * Dropcontact credits the call consumed, derived from the tool's final output. + */ +export function dropcontactHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: DROPCONTACT_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'dropcontact', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * DROPCONTACT_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + // Dropcontact rate limit: 60 requests per second = 3600 requests per minute + // Source: https://developer.dropcontact.com (retrieved 2026-05) + mode: 'per_request', + requestsPerMinute: 3600, + }, + } +} diff --git a/apps/sim/tools/dropcontact/index.ts b/apps/sim/tools/dropcontact/index.ts new file mode 100644 index 0000000000..055aed4852 --- /dev/null +++ b/apps/sim/tools/dropcontact/index.ts @@ -0,0 +1,5 @@ +export * from './types' + +import { dropcontactEnrichContactTool } from '@/tools/dropcontact/enrich_contact' + +export { dropcontactEnrichContactTool } diff --git a/apps/sim/tools/dropcontact/types.ts b/apps/sim/tools/dropcontact/types.ts new file mode 100644 index 0000000000..34d2fec5f0 --- /dev/null +++ b/apps/sim/tools/dropcontact/types.ts @@ -0,0 +1,140 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +export interface DropcontactBaseParams { + apiKey: string +} + +// --------------------------------------------------------------------------- +// Shared output property constants +// --------------------------------------------------------------------------- + +export const DROPCONTACT_EMAIL_ITEM_OUTPUT_PROPERTIES = { + email: { type: 'string', description: 'Email address' }, + qualification: { + type: 'string', + description: + 'Email qualification in the format @, e.g. nominative@pro, catch_all@pro, generic@perso', + }, +} as const satisfies Record + +export const DROPCONTACT_EMAILS_OUTPUT: OutputProperty = { + type: 'array', + description: 'All email addresses found for the contact', + items: { + type: 'object', + properties: DROPCONTACT_EMAIL_ITEM_OUTPUT_PROPERTIES, + }, +} + +// --------------------------------------------------------------------------- +// Enrich Contact (single-contact async enrichment) +// --------------------------------------------------------------------------- + +export interface DropcontactEnrichContactParams extends DropcontactBaseParams { + /** Email address of the contact to enrich */ + email?: string + /** First name of the contact */ + first_name?: string + /** Last name of the contact */ + last_name?: string + /** Full name (alternative to first_name + last_name) */ + full_name?: string + /** Company name */ + company?: string + /** Company website (e.g. acme.com) */ + website?: string + /** French company SIREN number */ + num_siren?: string + /** Phone number */ + phone?: string + /** LinkedIn profile URL */ + linkedin?: string + /** Country code (ISO 3166-1 alpha-2) */ + country?: string + /** Whether to include SIREN/SIRET enrichment (France only) */ + siren?: boolean + /** Language for returned data (e.g. "en", "fr") */ + language?: string +} + +/** Per-contact email entry returned by the Dropcontact API */ +export interface DropcontactEmailEntry { + email: string + qualification: string +} + +/** Enriched contact data returned in the poll result */ +export interface DropcontactEnrichedContact { + civility: string | null + first_name: string | null + last_name: string | null + full_name: string | null + email: DropcontactEmailEntry[] | null + phone: string | null + mobile_phone: string | null + company: string | null + website: string | null + company_linkedin: string | null + linkedin: string | null + country: string | null + siren: string | null + siret: string | null + siret_address: string | null + siret_zip: string | null + siret_city: string | null + vat: string | null + nb_employees: string | null + employee_count: number | null + naf5_code: string | null + naf5_des: string | null + industry: string | null + job: string | null + job_level: string | null + job_function: string | null + company_turnover: string | null + company_results: string | null +} + +export interface DropcontactEnrichContactResponse extends ToolResponse { + output: { + request_id: string | null + /** Whether the enrichment returned a verified email */ + email_found: boolean + /** First verified email address, if any */ + email: string | null + /** All emails returned by Dropcontact */ + emails: DropcontactEmailEntry[] | null + /** Email qualification (e.g. nominative@pro) */ + qualification: string | null + first_name: string | null + last_name: string | null + full_name: string | null + civility: string | null + phone: string | null + mobile_phone: string | null + company: string | null + website: string | null + company_linkedin: string | null + linkedin: string | null + country: string | null + siren: string | null + siret: string | null + siret_address: string | null + siret_zip: string | null + siret_city: string | null + vat: string | null + nb_employees: string | null + employee_count: number | null + naf5_code: string | null + naf5_des: string | null + industry: string | null + job: string | null + job_level: string | null + job_function: string | null + company_turnover: string | null + company_results: string | null + } +} + +/** Discriminated union of all Dropcontact tool responses */ +export type DropcontactResponse = DropcontactEnrichContactResponse diff --git a/apps/sim/tools/enrow-hosting.test.ts b/apps/sim/tools/enrow-hosting.test.ts new file mode 100644 index 0000000000..46e9d7a453 --- /dev/null +++ b/apps/sim/tools/enrow-hosting.test.ts @@ -0,0 +1,170 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { enrowFindEmailTool } from '@/tools/enrow/find_email' +import { ENROW_CREDIT_USD } from '@/tools/enrow/hosting' +import { enrowVerifyEmailTool } from '@/tools/enrow/verify_email' +import type { ToolConfig } from '@/tools/types' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost( + tool: ToolConfig, + params: unknown, + output: Record +) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Enrow hosted key config', () => { + it('declares the correct env key prefix and BYOK provider for find_email', () => { + expect(enrowFindEmailTool.hosting?.envKeyPrefix).toBe('ENROW_API_KEY') + expect(enrowFindEmailTool.hosting?.byokProviderId).toBe('enrow') + }) + + it('declares the correct env key prefix and BYOK provider for verify_email', () => { + expect(enrowVerifyEmailTool.hosting?.envKeyPrefix).toBe('ENROW_API_KEY') + expect(enrowVerifyEmailTool.hosting?.byokProviderId).toBe('enrow') + }) +}) + +describe('Enrow find_email pricing', () => { + it('charges 1 credit when qualification is valid (case-insensitive)', () => { + expect(cost(enrowFindEmailTool, {}, { qualification: 'valid' }).cost).toBeCloseTo( + 1 * ENROW_CREDIT_USD + ) + expect(cost(enrowFindEmailTool, {}, { qualification: 'VALID' }).cost).toBeCloseTo( + 1 * ENROW_CREDIT_USD + ) + }) + + it('charges 0 credits when qualification is invalid', () => { + expect(cost(enrowFindEmailTool, {}, { qualification: 'invalid' }).cost).toBe(0) + }) + + it('charges 0 credits when qualification is null (no result)', () => { + expect(cost(enrowFindEmailTool, {}, { qualification: null }).cost).toBe(0) + }) +}) + +describe('Enrow verify_email pricing', () => { + it('charges 0.25 credits for a completed verification (valid or invalid)', () => { + expect(cost(enrowVerifyEmailTool, {}, { qualification: 'valid' }).cost).toBeCloseTo( + 0.25 * ENROW_CREDIT_USD + ) + expect(cost(enrowVerifyEmailTool, {}, { qualification: 'invalid' }).cost).toBeCloseTo( + 0.25 * ENROW_CREDIT_USD + ) + }) + + it('charges 0 credits when the job did not complete (no qualification)', () => { + expect(cost(enrowVerifyEmailTool, {}, { qualification: null }).cost).toBe(0) + expect(cost(enrowVerifyEmailTool, {}, {}).cost).toBe(0) + }) +}) + +describe('Enrow find_email postProcess polling', () => { + it('polls until 200 and resolves the result', async () => { + vi.useFakeTimers() + + const fetchMock = vi + .fn() + // First poll → 202 (still in progress) + .mockResolvedValueOnce(new Response(null, { status: 202 })) + // Second poll → 200 (complete) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + email: 'john@stripe.com', + qualification: 'valid', + fullname: 'John Doe', + company_name: 'Stripe', + company_domain: 'stripe.com', + linkedin_url: 'https://linkedin.com/in/johndoe', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + id: 'abc-123', + email: null, + qualification: null, + fullname: null, + company_name: null, + company_domain: null, + linkedin_url: null, + }, + } + + const promise = enrowFindEmailTool.postProcess!( + initial as never, + { apiKey: 'test-key', fullname: 'John Doe', company_domain: 'stripe.com' } as never, + vi.fn() + ) + + // Advance past two POLL_INTERVAL_MS intervals (3000ms each) + await vi.advanceTimersByTimeAsync(3000) + await vi.advanceTimersByTimeAsync(3000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledTimes(2) + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.enrow.io/email/find/single?id=abc-123', + expect.objectContaining({ headers: expect.objectContaining({ 'x-api-key': 'test-key' }) }) + ) + expect(result.success).toBe(true) + expect((result.output as Record).email).toBe('john@stripe.com') + expect((result.output as Record).qualification).toBe('valid') + }) +}) + +describe('Enrow verify_email postProcess polling', () => { + it('polls until 200 and resolves the verification result', async () => { + vi.useFakeTimers() + + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + email: 'john@stripe.com', + qualification: 'valid', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 'xyz-456', email: null, qualification: null }, + } + + const promise = enrowVerifyEmailTool.postProcess!( + initial as never, + { apiKey: 'test-key', email: 'john@stripe.com' } as never, + vi.fn() + ) + + await vi.advanceTimersByTimeAsync(3000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.enrow.io/email/verify/single?id=xyz-456', + expect.objectContaining({ headers: expect.objectContaining({ 'x-api-key': 'test-key' }) }) + ) + expect(result.success).toBe(true) + expect((result.output as Record).qualification).toBe('valid') + }) +}) diff --git a/apps/sim/tools/enrow/find_email.ts b/apps/sim/tools/enrow/find_email.ts new file mode 100644 index 0000000000..8a44a28d61 --- /dev/null +++ b/apps/sim/tools/enrow/find_email.ts @@ -0,0 +1,193 @@ +import { sleep } from '@sim/utils/helpers' +import { enrowHosting } from '@/tools/enrow/hosting' +import type { + EnrowFindEmailParams, + EnrowFindEmailResponse, + EnrowFindEmailResult, +} from '@/tools/enrow/types' +import { + ENROW_EMAIL_OUTPUT, + ENROW_ID_OUTPUT, + ENROW_QUALIFICATION_OUTPUT, +} from '@/tools/enrow/types' +import type { ToolConfig } from '@/tools/types' + +const POLL_INTERVAL_MS = 3000 +const MAX_POLL_TIME_MS = 120_000 + +/** Map a raw Enrow find-email result payload to the typed output shape. */ +function mapFindResult(data: Record): EnrowFindEmailResult { + return { + id: (data.id as string) ?? '', + email: (data.email as string) ?? null, + qualification: (data.qualification as string) ?? null, + fullname: (data.fullname as string) ?? null, + company_name: (data.company_name as string) ?? null, + company_domain: (data.company_domain as string) ?? null, + linkedin_url: (data.linkedin_url as string) ?? null, + } +} + +/** + * Enrow — Find Email (single, async). + * + * Submits a search via `POST https://api.enrow.io/email/find/single`, receives + * a job `id`, then polls `GET https://api.enrow.io/email/find/single?id=` + * until HTTP 200 (complete) or the polling window expires. HTTP 202 means the + * search is still in progress. + * + * Pricing: 1 credit per valid email found (charged only on success). + * Docs: https://enrow.readme.io/reference/find-single-email + */ +export const enrowFindEmailTool: ToolConfig = { + id: 'enrow_find_email', + name: 'Enrow Find Email', + description: + 'Find a verified B2B email address from a full name and company domain or name. Uses the Enrow async finder — submits a search and polls until the result is ready. Costs 1 credit per valid email found. (https://enrow.readme.io/reference/find-single-email)', + version: '1.0.0', + + hosting: enrowHosting((_params, output) => { + // 1 credit charged only when a valid email is returned. Compare + // case-insensitively so the API's qualifier casing can't zero out billing. + return String(output.qualification ?? '').toLowerCase() === 'valid' ? 1 : 0 + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Enrow API key', + }, + fullname: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full name of the person (e.g. "John Doe")', + }, + company_domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company domain (e.g. "apple.com"). Preferred over company_name.', + }, + company_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name (e.g. "Apple"). Used when domain is unavailable.', + }, + }, + + request: { + url: 'https://api.enrow.io/email/find/single', + method: 'POST', + headers: (params: EnrowFindEmailParams) => ({ + 'x-api-key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params: EnrowFindEmailParams) => { + const body: Record = { fullname: params.fullname } + if (params.company_domain) body.company_domain = params.company_domain + if (params.company_name) body.company_name = params.company_name + return body + }, + }, + + transformResponse: async (response: Response): Promise => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Enrow API error: ${response.status} - ${errorText}`) + } + const json = await response.json() + const id = (json.id as string) ?? null + if (!id) { + throw new Error('Enrow find-email did not return a job id') + } + return { + success: true, + output: { + id, + email: null, + qualification: null, + fullname: null, + company_name: null, + company_domain: null, + linkedin_url: null, + }, + } + }, + + postProcess: async ( + result: EnrowFindEmailResponse, + params: EnrowFindEmailParams + ): Promise => { + if (!result.success) return result + + const jobId = result.output.id + if (!jobId) { + throw new Error('Enrow find-email did not return a job id to poll') + } + + let elapsed = 0 + while (elapsed < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsed += POLL_INTERVAL_MS + + const pollResponse = await fetch( + `https://api.enrow.io/email/find/single?id=${encodeURIComponent(jobId)}`, + { + headers: { + 'x-api-key': params.apiKey, + }, + } + ) + + if (pollResponse.status === 202) { + // Still in progress — keep polling + continue + } + + if (!pollResponse.ok) { + const errorText = await pollResponse.text() + throw new Error(`Enrow find-email poll error: ${pollResponse.status} - ${errorText}`) + } + + // HTTP 200 → complete + const json = await pollResponse.json() + const data = (json as Record) ?? {} + return { + success: true, + output: mapFindResult({ ...data, id: jobId }), + } + } + + throw new Error('Enrow find-email did not complete within the polling window') + }, + + outputs: { + id: ENROW_ID_OUTPUT, + email: ENROW_EMAIL_OUTPUT, + qualification: ENROW_QUALIFICATION_OUTPUT, + fullname: { + type: 'string', + description: 'Full name of the person searched', + optional: true, + }, + company_name: { + type: 'string', + description: 'Company name associated with the result', + optional: true, + }, + company_domain: { + type: 'string', + description: 'Company domain associated with the result', + optional: true, + }, + linkedin_url: { + type: 'string', + description: 'LinkedIn profile URL of the person', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/enrow/hosting.ts b/apps/sim/tools/enrow/hosting.ts new file mode 100644 index 0000000000..292905043e --- /dev/null +++ b/apps/sim/tools/enrow/hosting.ts @@ -0,0 +1,44 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Enrow hosted keys. Provide keys as `ENROW_API_KEY_COUNT` + * plus `ENROW_API_KEY_1..N`. + */ +export const ENROW_API_KEY_PREFIX = 'ENROW_API_KEY' + +/** + * Dollar cost of a single Enrow credit. + * + * Enrow's Starter plan is $24/month for 2,000 finder credits/month — $0.012 + * per credit. The email verifier costs 0.25 credits per verification and the + * email finder costs 1 credit per valid result. + * Source: https://enrow.io/pricing + */ +export const ENROW_CREDIT_USD = 0.012 + +/** + * Build an Enrow `hosting` config. `getCredits` returns the number of Enrow + * credits consumed by the call, derived from the tool's final output. + */ +export function enrowHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: ENROW_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'enrow', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * ENROW_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + // Enrow rate limit is ~50 req/s; cap at 60 req/min to stay conservative + // and avoid bursting into the limit during polling. + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/enrow/index.ts b/apps/sim/tools/enrow/index.ts new file mode 100644 index 0000000000..37c44e05bd --- /dev/null +++ b/apps/sim/tools/enrow/index.ts @@ -0,0 +1,6 @@ +export * from './types' + +import { enrowFindEmailTool } from '@/tools/enrow/find_email' +import { enrowVerifyEmailTool } from '@/tools/enrow/verify_email' + +export { enrowFindEmailTool, enrowVerifyEmailTool } diff --git a/apps/sim/tools/enrow/types.ts b/apps/sim/tools/enrow/types.ts new file mode 100644 index 0000000000..1638a9ad81 --- /dev/null +++ b/apps/sim/tools/enrow/types.ts @@ -0,0 +1,82 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +/** Common params shared by all Enrow tool operations. */ +export interface EnrowBaseParams { + apiKey: string +} + +// --------------------------------------------------------------------------- +// Email Finder — single +// --------------------------------------------------------------------------- + +export interface EnrowFindEmailParams extends EnrowBaseParams { + fullname: string + company_domain?: string + company_name?: string +} + +export interface EnrowFindEmailResult { + /** Job ID returned by the submit call; used to poll for the result. */ + id: string + email: string | null + /** Enrow quality qualifier: "valid" | "invalid" | null (if not yet finished). */ + qualification: string | null + fullname: string | null + company_name: string | null + company_domain: string | null + linkedin_url: string | null +} + +export interface EnrowFindEmailResponse extends ToolResponse { + output: EnrowFindEmailResult +} + +// --------------------------------------------------------------------------- +// Email Verifier — single +// --------------------------------------------------------------------------- + +export interface EnrowVerifyEmailParams extends EnrowBaseParams { + email: string +} + +export interface EnrowVerifyEmailResult { + /** Job ID returned by the submit call; used to poll for the result. */ + id: string + email: string | null + /** Enrow quality qualifier: "valid" | "invalid" | null (if not yet finished). */ + qualification: string | null +} + +export interface EnrowVerifyEmailResponse extends ToolResponse { + output: EnrowVerifyEmailResult +} + +// --------------------------------------------------------------------------- +// Union response type (used in BlockConfig generic) +// --------------------------------------------------------------------------- + +export type EnrowResponse = EnrowFindEmailResponse | EnrowVerifyEmailResponse + +// --------------------------------------------------------------------------- +// Shared output property constants +// --------------------------------------------------------------------------- + +/** Reusable output-property definition for the Enrow job ID. */ +export const ENROW_ID_OUTPUT: OutputProperty = { + type: 'string', + description: 'Enrow job identifier used for polling', +} + +/** Reusable output-property definition for the returned email address. */ +export const ENROW_EMAIL_OUTPUT: OutputProperty = { + type: 'string', + description: 'Email address found or verified', + optional: true, +} + +/** Reusable output-property definition for the qualification field. */ +export const ENROW_QUALIFICATION_OUTPUT: OutputProperty = { + type: 'string', + description: 'Enrow quality result: "valid" or "invalid"', + optional: true, +} diff --git a/apps/sim/tools/enrow/verify_email.ts b/apps/sim/tools/enrow/verify_email.ts new file mode 100644 index 0000000000..4821f32aa2 --- /dev/null +++ b/apps/sim/tools/enrow/verify_email.ts @@ -0,0 +1,151 @@ +import { sleep } from '@sim/utils/helpers' +import { enrowHosting } from '@/tools/enrow/hosting' +import type { + EnrowVerifyEmailParams, + EnrowVerifyEmailResponse, + EnrowVerifyEmailResult, +} from '@/tools/enrow/types' +import { + ENROW_EMAIL_OUTPUT, + ENROW_ID_OUTPUT, + ENROW_QUALIFICATION_OUTPUT, +} from '@/tools/enrow/types' +import type { ToolConfig } from '@/tools/types' + +const POLL_INTERVAL_MS = 3000 +const MAX_POLL_TIME_MS = 120_000 + +/** Map a raw Enrow verify-email result payload to the typed output shape. */ +function mapVerifyResult(data: Record, jobId: string): EnrowVerifyEmailResult { + return { + id: jobId, + email: (data.email as string) ?? null, + qualification: (data.qualification as string) ?? null, + } +} + +/** + * Enrow — Verify Email (single, async). + * + * Submits a verification via `POST https://api.enrow.io/email/verify/single`, + * receives a job `id`, then polls + * `GET https://api.enrow.io/email/verify/single?id=` until HTTP 200 + * (complete) or the polling window expires. HTTP 202 means still in progress. + * + * Pricing: 0.25 credits per verification (charged per call). + * Docs: https://enrow.readme.io/reference/verify-single-email + */ +export const enrowVerifyEmailTool: ToolConfig = { + id: 'enrow_verify_email', + name: 'Enrow Verify Email', + description: + 'Verify the deliverability of an email address using the Enrow async verifier. Submits a verification request and polls until the result is ready. Costs 0.25 credits per verification. (https://enrow.readme.io/reference/verify-single-email)', + version: '1.0.0', + + hosting: enrowHosting((_params, output) => { + // 0.25 credits per completed verification. Bill only when the job resolved + // to a qualification — a fall-back to the initial submit response (poll never + // finished) has no qualification and must not be charged. + return output.qualification ? 0.25 : 0 + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Enrow API key', + }, + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to verify (e.g. "john@example.com")', + }, + }, + + request: { + url: 'https://api.enrow.io/email/verify/single', + method: 'POST', + headers: (params: EnrowVerifyEmailParams) => ({ + 'x-api-key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params: EnrowVerifyEmailParams) => ({ + email: params.email, + }), + }, + + transformResponse: async (response: Response): Promise => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Enrow API error: ${response.status} - ${errorText}`) + } + const json = await response.json() + const id = (json.id as string) ?? null + if (!id) { + throw new Error('Enrow verify-email did not return a job id') + } + return { + success: true, + output: { + id, + email: null, + qualification: null, + }, + } + }, + + postProcess: async ( + result: EnrowVerifyEmailResponse, + params: EnrowVerifyEmailParams + ): Promise => { + if (!result.success) return result + + const jobId = result.output.id + if (!jobId) { + throw new Error('Enrow verify-email did not return a job id to poll') + } + + let elapsed = 0 + while (elapsed < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsed += POLL_INTERVAL_MS + + const pollResponse = await fetch( + `https://api.enrow.io/email/verify/single?id=${encodeURIComponent(jobId)}`, + { + headers: { + 'x-api-key': params.apiKey, + }, + } + ) + + if (pollResponse.status === 202) { + // Still in progress — keep polling + continue + } + + if (!pollResponse.ok) { + const errorText = await pollResponse.text() + throw new Error(`Enrow verify-email poll error: ${pollResponse.status} - ${errorText}`) + } + + // HTTP 200 → complete + const json = await pollResponse.json() + const data = (json as Record) ?? {} + return { + success: true, + output: mapVerifyResult(data, jobId), + } + } + + throw new Error('Enrow verify-email did not complete within the polling window') + }, + + outputs: { + id: ENROW_ID_OUTPUT, + email: ENROW_EMAIL_OUTPUT, + qualification: ENROW_QUALIFICATION_OUTPUT, + }, +} diff --git a/apps/sim/tools/icypeas-hosting.test.ts b/apps/sim/tools/icypeas-hosting.test.ts new file mode 100644 index 0000000000..288f2e773f --- /dev/null +++ b/apps/sim/tools/icypeas-hosting.test.ts @@ -0,0 +1,219 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { icypeasFindEmailTool } from '@/tools/icypeas/find_email' +import { ICYPEAS_CREDIT_USD } from '@/tools/icypeas/hosting' +import { icypeasVerifyEmailTool } from '@/tools/icypeas/verify_email' +import type { ToolConfig } from '@/tools/types' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Icypeas hosted key config', () => { + it('declares the correct env prefix and BYOK provider ID', () => { + expect(icypeasFindEmailTool.hosting?.envKeyPrefix).toBe('ICYPEAS_API_KEY') + expect(icypeasFindEmailTool.hosting?.byokProviderId).toBe('icypeas') + expect(icypeasVerifyEmailTool.hosting?.envKeyPrefix).toBe('ICYPEAS_API_KEY') + expect(icypeasVerifyEmailTool.hosting?.byokProviderId).toBe('icypeas') + }) +}) + +describe('Icypeas find-email pricing', () => { + it('charges 1 credit when status is FOUND', () => { + expect(cost(icypeasFindEmailTool, {}, { status: 'FOUND', email: 'a@b.com' }).cost).toBeCloseTo( + ICYPEAS_CREDIT_USD + ) + }) + + it('charges 1 credit when status is DEBITED', () => { + expect( + cost(icypeasFindEmailTool, {}, { status: 'DEBITED', email: 'a@b.com' }).cost + ).toBeCloseTo(ICYPEAS_CREDIT_USD) + }) + + it('charges 0 credits when the email was not found', () => { + expect(cost(icypeasFindEmailTool, {}, { status: 'NOT_FOUND', email: null }).cost).toBe(0) + expect(cost(icypeasFindEmailTool, {}, { status: 'DEBITED_NOT_FOUND', email: null }).cost).toBe( + 0 + ) + expect(cost(icypeasFindEmailTool, {}, { status: 'BAD_INPUT', email: null }).cost).toBe(0) + }) +}) + +describe('Icypeas verify-email pricing', () => { + it('charges 0.1 credits for FOUND status', () => { + expect( + cost(icypeasVerifyEmailTool, {}, { status: 'FOUND', email: 'a@b.com' }).cost + ).toBeCloseTo(0.1 * ICYPEAS_CREDIT_USD) + }) + + it('charges 0.1 credits for DEBITED status', () => { + expect( + cost(icypeasVerifyEmailTool, {}, { status: 'DEBITED', email: 'a@b.com' }).cost + ).toBeCloseTo(0.1 * ICYPEAS_CREDIT_USD) + }) + + it('charges 0.1 credits for DEBITED_NOT_FOUND (credits were consumed)', () => { + expect( + cost(icypeasVerifyEmailTool, {}, { status: 'DEBITED_NOT_FOUND', email: 'a@b.com' }).cost + ).toBeCloseTo(0.1 * ICYPEAS_CREDIT_USD) + }) + + it('charges 0 credits for non-billable statuses', () => { + expect(cost(icypeasVerifyEmailTool, {}, { status: 'NOT_FOUND', email: 'a@b.com' }).cost).toBe(0) + expect(cost(icypeasVerifyEmailTool, {}, { status: 'BAD_INPUT', email: 'a@b.com' }).cost).toBe(0) + expect( + cost(icypeasVerifyEmailTool, {}, { status: 'INSUFFICIENT_FUNDS', email: 'a@b.com' }).cost + ).toBe(0) + expect(cost(icypeasVerifyEmailTool, {}, { status: 'ABORTED', email: 'a@b.com' }).cost).toBe(0) + }) + + it('throws when status is missing', () => { + expect(() => cost(icypeasVerifyEmailTool, {}, { email: 'a@b.com' })).toThrow(/status/) + }) +}) + +describe('Icypeas find-email postProcess poll', () => { + it('polls the results endpoint until terminal status and returns the email', async () => { + vi.useFakeTimers() + + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + item: { + _id: 'abc123', + status: 'FOUND', + results: { + firstname: 'John', + lastname: 'Doe', + emails: [{ email: 'john@stripe.com', certainty: 'ultra_sure' }], + }, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + searchId: 'abc123', + status: 'NONE', + email: null, + firstname: null, + lastname: null, + item: { _id: 'abc123', status: 'NONE' }, + }, + } + + const promise = icypeasFindEmailTool.postProcess!( + initial as any, + { apiKey: 'test-key', domainOrCompany: 'stripe.com' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(3000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledWith( + 'https://app.icypeas.com/api/bulk-single-searchs/read', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ Authorization: 'test-key' }), + }) + ) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('john@stripe.com') + expect((result.output as any).status).toBe('FOUND') + }) + + it('returns success=true with a null email for NOT_FOUND terminal status', async () => { + vi.useFakeTimers() + + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + item: { _id: 'abc456', status: 'NOT_FOUND', email: null }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + searchId: 'abc456', + status: 'SCHEDULED', + email: null, + firstname: null, + lastname: null, + item: { _id: 'abc456', status: 'SCHEDULED' }, + }, + } + + const promise = icypeasFindEmailTool.postProcess!( + initial as any, + { apiKey: 'test-key', domainOrCompany: 'stripe.com' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(3000) + const result = await promise + + expect(result.success).toBe(true) + expect((result.output as any).status).toBe('NOT_FOUND') + expect((result.output as any).email).toBeNull() + }) +}) + +describe('Icypeas verify-email postProcess poll', () => { + it('polls the results endpoint until terminal status and returns valid=true for FOUND', async () => { + vi.useFakeTimers() + + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + item: { _id: 'xyz789', status: 'DEBITED', email: 'jane@example.com' }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + searchId: 'xyz789', + status: 'IN_PROGRESS', + email: 'jane@example.com', + valid: null, + item: { _id: 'xyz789', status: 'IN_PROGRESS' }, + }, + } + + const promise = icypeasVerifyEmailTool.postProcess!( + initial as any, + { apiKey: 'test-key', email: 'jane@example.com' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(3000) + const result = await promise + + expect(result.success).toBe(true) + expect((result.output as any).valid).toBe(true) + expect((result.output as any).status).toBe('DEBITED') + }) +}) diff --git a/apps/sim/tools/icypeas/find_email.ts b/apps/sim/tools/icypeas/find_email.ts new file mode 100644 index 0000000000..89fe64ab83 --- /dev/null +++ b/apps/sim/tools/icypeas/find_email.ts @@ -0,0 +1,195 @@ +import { sleep } from '@sim/utils/helpers' +import { icypeasHosting } from '@/tools/icypeas/hosting' +import type { + IcypeasFindEmailOutput, + IcypeasFindEmailParams, + IcypeasFindEmailResponse, +} from '@/tools/icypeas/types' +import { + ICYPEAS_EMAIL_OUTPUT, + ICYPEAS_ITEM_OUTPUT, + ICYPEAS_SEARCH_ID_OUTPUT, + ICYPEAS_STATUS_OUTPUT, +} from '@/tools/icypeas/types' +import type { ToolConfig } from '@/tools/types' + +/** Icypeas statuses that indicate the search has finished (success or failure). */ +const TERMINAL_STATUSES = new Set([ + 'FOUND', + 'DEBITED', + 'NOT_FOUND', + 'DEBITED_NOT_FOUND', + 'BAD_INPUT', + 'INSUFFICIENT_FUNDS', + 'ABORTED', +]) + +/** Icypeas statuses that indicate a result was actually found. */ +const FOUND_STATUSES = new Set(['FOUND', 'DEBITED']) + +const POLL_INTERVAL_MS = 3000 +const MAX_POLL_TIME_MS = 120000 + +/** Map a raw Icypeas item object to the tool output shape. */ +function mapItem(item: Record): IcypeasFindEmailOutput { + const status = (item.status as string | undefined) ?? null + // Results are nested under item.results; emails are in item.results.emails[0].email + const results = (item.results as Record | undefined) ?? {} + const emails = Array.isArray(results.emails) ? (results.emails as Record[]) : [] + const email = (emails[0]?.email as string | undefined) ?? null + const firstname = (results.firstname as string | undefined) ?? null + const lastname = (results.lastname as string | undefined) ?? null + return { + searchId: (item._id as string | undefined) ?? null, + status, + email, + firstname, + lastname, + item, + } +} + +export const icypeasFindEmailTool: ToolConfig = { + id: 'icypeas_find_email', + name: 'Icypeas Find Email', + description: + 'Find a professional email address from a first name, last name, and company domain or name. Submits the search and polls until a result is available. Costs 1 credit per found email (https://www.icypeas.com/pricing).', + version: '1.0.0', + + hosting: icypeasHosting((_params, output) => { + const status = output.status as string | undefined + // 1 credit charged only when a result is found (FOUND / DEBITED status). + return status && FOUND_STATUSES.has(status) ? 1 : 0 + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Icypeas API key', + }, + firstname: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Target person's first name", + }, + lastname: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Target person's last name", + }, + domainOrCompany: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Target company domain (e.g. stripe.com) or company name (e.g. Stripe)', + }, + }, + + request: { + url: 'https://app.icypeas.com/api/email-search', + method: 'POST', + headers: (params: IcypeasFindEmailParams) => ({ + Authorization: params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params: IcypeasFindEmailParams) => { + const body: Record = { + domainOrCompany: params.domainOrCompany, + } + if (params.firstname) body.firstname = params.firstname + if (params.lastname) body.lastname = params.lastname + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Icypeas API error: ${response.status} - ${errorText}`) + } + const json = (await response.json()) as Record + // Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } } + const item = (json.item as Record | undefined) ?? {} + const searchId = (item._id as string | undefined) ?? null + if (!searchId) { + throw new Error('Icypeas email-search did not return an item _id') + } + return { + success: true, + output: mapItem(item), + } + }, + + postProcess: async (result, params) => { + if (!result.success) return result + + const searchId = result.output.searchId + if (!searchId) { + throw new Error('Icypeas find-email result is missing a searchId') + } + + // If already terminal (unlikely on submit but defensive), return immediately. + if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) { + return result + } + + let elapsed = 0 + while (elapsed < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsed += POLL_INTERVAL_MS + + const pollResponse = await fetch('https://app.icypeas.com/api/bulk-single-searchs/read', { + method: 'POST', + headers: { + Authorization: params.apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: searchId }), + }) + + if (!pollResponse.ok) { + const errorText = await pollResponse.text() + throw new Error(`Icypeas poll error: ${pollResponse.status} - ${errorText}`) + } + + const json = (await pollResponse.json()) as Record + // Poll response: { success: true, item: { _id: '...', status: '...', results: { emails: [...], firstname, lastname } } } + const item = (json.item as Record | undefined) ?? {} + const status = (item.status as string | undefined) ?? null + + if (status && TERMINAL_STATUSES.has(status)) { + // Any terminal status is a successful run — a clean no-match is not a + // failure. The enrichment cascade only calls mapOutput when success is + // true, so returning false would skip the verdict and inflate the + // runner's error count. A null email signals "not found" downstream. + return { + success: true, + output: mapItem(item), + } + } + } + + throw new Error('Icypeas email-search did not complete within the polling window') + }, + + outputs: { + searchId: ICYPEAS_SEARCH_ID_OUTPUT, + status: ICYPEAS_STATUS_OUTPUT, + email: ICYPEAS_EMAIL_OUTPUT, + firstname: { + type: 'string', + description: "Found person's first name", + optional: true, + }, + lastname: { + type: 'string', + description: "Found person's last name", + optional: true, + }, + item: ICYPEAS_ITEM_OUTPUT, + }, +} diff --git a/apps/sim/tools/icypeas/hosting.ts b/apps/sim/tools/icypeas/hosting.ts new file mode 100644 index 0000000000..98e6ff9091 --- /dev/null +++ b/apps/sim/tools/icypeas/hosting.ts @@ -0,0 +1,50 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Icypeas hosted keys. Provide keys as `ICYPEAS_API_KEY_COUNT` + * plus `ICYPEAS_API_KEY_1..N`. + */ +export const ICYPEAS_API_KEY_PREFIX = 'ICYPEAS_API_KEY' + +/** + * Dollar cost of a single Icypeas credit. + * + * Icypeas meters usage in credits at approximately $0.019/credit on the entry + * Basic plan (1,000 credits for $19/month). Higher-tier plans reduce cost to as + * low as $0.00499/credit. We use the Basic-plan rate as a conservative baseline. + * + * Credit costs per operation (source: https://www.icypeas.com/pricing): + * - Email Finder: 1 credit per found email + * - Email Verifier: 0.1 credit per verification + * - Domain Scan: 1 credit per domain + * - Profile Scraper: 1.5 credits per profile + * - Reverse Email Lookup: 10 credits per found profile + * + * Credits are charged only when a result is returned (FOUND / DEBITED status). + */ +export const ICYPEAS_CREDIT_USD = 0.019 + +/** + * Build an Icypeas `hosting` config. `getCredits` returns the number of Icypeas + * credits the call consumed, derived from the tool's final output. + */ +export function icypeasHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: ICYPEAS_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'icypeas', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * ICYPEAS_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/icypeas/index.ts b/apps/sim/tools/icypeas/index.ts new file mode 100644 index 0000000000..69d83baeb1 --- /dev/null +++ b/apps/sim/tools/icypeas/index.ts @@ -0,0 +1,6 @@ +export * from './types' + +import { icypeasFindEmailTool } from '@/tools/icypeas/find_email' +import { icypeasVerifyEmailTool } from '@/tools/icypeas/verify_email' + +export { icypeasFindEmailTool, icypeasVerifyEmailTool } diff --git a/apps/sim/tools/icypeas/types.ts b/apps/sim/tools/icypeas/types.ts new file mode 100644 index 0000000000..44f0fafe62 --- /dev/null +++ b/apps/sim/tools/icypeas/types.ts @@ -0,0 +1,89 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +/** Base params shared by every Icypeas operation. */ +export interface IcypeasBaseParams { + apiKey: string +} + +// --------------------------------------------------------------------------- +// Email Finder (single email discovery) +// --------------------------------------------------------------------------- + +export interface IcypeasFindEmailParams extends IcypeasBaseParams { + firstname?: string + lastname?: string + domainOrCompany: string +} + +export interface IcypeasFindEmailOutput { + /** Icypeas internal search ID used to poll the result. */ + searchId: string | null + status: string | null + email: string | null + firstname: string | null + lastname: string | null + /** Raw item object from the results endpoint. */ + item: Record | null +} + +export interface IcypeasFindEmailResponse extends ToolResponse { + output: IcypeasFindEmailOutput +} + +// --------------------------------------------------------------------------- +// Email Verification +// --------------------------------------------------------------------------- + +export interface IcypeasVerifyEmailParams extends IcypeasBaseParams { + email: string +} + +export interface IcypeasVerifyEmailOutput { + /** Icypeas internal search ID used to poll the result. */ + searchId: string | null + status: string | null + email: string | null + /** Whether the email is valid/found. Derived from terminal status. */ + valid: boolean | null + /** Raw item object from the results endpoint. */ + item: Record | null +} + +export interface IcypeasVerifyEmailResponse extends ToolResponse { + output: IcypeasVerifyEmailOutput +} + +// --------------------------------------------------------------------------- +// Union response type used by the block +// --------------------------------------------------------------------------- + +export type IcypeasResponse = IcypeasFindEmailResponse | IcypeasVerifyEmailResponse + +// --------------------------------------------------------------------------- +// Shared output property constants +// --------------------------------------------------------------------------- + +export const ICYPEAS_SEARCH_ID_OUTPUT: OutputProperty = { + type: 'string', + description: 'Icypeas internal search ID', + optional: true, +} + +export const ICYPEAS_STATUS_OUTPUT: OutputProperty = { + type: 'string', + description: + 'Terminal search status: FOUND | DEBITED | NOT_FOUND | DEBITED_NOT_FOUND | BAD_INPUT | INSUFFICIENT_FUNDS | ABORTED', + optional: true, +} + +export const ICYPEAS_EMAIL_OUTPUT: OutputProperty = { + type: 'string', + description: 'Email address found or verified', + optional: true, +} + +export const ICYPEAS_ITEM_OUTPUT: OutputProperty = { + type: 'json', + description: 'Full raw item object returned by the Icypeas results endpoint', + optional: true, +} diff --git a/apps/sim/tools/icypeas/verify_email.ts b/apps/sim/tools/icypeas/verify_email.ts new file mode 100644 index 0000000000..827a81fc76 --- /dev/null +++ b/apps/sim/tools/icypeas/verify_email.ts @@ -0,0 +1,183 @@ +import { sleep } from '@sim/utils/helpers' +import { icypeasHosting } from '@/tools/icypeas/hosting' +import type { + IcypeasVerifyEmailOutput, + IcypeasVerifyEmailParams, + IcypeasVerifyEmailResponse, +} from '@/tools/icypeas/types' +import { + ICYPEAS_EMAIL_OUTPUT, + ICYPEAS_ITEM_OUTPUT, + ICYPEAS_SEARCH_ID_OUTPUT, + ICYPEAS_STATUS_OUTPUT, +} from '@/tools/icypeas/types' +import type { ToolConfig } from '@/tools/types' + +/** Icypeas statuses that indicate the search has finished (success or failure). */ +const TERMINAL_STATUSES = new Set([ + 'FOUND', + 'DEBITED', + 'NOT_FOUND', + 'DEBITED_NOT_FOUND', + 'BAD_INPUT', + 'INSUFFICIENT_FUNDS', + 'ABORTED', +]) + +/** Icypeas statuses that indicate the email address is valid/deliverable. */ +const VALID_STATUSES = new Set(['FOUND', 'DEBITED']) + +const POLL_INTERVAL_MS = 3000 +const MAX_POLL_TIME_MS = 120000 + +/** Map a raw Icypeas item object to the verify-email output shape. */ +function mapItem(item: Record): IcypeasVerifyEmailOutput { + const status = (item.status as string | undefined) ?? null + // Verify payloads put the address on item.email; fall back to the nested + // results.emails[0].email shape that some responses use. + const results = (item.results as Record | undefined) ?? {} + const emails = Array.isArray(results.emails) ? (results.emails as Record[]) : [] + const email = + (item.email as string | undefined) ?? (emails[0]?.email as string | undefined) ?? null + const valid = status !== null ? VALID_STATUSES.has(status) : null + return { + searchId: (item._id as string | undefined) ?? null, + status, + email, + valid, + item, + } +} + +export const icypeasVerifyEmailTool: ToolConfig< + IcypeasVerifyEmailParams, + IcypeasVerifyEmailResponse +> = { + id: 'icypeas_verify_email', + name: 'Icypeas Verify Email', + description: + 'Verify whether an email address is valid and deliverable. Submits the verification and polls until a result is available. Costs 0.1 credit per verification (https://www.icypeas.com/pricing).', + version: '1.0.0', + + hosting: icypeasHosting((_params, output) => { + // 0.1 credit per verification that consumed credits: FOUND/DEBITED (verdict + // delivered) and DEBITED_NOT_FOUND (debited even though unresolved). + // BAD_INPUT / INSUFFICIENT_FUNDS / ABORTED / NOT_FOUND are never charged. + const status = output.status as string | undefined + if (!status) { + throw new Error('Icypeas verify-email: cannot determine cost — status is missing') + } + const billable = status === 'FOUND' || status.includes('DEBITED') + // 0.1 credit; express as a fractional number so ICYPEAS_CREDIT_USD math works. + return billable ? 0.1 : 0 + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Icypeas API key', + }, + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to verify (e.g. john@stripe.com)', + }, + }, + + request: { + url: 'https://app.icypeas.com/api/email-verification', + method: 'POST', + headers: (params: IcypeasVerifyEmailParams) => ({ + Authorization: params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params: IcypeasVerifyEmailParams) => ({ + email: params.email, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Icypeas API error: ${response.status} - ${errorText}`) + } + const json = (await response.json()) as Record + // Submit response: { success: true, item: { _id: '...', status: 'NONE', ... } } + const item = (json.item as Record | undefined) ?? {} + const searchId = (item._id as string | undefined) ?? null + if (!searchId) { + throw new Error('Icypeas email-verification did not return an item _id') + } + return { + success: true, + output: mapItem(item), + } + }, + + postProcess: async (result, params) => { + if (!result.success) return result + + const searchId = result.output.searchId + if (!searchId) { + throw new Error('Icypeas verify-email result is missing a searchId') + } + + // If already terminal, return immediately. + if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) { + return result + } + + let elapsed = 0 + while (elapsed < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsed += POLL_INTERVAL_MS + + const pollResponse = await fetch('https://app.icypeas.com/api/bulk-single-searchs/read', { + method: 'POST', + headers: { + Authorization: params.apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id: searchId }), + }) + + if (!pollResponse.ok) { + const errorText = await pollResponse.text() + throw new Error(`Icypeas poll error: ${pollResponse.status} - ${errorText}`) + } + + const json = (await pollResponse.json()) as Record + // Poll response: { success: true, item: { _id: '...', status: '...', results: { emails: [...] } } } + const item = (json.item as Record | undefined) ?? {} + const status = (item.status as string | undefined) ?? null + + if (status && TERMINAL_STATUSES.has(status)) { + // Any terminal status is a successful run — NOT_FOUND/DEBITED_NOT_FOUND are + // definitive verdicts, not failures. The enrichment cascade only calls + // mapOutput when success is true, so returning false here would skip those + // verdicts and inflate the runner's error count. `valid` carries the result. + return { + success: true, + output: mapItem(item), + } + } + } + + throw new Error('Icypeas email-verification did not complete within the polling window') + }, + + outputs: { + searchId: ICYPEAS_SEARCH_ID_OUTPUT, + status: ICYPEAS_STATUS_OUTPUT, + email: ICYPEAS_EMAIL_OUTPUT, + valid: { + type: 'boolean', + description: 'Whether the email is valid/deliverable (true for FOUND/DEBITED status)', + optional: true, + }, + item: ICYPEAS_ITEM_OUTPUT, + }, +} diff --git a/apps/sim/tools/leadmagic-hosting.test.ts b/apps/sim/tools/leadmagic-hosting.test.ts new file mode 100644 index 0000000000..5bbd5f8dff --- /dev/null +++ b/apps/sim/tools/leadmagic-hosting.test.ts @@ -0,0 +1,129 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { companySearchTool } from '@/tools/leadmagic/company_search' +import { emailToProfileTool } from '@/tools/leadmagic/email_to_profile' +import { findEmailTool } from '@/tools/leadmagic/find_email' +import { findMobileTool } from '@/tools/leadmagic/find_mobile' +import { getCreditsTool } from '@/tools/leadmagic/get_credits' +import { LEADMAGIC_CREDIT_USD } from '@/tools/leadmagic/hosting' +import { profileSearchTool } from '@/tools/leadmagic/profile_search' +import { profileToEmailTool } from '@/tools/leadmagic/profile_to_email' +import { roleFinderTool } from '@/tools/leadmagic/role_finder' +import { validateEmailTool } from '@/tools/leadmagic/validate_email' +import type { ToolConfig } from '@/tools/types' + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('LeadMagic hosted key config', () => { + it('declares the correct env prefix and BYOK provider for all credit-consuming tools', () => { + const tools = [ + validateEmailTool, + findEmailTool, + findMobileTool, + profileSearchTool, + profileToEmailTool, + emailToProfileTool, + companySearchTool, + roleFinderTool, + ] + for (const tool of tools) { + expect(tool.hosting?.envKeyPrefix).toBe('LEADMAGIC_API_KEY') + expect(tool.hosting?.byokProviderId).toBe('leadmagic') + } + }) + + it('get_credits has no hosting config (free endpoint)', () => { + expect(getCreditsTool.hosting).toBeUndefined() + }) +}) + +describe('LeadMagic hosted key pricing', () => { + it('validate_email: uses API-reported credits_consumed', () => { + expect(cost(validateEmailTool, {}, { credits_consumed: 0.25 }).cost).toBeCloseTo( + 0.25 * LEADMAGIC_CREDIT_USD + ) + expect(cost(validateEmailTool, {}, { credits_consumed: 0 }).cost).toBe(0) + }) + + it('find_email: 1 credit when email found, 0 otherwise', () => { + expect(cost(findEmailTool, {}, { credits_consumed: 1 }).cost).toBeCloseTo(LEADMAGIC_CREDIT_USD) + expect(cost(findEmailTool, {}, { credits_consumed: 0, email: null }).cost).toBe(0) + // fallback path when credits_consumed missing + expect(cost(findEmailTool, {}, { email: 'a@b.com' }).cost).toBeCloseTo(LEADMAGIC_CREDIT_USD) + expect(cost(findEmailTool, {}, { email: null }).cost).toBe(0) + }) + + it('find_mobile: 5 credits when mobile found, 0 otherwise', () => { + expect(cost(findMobileTool, {}, { credits_consumed: 5 }).cost).toBeCloseTo( + 5 * LEADMAGIC_CREDIT_USD + ) + expect(cost(findMobileTool, {}, { credits_consumed: 0 }).cost).toBe(0) + // fallback path + expect(cost(findMobileTool, {}, { mobile_number: '+15551234567' }).cost).toBeCloseTo( + 5 * LEADMAGIC_CREDIT_USD + ) + expect(cost(findMobileTool, {}, { mobile_number: null }).cost).toBe(0) + }) + + it('profile_search: 1 credit when profile found, 0 otherwise', () => { + expect(cost(profileSearchTool, {}, { credits_consumed: 1 }).cost).toBeCloseTo( + LEADMAGIC_CREDIT_USD + ) + expect(cost(profileSearchTool, {}, { credits_consumed: 0 }).cost).toBe(0) + }) + + it('profile_to_email: 5 credits when email found, 0 otherwise', () => { + expect(cost(profileToEmailTool, {}, { credits_consumed: 5 }).cost).toBeCloseTo( + 5 * LEADMAGIC_CREDIT_USD + ) + expect(cost(profileToEmailTool, {}, { credits_consumed: 0 }).cost).toBe(0) + // fallback path + expect(cost(profileToEmailTool, {}, { email: 'a@b.com' }).cost).toBeCloseTo( + 5 * LEADMAGIC_CREDIT_USD + ) + expect(cost(profileToEmailTool, {}, { email: null }).cost).toBe(0) + }) + + it('email_to_profile: 10 credits when profile found, 0 otherwise', () => { + expect(cost(emailToProfileTool, {}, { credits_consumed: 10 }).cost).toBeCloseTo( + 10 * LEADMAGIC_CREDIT_USD + ) + expect(cost(emailToProfileTool, {}, { credits_consumed: 0 }).cost).toBe(0) + // fallback path + expect( + cost(emailToProfileTool, {}, { profile_url: 'https://linkedin.com/in/johndoe' }).cost + ).toBeCloseTo(10 * LEADMAGIC_CREDIT_USD) + expect(cost(emailToProfileTool, {}, { profile_url: null }).cost).toBe(0) + }) + + it('company_search: 1 credit when company found, 0 otherwise', () => { + expect(cost(companySearchTool, {}, { credits_consumed: 1 }).cost).toBeCloseTo( + LEADMAGIC_CREDIT_USD + ) + expect(cost(companySearchTool, {}, { credits_consumed: 0 }).cost).toBe(0) + // fallback path + expect(cost(companySearchTool, {}, { companyName: 'Stripe' }).cost).toBeCloseTo( + LEADMAGIC_CREDIT_USD + ) + expect(cost(companySearchTool, {}, { companyName: null }).cost).toBe(0) + }) + + it('role_finder: 2 credits when person found, 0 otherwise', () => { + expect(cost(roleFinderTool, {}, { credits_consumed: 2 }).cost).toBeCloseTo( + 2 * LEADMAGIC_CREDIT_USD + ) + expect(cost(roleFinderTool, {}, { credits_consumed: 0 }).cost).toBe(0) + // fallback path + expect(cost(roleFinderTool, {}, { full_name: 'John Doe' }).cost).toBeCloseTo( + 2 * LEADMAGIC_CREDIT_USD + ) + expect(cost(roleFinderTool, {}, { full_name: null }).cost).toBe(0) + }) +}) diff --git a/apps/sim/tools/leadmagic/company_search.ts b/apps/sim/tools/leadmagic/company_search.ts new file mode 100644 index 0000000000..e16e677d0b --- /dev/null +++ b/apps/sim/tools/leadmagic/company_search.ts @@ -0,0 +1,134 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicCompanySearchParams, + LeadMagicCompanySearchResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const companySearchTool: ToolConfig< + LeadMagicCompanySearchParams, + LeadMagicCompanySearchResponse +> = { + id: 'leadmagic_company_search', + name: 'LeadMagic Company Search', + description: + 'Enrich company data including firmographics, headcount, funding, and social profiles by domain, LinkedIn URL, or name. Charges 1 credit when a company is found; free when no result.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 1 credit when company found, 0 otherwise. + // Source: https://leadmagic.io/docs/v1/reference/company-search + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.companyName ? 1 : 0 + }), + + params: { + company_domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website domain (e.g., stripe.com). Provide at least one identifier.', + }, + profile_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'LinkedIn company profile URL (e.g., https://linkedin.com/company/stripe). Provide at least one identifier.', + }, + company_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Company name (fallback if domain/URL unavailable). Provide at least one identifier.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/companies/company-search', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.company_domain) body.company_domain = params.company_domain + if (params.profile_url) body.profile_url = params.profile_url + if (params.company_name) body.company_name = params.company_name + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + companyName: data.companyName ?? null, + companyId: data.companyId ?? null, + industry: data.industry ?? null, + employeeCount: data.employeeCount ?? null, + employeeRange: data.employeeRange ?? null, + founded: data.founded ?? null, + headquarters: data.headquarters ?? null, + revenue: data.revenue ?? null, + funding: data.funding ?? null, + description: data.description ?? null, + specialties: data.specialties ?? [], + competitors: data.competitors ?? [], + followerCount: data.followerCount ?? null, + twitter_url: data.twitter_url ?? null, + facebook_url: data.facebook_url ?? null, + b2b_profile_url: data.b2b_profile_url ?? null, + logo_url: data.logo_url ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + companyName: { type: 'string', description: 'Company name', optional: true }, + companyId: { type: 'number', description: 'Internal company identifier', optional: true }, + industry: { type: 'string', description: 'Industry classification', optional: true }, + employeeCount: { type: 'number', description: 'Number of employees', optional: true }, + employeeRange: { + type: 'string', + description: 'Headcount range (e.g., 1001-5000)', + optional: true, + }, + founded: { type: 'number', description: 'Year the company was founded', optional: true }, + headquarters: { type: 'json', description: 'Headquarters location object', optional: true }, + revenue: { type: 'string', description: 'Revenue range', optional: true }, + funding: { type: 'string', description: 'Total funding amount', optional: true }, + description: { type: 'string', description: 'Company description', optional: true }, + specialties: { type: 'array', description: 'Company specialties and focus areas' }, + competitors: { type: 'array', description: 'Competitor companies' }, + followerCount: { type: 'number', description: 'LinkedIn follower count', optional: true }, + twitter_url: { type: 'string', description: 'Twitter/X profile URL', optional: true }, + facebook_url: { type: 'string', description: 'Facebook page URL', optional: true }, + b2b_profile_url: { + type: 'string', + description: 'LinkedIn company profile URL', + optional: true, + }, + logo_url: { type: 'string', description: 'Company logo URL', optional: true }, + credits_consumed: { type: 'number', description: 'Credits charged (1 when company found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/email_to_profile.ts b/apps/sim/tools/leadmagic/email_to_profile.ts new file mode 100644 index 0000000000..51d11a61b2 --- /dev/null +++ b/apps/sim/tools/leadmagic/email_to_profile.ts @@ -0,0 +1,89 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicEmailToProfileParams, + LeadMagicEmailToProfileResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const emailToProfileTool: ToolConfig< + LeadMagicEmailToProfileParams, + LeadMagicEmailToProfileResponse +> = { + id: 'leadmagic_email_to_profile', + name: 'LeadMagic Email to Profile', + description: + 'Retrieve a LinkedIn profile URL from a work or personal email address. Charges 10 credits when a profile is found; free when no result.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 10 credits when profile found, 0 otherwise. + // Source: https://leadmagic.io/docs/v1/reference/email-to-profile + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.profile_url ? 10 : 0 + }), + + params: { + work_email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Work email address (provide at least one of work_email or personal_email)', + }, + personal_email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Personal email address (provide at least one of work_email or personal_email)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/b2b-profile', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.work_email) body.work_email = params.work_email + if (params.personal_email) body.personal_email = params.personal_email + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + profile_url: data.profile_url ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + profile_url: { + type: 'string', + description: 'LinkedIn profile URL for the provided email', + optional: true, + }, + credits_consumed: { type: 'number', description: 'Credits charged (10 when profile found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/find_email.ts b/apps/sim/tools/leadmagic/find_email.ts new file mode 100644 index 0000000000..dae11024ad --- /dev/null +++ b/apps/sim/tools/leadmagic/find_email.ts @@ -0,0 +1,130 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { LeadMagicFindEmailParams, LeadMagicFindEmailResponse } from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const findEmailTool: ToolConfig = { + id: 'leadmagic_find_email', + name: 'LeadMagic Find Email', + description: + "Find someone's verified work email from their name and company domain. Charges 1 credit when a valid email is found; free when no result.", + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 1 credit per valid email found, 0 credits when not found. + // Source: https://leadmagic.io/docs/v1/reference/email-finder + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.email ? 1 : 0 + }), + + params: { + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Person's first name (use with last_name, or use full_name instead)", + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Person's last name (use with first_name, or use full_name instead)", + }, + full_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Person's full name (alternative to first_name + last_name)", + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company domain (preferred, e.g. stripe.com)', + }, + company_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name (fallback if domain is unavailable)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/email-finder', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.first_name) body.first_name = params.first_name + if (params.last_name) body.last_name = params.last_name + if (params.full_name) body.full_name = params.full_name + if (params.domain) body.domain = params.domain + if (params.company_name) body.company_name = params.company_name + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + email: data.email ?? null, + status: data.status ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + employment_verified: data.employment_verified ?? null, + has_mx: data.has_mx ?? null, + mx_record: data.mx_record ?? null, + mx_provider: data.mx_provider ?? null, + company_name: data.company_name ?? null, + company_industry: data.company_industry ?? null, + company_size: data.company_size ?? null, + company_profile_url: data.company_profile_url ?? null, + }, + } + }, + + outputs: { + email: { type: 'string', description: 'Found work email address', optional: true }, + status: { type: 'string', description: 'Result status (valid, invalid, etc.)', optional: true }, + credits_consumed: { type: 'number', description: 'Credits charged (1 when email found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + employment_verified: { + type: 'boolean', + description: 'Whether employment at the company was verified', + optional: true, + }, + has_mx: { + type: 'boolean', + description: 'Whether the domain has a valid MX record', + optional: true, + }, + mx_record: { type: 'string', description: 'MX record for the email domain', optional: true }, + mx_provider: { type: 'string', description: 'Email provider', optional: true }, + company_name: { type: 'string', description: 'Company name', optional: true }, + company_industry: { type: 'string', description: 'Company industry', optional: true }, + company_size: { type: 'string', description: 'Company size range', optional: true }, + company_profile_url: { + type: 'string', + description: 'Company LinkedIn/B2B profile URL', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/leadmagic/find_mobile.ts b/apps/sim/tools/leadmagic/find_mobile.ts new file mode 100644 index 0000000000..859959a46b --- /dev/null +++ b/apps/sim/tools/leadmagic/find_mobile.ts @@ -0,0 +1,101 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicFindMobileParams, + LeadMagicFindMobileResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const findMobileTool: ToolConfig = { + id: 'leadmagic_find_mobile', + name: 'LeadMagic Find Mobile', + description: + "Find a person's direct mobile number from their LinkedIn profile URL or email. Charges 5 credits when a number is found; free when no result.", + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 5 credits per mobile number found, 0 when not found. + // Source: https://leadmagic.io/docs/v1/reference/mobile-finder + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.mobile_number ? 5 : 0 + }), + + params: { + profile_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL (provide at least one identifier)', + }, + work_email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Work email address (provide at least one identifier)', + }, + personal_email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Personal email address (provide at least one identifier)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/mobile-finder', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.profile_url) body.profile_url = params.profile_url + if (params.work_email) body.work_email = params.work_email + if (params.personal_email) body.personal_email = params.personal_email + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + profile_url: data.profile_url ?? null, + email: data.email ?? null, + mobile_number: data.mobile_number ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + profile_url: { + type: 'string', + description: 'LinkedIn profile URL used for lookup', + optional: true, + }, + email: { + type: 'string', + description: 'Email address associated with the profile', + optional: true, + }, + mobile_number: { type: 'string', description: 'Direct mobile phone number', optional: true }, + credits_consumed: { type: 'number', description: 'Credits charged (5 when mobile found)' }, + message: { type: 'string', description: 'Status message from the API', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/get_credits.ts b/apps/sim/tools/leadmagic/get_credits.ts new file mode 100644 index 0000000000..e680c7f144 --- /dev/null +++ b/apps/sim/tools/leadmagic/get_credits.ts @@ -0,0 +1,51 @@ +import type { + LeadMagicGetCreditsParams, + LeadMagicGetCreditsResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const getCreditsTool: ToolConfig = { + id: 'leadmagic_get_credits', + name: 'LeadMagic Get Credits', + description: + 'Retrieve the current credit balance for the authenticated LeadMagic account. This endpoint is free and consumes no credits.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/credits', + method: 'GET', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + credits: data.credits ?? 0, + }, + } + }, + + outputs: { + credits: { type: 'number', description: 'Current credit balance' }, + }, +} diff --git a/apps/sim/tools/leadmagic/hosting.ts b/apps/sim/tools/leadmagic/hosting.ts new file mode 100644 index 0000000000..0595097f68 --- /dev/null +++ b/apps/sim/tools/leadmagic/hosting.ts @@ -0,0 +1,48 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for LeadMagic hosted keys. Provide keys as + * `LEADMAGIC_API_KEY_COUNT` plus `LEADMAGIC_API_KEY_1..N`. + */ +export const LEADMAGIC_API_KEY_PREFIX = 'LEADMAGIC_API_KEY' + +/** + * Dollar cost of a single LeadMagic credit. + * + * LeadMagic charges only when data is found (not_found results are free). + * Based on the entry Basic plan ($49/month, 2,000 credits ≈ $0.0245/credit); + * per-credit drops at higher tiers (Essential/Growth). Email finder: 1 credit. + * Mobile finder: 5 credits. Company search: 1 credit. + * + * Source: https://leadmagic.io/pricing + */ +export const LEADMAGIC_CREDIT_USD = 0.0245 + +/** + * Build a LeadMagic `hosting` config. `getCredits` returns the number of + * LeadMagic credits the call consumed, derived from the tool's output (per the + * documented per-endpoint credit model at https://leadmagic.io/docs). + * + * LeadMagic responses include a `credits_consumed` field on every endpoint. + * When no result is found, `credits_consumed` is 0. + */ +export function leadmagicHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: LEADMAGIC_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'leadmagic', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * LEADMAGIC_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/leadmagic/index.ts b/apps/sim/tools/leadmagic/index.ts new file mode 100644 index 0000000000..bac5a556ca --- /dev/null +++ b/apps/sim/tools/leadmagic/index.ts @@ -0,0 +1,21 @@ +export * from './types' + +import { companySearchTool } from '@/tools/leadmagic/company_search' +import { emailToProfileTool } from '@/tools/leadmagic/email_to_profile' +import { findEmailTool } from '@/tools/leadmagic/find_email' +import { findMobileTool } from '@/tools/leadmagic/find_mobile' +import { getCreditsTool } from '@/tools/leadmagic/get_credits' +import { profileSearchTool } from '@/tools/leadmagic/profile_search' +import { profileToEmailTool } from '@/tools/leadmagic/profile_to_email' +import { roleFinderTool } from '@/tools/leadmagic/role_finder' +import { validateEmailTool } from '@/tools/leadmagic/validate_email' + +export const leadmagicValidateEmailTool = validateEmailTool +export const leadmagicFindEmailTool = findEmailTool +export const leadmagicFindMobileTool = findMobileTool +export const leadmagicProfileSearchTool = profileSearchTool +export const leadmagicProfileToEmailTool = profileToEmailTool +export const leadmagicEmailToProfileTool = emailToProfileTool +export const leadmagicCompanySearchTool = companySearchTool +export const leadmagicRoleFinderTool = roleFinderTool +export const leadmagicGetCreditsTool = getCreditsTool diff --git a/apps/sim/tools/leadmagic/profile_search.ts b/apps/sim/tools/leadmagic/profile_search.ts new file mode 100644 index 0000000000..aa52b679f2 --- /dev/null +++ b/apps/sim/tools/leadmagic/profile_search.ts @@ -0,0 +1,128 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicProfileSearchParams, + LeadMagicProfileSearchResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const profileSearchTool: ToolConfig< + LeadMagicProfileSearchParams, + LeadMagicProfileSearchResponse +> = { + id: 'leadmagic_profile_search', + name: 'LeadMagic Profile Search', + description: + 'Enrich a LinkedIn profile with work history, education, skills, and contact data. Charges 1 credit per successful enrichment; free when profile not found.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 1 credit per successful enrichment, 0 when not found. + // Source: https://leadmagic.io/docs/v1/reference/profile-search + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.full_name ? 1 : 0 + }), + + params: { + profile_url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL or username (e.g., https://linkedin.com/in/johndoe)', + }, + extended_response: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include additional profile image URL in the response (default: false)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/profile-search', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { profile_url: params.profile_url } + if (params.extended_response !== undefined) body.extended_response = params.extended_response + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + profile_url: data.profile_url ?? null, + first_name: data.first_name ?? null, + last_name: data.last_name ?? null, + full_name: data.full_name ?? null, + professional_title: data.professional_title ?? null, + bio: data.bio ?? null, + location: data.location ?? null, + country: data.country ?? null, + followers_range: data.followers_range ?? null, + company_name: data.company_name ?? null, + company_industry: data.company_industry ?? null, + company_website: data.company_website ?? null, + total_tenure_years: data.total_tenure_years ?? null, + total_tenure_months: data.total_tenure_months ?? null, + work_experience: data.work_experience ?? [], + education: data.education ?? [], + certifications: data.certifications ?? [], + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + profile_url: { type: 'string', description: 'LinkedIn profile URL', optional: true }, + first_name: { type: 'string', description: 'First name', optional: true }, + last_name: { type: 'string', description: 'Last name', optional: true }, + full_name: { type: 'string', description: 'Full name', optional: true }, + professional_title: { type: 'string', description: 'Current job title', optional: true }, + bio: { type: 'string', description: 'Profile bio / summary', optional: true }, + location: { type: 'string', description: 'Location string', optional: true }, + country: { type: 'string', description: 'Country', optional: true }, + followers_range: { type: 'string', description: 'LinkedIn follower range', optional: true }, + company_name: { type: 'string', description: 'Current employer', optional: true }, + company_industry: { + type: 'string', + description: 'Industry of current employer', + optional: true, + }, + company_website: { type: 'string', description: 'Company website', optional: true }, + total_tenure_years: { + type: 'string', + description: 'Total professional tenure in years', + optional: true, + }, + total_tenure_months: { + type: 'string', + description: 'Total professional tenure in months', + optional: true, + }, + work_experience: { type: 'array', description: 'Work history entries' }, + education: { type: 'array', description: 'Education history entries' }, + certifications: { type: 'array', description: 'Professional certifications' }, + credits_consumed: { type: 'number', description: 'Credits charged (1 when profile found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/profile_to_email.ts b/apps/sim/tools/leadmagic/profile_to_email.ts new file mode 100644 index 0000000000..0d3acd6413 --- /dev/null +++ b/apps/sim/tools/leadmagic/profile_to_email.ts @@ -0,0 +1,84 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicProfileToEmailParams, + LeadMagicProfileToEmailResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const profileToEmailTool: ToolConfig< + LeadMagicProfileToEmailParams, + LeadMagicProfileToEmailResponse +> = { + id: 'leadmagic_profile_to_email', + name: 'LeadMagic Profile to Email', + description: + 'Extract a verified work email from a LinkedIn profile URL. Charges 5 credits when an email is found; free when no result.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 5 credits when email found, 0 otherwise. + // Source: https://leadmagic.io/docs/v1/reference/profile-to-email + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.email ? 5 : 0 + }), + + params: { + profile_url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL or username (e.g., https://linkedin.com/in/johndoe)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/b2b-profile-email', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => ({ profile_url: params.profile_url }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + email: data.email ?? null, + profile_url: data.profile_url ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + email: { + type: 'string', + description: 'Work email address found for this profile', + optional: true, + }, + profile_url: { + type: 'string', + description: 'LinkedIn profile URL used for lookup', + optional: true, + }, + credits_consumed: { type: 'number', description: 'Credits charged (5 when email found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/role_finder.ts b/apps/sim/tools/leadmagic/role_finder.ts new file mode 100644 index 0000000000..0eaee0e55a --- /dev/null +++ b/apps/sim/tools/leadmagic/role_finder.ts @@ -0,0 +1,100 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicRoleFinderParams, + LeadMagicRoleFinderResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const roleFinderTool: ToolConfig = { + id: 'leadmagic_role_finder', + name: 'LeadMagic Role Finder', + description: + 'Find the person holding a specific job role at a company. Charges 2 credits when a matching person is found; free when no result.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 2 credits when a person is found, 0 otherwise. + // Source: https://leadmagic.io/docs/v1/reference/role-finder + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : output.full_name ? 2 : 0 + }), + + params: { + job_title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Job role to search for (e.g., Head of Sales, CTO). Supports partial matching.', + }, + company_domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website domain (e.g., stripe.com). Provide domain or company_name.', + }, + company_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name (fallback if domain unavailable). Provide domain or company_name.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/role-finder', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { job_title: params.job_title } + if (params.company_domain) body.company_domain = params.company_domain + if (params.company_name) body.company_name = params.company_name + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + first_name: data.first_name ?? null, + last_name: data.last_name ?? null, + full_name: data.full_name ?? null, + profile_url: data.profile_url ?? null, + job_title: data.job_title ?? null, + company_name: data.company_name ?? null, + company_website: data.company_website ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + }, + } + }, + + outputs: { + first_name: { type: 'string', description: 'First name of the person found', optional: true }, + last_name: { type: 'string', description: 'Last name of the person found', optional: true }, + full_name: { type: 'string', description: 'Full name of the person found', optional: true }, + profile_url: { type: 'string', description: 'LinkedIn profile URL', optional: true }, + job_title: { type: 'string', description: 'Verified job title at the company', optional: true }, + company_name: { type: 'string', description: 'Company name', optional: true }, + company_website: { type: 'string', description: 'Company website', optional: true }, + credits_consumed: { type: 'number', description: 'Credits charged (2 when person found)' }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + }, +} diff --git a/apps/sim/tools/leadmagic/types.ts b/apps/sim/tools/leadmagic/types.ts new file mode 100644 index 0000000000..394106f38a --- /dev/null +++ b/apps/sim/tools/leadmagic/types.ts @@ -0,0 +1,249 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +interface LeadMagicBaseParams { + apiKey: string +} + +// --------------------------------------------------------------------------- +// Shared output property constants +// --------------------------------------------------------------------------- + +export const LEADMAGIC_PROFILE_OUTPUT_PROPERTIES = { + profile_url: { type: 'string', description: 'LinkedIn profile URL' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + full_name: { type: 'string', description: 'Full name' }, + professional_title: { type: 'string', description: 'Current job title', optional: true }, + bio: { type: 'string', description: 'Profile bio / summary', optional: true }, + location: { type: 'string', description: 'Location string', optional: true }, + country: { type: 'string', description: 'Country', optional: true }, + company_name: { type: 'string', description: 'Current employer', optional: true }, + company_industry: { type: 'string', description: 'Industry of current employer', optional: true }, + company_website: { type: 'string', description: 'Company website', optional: true }, +} as const satisfies Record + +// --------------------------------------------------------------------------- +// Email Validation +// --------------------------------------------------------------------------- + +export interface LeadMagicValidateEmailParams extends LeadMagicBaseParams { + email: string +} + +export interface LeadMagicValidateEmailResponse extends ToolResponse { + output: { + email: string + email_status: string + is_domain_catch_all: boolean | null + credits_consumed: number + message: string | null + mx_record: string | null + mx_provider: string | null + mx_gateway: string | null + mx_security_gateway: boolean | null + company_name: string | null + company_industry: string | null + company_size: string | null + } +} + +// --------------------------------------------------------------------------- +// Email Finder +// --------------------------------------------------------------------------- + +export interface LeadMagicFindEmailParams extends LeadMagicBaseParams { + first_name?: string + last_name?: string + full_name?: string + domain?: string + company_name?: string +} + +export interface LeadMagicFindEmailResponse extends ToolResponse { + output: { + email: string | null + status: string | null + credits_consumed: number + message: string | null + employment_verified: boolean | null + has_mx: boolean | null + mx_record: string | null + mx_provider: string | null + company_name: string | null + company_industry: string | null + company_size: string | null + company_profile_url: string | null + } +} + +// --------------------------------------------------------------------------- +// Mobile Finder +// --------------------------------------------------------------------------- + +export interface LeadMagicFindMobileParams extends LeadMagicBaseParams { + profile_url?: string + work_email?: string + personal_email?: string +} + +export interface LeadMagicFindMobileResponse extends ToolResponse { + output: { + profile_url: string | null + email: string | null + mobile_number: string | null + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Profile Search (LinkedIn enrichment by profile URL) +// --------------------------------------------------------------------------- + +export interface LeadMagicProfileSearchParams extends LeadMagicBaseParams { + profile_url: string + extended_response?: boolean +} + +export interface LeadMagicProfileSearchResponse extends ToolResponse { + output: { + profile_url: string | null + first_name: string | null + last_name: string | null + full_name: string | null + professional_title: string | null + bio: string | null + location: string | null + country: string | null + followers_range: string | null + company_name: string | null + company_industry: string | null + company_website: string | null + total_tenure_years: string | null + total_tenure_months: string | null + work_experience: unknown[] + education: unknown[] + certifications: unknown[] + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Profile to Email (LinkedIn URL → work email) +// --------------------------------------------------------------------------- + +export interface LeadMagicProfileToEmailParams extends LeadMagicBaseParams { + profile_url: string +} + +export interface LeadMagicProfileToEmailResponse extends ToolResponse { + output: { + email: string | null + profile_url: string | null + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Email to Profile (work/personal email → LinkedIn profile URL) +// --------------------------------------------------------------------------- + +export interface LeadMagicEmailToProfileParams extends LeadMagicBaseParams { + work_email?: string + personal_email?: string +} + +export interface LeadMagicEmailToProfileResponse extends ToolResponse { + output: { + profile_url: string | null + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Company Search +// --------------------------------------------------------------------------- + +export interface LeadMagicCompanySearchParams extends LeadMagicBaseParams { + company_domain?: string + profile_url?: string + company_name?: string +} + +export interface LeadMagicCompanySearchResponse extends ToolResponse { + output: { + companyName: string | null + companyId: number | null + industry: string | null + employeeCount: number | null + employeeRange: string | null + founded: number | null + headquarters: Record | null + revenue: string | null + funding: string | null + description: string | null + specialties: unknown[] + competitors: unknown[] + followerCount: number | null + twitter_url: string | null + facebook_url: string | null + b2b_profile_url: string | null + logo_url: string | null + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Role Finder +// --------------------------------------------------------------------------- + +export interface LeadMagicRoleFinderParams extends LeadMagicBaseParams { + job_title: string + company_domain?: string + company_name?: string +} + +export interface LeadMagicRoleFinderResponse extends ToolResponse { + output: { + first_name: string | null + last_name: string | null + full_name: string | null + profile_url: string | null + job_title: string | null + company_name: string | null + company_website: string | null + credits_consumed: number + message: string | null + } +} + +// --------------------------------------------------------------------------- +// Get Credits (balance check — free, no hosting) +// --------------------------------------------------------------------------- + +export interface LeadMagicGetCreditsParams extends LeadMagicBaseParams {} + +export interface LeadMagicGetCreditsResponse extends ToolResponse { + output: { + credits: number + } +} + +// --------------------------------------------------------------------------- +// Union response type +// --------------------------------------------------------------------------- + +export type LeadMagicResponse = + | LeadMagicValidateEmailResponse + | LeadMagicFindEmailResponse + | LeadMagicFindMobileResponse + | LeadMagicProfileSearchResponse + | LeadMagicProfileToEmailResponse + | LeadMagicEmailToProfileResponse + | LeadMagicCompanySearchResponse + | LeadMagicRoleFinderResponse + | LeadMagicGetCreditsResponse diff --git a/apps/sim/tools/leadmagic/validate_email.ts b/apps/sim/tools/leadmagic/validate_email.ts new file mode 100644 index 0000000000..99e22736f7 --- /dev/null +++ b/apps/sim/tools/leadmagic/validate_email.ts @@ -0,0 +1,119 @@ +import { leadmagicHosting } from '@/tools/leadmagic/hosting' +import type { + LeadMagicValidateEmailParams, + LeadMagicValidateEmailResponse, +} from '@/tools/leadmagic/types' +import type { ToolConfig } from '@/tools/types' + +export const validateEmailTool: ToolConfig< + LeadMagicValidateEmailParams, + LeadMagicValidateEmailResponse +> = { + id: 'leadmagic_validate_email', + name: 'LeadMagic Validate Email', + description: + 'Verify an email address for deliverability. Charges 0.25 credits for definitive SMTP results (valid/invalid); unknown and RFC-invalid results are free.', + version: '1.0.0', + + hosting: leadmagicHosting((_params, output) => { + // 0.25 credits for valid or SMTP-verified-invalid; free for unknown/syntax failures. + // We use the API-reported credits_consumed field. + // Source: https://leadmagic.io/docs/v1/reference/email-validation + const consumed = output.credits_consumed + return typeof consumed === 'number' ? consumed : 0 + }), + + params: { + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to validate (e.g., john@example.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LeadMagic API Key', + }, + }, + + request: { + url: 'https://api.leadmagic.io/v1/people/email-validation', + method: 'POST', + headers: (params) => ({ + 'X-API-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => ({ email: params.email }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + (errorData as Record).message || + `LeadMagic API error: ${response.status} ${response.statusText}` + ) + } + const data = await response.json() + return { + success: true, + output: { + email: data.email ?? '', + email_status: data.email_status ?? '', + is_domain_catch_all: data.is_domain_catch_all ?? null, + credits_consumed: data.credits_consumed ?? 0, + message: data.message ?? null, + mx_record: data.mx_record ?? null, + mx_provider: data.mx_provider ?? null, + mx_gateway: data.mx_gateway ?? null, + mx_security_gateway: data.mx_security_gateway ?? null, + company_name: data.company_name ?? null, + company_industry: data.company_industry ?? null, + company_size: data.company_size ?? null, + }, + } + }, + + outputs: { + email: { type: 'string', description: 'The validated email address' }, + email_status: { + type: 'string', + description: 'Validation result: valid, invalid, or unknown', + }, + is_domain_catch_all: { + type: 'boolean', + description: 'Whether the domain accepts all emails (catch-all)', + optional: true, + }, + credits_consumed: { + type: 'number', + description: 'Credits charged for this request (0.25 for definitive results)', + }, + message: { type: 'string', description: 'Human-readable status message', optional: true }, + mx_record: { type: 'string', description: 'MX record for the domain', optional: true }, + mx_provider: { + type: 'string', + description: 'Email provider (e.g., Google, Microsoft)', + optional: true, + }, + mx_gateway: { + type: 'string', + description: 'MX gateway for the domain', + optional: true, + }, + mx_security_gateway: { + type: 'boolean', + description: 'Whether the domain uses a security gateway', + optional: true, + }, + company_name: { + type: 'string', + description: 'Company name associated with the email domain', + optional: true, + }, + company_industry: { type: 'string', description: 'Industry of the company', optional: true }, + company_size: { type: 'string', description: 'Company size range', optional: true }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index bb80d09d21..4bc16046b9 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -635,6 +635,13 @@ import { datadogSendLogsTool, datadogSubmitMetricsTool, } from '@/tools/datadog' +import { + datagmaEnrichCompanyTool, + datagmaEnrichPersonTool, + datagmaFindEmailTool, + datagmaFindPhoneTool, + datagmaGetCreditsTool, +} from '@/tools/datagma' import { daytonaCreateSandboxTool, daytonaDeleteSandboxTool, @@ -728,6 +735,7 @@ import { dropboxSearchTool, dropboxUploadTool, } from '@/tools/dropbox' +import { dropcontactEnrichContactTool } from '@/tools/dropcontact' import { chainOfThoughtTool, predictTool, reactTool } from '@/tools/dspy' import { dubBulkCreateLinksTool, @@ -820,6 +828,7 @@ import { enrichVerifyEmailTool, } from '@/tools/enrich' import { enrichmentRunTool } from '@/tools/enrichment' +import { enrowFindEmailTool, enrowVerifyEmailTool } from '@/tools/enrow' import { evernoteCopyNoteTool, evernoteCreateNotebookTool, @@ -1537,6 +1546,7 @@ import { iamRemoveUserFromGroupTool, iamSimulatePrincipalPolicyTool, } from '@/tools/iam' +import { icypeasFindEmailTool, icypeasVerifyEmailTool } from '@/tools/icypeas' import { identityCenterCheckAssignmentDeletionStatusTool, identityCenterCheckAssignmentStatusTool, @@ -1834,6 +1844,17 @@ import { launchDarklyToggleFlagTool, launchDarklyUpdateFlagTool, } from '@/tools/launchdarkly' +import { + leadmagicCompanySearchTool, + leadmagicEmailToProfileTool, + leadmagicFindEmailTool, + leadmagicFindMobileTool, + leadmagicGetCreditsTool, + leadmagicProfileSearchTool, + leadmagicProfileToEmailTool, + leadmagicRoleFinderTool, + leadmagicValidateEmailTool, +} from '@/tools/leadmagic' import { lemlistGetActivitiesTool, lemlistGetLeadTool, lemlistSendEmailTool } from '@/tools/lemlist' import { linearAddLabelToIssueTool, @@ -6526,6 +6547,25 @@ export const tools: Record = { prospeo_search_person: prospeoSearchPersonTool, prospeo_search_company: prospeoSearchCompanyTool, prospeo_search_suggestions: prospeoSearchSuggestionsTool, + datagma_find_email: datagmaFindEmailTool, + datagma_find_phone: datagmaFindPhoneTool, + datagma_enrich_person: datagmaEnrichPersonTool, + datagma_enrich_company: datagmaEnrichCompanyTool, + datagma_get_credits: datagmaGetCreditsTool, + dropcontact_enrich_contact: dropcontactEnrichContactTool, + leadmagic_validate_email: leadmagicValidateEmailTool, + leadmagic_find_email: leadmagicFindEmailTool, + leadmagic_find_mobile: leadmagicFindMobileTool, + leadmagic_profile_search: leadmagicProfileSearchTool, + leadmagic_profile_to_email: leadmagicProfileToEmailTool, + leadmagic_email_to_profile: leadmagicEmailToProfileTool, + leadmagic_company_search: leadmagicCompanySearchTool, + leadmagic_role_finder: leadmagicRoleFinderTool, + leadmagic_get_credits: leadmagicGetCreditsTool, + icypeas_find_email: icypeasFindEmailTool, + icypeas_verify_email: icypeasVerifyEmailTool, + enrow_find_email: enrowFindEmailTool, + enrow_verify_email: enrowVerifyEmailTool, iam_list_users: iamListUsersTool, iam_get_user: iamGetUserTool, iam_create_user: iamCreateUserTool, diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index c8da61e06c..dcce33cbe2 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -30,6 +30,11 @@ export type BYOKProviderId = | 'zerobounce' | 'neverbounce' | 'millionverifier' + | 'datagma' + | 'dropcontact' + | 'leadmagic' + | 'icypeas' + | 'enrow' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' From 2ffc004a0a33c77f4a00e335cefda6a2f842793e Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 18:01:04 -0700 Subject: [PATCH 06/26] improvement(models): add DeepSeek V4 + Mistral Medium 3.5, fix Codestral context window (#5103) --- apps/sim/providers/models.ts | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index b8fa2a2bae..99aaf203cf 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -1718,6 +1718,32 @@ export const PROVIDER_DEFINITIONS: Record = { toolUsageControl: true, }, models: [ + { + id: 'deepseek-v4-pro', + pricing: { + input: 0.435, + cachedInput: 0.003625, + output: 0.87, + updatedAt: '2026-06-16', + }, + capabilities: {}, + contextWindow: 1000000, + releaseDate: '2026-04-24', + }, + { + id: 'deepseek-v4-flash', + pricing: { + input: 0.14, + cachedInput: 0.0028, + output: 0.28, + updatedAt: '2026-06-16', + }, + capabilities: { + temperature: { min: 0, max: 2 }, + }, + contextWindow: 1000000, + releaseDate: '2026-04-24', + }, { id: 'deepseek-chat', pricing: { @@ -2324,6 +2350,19 @@ export const PROVIDER_DEFINITIONS: Record = { contextWindow: 128000, releaseDate: '2025-08-12', }, + { + id: 'mistral-medium-2604', + pricing: { + input: 1.5, + output: 7.5, + updatedAt: '2026-06-16', + }, + capabilities: { + temperature: { min: 0, max: 1.5 }, + }, + contextWindow: 256000, + releaseDate: '2026-04-29', + }, { id: 'mistral-medium-2508', pricing: { @@ -2400,7 +2439,7 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1.5 }, }, - contextWindow: 128000, + contextWindow: 256000, releaseDate: '2025-07-30', }, { @@ -2413,7 +2452,7 @@ export const PROVIDER_DEFINITIONS: Record = { capabilities: { temperature: { min: 0, max: 1.5 }, }, - contextWindow: 128000, + contextWindow: 256000, releaseDate: '2025-07-30', }, { From 8fe090a3a184e0dd1ab6d7fa55140fbb7bbc212b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Jun 2026 19:03:10 -0700 Subject: [PATCH 07/26] fix(input-format): field not editable race condition (#5102) * fix(input-format): field not editable race condition * remove dead code * simplify --- .../components/starter/input-format.tsx | 45 ++++++------ apps/sim/lib/workflows/defaults.ts | 11 +-- apps/sim/lib/workflows/input-format.test.ts | 26 +++++++ apps/sim/lib/workflows/input-format.ts | 31 +++++++++ apps/sim/stores/workflows/utils.ts | 11 +-- apps/sim/stores/workflows/workflow/store.ts | 68 ------------------- 6 files changed, 81 insertions(+), 111 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index 474c82a3c1..f63cb9ff97 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -1,5 +1,4 @@ import { useCallback, useRef } from 'react' -import { generateId } from '@sim/utils/id' import { Plus } from 'lucide-react' import { Trash } from '@/components/emcn/icons/trash' import 'prismjs/components/prism-json' @@ -21,6 +20,7 @@ import { } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' +import { createDefaultInputFormatField } from '@/lib/workflows/input-format' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' @@ -74,18 +74,6 @@ const BOOLEAN_OPTIONS: ComboboxOption[] = [ { label: 'false', value: 'false' }, ] -/** - * Creates a new field with default values - */ -const createDefaultField = (): Field => ({ - id: generateId(), - name: '', - type: 'string', - value: '', - description: '', - collapsed: false, -}) - /** * Validates and sanitizes field names by removing control characters and quotes */ @@ -127,8 +115,17 @@ export function FieldFormat({ disabled, }) + /** + * Stable fallback field used while the store value is still empty (e.g. a + * newly added block). Caching it in a ref keeps the field id constant across + * renders, so the inputs don't remount on each keystroke and edits commit to + * the same id instead of a freshly generated one. + */ + const fallbackFieldRef = useRef(null) + const fallbackField = (fallbackFieldRef.current ??= createDefaultInputFormatField()) + const value = isPreview ? previewValue : storeValue - const fields: Field[] = Array.isArray(value) && value.length > 0 ? value : [createDefaultField()] + const fields: Field[] = Array.isArray(value) && value.length > 0 ? value : [fallbackField] const isReadOnly = isPreview || disabled const renderFieldLabel = (label: string) => @@ -138,7 +135,7 @@ export function FieldFormat({ */ const addField = () => { if (isReadOnly) return - setStoreValue([...fields, createDefaultField()]) + setStoreValue([...fields, createDefaultInputFormatField()]) } /** @@ -148,15 +145,19 @@ export function FieldFormat({ if (isReadOnly) return if (fields.length === 1) { - setStoreValue([createDefaultField()]) + setStoreValue([createDefaultInputFormatField()]) return } setStoreValue(fields.filter((field) => field.id !== id)) } - const storeValueRef = useRef(storeValue) - storeValueRef.current = storeValue + /** + * Mirrors the rendered fields (store value or stable fallback) so updateField + * always commits against the same ids the UI is currently showing. + */ + const fieldsRef = useRef(fields) + fieldsRef.current = fields const isReadOnlyRef = useRef(isReadOnly) isReadOnlyRef.current = isReadOnly @@ -173,14 +174,8 @@ export function FieldFormat({ ? validateFieldName(fieldValue) : fieldValue - const currentStoreValue = storeValueRef.current - const currentFields: Field[] = - Array.isArray(currentStoreValue) && currentStoreValue.length > 0 - ? currentStoreValue - : [createDefaultField()] - setStoreValueRef.current( - currentFields.map((f) => (f.id === id ? { ...f, [fieldKey]: updatedValue } : f)) + fieldsRef.current.map((f) => (f.id === id ? { ...f, [fieldKey]: updatedValue } : f)) ) }, [] diff --git a/apps/sim/lib/workflows/defaults.ts b/apps/sim/lib/workflows/defaults.ts index e9dc0f8076..c326ca43a6 100644 --- a/apps/sim/lib/workflows/defaults.ts +++ b/apps/sim/lib/workflows/defaults.ts @@ -1,5 +1,6 @@ import { generateId } from '@sim/utils/id' import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { createDefaultInputFormatField } from '@/lib/workflows/input-format' import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import type { BlockState, SubBlockState, WorkflowState } from '@/stores/workflows/workflow/types' @@ -39,15 +40,7 @@ function resolveInitialValue(subBlock: SubBlockConfig): unknown { } if (subBlock.type === 'input-format') { - return [ - { - id: generateId(), - name: '', - type: 'string', - value: '', - collapsed: false, - }, - ] + return [createDefaultInputFormatField()] } if (subBlock.type === 'table') { diff --git a/apps/sim/lib/workflows/input-format.test.ts b/apps/sim/lib/workflows/input-format.test.ts index 230e7d0890..886e9aac5a 100644 --- a/apps/sim/lib/workflows/input-format.test.ts +++ b/apps/sim/lib/workflows/input-format.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { + createDefaultInputFormatField, extractInputFieldsFromBlocks, normalizeInputFormatValue, } from '@/lib/workflows/input-format' @@ -227,3 +228,28 @@ describe('normalizeInputFormatValue', () => { expect(normalizeInputFormatValue(input)).toEqual(input) }) }) + +describe('createDefaultInputFormatField', () => { + it.concurrent('creates an empty field with the canonical default shape', () => { + const field = createDefaultInputFormatField() + expect(field).toEqual({ + id: expect.any(String), + name: '', + type: 'string', + value: '', + collapsed: false, + }) + expect(field.id.length).toBeGreaterThan(0) + }) + + it.concurrent('omits description so it is not persisted by default', () => { + expect('description' in createDefaultInputFormatField()).toBe(false) + }) + + it.concurrent('returns a fresh id and a new object on each call', () => { + const first = createDefaultInputFormatField() + const second = createDefaultInputFormatField() + expect(first.id).not.toBe(second.id) + expect(first).not.toBe(second) + }) +}) diff --git a/apps/sim/lib/workflows/input-format.ts b/apps/sim/lib/workflows/input-format.ts index 56455a2e8f..b5a8ac2612 100644 --- a/apps/sim/lib/workflows/input-format.ts +++ b/apps/sim/lib/workflows/input-format.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import type { InputFormatField } from '@/lib/workflows/types' @@ -10,6 +11,36 @@ export interface WorkflowInputField { description?: string } +/** + * Stateful input-format field as stored in sub-block values: the editor's + * per-row shape, including the editor-only `id` and `collapsed` fields. Stricter + * than the wire-level {@link InputFormatField} (required `name`/`type`/`value`). + */ +interface InputFormatFieldState { + id: string + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' + value: string + description?: string + collapsed: boolean +} + +/** + * Creates a new empty input-format field with a fresh id. + * + * Single source of truth for the default field shape used when seeding + * input-format / response-format sub-blocks and when adding rows in the editor. + */ +export function createDefaultInputFormatField(): InputFormatFieldState { + return { + id: generateId(), + name: '', + type: 'string', + value: '', + collapsed: false, + } +} + /** * Extracts input fields from workflow blocks. * Finds the trigger block (start_trigger, input_trigger, or starter) and extracts its inputFormat. diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index ceee7b786e..5505a7738f 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -4,6 +4,7 @@ import type { Edge } from 'reactflow' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' +import { createDefaultInputFormatField } from '@/lib/workflows/input-format' import { buildDefaultCanonicalModes } from '@/lib/workflows/subblocks/visibility' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { getBlock } from '@/blocks' @@ -151,15 +152,7 @@ export function prepareBlockState(options: PrepareBlockStateOptions): BlockState } else if (subBlock.defaultValue !== undefined) { initialValue = subBlock.defaultValue } else if (subBlock.type === 'input-format' || subBlock.type === 'response-format') { - initialValue = [ - { - id: generateId(), - name: '', - type: 'string', - value: '', - collapsed: false, - }, - ] + initialValue = [createDefaultInputFormatField()] } else if (subBlock.type === 'table') { initialValue = [] } diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 0e8f011ede..d7bc200a80 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -1,5 +1,4 @@ import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import type { Edge } from 'reactflow' import { create } from 'zustand' @@ -9,7 +8,6 @@ import { getDynamicHandleSubblockType, isDynamicHandleSubblock, } from '@/lib/workflows/dynamic-handle-topology' -import type { SubBlockConfig } from '@/blocks/types' import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { @@ -37,72 +35,6 @@ import { normalizeWorkflowState } from '@/stores/workflows/workflow/validation' const logger = createLogger('WorkflowStore') -/** - * Creates a deep clone of an initial sub-block value to avoid shared references. - * - * @param value - The value to clone. - * @returns A cloned value suitable for initializing sub-block state. - */ -function cloneInitialSubblockValue(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map((item) => cloneInitialSubblockValue(item)) - } - - if (value && typeof value === 'object') { - return Object.entries(value as Record).reduce>( - (acc, [key, entry]) => { - acc[key] = cloneInitialSubblockValue(entry) - return acc - }, - {} - ) - } - - return value ?? null -} - -/** - * Resolves the initial value for a sub-block based on its configuration. - * - * @param config - The sub-block configuration. - * @returns The resolved initial value or null when no defaults are defined. - */ -function resolveInitialSubblockValue(config: SubBlockConfig): unknown { - if (typeof config.value === 'function') { - try { - const resolved = config.value({}) - return cloneInitialSubblockValue(resolved) - } catch (error) { - logger.warn('Failed to resolve dynamic sub-block default value', { - subBlockId: config.id, - error: toError(error).message, - }) - } - } - - if (config.defaultValue !== undefined) { - return cloneInitialSubblockValue(config.defaultValue) - } - - if (config.type === 'input-format') { - return [ - { - id: generateId(), - name: '', - type: 'string', - value: '', - collapsed: false, - }, - ] - } - - if (config.type === 'table') { - return [] - } - - return null -} - const initialState = { currentWorkflowId: null, blocks: {}, From a82b44d36d1a14fb3263b06256d3c56c9b6cd5cd Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 19:10:25 -0700 Subject: [PATCH 08/26] perf(db): logs-list index, drop redundant indexes, replica routing, hot-path write cleanups (#5105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(db): logs-list index, drop redundant indexes, replica routing, hot-path write cleanups * fix(logs): keep /api/v1/logs on primary db — its permissions join is the auth gate, not replica-safe --- .../api/mothership/chats/read/route.test.ts | 93 + .../app/api/mothership/chats/read/route.ts | 10 +- apps/sim/app/api/v1/admin/audit-logs/route.ts | 6 +- .../app/api/v1/admin/organizations/route.ts | 6 +- apps/sim/lib/uploads/server/metadata.ts | 34 - apps/sim/lib/webhooks/polling/utils.test.ts | 80 + apps/sim/lib/webhooks/polling/utils.ts | 15 +- ...el_logs_desc_index_and_redundant_drops.sql | 25 + .../db/migrations/meta/0239_snapshot.json | 16566 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 8 +- 11 files changed, 16797 insertions(+), 53 deletions(-) create mode 100644 apps/sim/app/api/mothership/chats/read/route.test.ts create mode 100644 apps/sim/lib/webhooks/polling/utils.test.ts create mode 100644 packages/db/migrations/0239_wel_logs_desc_index_and_redundant_drops.sql create mode 100644 packages/db/migrations/meta/0239_snapshot.json diff --git a/apps/sim/app/api/mothership/chats/read/route.test.ts b/apps/sim/app/api/mothership/chats/read/route.test.ts new file mode 100644 index 0000000000..ba2270510a --- /dev/null +++ b/apps/sim/app/api/mothership/chats/read/route.test.ts @@ -0,0 +1,93 @@ +/** + * @vitest-environment node + */ +import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockUpdate, mockSet, mockWhere, mockParseRequest } = vi.hoisted(() => ({ + mockUpdate: vi.fn(), + mockSet: vi.fn(), + mockWhere: vi.fn(), + mockParseRequest: vi.fn(), +})) + +vi.mock('@sim/db', () => ({ + db: { update: mockUpdate }, +})) + +vi.mock('@sim/db/schema', () => ({ + copilotChats: { + id: 'copilotChats.id', + userId: 'copilotChats.userId', + updatedAt: 'copilotChats.updatedAt', + lastSeenAt: 'copilotChats.lastSeenAt', + }, +})) + +vi.mock('drizzle-orm', () => ({ + and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })), + eq: vi.fn((field: unknown, value: unknown) => ({ type: 'eq', field, value })), + or: vi.fn((...conditions: unknown[]) => ({ type: 'or', conditions })), + isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })), + lt: vi.fn((field: unknown, value: unknown) => ({ type: 'lt', field, value })), + sql: vi.fn(() => ({ type: 'sql' })), +})) + +vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) +vi.mock('@/lib/api/server', () => ({ parseRequest: mockParseRequest })) +vi.mock('@/lib/api/contracts/mothership-chats', () => ({ markMothershipChatReadContract: {} })) + +import { POST } from '@/app/api/mothership/chats/read/route' + +function createRequest() { + return new NextRequest('http://localhost:3000/api/mothership/chats/read', { + method: 'POST', + body: JSON.stringify({ chatId: 'chat-1' }), + }) +} + +describe('POST /api/mothership/chats/read', () => { + beforeEach(() => { + vi.clearAllMocks() + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: 'user-1', + isAuthenticated: true, + }) + mockParseRequest.mockResolvedValue({ success: true, data: { body: { chatId: 'chat-1' } } }) + mockWhere.mockResolvedValue(undefined) + mockSet.mockReturnValue({ where: mockWhere }) + mockUpdate.mockReturnValue({ set: mockSet }) + }) + + it('guards the lastSeenAt write with the unread predicate (only writes when unread)', async () => { + const res = await POST(createRequest()) + expect(res.status).toBe(200) + + expect(mockUpdate).toHaveBeenCalledTimes(1) + const whereArg = mockWhere.mock.calls[0][0] as { + type: string + conditions: Array<{ type: string; conditions?: unknown[] }> + } + expect(whereArg.type).toBe('and') + + const orClause = whereArg.conditions.find((c) => c.type === 'or') + expect(orClause).toBeDefined() + expect(orClause?.conditions).toEqual( + expect.arrayContaining([ + { type: 'isNull', field: 'copilotChats.lastSeenAt' }, + { type: 'lt', field: 'copilotChats.lastSeenAt', value: 'copilotChats.updatedAt' }, + ]) + ) + }) + + it('does not touch the database when unauthenticated', async () => { + copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({ + userId: null, + isAuthenticated: false, + }) + const res = await POST(createRequest()) + expect(res.status).toBe(401) + expect(mockUpdate).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/mothership/chats/read/route.ts b/apps/sim/app/api/mothership/chats/read/route.ts index 2cda04a65f..beffb8c821 100644 --- a/apps/sim/app/api/mothership/chats/read/route.ts +++ b/apps/sim/app/api/mothership/chats/read/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, sql } from 'drizzle-orm' +import { and, eq, isNull, lt, or, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { markMothershipChatReadContract } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' @@ -28,7 +28,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { await db .update(copilotChats) .set({ lastSeenAt: sql`GREATEST(${copilotChats.updatedAt}, NOW())` }) - .where(and(eq(copilotChats.id, chatId), eq(copilotChats.userId, userId))) + .where( + and( + eq(copilotChats.id, chatId), + eq(copilotChats.userId, userId), + or(isNull(copilotChats.lastSeenAt), lt(copilotChats.lastSeenAt, copilotChats.updatedAt)) + ) + ) return NextResponse.json({ success: true }) } catch (error) { diff --git a/apps/sim/app/api/v1/admin/audit-logs/route.ts b/apps/sim/app/api/v1/admin/audit-logs/route.ts index 78d4f62067..9610232d35 100644 --- a/apps/sim/app/api/v1/admin/audit-logs/route.ts +++ b/apps/sim/app/api/v1/admin/audit-logs/route.ts @@ -18,7 +18,7 @@ * Response: AdminListResponse */ -import { db } from '@sim/db' +import { dbReplica } from '@sim/db' import { auditLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, desc } from 'drizzle-orm' @@ -70,8 +70,8 @@ export const GET = withRouteHandler( const whereClause = conditions.length > 0 ? and(...conditions) : undefined const [countResult, logs] = await Promise.all([ - db.select({ total: count() }).from(auditLog).where(whereClause), - db + dbReplica.select({ total: count() }).from(auditLog).where(whereClause), + dbReplica .select() .from(auditLog) .where(whereClause) diff --git a/apps/sim/app/api/v1/admin/organizations/route.ts b/apps/sim/app/api/v1/admin/organizations/route.ts index 07ee15890e..5799c2bf1a 100644 --- a/apps/sim/app/api/v1/admin/organizations/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/route.ts @@ -21,7 +21,7 @@ * Response: AdminSingleResponse */ -import { db } from '@sim/db' +import { db, dbReplica } from '@sim/db' import { member, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { count, eq } from 'drizzle-orm' @@ -70,8 +70,8 @@ export const GET = withRouteHandler( try { const [countResult, organizations] = await Promise.all([ - db.select({ total: count() }).from(organization), - db + dbReplica.select({ total: count() }).from(organization), + dbReplica .select({ id: organization.id, name: organization.name, diff --git a/apps/sim/lib/uploads/server/metadata.ts b/apps/sim/lib/uploads/server/metadata.ts index 9c2940e2d2..4a3faa5836 100644 --- a/apps/sim/lib/uploads/server/metadata.ts +++ b/apps/sim/lib/uploads/server/metadata.ts @@ -22,12 +22,6 @@ export interface FileMetadataInsertOptions { id?: string } -interface FileMetadataQueryOptions { - context?: StorageContext - workspaceId?: string - userId?: string -} - /** * Insert file metadata into workspaceFiles table * Handles duplicate key errors gracefully by returning existing record @@ -229,34 +223,6 @@ export async function getFileMetadataById( return record ?? null } -/** - * Get file metadata by context with optional workspaceId/userId filters - */ -async function getFileMetadataByContext( - context: StorageContext, - options?: FileMetadataQueryOptions & { includeDeleted?: boolean } -): Promise { - const conditions = [eq(workspaceFiles.context, context)] - - if (options?.workspaceId) { - conditions.push(eq(workspaceFiles.workspaceId, options.workspaceId)) - } - - if (options?.userId) { - conditions.push(eq(workspaceFiles.userId, options.userId)) - } - - if (!options?.includeDeleted) { - conditions.push(isNull(workspaceFiles.deletedAt)) - } - - return db - .select() - .from(workspaceFiles) - .where(conditions.length > 1 ? and(...conditions) : conditions[0]) - .orderBy(workspaceFiles.uploadedAt) -} - /** * Delete file metadata by key */ diff --git a/apps/sim/lib/webhooks/polling/utils.test.ts b/apps/sim/lib/webhooks/polling/utils.test.ts new file mode 100644 index 0000000000..d396f955c3 --- /dev/null +++ b/apps/sim/lib/webhooks/polling/utils.test.ts @@ -0,0 +1,80 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockUpdate, mockSet, mockWhere, sqlCalls } = vi.hoisted(() => ({ + mockUpdate: vi.fn(), + mockSet: vi.fn(), + mockWhere: vi.fn(), + sqlCalls: [] as Array<{ values: unknown[] }>, +})) + +vi.mock('@sim/db', () => ({ db: { update: mockUpdate } })) +vi.mock('@sim/db/schema', () => ({ + webhook: { + id: 'webhook.id', + providerConfig: 'webhook.providerConfig', + updatedAt: 'webhook.updatedAt', + }, + account: {}, + credentialSet: {}, + workflow: {}, + workflowDeploymentVersion: {}, +})) +vi.mock('drizzle-orm', () => ({ + sql: (_strings: readonly string[], ...values: unknown[]) => { + const node = { values } + sqlCalls.push(node) + return node + }, + and: vi.fn(), + eq: vi.fn((field: unknown, value: unknown) => ({ field, value })), + isNull: vi.fn(), + ne: vi.fn(), + or: vi.fn(), +})) +vi.mock('@/lib/billing', () => ({ isOrganizationOnTeamOrEnterprisePlan: vi.fn() })) +vi.mock('@/app/api/auth/oauth/utils', () => ({ + getOAuthToken: vi.fn(), + refreshAccessTokenIfNeeded: vi.fn(), + resolveOAuthAccountId: vi.fn(), +})) +vi.mock('@/triggers/constants', () => ({ MAX_CONSECUTIVE_FAILURES: 5 })) + +import { updateWebhookProviderConfig } from '@/lib/webhooks/polling/utils' + +const logger = { error: vi.fn() } as never + +function allInterpolatedValues(): unknown[] { + return sqlCalls.flatMap((c) => c.values) +} + +describe('updateWebhookProviderConfig (atomic jsonb merge)', () => { + beforeEach(() => { + vi.clearAllMocks() + sqlCalls.length = 0 + mockWhere.mockResolvedValue(undefined) + mockSet.mockReturnValue({ where: mockWhere }) + mockUpdate.mockReturnValue({ set: mockSet }) + }) + + it('merges defined keys (null preserved) and removes undefined keys', async () => { + await updateWebhookProviderConfig( + 'wh-1', + { historyId: 'h1', cleared: undefined, nulled: null }, + logger + ) + + expect(mockUpdate).toHaveBeenCalledTimes(1) + expect(allInterpolatedValues()).toContain(JSON.stringify({ historyId: 'h1', nulled: null })) + expect(allInterpolatedValues()).toContainEqual(['cleared']) + }) + + it('uses merge only (no key-removal expression) when nothing is undefined', async () => { + await updateWebhookProviderConfig('wh-1', { historyId: 'h1' }, logger) + + expect(allInterpolatedValues()).toContain(JSON.stringify({ historyId: 'h1' })) + expect(allInterpolatedValues().some((v) => Array.isArray(v))).toBe(false) + }) +}) diff --git a/apps/sim/lib/webhooks/polling/utils.ts b/apps/sim/lib/webhooks/polling/utils.ts index 277128699f..291dba3405 100644 --- a/apps/sim/lib/webhooks/polling/utils.ts +++ b/apps/sim/lib/webhooks/polling/utils.ts @@ -157,16 +157,19 @@ export async function updateWebhookProviderConfig( logger: Logger ): Promise { try { - const result = await db.select().from(webhook).where(eq(webhook.id, webhookId)) - const existingConfig = (result[0]?.providerConfig as Record) || {} + const defined: Record = {} + const removedKeys: string[] = [] + for (const [key, value] of Object.entries(configUpdates)) { + if (value === undefined) removedKeys.push(key) + else defined[key] = value + } + + const merged = sql`COALESCE(${webhook.providerConfig}, '{}'::jsonb) || ${JSON.stringify(defined)}::jsonb` await db .update(webhook) .set({ - providerConfig: { - ...existingConfig, - ...configUpdates, - } as Record, + providerConfig: removedKeys.length > 0 ? sql`(${merged}) - ${removedKeys}::text[]` : merged, updatedAt: new Date(), }) .where(eq(webhook.id, webhookId)) diff --git a/packages/db/migrations/0239_wel_logs_desc_index_and_redundant_drops.sql b/packages/db/migrations/0239_wel_logs_desc_index_and_redundant_drops.sql new file mode 100644 index 0000000000..90085e31d2 --- /dev/null +++ b/packages/db/migrations/0239_wel_logs_desc_index_and_redundant_drops.sql @@ -0,0 +1,25 @@ +-- Generated by drizzle-kit, then converted to CONCURRENTLY per scripts/migrate.ts (plain +-- CREATE/DROP INDEX takes ACCESS EXCLUSIVE for the whole op, write-locking the table). +-- +-- ADD workflow_execution_logs (workspace_id, started_at DESC NULLS LAST, id DESC): serves the +-- logs-list query (lib/logs/list-logs.ts, app/api/v1/logs, app/api/logs/export) ordered by +-- started_at DESC with an id tiebreaker as an index-only scan that early-exits under LIMIT, +-- replacing an in-memory sort over the matched rows. The existing +-- workflow_execution_logs_workspace_started_at_idx is intentionally KEPT — it still serves the +-- ascending/range log queries that this DESC index does not cover. +-- +-- DROP three redundant indexes, each a left-prefix of an existing superset (equality-prefix only, +-- no ordering subtlety; no query, foreign key, or constraint depends on the narrow index): +-- permission_group_workspace_group_id_idx ⊂ permission_group_workspace_group_workspace_unique +-- user_table_rows_table_id_idx ⊂ user_table_rows_table_order_key_idx (+ others) +-- workspace_byok_workspace_idx ⊂ workspace_byok_workspace_provider_idx +-- +-- The embedded COMMIT drops out of drizzle's batch transaction; everything below runs in +-- autocommit (CONCURRENTLY cannot run in a transaction) and is idempotent for safe replay. +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_workspace_started_at_id_desc_idx" ON "workflow_execution_logs" USING btree ("workspace_id","started_at" DESC NULLS LAST,"id" DESC);--> statement-breakpoint +DROP INDEX CONCURRENTLY IF EXISTS "permission_group_workspace_group_id_idx";--> statement-breakpoint +DROP INDEX CONCURRENTLY IF EXISTS "user_table_rows_table_id_idx";--> statement-breakpoint +DROP INDEX CONCURRENTLY IF EXISTS "workspace_byok_workspace_idx";--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/migrations/meta/0239_snapshot.json b/packages/db/migrations/meta/0239_snapshot.json new file mode 100644 index 0000000000..0484ad1b3a --- /dev/null +++ b/packages/db/migrations/meta/0239_snapshot.json @@ -0,0 +1,16566 @@ +{ + "id": "488b76cc-8da9-43fa-90fb-bfe7a54b34c1", + "prevId": "ec49405c-a007-4cf2-9706-ddb27a78ef19", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_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": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.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.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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 + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "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.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 + }, + "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_a2a_executions": { + "name": "total_a2a_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 + } + }, + "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 + }, + "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 + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "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_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + }, + "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" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + }, + "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 + }, + "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": {} + } + }, + "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" + } + }, + "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 + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "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_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "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_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 b7ce43b7f7..2667376721 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1667,6 +1667,13 @@ "when": 1781554278388, "tag": "0238_workspace_scoped_permission_groups", "breakpoints": true + }, + { + "idx": 239, + "version": "7", + "when": 1781659761380, + "tag": "0239_wel_logs_desc_index_and_redundant_drops", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 7a2c3e4d0b..de43647af2 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -372,6 +372,9 @@ export const workflowExecutionLogs = pgTable( table.workspaceId, table.startedAt ), + workspaceStartedAtIdDescIdx: index( + 'workflow_execution_logs_workspace_started_at_id_desc_idx' + ).on(table.workspaceId, sql`${table.startedAt} DESC NULLS LAST`, sql`${table.id} DESC`), workspaceCostTotalIdx: index('workflow_execution_logs_workspace_cost_total_idx').on( table.workspaceId, table.costTotal @@ -564,7 +567,6 @@ export const workspaceBYOKKeys = pgTable( table.workspaceId, table.providerId ), - workspaceIdx: index('workspace_byok_workspace_idx').on(table.workspaceId), }) ) @@ -2975,9 +2977,6 @@ export const permissionGroupWorkspace = pgTable( createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ - permissionGroupIdIdx: index('permission_group_workspace_group_id_idx').on( - table.permissionGroupId - ), workspaceIdIdx: index('permission_group_workspace_workspace_id_idx').on(table.workspaceId), groupWorkspaceUnique: uniqueIndex('permission_group_workspace_group_workspace_unique').on( table.permissionGroupId, @@ -3193,7 +3192,6 @@ export const userTableRows = pgTable( createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), }, (table) => ({ - tableIdIdx: index('user_table_rows_table_id_idx').on(table.tableId), /** * Tenant-scoped containment index (requires the `btree_gin` extension, * created in migration 0232). A plain GIN on `data` matches `@>` candidates From 83531452e55fa0c14db450b2068a61c17b8b69c7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 19:28:51 -0700 Subject: [PATCH 09/26] fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons (#5104) * fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons On a cold load (e.g. when the browser discards an idle tab and reloads), the persistent sidebar started with an empty React Query cache and client-fetched its chat + workflow lists, flashing loading skeletons. Prefetch both lists server-side in the workspace layout and hydrate them via HydrationBoundary, under the same query keys and mappers the client hooks use, so the sidebar paints populated on the first render. The prefetch runs concurrently with the existing org-settings fetch and never throws, so it adds no blocking work in the common case and falls back to client fetching on error. * refactor(prefetch): call data layer directly instead of internal HTTP self-fetch The sidebar and settings prefetches fetched their data by making internal HTTP requests to our own API routes. Replace those self-fetches with direct calls to shared server-side data functions, so each route handler and its prefetch read from one source with no extra network hop, serialization, or re-auth. - Extract listWorkflowsForUser (lib/workflows/queries) and listMothershipChats (lib/copilot/chat) from their routes; both routes and the sidebar prefetch now call them. - Extract getUserSettings/getUserProfile (lib/users/queries) shared by the settings/profile routes and their prefetches. - Subscription prefetch calls the existing getSimplifiedBillingSummary + getEffectiveBillingStatus directly. - Sidebar prefetch checks workspace access once via checkWorkspaceAccess and skips silently when denied. * refactor(prefetch): share mothership chat list staleTime constant Export MOTHERSHIP_CHAT_LIST_STALE_TIME from the chats hook and use it in both useMothershipChats and the sidebar prefetch, mirroring WORKFLOW_LIST_STALE_TIME so the prefetch and client hook can't drift. * fix(prefetch): keep subscription prefetch on the wire shape via internal billing API The billing summary returns Date fields (and an untyped metadata blob) that the JSON API serializes to strings. Calling the data layer directly would cache Date objects (App Router preserves them through RSC serialization), mismatching the string wire shape the client useSubscriptionData hook caches. Route the subscription prefetch through the internal billing API so server-hydrated and client-fetched data share the exact same shape. The date-free general-settings and profile prefetches keep calling the data layer directly. --- apps/sim/app/api/mothership/chats/route.ts | 33 +----- apps/sim/app/api/users/me/profile/route.ts | 13 +-- apps/sim/app/api/users/me/settings/route.ts | 75 +------------ apps/sim/app/api/workflows/route.ts | 65 +---------- .../app/workspace/[workspaceId]/layout.tsx | 22 +++- .../app/workspace/[workspaceId]/prefetch.ts | 57 ++++++++++ .../settings/[section]/prefetch.ts | 55 ++++------ apps/sim/hooks/queries/mothership-chats.ts | 7 +- .../lib/copilot/chat/list-mothership-chats.ts | 50 +++++++++ apps/sim/lib/users/queries.ts | 96 +++++++++++++++++ apps/sim/lib/workflows/queries.ts | 101 ++++++++++++++++++ 11 files changed, 362 insertions(+), 212 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/prefetch.ts create mode 100644 apps/sim/lib/copilot/chat/list-mothership-chats.ts create mode 100644 apps/sim/lib/users/queries.ts create mode 100644 apps/sim/lib/workflows/queries.ts diff --git a/apps/sim/app/api/mothership/chats/route.ts b/apps/sim/app/api/mothership/chats/route.ts index 7f0b582850..764b897eac 100644 --- a/apps/sim/app/api/mothership/chats/route.ts +++ b/apps/sim/app/api/mothership/chats/route.ts @@ -1,14 +1,13 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createMothershipChatContract, listMothershipChatsContract, } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' -import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness' +import { listMothershipChats } from '@/lib/copilot/chat/list-mothership-chats' import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, @@ -42,35 +41,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { await assertActiveWorkspaceAccess(workspaceId, userId) - const chats = await db - .select({ - id: copilotChats.id, - title: copilotChats.title, - updatedAt: copilotChats.updatedAt, - activeStreamId: copilotChats.conversationId, - lastSeenAt: copilotChats.lastSeenAt, - pinned: copilotChats.pinned, - }) - .from(copilotChats) - .where( - and( - eq(copilotChats.userId, userId), - eq(copilotChats.workspaceId, workspaceId), - eq(copilotChats.type, 'mothership') - ) - ) - .orderBy(desc(copilotChats.pinned), desc(copilotChats.updatedAt)) - - const streamMarkers = await reconcileChatStreamMarkers( - chats.map((c) => ({ chatId: c.id, streamId: c.activeStreamId })), - { repairVerifiedStaleMarkers: true } - ) - const reconciled = chats.map((c) => { - const activeStreamId = streamMarkers.get(c.id)?.streamId ?? null - return activeStreamId === c.activeStreamId ? c : { ...c, activeStreamId } - }) + const data = await listMothershipChats(userId, workspaceId) - return NextResponse.json({ success: true, data: reconciled }) + return NextResponse.json({ success: true, data }) } catch (error) { if (isWorkspaceAccessDeniedError(error)) { return createForbiddenResponse('Workspace access denied') diff --git a/apps/sim/app/api/users/me/profile/route.ts b/apps/sim/app/api/users/me/profile/route.ts index 81336d60de..b71fa183f2 100644 --- a/apps/sim/app/api/users/me/profile/route.ts +++ b/apps/sim/app/api/users/me/profile/route.ts @@ -8,6 +8,7 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getUserProfile } from '@/lib/users/queries' const logger = createLogger('UpdateUserProfileAPI') @@ -84,17 +85,7 @@ export const GET = withRouteHandler(async () => { const userId = session.user.id - const [userRecord] = await db - .select({ - id: user.id, - name: user.name, - email: user.email, - image: user.image, - emailVerified: user.emailVerified, - }) - .from(user) - .where(eq(user.id, userId)) - .limit(1) + const userRecord = await getUserProfile(userId) if (!userRecord) { return NextResponse.json({ error: 'User not found' }, { status: 404 }) diff --git a/apps/sim/app/api/users/me/settings/route.ts b/apps/sim/app/api/users/me/settings/route.ts index 7e07b2b38e..24ccacceb6 100644 --- a/apps/sim/app/api/users/me/settings/route.ts +++ b/apps/sim/app/api/users/me/settings/route.ts @@ -2,93 +2,26 @@ import { db } from '@sim/db' import { settings } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateShortId } from '@sim/utils/id' -import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { updateUserSettingsContract } from '@/lib/api/contracts' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { defaultUserSettings, getUserSettings } from '@/lib/users/queries' const logger = createLogger('UserSettingsAPI') -const defaultSettings = { - theme: 'system', - autoConnect: true, - telemetryEnabled: true, - emailPreferences: {}, - billingUsageNotificationsEnabled: true, - showTrainingControls: false, - superUserModeEnabled: false, - mothershipEnvironment: 'default', - errorNotificationsEnabled: true, - snapToGridSize: 0, - showActionBar: true, - timezone: null, - lastActiveWorkspaceId: null, -} - export const GET = withRouteHandler(async () => { const requestId = generateRequestId() try { const session = await getSession() - - if (!session?.user?.id) { - logger.info(`[${requestId}] Returning default settings for unauthenticated user`) - return NextResponse.json({ data: defaultSettings }, { status: 200 }) - } - - const userId = session.user.id - const result = await db - .select({ - theme: settings.theme, - autoConnect: settings.autoConnect, - telemetryEnabled: settings.telemetryEnabled, - emailPreferences: settings.emailPreferences, - billingUsageNotificationsEnabled: settings.billingUsageNotificationsEnabled, - showTrainingControls: settings.showTrainingControls, - superUserModeEnabled: settings.superUserModeEnabled, - mothershipEnvironment: settings.mothershipEnvironment, - errorNotificationsEnabled: settings.errorNotificationsEnabled, - snapToGridSize: settings.snapToGridSize, - showActionBar: settings.showActionBar, - timezone: settings.timezone, - lastActiveWorkspaceId: settings.lastActiveWorkspaceId, - }) - .from(settings) - .where(eq(settings.userId, userId)) - .limit(1) - - if (!result.length) { - return NextResponse.json({ data: defaultSettings }, { status: 200 }) - } - - const userSettings = result[0] - - return NextResponse.json( - { - data: { - theme: userSettings.theme, - autoConnect: userSettings.autoConnect, - telemetryEnabled: userSettings.telemetryEnabled, - emailPreferences: userSettings.emailPreferences ?? {}, - billingUsageNotificationsEnabled: userSettings.billingUsageNotificationsEnabled ?? true, - showTrainingControls: userSettings.showTrainingControls ?? false, - superUserModeEnabled: userSettings.superUserModeEnabled ?? false, - mothershipEnvironment: userSettings.mothershipEnvironment ?? 'default', - errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true, - snapToGridSize: userSettings.snapToGridSize ?? 0, - showActionBar: userSettings.showActionBar ?? true, - timezone: userSettings.timezone ?? null, - lastActiveWorkspaceId: userSettings.lastActiveWorkspaceId ?? null, - }, - }, - { status: 200 } - ) + const data = await getUserSettings(session?.user?.id ?? null) + return NextResponse.json({ data }, { status: 200 }) } catch (error: any) { logger.error(`[${requestId}] Settings fetch error`, error) - return NextResponse.json({ data: defaultSettings }, { status: 200 }) + return NextResponse.json({ data: defaultUserSettings }, { status: 200 }) } }) diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 6a8738869f..92a1798596 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -1,7 +1,4 @@ -import { db } from '@sim/db' -import { permissions, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, asc, eq, inArray, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowContract, workflowListQuerySchema } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' @@ -10,12 +7,12 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { performCreateWorkflow } from '@/lib/workflows/orchestration' +import { listWorkflowsForUser } from '@/lib/workflows/queries' import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('WorkflowAPI') -// GET /api/workflows - Get workflows for user (optionally filtered by workspaceId) export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() const startTime = Date.now() @@ -63,65 +60,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } } - let workflows - - /** - * Project only the columns declared in `workflowListItemSchema` so the - * wire response matches the contract shape exactly. The full row is - * larger (`state`, `variables`, `apiKey`, `runCount`, etc.) and would - * be dropped client-side by Zod parse anyway — narrowing here saves - * bytes over the wire. Keep this list aligned with the contract. - */ - const listColumns = { - id: workflow.id, - name: workflow.name, - description: workflow.description, - workspaceId: workflow.workspaceId, - folderId: workflow.folderId, - sortOrder: workflow.sortOrder, - createdAt: workflow.createdAt, - updatedAt: workflow.updatedAt, - archivedAt: workflow.archivedAt, - locked: workflow.locked, - } as const - const orderByClause = [asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)] - - if (workspaceId) { - workflows = await db - .select(listColumns) - .from(workflow) - .where( - scope === 'all' - ? eq(workflow.workspaceId, workspaceId) - : scope === 'archived' - ? and(eq(workflow.workspaceId, workspaceId), sql`${workflow.archivedAt} IS NOT NULL`) - : and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt)) - ) - .orderBy(...orderByClause) - } else { - const workspacePermissionRows = await db - .select({ workspaceId: permissions.entityId }) - .from(permissions) - .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) - const workspaceIds = workspacePermissionRows.map((row) => row.workspaceId) - if (workspaceIds.length === 0) { - return NextResponse.json({ data: [] }, { status: 200 }) - } - workflows = await db - .select(listColumns) - .from(workflow) - .where( - scope === 'all' - ? inArray(workflow.workspaceId, workspaceIds) - : scope === 'archived' - ? and( - inArray(workflow.workspaceId, workspaceIds), - sql`${workflow.archivedAt} IS NOT NULL` - ) - : and(inArray(workflow.workspaceId, workspaceIds), isNull(workflow.archivedAt)) - ) - .orderBy(...orderByClause) - } + const workflows = await listWorkflowsForUser({ userId, workspaceId, scope }) return NextResponse.json({ data: workflows }, { status: 200 }) } catch (error: any) { diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index 3dc8aadf66..ddc004a872 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -1,8 +1,11 @@ +import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { redirect } from 'next/navigation' import { ToastProvider } from '@/components/emcn' import { getSession } from '@/lib/auth' +import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner' import { WorkspaceChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome' +import { prefetchWorkspaceSidebar } from '@/app/workspace/[workspaceId]/prefetch' import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader' import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader' @@ -11,15 +14,28 @@ import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/work import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider' import { getOrgWhitelabelSettings } from '@/ee/whitelabeling/org-branding' -export default async function WorkspaceLayout({ children }: { children: React.ReactNode }) { +export default async function WorkspaceLayout({ + children, + params, +}: { + children: React.ReactNode + params: Promise<{ workspaceId: string }> +}) { const session = await getSession() if (!session?.user) { redirect('/login') } + + const { workspaceId } = await params + const queryClient = getQueryClient() + const sidebarPrefetch = prefetchWorkspaceSidebar(queryClient, workspaceId, session.user.id) + // The organization plugin is conditionally spread so TS can't infer activeOrganizationId on the base session type. const orgId = (session.session as { activeOrganizationId?: string } | null)?.activeOrganizationId const initialOrgSettings = orgId ? await getOrgWhitelabelSettings(orgId) : null + await sidebarPrefetch + return ( @@ -30,7 +46,9 @@ export default async function WorkspaceLayout({ children }: { children: React.Re - {children} + + {children} + diff --git a/apps/sim/app/workspace/[workspaceId]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/prefetch.ts new file mode 100644 index 0000000000..9eebb7f56e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/prefetch.ts @@ -0,0 +1,57 @@ +import type { QueryClient } from '@tanstack/react-query' +import { listMothershipChats } from '@/lib/copilot/chat/list-mothership-chats' +import { listWorkflowsForUser } from '@/lib/workflows/queries' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { + MOTHERSHIP_CHAT_LIST_STALE_TIME, + mapChat, + mothershipChatKeys, +} from '@/hooks/queries/mothership-chats' +import { workflowKeys } from '@/hooks/queries/utils/workflow-keys' +import { mapWorkflow, WORKFLOW_LIST_STALE_TIME } from '@/hooks/queries/utils/workflow-list-query' + +/** Resolves whether the user may access the workspace, swallowing errors to a `false`. */ +async function userCanAccessWorkspace(workspaceId: string, userId: string): Promise { + try { + const access = await checkWorkspaceAccess(workspaceId, userId) + return access.exists && access.hasAccess + } catch { + return false + } +} + +/** + * Prefetches the sidebar's workflow + chat lists for a workspace and stores them + * under the same query keys + mappers the client hooks use, so the persistent + * sidebar paints populated on the first server render instead of flashing skeletons + * on a cold load (e.g. after the browser discards an idle tab). Calls the data layer + * directly — the same functions the API routes use — with no internal HTTP hop. + * + * Skips silently when the user can't access the workspace, leaving the client to + * fetch and surface the real error instead of caching an empty list. + */ +export async function prefetchWorkspaceSidebar( + queryClient: QueryClient, + workspaceId: string, + userId: string +): Promise { + if (!(await userCanAccessWorkspace(workspaceId, userId))) return + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: workflowKeys.list(workspaceId, 'active'), + queryFn: async () => { + const rows = await listWorkflowsForUser({ userId, workspaceId, scope: 'active' }) + return rows.map(mapWorkflow) + }, + staleTime: WORKFLOW_LIST_STALE_TIME, + }), + queryClient.prefetchQuery({ + queryKey: mothershipChatKeys.list(workspaceId), + queryFn: async () => { + const data = await listMothershipChats(userId, workspaceId) + return data.map(mapChat) + }, + staleTime: MOTHERSHIP_CHAT_LIST_STALE_TIME, + }), + ]) +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts index d04d9481d1..0df3a17358 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts @@ -1,21 +1,14 @@ import type { QueryClient } from '@tanstack/react-query' import { headers } from 'next/headers' +import { getSession } from '@/lib/auth' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' +import { getUserProfile, getUserSettings } from '@/lib/users/queries' import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings' import { subscriptionKeys } from '@/hooks/queries/subscription' import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile' /** - * Forwards incoming request cookies so server-side API fetches authenticate correctly. - */ -async function getForwardedHeaders(): Promise> { - const h = await headers() - const cookie = h.get('cookie') - return cookie ? { cookie } : {} -} - -/** - * Prefetch general settings server-side via internal API fetch. + * Prefetch general settings server-side via the shared data layer. * Uses the same query keys as the client `useGeneralSettings` hook * so data is shared via HydrationBoundary. */ @@ -23,13 +16,8 @@ export function prefetchGeneralSettings(queryClient: QueryClient) { return queryClient.prefetchQuery({ queryKey: generalSettingsKeys.settings(), queryFn: async () => { - const fwdHeaders = await getForwardedHeaders() - const baseUrl = getInternalApiBaseUrl() - const response = await fetch(`${baseUrl}/api/users/me/settings`, { - headers: fwdHeaders, - }) - if (!response.ok) throw new Error(`Settings prefetch failed: ${response.status}`) - const { data } = await response.json() + const session = await getSession() + const data = await getUserSettings(session?.user?.id ?? null) return mapGeneralSettingsResponse(data) }, staleTime: 60 * 60 * 1000, @@ -37,19 +25,23 @@ export function prefetchGeneralSettings(queryClient: QueryClient) { } /** - * Prefetch subscription data server-side via internal API fetch. - * Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false) - * so data is shared via HydrationBoundary — ensuring the settings sidebar renders - * with the correct Team/Enterprise tabs on the first paint, with no flash. + * Prefetch subscription data server-side. Unlike the other prefetches this goes + * through the internal billing API rather than calling the data layer directly: + * the billing summary contains `Date` fields (and an untyped `metadata` blob) that + * `NextResponse.json` serializes to the string wire shape the client caches. Going + * through the route yields that exact shape, avoiding a Date-vs-string mismatch + * between server-hydrated and client-fetched data. Uses the same query key as the + * client `useSubscriptionData` hook (with includeOrg=false) so data is shared via + * HydrationBoundary. */ export function prefetchSubscriptionData(queryClient: QueryClient) { return queryClient.prefetchQuery({ queryKey: subscriptionKeys.user(false), queryFn: async () => { - const fwdHeaders = await getForwardedHeaders() - const baseUrl = getInternalApiBaseUrl() - const response = await fetch(`${baseUrl}/api/billing?context=user`, { - headers: fwdHeaders, + const h = await headers() + const cookie = h.get('cookie') + const response = await fetch(`${getInternalApiBaseUrl()}/api/billing?context=user`, { + headers: cookie ? { cookie } : {}, }) if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`) return response.json() @@ -59,7 +51,7 @@ export function prefetchSubscriptionData(queryClient: QueryClient) { } /** - * Prefetch user profile server-side via internal API fetch. + * Prefetch user profile server-side via the shared data layer. * Uses the same query keys as the client `useUserProfile` hook * so data is shared via HydrationBoundary. */ @@ -67,13 +59,10 @@ export function prefetchUserProfile(queryClient: QueryClient) { return queryClient.prefetchQuery({ queryKey: userProfileKeys.profile(), queryFn: async () => { - const fwdHeaders = await getForwardedHeaders() - const baseUrl = getInternalApiBaseUrl() - const response = await fetch(`${baseUrl}/api/users/me/profile`, { - headers: fwdHeaders, - }) - if (!response.ok) throw new Error(`Profile prefetch failed: ${response.status}`) - const { user } = await response.json() + const session = await getSession() + if (!session?.user?.id) throw new Error('Unauthorized') + const user = await getUserProfile(session.user.id) + if (!user) throw new Error('User not found') return mapUserProfileResponse(user) }, staleTime: 5 * 60 * 1000, diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index 01a4bf01a6..10e458c5ae 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -61,6 +61,9 @@ export const mothershipChatKeys = { detail: (chatId: string | undefined) => [...mothershipChatKeys.details(), chatId ?? ''] as const, } +/** Shared by the `useMothershipChats` hook and the workspace sidebar prefetch. */ +export const MOTHERSHIP_CHAT_LIST_STALE_TIME = 60 * 1000 + function assertValid(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(message) @@ -183,7 +186,7 @@ function parseChatResourcesResponse(value: unknown): { resources: MothershipReso } } -function mapChat(chat: MothershipChat): MothershipChatMetadata { +export function mapChat(chat: MothershipChat): MothershipChatMetadata { const updatedAt = new Date(chat.updatedAt) return { id: chat.id, @@ -217,7 +220,7 @@ export function useMothershipChats(workspaceId?: string) { queryKey: mothershipChatKeys.list(workspaceId), queryFn: workspaceId ? ({ signal }) => fetchMothershipChats(workspaceId, signal) : skipToken, placeholderData: keepPreviousData, - staleTime: 60 * 1000, + staleTime: MOTHERSHIP_CHAT_LIST_STALE_TIME, }) } diff --git a/apps/sim/lib/copilot/chat/list-mothership-chats.ts b/apps/sim/lib/copilot/chat/list-mothership-chats.ts new file mode 100644 index 0000000000..73839785a7 --- /dev/null +++ b/apps/sim/lib/copilot/chat/list-mothership-chats.ts @@ -0,0 +1,50 @@ +import { db } from '@sim/db' +import { copilotChats } from '@sim/db/schema' +import { and, desc, eq } from 'drizzle-orm' +import type { MothershipChat } from '@/lib/api/contracts/mothership-chats' +import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness' + +/** + * Lists a user's mothership (home) chats for a workspace as the contract wire + * shape, shared by the `GET /api/mothership/chats` route and the workspace + * sidebar prefetch. Performs no auth or workspace-access checks — callers + * enforce access before invoking. Reconciles stale live-stream markers and + * normalizes timestamps to ISO strings to honor the wire contract. + */ +export async function listMothershipChats( + userId: string, + workspaceId: string +): Promise { + const chats = await db + .select({ + id: copilotChats.id, + title: copilotChats.title, + updatedAt: copilotChats.updatedAt, + activeStreamId: copilotChats.conversationId, + lastSeenAt: copilotChats.lastSeenAt, + pinned: copilotChats.pinned, + }) + .from(copilotChats) + .where( + and( + eq(copilotChats.userId, userId), + eq(copilotChats.workspaceId, workspaceId), + eq(copilotChats.type, 'mothership') + ) + ) + .orderBy(desc(copilotChats.pinned), desc(copilotChats.updatedAt)) + + const streamMarkers = await reconcileChatStreamMarkers( + chats.map((c) => ({ chatId: c.id, streamId: c.activeStreamId })), + { repairVerifiedStaleMarkers: true } + ) + + return chats.map((c) => ({ + id: c.id, + title: c.title, + updatedAt: c.updatedAt.toISOString(), + activeStreamId: streamMarkers.get(c.id)?.streamId ?? null, + lastSeenAt: c.lastSeenAt ? c.lastSeenAt.toISOString() : null, + pinned: c.pinned, + })) +} diff --git a/apps/sim/lib/users/queries.ts b/apps/sim/lib/users/queries.ts new file mode 100644 index 0000000000..0098efc247 --- /dev/null +++ b/apps/sim/lib/users/queries.ts @@ -0,0 +1,96 @@ +import { db } from '@sim/db' +import { settings, user } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import type { UserSettingsApi } from '@/lib/api/contracts/user' + +/** + * Default user settings returned for unauthenticated users or when no + * settings row exists yet. + */ +export const defaultUserSettings: UserSettingsApi = { + theme: 'system', + autoConnect: true, + telemetryEnabled: true, + emailPreferences: {}, + billingUsageNotificationsEnabled: true, + showTrainingControls: false, + superUserModeEnabled: false, + mothershipEnvironment: 'default', + errorNotificationsEnabled: true, + snapToGridSize: 0, + showActionBar: true, + timezone: null, + lastActiveWorkspaceId: null, +} + +/** + * Loads a user's settings, falling back to {@link defaultUserSettings} when the + * user is unauthenticated or has no persisted settings row. + */ +export async function getUserSettings(userId: string | null): Promise { + if (!userId) { + return defaultUserSettings + } + + const result = await db + .select({ + theme: settings.theme, + autoConnect: settings.autoConnect, + telemetryEnabled: settings.telemetryEnabled, + emailPreferences: settings.emailPreferences, + billingUsageNotificationsEnabled: settings.billingUsageNotificationsEnabled, + showTrainingControls: settings.showTrainingControls, + superUserModeEnabled: settings.superUserModeEnabled, + mothershipEnvironment: settings.mothershipEnvironment, + errorNotificationsEnabled: settings.errorNotificationsEnabled, + snapToGridSize: settings.snapToGridSize, + showActionBar: settings.showActionBar, + timezone: settings.timezone, + lastActiveWorkspaceId: settings.lastActiveWorkspaceId, + }) + .from(settings) + .where(eq(settings.userId, userId)) + .limit(1) + + if (!result.length) { + return defaultUserSettings + } + + const userSettings = result[0] + + return { + theme: userSettings.theme as UserSettingsApi['theme'], + autoConnect: userSettings.autoConnect, + telemetryEnabled: userSettings.telemetryEnabled, + emailPreferences: userSettings.emailPreferences ?? {}, + billingUsageNotificationsEnabled: userSettings.billingUsageNotificationsEnabled ?? true, + showTrainingControls: userSettings.showTrainingControls ?? false, + superUserModeEnabled: userSettings.superUserModeEnabled ?? false, + mothershipEnvironment: + (userSettings.mothershipEnvironment as UserSettingsApi['mothershipEnvironment']) ?? 'default', + errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true, + snapToGridSize: userSettings.snapToGridSize ?? 0, + showActionBar: userSettings.showActionBar ?? true, + timezone: userSettings.timezone ?? null, + lastActiveWorkspaceId: userSettings.lastActiveWorkspaceId ?? null, + } +} + +/** + * Loads a user's public profile fields, or `null` when no matching user exists. + */ +export async function getUserProfile(userId: string) { + const [userRecord] = await db + .select({ + id: user.id, + name: user.name, + email: user.email, + image: user.image, + emailVerified: user.emailVerified, + }) + .from(user) + .where(eq(user.id, userId)) + .limit(1) + + return userRecord ?? null +} diff --git a/apps/sim/lib/workflows/queries.ts b/apps/sim/lib/workflows/queries.ts new file mode 100644 index 0000000000..751cbd2369 --- /dev/null +++ b/apps/sim/lib/workflows/queries.ts @@ -0,0 +1,101 @@ +import { db } from '@sim/db' +import { permissions, workflow } from '@sim/db/schema' +import { and, asc, eq, inArray, isNull, sql } from 'drizzle-orm' +import type { WorkflowListItem } from '@/lib/api/contracts/workflows' + +type WorkflowListScope = 'active' | 'archived' | 'all' + +/** + * Project only the columns declared in `workflowListItemSchema` so the result + * matches the contract wire shape exactly. The full row is larger (`state`, + * `variables`, `apiKey`, `runCount`, etc.) and would be dropped by the client + * Zod parse anyway — narrowing here keeps the payload small. Keep aligned with + * the contract. + */ +const listColumns = { + id: workflow.id, + name: workflow.name, + description: workflow.description, + workspaceId: workflow.workspaceId, + folderId: workflow.folderId, + sortOrder: workflow.sortOrder, + createdAt: workflow.createdAt, + updatedAt: workflow.updatedAt, + archivedAt: workflow.archivedAt, + locked: workflow.locked, +} as const + +const orderByClause = [asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)] + +type WorkflowListRow = { + id: string + name: string + description: string | null + workspaceId: string | null + folderId: string | null + sortOrder: number + createdAt: Date + updatedAt: Date + archivedAt: Date | null + locked: boolean +} + +/** Normalizes timestamp columns to ISO strings to honor the `WorkflowListItem` wire contract. */ +function toListItem(row: WorkflowListRow): WorkflowListItem { + return { + ...row, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + archivedAt: row.archivedAt ? row.archivedAt.toISOString() : null, + } +} + +function scopeCondition( + scope: WorkflowListScope, + base: ReturnType | ReturnType +) { + if (scope === 'all') return base + if (scope === 'archived') return and(base, sql`${workflow.archivedAt} IS NOT NULL`) + return and(base, isNull(workflow.archivedAt)) +} + +/** + * Lists workflows visible to a user as the contract wire shape, shared by the + * `GET /api/workflows` route and the workspace sidebar prefetch. Performs no auth + * or membership checks — callers enforce access before invoking. + * + * With `workspaceId`, returns that workspace's workflows; without it, returns + * workflows across every workspace the user has permissions on. + */ +export async function listWorkflowsForUser({ + userId, + workspaceId, + scope, +}: { + userId: string + workspaceId?: string + scope: WorkflowListScope +}): Promise { + if (workspaceId) { + const rows = await db + .select(listColumns) + .from(workflow) + .where(scopeCondition(scope, eq(workflow.workspaceId, workspaceId))) + .orderBy(...orderByClause) + return rows.map(toListItem) + } + + const workspacePermissionRows = await db + .select({ workspaceId: permissions.entityId }) + .from(permissions) + .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) + const workspaceIds = workspacePermissionRows.map((row) => row.workspaceId) + if (workspaceIds.length === 0) return [] + + const rows = await db + .select(listColumns) + .from(workflow) + .where(scopeCondition(scope, inArray(workflow.workspaceId, workspaceIds))) + .orderBy(...orderByClause) + return rows.map(toListItem) +} From 80735b424bef1d0b35628280f93138ca9ce11e29 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 20:28:35 -0700 Subject: [PATCH 10/26] fix(locks): enforce workflow/folder locks on the agent + close manual-UI create gaps (#5107) * fix(locks): enforce workflow/folder locks on the agent + close manual-UI create gaps The copilot/agent workflow & folder mutation tools and the edit_workflow tool bypassed lock enforcement, so the agent could edit a locked workflow and move/create workflows into a locked folder. Add assertWorkflowMutable/ assertFolderMutable guards (from @sim/workflow-authz) to every agent mutation path, mirroring the REST API. Also close two parity gaps on the manual-UI REST side: creating a workflow into a locked folder and creating a subfolder under a locked parent were previously unguarded. The realtime collaborative canvas already enforced workflow-level locks server-side. * fix(locks): normalize optional folderId to null for assertFolderMutable * refactor(locks): hoist constant folder-lock check out of move loop; scope test mocks with Once * refactor(locks): drop redundant ensureWorkflowAccess fetch in rename --- apps/sim/app/api/folders/route.test.ts | 21 +++++++ apps/sim/app/api/folders/route.ts | 6 ++ apps/sim/app/api/workflows/route.test.ts | 20 ++++++ apps/sim/app/api/workflows/route.ts | 6 ++ .../tools/handlers/workflow/mutations.test.ts | 63 ++++++++++++++++++- .../tools/handlers/workflow/mutations.ts | 41 ++++++++---- .../server/workflow/edit-workflow/index.ts | 4 +- 7 files changed, 147 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/folders/route.test.ts b/apps/sim/app/api/folders/route.test.ts index a72c0a7bbf..baafe6fd2a 100644 --- a/apps/sim/app/api/folders/route.test.ts +++ b/apps/sim/app/api/folders/route.test.ts @@ -9,6 +9,7 @@ import { createMockRequest, permissionsMock, permissionsMockFns, + workflowAuthzMockFns, } from '@sim/testing' import { drizzleOrmMock } from '@sim/testing/mocks' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -390,6 +391,26 @@ describe('Folders API Route', () => { }) }) + it('should reject creating a subfolder inside a locked parent folder', async () => { + mockAuthenticatedUser() + + const { FolderLockedError } = await import('@sim/workflow-authz') + workflowAuthzMockFns.mockAssertFolderMutable.mockRejectedValueOnce( + new FolderLockedError('Folder is locked') + ) + + const req = createMockRequest('POST', { + name: 'Subfolder', + workspaceId: 'workspace-123', + parentId: 'locked-folder', + }) + + const response = await POST(req) + + expect(response.status).toBe(423) + expect(mockTransaction).not.toHaveBeenCalled() + }) + it('should reject a parentId that does not resolve to a folder in the workspace', async () => { mockAuthenticatedUser() diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index 404ebe0873..f359e37654 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' import { and, asc, eq, isNotNull, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createFolderContract, listFoldersContract } from '@/lib/api/contracts' @@ -93,6 +94,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + await assertFolderMutable(parentId ?? null) + const result = await performCreateFolder({ id: clientId, userId: session.user.id, @@ -123,6 +126,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ folder: newFolder }) } catch (error) { + if (error instanceof FolderLockedError) { + return NextResponse.json({ error: error.message }, { status: error.status }) + } logger.error('Error creating folder:', { error }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index 30e75ad2ed..2bfddb3934 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -7,6 +7,7 @@ import { hybridAuthMockFns, permissionsMock, permissionsMockFns, + workflowAuthzMockFns, workflowsApiUtilsMock, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, @@ -80,11 +81,30 @@ describe('Workflows API Route - POST ordering', () => { userEmail: 'test@example.com', }) mockGetUserEntityPermissions.mockResolvedValue('write') + workflowAuthzMockFns.mockAssertFolderMutable.mockResolvedValue(undefined) workflowsPersistenceUtilsMockFns.mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true, }) }) + it('rejects creating a workflow inside a locked folder', async () => { + const { FolderLockedError } = await import('@sim/workflow-authz') + workflowAuthzMockFns.mockAssertFolderMutable.mockRejectedValueOnce( + new FolderLockedError('Folder is locked') + ) + + const req = createMockRequest('POST', { + name: 'New Workflow', + description: 'desc', + workspaceId: 'workspace-123', + folderId: 'locked-folder', + }) + + const response = await POST(req) + expect(response.status).toBe(423) + expect(mockDbInsert).not.toHaveBeenCalled() + }) + it('uses top insertion against mixed siblings (folders + workflows)', async () => { const minResultsQueue: Array> = [ [], diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 92a1798596..224cb68417 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { assertFolderMutable, FolderLockedError } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowContract, workflowListQuerySchema } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' @@ -116,6 +117,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ) } + await assertFolderMutable(folderId ?? null) + const result = await performCreateWorkflow({ id: clientId, name: requestedName, @@ -180,6 +183,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => { subBlockValues: createdWorkflow.subBlockValues, }) } catch (error) { + if (error instanceof FolderLockedError) { + return NextResponse.json({ error: error.message }, { status: error.status }) + } logger.error(`[${requestId}] Error creating workflow`, error) return NextResponse.json({ error: 'Failed to create workflow' }, { status: 500 }) } diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts index 582c42c6a6..037894bf30 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { createEnvMock } from '@sim/testing' +import { createEnvMock, workflowAuthzMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -88,7 +88,16 @@ vi.mock('../access', () => ({ getDefaultWorkspaceId: vi.fn(), })) -import { executeRunFromBlock, executeSetGlobalWorkflowVariables } from './mutations' +import { performUpdateWorkflow } from '@/lib/workflows/orchestration' +import { verifyFolderWorkspace } from '@/lib/workflows/utils' +import { + executeMoveWorkflow, + executeRunFromBlock, + executeSetGlobalWorkflowVariables, +} from './mutations' + +const performUpdateWorkflowMock = vi.mocked(performUpdateWorkflow) +const verifyFolderWorkspaceMock = vi.mocked(verifyFolderWorkspace) describe('executeSetGlobalWorkflowVariables', () => { beforeEach(() => { @@ -134,6 +143,56 @@ describe('executeSetGlobalWorkflowVariables', () => { }) }) +describe('lock enforcement', () => { + beforeEach(() => { + vi.clearAllMocks() + global.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 })) as typeof fetch + workflowAuthzMockFns.mockAssertWorkflowMutable.mockResolvedValue(undefined) + workflowAuthzMockFns.mockAssertFolderMutable.mockResolvedValue(undefined) + }) + + it('does not persist variable changes when the workflow is locked', async () => { + ensureWorkflowAccessMock.mockResolvedValue({ + workflow: { id: 'workflow-1', variables: {} }, + }) + workflowAuthzMockFns.mockAssertWorkflowMutable.mockRejectedValueOnce( + new Error('Workflow is locked') + ) + + const result = await executeSetGlobalWorkflowVariables( + { + workflowId: 'workflow-1', + operations: [{ operation: 'add', name: 'threshold', type: 'number', value: '5' }], + }, + { userId: 'user-1' } as any + ) + + expect(result.success).toBe(false) + expect(result.error).toBe('Workflow is locked') + expect(setWorkflowVariablesMock).not.toHaveBeenCalled() + }) + + it('does not move a workflow into a locked target folder', async () => { + ensureWorkflowAccessMock.mockResolvedValue({ + workspaceId: 'workspace-1', + workflow: { id: 'workflow-1', name: 'WF', folderId: null }, + }) + verifyFolderWorkspaceMock.mockResolvedValue(true) + workflowAuthzMockFns.mockAssertFolderMutable.mockRejectedValueOnce( + new Error('Folder is locked') + ) + + const result = await executeMoveWorkflow( + { workflowIds: ['workflow-1'], folderId: 'locked-folder' }, + { userId: 'user-1' } as any + ) + + expect(result.success).toBe(false) + expect(result.error).toBe('Folder is locked') + expect(performUpdateWorkflowMock).not.toHaveBeenCalled() + }) +}) + describe('executeRunFromBlock', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 115f8ff359..7ae8c2e6a4 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -3,6 +3,7 @@ import { db, workflow as workflowTable } from '@sim/db' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' +import { assertFolderMutable, assertWorkflowMutable } from '@sim/workflow-authz' import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import { eq } from 'drizzle-orm' import { performCreateWorkspaceApiKey } from '@/lib/api-key/orchestration' @@ -353,6 +354,7 @@ export async function executeCreateWorkflow( const folderId = params?.folderId || null await ensureWorkspaceAccess(workspaceId, context.userId, 'write') + await assertFolderMutable(folderId) assertWorkflowMutationNotAborted(context) const result = await performCreateWorkflow({ @@ -422,6 +424,7 @@ export async function executeCreateFolder( const parentId = params?.parentId || null await ensureWorkspaceAccess(workspaceId, context.userId, 'write') + await assertFolderMutable(parentId) assertWorkflowMutationNotAborted(context) const result = await performCreateFolder({ @@ -512,6 +515,7 @@ export async function executeSetGlobalWorkflowVariables( context.userId, 'write' ) + await assertWorkflowMutable(workflowId) interface WorkflowVariable { id: string @@ -633,9 +637,9 @@ export async function executeRenameWorkflow( return { success: false, error: 'Workflow name must be 200 characters or less' } } - await ensureWorkflowAccess(workflowId, context.userId, 'write') - assertWorkflowMutationNotAborted(context) const current = await ensureWorkflowAccess(workflowId, context.userId, 'write') + await assertWorkflowMutable(workflowId) + assertWorkflowMutationNotAborted(context) if (!current.workspaceId) { return { success: false, error: 'Workflow workspace is required' } } @@ -671,6 +675,8 @@ export async function executeMoveWorkflow( const moved: string[] = [] const failed: string[] = [] + await assertFolderMutable(folderId) + for (const workflowId of workflowIds) { try { const { workspaceId, workflow } = await ensureWorkflowAccess( @@ -688,6 +694,7 @@ export async function executeMoveWorkflow( continue } } + await assertWorkflowMutable(workflowId) assertWorkflowMutationNotAborted(context) const result = await performUpdateWorkflow({ workflowId, @@ -735,6 +742,8 @@ export async function executeMoveFolder( return { success: false, error: 'Parent folder not found' } } + await assertFolderMutable(folderId) + await assertFolderMutable(parentId) assertWorkflowMutationNotAborted(context) const result = await performUpdateFolder({ folderId, @@ -941,6 +950,7 @@ async function executeUpdateWorkflow( if (!current.workspaceId) { return { success: false, error: 'Workflow workspace is required' } } + await assertWorkflowMutable(workflowId) assertWorkflowMutationNotAborted(context) const result = await performUpdateWorkflow({ workflowId, @@ -984,6 +994,7 @@ export async function executeSetBlockEnabled( context.userId, 'write' ) + await assertWorkflowMutable(workflowId) assertWorkflowMutationNotAborted(context) const normalized = await loadWorkflowFromNormalizedTables(workflowId) @@ -1114,6 +1125,7 @@ export async function executeDeleteWorkflow( context.userId, 'write' ) + await assertWorkflowMutable(workflowId) assertWorkflowMutationNotAborted(context) const result = await performDeleteWorkflow({ workflowId, userId: context.userId }) @@ -1162,16 +1174,22 @@ export async function executeDeleteFolder( assertWorkflowMutationNotAborted(context) - const result = await performDeleteFolder({ - folderId, - workspaceId, - userId: context.userId, - folderName: folder.folderName, - }) + try { + await assertFolderMutable(folderId) - if (result.success) { - deleted.push(folderId) - } else { + const result = await performDeleteFolder({ + folderId, + workspaceId, + userId: context.userId, + folderName: folder.folderName, + }) + + if (result.success) { + deleted.push(folderId) + } else { + failed.push(folderId) + } + } catch { failed.push(folderId) } } @@ -1206,6 +1224,7 @@ async function executeRenameFolder( return { success: false, error: 'Folder not found' } } + await assertFolderMutable(folderId) assertWorkflowMutationNotAborted(context) const result = await performUpdateFolder({ folderId, diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index 44bb0ee4a8..96a4fa9835 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { EditWorkflow } from '@/lib/copilot/generated/tool-catalog-v1' import { @@ -106,6 +106,8 @@ export const editWorkflowServerTool: BaseServerTool throw new Error(authorization.message || 'Unauthorized workflow access') } + await assertWorkflowMutable(workflowId) + const workspaceId = authorization.workflow?.workspaceId ?? undefined const workflowName = authorization.workflow?.name ?? undefined From 8b93e43037d041519d283c6e3e9369816f0c0c9d Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 16 Jun 2026 23:05:30 -0700 Subject: [PATCH 11/26] improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs (#5109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs - BigQuery: mark null-defaulted outputs optional (get_table type/numRows/numBytes/creationTime/lastModifiedTime/location, list_datasets location, list_tables type, query totalBytesProcessed) - Google Forms: add response pagination (pageToken + filter params, nextPageToken output), fix pageSize visibility, advanced-mode pagination subBlocks + filter wandConfig - PageSpeed: add a 7th BlockMeta template (competitor benchmark) - Regenerate integration docs; add manual intro sections to new datagma/dropcontact/enrow/icypeas/leadmagic pages * fix(docs-gen): preserve apostrophes in tool descriptions when generating docs The doc generator extracted tool descriptions with a character class that excluded both quote types (['"]([^'"]...)['"]), so a double-quoted description containing an apostrophe (e.g. "Find someone's email") was truncated at the apostrophe — the generated docs/catalog showed stubs like "Find someone". Anchor extraction on the actual opening quote (single/double/backtick), matching the existing extractDescription helper, in both buildToolDescriptionMap and extractToolInfo. Regenerated docs restore full descriptions across all affected integrations (Apollo, Ahrefs, LeadMagic, Findymail, OpenAI, Slack, etc.). * fix(docs-gen): resolve tools defined in a sibling file + scope params per tool The doc generator located a tool's definition only by filename convention (decompress.ts / index.ts), so file_decompress — which lives in compress.ts alongside file_compress — fell back to index.ts and rendered an empty Input table. It also read the params block from the first tool in a multi-tool file, so every tool in such a file inherited the first tool's inputs/outputs. - getToolInfo: when no candidate file declares the exact tool ID, scan the whole tool-prefix directory for the file that does. - extractToolInfo: read the params block scoped to the specific tool, falling back to the full file for tools that inherit params via spread. Regenerated docs eliminate ~50 empty/incorrect input tables across integrations (clickhouse, rb2b, reddit, file, etc.); param-less OAuth-only tools correctly keep an empty input table. --- apps/docs/components/icons.tsx | 111 +++ apps/docs/components/ui/icon-mapping.ts | 10 + .../docs/en/integrations/agentphone.mdx | 2 +- .../content/docs/en/integrations/ahrefs.mdx | 2 +- .../content/docs/en/integrations/apollo.mdx | 14 +- .../docs/en/integrations/azure_devops.mdx | 2 +- .../docs/en/integrations/clickhouse.mdx | 348 ++++++---- .../docs/en/integrations/context_dev.mdx | 6 +- .../content/docs/en/integrations/datagma.mdx | 163 +++++ .../docs/en/integrations/dropcontact.mdx | 95 +++ .../content/docs/en/integrations/enrich.mdx | 6 +- .../content/docs/en/integrations/enrow.mdx | 77 +++ .../content/docs/en/integrations/extend.mdx | 8 +- .../content/docs/en/integrations/file.mdx | 79 ++- .../docs/en/integrations/findymail.mdx | 6 +- .../docs/en/integrations/google_calendar.mdx | 12 +- .../docs/en/integrations/google_forms.mdx | 3 + .../docs/en/integrations/google_groups.mdx | 2 +- .../docs/en/integrations/google_sheets.mdx | 22 +- .../docs/en/integrations/google_slides.mdx | 10 +- .../content/docs/en/integrations/icypeas.mdx | 78 +++ .../integrations/jira_service_management.mdx | 2 +- .../content/docs/en/integrations/kalshi.mdx | 34 +- .../docs/en/integrations/leadmagic.mdx | 275 ++++++++ .../content/docs/en/integrations/luma.mdx | 4 +- .../content/docs/en/integrations/meta.json | 5 + .../docs/en/integrations/microsoft_excel.mdx | 8 +- .../docs/en/integrations/mistral_parse.mdx | 10 +- .../content/docs/en/integrations/neo4j.mdx | 2 +- .../content/docs/en/integrations/openai.mdx | 2 +- .../docs/en/integrations/perplexity.mdx | 2 +- .../content/docs/en/integrations/pinecone.mdx | 2 +- .../content/docs/en/integrations/posthog.mdx | 2 +- .../content/docs/en/integrations/pulse.mdx | 11 +- .../content/docs/en/integrations/rb2b.mdx | 643 +++--------------- .../content/docs/en/integrations/reddit.mdx | 223 ++---- .../content/docs/en/integrations/redis.mdx | 2 +- .../content/docs/en/integrations/reducto.mdx | 7 +- .../docs/en/integrations/sap_s4hana.mdx | 2 +- .../content/docs/en/integrations/slack.mdx | 2 +- .../content/docs/en/integrations/table.mdx | 8 +- .../content/docs/en/integrations/tavily.mdx | 8 +- .../content/docs/en/integrations/textract.mdx | 10 +- .../content/docs/en/integrations/upstash.mdx | 2 +- .../content/docs/en/integrations/vercel.mdx | 2 +- .../content/docs/en/integrations/wiza.mdx | 2 +- apps/docs/content/docs/en/integrations/x.mdx | 4 +- apps/sim/blocks/blocks/google_forms.ts | 38 ++ apps/sim/blocks/blocks/google_pagespeed.ts | 10 + apps/sim/lib/integrations/icon-mapping.ts | 10 + apps/sim/lib/integrations/integrations.json | 265 ++++++-- apps/sim/tools/google_bigquery/get_table.ts | 17 +- .../tools/google_bigquery/list_datasets.ts | 6 +- apps/sim/tools/google_bigquery/list_tables.ts | 6 +- apps/sim/tools/google_bigquery/query.ts | 6 +- apps/sim/tools/google_forms/get_responses.ts | 23 +- apps/sim/tools/google_forms/types.ts | 2 + apps/sim/tools/google_forms/utils.ts | 15 +- scripts/generate-docs.ts | 57 +- 59 files changed, 1750 insertions(+), 1035 deletions(-) create mode 100644 apps/docs/content/docs/en/integrations/datagma.mdx create mode 100644 apps/docs/content/docs/en/integrations/dropcontact.mdx create mode 100644 apps/docs/content/docs/en/integrations/enrow.mdx create mode 100644 apps/docs/content/docs/en/integrations/icypeas.mdx create mode 100644 apps/docs/content/docs/en/integrations/leadmagic.mdx diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index cd31b9713c..13fa62588b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -7870,3 +7870,114 @@ export function TriggerDevIcon(props: SVGProps) { ) } + +/** Datagma brand icon: navy square with the white Datagma "D" mark. */ +export function DatagmaIcon(props: SVGProps) { + return ( + + + + + ) +} + +/** LeadMagic brand icon: purple gradient tile with the white spark mark. */ +export function LeadMagicIcon(props: SVGProps) { + const id = useId() + const gradient = `leadmagic_grad_${id}` + return ( + + + + + + + + + + + + + + + + + + ) +} + +/** Dropcontact brand icon: teal disc with the white open-"d" contact mark. */ +export function DropcontactIcon(props: SVGProps) { + return ( + + + + + + ) +} + +/** Icypeas brand icon: dark tile with the teal ring + rising-chart mark. */ +export function IcypeasIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +/** Enrow brand icon: blue tile with the three white stacked rows. */ +export function EnrowIcon(props: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index b65dd78c64..22cf6c737d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -45,12 +45,14 @@ import { DagsterIcon, DatabricksIcon, DatadogIcon, + DatagmaIcon, DaytonaIcon, DevinIcon, DiscordIcon, DocumentIcon, DocuSignIcon, DropboxIcon, + DropcontactIcon, DsPyIcon, DubIcon, DuckDuckGoIcon, @@ -60,6 +62,7 @@ import { EmailBisonIcon, EnrichmentIcon, EnrichSoIcon, + EnrowIcon, EvernoteIcon, ExaAIIcon, ExtendIcon, @@ -101,6 +104,7 @@ import { HuggingFaceIcon, HunterIOIcon, IAMIcon, + IcypeasIcon, IdentityCenterIcon, IncidentioIcon, InfisicalIcon, @@ -114,6 +118,7 @@ import { LangsmithIcon, LatexIcon, LaunchDarklyIcon, + LeadMagicIcon, LemlistIcon, LinearIcon, LinkedInIcon, @@ -273,11 +278,13 @@ export const blockTypeToIconMap: Record = { dagster: DagsterIcon, databricks: DatabricksIcon, datadog: DatadogIcon, + datagma: DatagmaIcon, daytona: DaytonaIcon, devin: DevinIcon, discord: DiscordIcon, docusign: DocuSignIcon, dropbox: DropboxIcon, + dropcontact: DropcontactIcon, dspy: DsPyIcon, dub: DubIcon, duckduckgo: DuckDuckGoIcon, @@ -287,6 +294,7 @@ export const blockTypeToIconMap: Record = { emailbison: EmailBisonIcon, enrich: EnrichSoIcon, enrichment: EnrichmentIcon, + enrow: EnrowIcon, evernote: EvernoteIcon, exa: ExaAIIcon, extend: ExtendIcon, @@ -338,6 +346,7 @@ export const blockTypeToIconMap: Record = { huggingface: HuggingFaceIcon, hunter: HunterIOIcon, iam: IAMIcon, + icypeas: IcypeasIcon, identity_center: IdentityCenterIcon, imap: MailServerIcon, incidentio: IncidentioIcon, @@ -355,6 +364,7 @@ export const blockTypeToIconMap: Record = { langsmith: LangsmithIcon, latex: LatexIcon, launchdarkly: LaunchDarklyIcon, + leadmagic: LeadMagicIcon, lemlist: LemlistIcon, linear: LinearIcon, linear_v2: LinearIcon, diff --git a/apps/docs/content/docs/en/integrations/agentphone.mdx b/apps/docs/content/docs/en/integrations/agentphone.mdx index f00b01b423..bce11e6338 100644 --- a/apps/docs/content/docs/en/integrations/agentphone.mdx +++ b/apps/docs/content/docs/en/integrations/agentphone.mdx @@ -568,7 +568,7 @@ Send an outbound SMS or iMessage from an AgentPhone agent ### `agentphone_update_contact` -Update a contact +Update a contact's fields #### Input diff --git a/apps/docs/content/docs/en/integrations/ahrefs.mdx b/apps/docs/content/docs/en/integrations/ahrefs.mdx index 553e19943b..c05bf0c8eb 100644 --- a/apps/docs/content/docs/en/integrations/ahrefs.mdx +++ b/apps/docs/content/docs/en/integrations/ahrefs.mdx @@ -35,7 +35,7 @@ Integrate Ahrefs SEO tools into your workflow. Analyze domain ratings, backlinks ### `ahrefs_domain_rating` -Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website +Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website's backlink profile on a scale from 0 to 100. #### Input diff --git a/apps/docs/content/docs/en/integrations/apollo.mdx b/apps/docs/content/docs/en/integrations/apollo.mdx index 62bd659d25..ab8eb3b7fa 100644 --- a/apps/docs/content/docs/en/integrations/apollo.mdx +++ b/apps/docs/content/docs/en/integrations/apollo.mdx @@ -41,7 +41,7 @@ Integrates Apollo.io into the workflow. Search for people and companies, enrich ### `apollo_people_search` -Search Apollo +Search Apollo's database for people using demographic filters #### Input @@ -126,7 +126,7 @@ Enrich data for up to 10 people at once using Apollo ### `apollo_organization_search` -Search Apollo +Search Apollo's database for companies using filters #### Input @@ -263,7 +263,7 @@ Update an existing contact in your Apollo database ### `apollo_contact_search` -Search your team +Search your team's contacts in Apollo #### Input @@ -381,7 +381,7 @@ Update an existing account in your Apollo database ### `apollo_account_search` -Search your team +Search your team's accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required. #### Input @@ -480,7 +480,7 @@ Create a new deal for an account in your Apollo database (master key required) ### `apollo_opportunity_search` -Search and list all deals/opportunities in your team +Search and list all deals/opportunities in your team's Apollo account #### Input @@ -544,7 +544,7 @@ Update an existing deal/opportunity in your Apollo database ### `apollo_sequence_search` -Search for sequences/campaigns in your team +Search for sequences/campaigns in your team's Apollo account (master key required) #### Input @@ -650,7 +650,7 @@ Search for tasks in Apollo ### `apollo_email_accounts` -Get list of team +Get list of team's linked email accounts in Apollo #### Input diff --git a/apps/docs/content/docs/en/integrations/azure_devops.mdx b/apps/docs/content/docs/en/integrations/azure_devops.mdx index e4d429d25c..394c03c9ef 100644 --- a/apps/docs/content/docs/en/integrations/azure_devops.mdx +++ b/apps/docs/content/docs/en/integrations/azure_devops.mdx @@ -373,7 +373,7 @@ Fetch full details of a single work item by ID from Azure DevOps, including titl ### `azure_devops_get_work_items_batch` -Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. +Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. "123,456,789"). Requests with more than 200 IDs are automatically split into chunks. #### Input diff --git a/apps/docs/content/docs/en/integrations/clickhouse.mdx b/apps/docs/content/docs/en/integrations/clickhouse.mdx index b22539620e..d9adfb5064 100644 --- a/apps/docs/content/docs/en/integrations/clickhouse.mdx +++ b/apps/docs/content/docs/en/integrations/clickhouse.mdx @@ -114,21 +114,28 @@ Insert a row into a ClickHouse table ### `clickhouse_insert_rows` +Insert multiple rows into a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table to insert into | +| `rows` | json | Yes | Array of row objects to insert, e.g. \[\{"id":1,"name":"a"\},\{"id":2,"name":"b"\}\] | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Inserted rows \(empty for ClickHouse inserts\) | +| `rowCount` | number | Number of rows inserted | ### `clickhouse_update` @@ -183,93 +190,120 @@ Delete rows from a ClickHouse table via an ALTER TABLE ... DELETE mutation ### `clickhouse_list_databases` +List all databases on a ClickHouse server + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | List of databases with engine and comment | +| `rowCount` | number | Number of rows returned | ### `clickhouse_list_tables` +List tables in the connected ClickHouse database + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | ### `clickhouse_describe_table` +Describe the columns of a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to describe | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | ### `clickhouse_show_create_table` +Get the CREATE TABLE statement (DDL) for a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to get the CREATE statement for | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `ddl` | string | The CREATE TABLE statement | ### `clickhouse_count_rows` +Count rows in a ClickHouse table, optionally filtered + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to count rows in | +| `where` | string | No | Optional WHERE clause condition without the WHERE keyword | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `count` | number | Number of rows | ### `clickhouse_introspect` @@ -306,254 +340,328 @@ Introspect a ClickHouse database to retrieve table structures, columns, and engi ### `clickhouse_create_database` +Create a new database on a ClickHouse server + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `name` | string | Yes | Name of the database to create | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_drop_database` +Drop a database from a ClickHouse server + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `name` | string | Yes | Name of the database to drop | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_create_table` +Create a new MergeTree-family table in ClickHouse + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Name of the table to create | +| `columns` | json | Yes | Array of column definitions, each an object with name and type, e.g. \[\{"name":"id","type":"UInt64"\},\{"name":"ts","type":"DateTime"\}\] | +| `engine` | string | No | Table engine \(default MergeTree\) | +| `orderBy` | string | Yes | ORDER BY expression, e.g. "id" or "\(id, ts\)" | +| `partitionBy` | string | No | Optional PARTITION BY expression, e.g. toYYYYMM\(ts\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_drop_table` +Drop a table from a ClickHouse database + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to drop | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_truncate_table` +Remove all rows from a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to truncate | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_rename_table` +Rename a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Current table name | +| `newTable` | string | Yes | New table name | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_optimize_table` +Trigger a merge of table parts via OPTIMIZE TABLE + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table to optimize | +| `final` | boolean | No | Force a merge to a single part using FINAL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_list_partitions` +List active partitions for a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name to inspect partitions for | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | ### `clickhouse_drop_partition` +Drop a partition from a ClickHouse table + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | Yes | Table name | +| `partition` | string | Yes | Partition expression, e.g. '2024-01' or 202401 | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | ### `clickhouse_list_mutations` +List mutations (async ALTER UPDATE/DELETE) for the connected database + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | No | Optional table name to filter mutations | +| `onlyRunning` | boolean | No | Only show mutations that are still running | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of mutation rows | +| `rowCount` | number | Number of rows returned | ### `clickhouse_list_running_queries` +List currently running queries on a ClickHouse server + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | ### `clickhouse_kill_query` +Kill a running query by its query ID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `queryId` | string | Yes | The query_id of the running query to kill | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Kill status rows | +| `rowCount` | number | Number of rows returned | ### `clickhouse_table_stats` +Get row counts and on-disk size for tables in the connected database + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | +| `table` | string | No | Optional table name to get stats for | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of table stats rows | +| `rowCount` | number | Number of rows returned | ### `clickhouse_list_clusters` +List configured clusters, shards, and replicas + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) | +| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | ClickHouse username | +| `password` | string | No | ClickHouse password | +| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Success or error message describing the operation outcome | -| `rows` | array | Array of rows returned from the operation | -| `rowCount` | number | Number of rows returned or affected by the operation | -| `count` | number | Row count \(count rows operation\) | -| `ddl` | string | CREATE TABLE statement \(show create table operation\) | -| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) | +| `message` | string | Operation status message | +| `rows` | array | Array of cluster node rows | +| `rowCount` | number | Number of rows returned | diff --git a/apps/docs/content/docs/en/integrations/context_dev.mdx b/apps/docs/content/docs/en/integrations/context_dev.mdx index 1da7889436..e2b5898af9 100644 --- a/apps/docs/content/docs/en/integrations/context_dev.mdx +++ b/apps/docs/content/docs/en/integrations/context_dev.mdx @@ -275,7 +275,7 @@ Detect and extract structured product details from a single product page URL. ### `context_dev_extract_products` -Extract the product catalog from a brand +Extract the product catalog from a brand's website by domain (beta). #### Input @@ -338,7 +338,7 @@ Extract the font families, usage stats, and font files used by a domain. ### `context_dev_scrape_styleguide` -Extract a domain +Extract a domain's design system: colors, typography, spacing, shadows, and UI components. #### Input @@ -657,7 +657,7 @@ Queue a domain for brand-data prefetching to reduce latency on later requests (s ### `context_dev_prefetch_by_email` -Queue an email +Queue an email's domain for brand-data prefetching to reduce later latency (subscribers; 0 credits). Free/disposable emails are rejected. #### Input diff --git a/apps/docs/content/docs/en/integrations/datagma.mdx b/apps/docs/content/docs/en/integrations/datagma.mdx new file mode 100644 index 0000000000..2df69bc0a3 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/datagma.mdx @@ -0,0 +1,163 @@ +--- +title: Datagma +description: Find verified B2B emails, mobile phones, and enrich person or company profiles +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Datagma](https://datagma.com/) is a B2B data enrichment platform for finding verified work emails, direct mobile numbers, and detailed person and company profiles from minimal input such as a name, company domain, or LinkedIn URL. + +With Datagma, you can: + +- **Find verified work emails:** Resolve a verified professional email from a person's full name and their company name or domain. +- **Enrich person profiles:** Pull job title, seniority, location, and social profiles from an email or LinkedIn URL. +- **Enrich company data:** Retrieve firmographics such as size, industry, and location from a domain or company name. +- **Find mobile phone numbers:** Look up direct dial mobile numbers from a LinkedIn profile. +- **Check your credit balance:** Monitor remaining Datagma credits before running large enrichment jobs. + +In Sim, the Datagma integration lets your agents enrich contacts and companies, find and verify emails, and look up phone numbers directly inside a workflow — automating lead generation, CRM hygiene, and outreach prep without leaving Sim. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Datagma to find verified work emails from a name and company, enrich person profiles via email or LinkedIn URL, enrich company data from a domain or name, look up mobile phone numbers from LinkedIn, and check your credit balance. + + + +## Actions + +### `datagma_find_email` + +Find a verified work email from a person's full name and company. Uses 1 credit when a verified email is found. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fullName` | string | Yes | Person's full name \(e.g., 'John Doe'\) | +| `company` | string | Yes | Company name or domain \(e.g., 'Stripe' or 'stripe.com'\) | +| `linkedInSlug` | string | No | LinkedIn company URL slug to improve match accuracy by 20%+ | +| `findEmailV2Step` | number | No | Lookup depth: 3 = full email \(default\), 2 = domain only | +| `findEmailV2Country` | string | No | User's location to improve accuracy \(e.g., 'General', 'Japan', 'France'\) | +| `apiKey` | string | Yes | Datagma API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | Verified work email address | +| `emailStatus` | string | Email verification status \(e.g., valid, invalid\) | +| `emailDomain` | string | Email domain | +| `mxfound` | boolean | Whether MX records were found | +| `smtpCheck` | boolean | Whether SMTP validation succeeded | +| `catchAll` | boolean | Whether the domain is catch-all | + +### `datagma_enrich_person` + +Enrich a person's profile using their email, LinkedIn URL, or full name and company. Returns job title, company, location, and social data. Uses 2 credits per match; add 30 credits when a phone number is found. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `data` | string | Yes | Email address, LinkedIn URL, or full name \(use companyKeyword when providing a name\) | +| `companyKeyword` | string | No | Company name or keyword to disambiguate when data is a full name | +| `countryCode` | string | No | Two-letter country code to improve match accuracy \(e.g., 'US', 'GB'\) | +| `personFull` | boolean | No | Include education and work history in the response | +| `phoneFull` | boolean | No | Attempt to find a mobile phone number \(costs 30 additional credits if found\) | +| `apiKey` | string | Yes | Datagma API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `name` | string | Full name | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Work email address | +| `emailStatus` | string | Email verification status | +| `jobTitle` | string | Current job title | +| `company` | string | Current company name | +| `linkedInUrl` | string | LinkedIn profile URL | +| `location` | string | Location string | +| `country` | string | Country | +| `region` | string | Region/state | +| `city` | string | City | +| `extractedRole` | string | Extracted role category | +| `extractedSeniority` | string | Extracted seniority level | +| `twitter` | string | Twitter handle | +| `phone` | string | Mobile phone number | +| `personConfidenceScore` | number | Confidence score for the person match \(0–1\) | + +### `datagma_enrich_company` + +Enrich a company profile using a domain, company name, or SIREN number (France). Returns size, industry, revenue, and description. Uses 2 credits per match. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `data` | string | Yes | Company domain \(e.g., 'stripe.com'\), company name, or French SIREN number to enrich | +| `companyPremium` | boolean | No | Include LinkedIn company data in the response | +| `companyFull` | boolean | No | Include financial information in the response | +| `apiKey` | string | Yes | Datagma API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `name` | string | Company name | +| `website` | string | Company website | +| `industries` | string | Industry classification | +| `companySize` | string | Employee headcount range | +| `type` | string | Company type \(e.g., Private, Public\) | +| `founded` | string | Year founded | +| `shortDescription` | string | Short company description | +| `revenueRange` | string | Estimated annual revenue range | +| `headquarters` | string | Headquarters location | + +### `datagma_find_phone` + +Find a mobile phone number from a person's LinkedIn URL. Optionally supply an email to improve match accuracy. Uses 30 credits when a number is found. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `username` | string | Yes | LinkedIn URL of the person \(e.g., 'https://linkedin.com/in/johndoe'\) | +| `email` | string | No | Email address to improve phone match accuracy | +| `minimumMatch` | number | No | Minimum match confidence threshold \(0–1; default 1 for highest precision\) | +| `apiKey` | string | Yes | Datagma API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `phone` | string | Mobile phone number | +| `countryCode` | string | Country code prefix \(e.g., +1\) | +| `isWhatsapp` | boolean | Whether the number is linked to WhatsApp | + +### `datagma_get_credits` + +Check remaining credit balance on a Datagma account. Free — no credits consumed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Datagma API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `credits` | number | Remaining Datagma credits | + + diff --git a/apps/docs/content/docs/en/integrations/dropcontact.mdx b/apps/docs/content/docs/en/integrations/dropcontact.mdx new file mode 100644 index 0000000000..1f5cf1c8f2 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/dropcontact.mdx @@ -0,0 +1,95 @@ +--- +title: Dropcontact +description: Enrich B2B contacts with verified email, phone, and company data +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Dropcontact](https://www.dropcontact.com/) is a GDPR-compliant B2B enrichment service that verifies and completes contact data without relying on a static database — it computes and double-checks each result on demand. + +With Dropcontact, you can: + +- **Verify and enrich contacts:** Submit a name, company, website, or LinkedIn URL and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. +- **Get deliverable emails only:** Dropcontact validates every email it returns, so credits are charged only when a verified address is found. +- **Handle async enrichment cleanly:** Requests are processed asynchronously and Sim polls until the result is ready, so your workflow waits for a complete answer. + +In Sim, the Dropcontact integration lets your agents enrich and verify B2B contacts inside a workflow — keeping your CRM accurate and your outreach lists clean without manual lookups. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Use Dropcontact to verify and enrich B2B contacts. Submit a contact with their name, company, website, or LinkedIn URL and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. Enrichment is async: Dropcontact processes the request, then Sim polls until the result is ready. Credits are only charged when a verified email is returned. + + + +## Actions + +### `dropcontact_enrich_contact` + +Enrich a contact with verified B2B email, phone, company data, and LinkedIn info via Dropcontact. Submits an async enrichment request, then polls until the result is ready (up to 2 minutes). Charges 1 credit only when a verified email is returned. Provide at least one of: email, first_name+last_name+company, full_name+company, or linkedin URL. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dropcontact API key \(X-Access-Token\) | +| `email` | string | No | Email address of the contact to enrich | +| `first_name` | string | No | First name of the contact | +| `last_name` | string | No | Last name of the contact | +| `full_name` | string | No | Full name \(alternative to first_name + last_name\) | +| `company` | string | No | Company name | +| `website` | string | No | Company website \(e.g. acme.com\) | +| `num_siren` | string | No | French company SIREN number | +| `phone` | string | No | Phone number | +| `linkedin` | string | No | LinkedIn profile URL | +| `country` | string | No | Country code \(ISO 3166-1 alpha-2, e.g. "US", "FR"\) | +| `siren` | boolean | No | Include SIREN/SIRET enrichment \(France only\) | +| `language` | string | No | Language for returned data \(e.g. "en", "fr"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `request_id` | string | Dropcontact async request ID | +| `email_found` | boolean | Whether a verified email was found | +| `email` | string | Primary verified email address | +| `emails` | array | All email addresses returned \(each with email and qualification\) | +| ↳ `email` | string | Email address | +| ↳ `qualification` | string | Email qualification \(e.g. nominative@pro\) | +| `qualification` | string | Primary email qualification \(e.g. nominative@pro, catch_all@pro\) | +| `first_name` | string | First name | +| `last_name` | string | Last name | +| `full_name` | string | Full name | +| `civility` | string | Civility \(Mr, Mrs, etc.\) | +| `phone` | string | Phone number | +| `mobile_phone` | string | Mobile phone number | +| `company` | string | Company name | +| `website` | string | Company website | +| `company_linkedin` | string | Company LinkedIn URL | +| `linkedin` | string | Personal LinkedIn URL | +| `country` | string | Country code \(ISO 3166-1 alpha-2\) | +| `siren` | string | French SIREN number | +| `siret` | string | French SIRET number | +| `siret_address` | string | SIRET registered address | +| `siret_zip` | string | SIRET registered postal code | +| `siret_city` | string | SIRET registered city | +| `vat` | string | VAT number | +| `nb_employees` | string | Employee count range | +| `employee_count` | number | Exact employee count \(Growth plan and above\) | +| `naf5_code` | string | NAF/APE code \(France\) | +| `naf5_des` | string | NAF/APE code description \(France\) | +| `industry` | string | Industry classification | +| `job` | string | Job title | +| `job_level` | string | Job seniority level \(e.g. C-level, Director\) | +| `job_function` | string | Job function \(e.g. Sales, Engineering\) | +| `company_turnover` | string | Company revenue/turnover range | +| `company_results` | string | Company net results | + + diff --git a/apps/docs/content/docs/en/integrations/enrich.mdx b/apps/docs/content/docs/en/integrations/enrich.mdx index 62ed693048..01f839a630 100644 --- a/apps/docs/content/docs/en/integrations/enrich.mdx +++ b/apps/docs/content/docs/en/integrations/enrich.mdx @@ -181,7 +181,7 @@ Enrich a LinkedIn profile URL with detailed information including positions, edu ### `enrich_find_email` -Find a person +Find a person's work email address using their full name and company domain. #### Input @@ -922,7 +922,7 @@ Get comments on a LinkedIn post by its URL. ### `enrich_search_people_activities` -Get a person +Get a person's LinkedIn activities (posts, comments, or articles) by profile ID. #### Input @@ -957,7 +957,7 @@ Get a person ### `enrich_search_company_activities` -Get a company +Get a company's LinkedIn activities (posts, comments, or articles) by company ID. #### Input diff --git a/apps/docs/content/docs/en/integrations/enrow.mdx b/apps/docs/content/docs/en/integrations/enrow.mdx new file mode 100644 index 0000000000..d0f8888603 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/enrow.mdx @@ -0,0 +1,77 @@ +--- +title: Enrow +description: Find and verify B2B emails with triple-verified accuracy +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Enrow](https://enrow.io/) is a B2B email-finding and verification service built for high accuracy, using triple verification — including deterministic checks on catch-all domains — so results are deliverable without a separate verifier. + +With Enrow, you can: + +- **Find verified B2B emails:** Resolve a professional email address from a person's full name and their company name or domain. +- **Verify existing emails:** Check the deliverability and validity of an email, including reliable handling of catch-all domains. + +In Sim, the Enrow integration lets your agents find and verify professional emails inside a workflow — powering accurate, high-deliverability outreach and clean contact lists without manual checks. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Enrow to find verified B2B email addresses from a full name and company, or verify the deliverability of an existing email. Enrow performs deterministic verifications including catch-all emails — no additional verifier needed. + + + +## Actions + +### `enrow_find_email` + +Find a verified B2B email address from a full name and company domain or name. Uses the Enrow async finder — submits a search and polls until the result is ready. Costs 1 credit per valid email found. (https://enrow.readme.io/reference/find-single-email) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Enrow API key | +| `fullname` | string | Yes | Full name of the person \(e.g. "John Doe"\) | +| `company_domain` | string | No | Company domain \(e.g. "apple.com"\). Preferred over company_name. | +| `company_name` | string | No | Company name \(e.g. "Apple"\). Used when domain is unavailable. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Enrow job identifier used for polling | +| `email` | string | Email address found or verified | +| `qualification` | string | Enrow quality result: "valid" or "invalid" | +| `fullname` | string | Full name of the person searched | +| `company_name` | string | Company name associated with the result | +| `company_domain` | string | Company domain associated with the result | +| `linkedin_url` | string | LinkedIn profile URL of the person | + +### `enrow_verify_email` + +Verify the deliverability of an email address using the Enrow async verifier. Submits a verification request and polls until the result is ready. Costs 0.25 credits per verification. (https://enrow.readme.io/reference/verify-single-email) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Enrow API key | +| `email` | string | Yes | Email address to verify \(e.g. "john@example.com"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Enrow job identifier used for polling | +| `email` | string | Email address found or verified | +| `qualification` | string | Enrow quality result: "valid" or "invalid" | + + diff --git a/apps/docs/content/docs/en/integrations/extend.mdx b/apps/docs/content/docs/en/integrations/extend.mdx index c1e6d2c80d..d0a31696b1 100644 --- a/apps/docs/content/docs/en/integrations/extend.mdx +++ b/apps/docs/content/docs/en/integrations/extend.mdx @@ -24,13 +24,7 @@ Integrate Extend AI into the workflow. Parse and extract structured content from | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | No | URL to a document to be processed | -| `file` | file | No | Document file to be processed | -| `fileUpload` | object | No | File upload data from file-upload component | -| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. | -| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. | -| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. | -| `apiKey` | string | Yes | Extend API key | +| `file` | file | Yes | Document to be processed | #### Output diff --git a/apps/docs/content/docs/en/integrations/file.mdx b/apps/docs/content/docs/en/integrations/file.mdx index 75759eb25d..92542762fb 100644 --- a/apps/docs/content/docs/en/integrations/file.mdx +++ b/apps/docs/content/docs/en/integrations/file.mdx @@ -1,6 +1,6 @@ --- title: File -description: Read, get content, fetch, write, and append files +description: Read, get content, fetch, write, append, compress, and decompress files --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -12,7 +12,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions -Read workspace file objects, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files. +Read workspace file objects, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, append content to existing files, compress files into a .zip archive, or extract a .zip archive into the workspace. @@ -24,60 +24,52 @@ Read workspace file objects, extract the text content of files, fetch and parse | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `fileId` | string | No | Canonical workspace file ID, or an array of canonical workspace file IDs. | +| `fileInput` | file | No | Selected workspace file object. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | file[] | Workspace file objects \(read\) or fetched file objects \(fetch\) | -| `contents` | array | Array of file text contents, one entry per file \(get content\) | -| `combinedContent` | string | All fetched file contents merged into a single text string \(fetch\) | -| `id` | string | File ID \(write and append\) | -| `name` | string | File name \(write and append\) | -| `size` | number | File size in bytes \(write and append\) | -| `url` | string | URL to access the file \(write and append\) | +| `files` | file[] | Workspace file objects | ### `file_get_content` +Extract the text content of one or more workspace files from selected file objects or canonical workspace file IDs. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `fileId` | string | No | Canonical workspace file ID, or an array of canonical workspace file IDs. | +| `fileInput` | file | No | Selected workspace file object, or an array of file objects. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | file[] | Workspace file objects \(read\) or fetched file objects \(fetch\) | -| `contents` | array | Array of file text contents, one entry per file \(get content\) | -| `combinedContent` | string | All fetched file contents merged into a single text string \(fetch\) | -| `id` | string | File ID \(write and append\) | -| `name` | string | File name \(write and append\) | -| `size` | number | File size in bytes \(write and append\) | -| `url` | string | URL to access the file \(write and append\) | +| `contents` | array | Array of file text contents, one entry per file in input order | ### `file_fetch` +Fetch and parse a file from a URL with optional custom headers. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `headers` | object | No | HTTP headers to include when fetching URL-based files. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | file[] | Workspace file objects \(read\) or fetched file objects \(fetch\) | -| `contents` | array | Array of file text contents, one entry per file \(get content\) | -| `combinedContent` | string | All fetched file contents merged into a single text string \(fetch\) | -| `id` | string | File ID \(write and append\) | -| `name` | string | File name \(write and append\) | -| `size` | number | File size in bytes \(write and append\) | -| `url` | string | URL to access the file \(write and append\) | +| `files` | file[] | Fetched files as UserFile objects | +| `combinedContent` | string | Combined content of all fetched files | ### `file_write` -Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g., +Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g., "data (1).csv"). #### Input @@ -116,4 +108,43 @@ Append content to an existing workspace file. The file must already exist. Conte | `size` | number | File size in bytes | | `url` | string | URL to access the file | +### `file_compress` + +Compress one or more workspace files into a single .zip archive stored in the workspace, for bundling files to download, transfer, or store. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | No | Canonical workspace file ID, or an array of canonical workspace file IDs. | +| `fileInput` | file | No | Selected workspace file object, or an array of file objects. | +| `archiveName` | string | No | Name for the .zip archive \(e.g., "documents.zip"\). Defaults to the source file name when compressing a single file, otherwise "archive.zip". | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Compressed archive file ID | +| `name` | string | Compressed archive file name | +| `size` | number | Compressed archive size in bytes | +| `url` | string | URL to access the compressed archive | +| `files` | file[] | Compressed archive file object, as a single-item array | + +### `file_decompress` + +Extract the contents of a .zip archive into the workspace, preserving the archive folder structure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | No | Canonical workspace file ID of the .zip archive to extract. | +| `fileInput` | file | No | Selected .zip archive file object. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `files` | file[] | Extracted workspace file objects | + diff --git a/apps/docs/content/docs/en/integrations/findymail.mdx b/apps/docs/content/docs/en/integrations/findymail.mdx index ef5948ec59..eac8a447e6 100644 --- a/apps/docs/content/docs/en/integrations/findymail.mdx +++ b/apps/docs/content/docs/en/integrations/findymail.mdx @@ -57,7 +57,7 @@ Verifies the deliverability of an email address. Uses one verifier credit. ### `findymail_find_email_from_name` -Find someone +Find someone's email from their name and a company domain or company name. Uses one finder credit when a verified email is found. #### Input @@ -99,7 +99,7 @@ Find verified contacts at a given domain matching one or more target roles (max ### `findymail_find_email_from_linkedin` -Find someone +Find someone's email from a LinkedIn profile URL or username. Uses one finder credit when a verified email is found. #### Input @@ -203,7 +203,7 @@ Find employees at a company by website and target job titles. Uses 1 credit per ### `findymail_find_phone` -Find someone +Find someone's phone number from a LinkedIn profile URL. Uses 10 finder credits if a phone is found. EU citizens are excluded for legal reasons. #### Input diff --git a/apps/docs/content/docs/en/integrations/google_calendar.mdx b/apps/docs/content/docs/en/integrations/google_calendar.mdx index 648d6896e1..2bedcf12eb 100644 --- a/apps/docs/content/docs/en/integrations/google_calendar.mdx +++ b/apps/docs/content/docs/en/integrations/google_calendar.mdx @@ -50,7 +50,7 @@ Create a new event in Google Calendar. Returns API-aligned fields only. | `location` | string | No | Event location | | `startDateTime` | string | Yes | Start time. Use a datetime with timezone offset \(2025-06-03T10:00:00-08:00\) or a date \(2025-06-03\) for an all-day event | | `endDateTime` | string | Yes | End time. Use a datetime with timezone offset \(2025-06-03T11:00:00-08:00\) or a date \(2025-06-04\) for an all-day event | -| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided. | +| `timeZone` | string | No | IANA time zone \(e.g., America/Los_Angeles\). Used as-is when provided. For recurring events a time zone is required to expand the recurrence correctly; for one-off events it is only needed when the datetime omits a UTC offset \(a naive datetime defaults to America/Los_Angeles\). | | `attendees` | array | No | Array of attendee email addresses | | `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. | | `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event | @@ -88,7 +88,7 @@ List events from Google Calendar. Returns API-aligned fields only. | `q` | string | No | Free-text search across event summary, description, location, attendees, and organizer | | `maxResults` | number | No | Maximum number of events to return \(max 2500\) | | `pageToken` | string | No | Token for retrieving the next page of results | -| `orderBy` | string | No | Order of events returned \(startTime or updated\). Defaults to startTime. | +| `orderBy` | string | No | Order of events: startTime \(chronological, the default\) or updated \(last-modified\). startTime is always valid here because singleEvents is set. | | `showDeleted` | boolean | No | Include deleted events | #### Output @@ -141,9 +141,9 @@ Update an existing event in Google Calendar. Returns API-aligned fields only. | `location` | string | No | New event location | | `startDateTime` | string | No | New start time. Use a datetime with timezone offset \(2025-06-03T10:00:00-08:00\) or a date \(2025-06-03\) for an all-day event | | `endDateTime` | string | No | New end time. Use a datetime with timezone offset \(2025-06-03T11:00:00-08:00\) or a date \(2025-06-04\) for an all-day event | -| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. | -| `attendees` | array | No | Array of attendee email addresses \(replaces the existing attendee list\) | -| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. | +| `timeZone` | string | No | IANA time zone \(e.g., America/Los_Angeles\) applied to the start/end times provided in this update. Provide a new start and/or end time to change the time zone; a time zone on its own is not applied. Required for recurring events to expand the recurrence correctly. | +| `attendees` | array | No | Array of attendee email addresses. When one or more emails are provided, they replace the existing attendee list. Leaving this empty keeps the current attendees unchanged \(it does not clear them\). | +| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. When provided, replaces the event's recurrence; leaving it empty keeps the existing recurrence unchanged. Requires a timeZone for timed events. | | `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event | | `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | @@ -239,7 +239,7 @@ Get instances of a recurring event from Google Calendar. Returns API-aligned fie ### `google_calendar_list_calendars` -List all calendars in the user +List all calendars in the user's calendar list. Returns API-aligned fields only. #### Input diff --git a/apps/docs/content/docs/en/integrations/google_forms.mdx b/apps/docs/content/docs/en/integrations/google_forms.mdx index a580afbbeb..cf3e481f7d 100644 --- a/apps/docs/content/docs/en/integrations/google_forms.mdx +++ b/apps/docs/content/docs/en/integrations/google_forms.mdx @@ -46,6 +46,8 @@ Retrieve a single response or list responses from a Google Form | `formId` | string | Yes | Google Forms form ID | | `responseId` | string | No | Response ID - if provided, returns this specific response | | `pageSize` | number | No | Maximum number of responses to return \(service may return fewer\). Defaults to 5000. | +| `pageToken` | string | No | Page token from a previous list response to fetch the next page of responses | +| `filter` | string | No | Filter responses, e.g. "timestamp > 2024-01-01T00:00:00Z" \(RFC3339 UTC\). Only timestamp filters are supported. | #### Output @@ -56,6 +58,7 @@ Retrieve a single response or list responses from a Google Form | ↳ `createTime` | string | When the response was created | | ↳ `lastSubmittedTime` | string | When the response was last submitted | | ↳ `answers` | json | Map of question IDs to answer values | +| `nextPageToken` | string | Token to fetch the next page of responses \(null when no more pages\) | | `response` | object | Single form response \(when responseId is provided\) | | ↳ `responseId` | string | Unique response ID | | ↳ `createTime` | string | When the response was created | diff --git a/apps/docs/content/docs/en/integrations/google_groups.mdx b/apps/docs/content/docs/en/integrations/google_groups.mdx index 24739a8296..1e49d5fc2a 100644 --- a/apps/docs/content/docs/en/integrations/google_groups.mdx +++ b/apps/docs/content/docs/en/integrations/google_groups.mdx @@ -199,7 +199,7 @@ Remove a member from a Google Group ### `google_groups_update_member` -Update a member +Update a member's role in a Google Group (promote or demote) #### Input diff --git a/apps/docs/content/docs/en/integrations/google_sheets.mdx b/apps/docs/content/docs/en/integrations/google_sheets.mdx index d52ebeaf95..c658c1030e 100644 --- a/apps/docs/content/docs/en/integrations/google_sheets.mdx +++ b/apps/docs/content/docs/en/integrations/google_sheets.mdx @@ -44,8 +44,12 @@ Read data from a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet \(found in the URL: docs.google.com/spreadsheets/d/\{SPREADSHEET_ID\}/edit\). | -| `range` | string | No | The A1 notation range to read \(e.g. "Sheet1!A1:D10", "A1:B5"\). Defaults to first sheet A1:Z1000 if not specified. | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetName` | string | Yes | The name of the sheet/tab to read from | +| `cellRange` | string | No | The cell range to read \(e.g. "A1:D10"\). Defaults to "A1:Z1000" if not specified. | +| `filterColumn` | string | No | Column name \(from the header row\) to filter on. Filtering is applied to the rows returned by the read range \(the default is A1:Z1000\), not the entire sheet. If not provided, no filtering is applied. | +| `filterValue` | string | No | Value to match against the filter column. | +| `filterMatchType` | string | No | How to match the filter value. Text: "contains", "not_contains", "exact", "not_equals", "starts_with", "ends_with". Numeric/ordering: "gt", "gte", "lt", "lte" \(numeric when both values are numbers, otherwise lexicographic\). Defaults to "contains". | #### Output @@ -66,8 +70,9 @@ Write data to a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet | -| `range` | string | No | The A1 notation range to write to \(e.g. "Sheet1!A1:D10", "A1:B5"\) | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetName` | string | Yes | The name of the sheet/tab to write to | +| `cellRange` | string | No | The cell range to write to \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | | `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | | `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | @@ -92,8 +97,9 @@ Update data in a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet to update | -| `range` | string | No | The A1 notation range to update \(e.g. "Sheet1!A1:D10", "A1:B5"\) | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetName` | string | Yes | The name of the sheet/tab to update | +| `cellRange` | string | No | The cell range to update \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | | `values` | array | Yes | The data to update as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to update | | `includeValuesInResponse` | boolean | No | Whether to include the updated values in the response | @@ -118,8 +124,8 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to | -| `range` | string | No | The A1 notation range to append after \(e.g. "Sheet1", "Sheet1!A:D"\) | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetName` | string | Yes | The name of the sheet/tab to append to | | `values` | array | Yes | The data to append as a 2D array \(e.g. \[\["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to append | | `insertDataOption` | string | No | How to insert the data \(OVERWRITE or INSERT_ROWS\) | diff --git a/apps/docs/content/docs/en/integrations/google_slides.mdx b/apps/docs/content/docs/en/integrations/google_slides.mdx index 0957eff851..591bd9dc97 100644 --- a/apps/docs/content/docs/en/integrations/google_slides.mdx +++ b/apps/docs/content/docs/en/integrations/google_slides.mdx @@ -539,7 +539,7 @@ Remove bullets/numbering from paragraphs in a shape or table cell. ### `google_slides_replace_all_shapes_with_image` -Find every shape whose text matches the given token (e.g. \{\{cover-image\}\}) and replace it with an image, preserving the shape +Find every shape whose text matches the given token (e.g. \{\{cover-image\}\}) and replace it with an image, preserving the shape's position and bounds. #### Input @@ -565,7 +565,7 @@ Find every shape whose text matches the given token (e.g. \{\{cover-image\}\}) a ### `google_slides_replace_image` -Replace the source of an existing image with a new image URL, preserving the image +Replace the source of an existing image with a new image URL, preserving the image's position, size, and properties. #### Input @@ -625,7 +625,7 @@ Update image properties — brightness, contrast, transparency, crop, outline, l ### `google_slides_update_shape_properties` -Update a shape +Update a shape's appearance — background fill color, outline, link, content alignment, autofit. Pass only the properties you want to change. #### Input @@ -887,7 +887,7 @@ Update line appearance — color, weight, dash style, arrows, link. ### `google_slides_update_line_category` -Change a connector line +Change a connector line's category (STRAIGHT, BENT, or CURVED). #### Input @@ -1243,7 +1243,7 @@ Refresh an embedded linked Sheets chart so it reflects the latest spreadsheet da ### `google_slides_replace_all_shapes_with_sheets_chart` -Find every shape matching a text token (e.g. \{\{revenue-chart\}\}) and replace each with the same embedded Sheets chart, preserving the shape +Find every shape matching a text token (e.g. \{\{revenue-chart\}\}) and replace each with the same embedded Sheets chart, preserving the shape's position and bounds. #### Input diff --git a/apps/docs/content/docs/en/integrations/icypeas.mdx b/apps/docs/content/docs/en/integrations/icypeas.mdx new file mode 100644 index 0000000000..e24c4e733d --- /dev/null +++ b/apps/docs/content/docs/en/integrations/icypeas.mdx @@ -0,0 +1,78 @@ +--- +title: Icypeas +description: Find and verify professional email addresses +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Icypeas](https://icypeas.com/) is a B2B prospecting platform for finding and verifying professional email addresses at scale, with results returned asynchronously via polling. + +With Icypeas, you can: + +- **Find professional emails:** Resolve a likely professional email from a person's name and their company domain. +- **Verify existing emails:** Check whether an email address is valid and deliverable before adding it to your outreach. + +In Sim, the Icypeas integration lets your agents find and verify professional emails inside a workflow — automating lead enrichment and keeping outreach lists accurate without manual lookups. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Icypeas to find a professional email address from a name and company domain, or verify whether an existing email is valid and deliverable. Results are returned asynchronously via polling. + + + +## Actions + +### `icypeas_find_email` + +Find a professional email address from a first name, last name, and company domain or name. Submits the search and polls until a result is available. Costs 1 credit per found email (https://www.icypeas.com/pricing). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Icypeas API key | +| `firstname` | string | No | Target person's first name | +| `lastname` | string | No | Target person's last name | +| `domainOrCompany` | string | Yes | Target company domain \(e.g. stripe.com\) or company name \(e.g. Stripe\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `searchId` | string | Icypeas internal search ID | +| `status` | string | Terminal search status: FOUND \| DEBITED \| NOT_FOUND \| DEBITED_NOT_FOUND \| BAD_INPUT \| INSUFFICIENT_FUNDS \| ABORTED | +| `email` | string | Email address found or verified | +| `item` | json | Full raw item object returned by the Icypeas results endpoint | +| `firstname` | string | Found person's first name | +| `lastname` | string | Found person's last name | + +### `icypeas_verify_email` + +Verify whether an email address is valid and deliverable. Submits the verification and polls until a result is available. Costs 0.1 credit per verification (https://www.icypeas.com/pricing). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Icypeas API key | +| `email` | string | Yes | Email address to verify \(e.g. john@stripe.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `searchId` | string | Icypeas internal search ID | +| `status` | string | Terminal search status: FOUND \| DEBITED \| NOT_FOUND \| DEBITED_NOT_FOUND \| BAD_INPUT \| INSUFFICIENT_FUNDS \| ABORTED | +| `email` | string | Email address found or verified | +| `item` | json | Full raw item object returned by the Icypeas results endpoint | +| `valid` | boolean | Whether the email is valid/deliverable \(true for FOUND/DEBITED status\) | + + diff --git a/apps/docs/content/docs/en/integrations/jira_service_management.mdx b/apps/docs/content/docs/en/integrations/jira_service_management.mdx index b22676bbbc..67d793049a 100644 --- a/apps/docs/content/docs/en/integrations/jira_service_management.mdx +++ b/apps/docs/content/docs/en/integrations/jira_service_management.mdx @@ -1109,7 +1109,7 @@ Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the ### `jsm_search_objects_aql` -Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = +Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = "Host" AND Status = "Running". Supports pagination. #### Input diff --git a/apps/docs/content/docs/en/integrations/kalshi.mdx b/apps/docs/content/docs/en/integrations/kalshi.mdx index e780c20b4d..81f60aff82 100644 --- a/apps/docs/content/docs/en/integrations/kalshi.mdx +++ b/apps/docs/content/docs/en/integrations/kalshi.mdx @@ -45,6 +45,15 @@ Retrieve a list of prediction markets from Kalshi with all filtering options (V2 | `status` | string | No | Filter by market status: "unopened", "open", "closed", or "settled" | | `seriesTicker` | string | No | Filter by series ticker \(e.g., "KXBTC", "INX", "FED-RATE"\) | | `eventTicker` | string | No | Filter by event ticker \(e.g., "KXBTC-24DEC31", "INX-25JAN03"\) | +| `minCreatedTs` | number | No | Minimum created timestamp in Unix seconds \(e.g., 1704067200\) | +| `maxCreatedTs` | number | No | Maximum created timestamp in Unix seconds \(e.g., 1704153600\) | +| `minUpdatedTs` | number | No | Minimum updated timestamp in Unix seconds \(e.g., 1704067200\) | +| `minCloseTs` | number | No | Minimum close timestamp in Unix seconds \(e.g., 1704067200\) | +| `maxCloseTs` | number | No | Maximum close timestamp in Unix seconds \(e.g., 1704153600\) | +| `minSettledTs` | number | No | Minimum settled timestamp in Unix seconds \(e.g., 1704067200\) | +| `maxSettledTs` | number | No | Maximum settled timestamp in Unix seconds \(e.g., 1704153600\) | +| `tickers` | string | No | Comma-separated list of tickers \(e.g., "KXBTC-24DEC31,INX-25JAN03"\) | +| `mveFilter` | string | No | Multivariate event filter: "only" or "exclude" | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -160,6 +169,8 @@ Retrieve a list of events from Kalshi with optional filtering (V2 - exact API re | `status` | string | No | Filter by event status: "open", "closed", or "settled" | | `seriesTicker` | string | No | Filter by series ticker \(e.g., "KXBTC", "INX", "FED-RATE"\) | | `withNestedMarkets` | string | No | Include nested markets in response: "true" or "false" | +| `withMilestones` | string | No | Include milestones in response: "true" or "false" | +| `minCloseTs` | number | No | Minimum close timestamp in Unix seconds \(e.g., 1704067200\) | | `limit` | string | No | Number of results to return \(1-200, default: 200\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -249,6 +260,8 @@ Retrieve your open positions from Kalshi (V2 - exact API response) | `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) | | `ticker` | string | No | Filter by market ticker \(e.g., "KXBTC-24DEC31"\) | | `eventTicker` | string | No | Filter by event ticker, max 10 comma-separated \(e.g., "KXBTC-24DEC31,INX-25JAN03"\) | +| `countFilter` | string | No | Restrict to positions with non-zero values for the given fields \(comma-separated\): "position", "total_traded" | +| `subaccount` | string | No | Subaccount identifier to get positions for | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -287,6 +300,9 @@ Retrieve your orders from Kalshi with optional filtering (V2 with full API respo | `ticker` | string | No | Filter by market ticker \(e.g., "KXBTC-24DEC31"\) | | `eventTicker` | string | No | Filter by event ticker, max 10 comma-separated \(e.g., "KXBTC-24DEC31,INX-25JAN03"\) | | `status` | string | No | Filter by order status: "resting", "canceled", or "executed" | +| `minTs` | string | No | Minimum timestamp filter \(Unix timestamp, e.g., "1704067200"\) | +| `maxTs` | string | No | Maximum timestamp filter \(Unix timestamp, e.g., "1704153600"\) | +| `subaccount` | string | No | Subaccount identifier to filter orders | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -375,6 +391,7 @@ Retrieve the orderbook (yes and no bids) for a specific market (V2 - includes de | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `ticker` | string | Yes | Market ticker identifier \(e.g., "KXBTC-24DEC31", "INX-25JAN03-T4485.99"\) | +| `depth` | number | No | Number of price levels to return \(e.g., 10, 20\). Default: all levels | #### Output @@ -397,6 +414,9 @@ Retrieve recent trades with additional filtering options (V2 - includes trade_id | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `ticker` | string | No | Filter by market ticker \(e.g., "KXBTC-24DEC31"\) | +| `minTs` | number | No | Minimum timestamp in Unix seconds \(e.g., 1704067200\) | +| `maxTs` | number | No | Maximum timestamp in Unix seconds \(e.g., 1704153600\) | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -458,7 +478,7 @@ Retrieve OHLC candlestick data aggregated across all markets in an event (V2 - f ### `kalshi_get_fills` -Retrieve your portfolio +Retrieve your portfolio's fills/trades from Kalshi (V2 - exact API response) #### Input @@ -470,6 +490,7 @@ Retrieve your portfolio | `orderId` | string | No | Filter by order ID \(e.g., "abc123-def456-ghi789"\) | | `minTs` | number | No | Minimum timestamp in Unix seconds \(e.g., 1704067200\) | | `maxTs` | number | No | Maximum timestamp in Unix seconds \(e.g., 1704153600\) | +| `subaccount` | string | No | Subaccount identifier to get fills for | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -504,6 +525,7 @@ Retrieve your portfolio settlement history from Kalshi (V2 - exact API response) | `eventTicker` | string | No | Filter by event ticker \(e.g., "KXBTC-24DEC31"\) | | `minTs` | number | No | Minimum settled timestamp in Unix seconds \(e.g., 1704067200\) | | `maxTs` | number | No | Maximum settled timestamp in Unix seconds \(e.g., 1704153600\) | +| `subaccount` | string | No | Subaccount number \(0 for primary, 1-63 for subaccounts\) | | `limit` | string | No | Number of results to return \(1-1000, default: 100\) | | `cursor` | string | No | Pagination cursor from previous response for fetching next page | @@ -534,6 +556,7 @@ Retrieve details of a specific market series by ticker (V2 - exact API response) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `seriesTicker` | string | Yes | Series ticker identifier \(e.g., "KXBTC", "INX", "FED-RATE"\) | +| `includeVolume` | string | No | Include volume data in response \(true/false\) | #### Output @@ -565,6 +588,9 @@ Retrieve a list of market series from Kalshi with optional filtering (V2 - exact | --------- | ---- | -------- | ----------- | | `category` | string | No | Filter by category \(e.g., "Economics", "Politics", "Crypto"\) | | `tags` | string | No | Filter by comma-separated tags | +| `includeProductMetadata` | string | No | Include product metadata in response \(true/false\) | +| `includeVolume` | string | No | Include volume data in response \(true/false\) | +| `minUpdatedTs` | number | No | Minimum updated timestamp in Unix seconds \(e.g., 1704067200\) | #### Output @@ -644,7 +670,7 @@ Create a new order on a Kalshi prediction market (V2 with full API response) | `ticker` | string | Yes | Market ticker identifier \(e.g., "KXBTC-24DEC31", "INX-25JAN03-T4485.99"\) | | `side` | string | Yes | Side of the order: "yes" or "no" | | `action` | string | Yes | Action type: "buy" or "sell" | -| `count` | string | Yes | Number of contracts to trade \(e.g., "10", "100"\) | +| `count` | string | No | Number of contracts to trade \(e.g., "10", "100"\). Provide count or countFp | | `type` | string | No | Order type: "limit" or "market" \(default: "limit"\) | | `yesPrice` | string | No | Yes price in cents \(1-99\) | | `noPrice` | string | No | No price in cents \(1-99\) | @@ -658,6 +684,9 @@ Create a new order on a Kalshi prediction market (V2 with full API response) | `reduceOnly` | string | No | Set to 'true' for position reduction only | | `selfTradePreventionType` | string | No | Self-trade prevention: 'taker_at_cross' or 'maker' | | `orderGroupId` | string | No | Associated order group ID | +| `countFp` | string | No | Count in fixed-point for fractional contracts | +| `cancelOrderOnPause` | string | No | Set to 'true' to cancel order on market pause | +| `subaccount` | string | No | Subaccount to use for the order | #### Output @@ -772,6 +801,7 @@ Modify the price or quantity of an existing order on Kalshi (V2 with full API re | `noPrice` | string | No | Updated no price in cents \(1-99\) | | `yesPriceDollars` | string | No | Updated yes price in dollars \(e.g., "0.56"\) | | `noPriceDollars` | string | No | Updated no price in dollars \(e.g., "0.56"\) | +| `countFp` | string | No | Count in fixed-point for fractional contracts | #### Output diff --git a/apps/docs/content/docs/en/integrations/leadmagic.mdx b/apps/docs/content/docs/en/integrations/leadmagic.mdx new file mode 100644 index 0000000000..1eb4fb3952 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/leadmagic.mdx @@ -0,0 +1,275 @@ +--- +title: LeadMagic +description: Find and enrich B2B contacts, emails, mobile numbers, and company data +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[LeadMagic](https://leadmagic.io/) is a B2B contact and company data platform for finding and enriching emails, direct mobile numbers, LinkedIn profiles, and firmographics from minimal input. + +With LeadMagic, you can: + +- **Find and validate work emails:** Resolve verified work emails by name or company, and check the deliverability of an existing address. +- **Find direct mobile numbers:** Look up mobile phone numbers for a contact. +- **Enrich and cross-reference profiles:** Enrich a LinkedIn profile, reverse-lookup a profile from an email, or find the email behind a profile. +- **Research accounts:** Search companies by domain and identify the people holding a given role at an account. +- **Check your credit balance:** Monitor remaining LeadMagic credits before running large jobs. + +In Sim, the LeadMagic integration lets your agents find, verify, and enrich B2B contacts and companies inside a workflow — automating lead generation, account research, and CRM enrichment without leaving Sim. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate LeadMagic to find verified work emails by name or company, validate email deliverability, find direct mobile numbers, enrich LinkedIn profiles, reverse-lookup profiles from emails, search companies by domain, identify role holders at accounts, and check account credit balance. + + + +## Actions + +### `leadmagic_validate_email` + +Verify an email address for deliverability. Charges 0.25 credits for definitive SMTP results (valid/invalid); unknown and RFC-invalid results are free. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `email` | string | Yes | Email address to validate \(e.g., john@example.com\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The validated email address | +| `email_status` | string | Validation result: valid, invalid, or unknown | +| `is_domain_catch_all` | boolean | Whether the domain accepts all emails \(catch-all\) | +| `credits_consumed` | number | Credits charged for this request \(0.25 for definitive results\) | +| `message` | string | Human-readable status message | +| `mx_record` | string | MX record for the domain | +| `mx_provider` | string | Email provider \(e.g., Google, Microsoft\) | +| `mx_gateway` | string | MX gateway for the domain | +| `mx_security_gateway` | boolean | Whether the domain uses a security gateway | +| `company_name` | string | Company name associated with the email domain | +| `company_industry` | string | Industry of the company | +| `company_size` | string | Company size range | + +### `leadmagic_find_email` + +Find someone's verified work email from their name and company domain. Charges 1 credit when a valid email is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `first_name` | string | No | Person's first name \(use with last_name, or use full_name instead\) | +| `last_name` | string | No | Person's last name \(use with first_name, or use full_name instead\) | +| `full_name` | string | No | Person's full name \(alternative to first_name + last_name\) | +| `domain` | string | No | Company domain \(preferred, e.g. stripe.com\) | +| `company_name` | string | No | Company name \(fallback if domain is unavailable\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | Found work email address | +| `status` | string | Result status \(valid, invalid, etc.\) | +| `credits_consumed` | number | Credits charged \(1 when email found\) | +| `message` | string | Human-readable status message | +| `employment_verified` | boolean | Whether employment at the company was verified | +| `has_mx` | boolean | Whether the domain has a valid MX record | +| `mx_record` | string | MX record for the email domain | +| `mx_provider` | string | Email provider | +| `company_name` | string | Company name | +| `company_industry` | string | Company industry | +| `company_size` | string | Company size range | +| `company_profile_url` | string | Company LinkedIn/B2B profile URL | + +### `leadmagic_find_mobile` + +Find a person's direct mobile number from their LinkedIn profile URL or email. Charges 5 credits when a number is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `profile_url` | string | No | LinkedIn profile URL \(provide at least one identifier\) | +| `work_email` | string | No | Work email address \(provide at least one identifier\) | +| `personal_email` | string | No | Personal email address \(provide at least one identifier\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `profile_url` | string | LinkedIn profile URL used for lookup | +| `email` | string | Email address associated with the profile | +| `mobile_number` | string | Direct mobile phone number | +| `credits_consumed` | number | Credits charged \(5 when mobile found\) | +| `message` | string | Status message from the API | + +### `leadmagic_profile_search` + +Enrich a LinkedIn profile with work history, education, skills, and contact data. Charges 1 credit per successful enrichment; free when profile not found. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `profile_url` | string | Yes | LinkedIn profile URL or username \(e.g., https://linkedin.com/in/johndoe\) | +| `extended_response` | boolean | No | Include additional profile image URL in the response \(default: false\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `profile_url` | string | LinkedIn profile URL | +| `first_name` | string | First name | +| `last_name` | string | Last name | +| `full_name` | string | Full name | +| `professional_title` | string | Current job title | +| `bio` | string | Profile bio / summary | +| `location` | string | Location string | +| `country` | string | Country | +| `followers_range` | string | LinkedIn follower range | +| `company_name` | string | Current employer | +| `company_industry` | string | Industry of current employer | +| `company_website` | string | Company website | +| `total_tenure_years` | string | Total professional tenure in years | +| `total_tenure_months` | string | Total professional tenure in months | +| `work_experience` | array | Work history entries | +| `education` | array | Education history entries | +| `certifications` | array | Professional certifications | +| `credits_consumed` | number | Credits charged \(1 when profile found\) | +| `message` | string | Human-readable status message | + +### `leadmagic_profile_to_email` + +Extract a verified work email from a LinkedIn profile URL. Charges 5 credits when an email is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `profile_url` | string | Yes | LinkedIn profile URL or username \(e.g., https://linkedin.com/in/johndoe\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | Work email address found for this profile | +| `profile_url` | string | LinkedIn profile URL used for lookup | +| `credits_consumed` | number | Credits charged \(5 when email found\) | +| `message` | string | Human-readable status message | + +### `leadmagic_email_to_profile` + +Retrieve a LinkedIn profile URL from a work or personal email address. Charges 10 credits when a profile is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `work_email` | string | No | Work email address \(provide at least one of work_email or personal_email\) | +| `personal_email` | string | No | Personal email address \(provide at least one of work_email or personal_email\) | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `profile_url` | string | LinkedIn profile URL for the provided email | +| `credits_consumed` | number | Credits charged \(10 when profile found\) | +| `message` | string | Human-readable status message | + +### `leadmagic_company_search` + +Enrich company data including firmographics, headcount, funding, and social profiles by domain, LinkedIn URL, or name. Charges 1 credit when a company is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `company_domain` | string | No | Company website domain \(e.g., stripe.com\). Provide at least one identifier. | +| `profile_url` | string | No | LinkedIn company profile URL \(e.g., https://linkedin.com/company/stripe\). Provide at least one identifier. | +| `company_name` | string | No | Company name \(fallback if domain/URL unavailable\). Provide at least one identifier. | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `companyName` | string | Company name | +| `companyId` | number | Internal company identifier | +| `industry` | string | Industry classification | +| `employeeCount` | number | Number of employees | +| `employeeRange` | string | Headcount range \(e.g., 1001-5000\) | +| `founded` | number | Year the company was founded | +| `headquarters` | json | Headquarters location object | +| `revenue` | string | Revenue range | +| `funding` | string | Total funding amount | +| `description` | string | Company description | +| `specialties` | array | Company specialties and focus areas | +| `competitors` | array | Competitor companies | +| `followerCount` | number | LinkedIn follower count | +| `twitter_url` | string | Twitter/X profile URL | +| `facebook_url` | string | Facebook page URL | +| `b2b_profile_url` | string | LinkedIn company profile URL | +| `logo_url` | string | Company logo URL | +| `credits_consumed` | number | Credits charged \(1 when company found\) | +| `message` | string | Human-readable status message | + +### `leadmagic_role_finder` + +Find the person holding a specific job role at a company. Charges 2 credits when a matching person is found; free when no result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `job_title` | string | Yes | Job role to search for \(e.g., Head of Sales, CTO\). Supports partial matching. | +| `company_domain` | string | No | Company website domain \(e.g., stripe.com\). Provide domain or company_name. | +| `company_name` | string | No | Company name \(fallback if domain unavailable\). Provide domain or company_name. | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `first_name` | string | First name of the person found | +| `last_name` | string | Last name of the person found | +| `full_name` | string | Full name of the person found | +| `profile_url` | string | LinkedIn profile URL | +| `job_title` | string | Verified job title at the company | +| `company_name` | string | Company name | +| `company_website` | string | Company website | +| `credits_consumed` | number | Credits charged \(2 when person found\) | +| `message` | string | Human-readable status message | + +### `leadmagic_get_credits` + +Retrieve the current credit balance for the authenticated LeadMagic account. This endpoint is free and consumes no credits. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | LeadMagic API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `credits` | number | Current credit balance | + + diff --git a/apps/docs/content/docs/en/integrations/luma.mdx b/apps/docs/content/docs/en/integrations/luma.mdx index 3abc9db2a2..ab3f8269b4 100644 --- a/apps/docs/content/docs/en/integrations/luma.mdx +++ b/apps/docs/content/docs/en/integrations/luma.mdx @@ -295,7 +295,7 @@ Retrieve the guest list for a Luma event with optional filtering by approval sta ### `luma_get_guest` -Retrieve a single guest +Retrieve a single guest's details on a Luma event, including approval status, registration timestamps, and contact info. #### Input @@ -361,7 +361,7 @@ Send email invitations to guests for a Luma event. Unlike Add Guests (which regi ### `luma_update_guest_status` -Update a guest +Update a guest's approval status on a Luma event — approve, decline, waitlist, or set to pending. Identify the guest by email or guest ID. #### Input diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index ab898490da..5e08cf704b 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -42,12 +42,14 @@ "dagster", "databricks", "datadog", + "datagma", "daytona", "deployments", "devin", "discord", "docusign", "dropbox", + "dropcontact", "dspy", "dub", "duckduckgo", @@ -57,6 +59,7 @@ "emailbison", "enrich", "enrichment", + "enrow", "evernote", "exa", "extend", @@ -100,6 +103,7 @@ "huggingface", "hunter", "iam", + "icypeas", "identity_center", "imap", "incidentio", @@ -115,6 +119,7 @@ "langsmith", "latex", "launchdarkly", + "leadmagic", "lemlist", "linear", "linkedin", diff --git a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx index 2bd43e7951..61b1158144 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx @@ -46,7 +46,8 @@ Read data from a specific sheet in a Microsoft Excel spreadsheet | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to read from \(e.g., "01ABC123DEF456"\) | | `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | -| `range` | string | No | The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet. | +| `sheetName` | string | Yes | The name of the sheet/tab to read from \(e.g., "Sheet1", "Sales Data"\) | +| `cellRange` | string | No | The cell range to read \(e.g., "A1:D10"\). If not specified, reads the entire used range. | #### Output @@ -69,8 +70,9 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to write to \(e.g., "01ABC123DEF456"\) | | `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | -| `range` | string | No | The range of cells to write to \(e.g., "Sheet1!A1:B2"\) | -| `values` | array | Yes | The data to write as a 2D array \(e.g., \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects | +| `sheetName` | string | Yes | The name of the sheet/tab to write to \(e.g., "Sheet1", "Sales Data"\) | +| `cellRange` | string | No | The cell range to write to \(e.g., "A1:D10", "A1"\). Defaults to "A1" if not specified. | +| `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | | `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | diff --git a/apps/docs/content/docs/en/integrations/mistral_parse.mdx b/apps/docs/content/docs/en/integrations/mistral_parse.mdx index 47c16ccc71..1a35502275 100644 --- a/apps/docs/content/docs/en/integrations/mistral_parse.mdx +++ b/apps/docs/content/docs/en/integrations/mistral_parse.mdx @@ -39,15 +39,7 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | No | URL to a PDF document to be processed | -| `file` | file | No | Document file to be processed | -| `fileUpload` | object | No | File upload data from file-upload component | -| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. | -| `includeImageBase64` | boolean | No | Include base64-encoded images in the response | -| `pages` | array | No | Specific pages to process \(array of page numbers, starting from 0\) | -| `imageLimit` | number | No | Maximum number of images to extract from the PDF | -| `imageMinSize` | number | No | Minimum height and width of images to extract from the PDF | -| `apiKey` | string | Yes | Mistral API key \(MISTRAL_API_KEY\) | +| `file` | file | Yes | Normalized UserFile from file upload or file reference | #### Output diff --git a/apps/docs/content/docs/en/integrations/neo4j.mdx b/apps/docs/content/docs/en/integrations/neo4j.mdx index a8d490b902..59cacf859e 100644 --- a/apps/docs/content/docs/en/integrations/neo4j.mdx +++ b/apps/docs/content/docs/en/integrations/neo4j.mdx @@ -36,7 +36,7 @@ Integrate Neo4j graph database into the workflow. Can query, create, merge, upda ### `neo4j_query` -Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g., +Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g., "MATCH (n:User) RETURN n LIMIT 100") or use LIMIT $limit with a limit parameter. #### Input diff --git a/apps/docs/content/docs/en/integrations/openai.mdx b/apps/docs/content/docs/en/integrations/openai.mdx index 251092e8ff..e9072e6cdc 100644 --- a/apps/docs/content/docs/en/integrations/openai.mdx +++ b/apps/docs/content/docs/en/integrations/openai.mdx @@ -37,7 +37,7 @@ Integrate Embeddings into the workflow. Can generate embeddings from text. ### `openai_embeddings` -Generate embeddings from text using OpenAI +Generate embeddings from text using OpenAI's embedding models #### Input diff --git a/apps/docs/content/docs/en/integrations/perplexity.mdx b/apps/docs/content/docs/en/integrations/perplexity.mdx index 3f63040c0c..fd8f9f2d0e 100644 --- a/apps/docs/content/docs/en/integrations/perplexity.mdx +++ b/apps/docs/content/docs/en/integrations/perplexity.mdx @@ -65,7 +65,7 @@ Generate completions using Perplexity AI chat models ### `perplexity_search` -Get ranked search results from Perplexity +Get ranked search results from Perplexity's continuously refreshed index with advanced filtering and customization options #### Input diff --git a/apps/docs/content/docs/en/integrations/pinecone.mdx b/apps/docs/content/docs/en/integrations/pinecone.mdx index 2c9cc03107..c7fab08fc7 100644 --- a/apps/docs/content/docs/en/integrations/pinecone.mdx +++ b/apps/docs/content/docs/en/integrations/pinecone.mdx @@ -37,7 +37,7 @@ Integrate Pinecone into the workflow. Can generate embeddings, upsert text, sear ### `pinecone_generate_embeddings` -Generate embeddings from text using Pinecone +Generate embeddings from text using Pinecone's hosted models #### Input diff --git a/apps/docs/content/docs/en/integrations/posthog.mdx b/apps/docs/content/docs/en/integrations/posthog.mdx index 4cb5f25de0..7b87e552fd 100644 --- a/apps/docs/content/docs/en/integrations/posthog.mdx +++ b/apps/docs/content/docs/en/integrations/posthog.mdx @@ -152,7 +152,7 @@ Delete a person from PostHog. This will remove all associated events and data. U ### `posthog_query` -Execute a HogQL query in PostHog. HogQL is PostHog +Execute a HogQL query in PostHog. HogQL is PostHog's SQL-like query language for analytics. Use this for advanced data retrieval and analysis. #### Input diff --git a/apps/docs/content/docs/en/integrations/pulse.mdx b/apps/docs/content/docs/en/integrations/pulse.mdx index f7c3eaec6d..6d13e325d0 100644 --- a/apps/docs/content/docs/en/integrations/pulse.mdx +++ b/apps/docs/content/docs/en/integrations/pulse.mdx @@ -43,16 +43,7 @@ Integrate Pulse into the workflow. Extract text from PDF documents, images, and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | No | URL to a document to be processed | -| `file` | file | No | Document file to be processed | -| `fileUpload` | object | No | File upload data from file-upload component | -| `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) | -| `extractFigure` | boolean | No | Enable figure extraction from the document | -| `figureDescription` | boolean | No | Generate descriptions/captions for extracted figures | -| `returnHtml` | boolean | No | Include HTML in the response | -| `chunking` | string | No | Chunking strategies \(comma-separated: semantic, header, page, recursive\) | -| `chunkSize` | number | No | Maximum characters per chunk when chunking is enabled | -| `apiKey` | string | Yes | Pulse API key | +| `file` | file | Yes | Document to be processed | #### Output diff --git a/apps/docs/content/docs/en/integrations/rb2b.mdx b/apps/docs/content/docs/en/integrations/rb2b.mdx index 74395f0ee1..45096416cf 100644 --- a/apps/docs/content/docs/en/integrations/rb2b.mdx +++ b/apps/docs/content/docs/en/integrations/rb2b.mdx @@ -20,244 +20,110 @@ Resolve IP addresses, hashed emails, and LinkedIn profiles into person-level ide ### `rb2b_credit_check` +Check the number of API credits remaining on your RB2B account. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `credits_remaining` | number | Number of API credits remaining on the account | ### `rb2b_ip_to_hem` +Convert an IP address (and optional user agent) into hashed email addresses (HEM) with accuracy scores. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | +| `user_agent` | string | No | Optional user agent string to improve match accuracy | +| `include_sha256` | boolean | No | Whether to include SHA-256 hashes in addition to MD5 hashes | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `results` | array | Up to 3 hashed email matches for the IP address | +| ↳ `md5` | string | MD5 hash of the matched email | +| ↳ `sha256` | string | SHA-256 hash of the matched email \(only when include_sha256 is true\) | +| ↳ `score` | number | Match accuracy score \(0 = probabilistic, 1 = deterministic\) | ### `rb2b_ip_to_maid` +Resolve an IP address (and optional user agent) into mobile advertising identifiers (MAIDs) observed over the last 60 days. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | +| `user_agent` | string | No | Optional user agent string to improve match accuracy | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `results` | array | Mobile advertising identifiers observed for the IP address | +| ↳ `device_id` | string | The mobile advertising identifier | +| ↳ `device_type` | string | The identifier type \(e.g. AAID, IDFA\) | ### `rb2b_ip_to_company` +Identify the company domains associated with an IP address, ranked by confidence. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `ip_address` | string | Yes | The IP address to resolve \(IPv4 or IPv6\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `results` | array | Company domain matches for the IP address | +| ↳ `domain` | string | Company domain associated with the IP | +| ↳ `percentage` | string | Confidence percentage for the match | ### `rb2b_hem_to_business_profile` +Return a full business profile (name, title, company, industry, seniority and more) for an email address or MD5-hashed email. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | +| `first_name` | string | First name | +| `last_name` | string | Last name | | `title` | string | Job title | | `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | +| `linkedinurl` | string | Personal LinkedIn profile URL | +| `link_email` | string | Linked business email address | +| `work_email_confirmed` | string | Whether the work email is confirmed | +| `personal_emails` | array | Associated personal emails \(hashed or plaintext depending on input\) | | `current_company` | string | Current company name | | `current_company_url` | string | Current company website | | `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | +| `current_industry` | string | Current industry | +| `functional_area` | string | Functional area | +| `country` | string | Country | | `company_employee_count` | string | Company employee count | | `company_employee_range` | string | Company employee range band | | `company_revenue_range` | string | Company revenue range band | @@ -265,492 +131,201 @@ Resolve IP addresses, hashed emails, and LinkedIn profiles into person-level ide ### `rb2b_hem_to_best_linkedin` +Return the most recently active LinkedIn profile URL for an email address or MD5-hashed email. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `linkedin_url` | string | Best LinkedIn profile URL for the email | ### `rb2b_hem_to_linkedin` +Return the LinkedIn slug (the profile identifier portion of the URL) for an email address or MD5-hashed email. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `linkedin_slug` | string | LinkedIn slug for the email | ### `rb2b_hem_to_maid` +Return up to five mobile advertising identifiers (MAIDs) associated with an email address or MD5-hashed email. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `email` | string | Yes | A plaintext email address or an MD5 hash of the email | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `results` | array | Mobile advertising identifiers associated with the email | +| ↳ `device_id` | string | The mobile advertising identifier | +| ↳ `device_type` | string | The identifier type \(e.g. AAID, IDFA\) | ### `rb2b_email_to_activity` +Return the last known active date for an email address. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `email` | string | Yes | The email address to look up | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | +| `results` | array | Activity records for the email | +| ↳ `email` | string | The email address | +| ↳ `last_active` | string | Date the email was last seen active \(YYYY-MM-DD\) | +| `match_count` | number | Number of matches found | +| `credits_charged` | number | Credits charged for this request | | `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | ### `rb2b_linkedin_to_business_profile` +Return a full business profile (name, title, company, emails and more) for a LinkedIn profile. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | +| `first_name` | string | First name | +| `last_name` | string | Last name | +| `full_name` | string | Full name | | `headline` | string | LinkedIn headline | | `title` | string | Job title | | `seniority` | string | Seniority level | | `country` | string | Country | | `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | +| `functional_area` | array | Functional areas | +| `linkedin_url` | string | Personal LinkedIn profile URL | | `business_email` | string | Business email address | | `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `company` | object | Current company details | +| ↳ `name` | string | Company name | +| ↳ `industry` | string | Company industry | +| ↳ `website_url` | string | Company website URL | +| ↳ `linkedin_url` | string | Company LinkedIn URL | ### `rb2b_linkedin_to_best_personal_email` +Return the personal email with the most recent known network activity for a LinkedIn profile. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `email` | string | Best personal email for the LinkedIn profile | ### `rb2b_linkedin_to_personal_email` +Return the personal email addresses associated with a LinkedIn profile. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `emails` | array | Personal email addresses for the LinkedIn profile | ### `rb2b_linkedin_to_hashed_emails` +Return the business and personal hashed emails (MD5 and SHA-256) associated with a LinkedIn profile. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | +| `linkedin_slug` | string | The LinkedIn slug | | `business_md5_array` | array | MD5 hashes of business emails | | `business_sha256_array` | array | SHA-256 hashes of business emails | | `personal_md5_array` | array | MD5 hashes of personal emails | | `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | ### `rb2b_linkedin_to_mobile_phone` +Return the mobile phone number with the most recent known network activity for a LinkedIn profile. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `linkedin_slug` | string | Yes | The LinkedIn profile slug or URL | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `mobile_phone` | string | Mobile phone number for the LinkedIn profile | ### `rb2b_linkedin_slug_search` +Find a LinkedIn profile URL from a first name, last name, and company domain. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | RB2B API key | +| `first_name` | string | Yes | The person’s first name | +| `last_name` | string | Yes | The person’s last name | +| `company_domain` | string | Yes | The company domain \(e.g. example.com\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `credits_remaining` | number | API credits remaining \(Credit Check\) | -| `results` | json | Result list for IP/MAID/company/activity lookups | -| `match_count` | number | Number of matches \(Email to Last Active Date\) | -| `credits_charged` | number | Credits charged for the request | -| `credits_exhausted` | boolean | Whether the account is out of credits | -| `linkedin_url` | string | LinkedIn profile URL | -| `linkedin_slug` | string | LinkedIn slug | -| `email` | string | Best personal email | -| `emails` | array | Personal email addresses | -| `mobile_phone` | string | Mobile phone number | -| `business_md5_array` | array | MD5 hashes of business emails | -| `business_sha256_array` | array | SHA-256 hashes of business emails | -| `personal_md5_array` | array | MD5 hashes of personal emails | -| `personal_sha256_array` | array | SHA-256 hashes of personal emails | -| `first_name` | string | First name \(business profile\) | -| `last_name` | string | Last name \(business profile\) | -| `full_name` | string | Full name \(LinkedIn business profile\) | -| `headline` | string | LinkedIn headline | -| `title` | string | Job title | -| `seniority` | string | Seniority level | -| `country` | string | Country | -| `current_industry` | string | Current industry | -| `functional_area` | json | Functional area\(s\) | -| `current_company` | string | Current company name | -| `current_company_url` | string | Current company website | -| `current_company_linkedinurl` | string | Current company LinkedIn URL | -| `company` | json | Company details \(LinkedIn business profile\) | -| `business_email` | string | Business email address | -| `personal_email` | string | Personal email address | -| `personal_emails` | array | Personal emails \(business profile\) | -| `linkedinurl` | string | LinkedIn profile URL \(business profile\) | -| `link_email` | string | Linked business email \(business profile\) | -| `work_email_confirmed` | string | Whether the work email is confirmed | -| `company_employee_count` | string | Company employee count | -| `company_employee_range` | string | Company employee range band | -| `company_revenue_range` | string | Company revenue range band | -| `md5` | string | MD5 hash of the resolved email | +| `linkedin_url` | string | LinkedIn profile URL for the person | diff --git a/apps/docs/content/docs/en/integrations/reddit.mdx b/apps/docs/content/docs/en/integrations/reddit.mdx index 42540dbd32..d5b5dfa6b7 100644 --- a/apps/docs/content/docs/en/integrations/reddit.mdx +++ b/apps/docs/content/docs/en/integrations/reddit.mdx @@ -265,38 +265,20 @@ Save a Reddit post or comment to your saved items ### `reddit_unsave` +Remove a Reddit post or comment from your saved items + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to unsave \(e.g., "t3_abc123" for post, "t1_def456" for comment\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the unsave was successful | +| `message` | string | Success or error message | ### `reddit_reply` @@ -814,146 +796,76 @@ Hide one or more Reddit posts from your listings ### `reddit_unhide` +Unhide one or more previously hidden Reddit posts + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Comma-separated list of post fullnames to unhide \(e.g., "t3_abc123,t3_def456"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the unhide was successful | +| `message` | string | Success or error message | ### `reddit_marknsfw` +Mark a Reddit post as NSFW (not safe for work) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Post fullname to mark as NSFW \(e.g., "t3_abc123"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the operation was successful | +| `message` | string | Success or error message | ### `reddit_unmarknsfw` +Remove the NSFW (not safe for work) mark from a Reddit post + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Post fullname to unmark as NSFW \(e.g., "t3_abc123"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the operation was successful | +| `message` | string | Success or error message | ### `reddit_mark_read` +Mark one or more private messages as read + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Comma-separated list of message fullnames to mark read \(e.g., "t4_abc123,t4_def456"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the operation was successful | +| `message` | string | Success or error message | ### `reddit_mark_all_read` +Mark all private messages in the inbox as read + #### Input | Parameter | Type | Required | Description | @@ -963,29 +875,8 @@ Hide one or more Reddit posts from your listings | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the operation was successful | +| `message` | string | Success or error message | ### `reddit_mod_approve` @@ -1043,73 +934,37 @@ Distinguish or un-distinguish a Reddit post or comment as a moderator ### `reddit_lock` +Lock a Reddit post or comment to prevent further replies (moderator action) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to lock \(e.g., "t3_abc123" for post, "t1_def456" for comment\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the lock was successful | +| `message` | string | Success or error message | ### `reddit_unlock` +Unlock a Reddit post or comment to allow replies again (moderator action) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to unlock \(e.g., "t3_abc123" for post, "t1_def456" for comment\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | \[\{id, name, title, author, url, permalink, score, num_comments, created_utc, is_self, selftext, thumbnail, subreddit\}\] | -| `post` | json | Single post \(id, name, title, author, selftext, score, created_utc, permalink\) | -| `comments` | json | \[\{id, name, author, body, score, created_utc, permalink, replies\}\] with nested replies | -| `success` | boolean | Operation success status | -| `message` | string | Result message | -| `data` | json | Write-operation result \(id, name, url, permalink, body — varies by operation\) | -| `after` | string | Pagination cursor \(next page\) | -| `before` | string | Pagination cursor \(previous page\) | -| `id` | string | Entity ID | -| `name` | string | Entity fullname | -| `messages` | json | \[\{id, name, author, dest, subject, body, created_utc, new, was_comment, context, distinguished\}\] | -| `display_name` | string | Subreddit display name | -| `subscribers` | number | Subscriber count | -| `description` | string | Description text | -| `link_karma` | number | Link karma | -| `comment_karma` | number | Comment karma | -| `total_karma` | number | Total karma | -| `icon_img` | string | Icon image URL | -| `subreddit_type` | string | Subreddit type \(public, private, restricted\) | -| `subreddits` | json | \[\{id, name, display_name, title, public_description, subscribers, accounts_active, created_utc, over18, url, subreddit_type, icon_img\}\] | -| `rules` | json | \[\{short_name, description, description_html, violation_reason, kind, created_utc, priority\}\] | -| `site_rules` | json | Reddit site-wide rules \(string\[\]\) | +| `success` | boolean | Whether the unlock was successful | +| `message` | string | Success or error message | ### `reddit_mod_sticky` diff --git a/apps/docs/content/docs/en/integrations/redis.mdx b/apps/docs/content/docs/en/integrations/redis.mdx index 6d9c658eeb..fc09198e4d 100644 --- a/apps/docs/content/docs/en/integrations/redis.mdx +++ b/apps/docs/content/docs/en/integrations/redis.mdx @@ -111,7 +111,7 @@ List all keys matching a pattern in Redis. Avoid using on large databases in pro ### `redis_command` -Execute a raw Redis command as a JSON array (e.g. [ +Execute a raw Redis command as a JSON array (e.g. ["HSET", "key", "field", "value"]). #### Input diff --git a/apps/docs/content/docs/en/integrations/reducto.mdx b/apps/docs/content/docs/en/integrations/reducto.mdx index 076fb08c46..96679dcd8e 100644 --- a/apps/docs/content/docs/en/integrations/reducto.mdx +++ b/apps/docs/content/docs/en/integrations/reducto.mdx @@ -41,12 +41,7 @@ Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF do | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | No | URL to a PDF document to be processed | -| `file` | file | No | Document file to be processed | -| `fileUpload` | object | No | File upload data from file-upload component | -| `pages` | array | No | Specific pages to process \(1-indexed page numbers\) | -| `tableOutputFormat` | string | No | Table output format \(html or markdown\). Defaults to markdown. | -| `apiKey` | string | Yes | Reducto API key \(REDUCTO_API_KEY\) | +| `file` | file | Yes | PDF document to be processed | #### Output diff --git a/apps/docs/content/docs/en/integrations/sap_s4hana.mdx b/apps/docs/content/docs/en/integrations/sap_s4hana.mdx index 75845287e0..4acb5f8ffd 100644 --- a/apps/docs/content/docs/en/integrations/sap_s4hana.mdx +++ b/apps/docs/content/docs/en/integrations/sap_s4hana.mdx @@ -338,7 +338,7 @@ Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_ ### `sap_s4hana_update_customer` -Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like +Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like "01"), PostingIsBlocked, and DeletionIndicator (Edm.Boolean). If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input diff --git a/apps/docs/content/docs/en/integrations/slack.mdx b/apps/docs/content/docs/en/integrations/slack.mdx index 832ecec7ff..a01cafe3b3 100644 --- a/apps/docs/content/docs/en/integrations/slack.mdx +++ b/apps/docs/content/docs/en/integrations/slack.mdx @@ -1642,7 +1642,7 @@ Push a new view onto an existing modal stack in Slack. Limited to 2 additional v ### `slack_publish_view` -Publish a static view to a user +Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience. #### Input diff --git a/apps/docs/content/docs/en/integrations/table.mdx b/apps/docs/content/docs/en/integrations/table.mdx index 523d550a01..25be3d7034 100644 --- a/apps/docs/content/docs/en/integrations/table.mdx +++ b/apps/docs/content/docs/en/integrations/table.mdx @@ -62,7 +62,7 @@ Create and manage custom data tables. Store, query, and manipulate structured da ### `table_insert_row` -Insert a new row into a table. IMPORTANT: You must use the +Insert a new row into a table. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the row contents. #### Input @@ -81,6 +81,8 @@ Insert a new row into a table. IMPORTANT: You must use the ### `table_batch_insert_rows` +Insert multiple rows into a table at once (up to $\{TABLE_LIMITS.MAX_BATCH_INSERT_SIZE\} rows) + #### Input | Parameter | Type | Required | Description | @@ -99,7 +101,7 @@ Insert a new row into a table. IMPORTANT: You must use the ### `table_upsert_row` -Insert or update a row based on unique column constraints. If a row with matching unique field exists, update it; otherwise insert a new row. IMPORTANT: You must use the +Insert or update a row based on unique column constraints. If a row with matching unique field exists, update it; otherwise insert a new row. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the row contents. #### Input @@ -119,7 +121,7 @@ Insert or update a row based on unique column constraints. If a row with matchin ### `table_update_row` -Update an existing row in a table. Supports partial updates - only include the fields you want to change. IMPORTANT: You must use the +Update an existing row in a table. Supports partial updates - only include the fields you want to change. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the fields to update. #### Input diff --git a/apps/docs/content/docs/en/integrations/tavily.mdx b/apps/docs/content/docs/en/integrations/tavily.mdx index 906a755d54..bb0958d522 100644 --- a/apps/docs/content/docs/en/integrations/tavily.mdx +++ b/apps/docs/content/docs/en/integrations/tavily.mdx @@ -35,7 +35,7 @@ Integrate Tavily into the workflow. Can search the web and extract content from ### `tavily_search` -Perform AI-powered web searches using Tavily +Perform AI-powered web searches using Tavily's search API. Returns structured results with titles, URLs, snippets, and optional raw content, optimized for relevance and accuracy. #### Input @@ -81,7 +81,7 @@ Perform AI-powered web searches using Tavily ### `tavily_extract` -Extract raw content from multiple web pages simultaneously using Tavily +Extract raw content from multiple web pages simultaneously using Tavily's extraction API. Supports basic and advanced extraction depths with detailed error reporting for failed URLs. #### Input @@ -110,7 +110,7 @@ Extract raw content from multiple web pages simultaneously using Tavily ### `tavily_crawl` -Systematically crawl and extract content from websites using Tavily +Systematically crawl and extract content from websites using Tavily's crawl API. Supports depth control, path filtering, domain restrictions, and natural language instructions for targeted crawling. #### Input @@ -146,7 +146,7 @@ Systematically crawl and extract content from websites using Tavily ### `tavily_map` -Discover and visualize website structure using Tavily +Discover and visualize website structure using Tavily's map API. Maps out all accessible URLs from a base URL with depth control, path filtering, and domain restrictions. #### Input diff --git a/apps/docs/content/docs/en/integrations/textract.mdx b/apps/docs/content/docs/en/integrations/textract.mdx index de346ed146..85473fc996 100644 --- a/apps/docs/content/docs/en/integrations/textract.mdx +++ b/apps/docs/content/docs/en/integrations/textract.mdx @@ -39,15 +39,7 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessKeyId` | string | Yes | AWS Access Key ID | -| `secretAccessKey` | string | Yes | AWS Secret Access Key | -| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | -| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | -| `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). | -| `file` | file | No | Document file to be processed \(JPEG, PNG, or single-page PDF\). | -| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). | -| `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. | -| `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. | +| `file` | file | No | Document to be processed \(JPEG, PNG, or single-page PDF\). | #### Output diff --git a/apps/docs/content/docs/en/integrations/upstash.mdx b/apps/docs/content/docs/en/integrations/upstash.mdx index af14be9fe6..e35bfdfe63 100644 --- a/apps/docs/content/docs/en/integrations/upstash.mdx +++ b/apps/docs/content/docs/en/integrations/upstash.mdx @@ -115,7 +115,7 @@ List keys matching a pattern in Upstash Redis. Defaults to listing all keys (*). ### `upstash_redis_command` -Execute an arbitrary Redis command against Upstash Redis. Pass the full command as a JSON array (e.g., [ +Execute an arbitrary Redis command against Upstash Redis. Pass the full command as a JSON array (e.g., ["HSET", "myhash", "field1", "value1"]). #### Input diff --git a/apps/docs/content/docs/en/integrations/vercel.mdx b/apps/docs/content/docs/en/integrations/vercel.mdx index 572a8af562..361ee626a5 100644 --- a/apps/docs/content/docs/en/integrations/vercel.mdx +++ b/apps/docs/content/docs/en/integrations/vercel.mdx @@ -540,7 +540,7 @@ Remove a domain from a Vercel project ### `vercel_update_project_domain` -Update a project domain +Update a project domain's configuration on Vercel #### Input diff --git a/apps/docs/content/docs/en/integrations/wiza.mdx b/apps/docs/content/docs/en/integrations/wiza.mdx index 38da4cfd83..cdd73645e9 100644 --- a/apps/docs/content/docs/en/integrations/wiza.mdx +++ b/apps/docs/content/docs/en/integrations/wiza.mdx @@ -72,7 +72,7 @@ Integrates Wiza into the workflow. Search prospects, enrich companies, reveal ve ### `wiza_prospect_search` -Search Wiza +Search Wiza's database of prospects using person, company, and financial filters #### Input diff --git a/apps/docs/content/docs/en/integrations/x.mdx b/apps/docs/content/docs/en/integrations/x.mdx index b4a22a8654..2d4ce91827 100644 --- a/apps/docs/content/docs/en/integrations/x.mdx +++ b/apps/docs/content/docs/en/integrations/x.mdx @@ -555,7 +555,7 @@ Bookmark a tweet for the authenticated user ### `x_delete_bookmark` -Remove a tweet from the authenticated user +Remove a tweet from the authenticated user's bookmarks #### Input @@ -572,7 +572,7 @@ Remove a tweet from the authenticated user ### `x_get_me` -Get the authenticated user +Get the authenticated user's profile information #### Input diff --git a/apps/sim/blocks/blocks/google_forms.ts b/apps/sim/blocks/blocks/google_forms.ts index eb8b0c51d8..66ac9e760b 100644 --- a/apps/sim/blocks/blocks/google_forms.ts +++ b/apps/sim/blocks/blocks/google_forms.ts @@ -103,9 +103,32 @@ export const GoogleFormsBlock: BlockConfig = { id: 'pageSize', title: 'Page Size', type: 'short-input', + mode: 'advanced', placeholder: 'Max responses to retrieve (default 5000)', condition: { field: 'operation', value: 'get_responses' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + mode: 'advanced', + placeholder: 'Token from a previous response for the next page', + condition: { field: 'operation', value: 'get_responses' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'short-input', + mode: 'advanced', + placeholder: 'timestamp > 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: 'get_responses' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a Google Forms responses filter expression based on the user\'s description. Only timestamp filters are supported, in the form "timestamp > N" or "timestamp >= N" where N is an RFC3339 UTC datetime (e.g. 2024-01-01T00:00:00Z). Return ONLY the filter expression - no explanations, no extra text.', + placeholder: 'Describe the time range to filter responses by...', + }, + }, // Create Form specific fields { id: 'title', @@ -249,6 +272,8 @@ Example for "Add a required multiple choice question about favorite color": formId, // Canonical param from formSelector (basic) or manualFormId (advanced) responseId, pageSize, + pageToken, + filter, title, documentTitle, unpublished, @@ -272,6 +297,8 @@ Example for "Add a required multiple choice question about favorite color": formId: effectiveFormId, responseId: responseId ? String(responseId).trim() : undefined, pageSize: pageSize ? Number(pageSize) : undefined, + pageToken: pageToken ? String(pageToken).trim() : undefined, + filter: filter ? String(filter).trim() : undefined, } case 'get_form': case 'list_watches': @@ -324,6 +351,8 @@ Example for "Add a required multiple choice question about favorite color": formId: { type: 'string', description: 'Google Form ID' }, responseId: { type: 'string', description: 'Specific response ID' }, pageSize: { type: 'string', description: 'Max responses to retrieve' }, + pageToken: { type: 'string', description: 'Page token for the next page of responses' }, + filter: { type: 'string', description: 'Timestamp filter for responses' }, title: { type: 'string', description: 'Form title for creation' }, documentTitle: { type: 'string', description: 'Document title in Drive' }, unpublished: { type: 'boolean', description: 'Create as unpublished' }, @@ -345,6 +374,15 @@ Example for "Add a required multiple choice question about favorite color": and: { field: 'responseId', value: ['', undefined, null] }, }, }, + nextPageToken: { + type: 'string', + description: 'Token to fetch the next page of responses', + condition: { + field: 'operation', + value: 'get_responses', + and: { field: 'responseId', value: ['', undefined, null] }, + }, + }, response: { type: 'json', description: 'Single form response', diff --git a/apps/sim/blocks/blocks/google_pagespeed.ts b/apps/sim/blocks/blocks/google_pagespeed.ts index d0df44162f..ab1e3ff58e 100644 --- a/apps/sim/blocks/blocks/google_pagespeed.ts +++ b/apps/sim/blocks/blocks/google_pagespeed.ts @@ -189,6 +189,16 @@ export const GooglePagespeedBlockMeta = { tags: ['devops', 'seo', 'monitoring'], alsoIntegrations: ['slack'], }, + { + icon: GooglePagespeedIcon, + title: 'PageSpeed competitor benchmark', + prompt: + 'Build a workflow that runs Google PageSpeed Insights against my homepage and a list of competitor URLs on mobile, compares the performance scores and Core Web Vitals side by side, and writes the ranked benchmark to a sheet.', + modules: ['agent', 'workflows'], + category: 'marketing', + tags: ['marketing', 'analysis'], + alsoIntegrations: ['google_sheets'], + }, ], skills: [ { diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 04b349d7c4..f290946186 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -45,12 +45,14 @@ import { DagsterIcon, DatabricksIcon, DatadogIcon, + DatagmaIcon, DaytonaIcon, DevinIcon, DiscordIcon, DocumentIcon, DocuSignIcon, DropboxIcon, + DropcontactIcon, DsPyIcon, DubIcon, DuckDuckGoIcon, @@ -60,6 +62,7 @@ import { EmailBisonIcon, EnrichmentIcon, EnrichSoIcon, + EnrowIcon, EvernoteIcon, ExaAIIcon, ExtendIcon, @@ -100,6 +103,7 @@ import { HuggingFaceIcon, HunterIOIcon, IAMIcon, + IcypeasIcon, IdentityCenterIcon, IncidentioIcon, InfisicalIcon, @@ -113,6 +117,7 @@ import { LangsmithIcon, LatexIcon, LaunchDarklyIcon, + LeadMagicIcon, LemlistIcon, LinearIcon, LinkedInIcon, @@ -269,11 +274,13 @@ export const blockTypeToIconMap: Record = { dagster: DagsterIcon, databricks: DatabricksIcon, datadog: DatadogIcon, + datagma: DatagmaIcon, daytona: DaytonaIcon, devin: DevinIcon, discord: DiscordIcon, docusign: DocuSignIcon, dropbox: DropboxIcon, + dropcontact: DropcontactIcon, dspy: DsPyIcon, dub: DubIcon, duckduckgo: DuckDuckGoIcon, @@ -283,6 +290,7 @@ export const blockTypeToIconMap: Record = { emailbison: EmailBisonIcon, enrich: EnrichSoIcon, enrichment: EnrichmentIcon, + enrow: EnrowIcon, evernote: EvernoteIcon, exa: ExaAIIcon, extend_v2: ExtendIcon, @@ -324,6 +332,7 @@ export const blockTypeToIconMap: Record = { huggingface: HuggingFaceIcon, hunter: HunterIOIcon, iam: IAMIcon, + icypeas: IcypeasIcon, identity_center: IdentityCenterIcon, imap: MailServerIcon, incidentio: IncidentioIcon, @@ -339,6 +348,7 @@ export const blockTypeToIconMap: Record = { langsmith: LangsmithIcon, latex: LatexIcon, launchdarkly: LaunchDarklyIcon, + leadmagic: LeadMagicIcon, lemlist: LemlistIcon, linear_v2: LinearIcon, linkedin: LinkedInIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 1c8b0a9a86..2ff2b8f785 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-16", + "updatedAt": "2026-06-17", "integrations": [ { "type": "onepassword", @@ -239,7 +239,7 @@ }, { "name": "Update Contact", - "description": "Update a contact" + "description": "Update a contact's fields" }, { "name": "Delete Contact", @@ -349,7 +349,7 @@ "operations": [ { "name": "Domain Rating", - "description": "Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website" + "description": "Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website's backlink profile on a scale from 0 to 100." }, { "name": "Backlinks", @@ -770,7 +770,7 @@ "operations": [ { "name": "Search People", - "description": "Search Apollo" + "description": "Search Apollo's database for people using demographic filters" }, { "name": "Enrich Person", @@ -782,7 +782,7 @@ }, { "name": "Search Organizations", - "description": "Search Apollo" + "description": "Search Apollo's database for companies using filters" }, { "name": "Enrich Organization", @@ -802,7 +802,7 @@ }, { "name": "Search Contacts", - "description": "Search your team" + "description": "Search your team's contacts in Apollo" }, { "name": "Bulk Create Contacts", @@ -822,7 +822,7 @@ }, { "name": "Search Accounts", - "description": "Search your team" + "description": "Search your team's accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required." }, { "name": "Bulk Create Accounts", @@ -838,7 +838,7 @@ }, { "name": "Search Opportunities", - "description": "Search and list all deals/opportunities in your team" + "description": "Search and list all deals/opportunities in your team's Apollo account" }, { "name": "Get Opportunity", @@ -850,7 +850,7 @@ }, { "name": "Search Sequences", - "description": "Search for sequences/campaigns in your team" + "description": "Search for sequences/campaigns in your team's Apollo account (master key required)" }, { "name": "Add to Sequence", @@ -866,7 +866,7 @@ }, { "name": "Get Email Accounts", - "description": "Get list of team" + "description": "Get list of team's linked email accounts in Apollo" } ], "operationCount": 25, @@ -2024,7 +2024,7 @@ }, { "name": "Get Work Items Batch", - "description": "Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. " + "description": "Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. \"123,456,789\"). Requests with more than 200 IDs are automatically split into chunks." }, { "name": "Create Work Item", @@ -3384,7 +3384,7 @@ }, { "name": "Extract Products", - "description": "Extract the product catalog from a brand" + "description": "Extract the product catalog from a brand's website by domain (beta)." }, { "name": "Scrape Fonts", @@ -3392,7 +3392,7 @@ }, { "name": "Scrape Styleguide", - "description": "Extract a domain" + "description": "Extract a domain's design system: colors, typography, spacing, shadows, and UI components." }, { "name": "Classify NAICS", @@ -3432,7 +3432,7 @@ }, { "name": "Prefetch by Email", - "description": "Queue an email" + "description": "Queue an email's domain for brand-data prefetching to reduce later latency (subscribers; 0 credits). Free/disposable emails are rejected." } ], "operationCount": 22, @@ -3817,6 +3817,45 @@ "integrationType": "observability", "tags": ["monitoring", "incident-management", "error-tracking"] }, + { + "type": "datagma", + "slug": "datagma", + "name": "Datagma", + "description": "Find verified B2B emails, mobile phones, and enrich person or company profiles", + "longDescription": "Integrate Datagma to find verified work emails from a name and company, enrich person profiles via email or LinkedIn URL, enrich company data from a domain or name, look up mobile phone numbers from LinkedIn, and check your credit balance.", + "bgColor": "#FFFFFF", + "iconName": "DatagmaIcon", + "docsUrl": "https://docs.sim.ai/tools/datagma", + "operations": [ + { + "name": "Find Email", + "description": "Find a verified work email from a person's full name and company. Uses 1 credit when a verified email is found." + }, + { + "name": "Enrich Person", + "description": "Enrich a person's profile using their email, LinkedIn URL, or full name and company. Returns job title, company, location, and social data. Uses 2 credits per match; add 30 credits when a phone number is found." + }, + { + "name": "Enrich Company", + "description": "Enrich a company profile using a domain, company name, or SIREN number (France). Returns size, industry, revenue, and description. Uses 2 credits per match." + }, + { + "name": "Find Phone", + "description": "Find a mobile phone number from a person's LinkedIn URL. Optionally supply an email to improve match accuracy. Uses 30 credits when a number is found." + }, + { + "name": "Get Remaining Credits", + "description": "Check remaining credit balance on a Datagma account. Free — no credits consumed." + } + ], + "operationCount": 5, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement"] + }, { "type": "daytona", "slug": "daytona", @@ -4218,6 +4257,29 @@ "integrationType": "documents", "tags": ["cloud", "document-processing"] }, + { + "type": "dropcontact", + "slug": "dropcontact", + "name": "Dropcontact", + "description": "Enrich B2B contacts with verified email, phone, and company data", + "longDescription": "Use Dropcontact to verify and enrich B2B contacts. Submit a contact with their name, company, website, or LinkedIn URL and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. Enrichment is async: Dropcontact processes the request, then Sim polls until the result is ready. Credits are only charged when a verified email is returned.", + "bgColor": "#0066FF", + "iconName": "DropcontactIcon", + "docsUrl": "https://docs.sim.ai/tools/dropcontact", + "operations": [ + { + "name": "Enrich Contact", + "description": "Enrich a contact with verified B2B email, phone, company data, and LinkedIn info via Dropcontact. Submits an async enrichment request, then polls until the result is ready (up to 2 minutes). Charges 1 credit only when a verified email is returned. Provide at least one of: email, first_name+last_name+company, full_name+company, or linkedin URL." + } + ], + "operationCount": 1, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement"] + }, { "type": "dspy", "slug": "dspy", @@ -4540,7 +4602,7 @@ }, { "name": "Find Email", - "description": "Find a person" + "description": "Find a person's work email address using their full name and company domain." }, { "name": "LinkedIn to Work Email", @@ -4636,11 +4698,11 @@ }, { "name": "Search People Activities", - "description": "Get a person" + "description": "Get a person's LinkedIn activities (posts, comments, or articles) by profile ID." }, { "name": "Search Company Activities", - "description": "Get a company" + "description": "Get a company's LinkedIn activities (posts, comments, or articles) by company ID." }, { "name": "Reverse Hash Lookup", @@ -4663,6 +4725,33 @@ "integrationType": "sales", "tags": ["enrichment"] }, + { + "type": "enrow", + "slug": "enrow", + "name": "Enrow", + "description": "Find and verify B2B emails with triple-verified accuracy", + "longDescription": "Integrate Enrow to find verified B2B email addresses from a full name and company, or verify the deliverability of an existing email. Enrow performs deterministic verifications including catch-all emails — no additional verifier needed.", + "bgColor": "#FFFFFF", + "iconName": "EnrowIcon", + "docsUrl": "https://enrow.readme.io", + "operations": [ + { + "name": "Find Email", + "description": "Find a verified B2B email address from a full name and company domain or name. Uses the Enrow async finder — submits a search and polls until the result is ready. Costs 1 credit per valid email found. (https://enrow.readme.io/reference/find-single-email)" + }, + { + "name": "Verify Email", + "description": "Verify the deliverability of an email address using the Enrow async verifier. Submits a verification request and polls until the result is ready. Costs 0.25 credits per verification. (https://enrow.readme.io/reference/verify-single-email)" + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement"] + }, { "type": "evernote", "slug": "evernote", @@ -4845,11 +4934,11 @@ "operations": [ { "name": "Find Email From Name", - "description": "Find someone" + "description": "Find someone's email from their name and a company domain or company name. Uses one finder credit when a verified email is found." }, { "name": "Find Email From LinkedIn", - "description": "Find someone" + "description": "Find someone's email from a LinkedIn profile URL or username. Uses one finder credit when a verified email is found." }, { "name": "Find Emails By Domain", @@ -4873,7 +4962,7 @@ }, { "name": "Find Phone", - "description": "Find someone" + "description": "Find someone's phone number from a LinkedIn profile URL. Uses 10 finder credits if a phone is found. EU citizens are excluded for legal reasons." }, { "name": "Search Technologies", @@ -5892,7 +5981,7 @@ }, { "name": "List Calendars", - "description": "List all calendars in the user" + "description": "List all calendars in the user's calendar list" }, { "name": "Quick Add (Natural Language)", @@ -6218,7 +6307,7 @@ }, { "name": "Update Member Role", - "description": "Update a member" + "description": "Update a member's role in a Google Group (promote or demote)" }, { "name": "Remove Member", @@ -6519,11 +6608,11 @@ }, { "name": "Replace All Shapes With Image", - "description": "Find every shape whose text matches the given token (e.g. {{cover-image}}) and replace it with an image, preserving the shape" + "description": "Find every shape whose text matches the given token (e.g. {{cover-image}}) and replace it with an image, preserving the shape's position and bounds." }, { "name": "Replace Image", - "description": "Replace the source of an existing image with a new image URL, preserving the image" + "description": "Replace the source of an existing image with a new image URL, preserving the image's position, size, and properties." }, { "name": "Update Image Properties", @@ -6595,7 +6684,7 @@ }, { "name": "Update Shape Properties", - "description": "Update a shape" + "description": "Update a shape's appearance — background fill color, outline, link, content alignment, autofit. Pass only the properties you want to change." }, { "name": "Update Page Properties", @@ -6631,7 +6720,7 @@ }, { "name": "Update Line Category", - "description": "Change a connector line" + "description": "Change a connector line's category (STRAIGHT, BENT, or CURVED)." }, { "name": "Reroute Line", @@ -6687,7 +6776,7 @@ }, { "name": "Replace All Shapes With Sheets Chart", - "description": "Find every shape matching a text token (e.g. {{revenue-chart}}) and replace each with the same embedded Sheets chart, preserving the shape" + "description": "Find every shape matching a text token (e.g. {{revenue-chart}}) and replace each with the same embedded Sheets chart, preserving the shape's position and bounds." }, { "name": "Embed Video", @@ -7533,6 +7622,33 @@ "integrationType": "sales", "tags": ["enrichment", "sales-engagement"] }, + { + "type": "icypeas", + "slug": "icypeas", + "name": "Icypeas", + "description": "Find and verify professional email addresses", + "longDescription": "Integrate Icypeas to find a professional email address from a name and company domain, or verify whether an existing email is valid and deliverable. Results are returned asynchronously via polling.", + "bgColor": "#0EA5E9", + "iconName": "IcypeasIcon", + "docsUrl": "https://docs.sim.ai/tools/icypeas", + "operations": [ + { + "name": "Find Email", + "description": "Find a professional email address from a first name, last name, and company domain or name. Submits the search and polls until a result is available. Costs 1 credit per found email (https://www.icypeas.com/pricing)." + }, + { + "name": "Verify Email", + "description": "Verify whether an email address is valid and deliverable. Submits the verification and polls until a result is available. Costs 0.1 credit per verification (https://www.icypeas.com/pricing)." + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement"] + }, { "type": "incidentio", "slug": "incident-io", @@ -8508,7 +8624,7 @@ }, { "name": "Search Assets (AQL)", - "description": "Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = " + "description": "Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = \"Host\" AND Status = \"Running\". Supports pagination." }, { "name": "Get Asset Object", @@ -8622,7 +8738,7 @@ }, { "name": "Get Fills", - "description": "Retrieve your portfolio" + "description": "Retrieve your portfolio's fills/trades from Kalshi" }, { "name": "Get Settlements", @@ -8837,6 +8953,61 @@ "integrationType": "devops", "tags": ["feature-flags", "ci-cd"] }, + { + "type": "leadmagic", + "slug": "leadmagic", + "name": "LeadMagic", + "description": "Find and enrich B2B contacts, emails, mobile numbers, and company data", + "longDescription": "Integrate LeadMagic to find verified work emails by name or company, validate email deliverability, find direct mobile numbers, enrich LinkedIn profiles, reverse-lookup profiles from emails, search companies by domain, identify role holders at accounts, and check account credit balance.", + "bgColor": "#FFFFFF", + "iconName": "LeadMagicIcon", + "docsUrl": "https://docs.sim.ai/tools/leadmagic", + "operations": [ + { + "name": "Find Email", + "description": "Find someone's verified work email from their name and company domain. Charges 1 credit when a valid email is found; free when no result." + }, + { + "name": "Validate Email", + "description": "Verify an email address for deliverability. Charges 0.25 credits for definitive SMTP results (valid/invalid); unknown and RFC-invalid results are free." + }, + { + "name": "Find Mobile", + "description": "Find a person's direct mobile number from their LinkedIn profile URL or email. Charges 5 credits when a number is found; free when no result." + }, + { + "name": "Profile Search", + "description": "Enrich a LinkedIn profile with work history, education, skills, and contact data. Charges 1 credit per successful enrichment; free when profile not found." + }, + { + "name": "Profile to Email", + "description": "Extract a verified work email from a LinkedIn profile URL. Charges 5 credits when an email is found; free when no result." + }, + { + "name": "Email to Profile", + "description": "Retrieve a LinkedIn profile URL from a work or personal email address. Charges 10 credits when a profile is found; free when no result." + }, + { + "name": "Company Search", + "description": "Enrich company data including firmographics, headcount, funding, and social profiles by domain, LinkedIn URL, or name. Charges 1 credit when a company is found; free when no result." + }, + { + "name": "Role Finder", + "description": "Find the person holding a specific job role at a company. Charges 2 credits when a matching person is found; free when no result." + }, + { + "name": "Get Credits", + "description": "Retrieve the current credit balance for the authenticated LeadMagic account. This endpoint is free and consumes no credits." + } + ], + "operationCount": 9, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "sales", + "tags": ["enrichment", "sales-engagement"] + }, { "type": "lemlist", "slug": "lemlist", @@ -9622,7 +9793,7 @@ }, { "name": "Get Guest", - "description": "Retrieve a single guest" + "description": "Retrieve a single guest's details on a Luma event, including approval status, registration timestamps, and contact info." }, { "name": "Add Guests", @@ -9634,7 +9805,7 @@ }, { "name": "Update Guest Status", - "description": "Update a guest" + "description": "Update a guest's approval status on a Luma event — approve, decline, waitlist, or set to pending. Identify the guest by email or guest ID." } ], "operationCount": 11, @@ -10568,7 +10739,7 @@ "operations": [ { "name": "Query (MATCH)", - "description": "Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g., " + "description": "Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g., \"MATCH (n:User) RETURN n LIMIT 100\") or use LIMIT $limit with a limit parameter." }, { "name": "Create Nodes/Relationships", @@ -11159,7 +11330,7 @@ }, { "name": "Search", - "description": "Get ranked search results from Perplexity" + "description": "Get ranked search results from Perplexity's continuously refreshed index with advanced filtering and customization options" } ], "operationCount": 2, @@ -11305,7 +11476,7 @@ "operations": [ { "name": "Generate Embeddings", - "description": "Generate embeddings from text using Pinecone" + "description": "Generate embeddings from text using Pinecone's hosted models" }, { "name": "Upsert Text", @@ -11597,7 +11768,7 @@ }, { "name": "Run Query (HogQL)", - "description": "Execute a HogQL query in PostHog. HogQL is PostHog" + "description": "Execute a HogQL query in PostHog. HogQL is PostHog's SQL-like query language for analytics. Use this for advanced data retrieval and analysis." }, { "name": "List Insights", @@ -12531,7 +12702,7 @@ }, { "name": "Command", - "description": "Execute a raw Redis command as a JSON array (e.g. [" + "description": "Execute a raw Redis command as a JSON array (e.g. [\"HSET\", \"key\", \"field\", \"value\"])." } ], "operationCount": 22, @@ -13822,7 +13993,7 @@ }, { "name": "Update Customer", - "description": "Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like " + "description": "" }, { "name": "List Suppliers", @@ -14676,7 +14847,7 @@ }, { "name": "Publish View", - "description": "Publish a static view to a user" + "description": "Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience." } ], "operationCount": 35, @@ -15435,19 +15606,19 @@ "operations": [ { "name": "Search", - "description": "Perform AI-powered web searches using Tavily" + "description": "Perform AI-powered web searches using Tavily's search API. Returns structured results with titles, URLs, snippets, and optional raw content, optimized for relevance and accuracy." }, { "name": "Extract Content", - "description": "Extract raw content from multiple web pages simultaneously using Tavily" + "description": "Extract raw content from multiple web pages simultaneously using Tavily's extraction API. Supports basic and advanced extraction depths with detailed error reporting for failed URLs." }, { "name": "Crawl Website", - "description": "Systematically crawl and extract content from websites using Tavily" + "description": "Systematically crawl and extract content from websites using Tavily's crawl API. Supports depth control, path filtering, domain restrictions, and natural language instructions for targeted crawling." }, { "name": "Map Website", - "description": "Discover and visualize website structure using Tavily" + "description": "Discover and visualize website structure using Tavily's map API. Maps out all accessible URLs from a base URL with depth control, path filtering, and domain restrictions." } ], "operationCount": 4, @@ -16076,7 +16247,7 @@ }, { "name": "Command", - "description": "Execute an arbitrary Redis command against Upstash Redis. Pass the full command as a JSON array (e.g., [" + "description": "Execute an arbitrary Redis command against Upstash Redis. Pass the full command as a JSON array (e.g., [\"HSET\", \"myhash\", \"field1\", \"value1\"])." } ], "operationCount": 16, @@ -16302,7 +16473,7 @@ }, { "name": "Update Project Domain", - "description": "Update a project domain" + "description": "Update a project domain's configuration on Vercel" }, { "name": "Verify Project Domain", @@ -16682,7 +16853,7 @@ "operations": [ { "name": "Prospect Search", - "description": "Search Wiza" + "description": "Search Wiza's database of prospects using person, company, and financial filters" }, { "name": "Company Enrichment", @@ -16964,11 +17135,11 @@ }, { "name": "Delete Bookmark", - "description": "Remove a tweet from the authenticated user" + "description": "Remove a tweet from the authenticated user's bookmarks" }, { "name": "Get My Profile", - "description": "Get the authenticated user" + "description": "Get the authenticated user's profile information" }, { "name": "Search Users", diff --git a/apps/sim/tools/google_bigquery/get_table.ts b/apps/sim/tools/google_bigquery/get_table.ts index 50cf0437a8..95ac54d6dc 100644 --- a/apps/sim/tools/google_bigquery/get_table.ts +++ b/apps/sim/tools/google_bigquery/get_table.ts @@ -95,12 +95,14 @@ export const googleBigQueryGetTableTool: ToolConfig< type: { type: 'string', description: 'Table type (TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL)', + optional: true, }, description: { type: 'string', description: 'Table description', optional: true }, - numRows: { type: 'string', description: 'Total number of rows' }, + numRows: { type: 'string', description: 'Total number of rows', optional: true }, numBytes: { type: 'string', description: 'Total size in bytes, excluding data in streaming buffer', + optional: true, }, schema: { type: 'array', @@ -122,11 +124,20 @@ export const googleBigQueryGetTableTool: ToolConfig< }, }, }, - creationTime: { type: 'string', description: 'Table creation time (milliseconds since epoch)' }, + creationTime: { + type: 'string', + description: 'Table creation time (milliseconds since epoch)', + optional: true, + }, lastModifiedTime: { type: 'string', description: 'Last modification time (milliseconds since epoch)', + optional: true, + }, + location: { + type: 'string', + description: 'Geographic location where the table resides', + optional: true, }, - location: { type: 'string', description: 'Geographic location where the table resides' }, }, } diff --git a/apps/sim/tools/google_bigquery/list_datasets.ts b/apps/sim/tools/google_bigquery/list_datasets.ts index 75da30bfe5..32c46f1ba4 100644 --- a/apps/sim/tools/google_bigquery/list_datasets.ts +++ b/apps/sim/tools/google_bigquery/list_datasets.ts @@ -108,7 +108,11 @@ export const googleBigQueryListDatasetsTool: ToolConfig< description: 'Descriptive name for the dataset', optional: true, }, - location: { type: 'string', description: 'Geographic location where the data resides' }, + location: { + type: 'string', + description: 'Geographic location where the data resides', + optional: true, + }, }, }, }, diff --git a/apps/sim/tools/google_bigquery/list_tables.ts b/apps/sim/tools/google_bigquery/list_tables.ts index 1d116a5c1e..2bb78efc59 100644 --- a/apps/sim/tools/google_bigquery/list_tables.ts +++ b/apps/sim/tools/google_bigquery/list_tables.ts @@ -114,7 +114,11 @@ export const googleBigQueryListTablesTool: ToolConfig< tableId: { type: 'string', description: 'Table identifier' }, datasetId: { type: 'string', description: 'Dataset ID containing this table' }, projectId: { type: 'string', description: 'Project ID containing this table' }, - type: { type: 'string', description: 'Table type (TABLE, VIEW, EXTERNAL, etc.)' }, + type: { + type: 'string', + description: 'Table type (TABLE, VIEW, EXTERNAL, etc.)', + optional: true, + }, friendlyName: { type: 'string', description: 'User-friendly name for the table', diff --git a/apps/sim/tools/google_bigquery/query.ts b/apps/sim/tools/google_bigquery/query.ts index ad11adc901..da41bc72ee 100644 --- a/apps/sim/tools/google_bigquery/query.ts +++ b/apps/sim/tools/google_bigquery/query.ts @@ -139,7 +139,11 @@ export const googleBigQueryQueryTool: ToolConfig< optional: true, }, jobComplete: { type: 'boolean', description: 'Whether the query completed within the timeout' }, - totalBytesProcessed: { type: 'string', description: 'Total bytes processed by the query' }, + totalBytesProcessed: { + type: 'string', + description: 'Total bytes processed by the query', + optional: true, + }, cacheHit: { type: 'boolean', description: 'Whether the query result was served from cache', diff --git a/apps/sim/tools/google_forms/get_responses.ts b/apps/sim/tools/google_forms/get_responses.ts index a2f82cfe63..29c62419f3 100644 --- a/apps/sim/tools/google_forms/get_responses.ts +++ b/apps/sim/tools/google_forms/get_responses.ts @@ -39,10 +39,23 @@ export const getResponsesTool: ToolConfig = { pageSize: { type: 'number', required: false, - visibility: 'user-only', + visibility: 'user-or-llm', description: 'Maximum number of responses to return (service may return fewer). Defaults to 5000.', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous list response to fetch the next page of responses', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter responses, e.g. "timestamp > 2024-01-01T00:00:00Z" (RFC3339 UTC). Only timestamp filters are supported.', + }, }, request: { @@ -52,6 +65,8 @@ export const getResponsesTool: ToolConfig = { : buildListResponsesUrl({ formId: params.formId, pageSize: params.pageSize ? Number(params.pageSize) : undefined, + pageToken: params.pageToken, + filter: params.filter, }), method: 'GET', headers: (params: GoogleFormsGetResponsesParams) => ({ @@ -147,6 +162,7 @@ export const getResponsesTool: ToolConfig = { const normalized = sorted.map((r) => normalizeResponse(r)) const output: Record = { responses: normalized, + nextPageToken: listData.nextPageToken ?? null, raw: listData, } return { @@ -188,6 +204,11 @@ export const getResponsesTool: ToolConfig = { }, }, }, + nextPageToken: { + type: 'string', + description: 'Token to fetch the next page of responses (null when no more pages)', + optional: true, + }, response: { type: 'object', description: 'Single form response (when responseId is provided)', diff --git a/apps/sim/tools/google_forms/types.ts b/apps/sim/tools/google_forms/types.ts index 4cee08edfc..9a191d3f54 100644 --- a/apps/sim/tools/google_forms/types.ts +++ b/apps/sim/tools/google_forms/types.ts @@ -92,6 +92,8 @@ export interface GoogleFormsGetResponsesParams { formId: string responseId?: string pageSize?: number + pageToken?: string + filter?: string } // ============================================ diff --git a/apps/sim/tools/google_forms/utils.ts b/apps/sim/tools/google_forms/utils.ts index bd9ea2bf70..4d7db7a44f 100644 --- a/apps/sim/tools/google_forms/utils.ts +++ b/apps/sim/tools/google_forms/utils.ts @@ -18,13 +18,24 @@ export function getGoogleFormsErrorMessage(data: unknown, fallback: string): str return typeof message === 'string' ? message : fallback } -export function buildListResponsesUrl(params: { formId: string; pageSize?: number }): string { - const { formId, pageSize } = params +export function buildListResponsesUrl(params: { + formId: string + pageSize?: number + pageToken?: string + filter?: string +}): string { + const { formId, pageSize, pageToken, filter } = params const url = new URL(`${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/responses`) if (pageSize && pageSize > 0) { const limited = Math.min(pageSize, 5000) url.searchParams.set('pageSize', String(limited)) } + if (pageToken) { + url.searchParams.set('pageToken', pageToken) + } + if (filter) { + url.searchParams.set('filter', filter) + } const finalUrl = url.toString() logger.debug('Built Google Forms list responses URL', { finalUrl }) return finalUrl diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 94929e68e3..07e2f87d11 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -551,10 +551,15 @@ async function buildToolDescriptionMap(): Promise { // Stop before any params block so we don't pick up param-level values const paramsOffset = window.search(/\bparams\s*:\s*\{/) const searchWindow = paramsOffset > 0 ? window.substring(0, paramsOffset) : window - const descMatch = searchWindow.match(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/) - const nameMatch = searchWindow.match(/\bname\s*:\s*['"]([^'"]+)['"]/) - if (descMatch) desc.set(toolId, descMatch[1]) - if (nameMatch) name.set(toolId, nameMatch[1]) + // Match against the actual opening quote so apostrophes inside a + // double-quoted description (e.g. "Find someone's email") are preserved + // rather than being treated as the closing quote and truncating the value. + const descMatch = searchWindow.match( + /\bdescription\s*:\s*(?:'([^']{5,})'|"([^"]{5,})"|`([^`]{5,})`)/ + ) + const nameMatch = searchWindow.match(/\bname\s*:\s*(?:'([^']+)'|"([^"]+)"|`([^`]+)`)/) + if (descMatch) desc.set(toolId, descMatch[1] ?? descMatch[2] ?? descMatch[3] ?? '') + if (nameMatch) name.set(toolId, nameMatch[1] ?? nameMatch[2] ?? nameMatch[3] ?? '') } } } catch { @@ -1782,10 +1787,13 @@ function extractToolInfo( } } - // Params are often inherited via spread, so search the full file for params + // Prefer the params block scoped to this specific tool so that files + // defining multiple tools (e.g. file_compress + file_decompress in + // compress.ts) don't all inherit the first tool's params. Fall back to the + // full file for tools that inherit params via spread from a base object. const toolConfigRegex = /params\s*:\s*{([\s\S]*?)},?\s*(?:outputs|oauth|request|directExecution|postProcess|transformResponse)\s*:/ - const toolConfigMatch = fileContent.match(toolConfigRegex) + const toolConfigMatch = toolContent.match(toolConfigRegex) ?? fileContent.match(toolConfigRegex) // Description should come from the specific tool block if found // Only search before nested objects (params, outputs, request, etc.) to avoid matching @@ -1807,7 +1815,9 @@ function extractToolInfo( } descriptionSearchContent = toolContent.substring(0, cutoffIndex) - const descriptionRegex = /description\s*:\s*['"](.*?)['"].*/ + // Match against the actual opening quote so apostrophes inside a double-quoted + // description (e.g. "Find someone's email") are not treated as the closing quote. + const descriptionRegex = /description\s*:\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)/ let descriptionMatch = descriptionSearchContent.match(descriptionRegex) // If description isn't found as a literal (might be inherited like description: baseTool.description), @@ -1820,7 +1830,7 @@ function extractToolInfo( const baseTool = inheritedDescMatch[1] // Try to find the base tool's description in the file const baseToolDescRegex = new RegExp( - `export\\s+const\\s+${baseTool}Tool[^{]*\\{[\\s\\S]*?description\\s*:\\s*['"]([^'"]+)['"]`, + `export\\s+const\\s+${baseTool}Tool[^{]*\\{[\\s\\S]*?description\\s*:\\s*(?:'([^']+)'|"([^"]+)"|\`([^\`]+)\`)`, 'i' ) const baseToolMatch = fileContent.match(baseToolDescRegex) @@ -1830,7 +1840,12 @@ function extractToolInfo( } } - const description = descriptionMatch ? descriptionMatch[1] : 'No description available' + const description = descriptionMatch + ? (descriptionMatch[1] ?? + descriptionMatch[2] ?? + descriptionMatch[3] ?? + 'No description available') + : 'No description available' const params: Array<{ name: string; type: string; required: boolean; description: string }> = [] @@ -2560,6 +2575,7 @@ async function getToolInfo(toolName: string): Promise<{ let toolFileContent = '' let foundFile = '' + let foundExactId = false // Try to find a file that contains the exact tool ID for (const location of possibleLocations) { @@ -2571,6 +2587,7 @@ async function getToolInfo(toolName: string): Promise<{ if (toolIdRegex.test(content)) { toolFileContent = content foundFile = location.path + foundExactId = true break } @@ -2582,6 +2599,28 @@ async function getToolInfo(toolName: string): Promise<{ } } + // The named-file candidates above miss tools defined inside a sibling tool's + // file (e.g. file_decompress lives in compress.ts). Before accepting an + // arbitrary fallback file, scan the whole tool-prefix directory for the file + // that declares this exact tool ID. + if (!foundExactId) { + const prefixDir = path.join(rootDir, `apps/sim/tools/${toolPrefix}`) + if (fs.existsSync(prefixDir)) { + const dirFiles = await glob(`${prefixDir}/**/*.ts`) + const toolIdRegex = new RegExp(`id:\\s*['"]${toolName}['"]`) + for (const dirFile of dirFiles) { + if (dirFile.endsWith('.test.ts')) continue + const content = fs.readFileSync(dirFile, 'utf-8') + if (toolIdRegex.test(content)) { + toolFileContent = content + foundFile = dirFile + foundExactId = true + break + } + } + } + } + // If we didn't find a file with the exact ID, use the first available file if (!toolFileContent) { for (const location of possibleLocations) { From 05cd7d92a88787afaa5f932cd89026b848d9b6b9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 10:47:17 -0700 Subject: [PATCH 12/26] feat(search): actions, fuzzy matching, and highlighting in cmd+k palette (#5110) * feat(search): actions, fuzzy matching, and highlighting in cmd+k palette Add a context-aware actions layer to the cmd+k search palette (Run workflow, Create workflow/folder, Import workflow, Fit to view, Copy link, Invite teammates, Toggle theme), replace the substring matcher with a boundary-anchored fuzzy matcher (initialisms, typos, multi-word) that is a strict superset of the old behavior, highlight matched characters, and rank against clean human text instead of structural id/uuid tokens. Expose invoke() on the global commands provider so the palette runs real registered commands. * fix(search): highlight the matched substring, not an earlier scattered occurrence Contiguous substring matches (exact/prefix/contains) now report the substring's own indices instead of the greedy subsequence scan positions, so HighlightedText bolds the characters the user actually matched. Restructures fuzzyMatch to handle the substring tier first; scores are unchanged for these cases. * fix(search): log clipboard copy failures and make fuzzy positions read-only - Copy workflow link now logs on clipboard write failure instead of silently swallowing the error, matching the sidebar's copy-link convention. - FuzzyResult.positions is now readonly and the NO_MATCH singleton's array is frozen, so the shared instance can never be mutated by a caller. --- .../providers/global-commands-provider.tsx | 31 +- .../command-items/command-items.tsx | 151 +++++++++- .../components/command-items/index.ts | 2 + .../components/search-groups/index.ts | 1 + .../search-groups/search-groups.tsx | 95 +++++-- .../components/search-modal/search-modal.tsx | 268 +++++++++++++++--- .../components/search-modal/utils.test.ts | 243 ++++++++++++++++ .../sidebar/components/search-modal/utils.ts | 190 +++++++++++-- .../w/components/sidebar/sidebar.tsx | 4 + apps/sim/lib/posthog/events.ts | 3 + apps/sim/stores/modals/search/store.ts | 2 +- 11 files changed, 897 insertions(+), 93 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx index f58f4ace97..3c3d6ae0db 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx @@ -39,6 +39,7 @@ interface RegistryCommand extends GlobalCommand { interface GlobalCommandsContextValue { register: (commands: GlobalCommand[]) => () => void + invoke: (id: string) => boolean } const GlobalCommandsContext = createContext(null) @@ -142,11 +143,39 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) }, [isMac, router]) - const value = useMemo(() => ({ register }), [register]) + const invoke = useCallback((id: string): boolean => { + const cmd = registryRef.current.get(id) + if (!cmd) return false + try { + cmd.handler(new KeyboardEvent('keydown')) + } catch (err) { + logger.error('Global command handler threw', { id, err }) + } + return true + }, []) + + const value = useMemo( + () => ({ register, invoke }), + [register, invoke] + ) return {children} } +/** + * Returns a function that runs a registered global command by id, mirroring its + * keyboard shortcut exactly. Returns `false` when no command with that id is + * currently registered (e.g. a workflow-only command invoked off-canvas), so + * callers can offer the action safely without knowing what is mounted. + */ +export function useInvokeGlobalCommand(): (id: string) => boolean { + const ctx = useContext(GlobalCommandsContext) + if (!ctx) { + throw new Error('useInvokeGlobalCommand must be used within GlobalCommandsProvider') + } + return ctx.invoke +} + export function useRegisterGlobalCommands(commands: GlobalCommand[] | (() => GlobalCommand[])) { const ctx = useContext(GlobalCommandsContext) if (!ctx) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx index 3a1f99aed6..ce60b91fd6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx @@ -6,7 +6,54 @@ import { Command } from 'cmdk' import { File, Workflow } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' -import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' +import { + COMMAND_ITEM_CLASSNAME, + fuzzyMatch, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' + +interface Segment { + text: string + hit: boolean +} + +function buildSegments(text: string, positions: readonly number[]): Segment[] { + const hits = new Set(positions) + const segments: Segment[] = [] + for (let i = 0; i < text.length; i++) { + const hit = hits.has(i) + const last = segments[segments.length - 1] + if (last && last.hit === hit) last.text += text[i] + else segments.push({ text: text[i], hit }) + } + return segments +} + +/** + * Renders `text` with the characters that match `query` emphasized. Falls back + * to plain text when there is no query or no positional match against the + * display text (e.g. the row matched on a hidden id rather than its label). + */ +export const HighlightedText = memo( + function HighlightedText({ text, query }: { text: string; query?: string }) { + if (!query) return <>{text} + const { positions } = fuzzyMatch(text, query) + if (positions.length === 0) return <>{text} + return ( + <> + {buildSegments(text, positions).map((segment, index) => + segment.hit ? ( + + {segment.text} + + ) : ( + {segment.text} + ) + )} + + ) + }, + (prev, next) => prev.text === next.text && prev.query === next.query +) export const MemoizedCommandItem = memo( function CommandItem({ @@ -15,7 +62,8 @@ export const MemoizedCommandItem = memo( icon: Icon, bgColor, showColoredIcon, - children, + label, + query, }: CommandItemProps) { return ( @@ -32,7 +80,9 @@ export const MemoizedCommandItem = memo( )} /> - {children} + + + ) }, @@ -41,7 +91,46 @@ export const MemoizedCommandItem = memo( prev.icon === next.icon && prev.bgColor === next.bgColor && prev.showColoredIcon === next.showColoredIcon && - prev.children === next.children + prev.label === next.label && + prev.query === next.query +) + +export const MemoizedActionItem = memo( + function ActionItem({ + value, + onSelect, + icon: Icon, + name, + shortcut, + query, + }: { + value: string + onSelect: () => void + icon: ComponentType<{ className?: string }> + name: string + shortcut?: string + query?: string + }) { + return ( + + + + + + {shortcut && ( + + {shortcut} + + )} + + ) + }, + (prev, next) => + prev.value === next.value && + prev.icon === next.icon && + prev.name === next.name && + prev.shortcut === next.shortcut && + prev.query === next.query ) export const MemoizedWorkflowItem = memo( @@ -51,12 +140,14 @@ export const MemoizedWorkflowItem = memo( name, folderPath, isCurrent, + query, }: { value: string onSelect: () => void name: string folderPath?: string[] isCurrent?: boolean + query?: string }) { return ( @@ -64,7 +155,9 @@ export const MemoizedWorkflowItem = memo( - {name} + + + {isCurrent && (current)} {folderPath && folderPath.length > 0 && ( @@ -87,6 +180,7 @@ export const MemoizedWorkflowItem = memo( prev.value === next.value && prev.name === next.name && prev.isCurrent === next.isCurrent && + prev.query === next.query && (prev.folderPath === next.folderPath || (prev.folderPath?.length === next.folderPath?.length && (prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i]))) @@ -98,11 +192,13 @@ export const MemoizedFileItem = memo( onSelect, name, folderPath, + query, }: { value: string onSelect: () => void name: string folderPath?: string[] + query?: string }) { return ( @@ -110,7 +206,9 @@ export const MemoizedFileItem = memo( - {name} + + + {folderPath && folderPath.length > 0 && ( @@ -131,6 +229,7 @@ export const MemoizedFileItem = memo( (prev, next) => prev.value === next.value && prev.name === next.name && + prev.query === next.query && (prev.folderPath === next.folderPath || (prev.folderPath?.length === next.folderPath?.length && (prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i]))) @@ -141,18 +240,22 @@ export const MemoizedTaskItem = memo( value, onSelect, name, + query, }: { value: string onSelect: () => void name: string + query?: string }) { return ( - {name} + + + ) }, - (prev, next) => prev.value === next.value && prev.name === next.name + (prev, next) => prev.value === next.value && prev.name === next.name && prev.query === next.query ) export const MemoizedWorkspaceItem = memo( @@ -161,23 +264,30 @@ export const MemoizedWorkspaceItem = memo( onSelect, name, isCurrent, + query, }: { value: string onSelect: () => void name: string isCurrent?: boolean + query?: string }) { return ( - {name} + + + {isCurrent && (current)} ) }, (prev, next) => - prev.value === next.value && prev.name === next.name && prev.isCurrent === next.isCurrent + prev.value === next.value && + prev.name === next.name && + prev.isCurrent === next.isCurrent && + prev.query === next.query ) export const MemoizedPageItem = memo( @@ -187,17 +297,21 @@ export const MemoizedPageItem = memo( icon: Icon, name, shortcut, + query, }: { value: string onSelect: () => void icon: ComponentType<{ className?: string }> name: string shortcut?: string + query?: string }) { return ( - {name} + + + {shortcut && ( {shortcut} @@ -210,7 +324,8 @@ export const MemoizedPageItem = memo( prev.value === next.value && prev.icon === next.icon && prev.name === next.name && - prev.shortcut === next.shortcut + prev.shortcut === next.shortcut && + prev.query === next.query ) export const MemoizedIconItem = memo( @@ -219,18 +334,26 @@ export const MemoizedIconItem = memo( onSelect, name, icon: Icon, + query, }: { value: string onSelect: () => void name: string icon: ComponentType<{ className?: string }> + query?: string }) { return ( - {name} + + + ) }, - (prev, next) => prev.value === next.value && prev.name === next.name && prev.icon === next.icon + (prev, next) => + prev.value === next.value && + prev.name === next.name && + prev.icon === next.icon && + prev.query === next.query ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/index.ts index 718fd4e318..49a29bec4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/index.ts @@ -1,4 +1,6 @@ export { + HighlightedText, + MemoizedActionItem, MemoizedCommandItem, MemoizedFileItem, MemoizedIconItem, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts index 7790ac3790..151685fd91 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/index.ts @@ -1,4 +1,5 @@ export { + ActionsGroup, BlocksGroup, ChatsGroup, ConnectedAccountsGroup, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx index b46f3eb452..bf69aacea0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx @@ -5,6 +5,7 @@ import { memo } from 'react' import { Command } from 'cmdk' import { Database, Table } from '@/components/emcn/icons' import { + MemoizedActionItem, MemoizedCommandItem, MemoizedFileItem, MemoizedIconItem, @@ -14,6 +15,7 @@ import { MemoizedWorkspaceItem, } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items' import type { + ActionItem, FileItem, IntegrationSearchItem, PageItem, @@ -28,12 +30,41 @@ import type { SearchToolOperationItem, } from '@/stores/modals/search/types' +export const ActionsGroup = memo(function ActionsGroup({ + items, + onSelect, + query, +}: { + items: ActionItem[] + onSelect: (action: ActionItem) => void + query?: string +}) { + if (items.length === 0) return null + return ( + + {items.map((action) => ( + onSelect(action)} + icon={action.icon} + name={action.name} + shortcut={action.shortcut} + query={query} + /> + ))} + + ) +}) + export const BlocksGroup = memo(function BlocksGroup({ items, onSelect, + query, }: { items: SearchBlockItem[] onSelect: (block: SearchBlockItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -46,9 +77,9 @@ export const BlocksGroup = memo(function BlocksGroup({ icon={block.icon} bgColor={block.bgColor} showColoredIcon - > - {block.name} - + label={block.name} + query={query} + /> ))} ) @@ -57,9 +88,11 @@ export const BlocksGroup = memo(function BlocksGroup({ export const ToolsGroup = memo(function ToolsGroup({ items, onSelect, + query, }: { items: SearchBlockItem[] onSelect: (tool: SearchBlockItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -72,9 +105,9 @@ export const ToolsGroup = memo(function ToolsGroup({ icon={tool.icon} bgColor={tool.bgColor} showColoredIcon - > - {tool.name} - + label={tool.name} + query={query} + /> ))} ) @@ -83,9 +116,11 @@ export const ToolsGroup = memo(function ToolsGroup({ export const TriggersGroup = memo(function TriggersGroup({ items, onSelect, + query, }: { items: SearchBlockItem[] onSelect: (trigger: SearchBlockItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -98,9 +133,9 @@ export const TriggersGroup = memo(function TriggersGroup({ icon={trigger.icon} bgColor={trigger.bgColor} showColoredIcon - > - {trigger.name} - + label={trigger.name} + query={query} + /> ))} ) @@ -109,9 +144,11 @@ export const TriggersGroup = memo(function TriggersGroup({ export const ToolOpsGroup = memo(function ToolOpsGroup({ items, onSelect, + query, }: { items: SearchToolOperationItem[] onSelect: (op: SearchToolOperationItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -124,9 +161,9 @@ export const ToolOpsGroup = memo(function ToolOpsGroup({ icon={op.icon} bgColor={op.bgColor} showColoredIcon - > - {op.name} - + label={op.name} + query={query} + /> ))} ) @@ -135,9 +172,11 @@ export const ToolOpsGroup = memo(function ToolOpsGroup({ export const DocsGroup = memo(function DocsGroup({ items, onSelect, + query, }: { items: SearchDocItem[] onSelect: (doc: SearchDocItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -150,9 +189,9 @@ export const DocsGroup = memo(function DocsGroup({ icon={doc.icon} bgColor='#6B7280' showColoredIcon - > - {doc.name} - + label={doc.name} + query={query} + /> ))} ) @@ -161,9 +200,11 @@ export const DocsGroup = memo(function DocsGroup({ export const WorkflowsGroup = memo(function WorkflowsGroup({ items, onSelect, + query, }: { items: WorkflowItem[] onSelect: (workflow: WorkflowItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -176,6 +217,7 @@ export const WorkflowsGroup = memo(function WorkflowsGroup({ name={workflow.name} folderPath={workflow.folderPath} isCurrent={workflow.isCurrent} + query={query} /> ))} @@ -185,9 +227,11 @@ export const WorkflowsGroup = memo(function WorkflowsGroup({ export const ChatsGroup = memo(function ChatsGroup({ items, onSelect, + query, }: { items: TaskItem[] onSelect: (task: TaskItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -198,6 +242,7 @@ export const ChatsGroup = memo(function ChatsGroup({ value={`${task.name} task-${task.id}`} onSelect={() => onSelect(task)} name={task.name} + query={query} /> ))} @@ -207,9 +252,11 @@ export const ChatsGroup = memo(function ChatsGroup({ export const WorkspacesGroup = memo(function WorkspacesGroup({ items, onSelect, + query, }: { items: WorkspaceItem[] onSelect: (workspace: WorkspaceItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -221,6 +268,7 @@ export const WorkspacesGroup = memo(function WorkspacesGroup({ onSelect={() => onSelect(workspace)} name={workspace.name} isCurrent={workspace.isCurrent} + query={query} /> ))} @@ -230,9 +278,11 @@ export const WorkspacesGroup = memo(function WorkspacesGroup({ export const PagesGroup = memo(function PagesGroup({ items, onSelect, + query, }: { items: PageItem[] onSelect: (page: PageItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -245,6 +295,7 @@ export const PagesGroup = memo(function PagesGroup({ icon={page.icon} name={page.name} shortcut={page.shortcut} + query={query} /> ))} @@ -260,9 +311,11 @@ export const IntegrationsGroup = createColoredIconGroup('Integrations', 'integra export const FilesGroup = memo(function FilesGroup({ items, onSelect, + query, }: { items: FileItem[] onSelect: (file: FileItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -274,6 +327,7 @@ export const FilesGroup = memo(function FilesGroup({ onSelect={() => onSelect(file)} name={file.name} folderPath={file.folderPath} + query={query} /> ))} @@ -290,9 +344,11 @@ function createColoredIconGroup(heading: string, prefix: string) { return memo(function ColoredIconGroup({ items, onSelect, + query, }: { items: IntegrationSearchItem[] onSelect: (item: IntegrationSearchItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -305,9 +361,9 @@ function createColoredIconGroup(heading: string, prefix: string) { icon={item.icon} bgColor={item.bgColor} showColoredIcon - > - {item.name} - + label={item.name} + query={query} + /> ))} ) @@ -322,9 +378,11 @@ function createIconGroup( return memo(function IconGroup({ items, onSelect, + query, }: { items: TaskItem[] onSelect: (item: TaskItem) => void + query?: string }) { if (items.length === 0) return null return ( @@ -336,6 +394,7 @@ function createIconGroup( onSelect={() => onSelect(item)} name={item.name} icon={icon} + query={query} /> ))} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 7137191b93..9332b44a3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -1,25 +1,36 @@ 'use client' import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' import { Command } from 'cmdk' import { useParams, useRouter } from 'next/navigation' +import { useTheme } from 'next-themes' import { usePostHog } from 'posthog-js/react' import { createPortal } from 'react-dom' import { Library } from '@/components/emcn' import { Calendar, Database, + Expand, File, + FolderPlus, HelpCircle, Home, Integration, + Link, + Palette, + Play, + Plus, Settings, Table, + Upload, + Users, } from '@/components/emcn/icons' import { Search } from '@/components/emcn/icons/search' import { cn } from '@/lib/core/utils/cn' import { captureEvent } from '@/lib/posthog/client' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' +import { useInvokeGlobalCommand } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { CMDK_ITEM_GAP_CLASS, CMDK_SECTION_GAP_CLASS, @@ -34,6 +45,7 @@ import type { SearchToolOperationItem, } from '@/stores/modals/search/types' import { + ActionsGroup, BlocksGroup, ChatsGroup, ConnectedAccountsGroup, @@ -50,6 +62,7 @@ import { WorkspacesGroup, } from './components/search-groups' import type { + ActionItem, FileItem, IntegrationSearchItem, PageItem, @@ -60,6 +73,8 @@ import type { } from './utils' import { filterAndSort } from './utils' +const logger = createLogger('SearchModal') + export type { SearchModalProps } from './utils' export function SearchModal({ @@ -75,6 +90,10 @@ export function SearchModal({ connectedAccounts = [], isOnWorkflowPage = false, isOnIntegrationsPage = false, + canEdit = false, + onCreateWorkflow, + onCreateFolder, + onImportWorkflow, }: SearchModalProps) { const params = useParams() const router = useRouter() @@ -83,6 +102,8 @@ export function SearchModal({ const [mounted, setMounted] = useState(false) const { navigateToSettings } = useSettingsNavigation() const { config: permissionConfig } = usePermissionConfig() + const { resolvedTheme, setTheme } = useTheme() + const invokeCommand = useInvokeGlobalCommand() const posthog = usePostHog() const routerRef = useRef(router) @@ -178,6 +199,100 @@ export function SearchModal({ ] ) + /** + * Verbs the palette can run directly. Entity navigation lives in the groups + * below; this list is for "do something" intents (create, import, toggle). + */ + const actions = useMemo((): ActionItem[] => { + const list: ActionItem[] = [] + list.push({ + id: 'run-workflow', + name: 'Run workflow', + keywords: 'execute start play test', + icon: Play, + shortcut: '⌘↵', + context: 'workflow', + run: () => invokeCommand('run-workflow'), + }) + if (canEdit && onCreateWorkflow) { + list.push({ + id: 'create-workflow', + name: 'Create workflow', + keywords: 'new add build', + icon: Plus, + context: 'global', + run: onCreateWorkflow, + }) + } + if (canEdit && onCreateFolder) { + list.push({ + id: 'create-folder', + name: 'Create folder', + keywords: 'new add group', + icon: FolderPlus, + context: 'global', + run: onCreateFolder, + }) + } + if (canEdit && onImportWorkflow) { + list.push({ + id: 'import-workflow', + name: 'Import workflow', + keywords: 'upload add', + icon: Upload, + context: 'global', + run: onImportWorkflow, + }) + } + list.push({ + id: 'fit-to-view', + name: 'Fit workflow to view', + keywords: 'zoom center recenter canvas reset', + icon: Expand, + shortcut: '⌘⇧F', + context: 'workflow', + run: () => invokeCommand('fit-to-view'), + }) + list.push({ + id: 'copy-workflow-url', + name: 'Copy workflow link', + keywords: 'url share clipboard', + icon: Link, + context: 'workflow', + run: () => { + navigator.clipboard.writeText(window.location.href).catch((error) => { + logger.error('Failed to copy workflow link to clipboard', { error }) + }) + }, + }) + list.push({ + id: 'invite-teammates', + name: 'Invite teammates', + keywords: 'members people add user organization', + icon: Users, + context: 'global', + run: () => navigateToSettings({ section: 'teammates' }), + }) + list.push({ + id: 'toggle-theme', + name: 'Toggle theme', + keywords: 'dark light mode appearance color', + icon: Palette, + context: 'global', + run: () => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'), + }) + return list + }, [ + canEdit, + onCreateWorkflow, + onCreateFolder, + onImportWorkflow, + invokeCommand, + navigateToSettings, + resolvedTheme, + setTheme, + ]) + const [search, setSearch] = useState('') const [prevOpen, setPrevOpen] = useState(open) if (open !== prevOpen) { @@ -410,6 +525,20 @@ export function SearchModal({ [workspaceId] ) + const handleActionSelect = useCallback( + (item: ActionItem) => { + onOpenChangeRef.current(false) + item.run() + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'action', + action_id: item.id, + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + }, + [workspaceId] + ) + const handleBlockSelectAsBlock = useCallback( (block: SearchBlockItem) => handleBlockSelect(block, 'block'), [handleBlockSelect] @@ -429,85 +558,88 @@ export function SearchModal({ onOpenChangeRef.current(false) }, []) + const filteredActions = useMemo(() => { + const available = actions.filter( + (a) => + a.context === 'global' || + (a.context === 'workflow' && isOnWorkflowPage) || + (a.context === 'integrations' && isOnIntegrationsPage) + ) + return filterAndSort(available, (a) => `${a.name} ${a.keywords ?? ''}`, deferredSearch) + }, [actions, isOnWorkflowPage, isOnIntegrationsPage, deferredSearch]) + + /** + * Ranking matches against clean, human-meaningful text only (names, types, + * aliases, folder paths) — never the structural `-`/uuid tokens used + * for cmdk row identity. Those tokens carry letters (e.g. "block", "tool") that + * would otherwise let short fuzzy queries scatter-match unrelated items. + */ const filteredBlocks = useMemo(() => { if (!isOnWorkflowPage) return [] - return filterAndSort(blocks, (b) => b.searchValue ?? `${b.name} block-${b.id}`, deferredSearch) + return filterAndSort(blocks, (b) => b.searchValue ?? b.name, deferredSearch) }, [isOnWorkflowPage, blocks, deferredSearch]) const filteredTools = useMemo(() => { if (!isOnWorkflowPage) return [] - return filterAndSort(tools, (t) => t.searchValue ?? `${t.name} tool-${t.id}`, deferredSearch) + return filterAndSort(tools, (t) => t.searchValue ?? t.name, deferredSearch) }, [isOnWorkflowPage, tools, deferredSearch]) const filteredTriggers = useMemo(() => { if (!isOnWorkflowPage) return [] - return filterAndSort(triggers, (t) => `${t.name} trigger-${t.id}`, deferredSearch) + return filterAndSort(triggers, (t) => `${t.name} ${t.id}`, deferredSearch) }, [isOnWorkflowPage, triggers, deferredSearch]) const filteredToolOps = useMemo(() => { if (!isOnWorkflowPage) return [] - return filterAndSort( - toolOperations, - (op) => `${op.searchValue} operation-${op.id}`, - deferredSearch - ) + return filterAndSort(toolOperations, (op) => op.searchValue, deferredSearch) }, [isOnWorkflowPage, toolOperations, deferredSearch]) const filteredDocs = useMemo(() => { if (!isOnWorkflowPage) return [] - return filterAndSort(docs, (d) => `${d.name} docs documentation doc-${d.id}`, deferredSearch) + return filterAndSort(docs, (d) => `${d.name} docs documentation`, deferredSearch) }, [isOnWorkflowPage, docs, deferredSearch]) const filteredTables = useMemo( - () => filterAndSort(tables, (t) => `${t.name} table-${t.id}`, deferredSearch), + () => filterAndSort(tables, (t) => t.name, deferredSearch), [tables, deferredSearch] ) const filteredFiles = useMemo( - () => - filterAndSort( - files, - (f) => `${f.name} ${f.folderPath?.join(' / ') ?? ''} file-${f.id}`, - deferredSearch - ), + () => filterAndSort(files, (f) => `${f.name} ${f.folderPath?.join(' ') ?? ''}`, deferredSearch), [files, deferredSearch] ) const filteredKnowledgeBases = useMemo( - () => - filterAndSort(knowledgeBases, (kb) => `${kb.name} knowledge-base-${kb.id}`, deferredSearch), + () => filterAndSort(knowledgeBases, (kb) => kb.name, deferredSearch), [knowledgeBases, deferredSearch] ) const filteredWorkflows = useMemo( - () => filterAndSort(workflows, (w) => `${w.name} workflow-${w.id}`, deferredSearch), + () => + filterAndSort(workflows, (w) => `${w.name} ${w.folderPath?.join(' ') ?? ''}`, deferredSearch), [workflows, deferredSearch] ) const filteredChats = useMemo( - () => filterAndSort(chats, (t) => `${t.name} task-${t.id}`, deferredSearch), + () => filterAndSort(chats, (t) => t.name, deferredSearch), [chats, deferredSearch] ) const filteredWorkspaces = useMemo( - () => filterAndSort(workspaces, (w) => `${w.name} workspace-${w.id}`, deferredSearch), + () => filterAndSort(workspaces, (w) => w.name, deferredSearch), [workspaces, deferredSearch] ) const filteredPages = useMemo( - () => filterAndSort(pages, (p) => `${p.name} page-${p.id}`, deferredSearch), + () => filterAndSort(pages, (p) => p.name, deferredSearch), [pages, deferredSearch] ) /** Connected accounts: visible on the integrations page even with empty input. */ const filteredConnectedAccounts = useMemo(() => { if (!isOnIntegrationsPage) return [] - return filterAndSort( - connectedAccounts, - (a) => `${a.name} connected-account-${a.id}`, - deferredSearch - ) + return filterAndSort(connectedAccounts, (a) => a.name, deferredSearch) }, [isOnIntegrationsPage, connectedAccounts, deferredSearch]) /** Catalog integrations: only shown once the user has typed something. */ const filteredIntegrations = useMemo(() => { if (!isOnIntegrationsPage || !deferredSearch) return [] - return filterAndSort(integrations, (i) => `${i.name} integration-${i.id}`, deferredSearch) + return filterAndSort(integrations, (i) => i.name, deferredSearch) }, [isOnIntegrationsPage, deferredSearch, integrations]) if (!mounted) return null @@ -561,23 +693,77 @@ export function SearchModal({ No results found. + + + + + + + + + + + + + + - - - - - - - - - - - - - diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.test.ts new file mode 100644 index 0000000000..eb9175abd2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.test.ts @@ -0,0 +1,243 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { filterAndSort, fuzzyMatch } from './utils' + +/** + * The matcher that shipped before fuzzy matching was introduced. Re-implemented + * here verbatim so the new matcher can be proven a strict superset: anything the + * old matcher returned, the new one must still return. This is the core + * no-regression guarantee. + */ +function oldScoreMatch(value: string, search: string): number { + if (!search) return 1 + const v = value.toLowerCase() + const s = search.toLowerCase() + if (v === s) return 1 + if (v.startsWith(s)) return 0.9 + if (v.includes(s)) return 0.7 + const words = s.split(/\s+/).filter(Boolean) + if (words.length > 1 && words.every((w) => v.includes(w))) return 0.5 + return 0 +} + +function oldFilterAndSort(items: T[], toValue: (item: T) => string, search: string): T[] { + if (!search) return items + const scored: [T, number][] = [] + for (const item of items) { + const score = oldScoreMatch(toValue(item), search) + if (score > 0) scored.push([item, score]) + } + scored.sort((a, b) => b[1] - a[1]) + return scored.map(([item]) => item) +} + +interface Entry { + label: string + /** The string the modal actually searches against (name + type/id junk). */ + value: string +} + +/** Mirrors how groups build their `value` strings (name + slug/id suffix). */ +function block(label: string, slug: string): Entry { + return { label, value: `${label} ${slug} block-${slug}` } +} +function workflow(label: string, folder: string): Entry { + return { label, value: `${label} ${folder} workflow-${slugUuid(label)}` } +} +function action(label: string, keywords: string): Entry { + const id = label.toLowerCase().replace(/\s+/g, '-') + return { label, value: `${label} ${keywords} action-${id}` } +} +function slugUuid(label: string): string { + return `${label.toLowerCase().replace(/\s+/g, '')}-9f2a3b4c5d6e` +} + +const CORPUS: Entry[] = [ + block('Slack', 'slack'), + block('Gmail', 'gmail'), + block('Google Sheets', 'google_sheets'), + block('Google PageSpeed', 'google_pagespeed'), + block('GitHub', 'github'), + block('Notion', 'notion'), + block('Postgres', 'postgresql'), + block('OpenAI', 'openai'), + block('Airtable', 'airtable'), + block('HubSpot', 'hubspot'), + block('Linear', 'linear'), + block('Discord', 'discord'), + block('Microsoft Teams', 'microsoft_teams'), + block('Webhook', 'webhook'), + block('Schedule', 'schedule'), + block('Agent', 'agent'), + block('Function', 'function'), + block('Condition', 'condition'), + block('Router', 'router'), + block('Knowledge Base', 'knowledge'), + workflow('Customer Onboarding Flow', 'Sales'), + workflow('Daily Report', 'Ops'), + workflow('Lead Enrichment', 'Sales'), + action('Create workflow', 'new add build'), + action('Create folder', 'new add group'), + action('Import workflow', 'upload add'), + action('Toggle theme', 'dark light mode appearance color'), +] + +const toValue = (e: Entry) => e.value + +/** + * A broad sweep of realistic query shapes, grouped by intent: single chars, + * exact-ish names, prefixes, contains/mid-word, multi-word, initialisms and + * scattered (the new wins), typos, and genuine non-matches. + */ +const QUERIES = [ + 's', + 'g', + 'a', + 'w', + 'slack', + 'gmail', + 'github', + 'notion', + 'postgres', + 'openai', + 'agent', + 'goog', + 'micro', + 'know', + 'cond', + 'rout', + 'sched', + 'hook', + 'table', + 'spot', + 'mail', + 'google sheets', + 'sheets google', + 'create workflow', + 'workflow create', + 'customer onboarding', + 'slk', + 'gps', + 'msteams', + 'crwf', + 'cwf', + 'kb', + 'githb', + 'postgrs', + 'zzz', + 'qqqq', +] + +describe('fuzzyMatch / filterAndSort — no regression vs. old matcher', () => { + it('returns a strict superset of the old matcher for every query (never loses a result)', () => { + for (const query of QUERIES) { + const oldLabels = new Set(oldFilterAndSort(CORPUS, toValue, query).map((e) => e.label)) + const newLabels = new Set(filterAndSort(CORPUS, toValue, query).map((e) => e.label)) + for (const label of oldLabels) { + expect( + newLabels.has(label), + `query "${query}": new matcher dropped "${label}" that the old matcher returned` + ).toBe(true) + } + } + }) + + it('preserves the old #1 result for exact/prefix/contains queries (no top-rank regression)', () => { + const exactish = [ + 'slack', + 'gmail', + 'github', + 'notion', + 'postgres', + 'openai', + 'agent', + 'goog', + 'micro', + 'know', + 'cond', + 'rout', + 'sched', + ] + for (const query of exactish) { + const oldTop = oldFilterAndSort(CORPUS, toValue, query)[0] + const newTop = filterAndSort(CORPUS, toValue, query)[0] + if (oldTop) { + expect(newTop?.label, `query "${query}" top result changed`).toBe(oldTop.label) + } + } + }) +}) + +describe('fuzzyMatch — new wins (initialisms & scattered)', () => { + const wins: Array<[string, string]> = [ + ['slk', 'Slack'], + ['gps', 'Google PageSpeed'], + ['crwf', 'Create workflow'], + ['msteams', 'Microsoft Teams'], + ] + for (const [query, expectedTop] of wins) { + it(`"${query}" surfaces "${expectedTop}" as the top result`, () => { + const results = filterAndSort(CORPUS, toValue, query) + expect(results[0]?.label).toBe(expectedTop) + }) + } + + it('finds initialisms the old matcher missed entirely (old returns 0 for "slk")', () => { + expect(oldFilterAndSort(CORPUS, toValue, 'slk')).toHaveLength(0) + expect(filterAndSort(CORPUS, toValue, 'slk').map((e) => e.label)).toContain('Slack') + }) +}) + +describe('fuzzyMatch — noise control', () => { + it('rejects a mid-word scattered subsequence ("oge" in P-o-st-g-r-e-s is not a substring)', () => { + expect(fuzzyMatch('Postgres', 'oge').matched).toBe(false) + }) + + it('ranks every "g"-prefixed result above results that only contain "g" deeper', () => { + const results = filterAndSort(CORPUS, toValue, 'g') + const labels = results.map((e) => e.label) + const firstNonPrefix = labels.findIndex((l) => !l.toLowerCase().startsWith('g')) + const lastPrefix = labels.reduce((acc, l, i) => (l.toLowerCase().startsWith('g') ? i : acc), -1) + if (firstNonPrefix !== -1 && lastPrefix !== -1) { + expect(lastPrefix).toBeLessThan(firstNonPrefix) + } + }) + + it('returns no matches for genuine non-matches', () => { + expect(filterAndSort(CORPUS, toValue, 'zzz')).toHaveLength(0) + expect(filterAndSort(CORPUS, toValue, 'qqqq')).toHaveLength(0) + }) +}) + +describe('fuzzyMatch — positions for highlighting', () => { + it('reports prefix match positions', () => { + expect(fuzzyMatch('Slack', 'sla').positions).toEqual([0, 1, 2]) + }) + + it('reports scattered match positions for "slk" against "Slack" (S, l, k)', () => { + expect(fuzzyMatch('Slack', 'slk').positions).toEqual([0, 1, 4]) + }) + + it('highlights the substring itself, not an earlier scattered occurrence', () => { + const result = fuzzyMatch('a_apple', 'apple') + expect(result.matched).toBe(true) + expect(result.positions).toEqual([2, 3, 4, 5, 6]) + }) + + it('highlights a mid-string substring at its real position', () => { + expect(fuzzyMatch('Webhook', 'hook').positions).toEqual([3, 4, 5, 6]) + }) + + it('reports empty positions for empty query', () => { + const result = fuzzyMatch('Slack', '') + expect(result.matched).toBe(true) + expect(result.positions).toEqual([]) + }) + + it('matches multi-word tokens order-independently', () => { + const result = fuzzyMatch('Slack Send Message', 'message slack') + expect(result.matched).toBe(true) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts index 5baacf36d8..7970294e93 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts @@ -1,4 +1,4 @@ -import type { ComponentType, ReactNode } from 'react' +import type { ComponentType } from 'react' export interface IntegrationSearchItem { id: string @@ -46,6 +46,26 @@ export interface FileItem { folderPath?: string[] } +/** Where an {@link ActionItem} (a verb) is available. */ +export type ActionContext = 'global' | 'workflow' | 'integrations' + +/** + * An action is a verb the palette can run directly (create, import, toggle), + * as opposed to an entity the user navigates to. Actions render at the top of + * the result list so the most common "do something" intents are one keystroke + * away. + */ +export interface ActionItem { + id: string + name: string + /** Extra terms folded into the search value (e.g. "new add"). */ + keywords?: string + icon: ComponentType<{ className?: string }> + shortcut?: string + context: ActionContext + run: () => void +} + export interface SearchModalProps { open: boolean onOpenChange: (open: boolean) => void @@ -59,6 +79,10 @@ export interface SearchModalProps { connectedAccounts?: IntegrationSearchItem[] isOnWorkflowPage?: boolean isOnIntegrationsPage?: boolean + canEdit?: boolean + onCreateWorkflow?: () => void + onCreateFolder?: () => void + onImportWorkflow?: () => void } export interface CommandItemProps { @@ -67,7 +91,10 @@ export interface CommandItemProps { icon: ComponentType<{ className?: string }> bgColor: string showColoredIcon?: boolean - children: ReactNode + /** Primary text. Matched characters are highlighted against {@link query}. */ + label: string + /** Active search query, used to bold matched characters. */ + query?: string } export const GROUP_HEADING_CLASSNAME = @@ -76,30 +103,157 @@ export const GROUP_HEADING_CLASSNAME = export const COMMAND_ITEM_CLASSNAME = 'group mx-0.5 flex h-[30px] w-full cursor-pointer items-center gap-2 rounded-lg border border-transparent px-2 text-left text-sm aria-selected:border-[var(--border-1)] aria-selected:bg-[var(--surface-active)] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50' -function scoreMatch(value: string, search: string): number { - if (!search) return 1 - const valueLower = value.toLowerCase() - const searchLower = search.toLowerCase() +/** Characters that begin a new word — a match here scores higher. */ +const SEPARATORS = new Set([' ', '-', '_', '/', '.', ':', '(', ')']) + +/** Result of matching a query against a single candidate string. */ +export interface FuzzyResult { + /** Whether every query character was found, in order. */ + matched: boolean + /** Relative ranking score; higher sorts first. Only meaningful when matched. */ + score: number + /** Indices into the candidate string that matched, ascending. Read-only. */ + positions: readonly number[] +} + +/** + * Shared singleton for the no-match case. The frozen empty array makes the + * read-only contract explicit and guarantees the shared instance can never be + * mutated by a caller. + */ +const NO_MATCH: FuzzyResult = { matched: false, score: 0, positions: Object.freeze([]) } + +function isCamelBoundary(text: string, index: number): boolean { + if (index === 0) return false + const prev = text[index - 1] + const curr = text[index] + return prev === prev.toLowerCase() && curr !== curr.toLowerCase() && curr === curr.toUpperCase() +} + +/** + * A "hard" boundary: the start of the string or immediately after a separator. + * Used to anchor scattered matches. Deliberately excludes camelCase so a fuzzy + * match cannot *start* in the middle of a word (e.g. the `S` in "PageSpeed"), + * which would let short queries scatter-match unrelated items. Interior + * camelCase still earns a scoring bonus — it just cannot anchor a match. + */ +function isHardBoundary(lowerText: string, index: number): boolean { + return index === 0 || SEPARATORS.has(lowerText[index - 1]) +} + +/** + * Order-independent fallback: a multi-word query matches when every token + * appears somewhere in the text. Preserves the original matcher's multi-word + * behavior (`message slack` → "Slack Send Message"). Single-word queries that + * reach here did not match as exact/prefix/contains and are rejected, so this + * never broadens single-token matching beyond the original behavior. + */ +function tokenFallback(lowerText: string, lowerQuery: string): FuzzyResult { + const tokens = lowerQuery.split(/\s+/).filter(Boolean) + if (tokens.length <= 1 || !tokens.every((token) => lowerText.includes(token))) return NO_MATCH - if (valueLower === searchLower) return 1 - if (valueLower.startsWith(searchLower)) return 0.9 - if (valueLower.includes(searchLower)) return 0.7 + const tokenPositions = new Set() + for (const token of tokens) { + const start = lowerText.indexOf(token) + for (let k = 0; k < token.length; k++) tokenPositions.add(start + k) + } + return { + matched: true, + score: 10 - lowerText.length * 0.1, + positions: Array.from(tokenPositions).sort((a, b) => a - b), + } +} + +/** + * Subsequence fuzzy match with positional scoring. Rewards matches at word + * boundaries (`slk` → **S**lack), consecutive runs, and prefix/exact hits, + * while still matching scattered characters so typos and partial recall work. + * + * Exact, prefix, contains, and multi-word token matches all reproduce the + * original substring matcher's behavior, making this a strict superset: any + * result the old matcher returned, this one returns too. The only additions are + * scattered subsequences, and those are accepted only when the match STARTS at a + * hard word boundary — so initialisms match (`slk` → **S**la**c**k) but loose + * noise does not (`slack` will not scatter-match "Page**S**peed", and `se` will + * not match every item containing s…e). + * + * Falls back to order-independent token matching for multi-word queries + * (`message slack` matches "Slack Send Message") which a strict left-to-right + * subsequence would miss. + * + * Contiguous substring matches report the indices of the substring itself, so + * highlighting always bolds the run the user actually matched rather than an + * earlier scattered occurrence of the same characters. + */ +export function fuzzyMatch(text: string, query: string): FuzzyResult { + if (!query) return { matched: true, score: 1, positions: [] } + if (!text) return NO_MATCH + + const lowerText = text.toLowerCase() + const lowerQuery = query.toLowerCase() + + const substringIndex = lowerText.indexOf(lowerQuery) + if (substringIndex !== -1) { + const length = lowerQuery.length + const positions = Array.from({ length }, (_, k) => substringIndex + k) + + let score = 1 + if (substringIndex === 0) score += 10 + else if (SEPARATORS.has(lowerText[substringIndex - 1])) score += 8 + else if (isCamelBoundary(text, substringIndex)) score += 6 + score += (length - 1) * 6 + + if (lowerText === lowerQuery) score += 120 + else if (substringIndex === 0) score += 50 + else score += 25 + + score -= substringIndex * 0.5 + score -= (length - 1) * 0.15 + score -= lowerText.length * 0.1 + return { matched: true, score, positions } + } + + const positions: number[] = [] + let queryIndex = 0 + let score = 0 + let prevMatch = -2 + + for (let i = 0; i < lowerText.length && queryIndex < lowerQuery.length; i++) { + if (lowerText[i] !== lowerQuery[queryIndex]) continue + + let charScore = 1 + if (i === 0) charScore += 10 + else if (SEPARATORS.has(lowerText[i - 1])) charScore += 8 + else if (isCamelBoundary(text, i)) charScore += 6 + if (prevMatch === i - 1) charScore += 5 + + score += charScore + positions.push(i) + prevMatch = i + queryIndex++ + } - const words = searchLower.split(/\s+/).filter(Boolean) - if (words.length > 1) { - if (words.every((w) => valueLower.includes(w))) return 0.5 + if (queryIndex === lowerQuery.length && isHardBoundary(lowerText, positions[0])) { + score -= positions[0] * 0.5 + score -= (positions[positions.length - 1] - positions[0]) * 0.15 + score -= lowerText.length * 0.1 + return { matched: true, score, positions } } - return 0 + return tokenFallback(lowerText, lowerQuery) } +/** + * Filters items whose value fuzzy-matches the search, ordered by descending + * score. Returns the input untouched when the search is empty. + */ export function filterAndSort(items: T[], toValue: (item: T) => string, search: string): T[] { if (!search) return items - const scored: [T, number][] = [] + const scored: Array<{ item: T; score: number }> = [] for (const item of items) { - const s = scoreMatch(toValue(item), search) - if (s > 0) scored.push([item, s]) + const { matched, score } = fuzzyMatch(toValue(item), search) + if (matched) scored.push({ item, score }) } - scored.sort((a, b) => b[1] - a[1]) - return scored.map(([item]) => item) + scored.sort((a, b) => b.score - a.score) + return scored.map((entry) => entry.item) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 785e2120e5..323827c7fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1735,6 +1735,10 @@ export const Sidebar = memo(function Sidebar() { connectedAccounts={searchModalConnectedAccounts} isOnWorkflowPage={!!workflowId} isOnIntegrationsPage={isOnIntegrationsPage} + canEdit={canEdit} + onCreateWorkflow={handleCreateWorkflow} + onCreateFolder={handleCreateFolder} + onImportWorkflow={handleImportWorkflow} /> ()( icon: block.icon, bgColor: block.bgColor || '#6B7280', type: block.type, - searchValue: `${block.name} ${block.type} block-${block.type} ${buildCommandSearchableOptionSearchValue(block)}`, + searchValue: `${block.name} ${block.type} ${buildCommandSearchableOptionSearchValue(block)}`, } if (block.category === 'blocks' && block.type !== 'starter') { From 9e9f2b9e1ff822e32e450b9b2fa6ece9bff127f6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 11:12:41 -0700 Subject: [PATCH 13/26] fix(realtime): debounce the reconnecting toast to stop transient-blip flashes (#5111) * fix(realtime): debounce the reconnecting toast to stop transient-blip flashes The "Reconnecting..." persistent toast fired the instant isReconnecting flipped true, so sub-second transport blips that self-heal on the first retry flashed a scary alert. Add useStableFlag, an anti-flicker boolean that delays the rising edge (2s, so brief blips never surface) and holds the falling edge (1.5s min visible, so a drop just past the delay does not flash-and-vanish). The socket flag stays accurate; only the user-facing alarm is smoothed. State machine extracted into a framework-agnostic controller with unit coverage for both flicker modes. * fix(realtime): reset stable-flag React state on options change; de-vacuous blip test Address Greptile review: - useStableFlag: reset React state to the fresh controller's baseline when the controller is recreated on an options change, so a dynamic consumer changing delayMs/minVisibleMs while active with value already false can no longer strand the flag at true. - test: read the live probe.active getter in the blip test instead of a destructured snapshot, which was bound to false at destructure time and made the assertion vacuous. --- .../workspace-permissions-provider.tsx | 18 +- apps/sim/hooks/use-stable-flag.test.ts | 163 ++++++++++++++++++ apps/sim/hooks/use-stable-flag.ts | 129 ++++++++++++++ 3 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 apps/sim/hooks/use-stable-flag.test.ts create mode 100644 apps/sim/hooks/use-stable-flag.ts diff --git a/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx index 56a4c8ad53..b6bcecf405 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx @@ -12,11 +12,23 @@ import { type WorkspacePermissions, workspaceKeys, } from '@/hooks/queries/workspace' +import { useStableFlag } from '@/hooks/use-stable-flag' import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions' import { useOperationQueueStore } from '@/stores/operation-queue/store' const logger = createLogger('WorkspacePermissionsProvider') +/** + * Anti-flicker timing for the "Reconnecting..." toast. Socket.IO flips + * `isReconnecting` on any disconnect — including sub-second transport hiccups + * that recover on the first retry — so we delay surfacing the toast until the + * drop has lasted long enough to matter, then hold it on screen long enough to + * read. Together these suppress both flicker modes (flash-on and flash-off) + * while still alerting on real outages. + */ +const RECONNECTING_TOAST_DELAY_MS = 2000 +const RECONNECTING_TOAST_MIN_VISIBLE_MS = 1500 + interface PersistentToastOptions { description?: string action?: { label: string; onClick: () => void } @@ -115,9 +127,13 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP const isOfflineMode = hasOperationError const isJoinBlocked = Boolean(blockedJoinWorkflowId) && blockedJoinWorkflowId === urlWorkflowId + const showReconnecting = useStableFlag(isReconnecting, { + delayMs: RECONNECTING_TOAST_DELAY_MS, + minVisibleMs: RECONNECTING_TOAST_MIN_VISIBLE_MS, + }) const realtimeStatusMessage = isOfflineMode ? null - : isReconnecting + : showReconnecting ? 'Reconnecting...' : isRetryingWorkflowJoin ? 'Joining workflow...' diff --git a/apps/sim/hooks/use-stable-flag.test.ts b/apps/sim/hooks/use-stable-flag.test.ts new file mode 100644 index 0000000000..f4d90503e0 --- /dev/null +++ b/apps/sim/hooks/use-stable-flag.test.ts @@ -0,0 +1,163 @@ +/** + * @vitest-environment node + * + * Tests for the `createStableFlagController` state machine behind `useStableFlag`. + * The controller is framework-agnostic so the anti-flicker timing can be driven + * with fake timers and no DOM; the thin React wrapper is covered by manual QA. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createStableFlagController } from '@/hooks/use-stable-flag' + +const DELAY_MS = 2000 +const MIN_VISIBLE_MS = 1500 + +function setup(options = { delayMs: DELAY_MS, minVisibleMs: MIN_VISIBLE_MS }) { + const states: boolean[] = [] + let active = false + const controller = createStableFlagController((next) => { + active = next + states.push(next) + }, options) + return { + controller, + states, + get active() { + return active + }, + } +} + +describe('createStableFlagController', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + }) + + it('suppresses a blip that heals before the delay (flash-on)', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS - 1) + probe.controller.setValue(false) + vi.advanceTimersByTime(10_000) + + expect(probe.states).toEqual([]) + expect(probe.active).toBe(false) + }) + + it('does not turn on one tick before the delay boundary', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS - 1) + + expect(probe.active).toBe(false) + expect(probe.states).toEqual([]) + }) + + it('turns on exactly at the delay boundary', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS) + + expect(probe.states).toEqual([true]) + expect(probe.active).toBe(true) + }) + + it('holds the flag on for the minimum-visible window (flash-off)', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS) // shown + expect(probe.active).toBe(true) + + // Value clears almost immediately after showing. + vi.advanceTimersByTime(100) + probe.controller.setValue(false) + expect(probe.active).toBe(true) // still held + + vi.advanceTimersByTime(MIN_VISIBLE_MS - 100 - 1) + expect(probe.active).toBe(true) + + vi.advanceTimersByTime(1) + expect(probe.active).toBe(false) + expect(probe.states).toEqual([true, false]) + }) + + it('clears immediately when the value has already been visible past the minimum', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS) + vi.advanceTimersByTime(MIN_VISIBLE_MS + 500) // well past the floor + probe.controller.setValue(false) + + expect(probe.active).toBe(false) + expect(probe.states).toEqual([true, false]) + }) + + it('keeps the flag on through a flap while held, without re-delaying', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS) // shown + probe.controller.setValue(false) // schedules hide + vi.advanceTimersByTime(500) + probe.controller.setValue(true) // reconnect flaps back before hide fires + + vi.advanceTimersByTime(10_000) + expect(probe.active).toBe(true) + expect(probe.states).toEqual([true]) + }) + + it('is idempotent on repeated setValue(true) — schedules a single show', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(500) + probe.controller.setValue(true) + probe.controller.setValue(true) + + vi.advanceTimersByTime(DELAY_MS - 500) + expect(probe.states).toEqual([true]) // exactly one transition, fired at the original deadline + }) + + it('dispose cancels a pending show', () => { + const probe = setup() + + probe.controller.setValue(true) + probe.controller.dispose() + vi.advanceTimersByTime(10_000) + + expect(probe.states).toEqual([]) + }) + + it('dispose cancels a pending hide', () => { + const probe = setup() + + probe.controller.setValue(true) + vi.advanceTimersByTime(DELAY_MS) + probe.controller.setValue(false) // schedules hide within min-visible window + probe.controller.dispose() + vi.advanceTimersByTime(10_000) + + expect(probe.states).toEqual([true]) // hide never fired + }) + + it('with zero options, mirrors the value on the next tick', () => { + const probe = setup({ delayMs: 0, minVisibleMs: 0 }) + + probe.controller.setValue(true) + vi.advanceTimersByTime(0) + expect(probe.active).toBe(true) + + probe.controller.setValue(false) + expect(probe.active).toBe(false) + expect(probe.states).toEqual([true, false]) + }) +}) diff --git a/apps/sim/hooks/use-stable-flag.ts b/apps/sim/hooks/use-stable-flag.ts new file mode 100644 index 0000000000..2fcd704f8b --- /dev/null +++ b/apps/sim/hooks/use-stable-flag.ts @@ -0,0 +1,129 @@ +import { useEffect, useRef, useState } from 'react' + +export interface StableFlagOptions { + /** + * Time `value` must stay continuously true before the flag turns on. Suppresses + * brief flashes for blips that heal within the window. Defaults to `0` (no delay). + */ + delayMs?: number + /** + * Minimum time the flag stays on once shown, even if `value` clears immediately + * after. Prevents a flash-and-vanish when `value` is true just past `delayMs`. + * Defaults to `0` (clears as soon as `value` does). + */ + minVisibleMs?: number +} + +/** + * Framework-agnostic state machine behind {@link useStableFlag}. Extracted so the + * anti-flicker timing can be unit-tested with fake timers without a DOM. Relies on + * the ambient `setTimeout`/`clearTimeout`/`Date.now`, which fake timers replace. + * + * `onChange` fires whenever the smoothed flag flips. `setValue` is idempotent — it + * is safe to feed it the same value repeatedly (e.g. from React effect re-runs). + */ +export function createStableFlagController( + onChange: (active: boolean) => void, + { delayMs = 0, minVisibleMs = 0 }: StableFlagOptions = {} +) { + let active = false + let shownAt: number | null = null + let showTimer: ReturnType | null = null + let hideTimer: ReturnType | null = null + + const clearShow = () => { + if (showTimer !== null) { + clearTimeout(showTimer) + showTimer = null + } + } + const clearHide = () => { + if (hideTimer !== null) { + clearTimeout(hideTimer) + hideTimer = null + } + } + + const show = () => { + showTimer = null + shownAt = Date.now() + active = true + onChange(true) + } + const hide = () => { + hideTimer = null + shownAt = null + active = false + onChange(false) + } + + return { + setValue(value: boolean) { + if (value) { + clearHide() + if (active || showTimer !== null) { + return + } + showTimer = setTimeout(show, delayMs) + return + } + + clearShow() + if (!active || hideTimer !== null) { + return + } + + const elapsed = shownAt === null ? minVisibleMs : Date.now() - shownAt + const remaining = minVisibleMs - elapsed + if (remaining <= 0) { + hide() + return + } + hideTimer = setTimeout(hide, remaining) + }, + dispose() { + clearShow() + clearHide() + }, + } +} + +/** + * Anti-flicker boolean. Mirrors `value` but smooths both edges so transient + * toggles never produce a visible flash: + * + * - Rising edge — `value` must hold true for `delayMs` before the flag turns on. + * - Falling edge — once on, the flag stays on for at least `minVisibleMs`. + * + * With both options at `0` it returns `value` unchanged (after a tick). Useful for + * connection/loading indicators that would otherwise flicker on sub-second changes. + */ +export function useStableFlag(value: boolean, options: StableFlagOptions = {}): boolean { + const [active, setActive] = useState(false) + const { delayMs = 0, minVisibleMs = 0 } = options + const valueRef = useRef(value) + valueRef.current = value + const controllerRef = useRef | null>(null) + + useEffect(() => { + // Reset to the fresh controller's baseline. Without this, recreating the + // controller on an options change while `active` is true and `value` is + // already false would strand the React state at true — the new controller + // starts internally false, so its `setValue(false)` early-returns and never + // emits `onChange(false)`. + setActive(false) + const controller = createStableFlagController(setActive, { delayMs, minVisibleMs }) + controllerRef.current = controller + controller.setValue(valueRef.current) + return () => { + controller.dispose() + controllerRef.current = null + } + }, [delayMs, minVisibleMs]) + + useEffect(() => { + controllerRef.current?.setValue(value) + }, [value]) + + return active +} From d7fd0405f9168956fefdbd61e74fb09c6287945e Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 11:52:57 -0700 Subject: [PATCH 14/26] improvement(search): align cmd+k action icons + highlight with the design system (#5114) * improvement(search): align cmd+k action icons + highlight with the design system - Each Actions verb now uses the exact icon from its real location: Fit to view -> Scan (workflow-controls), Copy workflow link -> Duplicate (nav context menu), Invite teammates -> User (settings teammates nav). Run/Create/Import already matched. - Remove the Toggle theme action and its now-dead useTheme wiring. - Matched-text highlight now uses the design-system search tokens (--highlight-match-bg / --highlight-match-text), matching the SearchHighlight component used in knowledge-base and code search, instead of an ad-hoc font-semibold. * improvement(search): use font-medium for matched-text emphasis in cmd+k Drop the colored background highlight in favor of the design system's standard emphasis weight (font-medium, used by Button/Label/Input/Table). Lighter than the previous semibold and avoids a background, keeping the palette's clean, undecorated text style. --- .../command-items/command-items.tsx | 2 +- .../components/search-modal/search-modal.tsx | 28 ++++++------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx index ce60b91fd6..2c795057f7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx @@ -42,7 +42,7 @@ export const HighlightedText = memo( <> {buildSegments(text, positions).map((segment, index) => segment.hit ? ( - + {segment.text} ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 9332b44a3a..96fe6c5ad3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -3,28 +3,26 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { Command } from 'cmdk' +import { Scan } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' -import { useTheme } from 'next-themes' import { usePostHog } from 'posthog-js/react' import { createPortal } from 'react-dom' import { Library } from '@/components/emcn' import { Calendar, Database, - Expand, + Duplicate, File, FolderPlus, HelpCircle, Home, Integration, - Link, - Palette, Play, Plus, Settings, Table, Upload, - Users, + User, } from '@/components/emcn/icons' import { Search } from '@/components/emcn/icons/search' import { cn } from '@/lib/core/utils/cn' @@ -102,7 +100,6 @@ export function SearchModal({ const [mounted, setMounted] = useState(false) const { navigateToSettings } = useSettingsNavigation() const { config: permissionConfig } = usePermissionConfig() - const { resolvedTheme, setTheme } = useTheme() const invokeCommand = useInvokeGlobalCommand() const posthog = usePostHog() @@ -201,7 +198,8 @@ export function SearchModal({ /** * Verbs the palette can run directly. Entity navigation lives in the groups - * below; this list is for "do something" intents (create, import, toggle). + * below; this list is for "do something" intents (run, create, import, copy, + * invite). */ const actions = useMemo((): ActionItem[] => { const list: ActionItem[] = [] @@ -248,7 +246,7 @@ export function SearchModal({ id: 'fit-to-view', name: 'Fit workflow to view', keywords: 'zoom center recenter canvas reset', - icon: Expand, + icon: Scan, shortcut: '⌘⇧F', context: 'workflow', run: () => invokeCommand('fit-to-view'), @@ -257,7 +255,7 @@ export function SearchModal({ id: 'copy-workflow-url', name: 'Copy workflow link', keywords: 'url share clipboard', - icon: Link, + icon: Duplicate, context: 'workflow', run: () => { navigator.clipboard.writeText(window.location.href).catch((error) => { @@ -269,18 +267,10 @@ export function SearchModal({ id: 'invite-teammates', name: 'Invite teammates', keywords: 'members people add user organization', - icon: Users, + icon: User, context: 'global', run: () => navigateToSettings({ section: 'teammates' }), }) - list.push({ - id: 'toggle-theme', - name: 'Toggle theme', - keywords: 'dark light mode appearance color', - icon: Palette, - context: 'global', - run: () => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'), - }) return list }, [ canEdit, @@ -289,8 +279,6 @@ export function SearchModal({ onImportWorkflow, invokeCommand, navigateToSettings, - resolvedTheme, - setTheme, ]) const [search, setSearch] = useState('') From 11e23131fe791969399ab7d903af7ee6100c5bf7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 12:15:48 -0700 Subject: [PATCH 15/26] feat(google): Maps Pollen/Solar, Custom Search expansion, and live-API fixes across Google integrations (#5113) * feat(google): add Maps Pollen/Solar, expand Custom Search, fix Ads/Groups/Contacts/Slides New capability: - Google Maps: add Pollen Forecast and Solar Potential tools (API-key, google_cloud BYOK) - Google Custom Search: add start/dateRestrict/fileType/safe/searchType/siteSearch/ siteSearchFilter/lr/gl/sort params, htmlTitle/htmlSnippet/formattedUrl/mime/fileFormat/ cacheId/image result fields, and nextPageStartIndex pagination Fixes (validated against live API docs): - Google Ads: bump all tools from sunset v19 to v24 - Google Groups: forward OAuth credential under oauthCredential (was dropping token in 11 ops), forward all update_settings fields, JSON.stringify update_settings/add_alias bodies - Google Contacts: include required metadata.sources[].etag in updateContact body (fixed 400) - Google Slides: remove unsupported GIF thumbnail mimeType (API only allows PNG) - Google Sheets: wire delete_rows/delete_sheet/delete_spreadsheet into the V2 block - Google Custom Search: throw on API error responses instead of returning empty success; num optional + Number-coerced; pagemap typed unknown * docs(google): regenerate integration docs for new and updated operations * fix(google_maps): correct Solar requiredQuality enum to BASE The Solar API ImageryQuality enum is HIGH/MEDIUM/BASE (+ UNSPECIFIED) per the live docs; there is no LOW. Selecting "Low" sent requiredQuality=LOW which the API rejects as INVALID_ARGUMENT, and the valid BASE tier was unreachable. Replace LOW with BASE in the tool param/output descriptions, the type union, and the block dropdown. * fix(google_maps): guard !response.ok in Pollen/Solar; use ?? for color channels Address Greptile review: - Pollen and Solar transformResponse now check !response.ok || data.error (matches the Custom Search fix); a gateway error without an error key in the body no longer returns empty/zeroed output silently. - Pollen color channels use ?? instead of || so a legitimate 0 isn't treated as missing (consistent with the other numeric fields in the file). * fix(google_maps): guard against NaN days in Pollen forecast Address Cursor Bugbot: a non-numeric `days` input parsed to NaN and was forwarded as `days=NaN` (the tool's `?? 1` only catches undefined, not NaN), breaking the forecast call. The block now coerces invalid input to undefined, and the tool defaults to 1 unless `days` is a finite number. * fix(google): clamp Pollen days to 1-5; stop forwarding stale group settings fields Address Cursor Bugbot: - Pollen: clamp days to the documented 1-5 range (truncating fractionals) so 0, negatives, or >5 can't be sent to the API. - Google Groups update_settings: the block has no dedicated settings subblocks, so forwarding name/description from params could leak stale values from create_group/update_group and unintentionally rename the group. Forward only oauthCredential + groupEmail from the block (the tool's own param schema still exposes the settings fields for the agent path). * fix(google_sheets): fail fast on non-numeric delete indices Address Cursor Bugbot: delete_sheet/delete_rows parsed deleteSheetId/startIndex/ endIndex with Number.parseInt but didn't validate, so non-numeric UI input became NaN and was forwarded (the v2 delete tools only reject null/undefined), breaking the batchUpdate. The block now throws a clear error when any of these is not a valid number. * fix(google_search): clamp num to 1-10 and normalize start Address Cursor Bugbot: num was coerced with Number() but not bounded, so values like 11 or fractionals reached the API and failed. The tool now truncates and clamps num to the documented 1-10 range and only sends a positive integer start, ignoring non-numeric/out-of-range input. --- .../docs/en/integrations/google_groups.mdx | 4 +- .../docs/en/integrations/google_maps.mdx | 67 +++++ .../docs/en/integrations/google_search.mdx | 29 +- .../docs/en/integrations/google_sheets.mdx | 62 +++++ .../docs/en/integrations/google_slides.mdx | 2 +- apps/sim/blocks/blocks/google.ts | 109 +++++++- apps/sim/blocks/blocks/google_groups.ts | 20 +- apps/sim/blocks/blocks/google_maps.ts | 83 +++++- apps/sim/blocks/blocks/google_sheets.ts | 133 +++++++++- apps/sim/blocks/blocks/google_slides.ts | 7 +- apps/sim/lib/integrations/integrations.json | 24 +- apps/sim/tools/google/search.ts | 111 +++++++- apps/sim/tools/google/types.ts | 64 ++++- apps/sim/tools/google_ads/ad_performance.ts | 2 +- .../tools/google_ads/campaign_performance.ts | 2 +- apps/sim/tools/google_ads/list_ad_groups.ts | 2 +- apps/sim/tools/google_ads/list_campaigns.ts | 2 +- apps/sim/tools/google_ads/list_customers.ts | 2 +- apps/sim/tools/google_ads/search.ts | 2 +- apps/sim/tools/google_contacts/update.ts | 1 + apps/sim/tools/google_groups/add_alias.ts | 7 +- .../tools/google_groups/update_settings.ts | 6 +- apps/sim/tools/google_maps/index.ts | 4 + apps/sim/tools/google_maps/pollen.ts | 249 ++++++++++++++++++ apps/sim/tools/google_maps/solar.ts | 158 +++++++++++ apps/sim/tools/google_maps/types.ts | 123 +++++++++ apps/sim/tools/google_slides/get_thumbnail.ts | 4 +- apps/sim/tools/registry.ts | 4 + 28 files changed, 1224 insertions(+), 59 deletions(-) create mode 100644 apps/sim/tools/google_maps/pollen.ts create mode 100644 apps/sim/tools/google_maps/solar.ts diff --git a/apps/docs/content/docs/en/integrations/google_groups.mdx b/apps/docs/content/docs/en/integrations/google_groups.mdx index 1e49d5fc2a..799be5a29e 100644 --- a/apps/docs/content/docs/en/integrations/google_groups.mdx +++ b/apps/docs/content/docs/en/integrations/google_groups.mdx @@ -352,7 +352,7 @@ Update the settings for a Google Group including access permissions, moderation, | `description` | string | No | The group description \(max 4096 characters\) | | `whoCanJoin` | string | No | Who can join: ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN | | `whoCanViewMembership` | string | No | Who can view membership: ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW | -| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW | +| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW, ALL_OWNERS_CAN_VIEW | | `whoCanPostMessage` | string | No | Who can post: NONE_CAN_POST, ALL_MANAGERS_CAN_POST, ALL_MEMBERS_CAN_POST, ALL_OWNERS_CAN_POST, ALL_IN_DOMAIN_CAN_POST, ANYONE_CAN_POST | | `allowExternalMembers` | string | No | Whether external users can be members: true or false | | `allowWebPosting` | string | No | Whether web posting is allowed: true or false | @@ -373,7 +373,7 @@ Update the settings for a Google Group including access permissions, moderation, | `whoCanContactOwner` | string | No | Who can contact owner: ALL_IN_DOMAIN_CAN_CONTACT, ALL_MANAGERS_CAN_CONTACT, ALL_MEMBERS_CAN_CONTACT, ANYONE_CAN_CONTACT | | `favoriteRepliesOnTop` | string | No | Whether favorite replies appear at top: true or false | | `whoCanApproveMembers` | string | No | Who can approve members: ALL_OWNERS_CAN_APPROVE, ALL_MANAGERS_CAN_APPROVE, ALL_MEMBERS_CAN_APPROVE, NONE_CAN_APPROVE | -| `whoCanBanUsers` | string | No | Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE | +| `whoCanBanUsers` | string | No | Who can ban users: ALL_MEMBERS, OWNERS_AND_MANAGERS, OWNERS_ONLY, NONE | | `whoCanModerateMembers` | string | No | Who can manage members: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | | `whoCanModerateContent` | string | No | Who can moderate content: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | | `whoCanAssistContent` | string | No | Who can assist with content metadata: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | diff --git a/apps/docs/content/docs/en/integrations/google_maps.mdx b/apps/docs/content/docs/en/integrations/google_maps.mdx index 3fcc90c3d6..0531162db9 100644 --- a/apps/docs/content/docs/en/integrations/google_maps.mdx +++ b/apps/docs/content/docs/en/integrations/google_maps.mdx @@ -326,6 +326,43 @@ Search for places using a text query | ↳ `businessStatus` | string | Business status | | `nextPageToken` | string | Token for fetching the next page of results | +### `google_maps_pollen` + +Get a daily pollen forecast (grass, tree, weed) for a location + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Maps API key with Pollen API enabled | +| `lat` | number | Yes | Latitude coordinate | +| `lng` | number | Yes | Longitude coordinate | +| `days` | number | No | Number of forecast days to return \(1-5, defaults to 1\) | +| `languageCode` | string | No | Language code for the response \(e.g., "en", "es"\) | +| `plantsDescription` | boolean | No | Include detailed plant descriptions \(defaults to true\) | +| `pricing` | per_request | No | No description | +| `rateLimit` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `regionCode` | string | Region code \(ISO 3166-1 alpha-2\) for the location | +| `dailyInfo` | array | Daily pollen forecast entries | +| ↳ `date` | object | Calendar date of the forecast entry | +| ↳ `pollenTypeInfo` | array | Pollen type indices \(grass, tree, weed\) | +| ↳ `code` | string | Pollen type code \(GRASS, TREE, WEED\) | +| ↳ `displayName` | string | Display name | +| ↳ `inSeason` | boolean | Whether the pollen type is in season | +| ↳ `indexInfo` | object | Universal Pollen Index \(UPI\) info | +| ↳ `healthRecommendations` | array | Health recommendations | +| ↳ `plantInfo` | array | Per-plant forecast with descriptions | +| ↳ `code` | string | Plant code \(e.g., BIRCH, RAGWEED\) | +| ↳ `displayName` | string | Display name | +| ↳ `inSeason` | boolean | Whether the plant is in season | +| ↳ `indexInfo` | object | Universal Pollen Index \(UPI\) info | +| ↳ `plantDescription` | object | Plant details \(type, family, season, cross-reactions\) | + ### `google_maps_reverse_geocode` Convert geographic coordinates (latitude and longitude) into a human-readable address @@ -379,6 +416,36 @@ Snap GPS coordinates to the nearest road segment | ↳ `placeId` | string | Place ID for this road segment | | `warningMessage` | string | Warning message if any \(e.g., if points could not be snapped\) | +### `google_maps_solar` + +Get solar potential and panel insights for the building nearest a location + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Maps API key with Solar API enabled | +| `lat` | number | Yes | Latitude coordinate | +| `lng` | number | Yes | Longitude coordinate | +| `requiredQuality` | string | No | Minimum imagery quality to accept \(HIGH, MEDIUM, or BASE\) | +| `pricing` | per_request | No | No description | +| `rateLimit` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `name` | string | Resource name of the building \(e.g., "buildings/ChIJ..."\) | +| `center` | object | Center coordinate of the building | +| ↳ `lat` | number | Latitude | +| ↳ `lng` | number | Longitude | +| `imageryDate` | object | Date the underlying imagery was captured | +| `imageryQuality` | string | Quality of the imagery used \(HIGH, MEDIUM, BASE\) | +| `regionCode` | string | Region code \(ISO 3166-1 alpha-2\) for the building | +| `postalCode` | string | Postal code of the building | +| `administrativeArea` | string | Administrative area \(e.g., state or province\) | +| `solarPotential` | object | Solar potential: max panel count/area, sunshine hours, carbon offset, panel specs, and configs | + ### `google_maps_speed_limits` Get speed limits for road segments. Requires either path coordinates or placeIds. diff --git a/apps/docs/content/docs/en/integrations/google_search.mdx b/apps/docs/content/docs/en/integrations/google_search.mdx index 94c9b59991..4bfd9acf38 100644 --- a/apps/docs/content/docs/en/integrations/google_search.mdx +++ b/apps/docs/content/docs/en/integrations/google_search.mdx @@ -39,7 +39,17 @@ Search the web with the Custom Search API | --------- | ---- | -------- | ----------- | | `query` | string | Yes | The search query to execute | | `searchEngineId` | string | Yes | Custom Search Engine ID | -| `num` | string | No | Number of results to return \(default: 10, max: 10\) | +| `num` | string | No | Number of results to return \(1-10, default 10\) | +| `start` | number | No | Index of the first result \(1-based, for pagination; start + num must be <= 100\) | +| `dateRestrict` | string | No | Restrict results by recency: d\[n\] days, w\[n\] weeks, m\[n\] months, y\[n\] years | +| `fileType` | string | No | Restrict to a file extension \(e.g., pdf, doc\) | +| `safe` | string | No | SafeSearch level: "active" or "off" \(default off\) | +| `searchType` | string | No | Set to "image" to perform an image search | +| `siteSearch` | string | No | A site to include or exclude from results | +| `siteSearchFilter` | string | No | Whether to include \("i"\) or exclude \("e"\) the siteSearch site | +| `lr` | string | No | Restrict to a language, e.g. "lang_en" | +| `gl` | string | No | Two-letter country code to boost geographically relevant results | +| `sort` | string | No | Sort expression, e.g. "date" | | `apiKey` | string | Yes | Google API key | #### Output @@ -48,14 +58,29 @@ Search the web with the Custom Search API | --------- | ---- | ----------- | | `items` | array | Array of search results from Google | | ↳ `title` | string | Title of the search result | +| ↳ `htmlTitle` | string | Title of the search result with HTML markup | | ↳ `link` | string | URL of the search result | -| ↳ `snippet` | string | Snippet or description of the search result | | ↳ `displayLink` | string | Display URL \(abbreviated form\) | +| ↳ `snippet` | string | Snippet or description of the search result | +| ↳ `htmlSnippet` | string | Snippet of the search result with HTML markup | +| ↳ `formattedUrl` | string | Display URL shown beneath the result | +| ↳ `mime` | string | MIME type of the result | +| ↳ `fileFormat` | string | File format of the result | +| ↳ `cacheId` | string | ID of Google's cached version | | ↳ `pagemap` | object | PageMap information for the result \(structured data\) | +| ↳ `image` | object | Image metadata \(present when searchType is image\) | +| ↳ `contextLink` | string | URL of the page hosting the image | +| ↳ `height` | number | Image height in pixels | +| ↳ `width` | number | Image width in pixels | +| ↳ `byteSize` | number | Image file size in bytes | +| ↳ `thumbnailLink` | string | Thumbnail image URL | +| ↳ `thumbnailHeight` | number | Thumbnail height in pixels | +| ↳ `thumbnailWidth` | number | Thumbnail width in pixels | | `searchInformation` | object | Information about the search query and results | | ↳ `totalResults` | string | Total number of search results available | | ↳ `searchTime` | number | Time taken to perform the search in seconds | | ↳ `formattedSearchTime` | string | Formatted search time for display | | ↳ `formattedTotalResults` | string | Formatted total results count for display | +| `nextPageStartIndex` | number | Start index for the next page of results \(null if no further results\) | diff --git a/apps/docs/content/docs/en/integrations/google_sheets.mdx b/apps/docs/content/docs/en/integrations/google_sheets.mdx index c658c1030e..27ce97e09f 100644 --- a/apps/docs/content/docs/en/integrations/google_sheets.mdx +++ b/apps/docs/content/docs/en/integrations/google_sheets.mdx @@ -320,6 +320,68 @@ Copy a sheet from one spreadsheet to another | `destinationSpreadsheetId` | string | The ID of the destination spreadsheet | | `destinationSpreadsheetUrl` | string | URL to the destination spreadsheet | +### `google_sheets_delete_rows` + +Delete rows from a sheet in a Google Sheets spreadsheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetId` | number | Yes | The numeric ID of the sheet/tab \(not the sheet name\). Use Get Spreadsheet to find sheet IDs. | +| `startIndex` | number | Yes | The start row index \(0-based, inclusive\) of the rows to delete | +| `endIndex` | number | Yes | The end row index \(0-based, exclusive\) of the rows to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | Google Sheets spreadsheet ID | +| `sheetId` | number | The numeric ID of the sheet | +| `deletedRowRange` | string | Description of the deleted row range | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_delete_sheet` + +Delete a sheet/tab from a Google Sheets spreadsheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID | +| `sheetId` | number | Yes | The numeric ID of the sheet/tab to delete \(not the sheet name\). Use Get Spreadsheet to find sheet IDs. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | Google Sheets spreadsheet ID | +| `deletedSheetId` | number | The numeric ID of the deleted sheet | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_delete_spreadsheet` + +Permanently delete a Google Sheets spreadsheet using the Google Drive API + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the Google Sheets spreadsheet to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The ID of the deleted spreadsheet | +| `deleted` | boolean | Whether the spreadsheet was successfully deleted | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/google_slides.mdx b/apps/docs/content/docs/en/integrations/google_slides.mdx index 591bd9dc97..9a30e156e3 100644 --- a/apps/docs/content/docs/en/integrations/google_slides.mdx +++ b/apps/docs/content/docs/en/integrations/google_slides.mdx @@ -193,7 +193,7 @@ Generate a thumbnail image of a specific slide in a Google Slides presentation | `presentationId` | string | Yes | Google Slides presentation ID | | `pageObjectId` | string | Yes | The object ID of the slide/page to get a thumbnail for | | `thumbnailSize` | string | No | The size of the thumbnail: SMALL \(200px\), MEDIUM \(800px\), or LARGE \(1600px\). Defaults to MEDIUM. | -| `mimeType` | string | No | The MIME type of the thumbnail image: PNG or GIF. Defaults to PNG. | +| `mimeType` | string | No | The MIME type of the thumbnail image: PNG. Defaults to PNG. | #### Output diff --git a/apps/sim/blocks/blocks/google.ts b/apps/sim/blocks/blocks/google.ts index 82214be769..5c5a4da597 100644 --- a/apps/sim/blocks/blocks/google.ts +++ b/apps/sim/blocks/blocks/google.ts @@ -61,8 +61,88 @@ Return ONLY the search query - no explanations, no quotes around the whole thing id: 'num', title: 'Number of Results', type: 'short-input', - placeholder: '10', - required: true, + placeholder: '10 (1-10)', + mode: 'advanced', + }, + { + id: 'start', + title: 'Start Index', + type: 'short-input', + placeholder: '1 (for pagination; start + num <= 100)', + mode: 'advanced', + }, + { + id: 'searchType', + title: 'Search Type', + type: 'dropdown', + options: [ + { label: 'Web', id: '' }, + { label: 'Image', id: 'image' }, + ], + mode: 'advanced', + }, + { + id: 'dateRestrict', + title: 'Date Restrict', + type: 'short-input', + placeholder: 'e.g., d7, w2, m1, y1', + mode: 'advanced', + }, + { + id: 'fileType', + title: 'File Type', + type: 'short-input', + placeholder: 'e.g., pdf, doc', + mode: 'advanced', + }, + { + id: 'safe', + title: 'SafeSearch', + type: 'dropdown', + options: [ + { label: 'Off', id: '' }, + { label: 'Active', id: 'active' }, + ], + mode: 'advanced', + }, + { + id: 'siteSearch', + title: 'Site Search', + type: 'short-input', + placeholder: 'Domain to include or exclude (e.g., wikipedia.org)', + mode: 'advanced', + }, + { + id: 'siteSearchFilter', + title: 'Site Search Filter', + type: 'dropdown', + options: [ + { label: 'Include', id: 'i' }, + { label: 'Exclude', id: 'e' }, + ], + condition: { field: 'siteSearch', value: '', not: true }, + mode: 'advanced', + }, + { + id: 'lr', + title: 'Language Restrict', + type: 'short-input', + placeholder: 'e.g., lang_en', + mode: 'advanced', + }, + { + id: 'gl', + title: 'Country (geolocation)', + type: 'short-input', + placeholder: 'Two-letter country code (e.g., us)', + mode: 'advanced', + }, + { + id: 'sort', + title: 'Sort', + type: 'short-input', + placeholder: 'e.g., date', + mode: 'advanced', }, ], @@ -74,7 +154,17 @@ Return ONLY the search query - no explanations, no quotes around the whole thing query: params.query, apiKey: params.apiKey, searchEngineId: params.searchEngineId, - num: params.num || undefined, + num: params.num ? Number(params.num) : undefined, + start: params.start ? Number(params.start) : undefined, + dateRestrict: params.dateRestrict || undefined, + fileType: params.fileType || undefined, + safe: params.safe || undefined, + searchType: params.searchType || undefined, + siteSearch: params.siteSearch || undefined, + siteSearchFilter: params.siteSearch ? params.siteSearchFilter || undefined : undefined, + lr: params.lr || undefined, + gl: params.gl || undefined, + sort: params.sort || undefined, }), }, }, @@ -83,12 +173,23 @@ Return ONLY the search query - no explanations, no quotes around the whole thing query: { type: 'string', description: 'Search query terms' }, apiKey: { type: 'string', description: 'Google API key' }, searchEngineId: { type: 'string', description: 'Custom search engine ID' }, - num: { type: 'string', description: 'Number of results' }, + num: { type: 'string', description: 'Number of results (1-10)' }, + start: { type: 'string', description: 'Start index for pagination (1-based)' }, + dateRestrict: { type: 'string', description: 'Restrict by recency (d/w/m/y notation)' }, + fileType: { type: 'string', description: 'Restrict to a file extension' }, + safe: { type: 'string', description: 'SafeSearch level (active/off)' }, + searchType: { type: 'string', description: 'Search type (image for image search)' }, + siteSearch: { type: 'string', description: 'Site to include or exclude' }, + siteSearchFilter: { type: 'string', description: 'Include (i) or exclude (e) the site' }, + lr: { type: 'string', description: 'Language restriction (e.g., lang_en)' }, + gl: { type: 'string', description: 'Country geolocation code' }, + sort: { type: 'string', description: 'Sort expression (e.g., date)' }, }, outputs: { items: { type: 'json', description: 'Search result items' }, searchInformation: { type: 'json', description: 'Search metadata' }, + nextPageStartIndex: { type: 'number', description: 'Start index for the next page of results' }, }, } diff --git a/apps/sim/blocks/blocks/google_groups.ts b/apps/sim/blocks/blocks/google_groups.ts index 76a2928906..537ae4a012 100644 --- a/apps/sim/blocks/blocks/google_groups.ts +++ b/apps/sim/blocks/blocks/google_groups.ts @@ -337,19 +337,19 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, case 'get_group': case 'delete_group': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, } case 'create_group': return { - credential: oauthCredential, + oauthCredential, email: rest.email, name: rest.name, description: rest.description, } case 'update_group': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, name: rest.newName, email: rest.newEmail, @@ -357,7 +357,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, } case 'list_members': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, maxResults: rest.maxResults ? Number(rest.maxResults) : undefined, roles: rest.roles, @@ -365,38 +365,38 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, case 'get_member': case 'remove_member': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, memberKey: rest.memberKey, } case 'add_member': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, email: rest.memberEmail, role: rest.role, } case 'update_member': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, memberKey: rest.memberKey, role: rest.role, } case 'has_member': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, memberKey: rest.memberKey, } case 'list_aliases': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, } case 'add_alias': return { - credential: oauthCredential, + oauthCredential, groupKey: rest.groupKey, alias: rest.alias, } diff --git a/apps/sim/blocks/blocks/google_maps.ts b/apps/sim/blocks/blocks/google_maps.ts index ad5ed8c7e2..26d3259b7f 100644 --- a/apps/sim/blocks/blocks/google_maps.ts +++ b/apps/sim/blocks/blocks/google_maps.ts @@ -34,6 +34,8 @@ export const GoogleMapsBlock: BlockConfig = { { label: 'Validate Address', id: 'validate_address' }, { label: 'Geolocate (WiFi/Cell)', id: 'geolocate' }, { label: 'Air Quality', id: 'air_quality' }, + { label: 'Pollen Forecast', id: 'pollen' }, + { label: 'Solar Potential', id: 'solar' }, ], value: () => 'geocode', }, @@ -354,23 +356,53 @@ export const GoogleMapsBlock: BlockConfig = { title: 'Latitude', type: 'short-input', placeholder: '37.4224764', - condition: { field: 'operation', value: 'air_quality' }, - required: { field: 'operation', value: 'air_quality' }, + condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, + required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, }, { id: 'aqLongitude', title: 'Longitude', type: 'short-input', placeholder: '-122.0842499', - condition: { field: 'operation', value: 'air_quality' }, - required: { field: 'operation', value: 'air_quality' }, + condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, + required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, }, { id: 'languageCode', title: 'Language Code', type: 'short-input', placeholder: 'Language code (e.g., en, es)', - condition: { field: 'operation', value: 'air_quality' }, + condition: { field: 'operation', value: ['air_quality', 'pollen'] }, + mode: 'advanced', + }, + + { + id: 'days', + title: 'Forecast Days', + type: 'short-input', + placeholder: 'Number of days (1-5, defaults to 1)', + condition: { field: 'operation', value: 'pollen' }, + mode: 'advanced', + }, + { + id: 'plantsDescription', + title: 'Include Plant Descriptions', + type: 'switch', + condition: { field: 'operation', value: 'pollen' }, + mode: 'advanced', + }, + + { + id: 'requiredQuality', + title: 'Minimum Imagery Quality', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'High', id: 'HIGH' }, + { label: 'Medium', id: 'MEDIUM' }, + { label: 'Base', id: 'BASE' }, + ], + condition: { field: 'operation', value: 'solar' }, mode: 'advanced', }, @@ -401,8 +433,10 @@ export const GoogleMapsBlock: BlockConfig = { 'google_maps_geolocate', 'google_maps_place_details', 'google_maps_places_search', + 'google_maps_pollen', 'google_maps_reverse_geocode', 'google_maps_snap_to_roads', + 'google_maps_solar', 'google_maps_speed_limits', 'google_maps_timezone', 'google_maps_validate_address', @@ -503,6 +537,18 @@ export const GoogleMapsBlock: BlockConfig = { considerIp = params.considerIp === 'true' || params.considerIp === true } + let days: number | undefined + if (params.days) { + const parsedDays = Number.parseInt(params.days, 10) + days = Number.isNaN(parsedDays) ? undefined : parsedDays + } + + let plantsDescription: boolean | undefined + if (params.plantsDescription !== undefined) { + plantsDescription = + params.plantsDescription === 'true' || params.plantsDescription === true + } + return { ...rest, address, @@ -519,6 +565,9 @@ export const GoogleMapsBlock: BlockConfig = { interpolate, enableUspsCass, considerIp, + days, + plantsDescription, + requiredQuality: params.requiredQuality || undefined, type: params.placeType || undefined, avoid: params.avoid || undefined, radioType: params.radioType || undefined, @@ -561,9 +610,12 @@ export const GoogleMapsBlock: BlockConfig = { carrier: { type: 'string', description: 'Carrier name' }, wifiAccessPoints: { type: 'string', description: 'WiFi access points JSON' }, cellTowers: { type: 'string', description: 'Cell towers JSON' }, - aqLatitude: { type: 'string', description: 'Latitude for air quality' }, - aqLongitude: { type: 'string', description: 'Longitude for air quality' }, - languageCode: { type: 'string', description: 'Language code for air quality' }, + aqLatitude: { type: 'string', description: 'Latitude for air quality, pollen, or solar' }, + aqLongitude: { type: 'string', description: 'Longitude for air quality, pollen, or solar' }, + languageCode: { type: 'string', description: 'Language code for air quality or pollen' }, + days: { type: 'string', description: 'Number of pollen forecast days (1-5)' }, + plantsDescription: { type: 'boolean', description: 'Include detailed plant descriptions' }, + requiredQuality: { type: 'string', description: 'Minimum solar imagery quality' }, }, outputs: { @@ -636,6 +688,21 @@ export const GoogleMapsBlock: BlockConfig = { indexes: { type: 'json', description: 'Air quality indexes' }, pollutants: { type: 'json', description: 'Pollutant concentrations' }, healthRecommendations: { type: 'json', description: 'Health recommendations' }, + + dailyInfo: { type: 'json', description: 'Daily pollen forecast (grass, tree, weed, plants)' }, + + center: { type: 'json', description: 'Center coordinate of the solar building' }, + imageryDate: { type: 'json', description: 'Date the solar imagery was captured' }, + imageryQuality: { type: 'string', description: 'Quality of the solar imagery used' }, + postalCode: { type: 'string', description: 'Postal code of the solar building' }, + administrativeArea: { + type: 'string', + description: 'Administrative area of the solar building', + }, + solarPotential: { + type: 'json', + description: 'Solar potential, panel specs, and configurations', + }, }, } diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index 4578a92aae..18397fbe73 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -326,6 +326,9 @@ export const GoogleSheetsV2Block: BlockConfig = { { label: 'Batch Update', id: 'batch_update' }, { label: 'Batch Clear', id: 'batch_clear' }, { label: 'Copy Sheet', id: 'copy_sheet' }, + { label: 'Delete Rows', id: 'delete_rows' }, + { label: 'Delete Sheet', id: 'delete_sheet' }, + { label: 'Delete Spreadsheet', id: 'delete_spreadsheet' }, ], value: () => 'read', }, @@ -720,6 +723,32 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, condition: { field: 'operation', value: 'copy_sheet' }, required: true, }, + // Delete Rows / Delete Sheet Fields + { + id: 'deleteSheetId', + title: 'Sheet ID', + type: 'short-input', + placeholder: 'Numeric ID of the sheet/tab (use Get Spreadsheet Info to find IDs)', + condition: { field: 'operation', value: ['delete_rows', 'delete_sheet'] }, + required: true, + }, + // Delete Rows Fields + { + id: 'startIndex', + title: 'Start Row Index', + type: 'short-input', + placeholder: '0-based, inclusive (e.g., 0 for the first row)', + condition: { field: 'operation', value: 'delete_rows' }, + required: true, + }, + { + id: 'endIndex', + title: 'End Row Index', + type: 'short-input', + placeholder: '0-based, exclusive (e.g., 5 to delete through the fifth row)', + condition: { field: 'operation', value: 'delete_rows' }, + required: true, + }, ...getTrigger('google_sheets_poller').subBlocks, ], tools: { @@ -735,6 +764,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, 'google_sheets_batch_update_v2', 'google_sheets_batch_clear_v2', 'google_sheets_copy_sheet_v2', + 'google_sheets_delete_rows_v2', + 'google_sheets_delete_sheet_v2', + 'google_sheets_delete_spreadsheet_v2', ], config: { tool: createVersionedToolSelector({ @@ -762,6 +794,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, return 'google_sheets_batch_clear' case 'copy_sheet': return 'google_sheets_copy_sheet' + case 'delete_rows': + return 'google_sheets_delete_rows' + case 'delete_sheet': + return 'google_sheets_delete_sheet' + case 'delete_spreadsheet': + return 'google_sheets_delete_spreadsheet' default: throw new Error(`Invalid Google Sheets operation: ${params.operation}`) } @@ -782,6 +820,9 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, batchData, sheetId, destinationSpreadsheetId, + deleteSheetId, + startIndex, + endIndex, filterColumn, filterValue, filterMatchType, @@ -857,6 +898,48 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, } } + // Handle delete_spreadsheet operation + if (operation === 'delete_spreadsheet') { + return { + spreadsheetId: effectiveSpreadsheetId, + oauthCredential, + } + } + + // Handle delete_sheet operation + if (operation === 'delete_sheet') { + const parsedSheetId = Number.parseInt(deleteSheetId as string, 10) + if (Number.isNaN(parsedSheetId)) { + throw new Error('Sheet ID must be a valid number') + } + return { + spreadsheetId: effectiveSpreadsheetId, + sheetId: parsedSheetId, + oauthCredential, + } + } + + // Handle delete_rows operation + if (operation === 'delete_rows') { + const parsedSheetId = Number.parseInt(deleteSheetId as string, 10) + const parsedStartIndex = Number.parseInt(startIndex as string, 10) + const parsedEndIndex = Number.parseInt(endIndex as string, 10) + if ( + Number.isNaN(parsedSheetId) || + Number.isNaN(parsedStartIndex) || + Number.isNaN(parsedEndIndex) + ) { + throw new Error('Sheet ID, start index, and end index must be valid numbers') + } + return { + spreadsheetId: effectiveSpreadsheetId, + sheetId: parsedSheetId, + startIndex: parsedStartIndex, + endIndex: parsedEndIndex, + oauthCredential, + } + } + // Handle read/write/update/append/clear operations (require sheet name) const effectiveSheetName = sheetName ? String(sheetName).trim() : '' @@ -900,6 +983,18 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, type: 'string', description: 'Destination spreadsheet ID for copy', }, + deleteSheetId: { + type: 'string', + description: 'Numeric sheet ID for delete rows/sheet operations', + }, + startIndex: { + type: 'string', + description: 'Start row index (0-based, inclusive) for delete rows operation', + }, + endIndex: { + type: 'string', + description: 'End row index (0-based, exclusive) for delete rows operation', + }, filterColumn: { type: 'string', description: 'Column header name to filter the read rows on (within the read range)', @@ -972,7 +1067,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, description: 'Spreadsheet ID', condition: { field: 'operation', - value: ['get_info', 'create', 'batch_get', 'batch_update', 'batch_clear'], + value: [ + 'get_info', + 'create', + 'batch_get', + 'batch_update', + 'batch_clear', + 'delete_rows', + 'delete_sheet', + 'delete_spreadsheet', + ], }, }, title: { @@ -1038,11 +1142,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, description: 'Array of ranges that were cleared', condition: { field: 'operation', value: 'batch_clear' }, }, - // Copy Sheet outputs + // Copy Sheet / Delete Rows outputs sheetId: { type: 'number', - description: 'ID of the copied sheet in the destination', - condition: { field: 'operation', value: 'copy_sheet' }, + description: + 'ID of the copied sheet in the destination, or the sheet the rows were deleted from', + condition: { field: 'operation', value: ['copy_sheet', 'delete_rows'] }, }, index: { type: 'number', @@ -1064,6 +1169,24 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, description: 'URL of the destination spreadsheet', condition: { field: 'operation', value: 'copy_sheet' }, }, + // Delete Rows outputs + deletedRowRange: { + type: 'string', + description: 'Description of the deleted row range', + condition: { field: 'operation', value: 'delete_rows' }, + }, + // Delete Sheet outputs + deletedSheetId: { + type: 'number', + description: 'The numeric ID of the deleted sheet', + condition: { field: 'operation', value: 'delete_sheet' }, + }, + // Delete Spreadsheet outputs + deleted: { + type: 'boolean', + description: 'Whether the spreadsheet was successfully deleted', + condition: { field: 'operation', value: 'delete_spreadsheet' }, + }, // Common metadata metadata: { type: 'json', @@ -1079,6 +1202,8 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, 'batch_get', 'batch_update', 'batch_clear', + 'delete_rows', + 'delete_sheet', ], }, }, diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index 3a36861650..b05ecf8fde 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -521,10 +521,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, id: 'mimeType', title: 'Image Format', type: 'dropdown', - options: [ - { label: 'PNG', id: 'PNG' }, - { label: 'GIF', id: 'GIF' }, - ], + options: [{ label: 'PNG', id: 'PNG' }], condition: { field: 'operation', value: 'get_thumbnail' }, value: () => 'PNG', }, @@ -3052,7 +3049,7 @@ Return ONLY the text content - no explanations, no markdown formatting markers, // Get thumbnail operation thumbnailPageId: { type: 'string', description: 'Slide object ID for thumbnail' }, thumbnailSize: { type: 'string', description: 'Thumbnail size' }, - mimeType: { type: 'string', description: 'Image format (PNG or GIF)' }, + mimeType: { type: 'string', description: 'Image format (PNG)' }, // Get page operation getPageObjectId: { type: 'string', description: 'Page/slide object ID to retrieve' }, // Delete object operation diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 2ff2b8f785..45ecb7990f 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -6408,9 +6408,17 @@ { "name": "Air Quality", "description": "Get current air quality data for a location" + }, + { + "name": "Pollen Forecast", + "description": "Get a daily pollen forecast (grass, tree, weed) for a location" + }, + { + "name": "Solar Potential", + "description": "Get solar potential and panel insights for the building nearest a location" } ], - "operationCount": 13, + "operationCount": 15, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -6551,9 +6559,21 @@ { "name": "Copy Sheet", "description": "Copy a sheet from one spreadsheet to another" + }, + { + "name": "Delete Rows", + "description": "Delete rows from a sheet in a Google Sheets spreadsheet" + }, + { + "name": "Delete Sheet", + "description": "Delete a sheet/tab from a Google Sheets spreadsheet" + }, + { + "name": "Delete Spreadsheet", + "description": "Permanently delete a Google Sheets spreadsheet using the Google Drive API" } ], - "operationCount": 11, + "operationCount": 14, "triggers": [ { "id": "google_sheets_poller", diff --git a/apps/sim/tools/google/search.ts b/apps/sim/tools/google/search.ts index 75fcc78fed..e7ed8ad769 100644 --- a/apps/sim/tools/google/search.ts +++ b/apps/sim/tools/google/search.ts @@ -28,7 +28,68 @@ export const searchTool: ToolConfig = type: 'string', // Treated as string for compatibility with tool interfaces required: false, visibility: 'user-only', - description: 'Number of results to return (default: 10, max: 10)', + description: 'Number of results to return (1-10, default 10)', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Index of the first result (1-based, for pagination; start + num must be <= 100)', + }, + dateRestrict: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict results by recency: d[n] days, w[n] weeks, m[n] months, y[n] years', + }, + fileType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict to a file extension (e.g., pdf, doc)', + }, + safe: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'SafeSearch level: "active" or "off" (default off)', + }, + searchType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Set to "image" to perform an image search', + }, + siteSearch: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'A site to include or exclude from results', + }, + siteSearchFilter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include ("i") or exclude ("e") the siteSearch site', + }, + lr: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Restrict to a language, e.g. "lang_en"', + }, + gl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Two-letter country code to boost geographically relevant results', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort expression, e.g. "date"', }, apiKey: { type: 'string', @@ -48,9 +109,41 @@ export const searchTool: ToolConfig = searchParams.append('q', params.query) searchParams.append('cx', params.searchEngineId) - // Add optional parameter - if (params.num) { - searchParams.append('num', params.num.toString()) + // Add optional parameters + const num = Math.trunc(Number(params.num)) + if (Number.isFinite(num) && num > 0) { + searchParams.append('num', Math.min(num, 10).toString()) + } + const start = Math.trunc(Number(params.start)) + if (Number.isFinite(start) && start > 0) { + searchParams.append('start', start.toString()) + } + if (params.dateRestrict) { + searchParams.append('dateRestrict', params.dateRestrict) + } + if (params.fileType) { + searchParams.append('fileType', params.fileType) + } + if (params.safe) { + searchParams.append('safe', params.safe) + } + if (params.searchType) { + searchParams.append('searchType', params.searchType) + } + if (params.siteSearch) { + searchParams.append('siteSearch', params.siteSearch) + if (params.siteSearchFilter) { + searchParams.append('siteSearchFilter', params.siteSearchFilter) + } + } + if (params.lr) { + searchParams.append('lr', params.lr) + } + if (params.gl) { + searchParams.append('gl', params.gl) + } + if (params.sort) { + searchParams.append('sort', params.sort) } return `${baseUrl}?${searchParams.toString()}` @@ -64,6 +157,10 @@ export const searchTool: ToolConfig = transformResponse: async (response: Response) => { const data = await response.json() + if (!response.ok || data.error) { + throw new Error(`Google Search failed: ${data.error?.message || response.statusText}`) + } + return { success: true, output: { @@ -74,6 +171,7 @@ export const searchTool: ToolConfig = formattedSearchTime: '0', formattedTotalResults: '0', }, + nextPageStartIndex: data.queries?.nextPage?.[0]?.startIndex ?? null, }, } }, @@ -92,5 +190,10 @@ export const searchTool: ToolConfig = description: 'Information about the search query and results', properties: GOOGLE_SEARCH_INFORMATION_OUTPUT_PROPERTIES, }, + nextPageStartIndex: { + type: 'number', + description: 'Start index for the next page of results (null if no further results)', + optional: true, + }, }, } diff --git a/apps/sim/tools/google/types.ts b/apps/sim/tools/google/types.ts index b9421706f6..50a9ac8fbb 100644 --- a/apps/sim/tools/google/types.ts +++ b/apps/sim/tools/google/types.ts @@ -11,14 +11,46 @@ import type { OutputProperty, ToolResponse } from '@/tools/types' */ export const GOOGLE_SEARCH_RESULT_OUTPUT_PROPERTIES = { title: { type: 'string', description: 'Title of the search result' }, + htmlTitle: { + type: 'string', + description: 'Title of the search result with HTML markup', + optional: true, + }, link: { type: 'string', description: 'URL of the search result' }, - snippet: { type: 'string', description: 'Snippet or description of the search result' }, displayLink: { type: 'string', description: 'Display URL (abbreviated form)', optional: true }, + snippet: { type: 'string', description: 'Snippet or description of the search result' }, + htmlSnippet: { + type: 'string', + description: 'Snippet of the search result with HTML markup', + optional: true, + }, + formattedUrl: { + type: 'string', + description: 'Display URL shown beneath the result', + optional: true, + }, + mime: { type: 'string', description: 'MIME type of the result', optional: true }, + fileFormat: { type: 'string', description: 'File format of the result', optional: true }, + cacheId: { type: 'string', description: "ID of Google's cached version", optional: true }, pagemap: { type: 'object', description: 'PageMap information for the result (structured data)', optional: true, }, + image: { + type: 'object', + description: 'Image metadata (present when searchType is image)', + optional: true, + properties: { + contextLink: { type: 'string', description: 'URL of the page hosting the image' }, + height: { type: 'number', description: 'Image height in pixels' }, + width: { type: 'number', description: 'Image width in pixels' }, + byteSize: { type: 'number', description: 'Image file size in bytes' }, + thumbnailLink: { type: 'string', description: 'Thumbnail image URL' }, + thumbnailHeight: { type: 'number', description: 'Thumbnail height in pixels' }, + thumbnailWidth: { type: 'number', description: 'Thumbnail width in pixels' }, + }, + }, } as const satisfies Record /** @@ -58,16 +90,41 @@ export interface GoogleSearchParams { apiKey: string searchEngineId: string num?: number | string + start?: number | string + dateRestrict?: string + fileType?: string + safe?: string + searchType?: string + siteSearch?: string + siteSearchFilter?: string + lr?: string + gl?: string + sort?: string } export interface GoogleSearchResponse extends ToolResponse { output: { items: Array<{ title: string + htmlTitle?: string link: string - snippet: string displayLink?: string - pagemap?: Record + snippet: string + htmlSnippet?: string + formattedUrl?: string + mime?: string + fileFormat?: string + cacheId?: string + pagemap?: Record + image?: { + contextLink?: string + height?: number + width?: number + byteSize?: number + thumbnailLink?: string + thumbnailHeight?: number + thumbnailWidth?: number + } }> searchInformation: { totalResults: string @@ -75,5 +132,6 @@ export interface GoogleSearchResponse extends ToolResponse { formattedSearchTime: string formattedTotalResults: string } + nextPageStartIndex: number | null } } diff --git a/apps/sim/tools/google_ads/ad_performance.ts b/apps/sim/tools/google_ads/ad_performance.ts index 337379298a..e1323e514d 100644 --- a/apps/sim/tools/google_ads/ad_performance.ts +++ b/apps/sim/tools/google_ads/ad_performance.ts @@ -86,7 +86,7 @@ export const googleAdsAdPerformanceTool: ToolConfig< request: { url: (params) => { const customerId = validateNumericId(params.customerId, 'customerId') - return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/google_ads/campaign_performance.ts b/apps/sim/tools/google_ads/campaign_performance.ts index 3e4ec688b7..47418a557c 100644 --- a/apps/sim/tools/google_ads/campaign_performance.ts +++ b/apps/sim/tools/google_ads/campaign_performance.ts @@ -74,7 +74,7 @@ export const googleAdsCampaignPerformanceTool: ToolConfig< request: { url: (params) => { const customerId = validateNumericId(params.customerId, 'customerId') - return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/google_ads/list_ad_groups.ts b/apps/sim/tools/google_ads/list_ad_groups.ts index 8d8d243031..e58c759fca 100644 --- a/apps/sim/tools/google_ads/list_ad_groups.ts +++ b/apps/sim/tools/google_ads/list_ad_groups.ts @@ -67,7 +67,7 @@ export const googleAdsListAdGroupsTool: ToolConfig< request: { url: (params) => { const customerId = validateNumericId(params.customerId, 'customerId') - return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/google_ads/list_campaigns.ts b/apps/sim/tools/google_ads/list_campaigns.ts index ed738cc955..43f543c0d8 100644 --- a/apps/sim/tools/google_ads/list_campaigns.ts +++ b/apps/sim/tools/google_ads/list_campaigns.ts @@ -61,7 +61,7 @@ export const googleAdsListCampaignsTool: ToolConfig< request: { url: (params) => { const customerId = validateNumericId(params.customerId, 'customerId') - return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/google_ads/list_customers.ts b/apps/sim/tools/google_ads/list_customers.ts index 9d764b8691..38324213bd 100644 --- a/apps/sim/tools/google_ads/list_customers.ts +++ b/apps/sim/tools/google_ads/list_customers.ts @@ -34,7 +34,7 @@ export const googleAdsListCustomersTool: ToolConfig< }, request: { - url: 'https://googleads.googleapis.com/v19/customers:listAccessibleCustomers', + url: 'https://googleads.googleapis.com/v24/customers:listAccessibleCustomers', method: 'GET', headers: (params) => ({ Authorization: `Bearer ${params.accessToken}`, diff --git a/apps/sim/tools/google_ads/search.ts b/apps/sim/tools/google_ads/search.ts index ffd497fc39..74f3e260b0 100644 --- a/apps/sim/tools/google_ads/search.ts +++ b/apps/sim/tools/google_ads/search.ts @@ -55,7 +55,7 @@ export const googleAdsSearchTool: ToolConfig { const customerId = validateNumericId(params.customerId, 'customerId') - return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + return `https://googleads.googleapis.com/v24/customers/${customerId}/googleAds:search` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/google_contacts/update.ts b/apps/sim/tools/google_contacts/update.ts index 51d50448d7..0dfa7f1b3b 100644 --- a/apps/sim/tools/google_contacts/update.ts +++ b/apps/sim/tools/google_contacts/update.ts @@ -121,6 +121,7 @@ export const updateTool: ToolConfig { const person: Record = { etag: params.etag, + metadata: { sources: [{ type: 'CONTACT', etag: params.etag }] }, } if (params.givenName || params.familyName) { diff --git a/apps/sim/tools/google_groups/add_alias.ts b/apps/sim/tools/google_groups/add_alias.ts index 4780e5db23..91d2998af6 100644 --- a/apps/sim/tools/google_groups/add_alias.ts +++ b/apps/sim/tools/google_groups/add_alias.ts @@ -47,9 +47,10 @@ export const addAliasTool: ToolConfig ({ - alias: params.alias.trim(), - }), + body: (params) => + JSON.stringify({ + alias: params.alias.trim(), + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/google_groups/update_settings.ts b/apps/sim/tools/google_groups/update_settings.ts index 30dd92497b..a76f235cb8 100644 --- a/apps/sim/tools/google_groups/update_settings.ts +++ b/apps/sim/tools/google_groups/update_settings.ts @@ -63,7 +63,7 @@ export const updateSettingsTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW', + 'Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW, ALL_OWNERS_CAN_VIEW', }, whoCanPostMessage: { type: 'string', @@ -194,7 +194,7 @@ export const updateSettingsTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE', + description: 'Who can ban users: ALL_MEMBERS, OWNERS_AND_MANAGERS, OWNERS_ONLY, NONE', }, whoCanModerateMembers: { type: 'string', @@ -297,7 +297,7 @@ export const updateSettingsTool: ToolConfig< if (params.whoCanDiscoverGroup !== undefined) body.whoCanDiscoverGroup = params.whoCanDiscoverGroup if (params.defaultSender !== undefined) body.defaultSender = params.defaultSender - return body + return JSON.stringify(body) }, }, diff --git a/apps/sim/tools/google_maps/index.ts b/apps/sim/tools/google_maps/index.ts index 8b47d6b91d..5b3465cc08 100644 --- a/apps/sim/tools/google_maps/index.ts +++ b/apps/sim/tools/google_maps/index.ts @@ -6,8 +6,10 @@ import { googleMapsGeocodeTool } from '@/tools/google_maps/geocode' import { googleMapsGeolocateTool } from '@/tools/google_maps/geolocate' import { googleMapsPlaceDetailsTool } from '@/tools/google_maps/place_details' import { googleMapsPlacesSearchTool } from '@/tools/google_maps/places_search' +import { googleMapsPollenTool } from '@/tools/google_maps/pollen' import { googleMapsReverseGeocodeTool } from '@/tools/google_maps/reverse_geocode' import { googleMapsSnapToRoadsTool } from '@/tools/google_maps/snap_to_roads' +import { googleMapsSolarTool } from '@/tools/google_maps/solar' import { googleMapsSpeedLimitsTool } from '@/tools/google_maps/speed_limits' import { googleMapsTimezoneTool } from '@/tools/google_maps/timezone' import { googleMapsValidateAddressTool } from '@/tools/google_maps/validate_address' @@ -21,8 +23,10 @@ export { googleMapsGeolocateTool, googleMapsPlaceDetailsTool, googleMapsPlacesSearchTool, + googleMapsPollenTool, googleMapsReverseGeocodeTool, googleMapsSnapToRoadsTool, + googleMapsSolarTool, googleMapsSpeedLimitsTool, googleMapsTimezoneTool, googleMapsValidateAddressTool, diff --git a/apps/sim/tools/google_maps/pollen.ts b/apps/sim/tools/google_maps/pollen.ts new file mode 100644 index 0000000000..f38e78a23a --- /dev/null +++ b/apps/sim/tools/google_maps/pollen.ts @@ -0,0 +1,249 @@ +import type { GoogleMapsPollenParams, GoogleMapsPollenResponse } from '@/tools/google_maps/types' +import type { ToolConfig } from '@/tools/types' + +interface RawIndexInfo { + code?: string + displayName?: string + value?: number + category?: string + indexDescription?: string + color?: { red?: number; green?: number; blue?: number } +} + +const mapIndexInfo = (indexInfo: RawIndexInfo | undefined) => + indexInfo + ? { + code: indexInfo.code || '', + displayName: indexInfo.displayName || '', + value: indexInfo.value ?? 0, + category: indexInfo.category || '', + indexDescription: indexInfo.indexDescription || '', + color: { + red: indexInfo.color?.red ?? 0, + green: indexInfo.color?.green ?? 0, + blue: indexInfo.color?.blue ?? 0, + }, + } + : null + +export const googleMapsPollenTool: ToolConfig = { + id: 'google_maps_pollen', + name: 'Google Maps Pollen', + description: 'Get a daily pollen forecast (grass, tree, weed) for a location', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Maps API key with Pollen API enabled', + }, + lat: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Latitude coordinate', + }, + lng: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Longitude coordinate', + }, + days: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of forecast days to return (1-5, defaults to 1)', + }, + languageCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Language code for the response (e.g., "en", "es")', + }, + plantsDescription: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include detailed plant descriptions (defaults to true)', + }, + }, + + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + + request: { + url: (params) => { + const url = new URL('https://pollen.googleapis.com/v1/forecast:lookup') + url.searchParams.set('location.latitude', params.lat.toString()) + url.searchParams.set('location.longitude', params.lng.toString()) + const rawDays = + typeof params.days === 'number' && Number.isFinite(params.days) + ? Math.trunc(params.days) + : 1 + const days = Math.min(Math.max(rawDays, 1), 5) + url.searchParams.set('days', days.toString()) + if (params.languageCode) { + url.searchParams.set('languageCode', params.languageCode.trim()) + } + if (params.plantsDescription !== undefined) { + url.searchParams.set('plantsDescription', String(params.plantsDescription)) + } + url.searchParams.set('key', params.apiKey.trim()) + return url.toString() + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok || data.error) { + throw new Error(`Pollen lookup failed: ${data.error?.message || response.statusText}`) + } + + const dailyInfo = (data.dailyInfo || []).map( + (day: { + date?: { year?: number; month?: number; day?: number } + pollenTypeInfo?: Array<{ + code?: string + displayName?: string + inSeason?: boolean + indexInfo?: RawIndexInfo + healthRecommendations?: string[] + }> + plantInfo?: Array<{ + code?: string + displayName?: string + inSeason?: boolean + indexInfo?: RawIndexInfo + plantDescription?: { + type?: string + family?: string + season?: string + specialColors?: string + specialShapes?: string + crossReaction?: string + picture?: string + pictureCloseup?: string + } + }> + }) => ({ + date: { + year: day.date?.year ?? 0, + month: day.date?.month ?? 0, + day: day.date?.day ?? 0, + }, + pollenTypeInfo: (day.pollenTypeInfo || []).map((type) => ({ + code: type.code || '', + displayName: type.displayName || '', + inSeason: type.inSeason ?? null, + indexInfo: mapIndexInfo(type.indexInfo), + healthRecommendations: type.healthRecommendations || [], + })), + plantInfo: (day.plantInfo || []).map((plant) => ({ + code: plant.code || '', + displayName: plant.displayName || '', + inSeason: plant.inSeason ?? null, + indexInfo: mapIndexInfo(plant.indexInfo), + plantDescription: plant.plantDescription + ? { + type: plant.plantDescription.type || '', + family: plant.plantDescription.family || '', + season: plant.plantDescription.season || '', + specialColors: plant.plantDescription.specialColors || '', + specialShapes: plant.plantDescription.specialShapes || '', + crossReaction: plant.plantDescription.crossReaction || '', + picture: plant.plantDescription.picture || '', + pictureCloseup: plant.plantDescription.pictureCloseup || '', + } + : null, + })), + }) + ) + + return { + success: true, + output: { + regionCode: data.regionCode || '', + dailyInfo, + }, + } + }, + + outputs: { + regionCode: { + type: 'string', + description: 'Region code (ISO 3166-1 alpha-2) for the location', + }, + dailyInfo: { + type: 'array', + description: 'Daily pollen forecast entries', + items: { + type: 'object', + properties: { + date: { + type: 'object', + description: 'Calendar date of the forecast entry', + properties: { + year: { type: 'number' }, + month: { type: 'number' }, + day: { type: 'number' }, + }, + }, + pollenTypeInfo: { + type: 'array', + description: 'Pollen type indices (grass, tree, weed)', + items: { + type: 'object', + properties: { + code: { type: 'string', description: 'Pollen type code (GRASS, TREE, WEED)' }, + displayName: { type: 'string', description: 'Display name' }, + inSeason: { type: 'boolean', description: 'Whether the pollen type is in season' }, + indexInfo: { type: 'object', description: 'Universal Pollen Index (UPI) info' }, + healthRecommendations: { + type: 'array', + description: 'Health recommendations', + items: { type: 'string' }, + }, + }, + }, + }, + plantInfo: { + type: 'array', + description: 'Per-plant forecast with descriptions', + items: { + type: 'object', + properties: { + code: { type: 'string', description: 'Plant code (e.g., BIRCH, RAGWEED)' }, + displayName: { type: 'string', description: 'Display name' }, + inSeason: { type: 'boolean', description: 'Whether the plant is in season' }, + indexInfo: { type: 'object', description: 'Universal Pollen Index (UPI) info' }, + plantDescription: { + type: 'object', + description: 'Plant details (type, family, season, cross-reactions)', + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_maps/solar.ts b/apps/sim/tools/google_maps/solar.ts new file mode 100644 index 0000000000..09fd117eff --- /dev/null +++ b/apps/sim/tools/google_maps/solar.ts @@ -0,0 +1,158 @@ +import type { GoogleMapsSolarParams, GoogleMapsSolarResponse } from '@/tools/google_maps/types' +import type { ToolConfig } from '@/tools/types' + +export const googleMapsSolarTool: ToolConfig = { + id: 'google_maps_solar', + name: 'Google Maps Solar', + description: 'Get solar potential and panel insights for the building nearest a location', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Maps API key with Solar API enabled', + }, + lat: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Latitude coordinate', + }, + lng: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Longitude coordinate', + }, + requiredQuality: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Minimum imagery quality to accept (HIGH, MEDIUM, or BASE)', + }, + }, + + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.005, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + + request: { + url: (params) => { + const url = new URL('https://solar.googleapis.com/v1/buildingInsights:findClosest') + url.searchParams.set('location.latitude', params.lat.toString()) + url.searchParams.set('location.longitude', params.lng.toString()) + if (params.requiredQuality) { + url.searchParams.set('requiredQuality', params.requiredQuality) + } + url.searchParams.set('key', params.apiKey.trim()) + return url.toString() + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok || data.error) { + throw new Error(`Solar lookup failed: ${data.error?.message || response.statusText}`) + } + + const potential = data.solarPotential + const solarPotential = potential + ? { + maxArrayPanelsCount: potential.maxArrayPanelsCount ?? 0, + maxArrayAreaMeters2: potential.maxArrayAreaMeters2 ?? 0, + maxSunshineHoursPerYear: potential.maxSunshineHoursPerYear ?? 0, + carbonOffsetFactorKgPerMwh: potential.carbonOffsetFactorKgPerMwh ?? 0, + panelCapacityWatts: potential.panelCapacityWatts ?? 0, + panelHeightMeters: potential.panelHeightMeters ?? 0, + panelWidthMeters: potential.panelWidthMeters ?? 0, + panelLifetimeYears: potential.panelLifetimeYears ?? 0, + solarPanelConfigs: (potential.solarPanelConfigs || []).map( + (config: { panelsCount?: number; yearlyEnergyDcKwh?: number }) => ({ + panelsCount: config.panelsCount ?? 0, + yearlyEnergyDcKwh: config.yearlyEnergyDcKwh ?? 0, + }) + ), + } + : null + + return { + success: true, + output: { + name: data.name || '', + center: { + lat: data.center?.latitude ?? 0, + lng: data.center?.longitude ?? 0, + }, + imageryDate: data.imageryDate + ? { + year: data.imageryDate.year ?? 0, + month: data.imageryDate.month ?? 0, + day: data.imageryDate.day ?? 0, + } + : null, + imageryQuality: data.imageryQuality || '', + regionCode: data.regionCode || '', + postalCode: data.postalCode || '', + administrativeArea: data.administrativeArea || '', + solarPotential, + }, + } + }, + + outputs: { + name: { + type: 'string', + description: 'Resource name of the building (e.g., "buildings/ChIJ...")', + }, + center: { + type: 'object', + description: 'Center coordinate of the building', + properties: { + lat: { type: 'number', description: 'Latitude' }, + lng: { type: 'number', description: 'Longitude' }, + }, + }, + imageryDate: { + type: 'object', + description: 'Date the underlying imagery was captured', + }, + imageryQuality: { + type: 'string', + description: 'Quality of the imagery used (HIGH, MEDIUM, BASE)', + }, + regionCode: { + type: 'string', + description: 'Region code (ISO 3166-1 alpha-2) for the building', + }, + postalCode: { + type: 'string', + description: 'Postal code of the building', + }, + administrativeArea: { + type: 'string', + description: 'Administrative area (e.g., state or province)', + }, + solarPotential: { + type: 'object', + description: + 'Solar potential: max panel count/area, sunshine hours, carbon offset, panel specs, and configs', + }, + }, +} diff --git a/apps/sim/tools/google_maps/types.ts b/apps/sim/tools/google_maps/types.ts index 2c1170dd70..d292d40f62 100644 --- a/apps/sim/tools/google_maps/types.ts +++ b/apps/sim/tools/google_maps/types.ts @@ -479,3 +479,126 @@ export interface GoogleMapsAirQualityResponse extends ToolResponse { } | null } } + +// ============================================================================ +// Pollen +// ============================================================================ + +/** + * Calendar date returned by the Pollen and Solar APIs + */ +interface GoogleMapsDate { + year: number + month: number + day: number +} + +/** + * Universal pollen index info shared by pollen types and plants + */ +interface PollenIndexInfo { + code: string + displayName: string + value: number + category: string + indexDescription: string + color: { + red: number + green: number + blue: number + } +} + +/** + * Pollen type forecast (grass, tree, weed) + */ +interface PollenTypeInfo { + code: string + displayName: string + inSeason: boolean | null + indexInfo: PollenIndexInfo | null + healthRecommendations: string[] +} + +/** + * Individual plant forecast with optional description + */ +interface PlantInfo { + code: string + displayName: string + inSeason: boolean | null + indexInfo: PollenIndexInfo | null + plantDescription: { + type: string + family: string + season: string + specialColors: string + specialShapes: string + crossReaction: string + picture: string + pictureCloseup: string + } | null +} + +interface PollenDailyInfo { + date: GoogleMapsDate + pollenTypeInfo: PollenTypeInfo[] + plantInfo: PlantInfo[] +} + +export interface GoogleMapsPollenParams { + apiKey: string + lat: number + lng: number + days?: number + languageCode?: string + plantsDescription?: boolean +} + +export interface GoogleMapsPollenResponse extends ToolResponse { + output: { + regionCode: string + dailyInfo: PollenDailyInfo[] + } +} + +// ============================================================================ +// Solar +// ============================================================================ + +interface SolarPanelConfig { + panelsCount: number + yearlyEnergyDcKwh: number +} + +interface SolarPotential { + maxArrayPanelsCount: number + maxArrayAreaMeters2: number + maxSunshineHoursPerYear: number + carbonOffsetFactorKgPerMwh: number + panelCapacityWatts: number + panelHeightMeters: number + panelWidthMeters: number + panelLifetimeYears: number + solarPanelConfigs: SolarPanelConfig[] +} + +export interface GoogleMapsSolarParams { + apiKey: string + lat: number + lng: number + requiredQuality?: 'HIGH' | 'MEDIUM' | 'BASE' +} + +export interface GoogleMapsSolarResponse extends ToolResponse { + output: { + name: string + center: LatLng + imageryDate: GoogleMapsDate | null + imageryQuality: string + regionCode: string + postalCode: string + administrativeArea: string + solarPotential: SolarPotential | null + } +} diff --git a/apps/sim/tools/google_slides/get_thumbnail.ts b/apps/sim/tools/google_slides/get_thumbnail.ts index 1bc496892c..4ee47efbb2 100644 --- a/apps/sim/tools/google_slides/get_thumbnail.ts +++ b/apps/sim/tools/google_slides/get_thumbnail.ts @@ -30,7 +30,7 @@ interface GetThumbnailResponse { const THUMBNAIL_SIZES = ['SMALL', 'MEDIUM', 'LARGE'] as const // Available MIME types for thumbnails -const MIME_TYPES = ['PNG', 'GIF'] as const +const MIME_TYPES = ['PNG'] as const export const getThumbnailTool: ToolConfig = { id: 'google_slides_get_thumbnail', @@ -73,7 +73,7 @@ export const getThumbnailTool: ToolConfig = { google_maps_geolocate: googleMapsGeolocateTool, google_maps_place_details: googleMapsPlaceDetailsTool, google_maps_places_search: googleMapsPlacesSearchTool, + google_maps_pollen: googleMapsPollenTool, google_maps_reverse_geocode: googleMapsReverseGeocodeTool, google_maps_snap_to_roads: googleMapsSnapToRoadsTool, + google_maps_solar: googleMapsSolarTool, google_maps_speed_limits: googleMapsSpeedLimitsTool, google_maps_timezone: googleMapsTimezoneTool, google_maps_validate_address: googleMapsValidateAddressTool, From c907b1194b3857ede732ddbc29328acb1502c415 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 12:23:55 -0700 Subject: [PATCH 16/26] improvement(supabase): add Edge Functions tool; correct storage output shapes + harden tools (#5112) * improvement(supabase): add Edge Functions tool; correct storage output shapes + harden tools - Add supabase_invoke_function tool (POST/GET/PUT/PATCH/DELETE /functions/v1/{name}) - upsert: support on_conflict; storage upload: support cache-control - Fix storage copy/move/upload/delete-bucket output properties to match live API - get_public_url: build URL via directExecution (no spurious network call) - text_search: validate column identifier - Strip non-TSDoc section-label comments * fix(supabase): harden rpc/text_search identifiers; drop unused get_public_url apiKey - rpc: validate + encode functionName (SSRF/injection parity with vector_search) - text_search: validate language config interpolated into the PostgREST operator - get_public_url: remove unused apiKey param + dead auth headers (public endpoint needs no auth) - create_bucket: tighten output description to match the {name}-only response * improvement(tavily): mark optional params advanced; fix empty content output - Mark 29 optional search/extract/crawl/map subBlocks as mode: advanced (keep query/urls/url/apiKey basic) - Fix search transformResponse: populate content from result.content (was result.snippet, always empty) - Guard data.results with ?? []; correct country placeholder to a lowercase name - Rewrite stale TavilySearch/Extract response interfaces; drop dead duplicate interfaces * fix(supabase): valid Cache-Control directive on upload; clarify functionName description - storage upload: expand a bare numeric cache-control to `max-age=` (a raw number is not a valid Cache-Control header) - block: functionName input description now covers RPC, vector search, and Edge Function invoke * fix(supabase): edge-function error handling + reject array headers - invoke_function: drop unreachable !response.ok branch (executor throws on non-OK before transformResponse runs and surfaces the error body); document the success-only contract - invoke_function: ignore non-object (array) headers so JSON arrays can't produce numeric-index header names - block: reject array/non-object Edge Function headers with a clear error in config.params * fix(supabase): scope Edge Function method/body/headers to invoke_function Prevents a stale `method` value (e.g. from the Edge Function field) from leaking into other operations' params. The tool executor lets `params.method` override a tool's static verb (tools/utils.ts), so an unscoped value could turn a read into DELETE/POST against PostgREST. Now method/body/headers are only passed for the invoke_function operation. Adds a block-level regression test. * fix(supabase): only parse Edge Function body/headers for invoke_function Stale or invalid functionBody/functionHeaders left in the block (common when switching operations) were parsed and validated for every operation, so they could throw before unrelated tools ran even while hidden. Moved parsing and validation inside the invoke_function guard; added a regression test. * fix(supabase): include last_accessed_at as a storage list sort option The Storage list API accepts last_accessed_at for sortBy; add it to the tool description and the block dropdown so the surfaced options match the API. --- .../tools/supabase/storage-upload/route.ts | 7 + apps/sim/blocks/blocks/supabase.test.ts | 75 ++++++++ apps/sim/blocks/blocks/supabase.ts | 169 +++++++++++++----- apps/sim/blocks/blocks/tavily.ts | 31 +++- .../api/contracts/tools/databases/supabase.ts | 1 + apps/sim/tools/registry.ts | 2 + apps/sim/tools/supabase/index.ts | 2 + apps/sim/tools/supabase/invoke_function.ts | 137 ++++++++++++++ apps/sim/tools/supabase/rpc.ts | 5 +- apps/sim/tools/supabase/storage_copy.ts | 6 +- .../tools/supabase/storage_create_bucket.ts | 2 +- .../tools/supabase/storage_delete_bucket.ts | 4 +- .../tools/supabase/storage_get_public_url.ts | 37 ++-- apps/sim/tools/supabase/storage_list.ts | 3 +- apps/sim/tools/supabase/storage_upload.ts | 8 + apps/sim/tools/supabase/text_search.ts | 4 + apps/sim/tools/supabase/types.ts | 52 +++--- apps/sim/tools/supabase/upsert.ts | 13 +- apps/sim/tools/tavily/search.ts | 4 +- apps/sim/tools/tavily/types.ts | 90 ++++------ 20 files changed, 490 insertions(+), 162 deletions(-) create mode 100644 apps/sim/blocks/blocks/supabase.test.ts create mode 100644 apps/sim/tools/supabase/invoke_function.ts diff --git a/apps/sim/app/api/tools/supabase/storage-upload/route.ts b/apps/sim/app/api/tools/supabase/storage-upload/route.ts index e36f88c501..9a3a2643c5 100644 --- a/apps/sim/app/api/tools/supabase/storage-upload/route.ts +++ b/apps/sim/app/api/tools/supabase/storage-upload/route.ts @@ -174,6 +174,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 'Content-Type': uploadContentType, } + if (validatedData.cacheControl) { + const cacheControl = validatedData.cacheControl.trim() + headers['cache-control'] = /^\d+$/.test(cacheControl) + ? `max-age=${cacheControl}` + : cacheControl + } + if (validatedData.upsert) { headers['x-upsert'] = 'true' } diff --git a/apps/sim/blocks/blocks/supabase.test.ts b/apps/sim/blocks/blocks/supabase.test.ts new file mode 100644 index 0000000000..baaa396d86 --- /dev/null +++ b/apps/sim/blocks/blocks/supabase.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { SupabaseBlock } from '@/blocks/blocks/supabase' + +describe('SupabaseBlock', () => { + const buildParams = SupabaseBlock.tools.config.params! + const selectTool = SupabaseBlock.tools.config.tool! + + it('maps each operation to its tool id', () => { + expect(selectTool({ operation: 'query' })).toBe('supabase_query') + expect(selectTool({ operation: 'invoke_function' })).toBe('supabase_invoke_function') + expect(selectTool({ operation: 'delete' })).toBe('supabase_delete') + }) + + it('does not leak the Edge Function method onto other operations', () => { + // A stale `method` from the Edge Function field must never reach a tool with a + // static verb — otherwise the executor would let it override e.g. GET with DELETE. + const params = buildParams({ + operation: 'query', + projectId: 'proj', + apiKey: 'key', + table: 'users', + method: 'DELETE', + }) + + expect(params).not.toHaveProperty('method') + expect(params).not.toHaveProperty('body') + expect(params).not.toHaveProperty('headers') + }) + + it('ignores stale invalid Edge Function fields on other operations', () => { + // Hidden Edge Function inputs left over from a prior selection must not be + // parsed/validated (and must never throw) for unrelated operations. + expect(() => + buildParams({ + operation: 'query', + projectId: 'proj', + apiKey: 'key', + table: 'users', + functionBody: '{not valid json', + functionHeaders: '["a","b"]', + }) + ).not.toThrow() + }) + + it('passes method, body, and headers through for invoke_function', () => { + const params = buildParams({ + operation: 'invoke_function', + projectId: 'proj', + apiKey: 'key', + functionName: 'hello-world', + method: 'POST', + functionBody: '{"name":"world"}', + functionHeaders: '{"x-trace":"1"}', + }) + + expect(params.method).toBe('POST') + expect(params.body).toEqual({ name: 'world' }) + expect(params.headers).toEqual({ 'x-trace': '1' }) + }) + + it('rejects non-object Edge Function headers', () => { + expect(() => + buildParams({ + operation: 'invoke_function', + projectId: 'proj', + apiKey: 'key', + functionName: 'hello-world', + functionHeaders: '["a","b"]', + }) + ).toThrow('Edge Function headers must be a JSON object') + }) +}) diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index f4f3aa9c9d..9f0d5eac72 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -13,7 +13,7 @@ export const SupabaseBlock: BlockConfig = { description: 'Use Supabase database', authMode: AuthMode.ApiKey, longDescription: - 'Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets).', + 'Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, Edge Function invocation, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets).', docsLink: 'https://docs.sim.ai/integrations/supabase', category: 'tools', integrationType: IntegrationType.Databases, @@ -25,7 +25,6 @@ export const SupabaseBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ - // Database Operations { label: 'Get Many Rows', id: 'query' }, { label: 'Get a Row', id: 'get_row' }, { label: 'Create a Row', id: 'insert' }, @@ -33,12 +32,11 @@ export const SupabaseBlock: BlockConfig = { { label: 'Delete a Row', id: 'delete' }, { label: 'Upsert a Row', id: 'upsert' }, { label: 'Count Rows', id: 'count' }, - // Advanced Database Operations { label: 'Full-Text Search', id: 'text_search' }, { label: 'Vector Search', id: 'vector_search' }, { label: 'Call RPC Function', id: 'rpc' }, + { label: 'Invoke Edge Function', id: 'invoke_function' }, { label: 'Introspect Schema', id: 'introspect' }, - // Storage - File Operations { label: 'Storage: Upload File', id: 'storage_upload' }, { label: 'Storage: Download File', id: 'storage_download' }, { label: 'Storage: List Files', id: 'storage_list' }, @@ -47,7 +45,6 @@ export const SupabaseBlock: BlockConfig = { { label: 'Storage: Copy File', id: 'storage_copy' }, { label: 'Storage: Get Public URL', id: 'storage_get_public_url' }, { label: 'Storage: Create Signed URL', id: 'storage_create_signed_url' }, - // Storage - Bucket Operations { label: 'Storage: Create Bucket', id: 'storage_create_bucket' }, { label: 'Storage: List Buckets', id: 'storage_list_buckets' }, { label: 'Storage: Delete Bucket', id: 'storage_delete_bucket' }, @@ -101,7 +98,6 @@ export const SupabaseBlock: BlockConfig = { password: true, required: true, }, - // Data input for create/update operations { id: 'data', title: 'Data', @@ -126,7 +122,14 @@ export const SupabaseBlock: BlockConfig = { condition: { field: 'operation', value: 'upsert' }, required: true, }, - // Filter for get_row, update, delete operations (required) + { + id: 'onConflict', + title: 'On Conflict (column)', + type: 'short-input', + placeholder: 'email (defaults to primary key)', + condition: { field: 'operation', value: 'upsert' }, + mode: 'advanced', + }, { id: 'filter', title: 'Filter (PostgREST syntax)', @@ -331,7 +334,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e generationType: 'postgrest', }, }, - // Optional filter for query operation { id: 'filter', title: 'Filter (PostgREST syntax)', @@ -399,7 +401,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e generationType: 'postgrest', }, }, - // Optional order by for query operation { id: 'orderBy', title: 'Order By', @@ -439,7 +440,6 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: 'Describe how to sort (e.g., "newest first by created_at")...', }, }, - // Optional limit for query operation { id: 'limit', title: 'Limit', @@ -454,7 +454,6 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: '0', condition: { field: 'operation', value: 'query' }, }, - // Vector search operation fields { id: 'functionName', title: 'Function Name', @@ -485,7 +484,6 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: '10', condition: { field: 'operation', value: 'vector_search' }, }, - // RPC operation fields { id: 'functionName', title: 'Function Name', @@ -501,7 +499,43 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: '{\n "param1": "value1",\n "param2": "value2"\n}', condition: { field: 'operation', value: 'rpc' }, }, - // Introspect operation fields + { + id: 'functionName', + title: 'Function Name', + type: 'short-input', + placeholder: 'hello-world', + condition: { field: 'operation', value: 'invoke_function' }, + required: true, + }, + { + id: 'method', + title: 'HTTP Method', + type: 'dropdown', + options: [ + { label: 'POST', id: 'POST' }, + { label: 'GET', id: 'GET' }, + { label: 'PUT', id: 'PUT' }, + { label: 'PATCH', id: 'PATCH' }, + { label: 'DELETE', id: 'DELETE' }, + ], + value: () => 'POST', + condition: { field: 'operation', value: 'invoke_function' }, + }, + { + id: 'functionBody', + title: 'Request Body (JSON)', + type: 'code', + placeholder: '{\n "name": "world"\n}', + condition: { field: 'operation', value: 'invoke_function' }, + }, + { + id: 'functionHeaders', + title: 'Headers (JSON)', + type: 'code', + placeholder: '{\n "x-custom-header": "value"\n}', + condition: { field: 'operation', value: 'invoke_function' }, + mode: 'advanced', + }, { id: 'schema', title: 'Schema', @@ -509,7 +543,6 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: 'public (leave empty for all user schemas)', condition: { field: 'operation', value: 'introspect' }, }, - // Text Search operation fields { id: 'column', title: 'Column to Search', @@ -559,7 +592,6 @@ Return ONLY the order by expression - no explanations, no extra text.`, placeholder: '0', condition: { field: 'operation', value: 'text_search' }, }, - // Count operation fields { id: 'filter', title: 'Filter (PostgREST syntax)', @@ -639,7 +671,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e value: () => 'exact', condition: { field: 'operation', value: 'count' }, }, - // Storage bucket field (for all storage operations except list_buckets) { id: 'bucket', title: 'Bucket Name', @@ -662,7 +693,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e }, required: true, }, - // Storage Upload fields { id: 'fileName', title: 'File Name', @@ -706,6 +736,14 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e placeholder: 'image/jpeg', condition: { field: 'operation', value: 'storage_upload' }, }, + { + id: 'cacheControl', + title: 'Cache Control (seconds)', + type: 'short-input', + placeholder: '3600', + condition: { field: 'operation', value: 'storage_upload' }, + mode: 'advanced', + }, { id: 'upsert', title: 'Upsert (overwrite if exists)', @@ -717,7 +755,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e value: () => 'false', condition: { field: 'operation', value: 'storage_upload' }, }, - // Storage Download fields { id: 'path', title: 'File Path', @@ -733,7 +770,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e placeholder: 'my-file.jpg', condition: { field: 'operation', value: 'storage_download' }, }, - // Storage List fields { id: 'path', title: 'Folder Path', @@ -763,6 +799,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e { label: 'Name', id: 'name' }, { label: 'Created At', id: 'created_at' }, { label: 'Updated At', id: 'updated_at' }, + { label: 'Last Accessed At', id: 'last_accessed_at' }, ], value: () => 'name', condition: { field: 'operation', value: 'storage_list' }, @@ -785,7 +822,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e placeholder: 'search term', condition: { field: 'operation', value: 'storage_list' }, }, - // Storage Delete fields { id: 'paths', title: 'File Paths (JSON array)', @@ -794,7 +830,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e condition: { field: 'operation', value: 'storage_delete' }, required: true, }, - // Storage Move fields { id: 'fromPath', title: 'From Path', @@ -811,7 +846,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e condition: { field: 'operation', value: 'storage_move' }, required: true, }, - // Storage Copy fields { id: 'fromPath', title: 'From Path', @@ -828,7 +862,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e condition: { field: 'operation', value: 'storage_copy' }, required: true, }, - // Storage Get Public URL fields { id: 'path', title: 'File Path', @@ -848,7 +881,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e value: () => 'false', condition: { field: 'operation', value: 'storage_get_public_url' }, }, - // Storage Create Signed URL fields { id: 'path', title: 'File Path', @@ -876,7 +908,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e value: () => 'false', condition: { field: 'operation', value: 'storage_create_signed_url' }, }, - // Storage Create Bucket fields { id: 'isPublic', title: 'Public Bucket', @@ -915,6 +946,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e 'supabase_text_search', 'supabase_vector_search', 'supabase_rpc', + 'supabase_invoke_function', 'supabase_introspect', 'supabase_storage_upload', 'supabase_storage_download', @@ -951,6 +983,8 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e return 'supabase_vector_search' case 'rpc': return 'supabase_rpc' + case 'invoke_function': + return 'supabase_invoke_function' case 'introspect': return 'supabase_introspect' case 'storage_upload': @@ -991,22 +1025,21 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e upsert, download, fileData, + functionBody, + functionHeaders, + method, ...rest } = params - // Normalize file input for storage_upload operation - // fileData is the canonical param for both basic (file) and advanced (fileContent) modes const normalizedFileData = normalizeFileInput(fileData, { single: true, }) - // Parse JSON data if it's a string let parsedData if (data && typeof data === 'string' && data.trim()) { try { parsedData = JSON.parse(data) } catch (parseError) { - // Provide more detailed error information const errorMsg = getErrorMessage(parseError, 'Unknown JSON error') throw new Error( `Invalid JSON data format: ${errorMsg}. Please check your JSON syntax (e.g., strings must be quoted like "value").` @@ -1016,13 +1049,11 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedData = data } - // Handle filter - just pass through PostgREST syntax let parsedFilter if (filter && typeof filter === 'string' && filter.trim()) { parsedFilter = filter.trim() } - // Handle query embedding for vector search let parsedQueryEmbedding if (queryEmbedding && typeof queryEmbedding === 'string' && queryEmbedding.trim()) { try { @@ -1037,7 +1068,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedQueryEmbedding = queryEmbedding } - // Handle RPC params let parsedRpcParams if (rpcParams && typeof rpcParams === 'string' && rpcParams.trim()) { try { @@ -1052,7 +1082,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedRpcParams = rpcParams } - // Handle paths array for storage delete let parsedPaths if (paths && typeof paths === 'string' && paths.trim()) { try { @@ -1067,7 +1096,6 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedPaths = paths } - // Handle allowedMimeTypes array let parsedAllowedMimeTypes if (allowedMimeTypes && typeof allowedMimeTypes === 'string' && allowedMimeTypes.trim()) { try { @@ -1082,12 +1110,10 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedAllowedMimeTypes = allowedMimeTypes } - // Convert string booleans to actual booleans const parsedUpsert = upsert === 'true' || upsert === true const parsedDownload = download === 'true' || download === true const parsedIsPublic = rest.isPublic === 'true' || rest.isPublic === true - // Build params object, only including defined values const result = { ...rest } if (parsedData !== undefined) { @@ -1106,6 +1132,54 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e result.params = parsedRpcParams } + if (operation === 'invoke_function') { + if (method !== undefined) { + result.method = method + } + + if (functionBody && typeof functionBody === 'string' && functionBody.trim()) { + try { + result.body = JSON.parse(functionBody) + } catch (parseError) { + const errorMsg = getErrorMessage(parseError, 'Unknown JSON error') + throw new Error( + `Invalid Edge Function body format: ${errorMsg}. Please provide a valid JSON object.` + ) + } + } else if (functionBody && typeof functionBody === 'object') { + result.body = functionBody + } + + if (functionHeaders) { + let parsedHeaders + if (typeof functionHeaders === 'string' && functionHeaders.trim()) { + try { + parsedHeaders = JSON.parse(functionHeaders) + } catch (parseError) { + const errorMsg = getErrorMessage(parseError, 'Unknown JSON error') + throw new Error( + `Invalid Edge Function headers format: ${errorMsg}. Please provide a valid JSON object.` + ) + } + } else if (typeof functionHeaders === 'object') { + parsedHeaders = functionHeaders + } + + if (parsedHeaders !== undefined) { + if ( + typeof parsedHeaders !== 'object' || + parsedHeaders === null || + Array.isArray(parsedHeaders) + ) { + throw new Error( + 'Edge Function headers must be a JSON object of header name to value (not an array).' + ) + } + result.headers = parsedHeaders + } + } + } + if (parsedPaths !== undefined) { result.paths = parsedPaths } @@ -1140,38 +1214,35 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e table: { type: 'string', description: 'Database table name' }, select: { type: 'string', description: 'Columns to return (comma-separated, defaults to *)' }, apiKey: { type: 'string', description: 'Service role secret key' }, - // Data for insert/update operations data: { type: 'json', description: 'Row data' }, - // Filter for operations filter: { type: 'string', description: 'PostgREST filter syntax' }, - // Query operation inputs orderBy: { type: 'string', description: 'Sort column' }, limit: { type: 'number', description: 'Result limit' }, offset: { type: 'number', description: 'Number of rows to skip' }, - // Vector search operation inputs functionName: { type: 'string', - description: 'PostgreSQL function name for vector search or RPC', + description: + 'Function name — PostgreSQL function for RPC or vector search, or Edge Function name to invoke', }, queryEmbedding: { type: 'array', description: 'Query vector/embedding for similarity search' }, matchThreshold: { type: 'number', description: 'Minimum similarity threshold (0-1)' }, matchCount: { type: 'number', description: 'Maximum number of similar results to return' }, - // RPC operation inputs params: { type: 'json', description: 'Parameters to pass to RPC function' }, - // Text search inputs + method: { type: 'string', description: 'HTTP method for the Edge Function request' }, + body: { type: 'json', description: 'Request body to send to the Edge Function' }, + headers: { type: 'json', description: 'Additional headers for the Edge Function request' }, + onConflict: { type: 'string', description: 'Conflict target column(s) for upsert' }, column: { type: 'string', description: 'Column name to search in' }, query: { type: 'string', description: 'Search query' }, searchType: { type: 'string', description: 'Search type: plain, phrase, or websearch' }, language: { type: 'string', description: 'Language for text search' }, - // Count operation inputs countType: { type: 'string', description: 'Count type: exact, planned, or estimated' }, - // Introspect operation inputs schema: { type: 'string', description: 'Database schema to introspect (e.g., public)' }, - // Storage operation inputs bucket: { type: 'string', description: 'Storage bucket name' }, path: { type: 'string', description: 'File or folder path in storage' }, fileData: { type: 'json', description: 'File data (UserFile)' }, contentType: { type: 'string', description: 'MIME type of the file' }, + cacheControl: { type: 'string', description: 'Cache-Control max-age in seconds for upload' }, fileName: { type: 'string', description: 'File name for upload or download override' }, upsert: { type: 'boolean', description: 'Whether to overwrite existing file' }, download: { type: 'boolean', description: 'Whether to force download' }, @@ -1325,5 +1396,11 @@ export const SupabaseBlockMeta = { content: '# Upload a File to Supabase Storage\n\nStore a generated or received file in a bucket and hand back a shareable link.\n\n## Steps\n1. Use Storage: Upload File with the bucket name, file name, and the file reference from a previous block.\n2. Set an optional Folder Path and Content Type, and enable Upsert if you want to overwrite an existing object.\n3. For a permanent link on a public bucket use Storage: Get Public URL with the file path.\n4. For private buckets use Storage: Create Signed URL with an Expires In value such as 3600 seconds.\n\n## Output\nReturn the stored object path plus the public or signed URL so later steps can reference or share the file.', }, + { + name: 'invoke-edge-function', + description: 'Call a deployed Supabase Edge Function over HTTP and use its JSON response.', + content: + '# Invoke a Supabase Edge Function\n\nRun server-side logic deployed as a Supabase Edge Function and feed its result into the workflow.\n\n## Steps\n1. Use the Invoke Edge Function operation with the project ID, service role secret, and the function name (for example hello-world).\n2. Choose the HTTP Method (defaults to POST) and provide a JSON Request Body the function expects.\n3. Add optional custom Headers as a JSON object when the function reads specific headers.\n4. This is different from Call RPC Function, which runs a PostgreSQL function inside the database rather than deployed function code.\n\n## Output\nReturn the function response body as JSON so downstream steps can branch on or transform the result.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index 1ca4e33853..b2dd31a996 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -41,6 +41,7 @@ export const TavilyBlock: BlockConfig = { title: 'Max Results', type: 'short-input', placeholder: '5', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -53,6 +54,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Finance', id: 'finance' }, ], value: () => 'general', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -64,6 +66,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Advanced', id: 'advanced' }, ], value: () => 'basic', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -76,6 +79,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Advanced', id: 'advanced' }, ], value: () => '', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -88,24 +92,28 @@ export const TavilyBlock: BlockConfig = { { label: 'Text', id: 'text' }, ], value: () => '', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { id: 'include_images', title: 'Include Images', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { id: 'include_image_descriptions', title: 'Include Image Descriptions', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { id: 'include_favicon', title: 'Include Favicon', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -120,6 +128,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Year', id: 'y' }, ], value: () => '', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -127,6 +136,7 @@ export const TavilyBlock: BlockConfig = { title: 'Include Domains', type: 'long-input', placeholder: 'example.com, another.com (comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -134,13 +144,15 @@ export const TavilyBlock: BlockConfig = { title: 'Exclude Domains', type: 'long-input', placeholder: 'example.com, another.com (comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { id: 'country', title: 'Country', type: 'short-input', - placeholder: 'US', + placeholder: 'united states', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_search' }, }, { @@ -160,6 +172,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Advanced', id: 'advanced' }, ], value: () => 'basic', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_extract' }, }, { @@ -171,18 +184,21 @@ export const TavilyBlock: BlockConfig = { { label: 'Text', id: 'text' }, ], value: () => 'markdown', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_extract' }, }, { id: 'include_images', title: 'Include Images', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_extract' }, }, { id: 'include_favicon', title: 'Include Favicon', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_extract' }, }, { @@ -198,6 +214,7 @@ export const TavilyBlock: BlockConfig = { title: 'Instructions', type: 'long-input', placeholder: 'Natural language directions for the crawler...', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -205,6 +222,7 @@ export const TavilyBlock: BlockConfig = { title: 'Max Depth', type: 'short-input', placeholder: '1', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -212,6 +230,7 @@ export const TavilyBlock: BlockConfig = { title: 'Max Breadth', type: 'short-input', placeholder: '20', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -219,6 +238,7 @@ export const TavilyBlock: BlockConfig = { title: 'Limit', type: 'short-input', placeholder: '50', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -226,6 +246,7 @@ export const TavilyBlock: BlockConfig = { title: 'Select Paths', type: 'long-input', placeholder: '/docs/.*, /api/.* (regex patterns, comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -233,6 +254,7 @@ export const TavilyBlock: BlockConfig = { title: 'Select Domains', type: 'long-input', placeholder: '^docs\\.example\\.com$ (regex patterns, comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -240,6 +262,7 @@ export const TavilyBlock: BlockConfig = { title: 'Exclude Paths', type: 'long-input', placeholder: '/private/.*, /admin/.* (regex patterns, comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { @@ -247,18 +270,21 @@ export const TavilyBlock: BlockConfig = { title: 'Exclude Domains', type: 'long-input', placeholder: '^private\\.example\\.com$ (regex patterns, comma-separated)', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { id: 'allow_external', title: 'Allow External Links', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, }, { id: 'include_images', title: 'Include Images', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_crawl' }, }, { @@ -270,6 +296,7 @@ export const TavilyBlock: BlockConfig = { { label: 'Advanced', id: 'advanced' }, ], value: () => 'basic', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_crawl' }, }, { @@ -281,12 +308,14 @@ export const TavilyBlock: BlockConfig = { { label: 'Text', id: 'text' }, ], value: () => 'markdown', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_crawl' }, }, { id: 'include_favicon', title: 'Include Favicon', type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: 'tavily_crawl' }, }, { diff --git a/apps/sim/lib/api/contracts/tools/databases/supabase.ts b/apps/sim/lib/api/contracts/tools/databases/supabase.ts index ccadc74a94..acb24feca7 100644 --- a/apps/sim/lib/api/contracts/tools/databases/supabase.ts +++ b/apps/sim/lib/api/contracts/tools/databases/supabase.ts @@ -18,6 +18,7 @@ export const supabaseStorageUploadBodySchema = z.object({ path: z.string().optional().nullable(), fileData: FileInputSchema, contentType: z.string().optional().nullable(), + cacheControl: z.string().optional().nullable(), upsert: z.boolean().optional().default(false), }) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index e569e1e96a..18ec21ad39 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3311,6 +3311,7 @@ import { supabaseGetRowTool, supabaseInsertTool, supabaseIntrospectTool, + supabaseInvokeFunctionTool, supabaseQueryTool, supabaseRpcTool, supabaseStorageCopyTool, @@ -4429,6 +4430,7 @@ export const tools: Record = { supabase_vector_search: supabaseVectorSearchTool, supabase_rpc: supabaseRpcTool, supabase_introspect: supabaseIntrospectTool, + supabase_invoke_function: supabaseInvokeFunctionTool, supabase_storage_upload: supabaseStorageUploadTool, supabase_storage_download: supabaseStorageDownloadTool, supabase_storage_list: supabaseStorageListTool, diff --git a/apps/sim/tools/supabase/index.ts b/apps/sim/tools/supabase/index.ts index d4fed32da3..2db28f8031 100644 --- a/apps/sim/tools/supabase/index.ts +++ b/apps/sim/tools/supabase/index.ts @@ -3,6 +3,7 @@ import { deleteTool } from '@/tools/supabase/delete' import { getRowTool } from '@/tools/supabase/get_row' import { insertTool } from '@/tools/supabase/insert' import { introspectTool } from '@/tools/supabase/introspect' +import { invokeFunctionTool } from '@/tools/supabase/invoke_function' import { queryTool } from '@/tools/supabase/query' import { rpcTool } from '@/tools/supabase/rpc' import { storageCopyTool } from '@/tools/supabase/storage_copy' @@ -30,6 +31,7 @@ export const supabaseUpsertTool = upsertTool export const supabaseVectorSearchTool = vectorSearchTool export const supabaseRpcTool = rpcTool export const supabaseIntrospectTool = introspectTool +export const supabaseInvokeFunctionTool = invokeFunctionTool export const supabaseTextSearchTool = textSearchTool export const supabaseCountTool = countTool export const supabaseStorageUploadTool = storageUploadTool diff --git a/apps/sim/tools/supabase/invoke_function.ts b/apps/sim/tools/supabase/invoke_function.ts new file mode 100644 index 0000000000..4b02f65bd8 --- /dev/null +++ b/apps/sim/tools/supabase/invoke_function.ts @@ -0,0 +1,137 @@ +import type { + SupabaseInvokeFunctionParams, + SupabaseInvokeFunctionResponse, +} from '@/tools/supabase/types' +import { supabaseBaseUrl } from '@/tools/supabase/utils' +import type { HttpMethod, ToolConfig } from '@/tools/types' + +const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) + +function resolveMethod(method?: string): HttpMethod { + const normalized = (method || 'POST').toUpperCase() as HttpMethod + return ALLOWED_METHODS.has(normalized) ? normalized : 'POST' +} + +/** + * Edge Function names are URL path segments and may contain letters, digits, + * underscores, and hyphens (e.g. `hello-world`). Reject anything else to + * prevent path traversal / injection. + */ +function validateFunctionName(name: string): string { + const trimmed = name?.trim() + if (!trimmed || !/^[A-Za-z0-9_-]+$/.test(trimmed)) { + throw new Error( + 'Invalid function name: must contain only letters, digits, underscores, and hyphens' + ) + } + return trimmed +} + +export const invokeFunctionTool: ToolConfig< + SupabaseInvokeFunctionParams, + SupabaseInvokeFunctionResponse +> = { + id: 'supabase_invoke_function', + name: 'Supabase Invoke Edge Function', + description: 'Invoke a Supabase Edge Function over HTTP', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + functionName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the Edge Function to invoke (e.g., "hello-world")', + }, + method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'HTTP method to use: GET, POST, PUT, PATCH, or DELETE (default: POST)', + }, + body: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Request payload to send to the function as a JSON object (ignored for GET)', + }, + headers: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Additional request headers as a JSON object of header name to value', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + const functionName = validateFunctionName(params.functionName) + return `${supabaseBaseUrl(params.projectId)}/functions/v1/${functionName}` + }, + method: (params) => resolveMethod(params.method), + headers: (params) => { + const headers: Record = { + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + } + if (params.headers && typeof params.headers === 'object' && !Array.isArray(params.headers)) { + for (const [key, value] of Object.entries(params.headers)) { + headers[key] = String(value) + } + } + return headers + }, + body: (params) => { + if (resolveMethod(params.method) === 'GET') { + return undefined + } + return params.body ?? {} + }, + }, + + /** + * Only the success path is handled here — the tool executor throws on + * non-OK responses before `transformResponse` runs, surfacing the Edge + * Function's error body via the shared error extractor. + */ + transformResponse: async (response: Response) => { + const contentType = response.headers.get('content-type') || '' + let results: unknown + if (contentType.includes('application/json')) { + try { + results = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase Edge Function response: ${parseError}`) + } + } else { + results = await response.text() + } + + return { + success: true, + output: { + message: 'Successfully invoked Edge Function', + results, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { type: 'json', description: 'Response body returned by the Edge Function' }, + }, +} diff --git a/apps/sim/tools/supabase/rpc.ts b/apps/sim/tools/supabase/rpc.ts index d05271a7b3..c9295c0854 100644 --- a/apps/sim/tools/supabase/rpc.ts +++ b/apps/sim/tools/supabase/rpc.ts @@ -1,3 +1,4 @@ +import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation' import type { SupabaseRpcParams, SupabaseRpcResponse } from '@/tools/supabase/types' import { supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' @@ -37,7 +38,9 @@ export const rpcTool: ToolConfig = { request: { url: (params) => { - return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${params.functionName}` + const fnValidation = validateDatabaseIdentifier(params.functionName, 'functionName') + if (!fnValidation.isValid) throw new Error(fnValidation.error) + return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${encodeURIComponent(params.functionName)}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_copy.ts b/apps/sim/tools/supabase/storage_copy.ts index 103f242ab4..027a03b823 100644 --- a/apps/sim/tools/supabase/storage_copy.ts +++ b/apps/sim/tools/supabase/storage_copy.ts @@ -1,5 +1,5 @@ import { - STORAGE_MOVE_OUTPUT_PROPERTIES, + STORAGE_COPY_OUTPUT_PROPERTIES, type SupabaseStorageCopyParams, type SupabaseStorageCopyResponse, } from '@/tools/supabase/types' @@ -86,8 +86,8 @@ export const storageCopyTool: ToolConfig `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${params.bucket}`, - method: 'GET', - headers: (params) => ({ - apikey: params.apiKey, - Authorization: `Bearer ${params.apiKey}`, - }), - }, - - transformResponse: async (response: Response, params?: SupabaseStorageGetPublicUrlParams) => { - if (!params?.projectId) { - throw new Error('projectId is required to construct the public URL') - } + /** + * Public URLs are deterministic and built entirely from the project ID, + * bucket, and path — no network request is required. `directExecution` + * short-circuits the HTTP request so we never hit the API just to discard + * its response. + */ + directExecution: async (params: SupabaseStorageGetPublicUrlParams) => { let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${params.bucket}/${params.path}` - if (params?.download) { + if (params.download) { publicUrl += '?download=true' } @@ -70,12 +58,19 @@ export const storageGetPublicUrlTool: ToolConfig< success: true, output: { message: 'Successfully generated public URL', - publicUrl: publicUrl, + publicUrl, }, error: undefined, } }, + request: { + url: (params) => + `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${params.bucket}/${params.path}`, + method: 'GET', + headers: () => ({}), + }, + outputs: { message: { type: 'string', description: 'Operation status message' }, publicUrl: { diff --git a/apps/sim/tools/supabase/storage_list.ts b/apps/sim/tools/supabase/storage_list.ts index fc7599156d..fd13ca475c 100644 --- a/apps/sim/tools/supabase/storage_list.ts +++ b/apps/sim/tools/supabase/storage_list.ts @@ -47,7 +47,8 @@ export const storageListTool: ToolConfig { const tableValidation = validateDatabaseIdentifier(params.table, 'table') if (!tableValidation.isValid) throw new Error(tableValidation.error) + const columnValidation = validateDatabaseIdentifier(params.column, 'column') + if (!columnValidation.isValid) throw new Error(columnValidation.error) const searchType = params.searchType || 'websearch' const language = params.language || 'english' + const languageValidation = validateDatabaseIdentifier(language, 'language') + if (!languageValidation.isValid) throw new Error(languageValidation.error) let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*` diff --git a/apps/sim/tools/supabase/types.ts b/apps/sim/tools/supabase/types.ts index bb99e72cde..e8d126701d 100644 --- a/apps/sim/tools/supabase/types.ts +++ b/apps/sim/tools/supabase/types.ts @@ -84,30 +84,38 @@ export const STORAGE_BUCKET_OUTPUT: OutputProperty = { } /** - * Output definition for storage upload response + * Output definition for storage upload response. + * The Supabase Storage REST API returns `{ Id, Key }` (Key required); Sim's + * upload route augments this with `path`, `bucket`, and `publicUrl`. * @see https://supabase.com/docs/reference/javascript/storage-from-upload */ export const STORAGE_UPLOAD_OUTPUT_PROPERTIES = { - id: { type: 'string', description: 'Unique identifier for the uploaded file' }, + Id: { type: 'string', description: 'Unique identifier for the uploaded file', optional: true }, + Key: { type: 'string', description: 'Full object key including bucket name' }, path: { type: 'string', description: 'Path to the uploaded file within the bucket' }, - fullPath: { type: 'string', description: 'Full path including bucket name' }, + bucket: { type: 'string', description: 'Name of the bucket the file was uploaded to' }, + publicUrl: { type: 'string', description: 'Public URL for the uploaded file' }, } as const satisfies Record /** - * Output definition for storage move/copy response + * Output definition for storage move response. + * The move endpoint returns `{ message, Id, Key }`. * @see https://supabase.com/docs/reference/javascript/storage-from-move */ export const STORAGE_MOVE_OUTPUT_PROPERTIES = { message: { type: 'string', description: 'Operation status message' }, + Id: { type: 'string', description: 'Identifier of the destination object', optional: true }, + Key: { type: 'string', description: 'Full object key of the destination', optional: true }, } as const satisfies Record /** - * Output definition for storage copy response - * Returns: { path: string } - * @see https://github.com/supabase/storage-js/blob/main/src/packages/StorageFileApi.ts + * Output definition for storage copy response. + * The copy endpoint returns `{ Id, Key }` (Key required; Id deprecated). + * @see https://github.com/supabase/storage/blob/master/src/http/routes/object/copyObject.ts */ export const STORAGE_COPY_OUTPUT_PROPERTIES = { - path: { type: 'string', description: 'Path to the copied file' }, + Key: { type: 'string', description: 'Full object key of the copied file' }, + Id: { type: 'string', description: 'Identifier of the copied object', optional: true }, } as const satisfies Record /** @@ -358,6 +366,7 @@ export interface SupabaseUpsertParams { table: string schema?: string data: any + onConflict?: string } export interface SupabaseVectorSearchParams { @@ -393,7 +402,6 @@ export interface SupabaseVectorSearchResponse extends SupabaseBaseResponse {} export interface SupabaseResponse extends SupabaseBaseResponse {} -// RPC types export interface SupabaseRpcParams { apiKey: string projectId: string @@ -403,7 +411,6 @@ export interface SupabaseRpcParams { export interface SupabaseRpcResponse extends SupabaseBaseResponse {} -// Text Search types export interface SupabaseTextSearchParams { apiKey: string projectId: string @@ -419,7 +426,6 @@ export interface SupabaseTextSearchParams { export interface SupabaseTextSearchResponse extends SupabaseBaseResponse {} -// Count types export interface SupabaseCountParams { apiKey: string projectId: string @@ -437,7 +443,17 @@ export interface SupabaseCountResponse extends ToolResponse { error?: string } -// Storage Upload types +export interface SupabaseInvokeFunctionParams { + apiKey: string + projectId: string + functionName: string + method?: string + body?: any + headers?: Record +} + +export interface SupabaseInvokeFunctionResponse extends SupabaseBaseResponse {} + export interface SupabaseStorageUploadParams { apiKey: string projectId: string @@ -446,12 +462,12 @@ export interface SupabaseStorageUploadParams { path?: string fileData: UserFile | string contentType?: string + cacheControl?: string upsert?: boolean } export interface SupabaseStorageUploadResponse extends SupabaseBaseResponse {} -// Storage Download types export interface SupabaseStorageDownloadParams { apiKey: string projectId: string @@ -472,7 +488,6 @@ export interface SupabaseStorageDownloadResponse extends ToolResponse { error?: string } -// Storage List types export interface SupabaseStorageListParams { apiKey: string projectId: string @@ -487,7 +502,6 @@ export interface SupabaseStorageListParams { export interface SupabaseStorageListResponse extends SupabaseBaseResponse {} -// Storage Delete types export interface SupabaseStorageDeleteParams { apiKey: string projectId: string @@ -497,7 +511,6 @@ export interface SupabaseStorageDeleteParams { export interface SupabaseStorageDeleteResponse extends SupabaseBaseResponse {} -// Storage Move types export interface SupabaseStorageMoveParams { apiKey: string projectId: string @@ -508,7 +521,6 @@ export interface SupabaseStorageMoveParams { export interface SupabaseStorageMoveResponse extends SupabaseBaseResponse {} -// Storage Copy types export interface SupabaseStorageCopyParams { apiKey: string projectId: string @@ -519,7 +531,6 @@ export interface SupabaseStorageCopyParams { export interface SupabaseStorageCopyResponse extends SupabaseBaseResponse {} -// Storage Create Bucket types export interface SupabaseStorageCreateBucketParams { apiKey: string projectId: string @@ -531,7 +542,6 @@ export interface SupabaseStorageCreateBucketParams { export interface SupabaseStorageCreateBucketResponse extends SupabaseBaseResponse {} -// Storage List Buckets types export interface SupabaseStorageListBucketsParams { apiKey: string projectId: string @@ -539,7 +549,6 @@ export interface SupabaseStorageListBucketsParams { export interface SupabaseStorageListBucketsResponse extends SupabaseBaseResponse {} -// Storage Delete Bucket types export interface SupabaseStorageDeleteBucketParams { apiKey: string projectId: string @@ -548,9 +557,7 @@ export interface SupabaseStorageDeleteBucketParams { export interface SupabaseStorageDeleteBucketResponse extends SupabaseBaseResponse {} -// Storage Get Public URL types export interface SupabaseStorageGetPublicUrlParams { - apiKey: string projectId: string bucket: string path: string @@ -565,7 +572,6 @@ export interface SupabaseStorageGetPublicUrlResponse extends ToolResponse { error?: string } -// Storage Create Signed URL types export interface SupabaseStorageCreateSignedUrlParams { apiKey: string projectId: string diff --git a/apps/sim/tools/supabase/upsert.ts b/apps/sim/tools/supabase/upsert.ts index 8b0fe7213d..c5abf40b42 100644 --- a/apps/sim/tools/supabase/upsert.ts +++ b/apps/sim/tools/supabase/upsert.ts @@ -35,6 +35,13 @@ export const upsertTool: ToolConfig { const tableValidation = validateDatabaseIdentifier(params.table, 'table') if (!tableValidation.isValid) throw new Error(tableValidation.error) - return `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*` + let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*` + if (params.onConflict?.trim()) { + url += `&on_conflict=${encodeURIComponent(params.onConflict.trim())}` + } + return url }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/tavily/search.ts b/apps/sim/tools/tavily/search.ts index cf4b78e437..58f4143db5 100644 --- a/apps/sim/tools/tavily/search.ts +++ b/apps/sim/tools/tavily/search.ts @@ -192,10 +192,10 @@ export const searchTool: ToolConfig = success: true, output: { query: data.query, - results: data.results.map((result: any) => ({ + results: (data.results ?? []).map((result: any) => ({ title: result.title, url: result.url, - snippet: result.snippet, + content: result.content, ...(result.score !== undefined && { score: result.score }), ...(result.raw_content && { raw_content: result.raw_content }), ...(result.favicon && { favicon: result.favicon }), diff --git a/apps/sim/tools/tavily/types.ts b/apps/sim/tools/tavily/types.ts index f185b64315..8ad15732e9 100644 --- a/apps/sim/tools/tavily/types.ts +++ b/apps/sim/tools/tavily/types.ts @@ -142,27 +142,42 @@ interface TavilySearchResult { title: string url: string content: string - score: number - images?: string[] + score?: number raw_content?: string + favicon?: string +} + +interface TavilyImageResult { + url: string + description?: string } export interface TavilySearchResponse extends ToolResponse { output: { + query: string results: TavilySearchResult[] answer?: string - query: string - images?: string[] - rawContent?: string + images?: Array + auto_parameters?: Record + response_time: number } } +interface TavilyExtractResultItem { + url: string + raw_content: string + images?: string[] + favicon?: string +} + export interface TavilyExtractResponse extends ToolResponse { output: { - content: string - title: string - url: string - rawContent?: string + results: TavilyExtractResultItem[] + failed_results?: Array<{ + url: string + error: string + }> + response_time: number } } @@ -175,22 +190,6 @@ export interface TavilyExtractParams { include_favicon?: boolean } -interface ExtractResult { - url: string - raw_content: string -} - -interface ExtractResponse extends ToolResponse { - output: { - results: ExtractResult[] - failed_results?: Array<{ - url: string - error: string - }> - response_time: number - } -} - export interface TavilySearchParams { query: string apiKey: string @@ -212,24 +211,11 @@ export interface TavilySearchParams { auto_parameters?: boolean } -interface SearchResult { - title: string - url: string - snippet: string - raw_content?: string -} - -interface SearchResponse extends ToolResponse { - output: { - query: string - results: SearchResult[] - response_time: number - } -} - export type TavilyResponse = TavilySearchResponse | TavilyExtractResponse -// Crawl API types +/** + * Parameters for the Tavily Crawl tool. + */ export interface TavilyCrawlParams { url: string apiKey: string @@ -263,16 +249,9 @@ export interface CrawlResponse extends ToolResponse { } } -interface TavilyCrawlResponse extends ToolResponse { - output: { - base_url: string - results: CrawlResult[] - response_time: number - request_id?: string - } -} - -// Map API types +/** + * Parameters for the Tavily Map tool. + */ export interface TavilyMapParams { url: string apiKey: string @@ -299,12 +278,3 @@ export interface MapResponse extends ToolResponse { request_id?: string } } - -interface TavilyMapResponse extends ToolResponse { - output: { - base_url: string - results: MapResult[] - response_time: number - request_id?: string - } -} From ea505f0388041d4581de0ad97803ea276d0a9514 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 12:26:05 -0700 Subject: [PATCH 17/26] improvement(tables): versioned CSV snapshot cache for table mounts + parallel multipart uploader (#5108) * improvement(tables): versioned CSV snapshot cache for table mounts + parallel multipart uploader * chore(db): drop colliding 0239 migration (renumber pending) * chore(db): renumber rows_version migration to 0240 (off staging's 0239) * improvement(tables): mount snapshots by presigned URL so the sandbox fetches directly (raise cap to 500MB) * fix(tables): allow url sandbox entries in the function-execute contract; key snapshot by column shape so schema edits invalidate it * chore(e2b): log sandbox inputs split by url-fetch vs inline write * improvement(tables): order export + snapshot rows by order_key so the CSV matches the grid under fractional ordering --- apps/sim/background/table-export.ts | 6 +- apps/sim/lib/api/contracts/hotspots.ts | 19 +- .../tools/handlers/function-execute.test.ts | 201 + .../tools/handlers/function-execute.ts | 87 +- apps/sim/lib/core/config/env.ts | 1 + apps/sim/lib/core/config/feature-flags.ts | 8 + apps/sim/lib/execution/e2b.test.ts | 108 + apps/sim/lib/execution/e2b.ts | 113 +- apps/sim/lib/table/export-runner.test.ts | 86 +- apps/sim/lib/table/export-runner.ts | 63 +- apps/sim/lib/table/jobs/service.ts | 24 +- apps/sim/lib/table/snapshot-cache.test.ts | 188 + apps/sim/lib/table/snapshot-cache.ts | 188 + .../lib/uploads/core/storage-service.test.ts | 102 + apps/sim/lib/uploads/core/storage-service.ts | 182 +- apps/sim/lib/uploads/providers/blob/client.ts | 63 + apps/sim/lib/uploads/providers/s3/client.ts | 28 + apps/sim/tools/function/types.ts | 5 +- .../db/migrations/0240_table_rows_version.sql | 51 + .../db/migrations/meta/0240_snapshot.json | 16573 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 8 + 22 files changed, 17978 insertions(+), 133 deletions(-) create mode 100644 apps/sim/lib/copilot/tools/handlers/function-execute.test.ts create mode 100644 apps/sim/lib/execution/e2b.test.ts create mode 100644 apps/sim/lib/table/snapshot-cache.test.ts create mode 100644 apps/sim/lib/table/snapshot-cache.ts create mode 100644 apps/sim/lib/uploads/core/storage-service.test.ts create mode 100644 packages/db/migrations/0240_table_rows_version.sql create mode 100644 packages/db/migrations/meta/0240_snapshot.json diff --git a/apps/sim/background/table-export.ts b/apps/sim/background/table-export.ts index 49a9c622c3..a816251068 100644 --- a/apps/sim/background/table-export.ts +++ b/apps/sim/background/table-export.ts @@ -3,9 +3,9 @@ import { runTableExport, type TableExportPayload } from '@/lib/table/export-runn /** * Trigger.dev wrapper around `runTableExport`. Retry-safe: a retried attempt regenerates the file - * from scratch (failures clean up their partial upload), and the `table_jobs` ownership gate - * stops a run that lost the job. `medium-1x` — the serialized file is buffered in memory before - * the single-shot storage upload (~hundreds of MB worst case for enterprise 1M-row tables). + * from scratch (failures abort/clean up their partial upload), and the `table_jobs` ownership gate + * stops a run that lost the job. The file streams to storage in bounded multipart chunks (no longer + * buffered whole), so `medium-1x` is now headroom rather than a hard requirement. */ export const tableExportTask = task({ id: 'table-export', diff --git a/apps/sim/lib/api/contracts/hotspots.ts b/apps/sim/lib/api/contracts/hotspots.ts index db667c50aa..6c280898c3 100644 --- a/apps/sim/lib/api/contracts/hotspots.ts +++ b/apps/sim/lib/api/contracts/hotspots.ts @@ -162,11 +162,20 @@ export const functionExecuteContract = defineRouteContract({ isCustomTool: z.boolean().optional().default(false), _sandboxFiles: z .array( - z.object({ - path: z.string(), - content: z.string(), - encoding: z.literal('base64').optional(), - }) + z.union([ + z.object({ + type: z.literal('content').optional(), + path: z.string(), + content: z.string(), + encoding: z.literal('base64').optional(), + }), + // Mounted by reference: the sandbox fetches `url` itself (no bytes through the web tier). + z.object({ + type: z.literal('url'), + path: z.string(), + url: z.string(), + }), + ]) ) .optional(), }), diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts new file mode 100644 index 0000000000..ce56f51254 --- /dev/null +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts @@ -0,0 +1,201 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockIsFeatureEnabled, + mockGetTableById, + mockListTables, + mockQueryRows, + mockGetOrCreateTableSnapshot, + mockDownloadFile, + mockGeneratePresignedDownloadUrl, + mockHasCloudStorage, + mockExecuteTool, +} = vi.hoisted(() => ({ + mockIsFeatureEnabled: vi.fn(), + mockGetTableById: vi.fn(), + mockListTables: vi.fn(), + mockQueryRows: vi.fn(), + mockGetOrCreateTableSnapshot: vi.fn(), + mockDownloadFile: vi.fn(), + mockGeneratePresignedDownloadUrl: vi.fn(), + mockHasCloudStorage: vi.fn(), + mockExecuteTool: vi.fn(), +})) + +vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: mockIsFeatureEnabled })) +vi.mock('@/lib/table/service', () => ({ + getTableById: mockGetTableById, + listTables: mockListTables, +})) +vi.mock('@/lib/table/rows/service', () => ({ queryRows: mockQueryRows })) +vi.mock('@/lib/table/snapshot-cache', () => ({ + getOrCreateTableSnapshot: mockGetOrCreateTableSnapshot, + SNAPSHOT_MAX_BYTES: 500 * 1024 * 1024, +})) +vi.mock('@/lib/uploads/core/storage-service', () => ({ + downloadFile: mockDownloadFile, + generatePresignedDownloadUrl: mockGeneratePresignedDownloadUrl, + hasCloudStorage: mockHasCloudStorage, +})) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) +// Workspace-file + VFS surfaces are unused on the tables-only path; stub to avoid heavy loads. +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + fetchWorkspaceFileBuffer: vi.fn(), + findWorkspaceFileRecord: vi.fn(), + getSandboxWorkspaceFilePath: vi.fn(), + listWorkspaceFiles: vi.fn(), +})) +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-folder-manager', () => ({ + listWorkspaceFileFolders: vi.fn(), +})) +vi.mock('@/lib/copilot/vfs/path-utils', () => ({ + decodeVfsPathSegments: (p: string) => p.split('/'), + encodeVfsPathSegments: (s: string[]) => s.join('/'), +})) +vi.mock('@/lib/copilot/vfs/workflow-alias-resolver', () => ({ + resolveWorkflowAliasForWorkspace: vi.fn().mockResolvedValue(null), +})) +vi.mock('@/lib/copilot/vfs/workflow-aliases', () => ({ + isPlanAliasPath: () => false, + workflowAliasSandboxPath: (p: string) => p, +})) + +import { executeFunctionExecute } from '@/lib/copilot/tools/handlers/function-execute' + +const table = { + id: 'tbl_1', + workspaceId: 'ws_1', + rowCount: 1000, + schema: { columns: [{ id: 'col_name', name: 'name', type: 'string' }] }, +} + +const context = { workspaceId: 'ws_1', userId: 'u1' } + +function mountedFiles() { + const params = mockExecuteTool.mock.calls[0][1] as { + _sandboxFiles?: Array<{ path: string; type?: string; content?: string; url?: string }> + } + return params._sandboxFiles ?? [] +} + +const snapshotCacheOn = (flag: string) => Promise.resolve(flag === 'table-snapshot-cache') + +describe('executeFunctionExecute table mounts', () => { + beforeEach(() => { + vi.clearAllMocks() + mockExecuteTool.mockResolvedValue({ success: true }) + mockGetTableById.mockResolvedValue(table) + mockIsFeatureEnabled.mockResolvedValue(false) + mockQueryRows.mockResolvedValue({ rows: [{ data: { name: 'Ada' } }] }) + mockHasCloudStorage.mockReturnValue(true) + mockGeneratePresignedDownloadUrl.mockResolvedValue('https://s3.example/presigned?sig=abc') + }) + + it('flag OFF: drains the table inline via queryRows (existing path)', async () => { + await executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + + expect(mockQueryRows).toHaveBeenCalledTimes(1) + expect(mockGetOrCreateTableSnapshot).not.toHaveBeenCalled() + const files = mountedFiles() + expect(files[0].path).toBe('/home/user/tables/tbl_1.csv') + expect(files[0].content).toBe('name\nAda') + }) + + it('flag ON + cloud storage: mounts by presigned URL, no bytes through web', async () => { + mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) + mockGetOrCreateTableSnapshot.mockResolvedValue({ + key: 'table-snapshots/ws_1/tbl_1/v5.csv', + size: 9, + version: 5, + }) + + await executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + + expect(mockGetOrCreateTableSnapshot).toHaveBeenCalledTimes(1) + expect(mockQueryRows).not.toHaveBeenCalled() + expect(mockDownloadFile).not.toHaveBeenCalled() + expect(mockGeneratePresignedDownloadUrl).toHaveBeenCalledWith( + 'table-snapshots/ws_1/tbl_1/v5.csv', + 'execution', + expect.any(Number) + ) + expect(mountedFiles()[0]).toEqual({ + type: 'url', + path: '/home/user/tables/tbl_1.csv', + url: 'https://s3.example/presigned?sig=abc', + }) + }) + + it('flag ON + local storage: falls back to a buffered content mount', async () => { + mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) + mockHasCloudStorage.mockReturnValue(false) + mockGetOrCreateTableSnapshot.mockResolvedValue({ + key: 'table-snapshots/ws_1/tbl_1/v5.csv', + size: 9, + version: 5, + }) + mockDownloadFile.mockResolvedValue(Buffer.from('name\nAda\n')) + + await executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + + expect(mockGeneratePresignedDownloadUrl).not.toHaveBeenCalled() + expect(mockDownloadFile).toHaveBeenCalledWith( + expect.objectContaining({ key: 'table-snapshots/ws_1/tbl_1/v5.csv', context: 'execution' }) + ) + const file = mountedFiles()[0] + expect(file.path).toBe('/home/user/tables/tbl_1.csv') + expect(file.content).toBe('name\nAda\n') + expect(file.type).toBeUndefined() + }) + + it('flag ON but small table stays on the inline path', async () => { + mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) + mockGetTableById.mockResolvedValue({ ...table, rowCount: 10 }) + + await executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + + expect(mockGetOrCreateTableSnapshot).not.toHaveBeenCalled() + expect(mockQueryRows).toHaveBeenCalledTimes(1) + }) + + it('flag ON + cloud: throws when the snapshot exceeds the table mount limit', async () => { + mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) + mockGetOrCreateTableSnapshot.mockResolvedValue({ + key: 'table-snapshots/ws_1/tbl_1/v5.csv', + size: 600 * 1024 * 1024, + version: 5, + }) + + await expect( + executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + ).rejects.toThrow(/table mount limit/) + expect(mockGeneratePresignedDownloadUrl).not.toHaveBeenCalled() + }) + + it('flag ON + local: throws when the snapshot exceeds the per-file mount limit', async () => { + mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) + mockHasCloudStorage.mockReturnValue(false) + mockGetOrCreateTableSnapshot.mockResolvedValue({ + key: 'table-snapshots/ws_1/tbl_1/v5.csv', + size: 20 * 1024 * 1024, + version: 5, + }) + + await expect( + executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + ).rejects.toThrow(/per-file mount limit/) + expect(mockDownloadFile).not.toHaveBeenCalled() + }) + + it('rejects a table that belongs to another workspace (tenant isolation)', async () => { + mockGetTableById.mockResolvedValue({ ...table, workspaceId: 'ws_2' }) + + await expect( + executeFunctionExecute({ inputTables: ['tbl_1'] }, context as never) + ).rejects.toThrow(/Input table not found/) + expect(mockGetOrCreateTableSnapshot).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 895b218ffb..a2bfb549b5 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -5,6 +5,7 @@ import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/wor import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { queryRows } from '@/lib/table/rows/service' import { getTableById, listTables } from '@/lib/table/service' +import { getOrCreateTableSnapshot, SNAPSHOT_MAX_BYTES } from '@/lib/table/snapshot-cache' import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { fetchWorkspaceFileBuffer, @@ -12,6 +13,11 @@ import { getSandboxWorkspaceFilePath, listWorkspaceFiles, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { + downloadFile, + generatePresignedDownloadUrl, + hasCloudStorage, +} from '@/lib/uploads/core/storage-service' import { executeTool as executeAppTool } from '@/tools' import type { ToolExecutionContext, ToolExecutionResult } from '../../tool-executor/types' @@ -21,11 +27,22 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024 const MAX_TOTAL_SIZE = 50 * 1024 * 1024 const MAX_MOUNTED_FILES = 500 -interface SandboxFile { - path: string - content: string - encoding?: 'base64' -} +/** + * Below this row count a table mounts via the direct inline CSV path — the version-keyed snapshot + * cache (storage round-trip) only pays off for larger/hot tables. Behind the feature flag either + * way; this just keeps tiny one-shot tables on the cheaper path. + */ +const SNAPSHOT_MIN_ROWS = 500 + +/** + * Lifetime of the presigned URL handed to the sandbox to fetch a snapshot. Long enough to download + * a large file at sandbox startup; the URL grants read to only that one version-pinned object. + */ +const SNAPSHOT_URL_TTL_SECONDS = 600 + +type SandboxFile = + | { type?: 'content'; path: string; content: string; encoding?: 'base64' } + | { type: 'url'; path: string; url: string } interface CanonicalFileInput { path: string @@ -249,6 +266,7 @@ async function resolveInputFiles( const tablePathLookup = hasTablePathRefs ? new Map((await listTables(workspaceId)).map((table) => [table.name, table])) : undefined + const snapshotCacheEnabled = await isFeatureEnabled('table-snapshot-cache') for (const tableRef of inputTables) { const tableId = typeof tableRef === 'string' @@ -263,6 +281,56 @@ async function resolveInputFiles( `Input table not found: "${tableId}". Pass the table id (tbl_...) from tables/{name}/meta.json, or a tables/{name}/meta.json path.` ) } + const sandboxPath = + typeof tableRef === 'object' && tableRef !== null + ? (tableRef as CanonicalTableInput).sandboxPath + : undefined + const mountPath = sandboxPath || `/home/user/tables/${table.id}.csv` + + // Large/hot tables mount by reference from a version-keyed CSV snapshot in object storage. + if (snapshotCacheEnabled && table.rowCount >= SNAPSHOT_MIN_ROWS) { + const snapshot = await getOrCreateTableSnapshot(table, 'copilot-fn-exec') + + if (hasCloudStorage()) { + // Mount by reference: the sandbox fetches the snapshot straight from storage via a + // presigned URL, so the bytes never pass through the web process — the only ceiling is + // sandbox disk (enforced at materialization by SNAPSHOT_MAX_BYTES). + if (snapshot.size > SNAPSHOT_MAX_BYTES) { + throw new Error( + `Input table "${tableId}" is ${Math.round(snapshot.size / 1024 / 1024)}MB, over the ${SNAPSHOT_MAX_BYTES / 1024 / 1024}MB table mount limit.` + ) + } + const url = await generatePresignedDownloadUrl( + snapshot.key, + 'execution', + SNAPSHOT_URL_TTL_SECONDS + ) + sandboxFiles.push({ type: 'url', path: mountPath, url }) + continue + } + + // Local storage: a presigned URL is an app-internal serve path a remote sandbox can't + // reach, so fall back to buffering the bytes through the web process (file-mount guards). + if (snapshot.size > MAX_FILE_SIZE) { + throw new Error( + `Input table "${tableId}" is ${Math.round(snapshot.size / 1024 / 1024)}MB, over the ${MAX_FILE_SIZE / 1024 / 1024}MB per-file mount limit.` + ) + } + if (totalSize + snapshot.size > MAX_TOTAL_SIZE) { + throw new Error( + `Mounting "${tableId}" would exceed the ${MAX_TOTAL_SIZE / 1024 / 1024}MB total mount limit. Mount fewer or smaller tables.` + ) + } + const buffer = await downloadFile({ + key: snapshot.key, + context: 'execution', + maxBytes: MAX_FILE_SIZE, + }) + totalSize += buffer.length + sandboxFiles.push({ path: mountPath, content: buffer.toString('utf-8') }) + continue + } + const rows = await queryRows(table, {}, 'copilot-fn-exec') const allKeys = new Set(table.schema.columns.map((column) => column.name)) @@ -290,14 +358,7 @@ async function resolveInputFiles( ) } const csvContent = csvLines.join('\n') - const sandboxPath = - typeof tableRef === 'object' && tableRef !== null - ? (tableRef as CanonicalTableInput).sandboxPath - : undefined - sandboxFiles.push({ - path: sandboxPath || `/home/user/tables/${table.id}.csv`, - content: csvContent, - }) + sandboxFiles.push({ path: mountPath, content: csvContent }) } } diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 293c7560e5..1ea9e3e105 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -73,6 +73,7 @@ export const env = createEnv({ BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking FREE_API_DEPLOYMENT_GATE_ENABLED: z.boolean().optional(), // Block free-plan accounts from programmatic execution (API/MCP/A2A/generic webhooks/chat embeds). Requires BILLING_ENABLED. Off by default for dark rollout TABLES_FRACTIONAL_ORDERING: z.boolean().optional(), // Order table rows by fractional order_key (O(1) insert/delete) instead of integer position + TABLE_SNAPSHOT_CACHE: z.boolean().optional(), // Mount tables into sandboxes by reference via a version-keyed CSV snapshot in object storage instead of draining the whole table into web-process heap // Table feature limits (per plan). Apply when billing is disabled (free tier defaults) or for billed plans. FREE_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on free tier (default: 3) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index d3bc89ad79..85107d7455 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -74,6 +74,14 @@ const FEATURE_FLAGS = { 'user context — use enabled:true for global rollout rather than per-user targeting.', fallback: 'MOTHERSHIP_BETA_FEATURES', }, + 'table-snapshot-cache': { + description: + 'Mount Sim tables into code sandboxes by reference via a version-keyed CSV snapshot in ' + + 'object storage (reused across runs until the table mutates) instead of draining the whole ' + + 'table into web-process heap. resolveInputFiles evaluates without user context — use ' + + 'enabled:true for global rollout rather than per-user targeting.', + fallback: 'TABLE_SNAPSHOT_CACHE', + }, } satisfies Record /** diff --git a/apps/sim/lib/execution/e2b.test.ts b/apps/sim/lib/execution/e2b.test.ts new file mode 100644 index 0000000000..738d1fdfff --- /dev/null +++ b/apps/sim/lib/execution/e2b.test.ts @@ -0,0 +1,108 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CodeLanguage } from '@/lib/execution/languages' + +const { mockCreate, mockRunCode, mockCommandsRun, mockFilesWrite, mockKill } = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockRunCode: vi.fn(), + mockCommandsRun: vi.fn(), + mockFilesWrite: vi.fn(), + mockKill: vi.fn(), +})) + +vi.mock('@e2b/code-interpreter', () => ({ Sandbox: { create: mockCreate } })) +vi.mock('@/lib/core/config/env', () => ({ env: { E2B_API_KEY: 'test-key' } })) + +import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b' + +describe('e2b sandbox inputs', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreate.mockResolvedValue({ + sandboxId: 'sb_1', + files: { write: mockFilesWrite }, + commands: { run: mockCommandsRun }, + runCode: mockRunCode, + kill: mockKill, + }) + mockRunCode.mockResolvedValue({ + error: null, + text: '', + logs: { stdout: [], stderr: [] }, + results: [], + }) + // Default: shell code run + any fetch succeed. + mockCommandsRun.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }) + }) + + it('fetches a url entry via curl with URL/DST/DIR passed as envs (no inline write)', async () => { + await executeInE2B({ + code: 'x', + language: CodeLanguage.JavaScript, + timeoutMs: 1000, + sandboxFiles: [ + { type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p?a=1&b=2' }, + ], + }) + + expect(mockCommandsRun).toHaveBeenCalledTimes(1) + const [cmd, opts] = mockCommandsRun.mock.calls[0] + expect(cmd).toContain('curl') + expect(cmd).toContain('mkdir -p') + // URL/path go through envs, never interpolated into the command string. + expect(cmd).not.toContain('https://s3.example') + expect(opts.envs).toEqual({ + URL: 'https://s3.example/p?a=1&b=2', + DST: '/home/user/tables/t.csv', + DIR: '/home/user/tables', + }) + expect(opts.user).toBeUndefined() // code sandbox runs as default user + expect(mockFilesWrite).not.toHaveBeenCalled() + }) + + it('writes a content entry inline (no fetch)', async () => { + await executeInE2B({ + code: 'x', + language: CodeLanguage.JavaScript, + timeoutMs: 1000, + sandboxFiles: [{ path: '/home/user/f.txt', content: 'hi' }], + }) + + expect(mockFilesWrite).toHaveBeenCalledWith('/home/user/f.txt', 'hi') + expect(mockCommandsRun).not.toHaveBeenCalled() + }) + + it('fetches as root in the shell sandbox', async () => { + await executeShellInE2B({ + code: 'echo hi', + envs: {}, + timeoutMs: 1000, + sandboxFiles: [{ type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p' }], + }) + + const fetchCall = mockCommandsRun.mock.calls.find((c) => c[1]?.envs?.URL) + expect(fetchCall).toBeDefined() + expect(fetchCall?.[0]).toContain('curl') + expect(fetchCall?.[1].user).toBe('root') + }) + + it('throws a clear error and kills the sandbox when the fetch fails', async () => { + mockCommandsRun.mockRejectedValueOnce(new Error('curl: (22) 403')) + + await expect( + executeInE2B({ + code: 'x', + language: CodeLanguage.JavaScript, + timeoutMs: 1000, + sandboxFiles: [ + { type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p' }, + ], + }) + ).rejects.toThrow(/Failed to fetch mounted file into sandbox/) + + expect(mockKill).toHaveBeenCalled() + expect(mockRunCode).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/execution/e2b.ts b/apps/sim/lib/execution/e2b.ts index c56bfb5554..697fc5992d 100644 --- a/apps/sim/lib/execution/e2b.ts +++ b/apps/sim/lib/execution/e2b.ts @@ -1,13 +1,16 @@ import type { Sandbox as E2BSandbox } from '@e2b/code-interpreter' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { env } from '@/lib/core/config/env' import { CodeLanguage } from '@/lib/execution/languages' -export interface SandboxFile { - path: string - content: string - encoding?: 'base64' -} +/** + * A sandbox input file. `content` entries are written inline; `url` entries are fetched from inside + * the sandbox (so large mounts never pass their bytes through the web process). + */ +export type SandboxFile = + | { type?: 'content'; path: string; content: string; encoding?: 'base64' } + | { type: 'url'; path: string; url: string } export interface E2BExecutionRequest { code: string @@ -48,6 +51,62 @@ export interface E2BExecutionResult { const logger = createLogger('E2BExecution') +/** + * Materializes sandbox input files before user code runs. `content` entries are written inline; + * `url` entries are fetched from inside the sandbox via `curl` — their bytes never pass through the + * web process, so the mount size is bounded by sandbox disk, not web heap. The URL and paths are + * passed as env vars (never interpolated into the shell) so a presigned query string can't break or + * inject. A failed fetch throws so user code never runs against a missing mount. `rootUser` matches + * the shell sandbox's root execution context. + */ +async function writeSandboxInputs( + sandbox: E2BSandbox, + files: SandboxFile[] | undefined, + opts: { sandboxId?: string; rootUser?: boolean } +): Promise { + if (!files?.length) return + const fetchedByUrl: string[] = [] + const writtenInline: string[] = [] + for (const file of files) { + if (file.type === 'url') { + const dir = file.path.slice(0, file.path.lastIndexOf('/')) + try { + await sandbox.commands.run( + 'set -e; [ -n "$DIR" ] && mkdir -p "$DIR"; curl -fsS --retry 3 --retry-connrefused --max-time 300 "$URL" -o "$DST"', + { + envs: { URL: file.url, DST: file.path, DIR: dir }, + ...(opts.rootUser ? { user: 'root' } : {}), + } + ) + fetchedByUrl.push(file.path) + } catch (error) { + throw new Error( + `Failed to fetch mounted file into sandbox at ${file.path}: ${getErrorMessage(error)}` + ) + } + } else if (file.encoding === 'base64') { + const buf = Buffer.from(file.content, 'base64') + await sandbox.files.write( + file.path, + buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) + ) + writtenInline.push(file.path) + } else { + await sandbox.files.write(file.path, file.content) + writtenInline.push(file.path) + } + } + // Split counts so it's visible whether a mount was fetched in-sandbox (by presigned URL, no bytes + // through the web process) or written inline. + logger.info('Materialized sandbox inputs', { + sandboxId: opts.sandboxId, + fetchedByUrlCount: fetchedByUrl.length, + writtenInlineCount: writtenInline.length, + fetchedByUrl, + writtenInline, + }) +} + async function createE2BSandbox(kind: 'code' | 'shell' | 'doc'): Promise { const apiKey = env.E2B_API_KEY if (!apiKey) { @@ -127,28 +186,12 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise f.path), - }) - } - const stdoutChunks = [] try { + // Inside the try so a failed mount still kills the sandbox via the finally below. + await writeSandboxInputs(sandbox, req.sandboxFiles, { sandboxId }) + const execution = await sandbox.runCode(code, { language: language === CodeLanguage.Python ? 'python' : 'javascript', timeoutMs, @@ -247,26 +290,10 @@ export async function executeShellInE2B( const sandbox = await createE2BSandbox(req.sandboxKind ?? 'shell') const sandboxId = sandbox.sandboxId - if (req.sandboxFiles?.length) { - for (const file of req.sandboxFiles) { - if (file.encoding === 'base64') { - const buf = Buffer.from(file.content, 'base64') - await sandbox.files.write( - file.path, - buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) - ) - } else { - await sandbox.files.write(file.path, file.content) - } - } - logger.info('Wrote sandbox input files', { - sandboxId, - fileCount: req.sandboxFiles.length, - paths: req.sandboxFiles.map((f) => f.path), - }) - } - try { + // Inside the try so a failed mount still kills the sandbox via the finally below. + await writeSandboxInputs(sandbox, req.sandboxFiles, { sandboxId, rootUser: true }) + let result: { stdout: string; stderr: string; exitCode: number } try { result = await sandbox.commands.run(code, { diff --git a/apps/sim/lib/table/export-runner.test.ts b/apps/sim/lib/table/export-runner.test.ts index 96032a7c82..f8477eac0c 100644 --- a/apps/sim/lib/table/export-runner.test.ts +++ b/apps/sim/lib/table/export-runner.test.ts @@ -11,7 +11,7 @@ const { mockMarkJobFailed, mockSetJobResultKey, mockAppendTableEvent, - mockUploadFile, + mockCreateMultipartUpload, mockDeleteFile, } = vi.hoisted(() => ({ mockGetTableById: vi.fn(), @@ -21,7 +21,7 @@ const { mockMarkJobFailed: vi.fn(), mockSetJobResultKey: vi.fn(), mockAppendTableEvent: vi.fn(), - mockUploadFile: vi.fn(), + mockCreateMultipartUpload: vi.fn(), mockDeleteFile: vi.fn(), })) @@ -37,7 +37,7 @@ vi.mock('@/lib/table/jobs/service', () => ({ })) vi.mock('@/lib/table/events', () => ({ appendTableEvent: mockAppendTableEvent })) vi.mock('@/lib/uploads/core/storage-service', () => ({ - uploadFile: mockUploadFile, + createMultipartUpload: mockCreateMultipartUpload, deleteFile: mockDeleteFile, })) @@ -52,36 +52,65 @@ const table = { const payload = { jobId: 'job_1', tableId: 'tbl_1', workspaceId: 'ws_1', format: 'csv' as const } +interface FakeHandle { + key: string + content: string + write: ReturnType + complete: ReturnType + abort: ReturnType +} + +let lastHandle: FakeHandle | null + describe('runTableExport', () => { beforeEach(() => { vi.clearAllMocks() + lastHandle = null mockGetTableById.mockResolvedValue(table) mockUpdateJobProgress.mockResolvedValue(true) mockMarkJobReady.mockResolvedValue(true) mockMarkJobFailed.mockResolvedValue(undefined) mockSetJobResultKey.mockResolvedValue(undefined) - // Echo the requested key back like preserveKey-aware providers do; the runner must record - // THIS returned key, not its own constructed one. - mockUploadFile.mockImplementation((opts: { customKey: string }) => - Promise.resolve({ key: opts.customKey }) - ) mockDeleteFile.mockResolvedValue(undefined) + // A handle that records every write so tests can assert the streamed bytes, and echoes the + // pinned key back from `complete` like the real uploader does. + mockCreateMultipartUpload.mockImplementation(({ key }: { key: string }) => { + const chunks: string[] = [] + const handle: FakeHandle = { + key, + content: '', + write: vi.fn((chunk: Buffer | string) => { + chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8')) + return Promise.resolve() + }), + complete: vi.fn(() => { + handle.content = chunks.join('') + return Promise.resolve({ key, size: Buffer.byteLength(handle.content) }) + }), + abort: vi.fn(() => Promise.resolve()), + } + lastHandle = handle + return Promise.resolve(handle) + }) mockSelectExportRowPage.mockResolvedValue([ - { id: 'r1', data: { col_name: 'Ada' }, position: 0 }, + { id: 'r1', data: { col_name: 'Ada' }, orderKey: 'a0' }, ]) }) - it('pages rows, uploads the file, stamps the result key, and marks ready', async () => { + it('streams rows to the uploader, stamps the result key, and marks ready', async () => { await runTableExport(payload) - expect(mockUploadFile).toHaveBeenCalledTimes(1) - const upload = mockUploadFile.mock.calls[0][0] - expect(upload.customKey).toBe('workspace/ws_1/exports/tbl_1/job_1/People.csv') - expect(upload.preserveKey).toBe(true) - expect(upload.contentType).toContain('text/csv') - expect(upload.file.toString('utf8')).toBe('name\nAda\n') + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(1) + const init = mockCreateMultipartUpload.mock.calls[0][0] + expect(init.key).toBe('workspace/ws_1/exports/tbl_1/job_1/People.csv') + expect(init.context).toBe('workspace') + expect(init.contentType).toContain('text/csv') + + expect(lastHandle?.content).toBe('name\nAda\n') + expect(lastHandle?.complete).toHaveBeenCalledTimes(1) + expect(lastHandle?.abort).not.toHaveBeenCalled() - expect(mockSetJobResultKey).toHaveBeenCalledWith('tbl_1', 'job_1', upload.customKey) + expect(mockSetJobResultKey).toHaveBeenCalledWith('tbl_1', 'job_1', init.key) expect(mockMarkJobReady).toHaveBeenCalledWith('tbl_1', 'job_1') expect(mockAppendTableEvent).toHaveBeenCalledWith( expect.objectContaining({ kind: 'job', type: 'export', status: 'ready', progress: 1 }) @@ -91,38 +120,40 @@ describe('runTableExport', () => { it('serializes JSON exports with display-name keys', async () => { await runTableExport({ ...payload, format: 'json' }) - const upload = mockUploadFile.mock.calls[0][0] - expect(upload.customKey.endsWith('/People.json')).toBe(true) - expect(JSON.parse(upload.file.toString('utf8'))).toEqual([{ name: 'Ada' }]) + const init = mockCreateMultipartUpload.mock.calls[0][0] + expect(init.key.endsWith('/People.json')).toBe(true) + expect(JSON.parse(lastHandle?.content ?? '')).toEqual([{ name: 'Ada' }]) }) - it('stops without uploading when the ownership gate is lost (cancel)', async () => { + it('aborts the upload and never completes when ownership is lost (cancel)', async () => { mockUpdateJobProgress.mockResolvedValue(false) await runTableExport(payload) - expect(mockUploadFile).not.toHaveBeenCalled() + expect(lastHandle?.complete).not.toHaveBeenCalled() + expect(lastHandle?.abort).toHaveBeenCalledTimes(1) expect(mockMarkJobReady).not.toHaveBeenCalled() expect(mockMarkJobFailed).not.toHaveBeenCalled() }) - it('stops before the upload when ownership is lost at the finalize gate', async () => { + it('aborts before completing when ownership is lost at the finalize gate', async () => { mockUpdateJobProgress.mockResolvedValueOnce(true).mockResolvedValue(false) await runTableExport(payload) expect(mockSelectExportRowPage).toHaveBeenCalledTimes(1) - expect(mockUploadFile).not.toHaveBeenCalled() + expect(lastHandle?.complete).not.toHaveBeenCalled() + expect(lastHandle?.abort).toHaveBeenCalledTimes(1) expect(mockMarkJobReady).not.toHaveBeenCalled() expect(mockMarkJobFailed).not.toHaveBeenCalled() }) - it('cleans up an orphaned upload when the job was canceled at the wire', async () => { + it('deletes the finalized object when the job was canceled at the wire', async () => { mockMarkJobReady.mockResolvedValue(false) await runTableExport(payload) - expect(mockUploadFile).toHaveBeenCalledTimes(1) + expect(lastHandle?.complete).toHaveBeenCalledTimes(1) expect(mockDeleteFile).toHaveBeenCalledWith( expect.objectContaining({ key: expect.stringContaining('exports/tbl_1/job_1') }) ) @@ -131,11 +162,12 @@ describe('runTableExport', () => { ) }) - it('marks the job failed and emits a failed event on error', async () => { + it('aborts the upload, marks the job failed, and emits a failed event on error', async () => { mockSelectExportRowPage.mockRejectedValue(new Error('boom')) await runTableExport(payload) + expect(lastHandle?.abort).toHaveBeenCalledTimes(1) expect(mockMarkJobFailed).toHaveBeenCalledWith('tbl_1', 'job_1', 'boom') expect(mockAppendTableEvent).toHaveBeenCalledWith( expect.objectContaining({ kind: 'job', type: 'export', status: 'failed', error: 'boom' }) diff --git a/apps/sim/lib/table/export-runner.ts b/apps/sim/lib/table/export-runner.ts index e6f6f834f1..6c72a01ff2 100644 --- a/apps/sim/lib/table/export-runner.ts +++ b/apps/sim/lib/table/export-runner.ts @@ -17,7 +17,11 @@ import { updateJobProgress, } from '@/lib/table/jobs/service' import { getTableById } from '@/lib/table/service' -import { deleteFile, uploadFile } from '@/lib/uploads/core/storage-service' +import { + createMultipartUpload, + deleteFile, + type MultipartUploadHandle, +} from '@/lib/uploads/core/storage-service' const logger = createLogger('TableExportRunner') @@ -47,6 +51,7 @@ export interface TableExportPayload { export async function runTableExport(payload: TableExportPayload): Promise { const { jobId, tableId, workspaceId, format } = payload const requestId = generateId().slice(0, 8) + let handle: MultipartUploadHandle | null = null let uploadedKey: string | null = null try { @@ -58,16 +63,22 @@ export async function runTableExport(payload: TableExportPayload): Promise // id → name on the way out (export is a name-friendly boundary). const nameById = buildNameById(table.schema) - const chunks: string[] = [] - if (format === 'csv') { - chunks.push(`${toCsvRow(columns.map((c) => neutralizeCsvFormula(c.name)))}\n`) - } else { - chunks.push('[') - } + const fileName = `${sanitizeExportFilename(table.name)}.${format}` + // The key is pinned up front so the streaming upload writes exactly where the download + // route presigns; the *returned* key (from `complete`) is recorded as the source of truth. + const key = `workspace/${workspaceId}/exports/${tableId}/${jobId}/${fileName}` + const contentType = format === 'csv' ? 'text/csv; charset=utf-8' : 'application/json' + + // Stream the serialized file straight into storage in bounded parts instead of buffering the + // whole thing in heap — a 1M-row export no longer holds hundreds of MB resident. + handle = await createMultipartUpload({ key, context: 'workspace', contentType }) + await handle.write( + format === 'csv' ? `${toCsvRow(columns.map((c) => neutralizeCsvFormula(c.name)))}\n` : '[' + ) let exported = 0 let firstJsonRow = true - let after: { position: number; id: string } | null = null + let after: { orderKey: string; id: string } | null = null while (true) { // Ownership gate before every page: a canceled job stops within one batch. const owns = await updateJobProgress(tableId, exported, jobId) @@ -76,39 +87,31 @@ export async function runTableExport(payload: TableExportPayload): Promise const page = await selectExportRowPage(table, after, EXPORT_BATCH_SIZE) if (page.length === 0) break + const pageChunks: string[] = [] for (const row of page) { if (format === 'csv') { - chunks.push(`${toCsvRow(columns.map((c) => formatCsvValue(row.data[getColumnId(c)])))}\n`) + pageChunks.push( + `${toCsvRow(columns.map((c) => formatCsvValue(row.data[getColumnId(c)])))}\n` + ) } else { const prefix = firstJsonRow ? '' : ',' firstJsonRow = false - chunks.push(prefix + JSON.stringify(rowDataIdToName(row.data, nameById))) + pageChunks.push(prefix + JSON.stringify(rowDataIdToName(row.data, nameById))) } } + await handle.write(pageChunks.join('')) exported += page.length const last = page[page.length - 1] - after = { position: last.position, id: last.id } + after = { orderKey: last.orderKey, id: last.id } if (page.length < EXPORT_BATCH_SIZE) break } - if (format === 'json') chunks.push(']') + if (format === 'json') await handle.write(']') const ownsFinalize = await updateJobProgress(tableId, exported, jobId) if (!ownsFinalize) throw new JobSupersededError() - const fileName = `${sanitizeExportFilename(table.name)}.${format}` - const key = `workspace/${workspaceId}/exports/${tableId}/${jobId}/${fileName}` - // `preserveKey` keeps the custom key verbatim (without it the provider rewrites the key to a - // timestamped, path-stripped name), and the *returned* key is recorded as the source of truth - // either way — the download route presigns exactly what was written. - const uploaded = await uploadFile({ - file: Buffer.from(chunks.join(''), 'utf8'), - fileName, - contentType: format === 'csv' ? 'text/csv; charset=utf-8' : 'application/json', - context: 'workspace', - customKey: key, - preserveKey: true, - }) + const uploaded = await handle.complete() uploadedKey = uploaded.key await setJobResultKey(tableId, jobId, uploaded.key) @@ -132,8 +135,14 @@ export async function runTableExport(payload: TableExportPayload): Promise logger.info(`[${requestId}] Export finished but no longer owns the run`, { tableId, jobId }) } } catch (err) { - // A partial/orphaned upload from this attempt is useless — clean it up best-effort. - if (uploadedKey) await deleteFile({ key: uploadedKey, context: 'workspace' }).catch(() => {}) + // A partial/orphaned upload from this attempt is useless — clean it up best-effort. An + // in-flight multipart upload (not yet completed) is aborted so no staged parts linger; a + // completed-but-unannounced upload is removed by key. + if (uploadedKey) { + await deleteFile({ key: uploadedKey, context: 'workspace' }).catch(() => {}) + } else if (handle) { + await handle.abort().catch(() => {}) + } if (err instanceof JobSupersededError) { logger.info(`[${requestId}] Export superseded/canceled; stopping`, { tableId, jobId }) } else { diff --git a/apps/sim/lib/table/jobs/service.ts b/apps/sim/lib/table/jobs/service.ts index 124f3d95c0..7b1706e32d 100644 --- a/apps/sim/lib/table/jobs/service.ts +++ b/apps/sim/lib/table/jobs/service.ts @@ -207,21 +207,21 @@ export async function getJobProgress(tableId: string, jobId: string): Promise> { +): Promise> { const deleteMask = await pendingDeleteMask(table) const rows = await db - .select({ id: userTableRows.id, data: userTableRows.data, position: userTableRows.position }) + .select({ id: userTableRows.id, data: userTableRows.data, orderKey: userTableRows.orderKey }) .from(userTableRows) .where( and( @@ -229,13 +229,13 @@ export async function selectExportRowPage( eq(userTableRows.workspaceId, table.workspaceId), deleteMask, after - ? sql`(${userTableRows.position}, ${userTableRows.id}) > (${after.position}, ${after.id})` + ? sql`(${userTableRows.orderKey}, ${userTableRows.id}) > (${after.orderKey}, ${after.id})` : undefined ) ) - .orderBy(asc(userTableRows.position), asc(userTableRows.id)) + .orderBy(asc(userTableRows.orderKey), asc(userTableRows.id)) .limit(limit) - return rows as Array<{ id: string; data: RowData; position: number }> + return rows as Array<{ id: string; data: RowData; orderKey: string }> } /** How long a terminal export stays listable (and re-downloadable from the tray). */ diff --git a/apps/sim/lib/table/snapshot-cache.test.ts b/apps/sim/lib/table/snapshot-cache.test.ts new file mode 100644 index 0000000000..2fe0927c53 --- /dev/null +++ b/apps/sim/lib/table/snapshot-cache.test.ts @@ -0,0 +1,188 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockDb, + mockSelectExportRowPage, + mockCreateMultipartUpload, + mockHeadObject, + mockDeleteFile, +} = vi.hoisted(() => { + const limit = vi.fn() + return { + mockDb: { limit, select: () => ({ from: () => ({ where: () => ({ limit }) }) }) }, + mockSelectExportRowPage: vi.fn(), + mockCreateMultipartUpload: vi.fn(), + mockHeadObject: vi.fn(), + mockDeleteFile: vi.fn(), + } +}) + +vi.mock('@sim/db', () => ({ db: mockDb })) +vi.mock('@/lib/table/jobs/service', () => ({ selectExportRowPage: mockSelectExportRowPage })) +vi.mock('@/lib/uploads/core/storage-service', () => ({ + createMultipartUpload: mockCreateMultipartUpload, + headObject: mockHeadObject, + deleteFile: mockDeleteFile, +})) + +import { getOrCreateTableSnapshot, TableSnapshotTooLargeError } from '@/lib/table/snapshot-cache' + +const table = { + id: 'tbl_1', + workspaceId: 'ws_1', + schema: { columns: [{ id: 'col_name', name: 'name', type: 'string' }] }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as any + +let lastHandle: { + content: string + complete: ReturnType + abort: ReturnType +} | null + +/** Queue the values successive `readRowsVersion` calls return. */ +function versions(...values: number[]) { + for (const v of values) mockDb.limit.mockResolvedValueOnce([{ rowsVersion: v }]) +} + +describe('getOrCreateTableSnapshot', () => { + beforeEach(() => { + vi.clearAllMocks() + lastHandle = null + mockDeleteFile.mockResolvedValue(undefined) + mockSelectExportRowPage.mockResolvedValueOnce([ + { id: 'r1', data: { col_name: 'Ada' }, orderKey: 'a0' }, + ]) + mockSelectExportRowPage.mockResolvedValue([]) + mockCreateMultipartUpload.mockImplementation(({ key }: { key: string }) => { + const chunks: string[] = [] + const handle = { + content: '', + write: vi.fn((chunk: Buffer | string) => { + chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8')) + return Promise.resolve() + }), + complete: vi.fn(() => { + handle.content = chunks.join('') + return Promise.resolve({ key, size: Buffer.byteLength(handle.content) }) + }), + abort: vi.fn(() => Promise.resolve()), + } + lastHandle = handle + return Promise.resolve(handle) + }) + }) + + it('returns the cached snapshot on a hit without reading rows', async () => { + versions(3) + mockHeadObject.mockResolvedValue({ size: 42 }) + + const ref = await getOrCreateTableSnapshot(table, 'req') + + expect(ref).toEqual({ + key: expect.stringMatching(/^table-snapshots\/ws_1\/tbl_1\/v3-[0-9a-f]{12}\.csv$/), + size: 42, + version: 3, + }) + expect(mockCreateMultipartUpload).not.toHaveBeenCalled() + expect(mockSelectExportRowPage).not.toHaveBeenCalled() + }) + + it('materializes and stores on a miss, then cleans up the previous version', async () => { + versions(3, 3) // initial read, then unchanged recheck + mockHeadObject.mockResolvedValue(null) + + const ref = await getOrCreateTableSnapshot(table, 'req') + + expect(mockCreateMultipartUpload).toHaveBeenCalledWith( + expect.objectContaining({ + key: expect.stringMatching(/^table-snapshots\/ws_1\/tbl_1\/v3-[0-9a-f]{12}\.csv$/), + context: 'execution', + }) + ) + expect(lastHandle?.content).toBe('name\nAda\n') + expect(ref).toEqual({ + key: expect.stringMatching(/^table-snapshots\/ws_1\/tbl_1\/v3-[0-9a-f]{12}\.csv$/), + size: Buffer.byteLength('name\nAda\n'), + version: 3, + }) + // Best-effort prune of v2. + expect(mockDeleteFile).toHaveBeenCalledWith( + expect.objectContaining({ + key: expect.stringMatching(/^table-snapshots\/ws_1\/tbl_1\/v2-[0-9a-f]{12}\.csv$/), + context: 'execution', + }) + ) + }) + + it('keys the snapshot by tenant — the same table id in another workspace gets a different key', async () => { + versions(1) + mockHeadObject.mockResolvedValue({ size: 1 }) + const ref = await getOrCreateTableSnapshot({ ...table, workspaceId: 'ws_2' }, 'req') + expect(ref.key).toMatch(/^table-snapshots\/ws_2\/tbl_1\/v1-[0-9a-f]{12}\.csv$/) + }) + + it('changes the key when the column shape changes (schema edits invalidate the cache)', async () => { + versions(7, 7) + mockHeadObject.mockResolvedValue({ size: 1 }) + + const a = await getOrCreateTableSnapshot(table, 'req') + const b = await getOrCreateTableSnapshot( + { + ...table, + schema: { columns: [{ id: 'col_name', name: 'renamed', type: 'string' }] }, + } as never, + 'req' + ) + + // Same workspace/table/row-version, but a renamed column flips the shape hash → different key. + expect(a.key).not.toBe(b.key) + expect(a.key).toMatch(/\/v7-[0-9a-f]{12}\.csv$/) + expect(b.key).toMatch(/\/v7-[0-9a-f]{12}\.csv$/) + }) + + it('re-keys and rebuilds when rows_version advances mid-scan', async () => { + versions(3, 4) // read v3, materialize, recheck sees v4 + mockHeadObject.mockResolvedValueOnce(null) // v3 miss + mockHeadObject.mockResolvedValueOnce(null) // v4 miss → rebuild + // second materialize needs its own page sequence + mockSelectExportRowPage.mockReset() + mockSelectExportRowPage + .mockResolvedValueOnce([{ id: 'r1', data: { col_name: 'Ada' }, orderKey: 'a0' }]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ id: 'r1', data: { col_name: 'Ada' }, orderKey: 'a0' }]) + .mockResolvedValueOnce([]) + + const ref = await getOrCreateTableSnapshot(table, 'req') + + expect(ref.version).toBe(4) + expect(ref.key).toMatch(/^table-snapshots\/ws_1\/tbl_1\/v4-[0-9a-f]{12}\.csv$/) + expect(mockCreateMultipartUpload).toHaveBeenCalledTimes(2) + // the stale v3 object is dropped + expect(mockDeleteFile).toHaveBeenCalledWith( + expect.objectContaining({ + key: expect.stringMatching(/^table-snapshots\/ws_1\/tbl_1\/v3-[0-9a-f]{12}\.csv$/), + }) + ) + }) + + it('aborts and throws when the CSV exceeds the size cap', async () => { + versions(1) + mockHeadObject.mockResolvedValue(null) + mockSelectExportRowPage.mockReset() + // A full batch of wide rows on every page → the materialize loop keeps paging until the running + // byte count crosses the cap, then aborts. Peak memory stays at one page (~MBs), not the cap. + const wideRow = { id: 'r', data: { col_name: 'x'.repeat(1000) }, orderKey: 'a0' } + const fullPage = Array.from({ length: 10000 }, () => wideRow) + mockSelectExportRowPage.mockResolvedValue(fullPage) + + await expect(getOrCreateTableSnapshot(table, 'req')).rejects.toBeInstanceOf( + TableSnapshotTooLargeError + ) + expect(lastHandle?.abort).toHaveBeenCalledTimes(1) + expect(lastHandle?.complete).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/table/snapshot-cache.ts b/apps/sim/lib/table/snapshot-cache.ts new file mode 100644 index 0000000000..716288fb98 --- /dev/null +++ b/apps/sim/lib/table/snapshot-cache.ts @@ -0,0 +1,188 @@ +/** + * Versioned CSV snapshot cache for table mounts. + * + * Materializes a table's CSV into object storage once per `rows_version` and reuses it across + * executions until the table mutates (the `bump_user_table_rows_version` trigger invalidates the + * key). This replaces draining the whole table into web-process heap on every mount. + * + * Tenant isolation: callers must pass a table they have already authorized (the + * `function-execute` mount path enforces `table.workspaceId === context.workspaceId`); the key is + * namespaced by `workspaceId` and the row reads are workspace-filtered, so a snapshot can only ever + * contain — and be addressed by — its owning tenant. + */ + +import { createHash } from 'crypto' +import { db } from '@sim/db' +import { userTableDefinitions } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { getColumnId } from '@/lib/table/column-keys' +import { formatCsvValue, neutralizeCsvFormula, toCsvRow } from '@/lib/table/export-format' +import { selectExportRowPage } from '@/lib/table/jobs/service' +import type { TableDefinition } from '@/lib/table/types' +import { createMultipartUpload, deleteFile, headObject } from '@/lib/uploads/core/storage-service' + +const logger = createLogger('TableSnapshotCache') + +const SNAPSHOT_STORAGE_CONTEXT = 'execution' as const +const SNAPSHOT_CONTENT_TYPE = 'text/csv; charset=utf-8' +const SNAPSHOT_BATCH_SIZE = 5000 + +/** + * Upper bound on a materialized snapshot. The sandbox now fetches snapshots by presigned URL (bytes + * never pass through web heap), so this is no longer a RAM limit — it bounds the worst-case inline + * materialization on a cache miss (a synchronous full-table scan in the copilot request). 500MB + * covers most large tables at ~tens of seconds; truly unbounded sizes want a background materializer. + */ +export const SNAPSHOT_MAX_BYTES = 500 * 1024 * 1024 + +export interface TableSnapshotRef { + key: string + size: number + version: number +} + +/** Thrown when a table's CSV would exceed {@link SNAPSHOT_MAX_BYTES}; surfaced as a mount error. */ +export class TableSnapshotTooLargeError extends Error { + constructor(tableId: string) { + super( + `Table ${tableId} is too large to mount (CSV exceeds ${SNAPSHOT_MAX_BYTES / 1024 / 1024}MB). Filter or split it before mounting.` + ) + this.name = 'TableSnapshotTooLargeError' + } +} + +/** + * Fingerprint of the table's column shape (id + display name + order). `rows_version` only advances + * on row mutations (the trigger fires on `user_table_rows`), so without this a schema edit — rename, + * add, remove, or reorder a column — would change the CSV header/columns but keep the same key and + * serve a stale snapshot. Folding it into the key invalidates the cache on any schema change. This + * is also the seam for a future column-subset / filtered projection (mix it into the same hash). + */ +function schemaFingerprint(table: TableDefinition): string { + const shape = table.schema.columns.map((c) => [getColumnId(c), c.name]) + return createHash('sha1').update(JSON.stringify(shape)).digest('hex').slice(0, 12) +} + +/** Storage key for a table's snapshot at a given row version + column shape. */ +function snapshotKey( + workspaceId: string, + tableId: string, + version: number, + shapeHash: string +): string { + return `table-snapshots/${workspaceId}/${tableId}/v${version}-${shapeHash}.csv` +} + +async function readRowsVersion(tableId: string): Promise { + const [row] = await db + .select({ rowsVersion: userTableDefinitions.rowsVersion }) + .from(userTableDefinitions) + .where(eq(userTableDefinitions.id, tableId)) + .limit(1) + if (!row) throw new Error(`Table ${tableId} not found while reading rows_version`) + return row.rowsVersion +} + +/** + * Streams the table CSV (keyset-paginated, like the export worker) into storage under `key`, + * aborting if it crosses {@link SNAPSHOT_MAX_BYTES}. Returns the stored byte size. Bytes match the + * canonical export format (id-keyed reads, display-name headers). + */ +async function materialize(table: TableDefinition, key: string): Promise { + const columns = table.schema.columns + const handle = await createMultipartUpload({ + key, + context: SNAPSHOT_STORAGE_CONTEXT, + contentType: SNAPSHOT_CONTENT_TYPE, + }) + + try { + let bytes = 0 + const header = `${toCsvRow(columns.map((c) => neutralizeCsvFormula(c.name)))}\n` + bytes += Buffer.byteLength(header) + await handle.write(header) + + let after: { orderKey: string; id: string } | null = null + while (true) { + const page = await selectExportRowPage(table, after, SNAPSHOT_BATCH_SIZE) + if (page.length === 0) break + + const chunk = page + .map((row) => `${toCsvRow(columns.map((c) => formatCsvValue(row.data[getColumnId(c)])))}\n`) + .join('') + bytes += Buffer.byteLength(chunk) + if (bytes > SNAPSHOT_MAX_BYTES) throw new TableSnapshotTooLargeError(table.id) + await handle.write(chunk) + + const last = page[page.length - 1] + after = { orderKey: last.orderKey, id: last.id } + if (page.length < SNAPSHOT_BATCH_SIZE) break + } + + const { size } = await handle.complete() + return size + } catch (err) { + await handle.abort().catch(() => {}) + throw err + } +} + +/** Best-effort removal of the immediately-prior version (the common single-mutation case). */ +async function deletePreviousVersion( + table: TableDefinition, + version: number, + shapeHash: string +): Promise { + if (version <= 0) return + await deleteFile({ + key: snapshotKey(table.workspaceId, table.id, version - 1, shapeHash), + context: SNAPSHOT_STORAGE_CONTEXT, + }).catch(() => {}) +} + +/** + * Returns the storage key + size of the table's snapshot at its current `rows_version`, + * materializing and storing it on a miss. The caller mounts by reference (head/download the key). + * + * Best-effort consistency: the version is read, the CSV materialized, then the version re-read. A + * mutation mid-scan (rare) re-keys to the new version and rebuilds once — no DB transaction is held + * across the upload. Concurrent misses write the same version-pinned key (idempotent). + */ +export async function getOrCreateTableSnapshot( + table: TableDefinition, + requestId: string +): Promise { + const shapeHash = schemaFingerprint(table) + const version = await readRowsVersion(table.id) + const key = snapshotKey(table.workspaceId, table.id, version, shapeHash) + + const head = await headObject(key, SNAPSHOT_STORAGE_CONTEXT) + if (head) { + logger.info(`[${requestId}] Snapshot hit`, { tableId: table.id, version, size: head.size }) + return { key, size: head.size, version } + } + + logger.info(`[${requestId}] Snapshot miss; materializing`, { tableId: table.id, version }) + const size = await materialize(table, key) + + const after = await readRowsVersion(table.id) + if (after !== version) { + // The table mutated mid-scan: the bytes under `key` may be torn. Re-key to the new version and + // rebuild once (or reuse if a concurrent writer already stored it); drop the stale object. + logger.info(`[${requestId}] rows_version advanced during materialize; re-keying`, { + tableId: table.id, + from: version, + to: after, + }) + const newKey = snapshotKey(table.workspaceId, table.id, after, shapeHash) + const newHead = await headObject(newKey, SNAPSHOT_STORAGE_CONTEXT) + const newSize = newHead ? newHead.size : await materialize(table, newKey) + await deleteFile({ key, context: SNAPSHOT_STORAGE_CONTEXT }).catch(() => {}) + void deletePreviousVersion(table, after, shapeHash) + return { key: newKey, size: newSize, version: after } + } + + void deletePreviousVersion(table, version, shapeHash) + return { key, size, version } +} diff --git a/apps/sim/lib/uploads/core/storage-service.test.ts b/apps/sim/lib/uploads/core/storage-service.test.ts new file mode 100644 index 0000000000..13dd2ddf17 --- /dev/null +++ b/apps/sim/lib/uploads/core/storage-service.test.ts @@ -0,0 +1,102 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockInitiate, mockUploadPart, mockComplete, mockAbort, mockUploadToS3, partBodies } = + vi.hoisted(() => ({ + mockInitiate: vi.fn(), + mockUploadPart: vi.fn(), + mockComplete: vi.fn(), + mockAbort: vi.fn(), + mockUploadToS3: vi.fn(), + partBodies: [] as Buffer[], + })) + +vi.mock('@/lib/uploads/config', () => ({ + USE_S3_STORAGE: true, + USE_BLOB_STORAGE: false, + getStorageConfig: () => ({ bucket: 'b', region: 'r' }), +})) + +vi.mock('@/lib/uploads/providers/s3/client', () => ({ + initiateS3MultipartUpload: mockInitiate, + uploadS3Part: mockUploadPart, + completeS3MultipartUpload: mockComplete, + abortS3MultipartUpload: mockAbort, + uploadToS3: mockUploadToS3, +})) + +import { createMultipartUpload } from '@/lib/uploads/core/storage-service' + +const PART_SIZE = 8 * 1024 * 1024 + +describe('createMultipartUpload', () => { + beforeEach(() => { + vi.clearAllMocks() + partBodies.length = 0 + mockInitiate.mockResolvedValue({ uploadId: 'up1', key: 'k' }) + mockUploadPart.mockImplementation((_key, _uploadId, partNumber: number, body: Buffer) => { + partBodies.push(body) + return Promise.resolve({ PartNumber: partNumber, ETag: `etag-${partNumber}` }) + }) + mockComplete.mockResolvedValue({ location: 'l', path: 'p', key: 'k' }) + mockAbort.mockResolvedValue(undefined) + mockUploadToS3.mockResolvedValue({ key: 'k', path: 'p', name: 'k', size: 0, type: 'text/csv' }) + }) + + it('takes the single-shot PutObject path for a payload smaller than one part', async () => { + const handle = await createMultipartUpload({ + key: 'k', + context: 'execution', + contentType: 'text/csv', + }) + await handle.write('hello') + const result = await handle.complete() + + expect(mockInitiate).not.toHaveBeenCalled() + expect(mockUploadPart).not.toHaveBeenCalled() + expect(mockUploadToS3).toHaveBeenCalledTimes(1) + expect((mockUploadToS3.mock.calls[0][0] as Buffer).toString('utf8')).toBe('hello') + expect(result).toEqual({ key: 'k', size: 5 }) + }) + + it('splits into parts and reassembles byte-for-byte over one part boundary', async () => { + const a = Buffer.alloc(5 * 1024 * 1024, 1) + const b = Buffer.alloc(5 * 1024 * 1024, 2) + + const handle = await createMultipartUpload({ + key: 'k', + context: 'execution', + contentType: 'text/csv', + }) + await handle.write(a) + await handle.write(b) + const result = await handle.complete() + + expect(mockInitiate).toHaveBeenCalledTimes(1) + // 10MB → one full 8MB part + a 2MB remainder on complete. + expect(mockUploadPart).toHaveBeenCalledTimes(2) + expect(partBodies[0].length).toBe(PART_SIZE) + const reassembled = Buffer.concat(partBodies) + expect(reassembled.length).toBe(10 * 1024 * 1024) + expect(reassembled.equals(Buffer.concat([a, b]))).toBe(true) + expect(mockComplete).toHaveBeenCalledTimes(1) + expect(result.size).toBe(10 * 1024 * 1024) + expect(mockUploadToS3).not.toHaveBeenCalled() + }) + + it('aborts the multipart upload and leaves no object', async () => { + const handle = await createMultipartUpload({ + key: 'k', + context: 'execution', + contentType: 'text/csv', + }) + await handle.write(Buffer.alloc(9 * 1024 * 1024, 7)) // crosses one part → multipart started + await handle.abort() + + expect(mockInitiate).toHaveBeenCalledTimes(1) + expect(mockAbort).toHaveBeenCalledTimes(1) + expect(mockComplete).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/uploads/core/storage-service.ts b/apps/sim/lib/uploads/core/storage-service.ts index d0973a5552..9584bb8996 100644 --- a/apps/sim/lib/uploads/core/storage-service.ts +++ b/apps/sim/lib/uploads/core/storage-service.ts @@ -4,8 +4,8 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { assertKnownSizeWithinLimit } from '@/lib/core/utils/stream-limits' import { getStorageConfig, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/config' -import type { BlobConfig } from '@/lib/uploads/providers/blob/types' -import type { S3Config } from '@/lib/uploads/providers/s3/types' +import type { AzureMultipartPart, BlobConfig } from '@/lib/uploads/providers/blob/types' +import type { S3Config, S3MultipartPart } from '@/lib/uploads/providers/s3/types' import type { DeleteFileOptions, DownloadFileOptions, @@ -182,6 +182,184 @@ export async function uploadFile(options: UploadFileOptions): Promise } } +/** Part size for streaming multipart uploads. ≥ S3's 5MB minimum (all but the last part). */ +const MULTIPART_PART_SIZE = 8 * 1024 * 1024 +/** Max parts uploading concurrently — caps in-flight memory at ~`this × PART_SIZE`. */ +const MULTIPART_MAX_INFLIGHT = 4 + +/** + * Streaming upload sink. The caller `write`s chunks (CSV rows, etc.) and `complete`s; + * the implementation buffers into ≥5MB parts and uploads them with bounded concurrency, + * so peak memory stays ~`MULTIPART_MAX_INFLIGHT × MULTIPART_PART_SIZE` regardless of total + * size. A payload that never crosses one part takes a plain single-shot PutObject. + */ +export interface MultipartUploadHandle { + write(chunk: Buffer | string): Promise + complete(): Promise<{ key: string; size: number }> + abort(): Promise +} + +interface MultipartBackend { + uploadPart(partNumber: number, body: Buffer): Promise + finish(): Promise + abort(): Promise +} + +async function createS3Backend( + key: string, + config: S3Config, + contentType: string, + purpose: string +): Promise { + const { + initiateS3MultipartUpload, + uploadS3Part, + completeS3MultipartUpload, + abortS3MultipartUpload, + } = await import('@/lib/uploads/providers/s3/client') + const { uploadId } = await initiateS3MultipartUpload({ + fileName: key, + contentType, + fileSize: 0, + customConfig: config, + customKey: key, + purpose, + }) + const parts: S3MultipartPart[] = [] + return { + async uploadPart(partNumber, body) { + parts.push(await uploadS3Part(key, uploadId, partNumber, body, config)) + }, + finish: () => completeS3MultipartUpload(key, uploadId, parts, config).then(() => undefined), + abort: () => abortS3MultipartUpload(key, uploadId, config), + } +} + +async function createBlobBackend( + key: string, + config: BlobConfig, + contentType: string +): Promise { + const { stageBlobPart, commitBlobBlockList, abortMultipartUpload } = await import( + '@/lib/uploads/providers/blob/client' + ) + const parts: AzureMultipartPart[] = [] + return { + async uploadPart(partNumber, body) { + parts.push(await stageBlobPart(key, partNumber, body, config)) + }, + finish: () => commitBlobBlockList(key, parts, contentType, config), + abort: () => abortMultipartUpload(key, config), + } +} + +/** + * Open a streaming multipart upload to the configured provider. On the local + * filesystem provider (and for any payload smaller than one part) the bytes are + * buffered and written via a single {@link uploadFile} on `complete`. + */ +export async function createMultipartUpload(options: { + key: string + context: StorageContext + contentType: string +}): Promise { + const { key, context, contentType } = options + const config = getStorageConfig(context) + const cloud = hasCloudStorage() + + let backend: MultipartBackend | null = null + // Accumulate writes as references, not a growing buffer — concatenating only when a part fills + // (or on complete) keeps total copying ~O(bytes) instead of O(bytes × writes). + let pendingChunks: Buffer[] = [] + let pendingBytes = 0 + let totalBytes = 0 + let partNumber = 0 + let aborted = false + let firstError: unknown + const inflight = new Set>() + + /** Merge the accumulated chunks into one ArrayBuffer-backed buffer (which `uploadFile` expects). */ + const drainPending = (): Buffer => Buffer.concat(pendingChunks, pendingBytes) + + const ensureBackend = async (): Promise => { + if (!backend) { + backend = USE_BLOB_STORAGE + ? await createBlobBackend(key, createBlobConfig(config), contentType) + : await createS3Backend(key, createS3Config(config), contentType, context) + } + return backend + } + + const dispatchPart = async (body: Buffer): Promise => { + // Bound concurrency: wait for a free slot before starting another part. + while (inflight.size >= MULTIPART_MAX_INFLIGHT) await Promise.race(inflight) + if (firstError) throw firstError + const be = await ensureBackend() + const partNo = ++partNumber + const p = be + .uploadPart(partNo, body) + .catch((err) => { + firstError ??= err + }) + .finally(() => { + inflight.delete(p) + }) + inflight.add(p) + } + + const abort = async (): Promise => { + aborted = true + await Promise.allSettled(inflight) + if (backend) await backend.abort().catch(() => {}) + } + + return { + async write(chunk) { + if (aborted) throw new Error('Multipart upload already aborted') + if (firstError) throw firstError + const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk + totalBytes += buf.length + pendingChunks.push(buf) + pendingBytes += buf.length + // Local storage has no multipart concept — accumulate and write once on complete. + if (!cloud) return + while (pendingBytes >= MULTIPART_PART_SIZE) { + const merged = drainPending() + const part = merged.subarray(0, MULTIPART_PART_SIZE) + const rest = merged.subarray(MULTIPART_PART_SIZE) + pendingChunks = rest.length ? [rest] : [] + pendingBytes = rest.length + await dispatchPart(part) + } + }, + async complete() { + try { + if (!backend) { + // Never crossed one part (or local provider): single-shot upload. + await uploadFile({ + file: drainPending(), + fileName: key, + contentType, + context, + preserveKey: true, + customKey: key, + }) + return { key, size: totalBytes } + } + if (pendingBytes > 0) await dispatchPart(drainPending()) + await Promise.all(inflight) + if (firstError) throw firstError + await backend.finish() + return { key, size: totalBytes } + } catch (err) { + await abort() + throw err + } + }, + abort, + } +} + /** * Download a file from the configured storage provider */ diff --git a/apps/sim/lib/uploads/providers/blob/client.ts b/apps/sim/lib/uploads/providers/blob/client.ts index b517d9ed36..f2fa592522 100644 --- a/apps/sim/lib/uploads/providers/blob/client.ts +++ b/apps/sim/lib/uploads/providers/blob/client.ts @@ -622,6 +622,69 @@ export async function getMultipartPartUrls( }) } +async function getBlockBlobClientFor(key: string, customConfig?: BlobConfig) { + const { BlobServiceClient, StorageSharedKeyCredential } = await import('@azure/storage-blob') + let blobServiceClient: BlobServiceClientType + let containerName: string + + if (customConfig) { + if (customConfig.connectionString) { + blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString) + } else if (customConfig.accountName && customConfig.accountKey) { + const credential = new StorageSharedKeyCredential( + customConfig.accountName, + customConfig.accountKey + ) + blobServiceClient = new BlobServiceClient( + `https://${customConfig.accountName}.blob.core.windows.net`, + credential + ) + } else { + throw new Error('Invalid custom blob configuration') + } + containerName = customConfig.containerName + } else { + blobServiceClient = await getBlobServiceClient() + containerName = BLOB_CONFIG.containerName + } + + return blobServiceClient.getContainerClient(containerName).getBlockBlobClient(key) +} + +/** + * Stage a single block from the server (body in hand), returning its + * `{ partNumber, blockId }`. The server-side streaming counterpart to the + * presigned {@link getMultipartPartUrls}. + */ +export async function stageBlobPart( + key: string, + partNumber: number, + body: Buffer, + customConfig?: BlobConfig +): Promise { + const blockBlobClient = await getBlockBlobClientFor(key, customConfig) + const blockId = deriveBlobBlockId(partNumber) + await blockBlobClient.stageBlock(blockId, body, body.length) + return { partNumber, blockId } +} + +/** + * Commit staged blocks into the final blob, setting the content type. Used by the + * server-side streaming uploader (the KB flow uses {@link completeMultipartUpload}). + */ +export async function commitBlobBlockList( + key: string, + parts: AzureMultipartPart[], + contentType: string, + customConfig?: BlobConfig +): Promise { + const blockBlobClient = await getBlockBlobClientFor(key, customConfig) + const sortedBlockIds = parts.sort((a, b) => a.partNumber - b.partNumber).map((p) => p.blockId) + await blockBlobClient.commitBlockList(sortedBlockIds, { + blobHTTPHeaders: { blobContentType: contentType }, + }) +} + /** * Complete multipart upload by committing all blocks */ diff --git a/apps/sim/lib/uploads/providers/s3/client.ts b/apps/sim/lib/uploads/providers/s3/client.ts index 978070ea24..fafe4fc889 100644 --- a/apps/sim/lib/uploads/providers/s3/client.ts +++ b/apps/sim/lib/uploads/providers/s3/client.ts @@ -372,6 +372,34 @@ export async function initiateS3MultipartUpload( } } +/** + * Upload a single multipart part from the server (Body in hand), returning its + * `{ PartNumber, ETag }`. The presigned variant ({@link getS3MultipartPartUrls}) + * is for browser uploads; this is the server-side streaming path. + */ +export async function uploadS3Part( + key: string, + uploadId: string, + partNumber: number, + body: Buffer, + customConfig?: S3Config +): Promise { + const config = customConfig || { bucket: S3_CONFIG.bucket, region: S3_CONFIG.region } + const response = await getS3Client().send( + new UploadPartCommand({ + Bucket: config.bucket, + Key: key, + PartNumber: partNumber, + UploadId: uploadId, + Body: body, + }) + ) + if (!response.ETag) { + throw new Error(`S3 UploadPart returned no ETag for part ${partNumber} of ${key}`) + } + return { PartNumber: partNumber, ETag: response.ETag } +} + /** * Generate presigned URLs for uploading parts to S3 */ diff --git a/apps/sim/tools/function/types.ts b/apps/sim/tools/function/types.ts index 9c1afce100..87337ddbeb 100644 --- a/apps/sim/tools/function/types.ts +++ b/apps/sim/tools/function/types.ts @@ -53,7 +53,10 @@ export interface CodeExecutionInput { copilotToolExecution?: boolean } isCustomTool?: boolean - _sandboxFiles?: Array<{ path: string; content: string; encoding?: 'base64' }> + _sandboxFiles?: Array< + | { type?: 'content'; path: string; content: string; encoding?: 'base64' } + | { type: 'url'; path: string; url: string } + > } export interface CodeExecutionOutput extends ToolResponse { diff --git a/packages/db/migrations/0240_table_rows_version.sql b/packages/db/migrations/0240_table_rows_version.sql new file mode 100644 index 0000000000..9dc64bcf37 --- /dev/null +++ b/packages/db/migrations/0240_table_rows_version.sql @@ -0,0 +1,51 @@ +ALTER TABLE "user_table_definitions" ADD COLUMN "rows_version" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint + +-- ============================================================ +-- Statement-level rows_version maintenance for user_table_rows. +-- +-- rows_version is a monotonic counter keyed by the snapshot cache: a CSV stored +-- under v{rows_version} stays valid until the table mutates. The bump MUST live +-- in a trigger, not application code -- every row write (insert/update/delete, +-- including order_key reorders, which are UPDATEs) must invalidate the snapshot, +-- and a trigger is the only bypass-proof layer. +-- +-- Mirrors the statement-level row_count triggers (migration 0224): transition +-- tables let one UPDATE bump every affected table once per statement, so a bulk +-- reorder or import is +1, not +N -- avoiding the per-row lock contention on the +-- single definition row that 0224 removed. The NEW transition table carries the +-- affected table_id for INSERT/UPDATE (table_id never changes), the OLD table for +-- DELETE; both are aliased `changed_rows` so one function serves all three. +-- ============================================================ + +CREATE OR REPLACE FUNCTION bump_user_table_rows_version() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE user_table_definitions d + SET rows_version = d.rows_version + 1 + FROM (SELECT DISTINCT table_id FROM changed_rows) c + WHERE d.id = c.table_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +--> statement-breakpoint + +CREATE TRIGGER user_table_rows_version_insert_trigger + AFTER INSERT ON user_table_rows + REFERENCING NEW TABLE AS changed_rows + FOR EACH STATEMENT + EXECUTE FUNCTION bump_user_table_rows_version(); +--> statement-breakpoint + +CREATE TRIGGER user_table_rows_version_update_trigger + AFTER UPDATE ON user_table_rows + REFERENCING NEW TABLE AS changed_rows + FOR EACH STATEMENT + EXECUTE FUNCTION bump_user_table_rows_version(); +--> statement-breakpoint + +CREATE TRIGGER user_table_rows_version_delete_trigger + AFTER DELETE ON user_table_rows + REFERENCING OLD TABLE AS changed_rows + FOR EACH STATEMENT + EXECUTE FUNCTION bump_user_table_rows_version(); diff --git a/packages/db/migrations/meta/0240_snapshot.json b/packages/db/migrations/meta/0240_snapshot.json new file mode 100644 index 0000000000..8c69feafcc --- /dev/null +++ b/packages/db/migrations/meta/0240_snapshot.json @@ -0,0 +1,16573 @@ +{ + "id": "34c63a54-3015-4e6d-9bed-5fe0d53f5741", + "prevId": "488b76cc-8da9-43fa-90fb-bfe7a54b34c1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_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": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.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.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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 + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "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.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 + }, + "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_a2a_executions": { + "name": "total_a2a_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 + } + }, + "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 + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "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_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + }, + "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" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + }, + "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 + }, + "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": {} + } + }, + "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" + } + }, + "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 + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "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_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "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_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 2667376721..1f61dfeddc 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1674,6 +1674,13 @@ "when": 1781659761380, "tag": "0239_wel_logs_desc_index_and_redundant_drops", "breakpoints": true + }, + { + "idx": 240, + "version": "7", + "when": 1781670551980, + "tag": "0240_table_rows_version", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index de43647af2..67a7b99f93 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3142,6 +3142,14 @@ export const userTableDefinitions = pgTable( metadata: jsonb('metadata'), maxRows: integer('max_rows').notNull().default(10000), rowCount: integer('row_count').notNull().default(0), + /** + * @remarks + * Monotonic counter bumped by a statement-level trigger on `user_table_rows` + * (INSERT/UPDATE/DELETE). Keys the versioned table-snapshot cache so a stored + * CSV under `v{rows_version}` is reused until the table mutates. Never written + * from application code — the trigger is the only writer (bypass-proof). + */ + rowsVersion: bigint('rows_version', { mode: 'number' }).notNull().default(0), archivedAt: timestamp('archived_at'), createdBy: text('created_by') .notNull() From 4d39b0cbf87b2af70ef255f941cc0e4a1a4189d6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 12:56:46 -0700 Subject: [PATCH 18/26] feat(connectors): use resource selectors for KB connector config (#5116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(connectors): use resource selectors for KB connector config Replace raw ID text inputs with selector pickers (canonical selector + manual-input pairs) across Google Drive/Docs/Forms/Sheets, Notion, Monday, and Webflow KB connectors, so users pick folders/spreadsheets/pages/boards/ collections instead of pasting IDs — matching the workflow blocks. - Add multi-select where the sync handler supports it (Drive/Docs/Forms folders, Monday boards, Webflow collections) via parseMultiValue - Add shared escapeDriveQueryValue/buildDriveParentsClause helpers for safe multi-folder Drive queries - Add ConnectorConfigField.mimeType, plumbed into the selector context - Fix Webflow listingCapped not set on maxItems truncation (deletion- reconciliation data-loss safety) Fully backward compatible: legacy single-string IDs and CSV both normalize via parseMultiValue; resolved canonical keys are unchanged. * fix(webflow): set listingCapped on within-page maxItems truncation When a collection's items fit in a single API page but maxItems cuts the list within that page, neither hasMoreInCollection nor hasMoreCollections is true, so listingCapped was not set and the sync engine could hard-delete still-existing documents. Add the within-page drop signal to the guard. --- .../connector-selector-field.tsx | 3 +- .../sim/connectors/google-docs/google-docs.ts | 65 ++++++++------- apps/sim/connectors/google-docs/meta.ts | 19 ++++- .../connectors/google-drive/google-drive.ts | 62 ++++++++------- apps/sim/connectors/google-drive/meta.ts | 19 ++++- .../connectors/google-forms/google-forms.ts | 79 +++++++++++-------- apps/sim/connectors/google-forms/meta.ts | 22 +++++- apps/sim/connectors/google-sheets/meta.ts | 13 +++ apps/sim/connectors/monday/meta.ts | 16 ++++ apps/sim/connectors/notion/meta.ts | 12 +++ apps/sim/connectors/types.ts | 2 + apps/sim/connectors/utils.ts | 20 +++++ apps/sim/connectors/webflow/meta.ts | 19 ++++- apps/sim/connectors/webflow/webflow.ts | 37 ++++++--- 14 files changed, 280 insertions(+), 108 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx index 242854ee87..ac16b24c93 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx @@ -38,6 +38,7 @@ export function ConnectorSelectorField({ const context = useMemo(() => { const ctx: SelectorContext = {} if (credentialId) ctx.oauthCredential = credentialId + if (field.mimeType) ctx.mimeType = field.mimeType for (const depFieldId of getDependsOnFields(field.dependsOn)) { const depField = configFields.find((f) => f.id === depFieldId) @@ -49,7 +50,7 @@ export function ConnectorSelectorField({ } return ctx - }, [credentialId, field.dependsOn, sourceConfig, configFields, canonicalModes]) + }, [credentialId, field.mimeType, field.dependsOn, sourceConfig, configFields, canonicalModes]) const depsResolved = useMemo(() => { if (!field.dependsOn) return true diff --git a/apps/sim/connectors/google-docs/google-docs.ts b/apps/sim/connectors/google-docs/google-docs.ts index 5c62fcc61d..f08b405eb5 100644 --- a/apps/sim/connectors/google-docs/google-docs.ts +++ b/apps/sim/connectors/google-docs/google-docs.ts @@ -3,7 +3,12 @@ import { toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { googleDocsConnectorMeta } from '@/connectors/google-docs/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { joinTagArray, parseTagDate } from '@/connectors/utils' +import { + buildDriveParentsClause, + joinTagArray, + parseMultiValue, + parseTagDate, +} from '@/connectors/utils' const logger = createLogger('GoogleDocsConnector') @@ -152,10 +157,8 @@ function fileToStub(file: DriveFile): ExternalDocument { function buildQuery(sourceConfig: Record): string { const parts: string[] = ['trashed = false', "mimeType = 'application/vnd.google-apps.document'"] - const folderId = sourceConfig.folderId as string | undefined - if (folderId?.trim()) { - parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`) - } + const parentsClause = buildDriveParentsClause(parseMultiValue(sourceConfig.folderId)) + if (parentsClause) parts.push(parentsClause) return parts.join(' and ') } @@ -298,7 +301,7 @@ export const googleDocsConnector: ConnectorConfig = { accessToken: string, sourceConfig: Record ): Promise<{ valid: boolean; error?: string }> => { - const folderId = sourceConfig.folderId as string | undefined + const folderIds = parseMultiValue(sourceConfig.folderId) const maxDocs = sourceConfig.maxDocs as string | undefined if (maxDocs && (Number.isNaN(Number(maxDocs)) || Number(maxDocs) <= 0)) { @@ -306,30 +309,38 @@ export const googleDocsConnector: ConnectorConfig = { } try { - if (folderId?.trim()) { - const url = `https://www.googleapis.com/drive/v3/files/${folderId.trim()}?fields=id,name,mimeType&supportsAllDrives=true` - const response = await fetchWithRetry( - url, - { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', + if (folderIds.length > 0) { + for (const folderId of folderIds) { + const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true` + const response = await fetchWithRetry( + url, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, }, - }, - VALIDATE_RETRY_OPTIONS - ) - - if (!response.ok) { - if (response.status === 404) { - return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' } + VALIDATE_RETRY_OPTIONS + ) + + if (!response.ok) { + if (response.status === 404) { + return { + valid: false, + error: `Folder "${folderId}" not found. Check the folder ID and permissions.`, + } + } + return { + valid: false, + error: `Failed to access folder "${folderId}": ${response.status}`, + } } - return { valid: false, error: `Failed to access folder: ${response.status}` } - } - const folder = await response.json() - if (folder.mimeType !== 'application/vnd.google-apps.folder') { - return { valid: false, error: 'The provided ID is not a folder' } + const folder = await response.json() + if (folder.mimeType !== 'application/vnd.google-apps.folder') { + return { valid: false, error: `"${folderId}" is not a folder` } + } } } else { const url = diff --git a/apps/sim/connectors/google-docs/meta.ts b/apps/sim/connectors/google-docs/meta.ts index f1cc2a8e6a..eaca1bd826 100644 --- a/apps/sim/connectors/google-docs/meta.ts +++ b/apps/sim/connectors/google-docs/meta.ts @@ -15,11 +15,26 @@ export const googleDocsConnectorMeta: ConnectorMeta = { }, configFields: [ + { + id: 'folderSelector', + title: 'Folders', + type: 'selector', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.folder', + canonicalParamId: 'folderId', + mode: 'basic', + multi: true, + placeholder: 'Select one or more folders (optional)', + required: false, + }, { id: 'folderId', - title: 'Folder ID', + title: 'Folder IDs', type: 'short-input', - placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)', + canonicalParamId: 'folderId', + mode: 'advanced', + multi: true, + placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)', required: false, }, { diff --git a/apps/sim/connectors/google-drive/google-drive.ts b/apps/sim/connectors/google-drive/google-drive.ts index 14d98bc413..e7c2def3b5 100644 --- a/apps/sim/connectors/google-drive/google-drive.ts +++ b/apps/sim/connectors/google-drive/google-drive.ts @@ -4,11 +4,13 @@ import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/document import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' import { + buildDriveParentsClause, CONNECTOR_MAX_FILE_BYTES, ConnectorFileTooLargeError, htmlToPlainText, joinTagArray, markSkipped, + parseMultiValue, parseTagDate, readBodyWithLimit, sizeLimitSkipReason, @@ -137,10 +139,8 @@ interface DriveFile { function buildQuery(sourceConfig: Record): string { const parts: string[] = ['trashed = false'] - const folderId = sourceConfig.folderId as string | undefined - if (folderId?.trim()) { - parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`) - } + const parentsClause = buildDriveParentsClause(parseMultiValue(sourceConfig.folderId)) + if (parentsClause) parts.push(parentsClause) const fileType = (sourceConfig.fileType as string) || 'all' switch (fileType) { @@ -324,7 +324,7 @@ export const googleDriveConnector: ConnectorConfig = { accessToken: string, sourceConfig: Record ): Promise<{ valid: boolean; error?: string }> => { - const folderId = sourceConfig.folderId as string | undefined + const folderIds = parseMultiValue(sourceConfig.folderId) const maxFiles = sourceConfig.maxFiles as string | undefined if (maxFiles && (Number.isNaN(Number(maxFiles)) || Number(maxFiles) <= 0)) { @@ -333,31 +333,39 @@ export const googleDriveConnector: ConnectorConfig = { // Verify access to Drive API try { - if (folderId?.trim()) { - // Verify the folder exists and is accessible - const url = `https://www.googleapis.com/drive/v3/files/${folderId.trim()}?fields=id,name,mimeType&supportsAllDrives=true` - const response = await fetchWithRetry( - url, - { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', + if (folderIds.length > 0) { + // Verify each folder exists, is accessible, and is actually a folder + for (const folderId of folderIds) { + const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true` + const response = await fetchWithRetry( + url, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, }, - }, - VALIDATE_RETRY_OPTIONS - ) - - if (!response.ok) { - if (response.status === 404) { - return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' } + VALIDATE_RETRY_OPTIONS + ) + + if (!response.ok) { + if (response.status === 404) { + return { + valid: false, + error: `Folder "${folderId}" not found. Check the folder ID and permissions.`, + } + } + return { + valid: false, + error: `Failed to access folder "${folderId}": ${response.status}`, + } } - return { valid: false, error: `Failed to access folder: ${response.status}` } - } - const folder = await response.json() - if (folder.mimeType !== 'application/vnd.google-apps.folder') { - return { valid: false, error: 'The provided ID is not a folder' } + const folder = await response.json() + if (folder.mimeType !== 'application/vnd.google-apps.folder') { + return { valid: false, error: `"${folderId}" is not a folder` } + } } } else { // Verify basic Drive access by listing one file diff --git a/apps/sim/connectors/google-drive/meta.ts b/apps/sim/connectors/google-drive/meta.ts index 6af635129b..eec6fd8fa2 100644 --- a/apps/sim/connectors/google-drive/meta.ts +++ b/apps/sim/connectors/google-drive/meta.ts @@ -15,11 +15,26 @@ export const googleDriveConnectorMeta: ConnectorMeta = { }, configFields: [ + { + id: 'folderSelector', + title: 'Folders', + type: 'selector', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.folder', + canonicalParamId: 'folderId', + mode: 'basic', + multi: true, + placeholder: 'Select one or more folders (optional)', + required: false, + }, { id: 'folderId', - title: 'Folder ID', + title: 'Folder IDs', type: 'short-input', - placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)', + canonicalParamId: 'folderId', + mode: 'advanced', + multi: true, + placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)', required: false, }, { diff --git a/apps/sim/connectors/google-forms/google-forms.ts b/apps/sim/connectors/google-forms/google-forms.ts index 992c698dd8..1849fd0b38 100644 --- a/apps/sim/connectors/google-forms/google-forms.ts +++ b/apps/sim/connectors/google-forms/google-forms.ts @@ -3,7 +3,12 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import { googleFormsConnectorMeta, MAX_RESPONSES_PER_FORM } from '@/connectors/google-forms/meta' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { joinTagArray, parseTagDate } from '@/connectors/utils' +import { + buildDriveParentsClause, + joinTagArray, + parseMultiValue, + parseTagDate, +} from '@/connectors/utils' const logger = createLogger('GoogleFormsConnector') @@ -448,16 +453,14 @@ function renderFormDocument(form: FormStructure, responses: FormResponse[]): str } /** - * Builds the Drive `q` query that selects form files, optionally scoped to a - * folder. Single quotes and backslashes in the folder ID are escaped to prevent - * query injection. + * Builds the Drive `q` query that selects form files, optionally scoped to one + * or more folders. Single quotes and backslashes in folder IDs are escaped to + * prevent query injection. */ -function buildDriveQuery(folderId?: string): string { +function buildDriveQuery(folderIds: string[]): string { const parts = ['trashed = false', `mimeType = '${FORM_MIME_TYPE}'`] - if (folderId?.trim()) { - const escaped = folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'") - parts.push(`'${escaped}' in parents`) - } + const parentsClause = buildDriveParentsClause(folderIds) + if (parentsClause) parts.push(parentsClause) return parts.join(' and ') } @@ -479,9 +482,9 @@ export const googleFormsConnector: ConnectorConfig = { return { documents: [], hasMore: false } } - const folderId = sourceConfig.folderId as string | undefined + const folderIds = parseMultiValue(sourceConfig.folderId) const queryParams = new URLSearchParams({ - q: buildDriveQuery(folderId), + q: buildDriveQuery(folderIds), pageSize: String(DRIVE_PAGE_SIZE), orderBy: 'modifiedTime desc', fields: 'nextPageToken,files(id,name,mimeType,modifiedTime,createdTime,webViewLink,owners)', @@ -493,7 +496,7 @@ export const googleFormsConnector: ConnectorConfig = { const url = `${DRIVE_API_BASE}/files?${queryParams.toString()}` logger.info('Listing Google Forms', { - folderId: folderId?.trim() || 'all', + folderId: folderIds.length > 0 ? folderIds.join(',') : 'all', contentScope, cursor: cursor ?? 'initial', }) @@ -667,7 +670,7 @@ export const googleFormsConnector: ConnectorConfig = { accessToken: string, sourceConfig: Record ): Promise<{ valid: boolean; error?: string }> => { - const folderId = sourceConfig.folderId as string | undefined + const folderIds = parseMultiValue(sourceConfig.folderId) const maxForms = sourceConfig.maxForms as string | undefined const maxResponsesPerForm = sourceConfig.maxResponsesPerForm as string | undefined @@ -683,30 +686,38 @@ export const googleFormsConnector: ConnectorConfig = { } try { - if (folderId?.trim()) { - const url = `${DRIVE_API_BASE}/files/${encodeURIComponent(folderId.trim())}?fields=id,name,mimeType&supportsAllDrives=true` - const response = await fetchWithRetry( - url, - { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', + if (folderIds.length > 0) { + for (const folderId of folderIds) { + const url = `${DRIVE_API_BASE}/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true` + const response = await fetchWithRetry( + url, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, }, - }, - VALIDATE_RETRY_OPTIONS - ) - - if (!response.ok) { - if (response.status === 404) { - return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' } + VALIDATE_RETRY_OPTIONS + ) + + if (!response.ok) { + if (response.status === 404) { + return { + valid: false, + error: `Folder "${folderId}" not found. Check the folder ID and permissions.`, + } + } + return { + valid: false, + error: `Failed to access folder "${folderId}": ${response.status}`, + } } - return { valid: false, error: `Failed to access folder: ${response.status}` } - } - const folder = await response.json() - if (folder.mimeType !== FOLDER_MIME_TYPE) { - return { valid: false, error: 'The provided ID is not a folder' } + const folder = await response.json() + if (folder.mimeType !== FOLDER_MIME_TYPE) { + return { valid: false, error: `"${folderId}" is not a folder` } + } } } else { const url = `${DRIVE_API_BASE}/files?pageSize=1&q=${encodeURIComponent(`mimeType = '${FORM_MIME_TYPE}'`)}&fields=files(id)&supportsAllDrives=true&includeItemsFromAllDrives=true` diff --git a/apps/sim/connectors/google-forms/meta.ts b/apps/sim/connectors/google-forms/meta.ts index 8ced1c05d1..3ea4b31fbd 100644 --- a/apps/sim/connectors/google-forms/meta.ts +++ b/apps/sim/connectors/google-forms/meta.ts @@ -25,13 +25,29 @@ export const googleFormsConnectorMeta: ConnectorMeta = { }, configFields: [ + { + id: 'folderSelector', + title: 'Folders', + type: 'selector', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.folder', + canonicalParamId: 'folderId', + mode: 'basic', + multi: true, + placeholder: 'Select one or more folders (optional)', + required: false, + description: 'Only sync forms inside these Drive folders. Leave blank to sync all forms.', + }, { id: 'folderId', - title: 'Folder ID', + title: 'Folder IDs', type: 'short-input', - placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)', + canonicalParamId: 'folderId', + mode: 'advanced', + multi: true, + placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)', required: false, - description: 'Only sync forms inside this Drive folder. Leave blank to sync all forms.', + description: 'Only sync forms inside these Drive folders. Leave blank to sync all forms.', }, { id: 'contentScope', diff --git a/apps/sim/connectors/google-sheets/meta.ts b/apps/sim/connectors/google-sheets/meta.ts index 1e7fd7b6da..7a20991d71 100644 --- a/apps/sim/connectors/google-sheets/meta.ts +++ b/apps/sim/connectors/google-sheets/meta.ts @@ -15,10 +15,23 @@ export const googleSheetsConnectorMeta: ConnectorMeta = { }, configFields: [ + { + id: 'spreadsheetSelector', + title: 'Spreadsheet', + type: 'selector', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.spreadsheet', + canonicalParamId: 'spreadsheetId', + mode: 'basic', + placeholder: 'Select a spreadsheet', + required: true, + }, { id: 'spreadsheetId', title: 'Spreadsheet ID', type: 'short-input', + canonicalParamId: 'spreadsheetId', + mode: 'advanced', placeholder: 'e.g. 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms', required: true, description: 'The ID from the spreadsheet URL: docs.google.com/spreadsheets/d/{ID}/edit', diff --git a/apps/sim/connectors/monday/meta.ts b/apps/sim/connectors/monday/meta.ts index 27b6287521..ab9ffc882e 100644 --- a/apps/sim/connectors/monday/meta.ts +++ b/apps/sim/connectors/monday/meta.ts @@ -15,10 +15,26 @@ export const mondayConnectorMeta: ConnectorMeta = { }, configFields: [ + { + id: 'boardSelector', + title: 'Boards', + type: 'selector', + selectorKey: 'monday.boards', + canonicalParamId: 'boardIds', + mode: 'basic', + multi: true, + required: false, + placeholder: 'Select boards (empty = all active boards)', + description: + 'Boards to sync. Leave empty to sync items from every active board you can access.', + }, { id: 'boardIds', title: 'Board IDs', type: 'short-input', + canonicalParamId: 'boardIds', + mode: 'advanced', + multi: true, required: false, placeholder: 'e.g. 1234567890, 9876543210 (empty = all active boards)', description: diff --git a/apps/sim/connectors/notion/meta.ts b/apps/sim/connectors/notion/meta.ts index f0fdd7bd6f..e1220cdb94 100644 --- a/apps/sim/connectors/notion/meta.ts +++ b/apps/sim/connectors/notion/meta.ts @@ -43,10 +43,22 @@ export const notionConnectorMeta: ConnectorMeta = { required: false, placeholder: 'e.g. 8a3b5f6e-..., 9c4d6e7f-... (comma-separated for multiple)', }, + { + id: 'rootPageSelector', + title: 'Page', + type: 'selector', + selectorKey: 'notion.pages', + canonicalParamId: 'rootPageId', + mode: 'basic', + placeholder: 'Select a page', + required: false, + }, { id: 'rootPageId', title: 'Page ID', type: 'short-input', + canonicalParamId: 'rootPageId', + mode: 'advanced', required: false, placeholder: 'e.g. 8a3b5f6e-1234-5678-abcd-ef0123456789', }, diff --git a/apps/sim/connectors/types.ts b/apps/sim/connectors/types.ts index c012a7ae56..2782fc588f 100644 --- a/apps/sim/connectors/types.ts +++ b/apps/sim/connectors/types.ts @@ -74,6 +74,8 @@ export interface ConnectorConfigField { /** Selector key from the selector registry (used when type is 'selector') */ selectorKey?: SelectorKey + /** MIME type filter passed to the selector context (e.g. limit a Google Drive picker to folders) */ + mimeType?: string /** Field IDs this field depends on — clears when deps change */ dependsOn?: string[] | { all?: string[]; any?: string[] } diff --git a/apps/sim/connectors/utils.ts b/apps/sim/connectors/utils.ts index 242edbae72..49d9c30669 100644 --- a/apps/sim/connectors/utils.ts +++ b/apps/sim/connectors/utils.ts @@ -95,6 +95,26 @@ export function parseMultiValue(value: unknown): string[] { return [] } +/** + * Escapes a value for safe interpolation into a Google Drive `q` query string, + * neutralizing backslashes and single quotes to prevent query injection. + */ +export function escapeDriveQueryValue(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") +} + +/** + * Builds a Drive `q` clause matching files parented by any of the given folder + * IDs — e.g. `('A' in parents or 'B' in parents)`. Returns null when no folder + * IDs are supplied so callers can omit the clause entirely. A single ID is + * emitted without wrapping parentheses to keep the query minimal. + */ +export function buildDriveParentsClause(folderIds: string[]): string | null { + if (folderIds.length === 0) return null + const clause = folderIds.map((id) => `'${escapeDriveQueryValue(id)}' in parents`).join(' or ') + return folderIds.length > 1 ? `(${clause})` : clause +} + /** * Reads a response body into a Buffer while enforcing a hard byte cap. The * declared `content-length` header cannot be trusted as the sole guard — diff --git a/apps/sim/connectors/webflow/meta.ts b/apps/sim/connectors/webflow/meta.ts index b3b6b37e34..81de15a16a 100644 --- a/apps/sim/connectors/webflow/meta.ts +++ b/apps/sim/connectors/webflow/meta.ts @@ -31,11 +31,26 @@ export const webflowConnectorMeta: ConnectorMeta = { placeholder: 'Your Webflow site ID', required: true, }, + { + id: 'collectionSelector', + title: 'Collections', + type: 'selector', + selectorKey: 'webflow.collections', + canonicalParamId: 'collectionId', + mode: 'basic', + multi: true, + dependsOn: ['siteSelector'], + placeholder: 'Select collections (default: all collections)', + required: false, + }, { id: 'collectionId', - title: 'Collection ID', + title: 'Collection IDs', type: 'short-input', - placeholder: 'Specific collection ID (default: all collections)', + canonicalParamId: 'collectionId', + mode: 'advanced', + multi: true, + placeholder: 'Specific collection IDs, comma-separated (default: all collections)', required: false, }, { diff --git a/apps/sim/connectors/webflow/webflow.ts b/apps/sim/connectors/webflow/webflow.ts index e6f208a971..c2172f0afa 100644 --- a/apps/sim/connectors/webflow/webflow.ts +++ b/apps/sim/connectors/webflow/webflow.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' -import { htmlToPlainText, parseTagDate } from '@/connectors/utils' +import { htmlToPlainText, parseMultiValue, parseTagDate } from '@/connectors/utils' import { webflowConnectorMeta } from '@/connectors/webflow/meta' const logger = createLogger('WebflowConnector') @@ -89,7 +89,7 @@ export const webflowConnector: ConnectorConfig = { syncContext?: Record ): Promise => { const siteId = sourceConfig.siteId as string - const collectionId = sourceConfig.collectionId as string | undefined + const collectionIds = parseMultiValue(sourceConfig.collectionId) const maxItems = sourceConfig.maxItems ? Number(sourceConfig.maxItems) : 0 let cursorState: CursorState @@ -97,7 +97,7 @@ export const webflowConnector: ConnectorConfig = { if (cursor) { cursorState = JSON.parse(cursor) as CursorState } else { - const collections = await fetchCollectionIds(accessToken, siteId, collectionId) + const collections = await fetchCollectionIds(accessToken, siteId, collectionIds) cursorState = { collectionIndex: 0, offset: 0, collections } } @@ -152,12 +152,13 @@ export const webflowConnector: ConnectorConfig = { } const items = data.items || [] - let documents: ExternalDocument[] = items.map((item) => + const pageDocuments: ExternalDocument[] = items.map((item) => itemToDocument(item, currentCollectionId, collectionName) ) + let documents = pageDocuments if (maxItems > 0) { - const remaining = maxItems - totalDocsFetched + const remaining = Math.max(0, maxItems - totalDocsFetched) if (documents.length > remaining) { documents = documents.slice(0, remaining) } @@ -171,6 +172,22 @@ export const webflowConnector: ConnectorConfig = { const hasMoreInCollection = cursorState.offset + pagination.limit < pagination.total const hasMoreCollections = cursorState.collectionIndex < cursorState.collections.length - 1 const hitMaxItems = maxItems > 0 && totalDocsFetched + documents.length >= maxItems + /** + * When the cap stops the sync, flag the listing as capped so the sync engine + * skips deletion reconciliation — otherwise still-existing documents that + * were never listed get hard-deleted. "More" means any of: items dropped + * from this page (`pageDocuments.length > documents.length`), more pages in + * this collection, or more collections still to visit. The within-page drop + * is the only signal when a collection fits in a single API response. + */ + const droppedWithinPage = documents.length < pageDocuments.length + if ( + syncContext && + hitMaxItems && + (droppedWithinPage || hasMoreInCollection || hasMoreCollections) + ) { + syncContext.listingCapped = true + } let nextCursor: string | undefined if (hitMaxItems) { @@ -236,7 +253,7 @@ export const webflowConnector: ConnectorConfig = { sourceConfig: Record ): Promise<{ valid: boolean; error?: string }> => { const siteId = sourceConfig.siteId as string - const collectionId = sourceConfig.collectionId as string | undefined + const collectionIds = parseMultiValue(sourceConfig.collectionId) const maxItems = sourceConfig.maxItems as string | undefined if (!siteId) { @@ -272,7 +289,7 @@ export const webflowConnector: ConnectorConfig = { return { valid: false, error: `Webflow API error: ${siteResponse.status} - ${errorText}` } } - if (collectionId) { + for (const collectionId of collectionIds) { const collectionUrl = `${WEBFLOW_API}/collections/${collectionId}` const collectionResponse = await fetchWithRetry( collectionUrl, @@ -357,10 +374,10 @@ function itemToDocument( async function fetchCollectionIds( accessToken: string, siteId: string, - collectionId?: string + collectionIds: string[] ): Promise { - if (collectionId) { - return [collectionId] + if (collectionIds.length > 0) { + return collectionIds } const url = `${WEBFLOW_API}/sites/${siteId}/collections` From cae176911798ced075c8a703afa813613b1ab70a Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 13:28:00 -0700 Subject: [PATCH 19/26] improvement(knowledge): align connected-sources rows and move source chip left of filter/sort (#5117) * improvement(knowledge): align connected-sources rows and move source chip left of filter/sort - Drop the -mx-2 on the connectors list so rows respect the ChipModalBody gutter: the row hover no longer bleeds to the modal edges and row content lines up with the px-4 header. - Add a 'leading' slot to ResourceOptions (left of the filter/sort cluster) and render the knowledge connected-source chip there instead of the far-right 'aside', so it reads as part of the control row. 'aside' stays right-aligned for the table editor's run/stop control. * improvement(resource): render options aside left of filter/sort The options-bar aside has a single other consumer (the table editor's embedded run/stop control), so instead of adding a separate slot, render aside itself to the left of the filter/sort cluster. Drops the extra slot and keeps one canonical control position; the run/stop control moves left too, which is fine for a status widget. * fix(resource): keep options aside grouped with filter/sort without a search bar Group aside + the filter/sort cluster in one ml-auto right-aligned container instead of relying on the search's flex-1 to anchor them. Without this, an options bar with no search (the embedded mothership table editor) split aside to the far left and filter/sort to the far right via justify-between. * docs(resource): clarify aside groups with filter/sort regardless of search --- .../resource-options/resource-options.tsx | 126 +++++++++--------- .../connectors-section/connectors-section.tsx | 2 +- .../[workspaceId]/tables/[tableId]/table.tsx | 6 +- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx index 545f12e681..ada916a41c 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx @@ -83,11 +83,11 @@ interface ResourceOptionsProps { filter?: FilterConfig filterTags?: FilterTag[] /** - * Supplementary right-aligned slot (pushed opposite the left-aligned - * filter/sort via `justify-between`) for lightweight status content — e.g. - * the knowledge list's connector badges or the table editor's run/stop - * control in embedded mode. Keep it to badges/status widgets; primary - * actions belong in the header's `actions`, not here. + * Lightweight control rendered immediately to the LEFT of the filter/sort + * cluster; the two form one right-aligned group, with or without a search — + * e.g. the knowledge view's connected-source badge or the table editor's + * embedded run/stop control. Keep it to badges/status widgets; primary actions + * belong in the header's `actions`. */ aside?: ReactNode } @@ -115,64 +115,68 @@ export const ResourceOptions = memo(function ResourceOptions({ return (

-
+
{search && } -
- {filterTags?.map((tag) => ( - - {tag.label} - - ))} - {isToggleFilter && filter.mode === 'toggle' ? ( - - Filter - - ) : popoverFilter ? ( - - setOpenMenu((current) => (open ? 'filter' : current === 'filter' ? null : current)) - } - > - -
- - - Filter - - - {sort && ( - - setOpenMenu((current) => - open ? 'sort' : current === 'sort' ? null : current - ) - } - /> - )} -
-
- - - {popoverFilter.content} - - -
- ) : null} - {sort && (isToggleFilter || !popoverFilter) && } +
+ {aside} +
+ {filterTags?.map((tag) => ( + + {tag.label} + + ))} + {isToggleFilter && filter.mode === 'toggle' ? ( + + Filter + + ) : popoverFilter ? ( + + setOpenMenu((current) => + open ? 'filter' : current === 'filter' ? null : current + ) + } + > + +
+ + + Filter + + + {sort && ( + + setOpenMenu((current) => + open ? 'sort' : current === 'sort' ? null : current + ) + } + /> + )} +
+
+ + + {popoverFilter.content} + + +
+ ) : null} + {sort && (isToggleFilter || !popoverFilter) && } +
- {aside &&
{aside}
}
) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 6c3764a4c2..81769e2aa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -202,7 +202,7 @@ export function ConnectorsSection({ No connected sources yet. Connect an external source to automatically sync documents.

) : ( -
+
{connectors.map((connector) => ( )} - {/* Sort + filter render in both modes (left-aligned). In embedded (mothership) - mode there's no Resource.Header, so the run/stop control rides in the options - bar's right-aligned `aside` slot — opposite the left-aligned filter/sort. */} + {/* Sort + filter render in both modes. In embedded (mothership) mode there's no + Resource.Header, so the run/stop control rides in the options bar's `aside` + slot, just left of filter/sort. */} Date: Wed, 17 Jun 2026 13:37:23 -0700 Subject: [PATCH 20/26] fix(azure): replace Azure DevOps icon with Azure icon and remove AzureDevOpsIcon (#5118) --- apps/sim/components/icons.tsx | 30 ------------------- apps/sim/connectors/azure-devops/meta.ts | 4 +-- apps/sim/connectors/utils.test.ts | 2 +- .../sim/triggers/azure_devops/build_failed.ts | 4 +-- apps/sim/triggers/azure_devops/webhook.ts | 4 +-- .../azure_devops/work_item_created.ts | 4 +-- 6 files changed, 9 insertions(+), 39 deletions(-) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 13fa62588b..89f0b818dd 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3396,36 +3396,6 @@ export function AzureIcon(props: SVGProps) { ) } -export function AzureDevOpsIcon(props: SVGProps) { - const id = useId() - const gradientId = `azure_devops_gradient_${id}` - return ( - - - - - - - - - - - - - ) -} - export const GroqIcon = (props: SVGProps) => ( ({ JiraServiceManagementIcon: () => null, S3Icon: () => null, GoogleFormsIcon: () => null, - AzureDevOpsIcon: () => null, xIcon: () => null, GranolaIcon: () => null, GreenhouseIcon: () => null, FathomIcon: () => null, RootlyIcon: () => null, + AzureIcon: () => null, })) vi.mock('@/lib/knowledge/documents/utils', () => ({ fetchWithRetry: vi.fn(), diff --git a/apps/sim/triggers/azure_devops/build_failed.ts b/apps/sim/triggers/azure_devops/build_failed.ts index f43619215f..1ec390b1f3 100644 --- a/apps/sim/triggers/azure_devops/build_failed.ts +++ b/apps/sim/triggers/azure_devops/build_failed.ts @@ -1,4 +1,4 @@ -import { AzureDevOpsIcon } from '@/components/icons' +import { AzureIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { azureDevOpsTriggerOptions, @@ -14,7 +14,7 @@ export const azureDevOpsBuildFailedTrigger: TriggerConfig = { description: 'Trigger workflow when an Azure DevOps build fails, is canceled, or partially succeeds', version: '1.0.0', - icon: AzureDevOpsIcon, + icon: AzureIcon, subBlocks: buildTriggerSubBlocks({ triggerId: 'azure_devops_build_failed', diff --git a/apps/sim/triggers/azure_devops/webhook.ts b/apps/sim/triggers/azure_devops/webhook.ts index fda0442409..7358f68dc8 100644 --- a/apps/sim/triggers/azure_devops/webhook.ts +++ b/apps/sim/triggers/azure_devops/webhook.ts @@ -1,4 +1,4 @@ -import { AzureDevOpsIcon } from '@/components/icons' +import { AzureIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { azureDevOpsTriggerOptions, @@ -18,7 +18,7 @@ export const azureDevOpsWebhookTrigger: TriggerConfig = { description: 'Trigger on whichever service hook event types you configure in Azure DevOps. Sim does not filter deliveries for this trigger.', version: '1.0.0', - icon: AzureDevOpsIcon, + icon: AzureIcon, subBlocks: buildTriggerSubBlocks({ triggerId: 'azure_devops_webhook', diff --git a/apps/sim/triggers/azure_devops/work_item_created.ts b/apps/sim/triggers/azure_devops/work_item_created.ts index 289ddf4676..12722a5429 100644 --- a/apps/sim/triggers/azure_devops/work_item_created.ts +++ b/apps/sim/triggers/azure_devops/work_item_created.ts @@ -1,4 +1,4 @@ -import { AzureDevOpsIcon } from '@/components/icons' +import { AzureIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { azureDevOpsTriggerOptions, @@ -13,7 +13,7 @@ export const azureDevOpsWorkItemCreatedTrigger: TriggerConfig = { provider: 'azure_devops', description: 'Trigger workflow when a work item is created in Azure DevOps', version: '1.0.0', - icon: AzureDevOpsIcon, + icon: AzureIcon, subBlocks: buildTriggerSubBlocks({ triggerId: 'azure_devops_work_item_created', From 08bcacd74e66ed94ba1d9c7792c6431263dca52f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 14:45:47 -0700 Subject: [PATCH 21/26] fix(copilot): mount input tables with display-name CSV headers, not column IDs (#5121) --- .../tools/handlers/function-execute.test.ts | 50 ++++++++++++++++++- .../tools/handlers/function-execute.ts | 29 +++-------- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts index ce56f51254..b47286a03b 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts @@ -89,7 +89,8 @@ describe('executeFunctionExecute table mounts', () => { mockExecuteTool.mockResolvedValue({ success: true }) mockGetTableById.mockResolvedValue(table) mockIsFeatureEnabled.mockResolvedValue(false) - mockQueryRows.mockResolvedValue({ rows: [{ data: { name: 'Ada' } }] }) + // Row data is keyed by stable column id at rest, not display name. + mockQueryRows.mockResolvedValue({ rows: [{ data: { col_name: 'Ada' } }] }) mockHasCloudStorage.mockReturnValue(true) mockGeneratePresignedDownloadUrl.mockResolvedValue('https://s3.example/presigned?sig=abc') }) @@ -104,6 +105,53 @@ describe('executeFunctionExecute table mounts', () => { expect(files[0].content).toBe('name\nAda') }) + it('mounts CSV with display-name headers and id-keyed values, never column ids', async () => { + mockGetTableById.mockResolvedValue({ + id: 'tbl_2', + workspaceId: 'ws_1', + rowCount: 2, + schema: { + columns: [ + { id: 'col_name', name: 'name', type: 'string' }, + { id: 'col_company', name: 'company', type: 'string' }, + ], + }, + }) + mockQueryRows.mockResolvedValue({ + rows: [ + { data: { col_name: 'Ada', col_company: 'Analytical Engine' } }, + { data: { col_name: 'Grace', col_company: 'Navy, Inc' } }, + ], + }) + + await executeFunctionExecute({ inputTables: ['tbl_2'] }, context as never) + + const csv = mountedFiles()[0].content as string + const lines = csv.split('\n') + expect(lines[0]).toBe('name,company') + expect(lines[1]).toBe('Ada,Analytical Engine') + // Value containing a comma is quoted. + expect(lines[2]).toBe('Grace,"Navy, Inc"') + // No stable column id leaks into the mounted file. + expect(csv).not.toContain('col_name') + expect(csv).not.toContain('col_company') + }) + + it('reads values by column id for legacy name-keyed rows too', async () => { + // Legacy column with no id: getColumnId falls back to name, so name-keyed data is correct. + mockGetTableById.mockResolvedValue({ + id: 'tbl_legacy', + workspaceId: 'ws_1', + rowCount: 1, + schema: { columns: [{ name: 'email', type: 'string' }] }, + }) + mockQueryRows.mockResolvedValue({ rows: [{ data: { email: 'a@b.com' } }] }) + + await executeFunctionExecute({ inputTables: ['tbl_legacy'] }, context as never) + + expect(mountedFiles()[0].content).toBe('email\na@b.com') + }) + it('flag ON + cloud storage: mounts by presigned URL, no bytes through web', async () => { mockIsFeatureEnabled.mockImplementation(snapshotCacheOn) mockGetOrCreateTableSnapshot.mockResolvedValue({ diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index a2bfb549b5..2550b63dde 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -3,6 +3,8 @@ import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/ import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { getColumnId } from '@/lib/table/column-keys' +import { formatCsvValue, neutralizeCsvFormula, toCsvRow } from '@/lib/table/export-format' import { queryRows } from '@/lib/table/rows/service' import { getTableById, listTables } from '@/lib/table/service' import { getOrCreateTableSnapshot, SNAPSHOT_MAX_BYTES } from '@/lib/table/snapshot-cache' @@ -80,7 +82,7 @@ async function resolveTableRef( return tablePathLookup?.get(tableName) ?? null } -async function resolveInputFiles( +export async function resolveInputFiles( workspaceId: string, inputFiles?: unknown[], inputTables?: unknown[], @@ -333,28 +335,11 @@ async function resolveInputFiles( const rows = await queryRows(table, {}, 'copilot-fn-exec') - const allKeys = new Set(table.schema.columns.map((column) => column.name)) - for (const row of rows.rows ?? []) { - if (row.data && typeof row.data === 'object') { - for (const key of Object.keys(row.data as Record)) { - allKeys.add(key) - } - } - } - const headers = Array.from(allKeys) - const csvLines = [headers.join(',')] - for (const row of rows.rows ?? []) { - const data = (row.data || {}) as Record + const columns = table.schema.columns + const csvLines = [toCsvRow(columns.map((column) => neutralizeCsvFormula(column.name)))] + for (const row of rows.rows) { csvLines.push( - headers - .map((h) => { - const val = data[h] - const str = val === null || val === undefined ? '' : String(val) - return str.includes(',') || str.includes('"') || str.includes('\n') - ? `"${str.replace(/"/g, '""')}"` - : str - }) - .join(',') + toCsvRow(columns.map((column) => formatCsvValue(row.data[getColumnId(column)]))) ) } const csvContent = csvLines.join('\n') From 7d46103d09f055de582af43d0d9164f15048e252 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 17 Jun 2026 14:56:43 -0700 Subject: [PATCH 22/26] chore(deps): remove unused dependencies and harden CI supply chain (#5119) * chore(deps): remove unused dependencies and harden CI supply chain Dependency cleanup: - Remove unused deps: papaparse, unified, and 6 unused Radix primitives (alert-dialog, radio-group, scroll-area, separator, toggle, visually-hidden) plus @tanstack/react-query-devtools (all verified zero imports repo-wide) - Consolidate jwt-decode into the existing jose dependency (decodeJwt) - Migrate react-window to @tanstack/react-virtual to drop a redundant virtualization library (terminal, structured-output, code viewer) - Remove the better-auth-harmony plugin and its gating env flag Supply-chain hardening: - SHA-pin every GitHub Action to a full commit SHA with a version comment - Pin CI bun-version to 1.3.13 (was "latest" in the release job) - Raise bun minimumReleaseAge cooldown from 3 to 7 days - Add a non-blocking `bun audit` step in CI - Add a CODEOWNERS gate routing dependency-manifest changes to @simstudioai/deps * chore(deps): remove unused apps/docs dependencies (@tabler/icons-react, dotenv-cli) * style(search-modal): use Send icon for Invite teammates action * feat(search-modal): surface New chat as the top action above Create workflow * feat(search-modal): add Secrets to the pages list --- .github/CODEOWNERS | 11 ++ .github/workflows/ci.yml | 46 ++--- .github/workflows/companion-pr-check.yml | 2 +- .github/workflows/docs-embeddings.yml | 8 +- .github/workflows/i18n.yml | 14 +- .github/workflows/images.yml | 24 +-- .github/workflows/migrations.yml | 6 +- .github/workflows/publish-cli.yml | 8 +- .github/workflows/publish-python-sdk.yml | 6 +- .github/workflows/publish-ts-sdk.yml | 10 +- .github/workflows/test-build.yml | 23 ++- apps/docs/package.json | 2 - .../api/auth/oauth/connections/route.test.ts | 10 +- .../app/api/auth/oauth/connections/route.ts | 4 +- .../components/structured-output.tsx | 92 ++++++--- .../components/terminal/terminal.tsx | 95 ++++----- .../components/search-modal/search-modal.tsx | 26 ++- .../components/emcn/components/code/code.tsx | 158 ++++++++------- apps/sim/lib/auth/auth.ts | 6 +- .../tools/server/user/get-credentials.test.ts | 10 +- .../tools/server/user/get-credentials.ts | 4 +- apps/sim/lib/core/config/env-flags.ts | 5 - apps/sim/lib/core/config/env.ts | 1 - apps/sim/lib/messaging/email/validation.ts | 1 - apps/sim/next.config.ts | 1 - apps/sim/package.json | 14 -- bun.lock | 187 +++++++++++------- bunfig.toml | 2 +- helm/sim/values.yaml | 1 - packages/testing/src/mocks/env-flags.mock.ts | 1 - 30 files changed, 419 insertions(+), 359 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e7e67faecf..a1b7bfedb9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,3 +27,14 @@ /apps/sim/app/workspace/*/home/hooks/preview/ @simstudioai/mothership /apps/sim/app/workspace/*/home/hooks/stream/ @simstudioai/mothership /apps/sim/hooks/queries/tasks.ts @simstudioai/mothership + +# Dependency manifests and package-manager config. Any change here — adding, +# removing, or bumping a dependency, or altering install/security settings — +# requires review to guard against supply-chain risk. (CODEOWNERS gates file +# changes, the closest proxy GitHub offers for "new dependency added".) +package.json @simstudioai/deps +**/package.json @simstudioai/deps +bun.lock @simstudioai/deps +**/bun.lock @simstudioai/deps +bunfig.toml @simstudioai/deps +.npmrc @simstudioai/deps diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a019331463..017d5f1476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,26 +90,26 @@ jobs: ecr_repo_secret: ECR_REALTIME steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 with: role-to-assume: ${{ secrets.DEV_AWS_ROLE_TO_ASSUME }} aws-region: ${{ secrets.DEV_AWS_REGION }} - name: Login to Amazon ECR id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 - name: Resolve ECR repo name id: ecr-repo @@ -118,7 +118,7 @@ jobs: ECR_REPO: ${{ matrix.ecr_repo_secret == 'ECR_APP' && secrets.ECR_APP || matrix.ecr_repo_secret == 'ECR_MIGRATIONS' && secrets.ECR_MIGRATIONS || matrix.ecr_repo_secret == 'ECR_REALTIME' && secrets.ECR_REALTIME || '' }} - name: Build and push - uses: useblacksmith/build-push-action@v2 + uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ${{ matrix.dockerfile }} @@ -155,34 +155,34 @@ jobs: ecr_repo_secret: ECR_REALTIME steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 with: role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }} aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }} - name: Login to Amazon ECR id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR if: github.ref == 'refs/heads/main' - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 - name: Resolve ECR repo name id: ecr-repo @@ -222,7 +222,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build and push images - uses: useblacksmith/build-push-action@v2 + uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ${{ matrix.dockerfile }} @@ -254,17 +254,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 - name: Generate ARM64 tags id: meta @@ -282,7 +282,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build and push ARM64 to GHCR - uses: useblacksmith/build-push-action@v2 + uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ${{ matrix.dockerfile }} @@ -309,7 +309,7 @@ jobs: steps: - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -349,10 +349,10 @@ jobs: outputs: docs_changed: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 2 # Need at least 2 commits to detect changes - - uses: dorny/paths-filter@v4 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 id: filter with: filters: | @@ -379,14 +379,14 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: latest + bun-version: 1.3.13 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/companion-pr-check.yml b/.github/workflows/companion-pr-check.yml index f164c82212..9c22877e37 100644 --- a/.github/workflows/companion-pr-check.yml +++ b/.github/workflows/companion-pr-check.yml @@ -31,7 +31,7 @@ jobs: companion: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: CROSS_REPO_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} with: diff --git a/.github/workflows/docs-embeddings.yml b/.github/workflows/docs-embeddings.yml index 13e2febbd3..d8b68d6018 100644 --- a/.github/workflows/docs-embeddings.yml +++ b/.github/workflows/docs-embeddings.yml @@ -15,20 +15,20 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: latest - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index 5f7b1dd001..07fc1a999d 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -14,19 +14,19 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: ref: staging token: ${{ secrets.GH_PAT }} fetch-depth: 0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache @@ -58,7 +58,7 @@ jobs: - name: Create Pull Request with translations if: steps.changes.outputs.changes == 'true' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@4e1beaa7521e8b457b572c090b25bd3db56bf1c5 # v5 with: token: ${{ secrets.GH_PAT }} commit-message: "feat(i18n): update translations" @@ -115,17 +115,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: ref: staging - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index f1ed176d35..54f8a2f47a 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -31,34 +31,34 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 with: role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }} aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }} - name: Login to Amazon ECR id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR if: github.ref == 'refs/heads/main' - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 - name: Generate tags id: meta @@ -90,7 +90,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build and push images - uses: useblacksmith/build-push-action@v2 + uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ${{ matrix.dockerfile }} @@ -117,17 +117,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: useblacksmith/setup-docker-builder@v1 + uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 - name: Generate ARM64 tags id: meta @@ -136,7 +136,7 @@ jobs: echo "tags=${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64" >> $GITHUB_OUTPUT - name: Build and push ARM64 to GHCR - uses: useblacksmith/build-push-action@v2 + uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ${{ matrix.dockerfile }} @@ -160,7 +160,7 @@ jobs: steps: - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index ea5ca45396..f789ec3262 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -28,15 +28,15 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index d7bae3e782..1ca35d80cb 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -14,21 +14,21 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Setup Node.js for npm publishing - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '18' registry-url: 'https://registry.npmjs.org/' - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml index 85d110b53d..9c2abbfe25 100644 --- a/.github/workflows/publish-python-sdk.yml +++ b/.github/workflows/publish-python-sdk.yml @@ -14,10 +14,10 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12' cache: 'pip' @@ -70,7 +70,7 @@ jobs: - name: Create GitHub Release if: steps.version_check.outputs.exists == 'false' - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/publish-ts-sdk.yml b/.github/workflows/publish-ts-sdk.yml index 2a527b7b42..85a3c138b8 100644 --- a/.github/workflows/publish-ts-sdk.yml +++ b/.github/workflows/publish-ts-sdk.yml @@ -14,21 +14,21 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Setup Node.js for npm publishing - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '22' registry-url: 'https://registry.npmjs.org/' - name: Cache Bun dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.bun/install/cache @@ -76,7 +76,7 @@ jobs: - name: Create GitHub Release if: steps.version_check.outputs.exists == 'false' - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index a272f713f9..40f8364514 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -14,38 +14,38 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.13 - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: latest - name: Mount Bun cache (Sticky Disk) - uses: useblacksmith/stickydisk@v1 + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 with: key: ${{ github.repository }}-bun-cache path: ~/.bun/install/cache - name: Mount node_modules (Sticky Disk) - uses: useblacksmith/stickydisk@v1 + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 with: key: ${{ github.repository }}-node-modules path: ./node_modules - name: Mount Turbo cache (Sticky Disk) - uses: useblacksmith/stickydisk@v1 + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 with: key: ${{ github.repository }}-turbo-cache path: ./.turbo - name: Restore Next.js build cache - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ./apps/sim/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }} @@ -55,6 +55,13 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + # Surfaces known CVEs in the dependency tree. Non-blocking until the + # existing advisory backlog is triaged, then flip to a required gate by + # removing continue-on-error. + - name: Security audit + run: bun audit + continue-on-error: true + - name: Validate env flags run: | FILE="apps/sim/lib/core/config/env-flags.ts" @@ -167,7 +174,7 @@ jobs: run: bunx turbo run build --filter=sim - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5 with: directory: ./apps/sim/coverage fail_ci_if_error: false diff --git a/apps/docs/package.json b/apps/docs/package.json index 5dafcf4638..3938b9ad1f 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@sim/db": "workspace:*", - "@tabler/icons-react": "^3.31.0", "@vercel/og": "^0.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -43,7 +42,6 @@ "@types/node": "^22.14.1", "@types/react": "^19.1.2", "@types/react-dom": "^19.0.4", - "dotenv-cli": "^8.0.0", "postcss": "^8.5.3", "tailwindcss": "^4.0.12", "typescript": "^5.8.2" diff --git a/apps/sim/app/api/auth/oauth/connections/route.test.ts b/apps/sim/app/api/auth/oauth/connections/route.test.ts index 92046b9efc..e5e5d74271 100644 --- a/apps/sim/app/api/auth/oauth/connections/route.test.ts +++ b/apps/sim/app/api/auth/oauth/connections/route.test.ts @@ -12,9 +12,9 @@ import { } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockParseProvider, mockJwtDecode, mockEq } = vi.hoisted(() => ({ +const { mockParseProvider, mockDecodeJwt, mockEq } = vi.hoisted(() => ({ mockParseProvider: vi.fn(), - mockJwtDecode: vi.fn(), + mockDecodeJwt: vi.fn(), mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), })) @@ -29,8 +29,8 @@ vi.mock('drizzle-orm', () => ({ eq: mockEq, })) -vi.mock('jwt-decode', () => ({ - jwtDecode: mockJwtDecode, +vi.mock('jose', () => ({ + decodeJwt: mockDecodeJwt, })) vi.mock('@/lib/oauth/utils', () => ({ @@ -161,7 +161,7 @@ describe('OAuth Connections API Route', () => { }, ] - mockJwtDecode.mockReturnValueOnce({ + mockDecodeJwt.mockReturnValueOnce({ email: 'decoded@example.com', name: 'Decoded User', }) diff --git a/apps/sim/app/api/auth/oauth/connections/route.ts b/apps/sim/app/api/auth/oauth/connections/route.ts index 11a98dfbfe..9af427f9c1 100644 --- a/apps/sim/app/api/auth/oauth/connections/route.ts +++ b/apps/sim/app/api/auth/oauth/connections/route.ts @@ -1,7 +1,7 @@ import { account, db, user } from '@sim/db' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { jwtDecode } from 'jwt-decode' +import { decodeJwt } from 'jose' import { type NextRequest, NextResponse } from 'next/server' import type { OAuthConnection } from '@/lib/api/contracts/oauth-connections' import { getSession } from '@/lib/auth' @@ -60,7 +60,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { // Method 1: Try to extract email from ID token (works for Google, etc.) if (acc.idToken) { try { - const decoded = jwtDecode(acc.idToken) + const decoded = decodeJwt(acc.idToken) if (decoded.email) { displayName = decoded.email } else if (decoded.name) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output.tsx index 2607270ab8..a8c5c6f25a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output.tsx @@ -11,7 +11,7 @@ import { useRef, useState, } from 'react' -import { List, type RowComponentProps, useListRef } from 'react-window' +import { useVirtualizer } from '@tanstack/react-virtual' import { Badge, ChevronDown } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { isUserFileDisplayMetadata } from '@/lib/core/utils/user-file' @@ -634,7 +634,8 @@ function countVisibleRows(data: unknown, expandedPaths: Set, isError: bo } interface VirtualizedRowProps { - rows: FlatRow[] + row: FlatRow + index: number onToggle: (path: string) => void wrapText: boolean searchQuery: string @@ -644,16 +645,21 @@ interface VirtualizedRowProps { /** * Virtualized row component for large data sets. */ -function VirtualizedRow({ index, style, ...props }: RowComponentProps) { - const { rows, onToggle, wrapText, searchQuery, currentMatchIndex } = props - const row = rows[index] +function VirtualizedRow({ + row, + index, + onToggle, + wrapText, + searchQuery, + currentMatchIndex, +}: VirtualizedRowProps) { const paddingLeft = CONFIG.BASE_PADDING + row.depth * CONFIG.INDENT_PER_LEVEL if (row.type === 'header') { const badgeVariant = row.isError ? 'red' : BADGE_VARIANTS[row.valueType] return ( -
+
onToggle(row.path)} @@ -684,14 +690,14 @@ function VirtualizedRow({ index, style, ...props }: RowComponentProps +
{row.displayText}
) } return ( -
+
(null) const prevIsErrorRef = useRef(isError) const internalRef = useRef(null) - const listRef = useListRef(null) + const scrollRef = useRef(null) const [containerHeight, setContainerHeight] = useState(400) const setContainerRef = useCallback( @@ -864,18 +870,25 @@ export const StructuredOutput = memo(function StructuredOutput({ return flattenTree(data, expandedPaths, pathToMatchIndices, isError) }, [data, expandedPaths, pathToMatchIndices, isError, useVirtualization]) + const virtualizer = useVirtualizer({ + count: flatRows.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => CONFIG.ROW_HEIGHT, + overscan: CONFIG.OVERSCAN_COUNT, + }) + // Scroll to match (virtualized) useEffect(() => { - if (!useVirtualization || allMatchPaths.length === 0 || !listRef.current) return + if (!useVirtualization || allMatchPaths.length === 0 || !scrollRef.current) return const currentPath = allMatchPaths[currentMatchIndex] const targetPath = currentPath.endsWith('.value') ? currentPath : `${currentPath}.value` const rowIndex = flatRows.findIndex((r) => r.path === targetPath || r.path === currentPath) if (rowIndex !== -1) { - listRef.current.scrollToRow({ index: rowIndex, align: 'center' }) + virtualizer.scrollToIndex(rowIndex, { align: 'center' }) } - }, [currentMatchIndex, allMatchPaths, flatRows, listRef, useVirtualization]) + }, [currentMatchIndex, allMatchPaths, flatRows, virtualizer, useVirtualization]) // Scroll to match (non-virtualized) useEffect(() => { @@ -891,9 +904,20 @@ export const StructuredOutput = memo(function StructuredOutput({ return () => cancelAnimationFrame(rafId) }, [currentMatchIndex, allMatchPaths.length, expandedPaths, useVirtualization]) + const setVirtualizedScrollRef = useCallback( + (node: HTMLDivElement | null) => { + scrollRef.current = node + setContainerRef(node) + }, + [setContainerRef] + ) + const containerClass = cn('flex flex-col pl-5', wrapText && 'overflow-x-hidden', className) - const virtualizedContainerClass = cn('relative', wrapText && 'overflow-x-hidden', className) - const listClass = wrapText ? 'overflow-x-hidden' : 'overflow-x-auto' + const virtualizedContainerClass = cn( + 'overflow-y-auto', + wrapText ? 'overflow-x-hidden' : 'overflow-x-auto', + className + ) // Running state if (isRunning && data === undefined) { @@ -922,26 +946,32 @@ export const StructuredOutput = memo(function StructuredOutput({ if (useVirtualization) { return (
- +
+ {virtualizer.getVirtualItems().map((virtualItem) => ( +
+ +
+ ))} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index e729d43040..e5381ff35f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -3,10 +3,10 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { formatDuration } from '@sim/utils/formatting' +import { useVirtualizer } from '@tanstack/react-virtual' import clsx from 'clsx' import { ArrowDown, ArrowUp, Database, MoreHorizontal, Palette, Pause, Trash2 } from 'lucide-react' import Link from 'next/link' -import { List, type RowComponentProps, useListRef } from 'react-window' import { Button, ChevronDown, @@ -549,7 +549,7 @@ const EntryNodeRow = memo(function EntryNodeRow({ }) interface TerminalLogListRowProps { - rows: VisibleTerminalRow[] + row: VisibleTerminalRow selectedEntryId: string | null onSelectEntry: (entry: ConsoleEntry) => void expandedNodes: Set @@ -557,23 +557,22 @@ interface TerminalLogListRowProps { } function TerminalLogListRow({ - index, - style, - ...props -}: RowComponentProps) { - const { rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode } = props - const row = rows[index] - + row, + selectedEntryId, + onSelectEntry, + expandedNodes, + onToggleNode, +}: TerminalLogListRowProps) { if (row.rowType === 'separator') { return ( -
+
) } return ( -
+
onToggleNode: (nodeId: string) => void }) { - const containerRef = useRef(null) - const listRef = useListRef(null) - const [listHeight, setListHeight] = useState(400) + const scrollRef = useRef(null) const rows = useMemo( () => flattenVisibleExecutionRows(executionGroups, expandedNodes), [executionGroups, expandedNodes] ) - useEffect(() => { - const container = containerRef.current - if (!container) return - - const updateHeight = () => { - if (container.clientHeight > 0) { - setListHeight(container.clientHeight) - } - } - - updateHeight() - const resizeObserver = new ResizeObserver(updateHeight) - resizeObserver.observe(container) - return () => resizeObserver.disconnect() - }, []) + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => TERMINAL_CONFIG.LOG_ROW_HEIGHT_PX, + overscan: 8, + }) const rowsRef = useRef(rows) rowsRef.current = rows @@ -638,32 +626,35 @@ const TerminalLogsPane = memo(function TerminalLogsPane({ ) if (rowIndex !== -1) { - listRef.current?.scrollToRow({ index: rowIndex, align: 'smart' }) + virtualizer.scrollToIndex(rowIndex, { align: 'auto' }) } - }, [selectedEntryId, listRef]) - - const rowProps = useMemo( - () => ({ - rows, - selectedEntryId, - onSelectEntry, - expandedNodes, - onToggleNode, - }), - [rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode] - ) + }, [selectedEntryId, virtualizer]) + + const virtualItems = virtualizer.getVirtualItems() return ( -
- +
+
+ {virtualItems.map((virtualItem) => ( +
+ +
+ ))} +
) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 96fe6c5ad3..7466c52ece 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -17,12 +17,13 @@ import { HelpCircle, Home, Integration, + Key, Play, Plus, + Send, Settings, Table, Upload, - User, } from '@/components/emcn/icons' import { Search } from '@/components/emcn/icons/search' import { cn } from '@/lib/core/utils/cn' @@ -125,12 +126,6 @@ export function SearchModal({ const pages = useMemo( (): PageItem[] => [ - { - id: 'home', - name: 'New chat', - icon: Home, - href: `/workspace/${workspaceId}/home`, - }, { id: 'integrations', name: 'Integrations', @@ -172,6 +167,12 @@ export function SearchModal({ href: `/workspace/${workspaceId}/logs`, shortcut: '⌘⇧L', }, + { + id: 'secrets', + name: 'Secrets', + icon: Key, + href: `/workspace/${workspaceId}/settings/secrets`, + }, { id: 'help', name: 'Help', @@ -212,6 +213,14 @@ export function SearchModal({ context: 'workflow', run: () => invokeCommand('run-workflow'), }) + list.push({ + id: 'new-chat', + name: 'New chat', + keywords: 'chat message ask sim assistant home', + icon: Home, + context: 'global', + run: () => routerRef.current.push(`/workspace/${workspaceId}/home`), + }) if (canEdit && onCreateWorkflow) { list.push({ id: 'create-workflow', @@ -267,12 +276,13 @@ export function SearchModal({ id: 'invite-teammates', name: 'Invite teammates', keywords: 'members people add user organization', - icon: User, + icon: Send, context: 'global', run: () => navigateToSettings({ section: 'teammates' }), }) return list }, [ + workspaceId, canEdit, onCreateWorkflow, onCreateFolder, diff --git a/apps/sim/components/emcn/components/code/code.tsx b/apps/sim/components/emcn/components/code/code.tsx index c9abf751fa..5f1c219036 100644 --- a/apps/sim/components/emcn/components/code/code.tsx +++ b/apps/sim/components/emcn/components/code/code.tsx @@ -10,9 +10,9 @@ import { useRef, useState, } from 'react' +import { useVirtualizer } from '@tanstack/react-virtual' import { ChevronRight } from 'lucide-react' import { highlight, languages } from 'prismjs' -import { List, type RowComponentProps, useDynamicRowHeight, useListRef } from 'react-window' import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-python' import 'prismjs/components/prism-json' @@ -583,8 +583,10 @@ interface HighlightedLine { * Props for virtualized row rendering. */ interface CodeRowProps { - /** Array of highlighted lines to render */ - lines: HighlightedLine[] + /** Index of this row within the virtualized window */ + index: number + /** The highlighted line to render */ + line: HighlightedLine /** Width of the gutter in pixels */ gutterWidth: number /** Whether to show the line number gutter */ @@ -609,26 +611,25 @@ interface CodeRowProps { * Row component for virtualized code viewer. * Renders a single line with optional gutter and collapse button. */ -function CodeRow({ index, style, ...props }: RowComponentProps) { - const { - lines, - gutterWidth, - showGutter, - gutterStyle, - leftOffset, - wrapText, - showCollapseColumn, - collapsibleLines, - collapsedLines, - onToggleCollapse, - } = props - const line = lines[index] +function CodeRow({ + index, + line, + gutterWidth, + showGutter, + gutterStyle, + leftOffset, + wrapText, + showCollapseColumn, + collapsibleLines, + collapsedLines, + onToggleCollapse, +}: CodeRowProps) { const originalLineIndex = line.lineNumber - 1 const isCollapsible = showCollapseColumn && collapsibleLines.has(originalLineIndex) const isCollapsed = collapsedLines.has(originalLineIndex) return ( -
+
{showGutter && (
void /** Ref for the content container (for scrolling to matches) */ contentRef?: React.RefObject - /** Enable virtualized rendering for large outputs (uses react-window) */ + /** Enable virtualized rendering for large outputs (uses @tanstack/react-virtual) */ virtualized?: boolean /** Whether to show a collapse column for JSON folding (only for json language) */ showCollapseColumn?: boolean @@ -823,7 +824,7 @@ type ViewerInnerProps = { } /** - * Virtualized code viewer implementation using react-window. + * Virtualized code viewer implementation using @tanstack/react-virtual. * Optimized for large outputs with efficient scrolling and dynamic row heights. */ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ @@ -841,14 +842,9 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ showCollapseColumn, }: ViewerInnerProps) { const containerRef = useRef(null) - const listRef = useListRef(null) + const scrollRef = useRef(null) const [containerHeight, setContainerHeight] = useState(400) - const dynamicRowHeight = useDynamicRowHeight({ - defaultRowHeight: CODE_LINE_HEIGHT_PX, - key: wrapText ? 'wrap' : 'nowrap', - }) - const lines = useMemo(() => code.split('\n'), [code]) const gutterWidth = useMemo(() => calculateGutterWidth(lines.length), [lines.length]) @@ -914,8 +910,26 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ }) }, [displayLines, language, visibleLineIndices, searchQuery, currentMatchIndex, matchOffsets]) + const hasCollapsibleContent = collapsibleLines.size > 0 + const effectiveShowCollapseColumn = showCollapseColumn && hasCollapsibleContent + + const virtualizer = useVirtualizer({ + count: visibleLines.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => CODE_LINE_HEIGHT_PX, + overscan: 5, + }) + + /** + * Re-measure rows when wrap mode or content changes so dynamic (wrapped) row + * heights stay accurate. Fixed-height (`nowrap`) rows do not need re-measuring. + */ useEffect(() => { - if (!searchQuery?.trim() || matchCount === 0 || !listRef.current) return + virtualizer.measure() + }, [wrapText, visibleLines, virtualizer]) + + useEffect(() => { + if (!searchQuery?.trim() || matchCount === 0 || !scrollRef.current) return let accumulated = 0 for (let i = 0; i < matchOffsets.length; i++) { @@ -923,13 +937,13 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ if (currentMatchIndex >= accumulated && currentMatchIndex < accumulated + matchesInThisLine) { const visibleIndex = visibleLineIndices.indexOf(i) if (visibleIndex !== -1) { - listRef.current.scrollToRow({ index: visibleIndex, align: 'center' }) + virtualizer.scrollToIndex(visibleIndex, { align: 'center' }) } break } accumulated += matchesInThisLine } - }, [currentMatchIndex, searchQuery, matchCount, matchOffsets, listRef, visibleLineIndices]) + }, [currentMatchIndex, searchQuery, matchCount, matchOffsets, virtualizer, visibleLineIndices]) useEffect(() => { const container = containerRef.current @@ -946,21 +960,10 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ return () => resizeObserver.disconnect() }, []) - useEffect(() => { - if (!wrapText) return - - const container = containerRef.current - if (!container) return - - const rows = container.querySelectorAll('[data-row-index]') - if (rows.length === 0) return - - return dynamicRowHeight.observeRowElements(rows) - }, [wrapText, dynamicRowHeight, visibleLines]) - const setRefs = useCallback( (el: HTMLDivElement | null) => { containerRef.current = el + scrollRef.current = el if (contentRef && 'current' in contentRef) { contentRef.current = el } @@ -968,57 +971,50 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({ [contentRef] ) - const hasCollapsibleContent = collapsibleLines.size > 0 - const effectiveShowCollapseColumn = showCollapseColumn && hasCollapsibleContent - - const rowProps = useMemo( - () => ({ - lines: visibleLines, - gutterWidth, - showGutter, - gutterStyle, - leftOffset: paddingLeft, - wrapText, - showCollapseColumn: effectiveShowCollapseColumn, - collapsibleLines, - collapsedLines, - onToggleCollapse: toggleCollapse, - }), - [ - visibleLines, - gutterWidth, - showGutter, - gutterStyle, - paddingLeft, - wrapText, - effectiveShowCollapseColumn, - collapsibleLines, - collapsedLines, - toggleCollapse, - ] - ) - return (
- +
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const line = visibleLines[virtualItem.index] + return ( +
+ +
+ ) + })} +
) }) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 9b0f4523a4..df2c01f5de 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -20,7 +20,6 @@ import { oneTimeToken, organization, } from 'better-auth/plugins' -import { emailHarmony } from 'better-auth-harmony' import { and, count, eq, inArray, sql } from 'drizzle-orm' import { headers } from 'next/headers' import Stripe from 'stripe' @@ -73,7 +72,6 @@ import { isMicrosoftAuthDisabled, isOrganizationsEnabled, isRegistrationDisabled, - isSignupEmailValidationEnabled, isSignupMxValidationEnabled, isSsoEnabled, } from '@/lib/core/config/env-flags' @@ -812,8 +810,7 @@ export const auth = betterAuth({ * the exact same set of returned fields a real freshly-created user would, otherwise * the differing response shape re-opens the enumeration oracle. The admin plugin * (always loaded) adds role/banned/banReason/banExpires, and the Stripe plugin — loaded - * only when billing is enabled — adds stripeCustomerId (null on a new user). The - * harmony plugin's normalizedEmail is `returned: false`, so it is intentionally omitted. + * only when billing is enabled — adds stripeCustomerId (null on a new user). */ customSyntheticUser: ({ coreFields, @@ -946,7 +943,6 @@ export const auth = betterAuth({ }), }, plugins: [ - ...(isSignupEmailValidationEnabled ? [emailHarmony()] : []), ...(env.TURNSTILE_SECRET_KEY ? [ captcha({ diff --git a/apps/sim/lib/copilot/tools/server/user/get-credentials.test.ts b/apps/sim/lib/copilot/tools/server/user/get-credentials.test.ts index 6b8cdd130e..07c0bfc2c3 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-credentials.test.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-credentials.test.ts @@ -9,12 +9,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const SECRET_ACCESS_TOKEN = 'ya29.a0SECRET_GOOGLE_BEARER_TOKEN_DO_NOT_LEAK' -const { selectMock, getAllOAuthServicesMock, getPersonalAndWorkspaceEnvMock, jwtDecodeMock } = +const { selectMock, getAllOAuthServicesMock, getPersonalAndWorkspaceEnvMock, decodeJwtMock } = vi.hoisted(() => ({ selectMock: vi.fn(), getAllOAuthServicesMock: vi.fn(), getPersonalAndWorkspaceEnvMock: vi.fn(), - jwtDecodeMock: vi.fn(), + decodeJwtMock: vi.fn(), })) vi.mock('@sim/db', () => ({ @@ -29,8 +29,8 @@ vi.mock('@/lib/environment/utils', () => ({ getPersonalAndWorkspaceEnv: getPersonalAndWorkspaceEnvMock, })) -vi.mock('jwt-decode', () => ({ - jwtDecode: jwtDecodeMock, +vi.mock('jose', () => ({ + decodeJwt: decodeJwtMock, })) import { getCredentialsServerTool } from './get-credentials' @@ -89,7 +89,7 @@ describe('getCredentialsServerTool', () => { conflicts: [], }) - jwtDecodeMock.mockReturnValue({ email: 'brent@cellular.so' }) + decodeJwtMock.mockReturnValue({ email: 'brent@cellular.so' }) }) it('never returns access tokens for connected OAuth credentials', async () => { diff --git a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts index c8088ee71a..247455e923 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-credentials.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-credentials.ts @@ -3,7 +3,7 @@ import { account, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { eq } from 'drizzle-orm' -import { jwtDecode } from 'jwt-decode' +import { decodeJwt } from 'jose' import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' @@ -84,7 +84,7 @@ export const getCredentialsServerTool: BaseServerTool let displayName = '' if (acc.idToken) { try { - const decoded = jwtDecode<{ email?: string; name?: string }>(acc.idToken) + const decoded = decodeJwt<{ email?: string; name?: string }>(acc.idToken) displayName = decoded.email || decoded.name || '' } catch (error) { logger.warn('Failed to decode JWT id token', { diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts index 8fcdb84155..e980a45242 100644 --- a/apps/sim/lib/core/config/env-flags.ts +++ b/apps/sim/lib/core/config/env-flags.ts @@ -84,11 +84,6 @@ export const isRegistrationDisabled = isTruthy(env.DISABLE_REGISTRATION) */ export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED) -/** - * Is signup email validation enabled (disposable email blocking via better-auth-harmony) - */ -export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED) - /** * Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam * mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 1ea9e3e105..5888e8d537 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -33,7 +33,6 @@ export const env = createEnv({ BLOCKED_EMAIL_MX_HOSTS: z.string().optional(), // Comma-separated MX-host substrings blocked from signing up; matched against the domain's resolved MX backend to catch throwaway domains that share a mail backend. No defaults — operators supply their own list. Only used when SIGNUP_MX_VALIDATION_ENABLED is set. TRUSTED_ORIGINS: z.string().optional(), // Comma-separated additional origins to trust for auth (e.g., "https://app.example.com,https://www.example.com"). Merged into Better Auth trustedOrigins. TURNSTILE_SECRET_KEY: z.string().min(1).optional(), // Cloudflare Turnstile secret key for captcha verification - SIGNUP_EMAIL_VALIDATION_ENABLED: z.boolean().optional(), // Enable disposable email blocking via better-auth-harmony (55K+ domains) ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data API_ENCRYPTION_KEY: z.string().min(32).optional(), // Dedicated key for encrypting API keys (optional for OSS) INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication diff --git a/apps/sim/lib/messaging/email/validation.ts b/apps/sim/lib/messaging/email/validation.ts index 8731f5f744..fc49ae95d1 100644 --- a/apps/sim/lib/messaging/email/validation.ts +++ b/apps/sim/lib/messaging/email/validation.ts @@ -69,7 +69,6 @@ function hasInvalidPatterns(email: string): boolean { /** * Quick email validation for client-side form feedback. - * Server-side disposable blocking is handled by better-auth-harmony (55K+ domains). */ export function quickValidateEmail(email: string): EmailValidationResult { const checks = { diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index eb565fe020..892fa4d391 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -135,7 +135,6 @@ const nextConfig: NextConfig = { '@t3-oss/env-nextjs', '@t3-oss/env-core', '@sim/db', - 'better-auth-harmony', ], async headers() { return [ diff --git a/apps/sim/package.json b/apps/sim/package.json index 9d8cd3f42d..be49eca24d 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -78,7 +78,6 @@ "@opentelemetry/sdk-trace-base": "^2.7.0", "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.32.0", - "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3", @@ -87,16 +86,11 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-radio-group": "^1.3.3", - "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-visually-hidden": "1.2.4", "@react-email/components": "0.5.7", "@react-email/render": "2.0.8", "@sim/audit": "workspace:*", @@ -109,12 +103,10 @@ "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", "@tanstack/react-query": "5.90.8", - "@tanstack/react-query-devtools": "5.90.2", "@tanstack/react-virtual": "3.13.24", "@trigger.dev/sdk": "4.4.3", "ajv": "8.18.0", "better-auth": "1.6.11", - "better-auth-harmony": "1.3.1", "binary-extensions": "3.1.0", "browser-image-compression": "^2.0.2", "busboy": "1.6.0", @@ -151,7 +143,6 @@ "js-yaml": "4.2.0", "json5": "2.2.3", "jszip": "3.10.1", - "jwt-decode": "^4.0.0", "lru-cache": "11.3.6", "lucide-react": "^0.479.0", "mammoth": "^1.9.0", @@ -168,7 +159,6 @@ "nodemailer": "8.0.9", "officeparser": "^5.2.0", "openai": "^4.91.1", - "papaparse": "5.5.3", "pdf-lib": "1.17.1", "pdfjs-dist": "5.4.296", "postgres": "^3.4.5", @@ -181,7 +171,6 @@ "react-hook-form": "^7.54.2", "react-pdf": "10.4.1", "react-simple-code-editor": "^0.14.1", - "react-window": "2.2.3", "reactflow": "^11.11.4", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", @@ -202,7 +191,6 @@ "tldts": "7.0.30", "twilio": "5.9.0", "undici": "7.25.0", - "unified": "11.0.5", "unpdf": "1.4.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "4.3.6", @@ -222,11 +210,9 @@ "@types/micromatch": "4.0.10", "@types/node": "24.2.1", "@types/nodemailer": "7.0.4", - "@types/papaparse": "5.3.16", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-window": "2.0.0", "@types/ssh2": "^1.15.5", "@types/three": "0.177.0", "@vitejs/plugin-react": "^4.3.4", diff --git a/bun.lock b/bun.lock index 722893ecaa..ad58c9cb04 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -22,7 +23,6 @@ "version": "0.0.0", "dependencies": { "@sim/db": "workspace:*", - "@tabler/icons-react": "^3.31.0", "@vercel/og": "^0.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -49,7 +49,6 @@ "@types/node": "^22.14.1", "@types/react": "^19.1.2", "@types/react-dom": "^19.0.4", - "dotenv-cli": "^8.0.0", "postcss": "^8.5.3", "tailwindcss": "^4.0.12", "typescript": "^5.8.2", @@ -135,7 +134,6 @@ "@opentelemetry/sdk-trace-base": "^2.7.0", "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.32.0", - "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3", @@ -144,16 +142,11 @@ "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-progress": "^1.1.2", - "@radix-ui/react-radio-group": "^1.3.3", - "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-visually-hidden": "1.2.4", "@react-email/components": "0.5.7", "@react-email/render": "2.0.8", "@sim/audit": "workspace:*", @@ -166,12 +159,10 @@ "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", "@tanstack/react-query": "5.90.8", - "@tanstack/react-query-devtools": "5.90.2", "@tanstack/react-virtual": "3.13.24", "@trigger.dev/sdk": "4.4.3", "ajv": "8.18.0", "better-auth": "1.6.11", - "better-auth-harmony": "1.3.1", "binary-extensions": "3.1.0", "browser-image-compression": "^2.0.2", "busboy": "1.6.0", @@ -208,7 +199,6 @@ "js-yaml": "4.2.0", "json5": "2.2.3", "jszip": "3.10.1", - "jwt-decode": "^4.0.0", "lru-cache": "11.3.6", "lucide-react": "^0.479.0", "mammoth": "^1.9.0", @@ -225,7 +215,6 @@ "nodemailer": "8.0.9", "officeparser": "^5.2.0", "openai": "^4.91.1", - "papaparse": "5.5.3", "pdf-lib": "1.17.1", "pdfjs-dist": "5.4.296", "postgres": "^3.4.5", @@ -238,7 +227,6 @@ "react-hook-form": "^7.54.2", "react-pdf": "10.4.1", "react-simple-code-editor": "^0.14.1", - "react-window": "2.2.3", "reactflow": "^11.11.4", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", @@ -259,7 +247,6 @@ "tldts": "7.0.30", "twilio": "5.9.0", "undici": "7.25.0", - "unified": "11.0.5", "unpdf": "1.4.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "4.3.6", @@ -279,11 +266,9 @@ "@types/micromatch": "4.0.10", "@types/node": "24.2.1", "@types/nodemailer": "7.0.4", - "@types/papaparse": "5.3.16", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-window": "2.0.0", "@types/ssh2": "^1.15.5", "@types/three": "0.177.0", "@vitejs/plugin-react": "^4.3.4", @@ -1213,8 +1198,6 @@ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collapsible": "1.1.14", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dialog": "1.1.17", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], @@ -1227,7 +1210,7 @@ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw=="], @@ -1257,20 +1240,16 @@ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw=="], "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.12", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.3.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.1", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.10", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g=="], - "@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.1", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.10", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], @@ -1279,8 +1258,6 @@ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-roving-focus": "1.1.13", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], @@ -1299,7 +1276,7 @@ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], @@ -1549,10 +1526,6 @@ "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.4", "", { "dependencies": { "@t3-oss/env-core": "0.13.4" }, "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-6ecXR7SH7zJKVcBODIkB7wV9QLMU23uV8D9ec6P+ULHJ5Ea/YXEHo+Z/2hSYip5i9ptD/qZh8VuOXyldspvTTg=="], - "@tabler/icons": ["@tabler/icons@3.44.0", "", {}, "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA=="], - - "@tabler/icons-react": ["@tabler/icons-react@3.44.0", "", { "dependencies": { "@tabler/icons": "3.44.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="], @@ -1587,12 +1560,8 @@ "@tanstack/query-core": ["@tanstack/query-core@5.90.8", "", {}, "sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.8", "", { "dependencies": { "@tanstack/query-core": "5.90.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], @@ -1743,16 +1712,12 @@ "@types/nodemailer": ["@types/nodemailer@7.0.4", "", { "dependencies": { "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow=="], - "@types/papaparse": ["@types/papaparse@5.3.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg=="], - "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/react-window": ["@types/react-window@2.0.0", "", { "dependencies": { "react-window": "*" } }, "sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ=="], - "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], @@ -1893,8 +1858,6 @@ "better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="], - "better-auth-harmony": ["better-auth-harmony@1.3.1", "", { "dependencies": { "libphonenumber-js": "^1.12.37", "mailchecker": "^6.0.19", "validator": "^13.15.26" }, "peerDependencies": { "better-auth": "^1.0.3" } }, "sha512-CiChZMBxuq35YqwyA2pcuL7KfAdrxa+VGLShL+yortprC5E04kttV0XsdsaTIej+d0MxFKIcq7PPaInaEyV3DQ=="], - "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -2235,10 +2198,6 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "dotenv-cli": ["dotenv-cli@8.0.0", "", { "dependencies": { "cross-spawn": "^7.0.6", "dotenv": "^16.3.0", "dotenv-expand": "^10.0.0", "minimist": "^1.2.6" }, "bin": { "dotenv": "cli.js" } }, "sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw=="], - - "dotenv-expand": ["dotenv-expand@10.0.0", "", {}, "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A=="], - "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], @@ -2717,8 +2676,6 @@ "libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="], - "libphonenumber-js": ["libphonenumber-js@1.13.6", "", {}, "sha512-NdB6O6QvlGMCoG003m0YIKG2+Xw7DjmCZhmc1RH+K6HncADUbRf8TZeLegxBBN1VFyPHcNpPTKpIhYLXzJVy1Q=="], - "libqp": ["libqp@2.1.1", "", {}, "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], @@ -2801,8 +2758,6 @@ "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "mailchecker": ["mailchecker@6.0.20", "", {}, "sha512-mZ3kmtfXzGj06prtNm6d8an7D++Kf1G4jEkPZ1QQyhknYNLkmGoMtfaNPNHJU6E8J+Bm3AcZlIIfq5D6L4MS2g=="], - "make-cancellable-promise": ["make-cancellable-promise@2.0.0", "", {}, "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw=="], "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], @@ -3101,8 +3056,6 @@ "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="], - "parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="], "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], @@ -3271,8 +3224,6 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "react-window": ["react-window@2.2.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ=="], - "reactflow": ["reactflow@11.11.4", "", { "dependencies": { "@reactflow/background": "11.3.14", "@reactflow/controls": "11.2.14", "@reactflow/core": "11.11.4", "@reactflow/minimap": "11.7.14", "@reactflow/node-resizer": "2.2.14", "@reactflow/node-toolbar": "1.3.14" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og=="], "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], @@ -3707,8 +3658,6 @@ "uzip": ["uzip@0.20201231.0", "", {}, "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="], - "validator": ["validator@13.15.35", "", {}, "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -3929,62 +3878,132 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], - "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], - "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], "@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-navigation-menu/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-navigation-menu/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], + "@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-scroll-area/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-scroll-area/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@radix-ui/react-select/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], "@radix-ui/react-select/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ=="], + "@radix-ui/react-slider/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], "@radix-ui/react-slider/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], "@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + + "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], + "@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], @@ -3993,7 +4012,7 @@ "@radix-ui/react-use-size/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="], "@react-email/components/@react-email/render": ["@react-email/render@1.4.0", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ZtJ3noggIvW1ZAryoui95KJENKdCzLmN5F7hyZY1F/17B1vwzuxHB7YkuCg0QqHjDivc5axqYEYdIOw4JIQdUw=="], @@ -4099,8 +4118,6 @@ "@types/nodemailer/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "@types/papaparse/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@types/through/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], @@ -4473,9 +4490,43 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-label/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], "@sim/realtime/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], @@ -4527,8 +4578,6 @@ "@types/nodemailer/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "@types/papaparse/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@types/through/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -4853,10 +4902,6 @@ "@grpc/proto-loader/protobufjs/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], "@trigger.dev/core/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], diff --git a/bunfig.toml b/bunfig.toml index 6f0019a2f8..b74c34f680 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,6 +1,6 @@ [install] exact = true -minimumReleaseAge = 259200 +minimumReleaseAge = 604800 [run] env = { NEXT_PUBLIC_APP_URL = "http://localhost:3000" } diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 4898f925fe..f54dc2b273 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -177,7 +177,6 @@ app: DISABLE_REGISTRATION: "" # Set to "true" to disable new user signups EMAIL_PASSWORD_SIGNUP_ENABLED: "" # Set to "false" to disable email/password login (SSO-only mode, server-side enforcement) NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: "" # Set to "false" to hide email/password login form (UI-side) - SIGNUP_EMAIL_VALIDATION_ENABLED: "" # Set to "true" to block 55K+ disposable email domains (requires normalized_email migration) # Bot Protection (Cloudflare Turnstile) TURNSTILE_SECRET_KEY: "" # Cloudflare Turnstile secret key (leave empty to disable captcha) diff --git a/packages/testing/src/mocks/env-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts index 02be7bfbbf..6165c236fa 100644 --- a/packages/testing/src/mocks/env-flags.mock.ts +++ b/packages/testing/src/mocks/env-flags.mock.ts @@ -19,7 +19,6 @@ export const envFlagsMock = { isAuthDisabled: false, isRegistrationDisabled: false, isEmailPasswordEnabled: false, - isSignupEmailValidationEnabled: false, isTriggerDevEnabled: false, isTablesFractionalOrderingEnabled: false, isSsoEnabled: false, From a028d07e7b49e66909b3aeed26c78b3be3182b0a Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 16:34:39 -0700 Subject: [PATCH 23/26] =?UTF-8?q?improvement(mothership):=20user=5Ftable?= =?UTF-8?q?=20speed=20parity=20=E2=80=94=20limit=20bounds,=20background=20?= =?UTF-8?q?import/delete/update=20jobs=20(#5012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(mothership): user_table speed parity — limit bounds, async import/delete/update jobs - query_rows / filter ops clamp limit to the contract maxes; query_rows skips execution metadata. - import_file / create_from_file (large CSV/TSV) and delete_rows_by_filter (>1000 unbounded matches) dispatch background table jobs, claiming the per-table job slot; inline paths claim the slot too. - update_rows_by_filter now escalates the same way: >1000 unbounded matches run as a background table job (new 'update' job type + runTableUpdate worker + tableUpdateTask), so a broad update on a huge table no longer loads every row into the request. Best-effort/non-atomic and skips workflow recompute (documented); unique-column patches stay inline. Pagination is limit/offset. Co-Authored-By: Claude Fable 5 * docs(mothership): trim user_table catalog copy to the essentials Drop the verbose doomedCount/affectedCount, delete-mask, workflow-recompute, and unique-column asides from the bulk-op descriptions. The model only needs: large ops return { jobId }, limit maxes at 1000, one job per table. Co-Authored-By: Claude Fable 5 * improvement(mothership): make user_table limit cap internal, not model-facing The model can now pass any limit — no "cannot exceed 1000" rejection. 1000 becomes an internal threshold: query_rows clamps the page to MAX_QUERY_LIMIT (totalCount signals truncation; the model pages with offset), and bulk filter ops above the cap run as background jobs. update_rows_by_filter loads full row data inline, so an explicit limit above the cap escalates to the background worker with a new maxRows budget (the worker stops after maxRows; update has no read mask so the cap is exact). delete only loads ids inline, so an explicit limit (any size) stays inline — only unbounded deletes use the masked background path, which would over-hide a bounded delete. Co-Authored-By: Claude Fable 5 * improvement(mothership): bounded delete above the cap runs async, not inline An explicit delete limit now mirrors update: ≤1000 runs inline, above the cap it escalates to the background worker honoring the limit via maxRows — instead of always staying inline. The worker stops after maxRows (per-page fetch capped to the remaining budget). Bounded background deletes skip pendingDeleteMask: the filter-based mask hides every match, which would over-hide the rows beyond the cap the job never deletes. Unmasked, a bounded delete is eventually consistent like a bounded update (rows disappear as deleted), and doomedCount is omitted from the payload so the count isn't double-subtracted. Co-Authored-By: Claude Fable 5 * docs(mothership): tidy user_table limit/offset param copy Drop "Any value is allowed" from the limit description and restore the original offset description. Co-Authored-By: Claude Fable 5 * fix(tables): skip pendingDeleteMask for bounded background deletes The bounded-delete commit (f1ee3e9) persisted maxRows and omitted doomedCount but the pendingDeleteMask guard that makes it work was left uncommitted, so the shipped mask still hid every filter+cutoff match — over-hiding the rows beyond maxRows that the job never deletes (they vanished from reads until the job ended, then reappeared). Return no mask when maxRows is set: a bounded delete is eventually consistent (rows disappear as deleted), like a bounded update. Co-Authored-By: Claude Fable 5 * docs(mothership): drop redundant background note from limit arg The op descriptions already cover background escalation; the limit arg only needs to say what the param does. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- apps/sim/background/table-update.ts | 38 ++ .../lib/copilot/generated/tool-catalog-v1.ts | 3 +- .../lib/copilot/generated/tool-schemas-v1.ts | 3 +- .../tools/server/table/user-table.test.ts | 507 +++++++++++++++- .../copilot/tools/server/table/user-table.ts | 551 +++++++++++++++--- apps/sim/lib/table/delete-runner.test.ts | 16 + apps/sim/lib/table/delete-runner.ts | 13 +- apps/sim/lib/table/events.ts | 2 +- apps/sim/lib/table/import-runner.test.ts | 117 ++++ apps/sim/lib/table/import-runner.ts | 18 +- apps/sim/lib/table/rows/ordering.ts | 79 +++ apps/sim/lib/table/rows/service.ts | 5 + apps/sim/lib/table/types.ts | 30 +- apps/sim/lib/table/update-runner.test.ts | 219 +++++++ apps/sim/lib/table/update-runner.ts | 196 +++++++ 15 files changed, 1699 insertions(+), 98 deletions(-) create mode 100644 apps/sim/background/table-update.ts create mode 100644 apps/sim/lib/table/import-runner.test.ts create mode 100644 apps/sim/lib/table/update-runner.test.ts create mode 100644 apps/sim/lib/table/update-runner.ts diff --git a/apps/sim/background/table-update.ts b/apps/sim/background/table-update.ts new file mode 100644 index 0000000000..412c241c53 --- /dev/null +++ b/apps/sim/background/table-update.ts @@ -0,0 +1,38 @@ +import { task } from '@trigger.dev/sdk' +import { + markTableUpdateFailed, + runTableUpdate, + type TableUpdatePayload, +} from '@/lib/table/update-runner' + +/** + * `TableUpdatePayload` with the cutoff as an ISO string — task payloads cross a JSON boundary, so + * the Date is rehydrated in `run` rather than trusting payload serialization. + */ +export interface TableUpdateTaskPayload extends Omit { + cutoff: string +} + +/** + * Trigger.dev wrapper around `runTableUpdate`. Errors propagate out of `run` so the retry policy + * fires; the job is marked failed only in `onFailure`, after the final attempt. Retry-safe: the + * worker keysets by id with a `created_at <= cutoff` floor and the JSONB-merge patch is idempotent + * (re-applying the same patch to an already-patched row is a no-op), so a retried attempt re-walks + * and re-applies whatever remains. The `table_jobs` ownership gate stops a retried run that lost + * the job within one page. + */ +export const tableUpdateTask = task({ + id: 'table-update', + machine: 'small-1x', + retry: { maxAttempts: 3 }, + queue: { + name: 'table-update', + concurrencyLimit: 10, + }, + run: async (payload: TableUpdateTaskPayload) => { + await runTableUpdate({ ...payload, cutoff: new Date(payload.cutoff) }) + }, + onFailure: async ({ payload, error }) => { + await markTableUpdateFailed(payload.tableId, payload.jobId, error) + }, +}) diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index ae4296db33..0cd92c61c8 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -3958,7 +3958,8 @@ export const UserTable: ToolCatalogEntry = { }, limit: { type: 'number', - description: 'Maximum rows to return or affect (optional, default 100)', + description: + 'Maximum rows to return or affect (optional, default 100). Omit on update_rows_by_filter / delete_rows_by_filter to act on every match.', }, mapping: { type: 'object', diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 5ed8d9224d..31171237e7 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -3686,7 +3686,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, limit: { type: 'number', - description: 'Maximum rows to return or affect (optional, default 100)', + description: + 'Maximum rows to return or affect (optional, default 100). Omit on update_rows_by_filter / delete_rows_by_filter to act on every match.', }, mapping: { type: 'object', diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts index 7d90e9c00a..abf8c7d6f4 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts @@ -15,6 +15,14 @@ const { mockCreateTable, mockDeleteTable, mockGetWorkspaceTableLimits, + mockMarkTableJobRunning, + mockReleaseJobClaim, + mockQueryRows, + mockDeleteRowsByFilter, + mockUpdateRowsByFilter, + mockRunTableImport, + mockRunTableDelete, + mockRunTableUpdate, fakeEnrichment, } = vi.hoisted(() => ({ mockResolveWorkspaceFileReference: vi.fn(), @@ -26,6 +34,14 @@ const { mockCreateTable: vi.fn(), mockDeleteTable: vi.fn(), mockGetWorkspaceTableLimits: vi.fn(), + mockMarkTableJobRunning: vi.fn(), + mockReleaseJobClaim: vi.fn(), + mockQueryRows: vi.fn(), + mockDeleteRowsByFilter: vi.fn(), + mockUpdateRowsByFilter: vi.fn(), + mockRunTableImport: vi.fn(), + mockRunTableDelete: vi.fn(), + mockRunTableUpdate: vi.fn(), fakeEnrichment: { id: 'work-email', name: 'Work Email', @@ -83,14 +99,33 @@ vi.mock('@/lib/table/rows/service', () => ({ batchInsertRows: mockBatchInsertRows, batchUpdateRows: vi.fn(), deleteRow: vi.fn(), - deleteRowsByFilter: vi.fn(), + deleteRowsByFilter: mockDeleteRowsByFilter, deleteRowsByIds: vi.fn(), getRowById: vi.fn(), insertRow: vi.fn(), - queryRows: vi.fn(), + queryRows: mockQueryRows, replaceTableRows: mockReplaceTableRows, updateRow: vi.fn(), - updateRowsByFilter: vi.fn(), + updateRowsByFilter: mockUpdateRowsByFilter, +})) + +vi.mock('@/lib/table/jobs/service', () => ({ + markTableJobRunning: mockMarkTableJobRunning, + releaseJobClaim: mockReleaseJobClaim, +})) + +vi.mock('@/lib/table/import-runner', () => ({ + runTableImport: mockRunTableImport, +})) + +vi.mock('@/lib/table/delete-runner', () => ({ + markTableDeleteFailed: vi.fn(), + runTableDelete: mockRunTableDelete, +})) + +vi.mock('@/lib/table/update-runner', () => ({ + markTableUpdateFailed: vi.fn(), + runTableUpdate: mockRunTableUpdate, })) vi.mock('@/lib/table/billing', () => ({ @@ -122,15 +157,25 @@ function buildTable(overrides: Partial = {}): TableDefinition { } } +/** Lets a runDetached microtask chain run before asserting on the work it dispatched. */ +async function flushDetached(): Promise { + await Promise.resolve() + await Promise.resolve() +} + describe('userTableServerTool.import_file', () => { beforeEach(() => { vi.clearAllMocks() mockResolveWorkspaceFileReference.mockResolvedValue({ name: 'people.csv', type: 'text/csv', + key: 'workspace/workspace-1/people.csv', + size: 100, }) mockDownloadWorkspaceFile.mockResolvedValue(Buffer.from('name,age\nAlice,30\nBob,40')) mockGetTableById.mockResolvedValue(buildTable()) + mockMarkTableJobRunning.mockResolvedValue(true) + mockReleaseJobClaim.mockResolvedValue(undefined) mockBatchInsertRows.mockImplementation(async (data: { rows: unknown[] }) => data.rows.map((_, i) => ({ id: `row_${i}` })) ) @@ -253,12 +298,95 @@ describe('userTableServerTool.import_file', () => { expect(result.message).toMatch(/missing required columns/i) expect(mockBatchInsertRows).not.toHaveBeenCalled() }) + + it('claims and releases the table job slot around an inline import', async () => { + const result = await userTableServerTool.execute( + { operation: 'import_file', args: { tableId: 'tbl_1', fileId: 'file-1' } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(true) + expect(mockMarkTableJobRunning).toHaveBeenCalledWith('tbl_1', expect.any(String), 'import') + expect(mockReleaseJobClaim).toHaveBeenCalledWith( + 'tbl_1', + mockMarkTableJobRunning.mock.calls[0][1] + ) + }) + + it('rejects an inline import while another job holds the table slot', async () => { + mockMarkTableJobRunning.mockResolvedValueOnce(false) + const result = await userTableServerTool.execute( + { operation: 'import_file', args: { tableId: 'tbl_1', fileId: 'file-1' } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(false) + expect(result.message).toMatch(/job is already in progress/i) + expect(mockBatchInsertRows).not.toHaveBeenCalled() + expect(mockReleaseJobClaim).not.toHaveBeenCalled() + }) + + it('dispatches a background import for large CSV files', async () => { + mockResolveWorkspaceFileReference.mockResolvedValueOnce({ + name: 'big.csv', + type: 'text/csv', + key: 'workspace/workspace-1/big.csv', + size: 9 * 1024 * 1024, + }) + + const result = await userTableServerTool.execute( + { operation: 'import_file', args: { tableId: 'tbl_1', fileId: 'file-1', mode: 'replace' } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + expect(result.data?.jobId).toBeDefined() + expect(result.message).toMatch(/background/i) + expect(mockMarkTableJobRunning).toHaveBeenCalledWith('tbl_1', expect.any(String), 'import') + expect(mockBatchInsertRows).not.toHaveBeenCalled() + expect(mockReplaceTableRows).not.toHaveBeenCalled() + expect(mockDownloadWorkspaceFile).not.toHaveBeenCalled() + expect(mockRunTableImport).toHaveBeenCalledTimes(1) + expect(mockRunTableImport.mock.calls[0][0]).toMatchObject({ + tableId: 'tbl_1', + workspaceId: 'workspace-1', + fileKey: 'workspace/workspace-1/big.csv', + mode: 'replace', + deleteSourceFile: false, + }) + }) + + it('rejects a background import while another job holds the table slot', async () => { + mockResolveWorkspaceFileReference.mockResolvedValueOnce({ + name: 'big.csv', + type: 'text/csv', + key: 'workspace/workspace-1/big.csv', + size: 9 * 1024 * 1024, + }) + mockMarkTableJobRunning.mockResolvedValueOnce(false) + + const result = await userTableServerTool.execute( + { operation: 'import_file', args: { tableId: 'tbl_1', fileId: 'file-1' } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(false) + expect(result.message).toMatch(/job is already in progress/i) + expect(mockRunTableImport).not.toHaveBeenCalled() + }) }) describe('userTableServerTool.create_from_file', () => { beforeEach(() => { vi.clearAllMocks() - mockResolveWorkspaceFileReference.mockResolvedValue({ name: 'people.csv', type: 'text/csv' }) + mockResolveWorkspaceFileReference.mockResolvedValue({ + name: 'people.csv', + type: 'text/csv', + key: 'workspace/workspace-1/people.csv', + size: 100, + }) mockDownloadWorkspaceFile.mockResolvedValue(Buffer.from('name,age\nAlice,30\nBob,40')) mockGetWorkspaceTableLimits.mockResolvedValue({ maxRowsPerTable: 1000, maxTables: 3 }) mockCreateTable.mockResolvedValue(buildTable({ id: 'tbl_new', name: 'people' })) @@ -313,6 +441,40 @@ describe('userTableServerTool.create_from_file', () => { expect(result.message).toMatch(/rolled back/i) expect(result.message).toMatch(/must be unique/i) }) + + it('creates a placeholder table and dispatches a background import for large CSV files', async () => { + mockResolveWorkspaceFileReference.mockResolvedValueOnce({ + name: 'big.csv', + type: 'text/csv', + key: 'workspace/workspace-1/big.csv', + size: 9 * 1024 * 1024, + }) + + const result = await userTableServerTool.execute( + { operation: 'create_from_file', args: { fileId: 'file-1' } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + expect(result.data?.tableId).toBe('tbl_new') + expect(result.data?.jobId).toBeDefined() + expect(mockDownloadWorkspaceFile).not.toHaveBeenCalled() + expect(mockBatchInsertRows).not.toHaveBeenCalled() + const createArgs = mockCreateTable.mock.calls[0][0] as Record + expect(createArgs).toMatchObject({ + jobStatus: 'running', + jobType: 'import', + jobId: result.data?.jobId, + }) + expect(mockRunTableImport).toHaveBeenCalledTimes(1) + expect(mockRunTableImport.mock.calls[0][0]).toMatchObject({ + tableId: 'tbl_new', + mode: 'create', + fileKey: 'workspace/workspace-1/big.csv', + deleteSourceFile: false, + }) + }) }) describe('userTableServerTool.create', () => { @@ -506,3 +668,340 @@ describe('userTableServerTool.add_enrichment', () => { expect(mockAddWorkflowGroup).not.toHaveBeenCalled() }) }) + +describe('userTableServerTool.query_rows', () => { + const queryRow = (i: number) => ({ + id: `row_${i}`, + data: { name: `r${i}` }, + executions: {}, + position: i, + orderKey: `a${i}`, + createdAt: new Date('2024-01-01'), + updatedAt: new Date('2024-01-01'), + }) + + beforeEach(() => { + vi.clearAllMocks() + mockGetTableById.mockResolvedValue(buildTable()) + mockQueryRows.mockResolvedValue({ + rows: [queryRow(1), queryRow(2)], + rowCount: 2, + totalCount: 10, + limit: 2, + offset: 0, + }) + }) + + it('clamps an over-large query limit to MAX_QUERY_LIMIT instead of rejecting', async () => { + const result = await userTableServerTool.execute( + { operation: 'query_rows', args: { tableId: 'tbl_1', limit: 100000 } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(true) + const options = mockQueryRows.mock.calls[0][1] as Record + expect(options.limit).toBe(1000) + }) + + it('queries without execution metadata and passes limit/offset through', async () => { + const result = await userTableServerTool.execute( + { operation: 'query_rows', args: { tableId: 'tbl_1', limit: 2, offset: 10 } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(true) + const options = mockQueryRows.mock.calls[0][1] as Record + expect(options.withExecutions).toBe(false) + expect(options.offset).toBe(10) + expect(result.data?.nextCursor).toBeUndefined() + }) +}) + +describe('userTableServerTool.delete_rows_by_filter', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetTableById.mockResolvedValue(buildTable({ rowCount: 50000, maxRows: 100000 })) + mockMarkTableJobRunning.mockResolvedValue(true) + mockDeleteRowsByFilter.mockResolvedValue({ affectedCount: 5, affectedRowIds: ['r1'] }) + mockQueryRows.mockResolvedValue({ + rows: [], + rowCount: 0, + totalCount: 5, + limit: 1, + offset: 0, + }) + }) + + it('escalates an explicit limit above the cap to a background delete with maxRows (unmasked)', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + + const result = await userTableServerTool.execute( + { + operation: 'delete_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, limit: 5000 }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + // target = min(limit 5000, matchCount 20000) = 5000, above the inline cap → background. + expect(result.data?.doomedCount).toBe(5000) + expect(mockDeleteRowsByFilter).not.toHaveBeenCalled() + const [, , type, payload] = mockMarkTableJobRunning.mock.calls[0] + expect(type).toBe('delete') + // Bounded delete carries maxRows and omits doomedCount so the mask is skipped and the count + // isn't double-subtracted. + expect(payload).toMatchObject({ maxRows: 5000 }) + expect((payload as { doomedCount?: number }).doomedCount).toBeUndefined() + expect(mockRunTableDelete.mock.calls[0][0]).toMatchObject({ maxRows: 5000 }) + }) + + it('deletes inline when the unbounded match count is within the cap', async () => { + const result = await userTableServerTool.execute( + { operation: 'delete_rows_by_filter', args: { tableId: 'tbl_1', filter: { name: 'x' } } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(true) + expect(result.data?.affectedCount).toBe(5) + expect(mockDeleteRowsByFilter).toHaveBeenCalledTimes(1) + // Inline delete still claims (and releases) the table's write-job slot. + expect(mockMarkTableJobRunning).toHaveBeenCalledWith('tbl_1', expect.any(String), 'delete') + expect(mockReleaseJobClaim).toHaveBeenCalled() + }) + + it('rejects an inline delete while another job holds the table slot', async () => { + mockMarkTableJobRunning.mockResolvedValueOnce(false) + + const result = await userTableServerTool.execute( + { + operation: 'delete_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, limit: 100 }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(false) + expect(result.message).toMatch(/job is already in progress/i) + expect(mockDeleteRowsByFilter).not.toHaveBeenCalled() + }) + + it('dispatches a background delete when the unbounded match count exceeds the cap', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + + const result = await userTableServerTool.execute( + { operation: 'delete_rows_by_filter', args: { tableId: 'tbl_1', filter: { name: 'x' } } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + expect(result.data?.jobId).toBeDefined() + expect(result.data?.doomedCount).toBe(20000) + expect(mockDeleteRowsByFilter).not.toHaveBeenCalled() + const [tableId, jobId, type, payload] = mockMarkTableJobRunning.mock.calls[0] + expect(tableId).toBe('tbl_1') + expect(type).toBe('delete') + expect(payload).toMatchObject({ doomedCount: 20000, cutoff: expect.any(String) }) + // Unbounded delete masks the whole set — no maxRows cap. + expect((payload as { maxRows?: number }).maxRows).toBeUndefined() + expect(mockRunTableDelete).toHaveBeenCalledTimes(1) + expect(mockRunTableDelete.mock.calls[0][0]).toMatchObject({ + jobId, + tableId: 'tbl_1', + workspaceId: 'workspace-1', + cutoff: expect.any(Date), + }) + }) + + it('rejects a background delete while another job holds the table slot', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + mockMarkTableJobRunning.mockResolvedValueOnce(false) + + const result = await userTableServerTool.execute( + { operation: 'delete_rows_by_filter', args: { tableId: 'tbl_1', filter: { name: 'x' } } }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(false) + expect(result.message).toMatch(/job is already in progress/i) + expect(mockDeleteRowsByFilter).not.toHaveBeenCalled() + expect(mockRunTableDelete).not.toHaveBeenCalled() + }) + + it('deletes inline with an explicit limit without counting first', async () => { + const result = await userTableServerTool.execute( + { + operation: 'delete_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, limit: 100 }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + + expect(result.success).toBe(true) + expect(mockQueryRows).not.toHaveBeenCalled() + expect(mockDeleteRowsByFilter).toHaveBeenCalledTimes(1) + }) +}) + +describe('userTableServerTool.update_rows_by_filter', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetTableById.mockResolvedValue(buildTable()) + mockMarkTableJobRunning.mockResolvedValue(true) + mockUpdateRowsByFilter.mockResolvedValue({ affectedCount: 5, affectedRowIds: ['r1'] }) + mockQueryRows.mockResolvedValue({ rows: [], rowCount: 0, totalCount: 5, limit: 1, offset: 0 }) + }) + + it('escalates an explicit limit above the cap to a background update with maxRows', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, data: { age: 1 }, limit: 5000 }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + // target = min(limit 5000, matchCount 20000) = 5000, above the inline cap → background. + expect(result.data?.affectedCount).toBe(5000) + expect(mockUpdateRowsByFilter).not.toHaveBeenCalled() + const [, , type, payload] = mockMarkTableJobRunning.mock.calls[0] + expect(type).toBe('update') + expect(payload).toMatchObject({ affectedCount: 5000, maxRows: 5000 }) + expect(mockRunTableUpdate.mock.calls[0][0]).toMatchObject({ maxRows: 5000 }) + }) + + it('updates inline when the unbounded match count is within the cap', async () => { + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, data: { age: 1 } }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + expect(result.success).toBe(true) + expect(result.data?.affectedCount).toBe(5) + expect(mockUpdateRowsByFilter).toHaveBeenCalledTimes(1) + expect(mockMarkTableJobRunning).not.toHaveBeenCalled() + }) + + it('dispatches a background update when the unbounded match count exceeds the cap', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, data: { age: 1 } }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + await flushDetached() + + expect(result.success).toBe(true) + expect(result.data?.jobId).toBeDefined() + expect(result.data?.affectedCount).toBe(20000) + expect(mockUpdateRowsByFilter).not.toHaveBeenCalled() + const [tableId, jobId, type, payload] = mockMarkTableJobRunning.mock.calls[0] + expect(tableId).toBe('tbl_1') + expect(type).toBe('update') + expect(payload).toMatchObject({ + affectedCount: 20000, + cutoff: expect.any(String), + data: { age: 1 }, + }) + // Unbounded match (no explicit limit) → the worker patches every match, no cap. + expect((payload as { maxRows?: number }).maxRows).toBeUndefined() + expect(mockRunTableUpdate).toHaveBeenCalledTimes(1) + expect(mockRunTableUpdate.mock.calls[0][0]).toMatchObject({ + jobId, + tableId: 'tbl_1', + workspaceId: 'workspace-1', + cutoff: expect.any(Date), + }) + }) + + it('keeps a unique-column patch inline even when many rows match', async () => { + mockGetTableById.mockResolvedValue( + buildTable({ schema: { columns: [{ name: 'email', type: 'string', unique: true }] } }) + ) + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { email: 'x' }, data: { email: 'y' } }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + expect(result.success).toBe(true) + expect(mockQueryRows).not.toHaveBeenCalled() + expect(mockMarkTableJobRunning).not.toHaveBeenCalled() + expect(mockUpdateRowsByFilter).toHaveBeenCalledTimes(1) + }) + + it('rejects a background update while another job holds the table slot', async () => { + mockQueryRows.mockResolvedValueOnce({ + rows: [], + rowCount: 0, + totalCount: 20000, + limit: 1, + offset: 0, + }) + mockMarkTableJobRunning.mockResolvedValueOnce(false) + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, data: { age: 1 } }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + expect(result.success).toBe(false) + expect(result.message).toMatch(/job is already in progress/i) + expect(mockUpdateRowsByFilter).not.toHaveBeenCalled() + expect(mockRunTableUpdate).not.toHaveBeenCalled() + }) + + it('updates inline with an explicit limit without counting first', async () => { + const result = await userTableServerTool.execute( + { + operation: 'update_rows_by_filter', + args: { tableId: 'tbl_1', filter: { name: 'x' }, data: { age: 1 }, limit: 100 }, + }, + { userId: 'user-1', workspaceId: 'workspace-1' } + ) + expect(result.success).toBe(true) + expect(mockQueryRows).not.toHaveBeenCalled() + expect(mockUpdateRowsByFilter).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index 7614dc63ae..024dda09ad 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -7,9 +7,12 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' +import { runDetached } from '@/lib/core/utils/background' import { buildAutoMapping, COLUMN_TYPES, + CSV_ASYNC_IMPORT_THRESHOLD_BYTES, CSV_MAX_BATCH_SIZE, type CsvHeaderMapping, CsvImportValidationError, @@ -17,6 +20,8 @@ import { getWorkspaceTableLimits, inferSchemaFromCsv, parseFileRows, + sanitizeName, + TABLE_LIMITS, validateMapping, } from '@/lib/table' import { @@ -36,6 +41,9 @@ import { updateColumnConstraints, updateColumnType, } from '@/lib/table/columns/service' +import { markTableDeleteFailed, runTableDelete } from '@/lib/table/delete-runner' +import { runTableImport, type TableImportPayload } from '@/lib/table/import-runner' +import { markTableJobRunning, releaseJobClaim } from '@/lib/table/jobs/service' import { batchInsertRows, batchUpdateRows, @@ -52,14 +60,18 @@ import { import { createTable, deleteTable, getTableById, renameTable } from '@/lib/table/service' import type { ColumnDefinition, + Filter, RowData, TableDefinition, + TableDeleteJobPayload, + TableUpdateJobPayload, WorkflowGroup, WorkflowGroupDependencies, WorkflowGroupDeploymentMode, WorkflowGroupInputMapping, WorkflowGroupOutput, } from '@/lib/table/types' +import { markTableUpdateFailed, runTableUpdate } from '@/lib/table/update-runner' import { cancelWorkflowGroupRuns, runWorkflowColumn } from '@/lib/table/workflow-columns' import { addWorkflowGroup, @@ -93,18 +105,132 @@ type UserTableResult = { const MAX_BATCH_SIZE = CSV_MAX_BATCH_SIZE -async function resolveWorkspaceFile( - fileReference: string, - workspaceId: string -): Promise<{ buffer: Buffer; name: string; type: string }> { +async function resolveWorkspaceFileRecordOrThrow(fileReference: string, workspaceId: string) { const record = await resolveWorkspaceFileReference(workspaceId, fileReference) if (!record) { throw new Error( `File not found: "${fileReference}". Use glob("files/**") and read the canonical file path metadata to find workspace files.` ) } - const buffer = await fetchWorkspaceFileBuffer(record) - return { buffer, name: record.name, type: record.type } + return record +} + +/** + * Whether a workspace file should import as a background job instead of inline: + * CSV/TSV at or above the same byte threshold the UI uses. Other formats + * (xlsx/json) aren't supported by the streaming import worker and stay inline. + */ +function shouldImportInBackground(record: { name: string; size: number }): boolean { + const ext = record.name.split('.').pop()?.toLowerCase() + return (ext === 'csv' || ext === 'tsv') && record.size >= CSV_ASYNC_IMPORT_THRESHOLD_BYTES +} + +/** + * Dispatches a background import for an already-claimed job slot, mirroring the + * import-async routes: trigger.dev when enabled (survives deploys, retries), + * detached in-process worker otherwise. A failed dispatch releases the claim so + * a ghost `running` job can't hold the table's one-write-job slot. + */ +async function dispatchImportJob(payload: TableImportPayload): Promise { + if (isTriggerDevEnabled) { + try { + const [{ tableImportTask }, { tasks }] = await Promise.all([ + import('@/background/table-import'), + import('@trigger.dev/sdk'), + ]) + await tasks.trigger('table-import', payload, { + tags: [`tableId:${payload.tableId}`, `jobId:${payload.importId}`], + }) + } catch (error) { + await releaseJobClaim(payload.tableId, payload.importId).catch(() => {}) + throw error + } + } else { + runDetached('table-import', () => runTableImport(payload)) + } +} + +/** + * Dispatches a background filter-delete for an already-claimed job slot, + * mirroring the delete-async route. Same release-on-failed-dispatch guard as + * {@link dispatchImportJob}. + */ +async function dispatchDeleteJob(params: { + jobId: string + tableId: string + workspaceId: string + filter: Filter + cutoff: Date + maxRows?: number +}): Promise { + const { jobId, tableId, workspaceId, filter, cutoff, maxRows } = params + if (isTriggerDevEnabled) { + try { + const [{ tableDeleteTask }, { tasks }] = await Promise.all([ + import('@/background/table-delete'), + import('@trigger.dev/sdk'), + ]) + await tasks.trigger( + 'table-delete', + { jobId, tableId, workspaceId, filter, cutoff: cutoff.toISOString(), maxRows }, + { tags: [`tableId:${tableId}`, `jobId:${jobId}`] } + ) + } catch (error) { + await releaseJobClaim(tableId, jobId).catch(() => {}) + throw error + } + } else { + runDetached('table-delete', () => + runTableDelete({ jobId, tableId, workspaceId, filter, cutoff, maxRows }).catch( + async (error) => { + await markTableDeleteFailed(tableId, jobId, error) + throw error + } + ) + ) + } +} + +/** + * Dispatches a background bulk update for an already-claimed job slot, mirroring + * {@link dispatchDeleteJob}: trigger.dev when enabled, detached worker otherwise, releasing the + * slot on a failed dispatch. + */ +async function dispatchUpdateJob(params: { + jobId: string + tableId: string + workspaceId: string + filter: Filter + data: RowData + cutoff: Date + maxRows?: number +}): Promise { + const { jobId, tableId, workspaceId, filter, data, cutoff, maxRows } = params + if (isTriggerDevEnabled) { + try { + const [{ tableUpdateTask }, { tasks }] = await Promise.all([ + import('@/background/table-update'), + import('@trigger.dev/sdk'), + ]) + await tasks.trigger( + 'table-update', + { jobId, tableId, workspaceId, filter, data, cutoff: cutoff.toISOString(), maxRows }, + { tags: [`tableId:${tableId}`, `jobId:${jobId}`] } + ) + } catch (error) { + await releaseJobClaim(tableId, jobId).catch(() => {}) + throw error + } + } else { + runDetached('table-update', () => + runTableUpdate({ jobId, tableId, workspaceId, filter, data, cutoff, maxRows }).catch( + async (error) => { + await markTableUpdateFailed(tableId, jobId, error) + throw error + } + ) + ) + } } /** @@ -159,6 +285,20 @@ function parseDeploymentMode(value: unknown): WorkflowGroupDeploymentMode | unde return value === 'live' || value === 'deployed' ? value : undefined } +/** + * Validates an optional row limit. There's no upper bound the caller must respect — the model may + * ask for any number. `MAX_QUERY_LIMIT` / `MAX_BULK_OPERATION_SIZE` are applied internally instead + * (query_rows clamps the page; bulk ops above the bound run as a background job). Returns an error + * message, or `null` when the limit is acceptable. + */ +function limitError(limit: unknown): string | null { + if (limit === undefined) return null + if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1) { + return 'Limit must be an integer of at least 1' + } + return null +} + async function batchInsertAll( tableId: string, rows: RowData[], @@ -444,6 +584,11 @@ export const userTableServerTool: BaseServerTool return { success: false, message: 'Workspace ID is required' } } + const queryLimitError = limitError(args.limit) + if (queryLimitError) { + return { success: false, message: queryLimitError } + } + const table = await getTableById(args.tableId) if (!table || table.workspaceId !== workspaceId) { return { success: false, message: `Table not found: ${args.tableId}` } @@ -452,13 +597,20 @@ export const userTableServerTool: BaseServerTool const requestId = generateId().slice(0, 8) const idByName = buildIdByName(table.schema) const nameById = buildNameById(table.schema) + // The model may request any number; we serve at most MAX_QUERY_LIMIT per page so a single + // tool result can't drain a whole table. `totalCount` in the response signals truncation, + // and the model pages with `offset`. const result = await queryRows( table, { filter: args.filter ? filterNamesToIds(args.filter, idByName) : undefined, sort: args.sort ? sortNamesToIds(args.sort, idByName) : undefined, - limit: args.limit, + limit: + args.limit !== undefined + ? Math.min(args.limit, TABLE_LIMITS.MAX_QUERY_LIMIT) + : undefined, offset: args.offset, + withExecutions: false, }, requestId ) @@ -559,6 +711,10 @@ export const userTableServerTool: BaseServerTool if (!workspaceId) { return { success: false, message: 'Workspace ID is required' } } + const updateLimitError = limitError(args.limit) + if (updateLimitError) { + return { success: false, message: updateLimitError } + } const table = await getTableById(args.tableId) if (!table || table.workspaceId !== workspaceId) { @@ -566,13 +722,67 @@ export const userTableServerTool: BaseServerTool } const requestId = generateId().slice(0, 8) - assertNotAborted() const idByName = buildIdByName(table.schema) + const idFilter = filterNamesToIds(args.filter, idByName) + const idData = rowDataNameToId(args.data, idByName) + + // Inline handles up to MAX_BULK_OPERATION_SIZE rows in one request; a larger operation + // (an explicit limit above the cap, or unbounded "update everything matching") runs in the + // background worker so a broad update on a huge table doesn't load every matching row into + // this request. A small explicit limit is the fast path — no count needed. A patch + // touching a unique column always stays inline (the service rejects bulk-setting a unique + // value across multiple rows). + const patchTouchesUnique = table.schema.columns.some( + (c) => c.unique === true && (c.id ?? c.name) in idData + ) + const updateInlineEligible = + args.limit !== undefined && args.limit <= TABLE_LIMITS.MAX_BULK_OPERATION_SIZE + if (!updateInlineEligible && !patchTouchesUnique) { + const { totalCount } = await queryRows( + table, + { filter: idFilter, limit: 1, withExecutions: false }, + requestId + ) + const matchCount = totalCount ?? 0 + const target = args.limit !== undefined ? Math.min(args.limit, matchCount) : matchCount + if (target > TABLE_LIMITS.MAX_BULK_OPERATION_SIZE) { + const cutoff = new Date() + const jobId = generateId() + const payload: TableUpdateJobPayload = { + filter: idFilter, + data: idData, + cutoff: cutoff.toISOString(), + affectedCount: target, + maxRows: args.limit, + } + assertNotAborted() + const claimed = await markTableJobRunning(table.id, jobId, 'update', payload) + if (!claimed) { + return { success: false, message: 'A job is already in progress for this table' } + } + await dispatchUpdateJob({ + jobId, + tableId: table.id, + workspaceId, + filter: idFilter, + data: idData, + cutoff, + maxRows: args.limit, + }) + return { + success: true, + message: `Started background update of ${target} matching rows (job ${jobId}). Rows update in the background — query_rows to check progress. Note: background updates don't auto-recompute workflow/enrichment columns; use run_column afterward if needed.`, + data: { jobId, affectedCount: target }, + } + } + } + + assertNotAborted() const result = await updateRowsByFilter( table, { - filter: filterNamesToIds(args.filter, idByName), - data: rowDataNameToId(args.data, idByName), + filter: idFilter, + data: idData, limit: args.limit, actorUserId: context.userId, }, @@ -596,6 +806,10 @@ export const userTableServerTool: BaseServerTool if (!workspaceId) { return { success: false, message: 'Workspace ID is required' } } + const deleteLimitError = limitError(args.limit) + if (deleteLimitError) { + return { success: false, message: deleteLimitError } + } const table = await getTableById(args.tableId) if (!table || table.workspaceId !== workspaceId) { @@ -603,16 +817,77 @@ export const userTableServerTool: BaseServerTool } const requestId = generateId().slice(0, 8) - assertNotAborted() const idByName = buildIdByName(table.schema) - const result = await deleteRowsByFilter( - table, - { - filter: filterNamesToIds(args.filter, idByName), - limit: args.limit, - }, - requestId - ) + const idFilter = filterNamesToIds(args.filter, idByName) + + // Inline handles up to MAX_BULK_OPERATION_SIZE rows; a larger delete (an explicit limit + // above the cap, or unbounded "delete everything matching") hands off to the background + // delete worker so a broad delete on a huge table doesn't load every matching id into this + // request. A small explicit limit is the fast path. + const deleteInlineEligible = + args.limit !== undefined && args.limit <= TABLE_LIMITS.MAX_BULK_OPERATION_SIZE + if (!deleteInlineEligible) { + const { totalCount } = await queryRows( + table, + { filter: idFilter, limit: 1, withExecutions: false }, + requestId + ) + const matchCount = totalCount ?? 0 + const target = args.limit !== undefined ? Math.min(args.limit, matchCount) : matchCount + if (target > TABLE_LIMITS.MAX_BULK_OPERATION_SIZE) { + const doomedCount = Math.min(target, table.rowCount) + const cutoff = new Date() + const jobId = generateId() + // Unbounded: mask the whole matching set (instant post-delete view), so `doomedCount` + // drives the count adjustment. Bounded (maxRows): no mask — `doomedCount` is omitted so + // the count isn't double-subtracted; rows disappear progressively as they're deleted. + const bounded = args.limit !== undefined + const payload: TableDeleteJobPayload = bounded + ? { filter: idFilter, cutoff: cutoff.toISOString(), maxRows: args.limit } + : { filter: idFilter, cutoff: cutoff.toISOString(), doomedCount } + assertNotAborted() + const claimed = await markTableJobRunning(table.id, jobId, 'delete', payload) + if (!claimed) { + return { success: false, message: 'A job is already in progress for this table' } + } + await dispatchDeleteJob({ + jobId, + tableId: table.id, + workspaceId, + filter: idFilter, + cutoff, + maxRows: args.limit, + }) + return { + success: true, + message: bounded + ? `Started background delete of up to ${doomedCount} matching rows (job ${jobId}). Rows delete in the background — query_rows to check progress.` + : `Started background delete of ${doomedCount} matching rows (job ${jobId}). The rows are hidden from reads immediately — query_rows already reflects the post-delete view.`, + data: { jobId, doomedCount }, + } + } + } + + // Claim the table's one-write-job slot for the inline delete too, so it + // can't interleave with a running background import/delete. Mask-safe: a + // payload-less delete job is ignored by pendingDeleteMask, and the delete + // completes synchronously within this request before the slot is released. + assertNotAborted() + const inlineDeleteId = generateId() + const deleteClaimed = await markTableJobRunning(table.id, inlineDeleteId, 'delete') + if (!deleteClaimed) { + return { success: false, message: 'A job is already in progress for this table' } + } + let result: Awaited> + try { + result = await deleteRowsByFilter( + table, + { filter: idFilter, limit: args.limit }, + requestId + ) + } finally { + await releaseJobClaim(table.id, inlineDeleteId).catch(() => {}) + } return { success: true, @@ -741,7 +1016,71 @@ export const userTableServerTool: BaseServerTool return { success: false, message: 'Workspace ID is required' } } - const file = await resolveWorkspaceFile(fileReference, workspaceId) + const record = await resolveWorkspaceFileRecordOrThrow(fileReference, workspaceId) + + // Large CSV/TSV: create a placeholder table whose creation claims the + // job slot, then let the streaming import worker infer the schema and + // populate rows in the background (mirrors POST /api/table/import-async). + if (shouldImportInBackground(record)) { + const planLimits = await getWorkspaceTableLimits(workspaceId) + const tableName = + args.name || + sanitizeName(record.name.replace(/\.[^.]+$/, ''), 'imported_table').slice( + 0, + TABLE_LIMITS.MAX_TABLE_NAME_LENGTH + ) + const requestId = generateId().slice(0, 8) + const importId = generateId() + assertNotAborted() + const table = await createTable( + { + name: tableName, + description: args.description || `Imported from ${record.name}`, + schema: { columns: [{ name: 'column_1', type: 'string' }] }, + workspaceId, + userId: context.userId, + maxRows: planLimits.maxRowsPerTable, + maxTables: planLimits.maxTables, + jobStatus: 'running', + jobType: 'import', + jobId: importId, + }, + requestId + ) + try { + await dispatchImportJob({ + importId, + tableId: table.id, + workspaceId, + userId: context.userId, + fileKey: record.key, + fileName: record.name, + delimiter: record.name.toLowerCase().endsWith('.tsv') ? '\t' : ',', + mode: 'create', + deleteSourceFile: false, + }) + } catch (dispatchError) { + // The user never saw the placeholder — archive it back out. + await deleteTable(table.id, generateId().slice(0, 8)).catch(() => {}) + throw dispatchError + } + return { + success: true, + message: `Created table "${table.name}" (${table.id}); importing rows from "${record.name}" in the background (job ${importId}). Columns and rows appear as the import progresses — query_rows to check what has landed.`, + data: { + tableId: table.id, + tableName: table.name, + jobId: importId, + sourceFile: record.name, + }, + } + } + + const file = { + buffer: await fetchWorkspaceFileBuffer(record), + name: record.name, + type: record.type, + } const { headers, rows } = await parseFileRows(file.buffer, file.name, file.type) if (rows.length === 0) { return { success: false, message: 'File contains no data rows' } @@ -866,95 +1205,143 @@ export const userTableServerTool: BaseServerTool return { success: false, message: `Table is archived: ${tableId}` } } - const file = await resolveWorkspaceFile(fileReference, workspaceId) - const { headers, rows } = await parseFileRows(file.buffer, file.name, file.type) - if (rows.length === 0) { - return { success: false, message: 'File contains no data rows' } - } - - const mapping: CsvHeaderMapping = rawMapping ?? buildAutoMapping(headers, table.schema) + const record = await resolveWorkspaceFileRecordOrThrow(fileReference, workspaceId) - let validation: ReturnType - try { - validation = validateMapping({ - csvHeaders: headers, - mapping, - tableSchema: table.schema, + // Large CSV/TSV: claim the table's one-write-job slot and hand the + // file to the streaming import worker (mirrors + // POST /api/table/[tableId]/import-async). + if (shouldImportInBackground(record)) { + const importId = generateId() + assertNotAborted() + const claimed = await markTableJobRunning(table.id, importId, 'import') + if (!claimed) { + return { success: false, message: 'A job is already in progress for this table' } + } + await dispatchImportJob({ + importId, + tableId: table.id, + workspaceId, + userId: context.userId, + fileKey: record.key, + fileName: record.name, + delimiter: record.name.toLowerCase().endsWith('.tsv') ? '\t' : ',', + mode, + mapping: rawMapping, + deleteSourceFile: false, }) - } catch (err) { - if (err instanceof CsvImportValidationError) { - return { success: false, message: err.message } + return { + success: true, + message: `Started background ${mode} import of "${record.name}" into "${table.name}" (job ${importId}). Rows appear as the import progresses — query_rows to check what has landed.`, + data: { tableId: table.id, jobId: importId, mode }, } - throw err } - if (validation.mappedHeaders.length === 0) { - return { - success: false, - message: `No matching columns between file (${headers.join(', ')}) and table (${table.schema.columns.map((c) => c.name).join(', ')})`, - } + // Claim the table's one-write-job slot up front — before the download + // and parse — so the inline import is mutually exclusive with any + // background import/delete for its whole duration, not just the write, + // and contention is detected before the parse work is spent. + const inlineImportId = generateId() + assertNotAborted() + const inlineClaimed = await markTableJobRunning(table.id, inlineImportId, 'import') + if (!inlineClaimed) { + return { success: false, message: 'A job is already in progress for this table' } } + try { + const file = { + buffer: await fetchWorkspaceFileBuffer(record), + name: record.name, + type: record.type, + } + const { headers, rows } = await parseFileRows(file.buffer, file.name, file.type) + if (rows.length === 0) { + return { success: false, message: 'File contains no data rows' } + } - const coerced = coerceRowsForTable(rows, table.schema, validation.effectiveMap) + const mapping: CsvHeaderMapping = rawMapping ?? buildAutoMapping(headers, table.schema) - if (mode === 'replace') { - assertNotAborted() - const requestId = generateId().slice(0, 8) - const result = await replaceTableRows( - { tableId: table.id, rows: coerced, workspaceId, userId: context.userId }, - table, - requestId - ) + let validation: ReturnType + try { + validation = validateMapping({ + csvHeaders: headers, + mapping, + tableSchema: table.schema, + }) + } catch (err) { + if (err instanceof CsvImportValidationError) { + return { success: false, message: err.message } + } + throw err + } + + if (validation.mappedHeaders.length === 0) { + return { + success: false, + message: `No matching columns between file (${headers.join(', ')}) and table (${table.schema.columns.map((c) => c.name).join(', ')})`, + } + } + + const coerced = coerceRowsForTable(rows, table.schema, validation.effectiveMap) + + if (mode === 'replace') { + const requestId = generateId().slice(0, 8) + const result = await replaceTableRows( + { tableId: table.id, rows: coerced, workspaceId, userId: context.userId }, + table, + requestId + ) + + logger.info('Rows replaced from file', { + tableId: table.id, + fileName: file.name, + mode, + matchedColumns: validation.mappedHeaders.length, + deleted: result.deletedCount, + inserted: result.insertedCount, + userId: context.userId, + }) + + return { + success: true, + message: `Replaced rows in "${table.name}" from "${file.name}": deleted ${result.deletedCount}, inserted ${result.insertedCount}`, + data: { + tableId: table.id, + tableName: table.name, + mode, + matchedColumns: validation.mappedHeaders, + skippedColumns: validation.skippedHeaders, + deletedCount: result.deletedCount, + insertedCount: result.insertedCount, + sourceFile: file.name, + }, + } + } + + const inserted = await batchInsertAll(table.id, coerced, table, workspaceId, context) - logger.info('Rows replaced from file', { + logger.info('Rows imported from file', { tableId: table.id, fileName: file.name, mode, matchedColumns: validation.mappedHeaders.length, - deleted: result.deletedCount, - inserted: result.insertedCount, + rows: inserted, userId: context.userId, }) return { success: true, - message: `Replaced rows in "${table.name}" from "${file.name}": deleted ${result.deletedCount}, inserted ${result.insertedCount}`, + message: `Imported ${inserted} rows into "${table.name}" from "${file.name}" (${validation.mappedHeaders.length} columns matched)`, data: { tableId: table.id, tableName: table.name, mode, matchedColumns: validation.mappedHeaders, skippedColumns: validation.skippedHeaders, - deletedCount: result.deletedCount, - insertedCount: result.insertedCount, + rowCount: inserted, sourceFile: file.name, }, } - } - - const inserted = await batchInsertAll(table.id, coerced, table, workspaceId, context) - - logger.info('Rows imported from file', { - tableId: table.id, - fileName: file.name, - mode, - matchedColumns: validation.mappedHeaders.length, - rows: inserted, - userId: context.userId, - }) - - return { - success: true, - message: `Imported ${inserted} rows into "${table.name}" from "${file.name}" (${validation.mappedHeaders.length} columns matched)`, - data: { - tableId: table.id, - tableName: table.name, - mode, - matchedColumns: validation.mappedHeaders, - skippedColumns: validation.skippedHeaders, - rowCount: inserted, - sourceFile: file.name, - }, + } finally { + await releaseJobClaim(table.id, inlineImportId).catch(() => {}) } } diff --git a/apps/sim/lib/table/delete-runner.test.ts b/apps/sim/lib/table/delete-runner.test.ts index d06285d51a..6894b59e44 100644 --- a/apps/sim/lib/table/delete-runner.test.ts +++ b/apps/sim/lib/table/delete-runner.test.ts @@ -82,6 +82,22 @@ describe('runTableDelete', () => { ) }) + it('stops once maxRows is reached and caps the final page fetch to the remaining budget', async () => { + // budget 3 with page size 2: first page fills 2, the second is capped to the remaining 1. + mockSelectRowIdPage.mockResolvedValueOnce(['a', 'b']).mockResolvedValueOnce(['c']) + + await runTableDelete(basePayload({ filter: { status: 'old' }, maxRows: 3 })) + + expect(mockSelectRowIdPage).toHaveBeenCalledTimes(2) + expect(mockSelectRowIdPage.mock.calls[0][0]).toMatchObject({ limit: 2 }) + expect(mockSelectRowIdPage.mock.calls[1][0]).toMatchObject({ limit: 1 }) + expect(mockDeletePageByIds).toHaveBeenCalledTimes(2) + expect(mockMarkJobReady).toHaveBeenCalledWith('tbl_1', 'job_1') + expect(mockAppendTableEvent).toHaveBeenCalledWith( + expect.objectContaining({ status: 'ready', progress: 3 }) + ) + }) + it('skips excluded rows but still advances the keyset cursor past them', async () => { mockSelectRowIdPage.mockResolvedValueOnce(['keep', 'x']).mockResolvedValueOnce([]) diff --git a/apps/sim/lib/table/delete-runner.ts b/apps/sim/lib/table/delete-runner.ts index 5fb4a6e370..024daab6af 100644 --- a/apps/sim/lib/table/delete-runner.ts +++ b/apps/sim/lib/table/delete-runner.ts @@ -36,6 +36,12 @@ export interface TableDeletePayload { excludeRowIds?: string[] /** Only rows created at/before this instant are deleted, so mid-job inserts survive. */ cutoff: Date + /** + * Stop after deleting this many rows (an explicit caller-supplied limit). Omitted = every match. + * Not combined with `excludeRowIds` (the UI's select-all path uses excludes and no cap; the + * copilot tool uses a cap and no excludes), so the per-page fetch can be bounded directly. + */ + maxRows?: number } /** @@ -52,8 +58,9 @@ export interface TableDeletePayload { * newer job took the table) returns quietly. */ export async function runTableDelete(payload: TableDeletePayload): Promise { - const { jobId, tableId, workspaceId, filter, excludeRowIds, cutoff } = payload + const { jobId, tableId, workspaceId, filter, excludeRowIds, cutoff, maxRows } = payload const requestId = generateId().slice(0, 8) + const budget = maxRows ?? Number.POSITIVE_INFINITY try { const table = await getTableById(tableId, { includeArchived: true }) @@ -74,7 +81,7 @@ export async function runTableDelete(payload: TableDeletePayload): Promise let lastReported = resumed let afterId: string | undefined - while (true) { + while (processed < budget) { // Ownership gate before every page: once this run loses the table (cancel/supersede), // updateJobProgress returns false and we stop before deleting further. const owns = await updateJobProgress(tableId, processed, jobId) @@ -86,7 +93,7 @@ export async function runTableDelete(payload: TableDeletePayload): Promise cutoff, filterClause, afterId, - limit: TABLE_LIMITS.DELETE_PAGE_SIZE, + limit: Math.min(TABLE_LIMITS.DELETE_PAGE_SIZE, budget - processed), }) if (page.length === 0) break // Advance the keyset cursor past the whole page — excluded ids are skipped (not deleted), diff --git a/apps/sim/lib/table/events.ts b/apps/sim/lib/table/events.ts index 86a6f7ec09..8b8dc6da93 100644 --- a/apps/sim/lib/table/events.ts +++ b/apps/sim/lib/table/events.ts @@ -122,7 +122,7 @@ export type TableEvent = kind: 'job' tableId: string jobId: string - type: 'import' | 'delete' | 'export' | 'backfill' + type: 'import' | 'delete' | 'export' | 'backfill' | 'update' status: 'running' | 'ready' | 'failed' | 'canceled' /** Rows processed so far (running) or in total (ready). */ progress?: number diff --git a/apps/sim/lib/table/import-runner.test.ts b/apps/sim/lib/table/import-runner.test.ts new file mode 100644 index 0000000000..29b076a180 --- /dev/null +++ b/apps/sim/lib/table/import-runner.test.ts @@ -0,0 +1,117 @@ +/** + * @vitest-environment node + */ +import { Readable } from 'node:stream' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetTableById, + mockBulkInsertImportBatch, + mockUpdateJobProgress, + mockMarkJobReady, + mockMarkJobFailed, + mockNextImportStartPosition, + mockNextImportStartOrderKey, + mockAppendTableEvent, + mockDeleteFile, + mockDownloadFileStream, + mockHeadObject, +} = vi.hoisted(() => ({ + mockGetTableById: vi.fn(), + mockBulkInsertImportBatch: vi.fn(), + mockUpdateJobProgress: vi.fn(), + mockMarkJobReady: vi.fn(), + mockMarkJobFailed: vi.fn(), + mockNextImportStartPosition: vi.fn(), + mockNextImportStartOrderKey: vi.fn(), + mockAppendTableEvent: vi.fn(), + mockDeleteFile: vi.fn(), + mockDownloadFileStream: vi.fn(), + mockHeadObject: vi.fn(), +})) + +vi.mock('@/lib/table/service', () => ({ + getTableById: mockGetTableById, +})) +vi.mock('@/lib/table/import-data', () => ({ + addImportColumns: vi.fn(), + bulkInsertImportBatch: mockBulkInsertImportBatch, + deleteAllTableRows: vi.fn(), + setTableSchemaForImport: vi.fn(), +})) +vi.mock('@/lib/table/jobs/service', () => ({ + markJobFailed: mockMarkJobFailed, + markJobReady: mockMarkJobReady, + updateJobProgress: mockUpdateJobProgress, +})) +vi.mock('@/lib/table/rows/ordering', () => ({ + nextImportStartOrderKey: mockNextImportStartOrderKey, + nextImportStartPosition: mockNextImportStartPosition, +})) +vi.mock('@/lib/table/events', () => ({ appendTableEvent: mockAppendTableEvent })) +vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() })) +vi.mock('@/lib/uploads/core/storage-service', () => ({ + deleteFile: mockDeleteFile, + downloadFileStream: mockDownloadFileStream, + headObject: mockHeadObject, +})) +vi.mock('@/app/api/table/utils', () => ({ + normalizeColumn: (col: unknown) => col, +})) + +import { runTableImport, type TableImportPayload } from '@/lib/table/import-runner' + +const table = { + id: 'tbl_1', + name: 'People', + workspaceId: 'ws_1', + rowCount: 0, + maxRows: 1000, + schema: { columns: [{ id: 'col_name', name: 'name', type: 'string' }] }, +} + +function buildPayload(overrides: Partial = {}): TableImportPayload { + return { + importId: 'job_1', + tableId: 'tbl_1', + workspaceId: 'ws_1', + userId: 'user_1', + fileKey: 'workspace/ws_1/people.csv', + fileName: 'people.csv', + delimiter: ',', + mode: 'append', + ...overrides, + } +} + +describe('runTableImport source-file cleanup', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetTableById.mockResolvedValue(table) + mockHeadObject.mockResolvedValue({ size: 20 }) + mockDownloadFileStream.mockResolvedValue(Readable.from('name\nAlice\nBob\n')) + mockNextImportStartPosition.mockResolvedValue(0) + mockNextImportStartOrderKey.mockResolvedValue(null) + mockUpdateJobProgress.mockResolvedValue(true) + mockBulkInsertImportBatch.mockResolvedValue({ inserted: 2, lastOrderKey: 'a1' }) + mockMarkJobReady.mockResolvedValue(true) + mockDeleteFile.mockResolvedValue(undefined) + }) + + it('deletes the single-use source object by default', async () => { + await runTableImport(buildPayload()) + + expect(mockMarkJobReady).toHaveBeenCalled() + expect(mockDeleteFile).toHaveBeenCalledWith({ + key: 'workspace/ws_1/people.csv', + context: 'workspace', + }) + }) + + it('keeps a persistent workspace file when deleteSourceFile is false', async () => { + await runTableImport(buildPayload({ deleteSourceFile: false })) + + expect(mockMarkJobReady).toHaveBeenCalled() + expect(mockDeleteFile).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/table/import-runner.ts b/apps/sim/lib/table/import-runner.ts index 5391d22a23..f9d2b3f503 100644 --- a/apps/sim/lib/table/import-runner.ts +++ b/apps/sim/lib/table/import-runner.ts @@ -60,6 +60,13 @@ export interface TableImportPayload { mapping?: CsvHeaderMapping /** (append/replace) CSV headers to auto-create as new columns (types inferred from the sample). */ createColumns?: string[] + /** + * Whether the source object is deleted once the import is terminal. Defaults + * to true (the UI routes upload a single-use temp object per import); pass + * false when importing a persistent workspace file (Mothership) that must + * survive the import. + */ + deleteSourceFile?: boolean } /** @@ -350,9 +357,12 @@ export async function runTableImport(payload: TableImportPayload): Promise // Release the storage stream so its HTTP connection doesn't leak on failure. source?.destroy() // The uploaded source file is single-use (a fresh upload per import) — delete it once the - // import is terminal so the workspace bucket doesn't accumulate. Best-effort. - await deleteFile({ key: fileKey, context: 'workspace' }).catch((err) => { - logger.warn(`[${requestId}] Failed to delete imported file`, { fileKey, err }) - }) + // import is terminal so the workspace bucket doesn't accumulate. Best-effort. Skipped for + // persistent workspace files (deleteSourceFile: false). + if (payload.deleteSourceFile !== false) { + await deleteFile({ key: fileKey, context: 'workspace' }).catch((err) => { + logger.warn(`[${requestId}] Failed to delete imported file`, { fileKey, err }) + }) + } } } diff --git a/apps/sim/lib/table/rows/ordering.ts b/apps/sim/lib/table/rows/ordering.ts index ccddfd51d3..7646f8c0a1 100644 --- a/apps/sim/lib/table/rows/ordering.ts +++ b/apps/sim/lib/table/rows/ordering.ts @@ -528,6 +528,51 @@ export async function selectRowIdPage(params: { return rows.map((r) => r.id) } +/** + * Like {@link selectRowIdPage} but returns each row's `data` too, for the bulk-update worker which + * must merge the patch into the existing row to validate the result. Same keyset walk on the + * `(table_id, id)` index, `created_at <= cutoff`, tenant-scoped, seqscan-off for jsonb filters. + * + * `excludeIfPatched` (a JSON patch string) skips rows that already contain the patch + * (`data @> patch`). The update worker passes it so a retried run doesn't re-walk and re-count + * rows an earlier attempt already updated — updated rows still exist (unlike deletes), and they + * still match the filter when the patch doesn't touch a filtered column, so without this a retry + * would double-count progress. It also skips no-op updates of rows that already hold those values. + */ +export async function selectRowDataPage(params: { + tableId: string + workspaceId: string + cutoff: Date + filterClause?: SQL + afterId?: string + limit: number + excludeIfPatched?: string +}): Promise> { + const { tableId, workspaceId, cutoff, filterClause, afterId, limit, excludeIfPatched } = params + const selectPage = (executor: DbExecutor) => + executor + .select({ id: userTableRows.id, data: userTableRows.data }) + .from(userTableRows) + .where( + and( + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId), + lte(userTableRows.createdAt, cutoff), + afterId ? gt(userTableRows.id, afterId) : undefined, + excludeIfPatched + ? sql`NOT (${userTableRows.data} @> ${excludeIfPatched}::jsonb)` + : undefined, + filterClause + ) + ) + .orderBy(asc(userTableRows.id)) + .limit(limit) + const rows = filterClause + ? await withSeqscanOff(async (trx) => selectPage(trx)) + : await selectPage(db) + return rows.map((r) => ({ id: r.id, data: r.data as RowData })) +} + /** * Deletes one page of rows for the async delete-job worker, committing each `DELETE_BATCH_SIZE` * chunk in its own short transaction. One statement per transaction bounds how long the @@ -563,3 +608,37 @@ export async function deletePageByIds( } return deleted } + +/** + * Applies a JSONB-merge patch (`data || patchJson`) to a page of row ids, committed in + * UPDATE_BATCH_SIZE chunks (each its own transaction, 60s timeout) so a large background update + * makes incremental, resumable progress. Returns the number of rows updated. + */ +export async function updatePageByIds( + tableId: string, + workspaceId: string, + rowIds: string[], + patchJson: string +): Promise { + const now = new Date() + let updated = 0 + for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) { + const batch = rowIds.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE) + const rows = await db.transaction(async (trx) => { + await setTableTxTimeouts(trx, { statementMs: 60_000 }) + return trx + .update(userTableRows) + .set({ data: sql`${userTableRows.data} || ${patchJson}::jsonb`, updatedAt: now }) + .where( + and( + eq(userTableRows.tableId, tableId), + eq(userTableRows.workspaceId, workspaceId), + inArray(userTableRows.id, batch) + ) + ) + .returning({ id: userTableRows.id }) + }) + updated += rows.length + } + return updated +} diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index c676b17813..7355985101 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -855,6 +855,11 @@ export async function pendingDeleteMask(table: TableDefinition): Promise 0) { try { diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index cf57842617..badd3356f7 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -199,7 +199,7 @@ export type TableJobStatus = 'running' | 'ready' | 'failed' | 'canceled' * mutate row data and share the single-running-job gate; `export` is read-only and bypasses it * (the partial-unique index excludes it), so an export can run alongside any other job. */ -export type TableJobType = 'import' | 'delete' | 'export' | 'backfill' +export type TableJobType = 'import' | 'delete' | 'export' | 'backfill' | 'update' /** * Persisted scope of a running delete job (`table_jobs.payload`). Defines the doomed row set — @@ -213,8 +213,34 @@ export interface TableDeleteJobPayload { /** ISO timestamp; rows created after it are spared. */ cutoff: string /** Doomed-row estimate captured at kickoff — display-only: list/detail counts subtract the - * not-yet-deleted remainder (doomedCount - rows_processed) while the job runs. */ + * not-yet-deleted remainder (doomedCount - rows_processed) while the job runs. Set only for an + * unbounded delete (the masked "delete everything matching" path); omitted when `maxRows` is set. */ doomedCount?: number + /** + * Stop after deleting this many rows (an explicit caller-supplied limit above the inline cap). + * Omitted = delete every match. When set, reads are NOT masked: the delete is eventually + * consistent (rows disappear as they're deleted) like a bounded update, because the filter-based + * mask would over-hide the rows beyond the cap that this job never deletes. + */ + maxRows?: number +} + +/** + * Persisted scope of a running bulk-update job (`table_jobs.payload`): the same `data` patch is + * merged into every row matching `filter` with `created_at <= cutoff` (so mid-job inserts are + * spared, matching the delete job's snapshot semantics). `affectedCount` is the kickoff estimate, + * display-only. Unlike delete, reads are not masked — updated rows still exist, so a background + * update is eventually consistent (readers may see a mix of patched/unpatched rows mid-job). + */ +export interface TableUpdateJobPayload { + filter: Filter + /** Column-id-keyed partial patch applied to every matched row (JSONB merge). */ + data: RowData + /** ISO timestamp; rows created after it are not patched. */ + cutoff: string + affectedCount?: number + /** Stop after updating this many rows (an explicit caller-supplied limit). Omitted = every match. */ + maxRows?: number } /** diff --git a/apps/sim/lib/table/update-runner.test.ts b/apps/sim/lib/table/update-runner.test.ts new file mode 100644 index 0000000000..d220effe0f --- /dev/null +++ b/apps/sim/lib/table/update-runner.test.ts @@ -0,0 +1,219 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetTableById, + mockGetJobProgress, + mockSelectRowDataPage, + mockUpdatePageByIds, + mockUpdateJobProgress, + mockMarkJobReady, + mockMarkJobFailed, + mockAppendTableEvent, + mockBuildFilterClause, + mockValidateRowSize, + mockCoerceRowToSchema, + mockCoerceRowValues, +} = vi.hoisted(() => ({ + mockGetTableById: vi.fn(), + mockGetJobProgress: vi.fn(), + mockSelectRowDataPage: vi.fn(), + mockUpdatePageByIds: vi.fn(), + mockUpdateJobProgress: vi.fn(), + mockMarkJobReady: vi.fn(), + mockMarkJobFailed: vi.fn(), + mockAppendTableEvent: vi.fn(), + mockBuildFilterClause: vi.fn(), + mockValidateRowSize: vi.fn(), + mockCoerceRowToSchema: vi.fn(), + mockCoerceRowValues: vi.fn(), +})) + +vi.mock('@/lib/table/service', () => ({ getTableById: mockGetTableById })) +vi.mock('@/lib/table/jobs/service', () => ({ + getJobProgress: mockGetJobProgress, + updateJobProgress: mockUpdateJobProgress, + markJobReady: mockMarkJobReady, + markJobFailed: mockMarkJobFailed, +})) +vi.mock('@/lib/table/rows/ordering', () => ({ + selectRowDataPage: mockSelectRowDataPage, + updatePageByIds: mockUpdatePageByIds, +})) +vi.mock('@/lib/table/events', () => ({ appendTableEvent: mockAppendTableEvent })) +vi.mock('@/lib/table/sql', () => ({ buildFilterClause: mockBuildFilterClause })) +vi.mock('@/lib/table/validation', () => ({ + validateRowSize: mockValidateRowSize, + coerceRowToSchema: mockCoerceRowToSchema, + coerceRowValues: mockCoerceRowValues, +})) +vi.mock('@/lib/table/constants', () => ({ + TABLE_LIMITS: { DELETE_PAGE_SIZE: 2, UPDATE_BATCH_SIZE: 100 }, + USER_TABLE_ROWS_SQL_NAME: 'user_table_rows', +})) + +import { markTableUpdateFailed, runTableUpdate } from '@/lib/table/update-runner' + +const table = { id: 'tbl_1', workspaceId: 'ws_1', schema: { columns: [] } } +const cutoff = new Date('2026-06-05T00:00:00Z') + +function basePayload(overrides = {}) { + return { + jobId: 'job_1', + tableId: 'tbl_1', + workspaceId: 'ws_1', + filter: { status: 'old' }, + data: { flag: true }, + cutoff, + ...overrides, + } +} +const row = (id: string) => ({ id, data: {} }) + +describe('runTableUpdate', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetTableById.mockResolvedValue(table) + mockGetJobProgress.mockResolvedValue(0) + mockUpdateJobProgress.mockResolvedValue(true) + mockMarkJobReady.mockResolvedValue(true) + mockMarkJobFailed.mockResolvedValue(undefined) + mockUpdatePageByIds.mockImplementation((_t, _w, ids: string[]) => Promise.resolve(ids.length)) + mockBuildFilterClause.mockReturnValue({}) + mockValidateRowSize.mockReturnValue({ valid: true, errors: [] }) + mockCoerceRowToSchema.mockReturnValue({ valid: true, errors: [] }) + }) + + it('updates every matching page then marks the job ready', async () => { + mockSelectRowDataPage + .mockResolvedValueOnce([row('a'), row('b')]) + .mockResolvedValueOnce([row('c')]) + .mockResolvedValueOnce([]) + + await runTableUpdate(basePayload()) + + expect(mockUpdatePageByIds).toHaveBeenNthCalledWith( + 1, + 'tbl_1', + 'ws_1', + ['a', 'b'], + expect.any(String) + ) + expect(mockUpdatePageByIds).toHaveBeenNthCalledWith( + 2, + 'tbl_1', + 'ws_1', + ['c'], + expect.any(String) + ) + expect(mockMarkJobReady).toHaveBeenCalledWith('tbl_1', 'job_1') + expect(mockAppendTableEvent).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'job', type: 'update', status: 'ready', progress: 3 }) + ) + }) + + it('fails (rethrows) when a merged row is invalid, without writing that page', async () => { + mockSelectRowDataPage.mockResolvedValueOnce([row('a')]) + mockValidateRowSize.mockReturnValueOnce({ valid: false, errors: ['row too large'] }) + + await expect(runTableUpdate(basePayload())).rejects.toThrow(/Row a: row too large/) + expect(mockUpdatePageByIds).not.toHaveBeenCalled() + expect(mockMarkJobFailed).not.toHaveBeenCalled() // caller decides via markTableUpdateFailed + }) + + it('stops without marking ready when the ownership gate is lost', async () => { + mockSelectRowDataPage.mockResolvedValue([row('a'), row('b')]) + mockUpdateJobProgress.mockResolvedValueOnce(true).mockResolvedValueOnce(false) + + await runTableUpdate(basePayload()) + + expect(mockUpdatePageByIds).toHaveBeenCalledTimes(1) + expect(mockMarkJobReady).not.toHaveBeenCalled() + }) + + it('rethrows the root cause so the clean message survives serialization', async () => { + const cause = new Error('canceling statement due to statement timeout') + mockSelectRowDataPage.mockRejectedValue(new Error('Failed query: update ...', { cause })) + + await expect(runTableUpdate(basePayload())).rejects.toThrow( + 'canceling statement due to statement timeout' + ) + expect(mockMarkJobFailed).not.toHaveBeenCalled() + }) + + it('resumes cumulative progress on retry instead of resetting to zero', async () => { + mockGetJobProgress.mockResolvedValue(7) + mockSelectRowDataPage.mockResolvedValueOnce([row('a'), row('b')]).mockResolvedValueOnce([]) + + await runTableUpdate(basePayload()) + + expect(mockUpdateJobProgress).toHaveBeenNthCalledWith(1, 'tbl_1', 7, 'job_1') + expect(mockAppendTableEvent).toHaveBeenCalledWith( + expect.objectContaining({ status: 'ready', progress: 9 }) + ) + }) + + it('stops at the seed read when the job is no longer owned', async () => { + mockGetJobProgress.mockResolvedValue(null) + + await expect(runTableUpdate(basePayload())).resolves.toBeUndefined() + expect(mockSelectRowDataPage).not.toHaveBeenCalled() + expect(mockUpdatePageByIds).not.toHaveBeenCalled() + }) + + it('stops once maxRows is reached and never over-fetches a page', async () => { + // budget 3 with page size 2: first page fills 2, second page is capped to the remaining 1. + mockSelectRowDataPage + .mockResolvedValueOnce([row('a'), row('b')]) + .mockResolvedValueOnce([row('c')]) + + await runTableUpdate(basePayload({ maxRows: 3 })) + + expect(mockSelectRowDataPage).toHaveBeenCalledTimes(2) + expect(mockSelectRowDataPage.mock.calls[0][0]).toMatchObject({ limit: 2 }) + expect(mockSelectRowDataPage.mock.calls[1][0]).toMatchObject({ limit: 1 }) + expect(mockUpdatePageByIds).toHaveBeenCalledTimes(2) + expect(mockAppendTableEvent).toHaveBeenCalledWith( + expect.objectContaining({ status: 'ready', progress: 3 }) + ) + }) + + it('passes the cutoff and filter clause through to the page query', async () => { + mockSelectRowDataPage.mockResolvedValueOnce([]) + + await runTableUpdate(basePayload()) + + expect(mockBuildFilterClause).toHaveBeenCalledWith( + { status: 'old' }, + 'user_table_rows', + table.schema.columns + ) + expect(mockSelectRowDataPage).toHaveBeenCalledWith( + expect.objectContaining({ + cutoff, + filterClause: {}, + limit: 2, + // Already-patched rows are excluded so a retry doesn't re-walk/double-count. + excludeIfPatched: JSON.stringify({ flag: true }), + }) + ) + }) +}) + +describe('markTableUpdateFailed', () => { + beforeEach(() => { + vi.clearAllMocks() + mockMarkJobFailed.mockResolvedValue(undefined) + }) + + it('marks the job failed and emits the failed event', async () => { + await markTableUpdateFailed('tbl_1', 'job_1', new Error('boom')) + + expect(mockMarkJobFailed).toHaveBeenCalledWith('tbl_1', 'job_1', 'boom') + expect(mockAppendTableEvent).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'job', type: 'update', status: 'failed', error: 'boom' }) + ) + }) +}) diff --git a/apps/sim/lib/table/update-runner.ts b/apps/sim/lib/table/update-runner.ts new file mode 100644 index 0000000000..194746830c --- /dev/null +++ b/apps/sim/lib/table/update-runner.ts @@ -0,0 +1,196 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { truncate } from '@sim/utils/string' +import type { Filter, RowData } from '@/lib/table' +import { TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' +import { appendTableEvent } from '@/lib/table/events' +import { + getJobProgress, + markJobFailed, + markJobReady, + updateJobProgress, +} from '@/lib/table/jobs/service' +import { selectRowDataPage, updatePageByIds } from '@/lib/table/rows/ordering' +import { getTableById } from '@/lib/table/service' +import { buildFilterClause } from '@/lib/table/sql' +import { coerceRowToSchema, coerceRowValues, validateRowSize } from '@/lib/table/validation' + +const logger = createLogger('TableUpdateRunner') + +/** Emit a progress event / heartbeat at most every this many rows. */ +const PROGRESS_INTERVAL_ROWS = 5000 + +/** + * Thrown when this worker discovers it no longer owns the table's job (canceled, or the + * stale-job janitor marked it failed and a newer job took over). The worker stops updating. + */ +class JobSupersededError extends Error {} + +export interface TableUpdatePayload { + jobId: string + tableId: string + workspaceId: string + /** Rows matching this filter get the patch. */ + filter: Filter + /** Column-id-keyed partial patch merged into every matched row. */ + data: RowData + /** Only rows created at/before this instant are patched, so mid-job inserts are spared. */ + cutoff: Date + /** Stop after updating this many rows (an explicit caller-supplied limit). Omitted = every match. */ + maxRows?: number +} + +/** + * Background worker for large filtered row updates (trigger.dev task, or detached on the web + * container when trigger.dev is disabled — see the update dispatch in the user_table tool). + * Applies the same `data` patch (JSONB merge) to every row matching `filter` with + * `created_at <= cutoff`, in keyset-paginated pages. Each page validates the merged result per + * row, then commits in batches — **best-effort, not atomic**: committed pages persist even if a + * later page fails validation (unlike the inline `updateRowsByFilter`, which pre-validates all + * rows in one transaction). Reads are not masked: updated rows still exist, so mid-job reads are + * eventually consistent. Ownership-gated per page so a cancel/supersede stops within one page. + * + * Unlike the inline path, the worker does NOT fire per-row table triggers or auto-recompute + * workflow/enrichment columns — that would be a runaway cascade across thousands of rows. Run + * the affected columns explicitly afterward if downstream recompute is needed. + * + * Unexpected errors are rethrown for the caller's retry machinery; the caller marks the job + * failed via `markTableUpdateFailed`. A superseded run returns quietly. + */ +export async function runTableUpdate(payload: TableUpdatePayload): Promise { + const { jobId, tableId, workspaceId, filter, data, cutoff, maxRows } = payload + const requestId = generateId().slice(0, 8) + const budget = maxRows ?? Number.POSITIVE_INFINITY + + try { + const table = await getTableById(tableId, { includeArchived: true }) + if (!table) throw new Error(`Update target table ${tableId} not found`) + + const filterClause = buildFilterClause(filter, USER_TABLE_ROWS_SQL_NAME, table.schema.columns) + if (!filterClause) throw new Error('Filter is required for bulk update') + + // Coerce the patch once to the schema's types — the merged validation below and the persisted + // JSONB merge both use this normalized copy. + coerceRowValues(data, table.schema) + const patchJson = JSON.stringify(data) + + // Resume the persisted count: a retried attempt's earlier pages are already committed, so + // starting at zero would overwrite cumulative progress. Doubles as the initial ownership gate. + const resumed = await getJobProgress(tableId, jobId) + if (resumed === null) throw new JobSupersededError() + + let processed = resumed + let lastReported = resumed + let afterId: string | undefined + + while (processed < budget) { + const owns = await updateJobProgress(tableId, processed, jobId) + if (!owns) throw new JobSupersededError() + + const page = await selectRowDataPage({ + tableId, + workspaceId, + cutoff, + filterClause, + afterId, + limit: Math.min(TABLE_LIMITS.DELETE_PAGE_SIZE, budget - processed), + // Skip rows already carrying the patch so a retried run resumes without re-walking / + // double-counting the rows an earlier attempt updated (updated rows still exist and may + // still match the filter, unlike deletes). + excludeIfPatched: patchJson, + }) + if (page.length === 0) break + afterId = page[page.length - 1].id + + // Validate each merged result before writing the page — a row that would overflow the size + // cap or violate the schema fails the job (earlier pages stay applied; best-effort). + for (const row of page) { + const merged = { ...row.data, ...data } + const sizeValidation = validateRowSize(merged) + if (!sizeValidation.valid) { + throw new Error(`Row ${row.id}: ${sizeValidation.errors.join(', ')}`) + } + const schemaValidation = coerceRowToSchema(merged, table.schema) + if (!schemaValidation.valid) { + throw new Error(`Row ${row.id}: ${schemaValidation.errors.join(', ')}`) + } + } + + processed += await updatePageByIds( + tableId, + workspaceId, + page.map((r) => r.id), + patchJson + ) + + if ( + processed - lastReported >= PROGRESS_INTERVAL_ROWS || + (lastReported === 0 && processed > 0) + ) { + lastReported = processed + void appendTableEvent({ + kind: 'job', + type: 'update', + tableId, + jobId, + status: 'running', + progress: processed, + }) + } + } + + await updateJobProgress(tableId, processed, jobId) + const becameReady = await markJobReady(tableId, jobId) + if (becameReady) { + void appendTableEvent({ + kind: 'job', + type: 'update', + tableId, + jobId, + status: 'ready', + progress: processed, + }) + logger.info(`[${requestId}] Update complete`, { tableId, rows: processed }) + } else { + logger.info( + `[${requestId}] Update finished but no longer owns the run (canceled/superseded)`, + { + tableId, + jobId, + } + ) + } + } catch (err) { + if (err instanceof JobSupersededError) { + logger.info(`[${requestId}] Update superseded by a newer run; stopping`, { tableId, jobId }) + return + } + const cause = toError(err).cause + const error = cause ? toError(cause) : toError(err) + logger.error(`[${requestId}] Update failed for table ${tableId}:`, error) + throw error + } +} + +/** + * Marks the update job failed and emits the failed SSE event. Called once the caller gives up on + * the run (trigger.dev `onFailure` after retries, or the detached fallback). Scoped to jobId — a + * no-op if a newer job has taken over. + */ +export async function markTableUpdateFailed( + tableId: string, + jobId: string, + error: unknown +): Promise { + const message = truncate(getErrorMessage(toError(error).cause ?? error, 'Update failed'), 500) + await markJobFailed(tableId, jobId, message).catch(() => {}) + void appendTableEvent({ + kind: 'job', + type: 'update', + tableId, + jobId, + status: 'failed', + error: message, + }) +} From 63a3e6d2cbe0958968a72289abb385888f6a6c32 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 18:54:18 -0700 Subject: [PATCH 24/26] feat(files): stream large CSV previews and add import-as-table (#5125) * feat(files): stream large CSV previews and add import-as-table * fix(files): validate fileId in csv-preview route, guard double-import, fix sniff perf and toggle flash * fix(files): scope mothership preview-toggle loading guard to CSV files only --- apps/sim/app/api/table/import-async/route.ts | 3 +- .../[id]/files/[fileId]/csv-preview/route.ts | 56 +++++++ .../components/file-viewer/csv-import.ts | 68 +++++++++ .../file-viewer/csv-table-preview.tsx | 49 ++++++ .../components/file-viewer/file-viewer.tsx | 30 ++++ .../files/components/file-viewer/index.ts | 2 +- .../components/file-viewer/preview-panel.tsx | 43 +++++- .../components/file-viewer/text-editor.tsx | 2 + .../workspace/[workspaceId]/files/files.tsx | 8 +- .../mothership-view/mothership-view.tsx | 21 ++- apps/sim/hooks/queries/tables.ts | 32 ++++ .../sim/hooks/queries/workspace-file-table.ts | 50 +++++++ apps/sim/lib/api/contracts/tables.ts | 6 + .../lib/api/contracts/workspace-file-table.ts | 48 ++++++ .../file-parsers/csv-preview-slice.test.ts | 103 +++++++++++++ .../sim/lib/file-parsers/csv-preview-slice.ts | 140 ++++++++++++++++++ scripts/check-api-validation-contracts.ts | 4 +- 17 files changed, 650 insertions(+), 15 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/files/[fileId]/csv-preview/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-table-preview.tsx create mode 100644 apps/sim/hooks/queries/workspace-file-table.ts create mode 100644 apps/sim/lib/api/contracts/workspace-file-table.ts create mode 100644 apps/sim/lib/file-parsers/csv-preview-slice.test.ts create mode 100644 apps/sim/lib/file-parsers/csv-preview-slice.ts diff --git a/apps/sim/app/api/table/import-async/route.ts b/apps/sim/app/api/table/import-async/route.ts index 0d5b6a418a..15b7f3e3ea 100644 --- a/apps/sim/app/api/table/import-async/route.ts +++ b/apps/sim/app/api/table/import-async/route.ts @@ -38,7 +38,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const parsed = await parseRequest(importTableAsyncContract, request, {}) if (!parsed.success) return parsed.response - const { workspaceId, fileKey, fileName } = parsed.data.body + const { workspaceId, fileKey, fileName, deleteSourceFile } = parsed.data.body const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'write' && permission !== 'admin') { @@ -111,6 +111,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { fileName, delimiter, mode: 'create', + deleteSourceFile, } if (isTriggerDevEnabled) { // Trigger.dev runs the import outside the web container, so it survives app deploys. diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/csv-preview/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/csv-preview/route.ts new file mode 100644 index 0000000000..0fd9553a4c --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/csv-preview/route.ts @@ -0,0 +1,56 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { getWorkspaceCsvPreviewContract } from '@/lib/api/contracts/workspace-file-table' +import { parseRequest } from '@/lib/api/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCsvPreviewSlice } from '@/lib/file-parsers/csv-preview-slice' +import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceCsvPreviewAPI') + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; fileId: string }> }) => { + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + const userId = authResult.userId + + const parsed = await parseRequest(getWorkspaceCsvPreviewContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, fileId } = parsed.data.params + const { key } = parsed.data.query + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission) { + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + + // Resolve the file record (active, in this workspace) and read from its authoritative key — + // never the client-supplied one. This rejects archived/deleted files and keys with no live + // row, matching the access guarantees of /api/files/serve. + const record = await getWorkspaceFile(workspaceId, fileId) + if (!record || record.key !== key) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + const slice = await getCsvPreviewSlice({ + key: record.key, + context: 'workspace', + signal: request.signal, + }) + + logger.info('CSV preview served', { + workspaceId, + rows: slice.rows.length, + truncated: slice.truncated, + }) + + return NextResponse.json({ success: true, ...slice }) + } +) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts new file mode 100644 index 0000000000..d852820bfc --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-import.ts @@ -0,0 +1,68 @@ +'use client' + +import { useCallback, useEffect, useRef } from 'react' +import { generateId } from '@sim/utils/id' +import { useRouter } from 'next/navigation' +import { toast } from '@/components/emcn' +import { CSV_PREVIEW_MAX_ROWS } from '@/lib/api/contracts/workspace-file-table' +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { useImportFileAsTable } from '@/hooks/queries/tables' +import { useImportTrayStore } from '@/stores/table/import-tray/store' + +export type CsvImportFileDescriptor = Pick + +/** + * Wires the "Import as a table" affordance for a capped CSV preview. When the preview is + * `truncated`, raises a one-time warning toast whose action kicks off a background import of the + * existing workspace file — no re-upload, source preserved — and navigates to the new table. + */ +export function useCsvTruncationImport( + workspaceId: string, + file: CsvImportFileDescriptor, + truncated: boolean +) { + const router = useRouter() + const importFile = useImportFileAsTable() + + // Guards against a double-tap on the toast action kicking off two parallel imports of the same + // file. Reset once the kickoff settles so a failed import can be retried. + const importingRef = useRef(false) + + const importAsTable = useCallback(() => { + if (importingRef.current) return + importingRef.current = true + const pendingId = `pending_${generateId()}` + useImportTrayStore + .getState() + .startUpload({ uploadId: pendingId, workspaceId, title: file.name }) + toast.success(`Importing "${file.name}" as a table`, { + description: 'This runs in the background.', + action: { + label: 'View tables', + onClick: () => router.push(`/workspace/${workspaceId}/tables`), + }, + }) + importFile.mutate( + { workspaceId, fileKey: file.key, fileName: file.name }, + { + onSettled: () => { + importingRef.current = false + useImportTrayStore.getState().endUpload(pendingId) + }, + } + ) + // importFile.mutate and router are stable references + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workspaceId, file.key, file.name]) + + // Surface the cap as a warning toast with an import action, once per file. + const notifiedKeyRef = useRef(null) + useEffect(() => { + if (!truncated || notifiedKeyRef.current === file.key) return + notifiedKeyRef.current = file.key + toast.warning(`Showing the first ${CSV_PREVIEW_MAX_ROWS.toLocaleString()} rows`, { + description: 'Import this file as a table to view all of its rows.', + action: { label: 'Import as a table', onClick: importAsTable }, + }) + }, [truncated, file.key, importAsTable]) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-table-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-table-preview.tsx new file mode 100644 index 0000000000..6b39c4eadb --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/csv-table-preview.tsx @@ -0,0 +1,49 @@ +'use client' + +import { memo } from 'react' +import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { useWorkspaceCsvPreview } from '@/hooks/queries/workspace-file-table' +import { useCsvTruncationImport } from './csv-import' +import { DataTable } from './data-table' +import { PreviewError, PreviewLoadingFrame, resolvePreviewError } from './preview-shared' + +/** + * Read-only preview for a CSV that is too large to load fully into the editor. Streams only the + * first {@link CSV_PREVIEW_MAX_ROWS} rows from storage; when there are more, a warning toast offers + * "Import as a table", which builds a full Table from the file (memory-safe streaming import). + */ +export const CsvTablePreview = memo(function CsvTablePreview({ + file, + workspaceId, +}: { + file: WorkspaceFileRecord + workspaceId: string +}) { + const version = Number(new Date(file.updatedAt)) || file.size + const { + data, + isLoading, + error: fetchError, + } = useWorkspaceCsvPreview(workspaceId, file.id, file.key, version) + useCsvTruncationImport(workspaceId, file, data?.truncated ?? false) + + const error = resolvePreviewError((fetchError as Error | null) ?? null, null) + if (error) return + if (isLoading || !data) { + return + } + + if (data.headers.length === 0) { + return ( +
+

No data to display

+
+ ) + } + + return ( +
+ +
+ ) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index f20d1762cc..d3c6fb21ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -13,6 +13,7 @@ import { useDocPreviewBinary } from './use-doc-preview-binary' export type { StreamingMode } from './text-editor-state' +import { CsvTablePreview } from './csv-table-preview' import { DocxPreview } from './docx-preview' import { ImagePreview } from './image-preview' import type { PdfDocumentSource } from './pdf-viewer' @@ -34,6 +35,13 @@ const PdfViewerCore = dynamic(() => import('./pdf-viewer').then((m) => m.PdfView const logger = createLogger('FileViewer') +/** + * CSVs at or below this size load fully into the editor (editable, with an inline preview). + * Larger CSVs would OOM the browser on `response.text()`, so they render a read-only, + * server-streamed preview of the first rows instead (see {@link CsvTablePreview}). + */ +const CSV_INLINE_EDIT_MAX_BYTES = 5 * 1024 * 1024 + export function isTextEditable(file: { type: string; name: string }): boolean { return resolveFileCategory(file.type, file.name) === 'text-editable' } @@ -42,6 +50,22 @@ export function isPreviewable(file: { type: string; name: string }): boolean { return resolvePreviewType(file.type, file.name) !== null } +/** + * A CSV larger than {@link CSV_INLINE_EDIT_MAX_BYTES} is shown as a streamed, read-only preview — + * the editor would OOM loading the whole file. The viewer renders {@link CsvTablePreview} for it, + * and toolbars use this to hide the edit/split/save controls (there is no editor to switch to). + */ +export function isCsvStreamOnly(file: { + type: string | null + name: string + size?: number | null +}): boolean { + return ( + resolvePreviewType(file.type, file.name) === 'csv' && + (file.size ?? 0) > CSV_INLINE_EDIT_MAX_BYTES + ) +} + export type PreviewMode = 'editor' | 'split' | 'preview' interface FileViewerProps { @@ -76,6 +100,12 @@ export function FileViewer({ const category = resolveFileCategory(file.type, file.name) if (category === 'text-editable') { + // A large CSV can't be loaded whole into the editor (the browser OOMs on the full text). + // Render a streamed, read-only preview of the first rows + an "Import as a table" path instead. + if (isCsvStreamOnly(file)) { + return + } + return ( void @@ -85,6 +89,8 @@ export const PreviewPanel = memo(function PreviewPanel({ content, mimeType, filename, + workspaceId, + fileKey, isStreaming, disableAutoScroll, onCheckboxToggle, @@ -101,7 +107,14 @@ export const PreviewPanel = memo(function PreviewPanel({ /> ) if (previewType === 'html') return - if (previewType === 'csv') return + if (previewType === 'csv') + return ( + + ) if (previewType === 'svg') return if (previewType === 'mermaid') return @@ -1150,8 +1163,17 @@ function MermaidFilePreview({ content, isStreaming }: { content: string; isStrea ) } -const CsvPreview = memo(function CsvPreview({ content }: { content: string }) { - const { headers, rows } = useMemo(() => parseCsv(content), [content]) +const CsvPreview = memo(function CsvPreview({ + content, + workspaceId, + file, +}: { + content: string + workspaceId: string + file: CsvImportFileDescriptor +}) { + const { headers, rows, truncated } = useMemo(() => parseCsv(content), [content]) + useCsvTruncationImport(workspaceId, file, truncated) if (headers.length === 0) { return ( @@ -1168,15 +1190,22 @@ const CsvPreview = memo(function CsvPreview({ content }: { content: string }) { ) }) -function parseCsv(text: string): { headers: string[]; rows: string[][] } { +/** + * Parses CSV text for the inline preview, capping at {@link CSV_PREVIEW_MAX_ROWS} rows so a + * small-but-many-rows file doesn't render thousands of ``s. Slices before parsing so only + * the capped rows are processed; `truncated` drives the "Import as a table" footer. + */ +function parseCsv(text: string): { headers: string[]; rows: string[][]; truncated: boolean } { const lines = text.split('\n').filter((line) => line.trim().length > 0) - if (lines.length === 0) return { headers: [], rows: [] } + if (lines.length === 0) return { headers: [], rows: [], truncated: false } const delimiter = detectDelimiter(lines[0]) const headers = parseCsvLine(lines[0], delimiter) - const rows = lines.slice(1).map((line) => parseCsvLine(line, delimiter)) + const dataLines = lines.slice(1) + const truncated = dataLines.length > CSV_PREVIEW_MAX_ROWS + const rows = dataLines.slice(0, CSV_PREVIEW_MAX_ROWS).map((line) => parseCsvLine(line, delimiter)) - return { headers, rows } + return { headers, rows, truncated } } function detectDelimiter(line: string): string { diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx index 8cdb1cce6f..60b1ec2bc8 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx @@ -755,6 +755,8 @@ export const TextEditor = memo(function TextEditor({ content={content} mimeType={file.type} filename={file.name} + workspaceId={workspaceId} + fileKey={file.key} isStreaming={isStreaming} disableAutoScroll={disableStreamingAutoScroll} onCheckboxToggle={canEdit && !isStreaming ? handleCheckboxToggle : undefined} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index b4ff6d2706..53e1ba66cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -65,6 +65,7 @@ import { FileRowContextMenu } from '@/app/workspace/[workspaceId]/files/componen import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { FileViewer, + isCsvStreamOnly, isPreviewable, isTextEditable, } from '@/app/workspace/[workspaceId]/files/components/file-viewer' @@ -1389,8 +1390,11 @@ export function Files() { const fileActions = useMemo(() => { if (!selectedFile) return [] - const canEditText = isTextEditable(selectedFile) - const canPreview = isPreviewable(selectedFile) + // A large CSV renders as a read-only streamed preview (no editor), so it gets neither the + // Save action nor the edit/split/preview toggle — just like a non-editable file. + const streamOnly = isCsvStreamOnly(selectedFile) + const canEditText = isTextEditable(selectedFile) && !streamOnly + const canPreview = isPreviewable(selectedFile) && !streamOnly const hasSplitView = canEditText && canPreview const saveLabel = diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index ae6857a104..ba4dce41d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -5,7 +5,10 @@ import type { FilePreviewSession } from '@/lib/copilot/request/session' import { cn } from '@/lib/core/utils/cn' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' -import { RICH_PREVIEWABLE_EXTENSIONS } from '@/app/workspace/[workspaceId]/files/components/file-viewer' +import { + isCsvStreamOnly, + RICH_PREVIEWABLE_EXTENSIONS, +} from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context' import { hasRenderableFilePreviewContent } from '@/app/workspace/[workspaceId]/home/hooks/preview' import type { @@ -13,6 +16,7 @@ import type { MothershipResource, } from '@/app/workspace/[workspaceId]/home/types' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' import { ResourceActions, ResourceContent, ResourceTabs } from './components' const PREVIEW_CYCLE: Record = { @@ -82,10 +86,23 @@ export const MothershipView = memo( setPreviewMode('preview') } + // A large CSV renders read-only (streamed) with no editor, so it must not offer the + // edit/split/preview toggle. Its size lives on the file record, not the resource tab. + const { data: files, isLoading: filesLoading } = useWorkspaceFiles(workspaceId, 'active', { + enabled: active?.type === 'file', + }) + const activeFile = active?.type === 'file' ? files?.find((f) => f.id === active.id) : undefined + const isActiveCsv = active?.type === 'file' && getFileExtension(active.title) === 'csv' + const isActivePreviewable = canEdit && active?.type === 'file' && - RICH_PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title)) + RICH_PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title)) && + // Only a CSV's previewability depends on its size (large = read-only, no editor). Wait for + // the record before deciding so the toggle doesn't flash on for a large CSV — but don't gate + // other rich types (markdown, html, svg, …) on the file list loading. + !(isActiveCsv && filesLoading) && + !(activeFile && isCsvStreamOnly(activeFile)) return (
{ + const response = await requestJson(importTableAsyncContract, { + body: { workspaceId, fileKey, fileName, deleteSourceFile: false }, + }) + return response.data + }, + onError: (error) => { + logger.error('Failed to start import from file:', error) + toast.error(error.message, { duration: 5000 }) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: tableKeys.lists() }) + }, + }) +} + export type CsvImportMode = 'append' | 'replace' interface ImportCsvIntoTableAsyncParams { diff --git a/apps/sim/hooks/queries/workspace-file-table.ts b/apps/sim/hooks/queries/workspace-file-table.ts new file mode 100644 index 0000000000..8538c97d26 --- /dev/null +++ b/apps/sim/hooks/queries/workspace-file-table.ts @@ -0,0 +1,50 @@ +import { useQuery } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import { + getWorkspaceCsvPreviewContract, + type WorkspaceCsvPreviewResponse, +} from '@/lib/api/contracts/workspace-file-table' + +/** + * Query keys for the streamed CSV file-viewer preview. `key` (storage object key) and + * `version` (the record's `updatedAt`) are folded in so a re-upload or edit busts the cache. + */ +export const workspaceFileTableKeys = { + all: ['workspaceFileTable'] as const, + previews: () => [...workspaceFileTableKeys.all, 'preview'] as const, + preview: (workspaceId: string, fileId: string, key: string, version?: number) => + [...workspaceFileTableKeys.previews(), workspaceId, fileId, key, version ?? ''] as const, +} + +async function fetchWorkspaceCsvPreview( + workspaceId: string, + fileId: string, + key: string, + version: number | undefined, + signal?: AbortSignal +): Promise { + return requestJson(getWorkspaceCsvPreviewContract, { + params: { id: workspaceId, fileId }, + query: version != null ? { key, v: version } : { key }, + signal, + }) +} + +/** + * Fetches the first {@link CSV_PREVIEW_MAX_ROWS} rows of a CSV via the streaming preview route. + * The server reads only that prefix from storage, so this is safe for arbitrarily large files. + */ +export function useWorkspaceCsvPreview( + workspaceId: string, + fileId: string, + key: string, + version?: number, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: workspaceFileTableKeys.preview(workspaceId, fileId, key, version), + queryFn: ({ signal }) => fetchWorkspaceCsvPreview(workspaceId, fileId, key, version, signal), + enabled: !!workspaceId && !!fileId && !!key && (options?.enabled ?? true), + staleTime: 30 * 1000, + }) +} diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index d5e4c7fa5c..9e89c84078 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -397,6 +397,12 @@ export const importTableAsyncBodySchema = z.object({ workspaceId: z.string().min(1, 'Workspace ID is required'), fileKey: z.string().min(1, 'fileKey is required'), fileName: z.string().min(1, 'fileName is required'), + /** + * Whether the source object is deleted once the import is terminal. Defaults to true (the upload + * flow stores a single-use temp object); pass false when importing an existing workspace file + * (e.g. the file viewer's "Import as a table") that must survive the import. + */ + deleteSourceFile: z.boolean().optional(), }) export type ImportTableAsyncBody = z.input diff --git a/apps/sim/lib/api/contracts/workspace-file-table.ts b/apps/sim/lib/api/contracts/workspace-file-table.ts new file mode 100644 index 0000000000..a6b14234a6 --- /dev/null +++ b/apps/sim/lib/api/contracts/workspace-file-table.ts @@ -0,0 +1,48 @@ +import { z } from 'zod' +import { workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { + type ContractJsonResponse, + type ContractParamsInput, + type ContractQueryInput, + defineRouteContract, +} from '@/lib/api/contracts/types' + +/** + * Maximum rows returned by the CSV file-viewer preview. The viewer streams only this + * many rows from storage; beyond it the user imports the file as a table to see the rest. + */ +export const CSV_PREVIEW_MAX_ROWS = 1_000 + +export const workspaceCsvPreviewParamsSchema = z.object({ + id: workspaceIdSchema, + fileId: z.string().min(1, 'File ID is required'), +}) + +export const workspaceCsvPreviewQuerySchema = z.object({ + /** Storage object key — drives the access check and busts the cache on re-upload. */ + key: z.string().min(1, 'File key is required'), + /** Content version (the file record's `updatedAt` epoch ms) — busts the cache on edit. */ + v: z.coerce.number().optional(), +}) + +export const workspaceCsvPreviewResponseSchema = z.object({ + success: z.literal(true), + headers: z.array(z.string()), + rows: z.array(z.array(z.string())), + /** True when the file has more than {@link CSV_PREVIEW_MAX_ROWS} data rows. */ + truncated: z.boolean(), +}) + +export const getWorkspaceCsvPreviewContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/files/[fileId]/csv-preview', + params: workspaceCsvPreviewParamsSchema, + query: workspaceCsvPreviewQuerySchema, + response: { mode: 'json', schema: workspaceCsvPreviewResponseSchema }, +}) + +export type WorkspaceCsvPreviewParams = ContractParamsInput +export type WorkspaceCsvPreviewQuery = ContractQueryInput +export type WorkspaceCsvPreviewResponse = ContractJsonResponse< + typeof getWorkspaceCsvPreviewContract +> diff --git a/apps/sim/lib/file-parsers/csv-preview-slice.test.ts b/apps/sim/lib/file-parsers/csv-preview-slice.test.ts new file mode 100644 index 0000000000..51d8574e46 --- /dev/null +++ b/apps/sim/lib/file-parsers/csv-preview-slice.test.ts @@ -0,0 +1,103 @@ +/** + * @vitest-environment node + */ +import { Readable } from 'node:stream' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDownloadFileStream } = vi.hoisted(() => ({ + mockDownloadFileStream: vi.fn(), +})) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ + downloadFileStream: mockDownloadFileStream, +})) + +import { CSV_PREVIEW_MAX_ROWS } from '@/lib/api/contracts/workspace-file-table' +import { getCsvPreviewSlice } from '@/lib/file-parsers/csv-preview-slice' + +function streamOf(text: string): Readable { + // Array-wrapped so the whole text is one chunk (a bare Buffer/string is iterated element-wise). + return Readable.from([Buffer.from(text, 'utf-8')]) +} + +const args = { key: 'workspace/ws_1/file.csv', context: 'workspace' as const } + +function csvWithRows(dataRows: number): string { + const lines = ['h1,h2'] + for (let i = 0; i < dataRows; i++) lines.push(`${i},x`) + return lines.join('\n') +} + +describe('getCsvPreviewSlice', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns headers and every row when under the cap', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf('a,b\n1,2\n3,4\n')) + const slice = await getCsvPreviewSlice(args) + expect(slice.headers).toEqual(['a', 'b']) + expect(slice.rows).toEqual([ + ['1', '2'], + ['3', '4'], + ]) + expect(slice.truncated).toBe(false) + }) + + it('caps at CSV_PREVIEW_MAX_ROWS and flags truncated', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf(csvWithRows(CSV_PREVIEW_MAX_ROWS + 500))) + const slice = await getCsvPreviewSlice(args) + expect(slice.rows).toHaveLength(CSV_PREVIEW_MAX_ROWS) + expect(slice.truncated).toBe(true) + }) + + it('is not truncated at exactly the cap', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf(csvWithRows(CSV_PREVIEW_MAX_ROWS))) + const slice = await getCsvPreviewSlice(args) + expect(slice.rows).toHaveLength(CSV_PREVIEW_MAX_ROWS) + expect(slice.truncated).toBe(false) + }) + + it('detects a semicolon delimiter', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf('a;b;c\n1;2;3\n')) + const slice = await getCsvPreviewSlice(args) + expect(slice.headers).toEqual(['a', 'b', 'c']) + expect(slice.rows).toEqual([['1', '2', '3']]) + }) + + it('detects a tab delimiter', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf('a\tb\n1\t2\n')) + const slice = await getCsvPreviewSlice(args) + expect(slice.headers).toEqual(['a', 'b']) + expect(slice.rows).toEqual([['1', '2']]) + }) + + it('returns empty for an empty file', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf('')) + const slice = await getCsvPreviewSlice(args) + expect(slice).toEqual({ headers: [], rows: [], truncated: false }) + }) + + it('tolerates ragged rows', async () => { + mockDownloadFileStream.mockResolvedValue(streamOf('a,b,c\n1,2\n4,5,6,7\n')) + const slice = await getCsvPreviewSlice(args) + expect(slice.headers).toEqual(['a', 'b', 'c']) + expect(slice.rows[0]).toEqual(['1', '2']) + }) + + it('truncates an oversized cell', async () => { + const big = 'x'.repeat(3000) + mockDownloadFileStream.mockResolvedValue(streamOf(`a\n${big}\n`)) + const slice = await getCsvPreviewSlice(args) + expect(slice.rows[0][0].length).toBeLessThan(3000) + }) + + it('destroys the source stream after reading the slice', async () => { + const source = streamOf(csvWithRows(CSV_PREVIEW_MAX_ROWS + 50)) + const destroySpy = vi.spyOn(source, 'destroy') + mockDownloadFileStream.mockResolvedValue(source) + const slice = await getCsvPreviewSlice(args) + expect(slice.truncated).toBe(true) + expect(destroySpy).toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/file-parsers/csv-preview-slice.ts b/apps/sim/lib/file-parsers/csv-preview-slice.ts new file mode 100644 index 0000000000..f705311995 --- /dev/null +++ b/apps/sim/lib/file-parsers/csv-preview-slice.ts @@ -0,0 +1,140 @@ +import { Readable } from 'node:stream' +import { truncate } from '@sim/utils/string' +import { parse as parseCsvStream } from 'csv-parse' +import { CSV_PREVIEW_MAX_ROWS } from '@/lib/api/contracts/workspace-file-table' +import type { StorageContext } from '@/lib/uploads/config' +import { downloadFileStream } from '@/lib/uploads/core/storage-service' + +/** Cap a single cell so one pathological field can't bloat the preview payload. */ +const MAX_CELL_LENGTH = 2_000 + +/** Read at most this many bytes while sniffing the first line for the delimiter. */ +const DELIMITER_SNIFF_MAX_BYTES = 256 * 1024 + +interface CsvPreviewSliceArgs { + key: string + context: StorageContext + signal?: AbortSignal +} + +export interface CsvPreviewSlice { + headers: string[] + rows: string[][] + /** True when the file has more than {@link CSV_PREVIEW_MAX_ROWS} data rows. */ + truncated: boolean +} + +/** + * Detects the CSV delimiter from a header line by frequency. Mirrors the file viewer's + * client-side heuristic (comma / tab / semicolon) so server-streamed previews match. + */ +function detectDelimiter(line: string): string { + const commaCount = (line.match(/,/g) || []).length + const tabCount = (line.match(/\t/g) || []).length + const semiCount = (line.match(/;/g) || []).length + if (tabCount > commaCount && tabCount > semiCount) return '\t' + if (semiCount > commaCount) return ';' + return ',' +} + +function cell(value: unknown): string { + return truncate(String(value ?? ''), MAX_CELL_LENGTH) +} + +/** + * Streams the first {@link CSV_PREVIEW_MAX_ROWS} rows of a CSV/TSV from storage without + * ever buffering the whole file. The source stream is destroyed as soon as enough rows are + * read (one past the cap, to detect truncation), so a multi-GB file costs O(rows) of memory. + */ +export async function getCsvPreviewSlice({ + key, + context, + signal, +}: CsvPreviewSliceArgs): Promise { + const source = await downloadFileStream({ key, context }) + const onAbort = () => source.destroy() + signal?.addEventListener('abort', onAbort, { once: true }) + + const reader = source[Symbol.asyncIterator]() + + try { + // Pull chunks until the first newline so the delimiter can be sniffed before parsing. + // Accumulate the header line incrementally — appending each chunk's decoded text rather than + // re-concatenating the whole buffer each iteration (which would be O(n²) for a header split + // across many small chunks). The delimiter chars (`,` `\t` `;`) are ASCII, so a multi-byte + // character split at a chunk boundary can't introduce a false delimiter into the count. + const sniffed: Buffer[] = [] + let firstLine = '' + let sniffedBytes = 0 + while (true) { + const { value, done } = await reader.next() + if (done) break + const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value) + sniffed.push(chunk) + sniffedBytes += chunk.length + const text = chunk.toString('utf-8') + const nl = text.indexOf('\n') + if (nl !== -1) { + firstLine += text.slice(0, nl) + break + } + firstLine += text + if (sniffedBytes >= DELIMITER_SNIFF_MAX_BYTES) break + } + + if (sniffed.length === 0) { + return { headers: [], rows: [], truncated: false } + } + + const delimiter = detectDelimiter(firstLine) + const parser = parseCsvStream({ + columns: false, + skip_empty_lines: true, + trim: true, + relax_column_count: true, + relax_quotes: true, + skip_records_with_error: true, + cast: false, + bom: true, + delimiter, + }) + + // Re-feed the sniffed prefix, then drain the rest of the source into the parser. + async function* rejoin() { + for (const chunk of sniffed) yield chunk + while (true) { + const { value, done } = await reader.next() + if (done) return + yield value + } + } + const piped = Readable.from(rejoin()) + piped.on('error', (err) => parser.destroy(err)) + piped.pipe(parser) + + let headers: string[] = [] + let headersSet = false + const rows: string[][] = [] + let truncated = false + + for await (const record of parser as AsyncIterable) { + if (!headersSet) { + headers = record.map(cell) + headersSet = true + continue + } + if (rows.length >= CSV_PREVIEW_MAX_ROWS) { + truncated = true + break + } + rows.push(record.map(cell)) + } + + piped.destroy() + parser.destroy() + return { headers, rows, truncated } + } finally { + signal?.removeEventListener('abort', onAbort) + source.destroy() + } +} diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index cee05f4d2b..bfd7a169d3 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: 852, - zodRoutes: 852, + totalRoutes: 853, + zodRoutes: 853, nonZodRoutes: 0, } as const From badfbc3bdf467f98836d94ba9fc05ab68a3b3d2f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 19:26:37 -0700 Subject: [PATCH 25/26] fix(resource): left-align table filter/sort when there's no search (#5128) The unconditional ml-auto from #5117 right-aligned the embedded table editor's filter/sort cluster, which has no search bar. Only push the aside + filter/sort group right when a search occupies the left; without a search it stays left-aligned as before. --- .../components/resource-options/resource-options.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx index ada916a41c..0926fb31ba 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx @@ -84,10 +84,11 @@ interface ResourceOptionsProps { filterTags?: FilterTag[] /** * Lightweight control rendered immediately to the LEFT of the filter/sort - * cluster; the two form one right-aligned group, with or without a search — - * e.g. the knowledge view's connected-source badge or the table editor's - * embedded run/stop control. Keep it to badges/status widgets; primary actions - * belong in the header's `actions`. + * cluster, forming one group with it — e.g. the knowledge view's + * connected-source badge or the table editor's embedded run/stop control. With + * a search the group is pushed right (opposite the search); without one it + * stays left-aligned (the embedded table editor). Keep it to badges/status + * widgets; primary actions belong in the header's `actions`. */ aside?: ReactNode } @@ -117,7 +118,7 @@ export const ResourceOptions = memo(function ResourceOptions({
{search && } -
+
{aside}
{filterTags?.map((tag) => ( From 597d7eafb558188fdd3937b65f481e1616e6e63d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 17 Jun 2026 19:38:55 -0700 Subject: [PATCH 26/26] fix(tables): enforce row limits against the current plan, not a frozen per-table cap (#5120) * fix(tables): enforce row limits against the current plan, not a frozen per-table cap * fix(tables): gate multi-batch CSV create + initial rows against the plan, harden limits cache bound * fix(tables): thread running row count through copilot batchInsertAll capacity check * chore(tables): align tx-variant capacity docstrings * fix(tables): map row-limit errors to 400 in create-from-CSV import * feat(tables): add Upgrade action to the row-limit toast * fix(tables): keep CreateTableData.maxRows so staging callers type-check after merge * improvement(tables): route row-limit Upgrade action to the explore-plans page --- .../api/table/[tableId]/import/route.test.ts | 18 +- .../app/api/table/[tableId]/import/route.ts | 9 +- apps/sim/app/api/table/import-async/route.ts | 1 - .../app/api/table/import-csv/route.test.ts | 17 + apps/sim/app/api/table/import-csv/route.ts | 26 +- apps/sim/app/api/table/route.ts | 1 - apps/sim/app/api/table/utils.test.ts | 14 +- apps/sim/app/api/table/utils.ts | 24 +- apps/sim/app/api/v1/tables/route.ts | 1 - .../components/table-grid/data-row.tsx | 2 +- .../components/table-grid/table-grid.tsx | 2 +- .../[tableId]/components/table-grid/utils.ts | 6 +- apps/sim/hooks/queries/tables.ts | 32 +- .../tools/server/table/user-table.test.ts | 6 +- .../copilot/tools/server/table/user-table.ts | 6 +- .../lib/table/__tests__/update-row.test.ts | 9 + apps/sim/lib/table/billing.test.ts | 160 + apps/sim/lib/table/billing.ts | 95 +- apps/sim/lib/table/import-data.ts | 14 + apps/sim/lib/table/import-runner.ts | 11 + apps/sim/lib/table/rows/service.ts | 67 +- apps/sim/lib/table/service.ts | 16 +- apps/sim/lib/table/types.ts | 3 +- .../0241_drop_table_row_cap_guard.sql | 27 + .../db/migrations/meta/0241_snapshot.json | 16573 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + 26 files changed, 17058 insertions(+), 89 deletions(-) create mode 100644 apps/sim/lib/table/billing.test.ts create mode 100644 packages/db/migrations/0241_drop_table_row_cap_guard.sql create mode 100644 packages/db/migrations/meta/0241_snapshot.json diff --git a/apps/sim/app/api/table/[tableId]/import/route.test.ts b/apps/sim/app/api/table/[tableId]/import/route.test.ts index 588e1813b0..0333d7fb4d 100644 --- a/apps/sim/app/api/table/[tableId]/import/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/import/route.test.ts @@ -13,6 +13,7 @@ const { mockDispatchAfterBatchInsert, mockMarkTableImporting, mockReleaseImportClaim, + mockGetMaxRowsPerTable, } = vi.hoisted(() => ({ mockCheckAccess: vi.fn(), mockImportAppendRows: vi.fn(), @@ -20,6 +21,7 @@ const { mockDispatchAfterBatchInsert: vi.fn(), mockMarkTableImporting: vi.fn(), mockReleaseImportClaim: vi.fn(), + mockGetMaxRowsPerTable: vi.fn(), })) vi.mock('@sim/utils/id', () => ({ @@ -65,6 +67,13 @@ vi.mock('@/lib/table/rows/service', () => ({ dispatchAfterBatchInsert: mockDispatchAfterBatchInsert, })) +/** The append pre-check reads the workspace's current plan row limit, not the frozen `table.maxRows`. */ +vi.mock('@/lib/table/billing', () => ({ + getMaxRowsPerTable: mockGetMaxRowsPerTable, + wouldExceedRowLimit: (limit: number, current: number, added: number) => + limit >= 0 && current + added > limit, +})) + import { POST } from '@/app/api/table/[tableId]/import/route' function createCsvFile(contents: string, name = 'data.csv', type = 'text/csv'): File { @@ -167,6 +176,7 @@ describe('POST /api/table/[tableId]/import', () => { mockImportReplaceRows.mockResolvedValue({ deletedCount: 0, insertedCount: 0 }) mockMarkTableImporting.mockResolvedValue(true) mockReleaseImportClaim.mockResolvedValue(undefined) + mockGetMaxRowsPerTable.mockResolvedValue(1_000_000) }) it('returns 401 when the user is not authenticated', async () => { @@ -288,11 +298,9 @@ describe('POST /api/table/[tableId]/import', () => { expect(mockImportAppendRows).toHaveBeenCalledTimes(1) }) - it('rejects append when it would exceed maxRows', async () => { - mockCheckAccess.mockResolvedValueOnce({ - ok: true, - table: buildTable({ rowCount: 99, maxRows: 100 }), - }) + it('rejects append when it would exceed the current plan row limit', async () => { + mockCheckAccess.mockResolvedValueOnce({ ok: true, table: buildTable({ rowCount: 99 }) }) + mockGetMaxRowsPerTable.mockResolvedValueOnce(100) const response = await callPost( createFormData(createCsvFile('name,age\nAlice,30\nBob,40'), { mode: 'append' }) ) diff --git a/apps/sim/app/api/table/[tableId]/import/route.ts b/apps/sim/app/api/table/[tableId]/import/route.ts index fc4a321a3c..79bb7238ab 100644 --- a/apps/sim/app/api/table/[tableId]/import/route.ts +++ b/apps/sim/app/api/table/[tableId]/import/route.ts @@ -25,6 +25,7 @@ import { createCsvParser, dispatchAfterBatchInsert, generateColumnId, + getMaxRowsPerTable, inferColumnType, markTableJobRunning, releaseJobClaim, @@ -32,6 +33,7 @@ import { type TableDefinition, type TableSchema, validateMapping, + wouldExceedRowLimit, } from '@/lib/table' import { importAppendRows, importReplaceRows } from '@/lib/table/import-data' import { @@ -264,11 +266,12 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro claimedImportId = syncImportId if (mode === 'append') { - if (prospectiveTable.rowCount + coerced.length > prospectiveTable.maxRows) { - const deficit = prospectiveTable.rowCount + coerced.length - prospectiveTable.maxRows + const maxRows = await getMaxRowsPerTable(workspaceId) + if (wouldExceedRowLimit(maxRows, prospectiveTable.rowCount, coerced.length)) { + const deficit = prospectiveTable.rowCount + coerced.length - maxRows return NextResponse.json( { - error: `Append would exceed table row limit (${prospectiveTable.maxRows}). Currently ${prospectiveTable.rowCount} rows, ${coerced.length} new rows, ${deficit} over.`, + error: `Append would exceed table row limit (${maxRows}). Currently ${prospectiveTable.rowCount} rows, ${coerced.length} new rows, ${deficit} over.`, }, { status: 400 } ) diff --git a/apps/sim/app/api/table/import-async/route.ts b/apps/sim/app/api/table/import-async/route.ts index 15b7f3e3ea..97afd2fd97 100644 --- a/apps/sim/app/api/table/import-async/route.ts +++ b/apps/sim/app/api/table/import-async/route.ts @@ -84,7 +84,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { schema: { columns: [{ name: 'column_1', type: 'string' }] }, workspaceId, userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, jobStatus: 'running', jobType: 'import', diff --git a/apps/sim/app/api/table/import-csv/route.test.ts b/apps/sim/app/api/table/import-csv/route.test.ts index 350f39241b..eaf0dde787 100644 --- a/apps/sim/app/api/table/import-csv/route.test.ts +++ b/apps/sim/app/api/table/import-csv/route.test.ts @@ -39,6 +39,12 @@ vi.mock('@/app/api/table/utils', async () => { { error: error.message }, { status: error.code === 'FILE_TOO_LARGE' ? 413 : 400 } ), + rowWriteErrorResponse: (error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + return message.includes('row limit') + ? NextResponse.json({ error: message }, { status: 400 }) + : null + }, } }) vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) @@ -175,6 +181,17 @@ describe('POST /api/table/import-csv', () => { expect(mockCreateTable).not.toHaveBeenCalled() }) + it('returns 400 with the reason when an insert exceeds the plan row limit', async () => { + mockBatchInsertRows.mockRejectedValueOnce( + new Error('This table has reached its row limit (1,000 rows) on your current plan.') + ) + const response = await POST(makeRequest(uploadParts(csvWithRows(250)))) + const data = await response.json() + + expect(response.status).toBe(400) + expect(data.error).toMatch(/row limit/) + }) + it('rolls back the created table when a batch insert fails mid-stream', async () => { mockBatchInsertRows .mockResolvedValueOnce(Array.from({ length: 100 }, () => ({ id: 'row' }))) diff --git a/apps/sim/app/api/table/import-csv/route.ts b/apps/sim/app/api/table/import-csv/route.ts index 9b6b4fd75d..91ee680706 100644 --- a/apps/sim/app/api/table/import-csv/route.ts +++ b/apps/sim/app/api/table/import-csv/route.ts @@ -30,6 +30,7 @@ import { csvProxyBodyCapResponse, multipartErrorResponse, normalizeColumn, + rowWriteErrorResponse, } from '@/app/api/table/utils' const logger = createLogger('TableImportCSV') @@ -105,12 +106,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => { headerToColumn: Map } - const insertRows = async (rows: Record[], state: ImportState) => { + const insertRows = async ( + rows: Record[], + state: ImportState, + currentRowCount: number + ) => { if (rows.length === 0) return 0 const coerced = coerceRowsForTable(rows, state.schema, state.headerToColumn) const result = await batchInsertRows( { tableId: state.table.id, rows: coerced, workspaceId, userId }, - state.table, + // The created table's rowCount is frozen at 0; pass the running total so the + // per-batch capacity check sees cumulative rows, not an always-empty table. + { ...state.table, rowCount: currentRowCount }, generateId().slice(0, 8) ) return result.length @@ -132,7 +139,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { schema, workspaceId, userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, }, requestId @@ -153,13 +159,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { sample.push(record) if (sample.length >= CSV_SCHEMA_SAMPLE_SIZE) { state = await buildTable(sample) - inserted += await insertRows(sample, state) + inserted += await insertRows(sample, state, inserted) } continue } batch.push(record) if (batch.length >= CSV_MAX_BATCH_SIZE) { - inserted += await insertRows(batch, state) + inserted += await insertRows(batch, state, inserted) batch = [] } } @@ -169,9 +175,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'CSV file has no data rows' }, { status: 400 }) } state = await buildTable(sample) - inserted += await insertRows(sample, state) + inserted += await insertRows(sample, state, inserted) } else { - inserted += await insertRows(batch, state) + inserted += await insertRows(batch, state, inserted) } } catch (streamError) { if (state) await deleteTable(state.table.id, requestId).catch(() => {}) @@ -200,9 +206,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } catch (error) { if (isMultipartError(error)) return multipartErrorResponse(error) - const message = toError(error).message logger.error(`[${requestId}] CSV import failed:`, error) + // Row-write failures (e.g. the plan row-limit check) map to a 400 with the real reason. + const rowWriteError = rowWriteErrorResponse(error) + if (rowWriteError) return rowWriteError + + const message = toError(error).message const isClientError = message.includes('maximum table limit') || message.includes('CSV file has no') || diff --git a/apps/sim/app/api/table/route.ts b/apps/sim/app/api/table/route.ts index 94aa8c45b4..45b530db29 100644 --- a/apps/sim/app/api/table/route.ts +++ b/apps/sim/app/api/table/route.ts @@ -82,7 +82,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { schema: normalizedSchema, workspaceId: params.workspaceId, userId: authResult.userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, initialRowCount: params.initialRowCount, }, diff --git a/apps/sim/app/api/table/utils.test.ts b/apps/sim/app/api/table/utils.test.ts index df1a05e7c7..2b23d45ffe 100644 --- a/apps/sim/app/api/table/utils.test.ts +++ b/apps/sim/app/api/table/utils.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' +import { TableRowLimitError } from '@/lib/table/billing' import { rootErrorMessage, rowWriteErrorResponse } from '@/app/api/table/utils' /** Mimics drizzle's DrizzleQueryError: message is the failed SQL, real error on `cause`. */ @@ -17,7 +18,7 @@ describe('rootErrorMessage', () => { }) it('unwraps the cause chain to the deepest error', () => { - const root = new Error('Maximum row limit (10000) reached for table tbl_abc') + const root = new Error('Value for column "email" must be unique') expect(rootErrorMessage(wrapLikeDrizzle(root))).toBe(root.message) }) @@ -27,14 +28,13 @@ describe('rootErrorMessage', () => { }) describe('rowWriteErrorResponse', () => { - it('rewrites the DB row-limit trigger error into a friendly 400', async () => { - const error = wrapLikeDrizzle( - new Error('Maximum row limit (10000) reached for table tbl_2b15ec29647040e7b8eb5d2949f556cf') - ) - const response = rowWriteErrorResponse(error) + it('passes the plan row-limit error through as a 400', async () => { + const response = rowWriteErrorResponse(new TableRowLimitError(10000)) expect(response?.status).toBe(400) const body = await response?.json() - expect(body.error).toBe('Row limit exceeded — this table is capped at 10,000 rows') + expect(body.error).toBe( + 'This table has reached its row limit (10,000 rows) on your current plan.' + ) }) it('passes known validation messages through as 400', async () => { diff --git a/apps/sim/app/api/table/utils.ts b/apps/sim/app/api/table/utils.ts index c8dde91313..33986a2964 100644 --- a/apps/sim/app/api/table/utils.ts +++ b/apps/sim/app/api/table/utils.ts @@ -36,9 +36,9 @@ export function tableFilterError( const logger = createLogger('TableUtils') /** - * Deepest `Error` message in the cause chain. Drizzle wraps DB errors (e.g. the - * row-limit trigger's RAISE) in a `DrizzleQueryError` whose own message is just - * the failed SQL — substring classification must look at the root cause. + * Deepest `Error` message in the cause chain. Drizzle wraps DB errors in a + * `DrizzleQueryError` whose own message is just the failed SQL — substring + * classification must look at the root cause. */ export function rootErrorMessage(error: unknown): string { let current: unknown = error @@ -49,9 +49,9 @@ export function rootErrorMessage(error: unknown): string { } /** - * Known user-facing row-write failures (service validation + the DB row-limit - * trigger). Anything outside this list stays a generic 500 — unknown errors can - * carry SQL/internals that don't belong in a toast. + * Known user-facing row-write failures (service validation + the best-effort + * plan row-limit check). Anything outside this list stays a generic 500 — + * unknown errors can carry SQL/internals that don't belong in a toast. */ const ROW_WRITE_ERROR_PATTERNS = [ 'row limit', @@ -79,18 +79,6 @@ const ROW_WRITE_ERROR_PATTERNS = [ export function rowWriteErrorResponse(error: unknown): NextResponse | null { const message = rootErrorMessage(error) - // Trigger message reads `Maximum row limit (N) reached for table tbl_...` — - // rewrite it for the toast instead of leaking the internal table id. - const limitMatch = message.match(/Maximum row limit \((\d+)\) reached/) - if (limitMatch) { - return NextResponse.json( - { - error: `Row limit exceeded — this table is capped at ${Number(limitMatch[1]).toLocaleString('en-US')} rows`, - }, - { status: 400 } - ) - } - if (ROW_WRITE_ERROR_PATTERNS.some((p) => message.includes(p)) || /^Row .+?:/.test(message)) { return NextResponse.json({ error: message }, { status: 400 }) } diff --git a/apps/sim/app/api/v1/tables/route.ts b/apps/sim/app/api/v1/tables/route.ts index b90245a8df..aaf113a376 100644 --- a/apps/sim/app/api/v1/tables/route.ts +++ b/apps/sim/app/api/v1/tables/route.ts @@ -108,7 +108,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { schema: normalizedSchema, workspaceId: params.workspaceId, userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, }, requestId diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index ef1558067e..82114fc398 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -50,7 +50,7 @@ export interface DataRowProps { runningCount: number /** Whether the table has at least one workflow column — controls whether a run/stop icon is rendered. */ hasWorkflowColumns: boolean - /** Width of the centered row-number/checkbox region in px, derived from the table's maxRows digit count. */ + /** Width of the centered row-number/checkbox region in px, derived from the table's row-count digit count. */ numRegionWidth: number onStopRow: (rowId: string) => void onRunRow: (rowId: string) => void diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 87786d0798..c69b797874 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -619,7 +619,7 @@ export function TableGrid({ const hasWorkflowColumns = columns.some((c) => !!c.workflowGroupId) const { colWidth: checkboxColWidth, numRegionWidth } = checkboxColLayout( - tableData?.maxRows ?? 0, + tableData?.rowCount ?? 0, hasWorkflowColumns ) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts index 39de6fb07d..674f00865e 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts @@ -52,12 +52,12 @@ export function rowSelectionCoversAll(sel: RowSelection, rows: TableRowType[]): return true } -/** Returns sticky row-number column dimensions sized to the digit count of `maxRows`. */ +/** Returns sticky row-number column dimensions sized to the digit count of `rowCount`. */ export function checkboxColLayout( - maxRows: number, + rowCount: number, hasWorkflowCols: boolean ): { colWidth: number; numRegionWidth: number } { - const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1 + const digits = rowCount > 0 ? Math.floor(Math.log10(rowCount)) + 1 : 1 const numWidth = Math.max(20, digits * 8 + 4) // Region the number/checkbox is centered within (digit width + 12px breathing // room, min 32). The select-all header checkbox centers in the same region so it diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 8d408e18d9..5825ebd6c4 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -14,6 +14,7 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' import { toast } from '@/components/emcn' import { isValidationError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' @@ -571,8 +572,26 @@ export function useDeleteTable(workspaceId: string) { * Populates the cache on success so the new row is immediately available * without waiting for the background refetch triggered by invalidation. */ +/** + * Toasts a failed row write. A plan row-limit failure (the best-effort cap in + * `assertRowCapacity`) gets an "Upgrade" action routing to the explore-plans page; + * other errors are a plain auto-dismissing toast. Validation errors are surfaced + * inline, not here. + */ +function notifyRowWriteError(error: Error, onUpgrade: () => void): void { + if (isValidationError(error)) return + if (error.message.toLowerCase().includes('row limit')) { + toast.error(error.message, { + action: { label: 'Upgrade', onClick: onUpgrade }, + }) + return + } + toast.error(error.message, { duration: 5000 }) +} + export function useCreateTableRow({ workspaceId, tableId }: RowMutationContext) { const queryClient = useQueryClient() + const router = useRouter() return useMutation({ mutationFn: async ( @@ -617,10 +636,8 @@ export function useCreateTableRow({ workspaceId, tableId }: RowMutationContext) predicate: (query) => !isDefaultOrderRowsQuery(query.queryKey), }) }, - onError: (error) => { - if (isValidationError(error)) return - toast.error(error.message, { duration: 5000 }) - }, + onError: (error) => + notifyRowWriteError(error, () => router.push(`/workspace/${workspaceId}/upgrade`)), onSettled: () => { // `reconcileCreatedRow` (onSuccess) is the source of truth for the rows // cache + its `totalCount`; only refresh the count surfaces here so a late @@ -783,6 +800,7 @@ type BatchCreateTableRowsResponse = ContractJsonResponse { - if (isValidationError(error)) return - toast.error(error.message, { duration: 5000 }) - }, + onError: (error) => + notifyRowWriteError(error, () => router.push(`/workspace/${workspaceId}/upgrade`)), onSettled: () => { invalidateRowCount(queryClient, tableId) }, diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts index abf8c7d6f4..b7320df325 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts @@ -405,8 +405,7 @@ describe('userTableServerTool.create_from_file', () => { expect(result.success).toBe(true) expect(mockGetWorkspaceTableLimits).toHaveBeenCalledWith('workspace-1') expect(mockCreateTable).toHaveBeenCalledTimes(1) - const createArgs = mockCreateTable.mock.calls[0][0] as { maxRows: number; maxTables: number } - expect(createArgs.maxRows).toBe(1000) + const createArgs = mockCreateTable.mock.calls[0][0] as { maxTables: number } expect(createArgs.maxTables).toBe(3) }) @@ -498,8 +497,7 @@ describe('userTableServerTool.create', () => { expect(result.success).toBe(true) expect(mockGetWorkspaceTableLimits).toHaveBeenCalledWith('workspace-1') - const createArgs = mockCreateTable.mock.calls[0][0] as { maxRows: number; maxTables: number } - expect(createArgs.maxRows).toBe(1000) + const createArgs = mockCreateTable.mock.calls[0][0] as { maxTables: number } expect(createArgs.maxTables).toBe(3) }) }) diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index 024dda09ad..c02948bf9a 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -314,7 +314,9 @@ async function batchInsertAll( const requestId = generateId().slice(0, 8) const result = await batchInsertRows( { tableId, rows: batch, workspaceId, userId }, - table, + // Pass the running total so each batch's capacity check sees cumulative rows, + // not the same pre-loop snapshot (which would let a multi-batch insert overshoot). + { ...table, rowCount: table.rowCount + inserted }, requestId ) inserted += result.length @@ -362,7 +364,6 @@ export const userTableServerTool: BaseServerTool schema: args.schema, workspaceId, userId: context.userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, }, requestId @@ -1102,7 +1103,6 @@ export const userTableServerTool: BaseServerTool schema: { columns }, workspaceId, userId: context.userId, - maxRows: planLimits.maxRowsPerTable, maxTables: planLimits.maxTables, }, requestId diff --git a/apps/sim/lib/table/__tests__/update-row.test.ts b/apps/sim/lib/table/__tests__/update-row.test.ts index a8919924aa..f4b9399637 100644 --- a/apps/sim/lib/table/__tests__/update-row.test.ts +++ b/apps/sim/lib/table/__tests__/update-row.test.ts @@ -16,6 +16,15 @@ import { getUniqueColumns } from '@/lib/table/validation' vi.mock('@sim/db', () => dbChainMock) +// Capacity is exercised in billing.test.ts; here it's a no-op so the timeout-scaling +// suites can use large synthetic row counts without tripping the plan limit. +vi.mock('@/lib/table/billing', () => ({ + assertRowCapacity: vi.fn().mockResolvedValue(undefined), + getMaxRowsPerTable: vi.fn().mockResolvedValue(1_000_000), + wouldExceedRowLimit: () => false, + TableRowLimitError: class TableRowLimitError extends Error {}, +})) + // These suites assert flag-off position-shift semantics; pin the flag so they're // deterministic regardless of a local TABLES_FRACTIONAL_ORDERING env value. vi.mock('@/lib/core/config/feature-flags', () => ({ diff --git a/apps/sim/lib/table/billing.test.ts b/apps/sim/lib/table/billing.test.ts new file mode 100644 index 0000000000..f49b8fc567 --- /dev/null +++ b/apps/sim/lib/table/billing.test.ts @@ -0,0 +1,160 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetWorkspaceBilledAccountUserId, + mockGetHighestPrioritySubscription, + mockGetPlanTypeForLimits, + mockGetTablePlanLimits, +} = vi.hoisted(() => ({ + mockGetWorkspaceBilledAccountUserId: vi.fn(), + mockGetHighestPrioritySubscription: vi.fn(), + mockGetPlanTypeForLimits: vi.fn(), + mockGetTablePlanLimits: vi.fn(), +})) + +vi.mock('@/lib/workspaces/utils', () => ({ + getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, +})) +vi.mock('@/lib/billing/core/subscription', () => ({ + getHighestPrioritySubscription: mockGetHighestPrioritySubscription, +})) +vi.mock('@/lib/billing/plan-helpers', () => ({ + getPlanTypeForLimits: mockGetPlanTypeForLimits, +})) +vi.mock('@/lib/table/constants', () => ({ + getTablePlanLimits: mockGetTablePlanLimits, +})) + +import { + assertRowCapacity, + getMaxRowsPerTable, + getWorkspaceTableLimits, + TableRowLimitError, + wouldExceedRowLimit, +} from '@/lib/table/billing' + +const LIMITS = { + free: { maxTables: 3, maxRowsPerTable: 1000 }, + pro: { maxTables: 25, maxRowsPerTable: 5000 }, + team: { maxTables: 100, maxRowsPerTable: 10000 }, + enterprise: { maxTables: 10000, maxRowsPerTable: 1000000 }, +} + +// The limits cache is module-level and keyed by workspaceId; a fresh id per test +// keeps one test's cached value from leaking into the next. +let wsCounter = 0 +const nextWorkspaceId = () => `ws-${++wsCounter}` + +beforeEach(() => { + vi.clearAllMocks() + mockGetTablePlanLimits.mockReturnValue(LIMITS) + mockGetWorkspaceBilledAccountUserId.mockResolvedValue('billed-user') + mockGetHighestPrioritySubscription.mockResolvedValue({ plan: 'pro' }) + mockGetPlanTypeForLimits.mockReturnValue('pro') +}) + +describe('getWorkspaceTableLimits', () => { + it('returns the limits for the workspace subscription plan', async () => { + expect(await getWorkspaceTableLimits(nextWorkspaceId())).toEqual(LIMITS.pro) + }) + + it('caches the resolved limits within the TTL', async () => { + const ws = nextWorkspaceId() + await getWorkspaceTableLimits(ws) + await getWorkspaceTableLimits(ws) + expect(mockGetWorkspaceBilledAccountUserId).toHaveBeenCalledTimes(1) + }) + + it('returns free-tier limits when the workspace has no billed account', async () => { + mockGetWorkspaceBilledAccountUserId.mockResolvedValueOnce(null) + expect(await getWorkspaceTableLimits(nextWorkspaceId())).toEqual(LIMITS.free) + }) + + it('falls back to free tier without caching when the lookup throws', async () => { + const ws = nextWorkspaceId() + mockGetWorkspaceBilledAccountUserId.mockRejectedValueOnce(new Error('db down')) + expect(await getWorkspaceTableLimits(ws)).toEqual(LIMITS.free) + // The fallback is never cached, so the next call re-attempts and resolves the real plan. + expect(await getWorkspaceTableLimits(ws)).toEqual(LIMITS.pro) + }) + + it('stays bounded under a burst of distinct all-fresh workspaces', async () => { + // Far more distinct workspaces than the cap, all within one TTL window. The Map + // must not grow without limit; eviction keeps it at/under the ceiling. + for (let i = 0; i < 6_000; i++) { + await getWorkspaceTableLimits(`burst-${i}`) + } + // Re-resolving an early (evicted) workspace must re-hit the billing lookup. + mockGetWorkspaceBilledAccountUserId.mockClear() + await getWorkspaceTableLimits('burst-0') + expect(mockGetWorkspaceBilledAccountUserId).toHaveBeenCalledTimes(1) + }) +}) + +describe('getMaxRowsPerTable', () => { + it('returns the plan maxRowsPerTable', async () => { + expect(await getMaxRowsPerTable(nextWorkspaceId())).toBe(5000) + }) +}) + +describe('wouldExceedRowLimit', () => { + it('is false under the limit and at the limit exactly', () => { + expect(wouldExceedRowLimit(1000, 10, 5)).toBe(false) + expect(wouldExceedRowLimit(1000, 999, 1)).toBe(false) + }) + + it('is true when the sum crosses the limit', () => { + expect(wouldExceedRowLimit(1000, 1000, 1)).toBe(true) + }) + + it('treats a negative limit as unlimited', () => { + expect(wouldExceedRowLimit(-1, 10_000_000, 1)).toBe(false) + }) + + it('treats a zero limit as no rows allowed', () => { + expect(wouldExceedRowLimit(0, 0, 1)).toBe(true) + }) +}) + +describe('assertRowCapacity', () => { + it('passes when the write stays under the plan limit', async () => { + await expect( + assertRowCapacity({ workspaceId: nextWorkspaceId(), currentRowCount: 10, addedRows: 5 }) + ).resolves.toBeUndefined() + }) + + it('allows reaching the limit exactly', async () => { + await expect( + assertRowCapacity({ workspaceId: nextWorkspaceId(), currentRowCount: 4999, addedRows: 1 }) + ).resolves.toBeUndefined() + }) + + it('throws TableRowLimitError when the write would exceed the limit', async () => { + await expect( + assertRowCapacity({ workspaceId: nextWorkspaceId(), currentRowCount: 5000, addedRows: 1 }) + ).rejects.toBeInstanceOf(TableRowLimitError) + }) + + it('names the plan limit in the error message', async () => { + await expect( + assertRowCapacity({ workspaceId: nextWorkspaceId(), currentRowCount: 5000, addedRows: 1 }) + ).rejects.toThrow(/row limit \(5,000 rows\)/) + }) + + it('skips the check when the plan is unlimited (-1)', async () => { + mockGetTablePlanLimits.mockReturnValue({ + ...LIMITS, + pro: { maxTables: 25, maxRowsPerTable: -1 }, + }) + await expect( + assertRowCapacity({ + workspaceId: nextWorkspaceId(), + currentRowCount: 10_000_000, + addedRows: 1, + }) + ).resolves.toBeUndefined() + }) +}) diff --git a/apps/sim/lib/table/billing.ts b/apps/sim/lib/table/billing.ts index 2dfbc30cd9..33c1028d76 100644 --- a/apps/sim/lib/table/billing.ts +++ b/apps/sim/lib/table/billing.ts @@ -12,16 +12,33 @@ import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' const logger = createLogger('TableBilling') +/** + * Plan lookups hit billing + subscription tables (2-3 queries). Row-limit checks + * run on every insert, so a short TTL keeps the hot path off the DB. Plan changes + * are rare and enforcement is best-effort, so brief staleness is acceptable. + */ +const LIMITS_CACHE_TTL_MS = 30_000 +/** Hard ceiling on cached workspaces; a sweep drops expired entries before this is exceeded so the Map can't grow unbounded. */ +const LIMITS_CACHE_MAX_ENTRIES = 5_000 +const limitsCache = new Map() + /** * Gets the table limits for a workspace based on its billing plan. * * Uses the workspace's billed account user to determine the subscription plan, - * then returns the corresponding table limits. + * then returns the corresponding table limits. Resolved limits are cached for + * {@link LIMITS_CACHE_TTL_MS}; the free-tier error fallback is never cached. * * @param workspaceId - The workspace ID to get limits for * @returns Table limits based on the workspace's billing plan */ export async function getWorkspaceTableLimits(workspaceId: string): Promise { + const cached = limitsCache.get(workspaceId) + if (cached) { + if (cached.expiresAt > Date.now()) return cached.limits + limitsCache.delete(workspaceId) + } + const planLimits = getTablePlanLimits() try { @@ -29,6 +46,7 @@ export async function getWorkspaceTableLimits(workspaceId: string): Promise= LIMITS_CACHE_MAX_ENTRIES && !limitsCache.has(workspaceId)) { + const now = Date.now() + for (const [key, entry] of limitsCache) { + if (entry.expiresAt <= now) limitsCache.delete(key) + } + while (limitsCache.size >= LIMITS_CACHE_MAX_ENTRIES) { + const oldest = limitsCache.keys().next().value + if (oldest === undefined) break + limitsCache.delete(oldest) + } + } + limitsCache.set(workspaceId, { limits, expiresAt: Date.now() + LIMITS_CACHE_TTL_MS }) +} + +/** + * Thrown by {@link assertRowCapacity} when a write would exceed the workspace's + * current plan row limit. The message includes the lowercase `row limit` token so + * `rowWriteErrorResponse` maps it to a 400 toast carrying the real reason. + */ +export class TableRowLimitError extends Error { + constructor(readonly limit: number) { + super( + `This table has reached its row limit (${limit.toLocaleString('en-US')} rows) on your current plan.` + ) + this.name = 'TableRowLimitError' + } +} + +/** + * Whether adding `addedRows` to `currentRowCount` would cross `limit`. A negative + * limit means unlimited. Single source of truth for the comparison so callers that + * fetch the limit themselves (e.g. inside a transaction, or to build a custom + * message) stay consistent with {@link assertRowCapacity}. + */ +export function wouldExceedRowLimit( + limit: number, + currentRowCount: number, + addedRows: number +): boolean { + return limit >= 0 && currentRowCount + addedRows > limit +} + +/** + * Best-effort capacity check against the workspace's CURRENT plan limit. + * + * Not transactional: reads the (trigger-maintained, possibly slightly stale) row + * count and the cached plan limit outside any lock, so concurrent writers may + * overshoot by a small amount. It rejects once the count is at/over the limit, so + * a table can't run away past its plan. + * + * Resolve the limit OUTSIDE any open transaction — `getMaxRowsPerTable` may hit the + * billing/subscription tables on the global pool, and doing that while holding a tx + * connection (and locks) risks pool starvation. Callers already inside a tx should + * fetch the limit up front and use {@link wouldExceedRowLimit} instead. + * + * @throws {TableRowLimitError} if `currentRowCount + addedRows` exceeds the limit + */ +export async function assertRowCapacity(params: { + workspaceId: string + currentRowCount: number + addedRows: number +}): Promise { + const limit = await getMaxRowsPerTable(params.workspaceId) + if (wouldExceedRowLimit(limit, params.currentRowCount, params.addedRows)) { + throw new TableRowLimitError(limit) + } +} + /** * Checks if a workspace can create more tables based on its plan limits. * @@ -80,7 +171,7 @@ async function canCreateTable( * @param workspaceId - The workspace ID * @returns Maximum rows per table (-1 for unlimited) */ -async function getMaxRowsPerTable(workspaceId: string): Promise { +export async function getMaxRowsPerTable(workspaceId: string): Promise { const limits = await getWorkspaceTableLimits(workspaceId) return limits.maxRowsPerTable } diff --git a/apps/sim/lib/table/import-data.ts b/apps/sim/lib/table/import-data.ts index bf25f2e15c..1d489afa87 100644 --- a/apps/sim/lib/table/import-data.ts +++ b/apps/sim/lib/table/import-data.ts @@ -9,6 +9,7 @@ import { userTableDefinitions, userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' +import { assertRowCapacity } from '@/lib/table/billing' import { CSV_MAX_BATCH_SIZE } from '@/lib/table/import' import { nKeysBetween } from '@/lib/table/order-key' import { acquireRowOrderLock } from '@/lib/table/rows/ordering' @@ -148,6 +149,12 @@ export async function importAppendRows( rows: RowData[], ctx: { workspaceId: string; userId?: string; requestId: string } ): Promise<{ inserted: TableRow[]; table: TableDefinition }> { + // Gate capacity before opening the tx — the lookup is a separate pool read. + await assertRowCapacity({ + workspaceId: ctx.workspaceId, + currentRowCount: table.rowCount, + addedRows: rows.length, + }) return db.transaction(async (trx) => { let working = table if (additions.length > 0) { @@ -184,6 +191,13 @@ export async function importReplaceRows( data: { rows: RowData[]; workspaceId: string; userId?: string }, requestId: string ): Promise { + // Replace deletes all existing rows, so the footprint is just the new set. Gate + // before opening the tx — the plan lookup is a separate pool read. + await assertRowCapacity({ + workspaceId: data.workspaceId, + currentRowCount: 0, + addedRows: data.rows.length, + }) return db.transaction(async (trx) => { let working = table if (additions.length > 0) { diff --git a/apps/sim/lib/table/import-runner.ts b/apps/sim/lib/table/import-runner.ts index f9d2b3f503..b53669c97d 100644 --- a/apps/sim/lib/table/import-runner.ts +++ b/apps/sim/lib/table/import-runner.ts @@ -17,6 +17,7 @@ import { type TableSchema, validateMapping, } from '@/lib/table' +import { assertRowCapacity } from '@/lib/table/billing' import { withGeneratedColumnIds } from '@/lib/table/column-keys' import { appendTableEvent } from '@/lib/table/events' import { @@ -102,6 +103,11 @@ export async function runTableImport(payload: TableImportPayload): Promise const basePosition = mode === 'append' ? await nextImportStartPosition(tableId) : 0 let lastOrderKey = mode === 'append' ? await nextImportStartOrderKey(tableId) : null + // Append keeps the existing rows; create/replace start from empty (replace deletes + // existing rows in resolveSetup). Per-batch capacity is checked against this base + the + // running total, so a stream that crosses the plan limit fails within one batch. + const existingRowCount = mode === 'append' ? table.rowCount : 0 + // Count bytes as they flow so the row total can be extrapolated from byte progress. let bytesRead = 0 const byteCounter = new Transform({ @@ -194,6 +200,11 @@ export async function runTableImport(payload: TableImportPayload): Promise const owns = await updateJobProgress(tableId, inserted, importId) if (!owns) throw new ImportSupersededError() const coerced = coerceRowsForTable(rows, schema, headerToColumn) + await assertRowCapacity({ + workspaceId, + currentRowCount: existingRowCount + inserted, + addedRows: coerced.length, + }) const result = await bulkInsertImportBatch( { tableId, diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index 7355985101..5cc156dbfa 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -17,6 +17,12 @@ import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, count, eq, inArray, lte, notInArray, type SQL, sql } from 'drizzle-orm' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { + assertRowCapacity, + getMaxRowsPerTable, + TableRowLimitError, + wouldExceedRowLimit, +} from '@/lib/table/billing' import { getColumnId } from '@/lib/table/column-keys' import { TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' import { nKeysBetween } from '@/lib/table/order-key' @@ -115,13 +121,16 @@ export async function insertRow( } } + // Best-effort capacity check against the workspace's current plan limit. + await assertRowCapacity({ + workspaceId: table.workspaceId, + currentRowCount: table.rowCount, + addedRows: 1, + }) + const rowId = `row_${generateId().replace(/-/g, '')}` const now = new Date() - // Capacity enforcement lives in the `increment_user_table_row_count` trigger - // (migration 0198): a single conditional UPDATE on user_table_definitions - // increments row_count iff row_count < max_rows, taking the row lock - // atomically. No app-level FOR UPDATE / COUNT needed. const row = await insertOrderedRow({ tableId: data.tableId, workspaceId: data.workspaceId, @@ -182,6 +191,14 @@ export async function batchInsertRows( table: TableDefinition, requestId: string ): Promise { + // Best-effort capacity check against the workspace's current plan limit. Import + // paths call `batchInsertRowsWithTx` directly and gate capacity up front instead. + await assertRowCapacity({ + workspaceId: table.workspaceId, + currentRowCount: table.rowCount, + addedRows: data.rows.length, + }) + const result = await db.transaction((trx) => batchInsertRowsWithTx(trx, data, table, requestId)) dispatchAfterBatchInsert(table, result, requestId, data.userId) return result @@ -193,9 +210,8 @@ export async function batchInsertRows( * is responsible for opening the transaction. Use when row inserts must be * atomic with other writes (e.g., schema mutations) on the same tx. * - * Capacity enforcement lives in the `increment_user_table_row_count` trigger - * (migration 0198) — fires per row and raises `Maximum row limit (%) reached ...` - * if the cap is hit mid-batch. + * Capacity is NOT checked here (it would mean a billing-pool read inside the tx). + * Callers gate it before opening the tx — see `batchInsertRows` and the import paths. */ export async function batchInsertRowsWithTx( trx: DbTransaction, @@ -317,7 +333,7 @@ export function dispatchAfterBatchInsert( * * Validates each row against the schema, enforces unique constraints within the * new rows (existing rows are deleted, so DB-side checks are unnecessary), and - * enforces `maxRows` before the replace executes. + * enforces the workspace's current plan row limit before the replace executes. * * @param data - Replace data (rows to install) * @param table - Table definition @@ -330,12 +346,22 @@ export async function replaceTableRows( table: TableDefinition, requestId: string ): Promise { + // All existing rows are deleted, so the footprint is just the new set. Checked + // before the tx opens — never inside it (the plan lookup is a separate pool read). + await assertRowCapacity({ + workspaceId: table.workspaceId, + currentRowCount: 0, + addedRows: data.rows.length, + }) return db.transaction((trx) => replaceTableRowsWithTx(trx, data, table, requestId)) } /** * Transaction-bound variant of `replaceTableRows`. Caller opens the transaction. * Use when the replace must be atomic with other writes (e.g., schema mutations). + * + * Capacity is NOT checked here (it would mean a billing-pool read inside the tx). + * Callers gate it before opening the tx — see `replaceTableRows` and `importReplaceRows`. */ export async function replaceTableRowsWithTx( trx: DbTransaction, @@ -349,11 +375,6 @@ export async function replaceTableRowsWithTx( if (data.workspaceId !== table.workspaceId) { throw new Error(`Workspace ID mismatch: ${data.workspaceId} does not own table ${data.tableId}`) } - if (data.rows.length > table.maxRows) { - throw new Error( - `Cannot replace: ${data.rows.length} rows exceeds table row limit (${table.maxRows})` - ) - } for (let i = 0; i < data.rows.length; i++) { const row = data.rows[i] @@ -452,11 +473,10 @@ export async function replaceTableRowsWithTx( * column, otherwise inserts a new row. * * Uses a single unique column for matching (not OR across all unique columns) to avoid - * ambiguous matches when multiple unique columns exist. Capacity enforcement lives - * in the `increment_user_table_row_count` trigger (migration 0198). On the insert - * path we acquire the per-table advisory lock and re-check for an existing match - * before inserting, so a concurrent upsert racing on the same conflict target - * cannot produce a duplicate row. + * ambiguous matches when multiple unique columns exist. Capacity is checked best-effort + * against the current plan limit on the insert path. On the insert path we acquire the + * per-table advisory lock and re-check for an existing match before inserting, so a + * concurrent upsert racing on the same conflict target cannot produce a duplicate row. * * @param data - Upsert data including optional conflictTarget * @param table - Table definition @@ -528,8 +548,11 @@ export async function upsertRow( ? sql`${userTableRows.data}->>${targetColumnKey}::text = ${String(targetValue)}` : sql`(${userTableRows.data}->${targetColumnKey}::text)::jsonb = ${JSON.stringify(targetValue)}::jsonb` - // Capacity enforcement for the insert path lives in the `increment_user_table_row_count` - // trigger (migration 0198). The update path doesn't change row_count, so no check needed. + // Resolve the plan limit BEFORE the tx (the lookup is a separate pool read; doing + // it inside the tx would hold a connection + the row-order lock during it). The + // insert branch enforces it; the update path doesn't add a row, so it's exempt. + const rowLimit = await getMaxRowsPerTable(table.workspaceId) + const result = await db.transaction(async (trx) => { await setTableTxTimeouts(trx) // The conflict lookups below match on `data->>key` — unestimatable, and an @@ -612,6 +635,10 @@ export async function upsertRow( } } + if (wouldExceedRowLimit(rowLimit, table.rowCount, 1)) { + throw new TableRowLimitError(rowLimit) + } + const [insertedRow] = await trx .insert(userTableRows) .values({ diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 923cc97e0c..086829e027 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -15,6 +15,7 @@ import { generateId } from '@sim/utils/id' import { and, count, eq, isNull, sql } from 'drizzle-orm' import { generateRestoreName } from '@/lib/core/utils/restore-name' import type { DbOrTx } from '@/lib/db/types' +import { assertRowCapacity } from '@/lib/table/billing' import { generateColumnId, getColumnId, withGeneratedColumnIds } from '@/lib/table/column-keys' import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants' import { EMPTY_JOB_FIELDS, latestJobForTable, latestJobsForTables } from '@/lib/table/jobs/service' @@ -257,7 +258,8 @@ export async function createTable( // Stamp stable ids so the table is id-keyed from its first row write. const schema = withGeneratedColumnIds(data.schema) - // Use provided maxRows (from billing plan) or fall back to default + // Row limits are enforced per-write against the current plan (see assertRowCapacity); the stored + // column is vestigial, so it just takes the caller's value (if any) or the default. const maxRows = data.maxRows ?? TABLE_LIMITS.MAX_ROWS_PER_TABLE const maxTables = data.maxTables ?? TABLE_LIMITS.MAX_TABLES_PER_WORKSPACE @@ -280,6 +282,17 @@ export async function createTable( ? { id: data.jobId, type: data.jobType ?? 'import', startedAt: now } : null + // Starter rows count against the plan too. Checked before the tx (the lookup is a + // separate pool read) — a new table starts empty, so the footprint is just these. + const initialRowCount = data.initialRowCount ?? 0 + if (initialRowCount > 0) { + await assertRowCapacity({ + workspaceId: data.workspaceId, + currentRowCount: 0, + addedRows: initialRowCount, + }) + } + // Wrap count check, duplicate check, and insert in a transaction with FOR UPDATE // to prevent TOCTOU race on the table count limit try { @@ -331,7 +344,6 @@ export async function createTable( }) } - const initialRowCount = data.initialRowCount ?? 0 if (initialRowCount > 0) { const orderKeys = nKeysBetween(null, null, initialRowCount) const rowsToInsert = Array.from({ length: initialRowCount }, (_, i) => ({ diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index badd3356f7..cbadfd75c6 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -433,7 +433,8 @@ export interface CreateTableData { schema: TableSchema workspaceId: string userId: string - /** Optional max rows override based on billing plan. Defaults to TABLE_LIMITS.MAX_ROWS_PER_TABLE. */ + /** Optional stored row cap. Vestigial under plan-based enforcement (the column is no longer + * consulted on insert), but retained so callers that still set it type-check. */ maxRows?: number /** Optional max tables override based on billing plan. Defaults to TABLE_LIMITS.MAX_TABLES_PER_WORKSPACE. */ maxTables?: number diff --git a/packages/db/migrations/0241_drop_table_row_cap_guard.sql b/packages/db/migrations/0241_drop_table_row_cap_guard.sql new file mode 100644 index 0000000000..5b5252e017 --- /dev/null +++ b/packages/db/migrations/0241_drop_table_row_cap_guard.sql @@ -0,0 +1,27 @@ +-- Drop the row-count cap from the statement-level insert trigger (migration 0224). +-- +-- Row-limit enforcement moved to a best-effort application check against the +-- workspace's CURRENT plan limit (see apps/sim/lib/table/billing.ts +-- `assertRowCapacity`). The previous design froze the plan limit into +-- user_table_definitions.max_rows at table-creation time, so plan upgrades never +-- raised an existing table's cap (and downgrades were never enforced). +-- +-- The trigger now only maintains user_table_definitions.row_count -- an +-- unconditional set-based UPDATE mirroring decrement_user_table_row_count_stmt(). +-- The frozen max_rows column is no longer consulted on insert. +CREATE OR REPLACE FUNCTION increment_user_table_row_count_stmt() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE user_table_definitions d + SET row_count = d.row_count + c.n, + updated_at = now() + FROM ( + SELECT table_id, count(*)::int AS n + FROM new_rows + GROUP BY table_id + ) c + WHERE d.id = c.table_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; diff --git a/packages/db/migrations/meta/0241_snapshot.json b/packages/db/migrations/meta/0241_snapshot.json new file mode 100644 index 0000000000..cdc923944b --- /dev/null +++ b/packages/db/migrations/meta/0241_snapshot.json @@ -0,0 +1,16573 @@ +{ + "id": "f7a9fd28-8bd2-4421-a048-a0a768fc8475", + "prevId": "34c63a54-3015-4e6d-9bed-5fe0d53f5741", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_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": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.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.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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 + }, + "applies_to_all_workspaces": { + "name": "applies_to_all_workspaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "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.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 + }, + "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_a2a_executions": { + "name": "total_a2a_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 + } + }, + "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 + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "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_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + }, + "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" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + }, + "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 + }, + "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": {} + } + }, + "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" + } + }, + "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 + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "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_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "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_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 1f61dfeddc..15f2e2e43b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1681,6 +1681,13 @@ "when": 1781670551980, "tag": "0240_table_rows_version", "breakpoints": true + }, + { + "idx": 241, + "version": "7", + "when": 1781700000000, + "tag": "0241_drop_table_row_cap_guard", + "breakpoints": true } ] }