From 2e62dcd1c3feb2c8c01ae233d6bc9af7374b2632 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 10 Apr 2026 20:21:12 -0700 Subject: [PATCH 01/15] Add configurable cleanup for soft delete and logs --- .../api/cron/cleanup-soft-deletes/route.ts | 314 ++++++++++++++ apps/sim/app/api/logs/cleanup/route.ts | 388 ++++++++++-------- .../workspaces/[id]/data-retention/route.ts | 245 +++++++++++ .../[workspaceId]/settings/[section]/page.tsx | 1 + .../settings/[section]/settings.tsx | 8 + .../[workspaceId]/settings/navigation.ts | 10 + .../settings-sidebar/settings-sidebar.tsx | 6 +- .../components/data-retention-settings.tsx | 251 +++++++++++ .../ee/data-retention/hooks/data-retention.ts | 76 ++++ apps/sim/lib/billing/cleanup-dispatcher.ts | 242 +++++++++++ apps/sim/lib/core/config/env.ts | 1 + packages/db/schema.ts | 79 +++- 12 files changed, 1441 insertions(+), 180 deletions(-) create mode 100644 apps/sim/app/api/cron/cleanup-soft-deletes/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/data-retention/route.ts create mode 100644 apps/sim/ee/data-retention/components/data-retention-settings.tsx create mode 100644 apps/sim/ee/data-retention/hooks/data-retention.ts create mode 100644 apps/sim/lib/billing/cleanup-dispatcher.ts diff --git a/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts new file mode 100644 index 00000000000..dc28ee02f0b --- /dev/null +++ b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts @@ -0,0 +1,314 @@ +import { db } from '@sim/db' +import { + a2aAgent, + knowledgeBase, + mcpServers, + memory, + subscription, + userTableDefinitions, + workflow, + workflowFolder, + workflowMcpServer, + workspace, + workspaceFile, + workspaceFiles, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, inArray, isNotNull, isNull, lt, or, sql } from 'drizzle-orm' +import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' +import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' +import { env } from '@/lib/core/config/env' +import { isUsingCloudStorage, StorageService } from '@/lib/uploads' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SoftDeleteCleanupAPI') + +const BATCH_SIZE = 2000 +const MAX_BATCHES_PER_TABLE = 10 + +interface TableCleanupResult { + table: string + deleted: number + failed: number +} + +/** + * Batch-delete rows from a table where the soft-delete column is older than the retention date, + * scoped to the given workspace IDs. + */ +async function cleanupTable( + tableDef: PgTable, + softDeleteCol: PgColumn, + workspaceIdCol: PgColumn, + workspaceIds: string[], + retentionDate: Date, + tableName: string +): Promise { + const result: TableCleanupResult = { table: tableName, deleted: 0, failed: 0 } + + if (workspaceIds.length === 0) { + logger.info(`[${tableName}] Skipped — no workspaces in this tier`) + return result + } + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < MAX_BATCHES_PER_TABLE) { + try { + const deleted = await db + .delete(tableDef) + .where( + and( + inArray(workspaceIdCol, workspaceIds), + isNotNull(softDeleteCol), + lt(softDeleteCol, retentionDate) + ) + ) + .returning({ id: sql`id` }) + + result.deleted += deleted.length + hasMore = deleted.length === BATCH_SIZE + batchesProcessed++ + + if (deleted.length > 0) { + logger.info(`[${tableName}] Batch ${batchesProcessed}: deleted ${deleted.length} rows`) + } else { + logger.info(`[${tableName}] No expired soft-deleted rows found`) + } + } catch (error) { + result.failed++ + logger.error(`[${tableName}] Batch delete failed:`, { error }) + hasMore = false + } + } + + return result +} + +/** + * Clean up soft-deleted workspace files from cloud storage before hard-deleting. + */ +async function cleanupWorkspaceFileStorage( + workspaceIds: string[], + retentionDate: Date +): Promise<{ filesDeleted: number; filesFailed: number }> { + const stats = { filesDeleted: 0, filesFailed: 0 } + + if (!isUsingCloudStorage() || workspaceIds.length === 0) return stats + + // Fetch keys of files about to be deleted + const filesToDelete = await db + .select({ key: workspaceFiles.key }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.workspaceId, workspaceIds), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, retentionDate) + ) + ) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + for (const file of filesToDelete) { + try { + await StorageService.deleteFile({ key: file.key, context: 'workspace' }) + stats.filesDeleted++ + } catch (error) { + stats.filesFailed++ + logger.error(`Failed to delete storage file ${file.key}:`, { error }) + } + } + + return stats +} + +/** All tables to clean up with their soft-delete column and workspace column. */ +const CLEANUP_TARGETS = [ + { table: workflow, softDeleteCol: workflow.archivedAt, wsCol: workflow.workspaceId, name: 'workflow' }, + { table: workflowFolder, softDeleteCol: workflowFolder.archivedAt, wsCol: workflowFolder.workspaceId, name: 'workflowFolder' }, + { table: knowledgeBase, softDeleteCol: knowledgeBase.deletedAt, wsCol: knowledgeBase.workspaceId, name: 'knowledgeBase' }, + { table: userTableDefinitions, softDeleteCol: userTableDefinitions.archivedAt, wsCol: userTableDefinitions.workspaceId, name: 'userTableDefinitions' }, + { table: workspaceFile, softDeleteCol: workspaceFile.deletedAt, wsCol: workspaceFile.workspaceId, name: 'workspaceFile' }, + { table: workspaceFiles, softDeleteCol: workspaceFiles.deletedAt, wsCol: workspaceFiles.workspaceId, name: 'workspaceFiles' }, + { table: memory, softDeleteCol: memory.deletedAt, wsCol: memory.workspaceId, name: 'memory' }, + { table: mcpServers, softDeleteCol: mcpServers.deletedAt, wsCol: mcpServers.workspaceId, name: 'mcpServers' }, + { table: workflowMcpServer, softDeleteCol: workflowMcpServer.deletedAt, wsCol: workflowMcpServer.workspaceId, name: 'workflowMcpServer' }, + { table: a2aAgent, softDeleteCol: a2aAgent.archivedAt, wsCol: a2aAgent.workspaceId, name: 'a2aAgent' }, +] as const + +async function cleanupTier( + workspaceIds: string[], + retentionDate: Date, + tierLabel: string +): Promise<{ tables: TableCleanupResult[]; filesDeleted: number; filesFailed: number }> { + const tables: TableCleanupResult[] = [] + + if (workspaceIds.length === 0) { + return { tables, filesDeleted: 0, filesFailed: 0 } + } + + // Clean cloud storage files before hard-deleting file metadata rows + const fileStats = await cleanupWorkspaceFileStorage(workspaceIds, retentionDate) + + for (const target of CLEANUP_TARGETS) { + const result = await cleanupTable( + target.table, + target.softDeleteCol, + target.wsCol, + workspaceIds, + retentionDate, + `${tierLabel}/${target.name}` + ) + tables.push(result) + } + + return { tables, ...fileStats } +} + +export async function GET(request: NextRequest) { + try { + const authError = verifyCronAuth(request, 'soft-delete cleanup') + if (authError) return authError + + const startTime = Date.now() + + const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') + const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') + + const freeRetentionDate = new Date(Date.now() - freeRetentionDays * 24 * 60 * 60 * 1000) + const paidRetentionDate = new Date(Date.now() - paidRetentionDays * 24 * 60 * 60 * 1000) + + logger.info('Starting soft-delete cleanup', { + freeRetentionDays, + paidRetentionDays, + freeRetentionDate: freeRetentionDate.toISOString(), + paidRetentionDate: paidRetentionDate.toISOString(), + }) + + // --- Group 1: Free workspaces --- + + const freeWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .leftJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + sqlIsPaid(subscription.plan) + ) + ) + .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) + + const freeIds = freeWorkspaceRows.map((w) => w.id) + logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) + const freeResults = await cleanupTier(freeIds, freeRetentionDate, 'free') + logger.info(`[free] Result: ${freeResults.tables.reduce((s, t) => s + t.deleted, 0)} total rows deleted across ${CLEANUP_TARGETS.length} tables`) + + // --- Group 2: Pro/Team workspaces --- + + const paidWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) + ) + ) + .where(isNull(workspace.archivedAt)) + + const paidIds = paidWorkspaceRows.map((w) => w.id) + logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) + const paidResults = await cleanupTier(paidIds, paidRetentionDate, 'paid') + logger.info(`[paid] Result: ${paidResults.tables.reduce((s, t) => s + t.deleted, 0)} total rows deleted across ${CLEANUP_TARGETS.length} tables`) + + // --- Group 3: Enterprise with custom softDeleteRetentionHours --- + + const enterpriseWorkspaceRows = await db + .select({ + id: workspace.id, + softDeleteRetentionHours: workspace.softDeleteRetentionHours, + }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + eq(subscription.plan, 'enterprise') + ) + ) + .where( + and(isNull(workspace.archivedAt), isNotNull(workspace.softDeleteRetentionHours)) + ) + + const enterpriseGroups = new Map() + for (const ws of enterpriseWorkspaceRows) { + const hours = ws.softDeleteRetentionHours! + const group = enterpriseGroups.get(hours) ?? [] + group.push(ws.id) + enterpriseGroups.set(hours, group) + } + + logger.info(`[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct retention periods). Workspaces with NULL retention are skipped.`) + + const enterpriseTables: TableCleanupResult[] = [] + let enterpriseFilesDeleted = 0 + let enterpriseFilesFailed = 0 + + for (const [hours, ids] of enterpriseGroups) { + const retentionDate = new Date(Date.now() - hours * 60 * 60 * 1000) + logger.info(`[enterprise-${hours}h] Processing ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}`) + const groupResults = await cleanupTier(ids, retentionDate, `enterprise-${hours}h`) + enterpriseTables.push(...groupResults.tables) + enterpriseFilesDeleted += groupResults.filesDeleted + enterpriseFilesFailed += groupResults.filesFailed + } + + const timeElapsed = (Date.now() - startTime) / 1000 + + const totalDeleted = (results: { tables: TableCleanupResult[] }) => + results.tables.reduce((sum, t) => sum + t.deleted, 0) + + return NextResponse.json({ + message: `Soft-delete cleanup completed in ${timeElapsed.toFixed(2)}s`, + tiers: { + free: { + workspaces: freeIds.length, + retentionDays: freeRetentionDays, + totalDeleted: totalDeleted(freeResults), + filesDeleted: freeResults.filesDeleted, + filesFailed: freeResults.filesFailed, + tables: freeResults.tables, + }, + paid: { + workspaces: paidIds.length, + retentionDays: paidRetentionDays, + totalDeleted: totalDeleted(paidResults), + filesDeleted: paidResults.filesDeleted, + filesFailed: paidResults.filesFailed, + tables: paidResults.tables, + }, + enterprise: { + workspaces: enterpriseWorkspaceRows.length, + groups: enterpriseGroups.size, + totalDeleted: enterpriseTables.reduce((sum, t) => sum + t.deleted, 0), + filesDeleted: enterpriseFilesDeleted, + filesFailed: enterpriseFilesFailed, + tables: enterpriseTables, + }, + }, + }) + } catch (error) { + logger.error('Error in soft-delete cleanup:', { error }) + return NextResponse.json({ error: 'Failed to process soft-delete cleanup' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/logs/cleanup/route.ts b/apps/sim/app/api/logs/cleanup/route.ts index 85623e7d2a8..16f9069aa3c 100644 --- a/apps/sim/app/api/logs/cleanup/route.ts +++ b/apps/sim/app/api/logs/cleanup/route.ts @@ -1,10 +1,10 @@ import { db } from '@sim/db' import { subscription, workflowExecutionLogs, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray, isNull, lt } from 'drizzle-orm' +import { and, eq, inArray, isNotNull, isNull, lt, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { sqlIsPaid } from '@/lib/billing/plan-helpers' +import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { env } from '@/lib/core/config/env' import { snapshotService } from '@/lib/logs/execution/snapshot/service' @@ -15,185 +15,243 @@ export const dynamic = 'force-dynamic' const logger = createLogger('LogsCleanupAPI') const BATCH_SIZE = 2000 +const MAX_BATCHES_PER_TIER = 10 -export async function GET(request: NextRequest) { - try { - const authError = verifyCronAuth(request, 'logs cleanup') - if (authError) { - return authError +interface TierResults { + total: number + deleted: number + deleteFailed: number + filesTotal: number + filesDeleted: number + filesDeleteFailed: number +} + +function emptyTierResults(): TierResults { + return { total: 0, deleted: 0, deleteFailed: 0, filesTotal: 0, filesDeleted: 0, filesDeleteFailed: 0 } +} + +async function deleteExecutionFiles( + files: unknown, + results: TierResults +): Promise { + if (!isUsingCloudStorage() || !files || !Array.isArray(files)) return + + for (const file of files) { + if (!file || typeof file !== 'object' || !file.key) continue + results.filesTotal++ + try { + await StorageService.deleteFile({ key: file.key, context: 'execution' }) + const { deleteFileMetadata } = await import('@/lib/uploads/server/metadata') + await deleteFileMetadata(file.key) + results.filesDeleted++ + } catch (fileError) { + results.filesDeleteFailed++ + logger.error(`Failed to delete file ${file.key}:`, { fileError }) } + } +} - const retentionDate = new Date() - retentionDate.setDate(retentionDate.getDate() - Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7')) +/** + * Run batch cleanup for a set of workspace IDs with a given retention date. + * Selects logs to find files, deletes files from storage, then deletes log rows. + */ +async function cleanupTier( + workspaceIds: string[], + retentionDate: Date, + tierLabel: string +): Promise { + const results = emptyTierResults() - const freeWorkspacesSubquery = db - .select({ id: workspace.id }) - .from(workspace) - .leftJoin( - subscription, + if (workspaceIds.length === 0) return results + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < MAX_BATCHES_PER_TIER) { + // Select logs with files before deleting so we can clean up storage + const batch = await db + .select({ + id: workflowExecutionLogs.id, + files: workflowExecutionLogs.files, + }) + .from(workflowExecutionLogs) + .where( and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - sqlIsPaid(subscription.plan) + inArray(workflowExecutionLogs.workspaceId, workspaceIds), + lt(workflowExecutionLogs.startedAt, retentionDate) ) ) - .where(isNull(subscription.id)) - - const results = { - enhancedLogs: { - total: 0, - archived: 0, - archiveFailed: 0, - deleted: 0, - deleteFailed: 0, - }, - files: { - total: 0, - deleted: 0, - deleteFailed: 0, - }, - snapshots: { - cleaned: 0, - cleanupFailed: 0, - }, + .limit(BATCH_SIZE) + + results.total += batch.length + + if (batch.length === 0) { + hasMore = false + break } - const startTime = Date.now() - const MAX_BATCHES = 10 - - let batchesProcessed = 0 - let hasMoreLogs = true - - logger.info('Starting enhanced logs cleanup for free-plan workspaces') - - while (hasMoreLogs && batchesProcessed < MAX_BATCHES) { - const oldEnhancedLogs = await db - .select({ - id: workflowExecutionLogs.id, - workflowId: workflowExecutionLogs.workflowId, - executionId: workflowExecutionLogs.executionId, - stateSnapshotId: workflowExecutionLogs.stateSnapshotId, - level: workflowExecutionLogs.level, - trigger: workflowExecutionLogs.trigger, - startedAt: workflowExecutionLogs.startedAt, - endedAt: workflowExecutionLogs.endedAt, - totalDurationMs: workflowExecutionLogs.totalDurationMs, - executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, - files: workflowExecutionLogs.files, - createdAt: workflowExecutionLogs.createdAt, - }) - .from(workflowExecutionLogs) - .where( - and( - inArray(workflowExecutionLogs.workspaceId, freeWorkspacesSubquery), - lt(workflowExecutionLogs.startedAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - results.enhancedLogs.total += oldEnhancedLogs.length - - for (const log of oldEnhancedLogs) { - const today = new Date().toISOString().split('T')[0] - - const enhancedLogKey = `logs/archived/${today}/${log.id}.json` - const enhancedLogData = JSON.stringify({ - ...log, - archivedAt: new Date().toISOString(), - logType: 'enhanced', - }) - - try { - await StorageService.uploadFile({ - file: Buffer.from(enhancedLogData), - fileName: enhancedLogKey, - contentType: 'application/json', - context: 'logs', - preserveKey: true, - customKey: enhancedLogKey, - metadata: { - logId: String(log.id), - workflowId: String(log.workflowId ?? ''), - executionId: String(log.executionId), - logType: 'enhanced', - archivedAt: new Date().toISOString(), - }, - }) - - results.enhancedLogs.archived++ - - if (isUsingCloudStorage() && log.files && Array.isArray(log.files)) { - for (const file of log.files) { - if (file && typeof file === 'object' && file.key) { - results.files.total++ - try { - await StorageService.deleteFile({ - key: file.key, - context: 'execution', - }) - results.files.deleted++ - - // Also delete from workspace_files table - const { deleteFileMetadata } = await import('@/lib/uploads/server/metadata') - await deleteFileMetadata(file.key) - - logger.info(`Deleted execution file: ${file.key}`) - } catch (fileError) { - results.files.deleteFailed++ - logger.error(`Failed to delete file ${file.key}:`, { fileError }) - } - } - } - } - - try { - const deleteResult = await db - .delete(workflowExecutionLogs) - .where(eq(workflowExecutionLogs.id, log.id)) - .returning({ id: workflowExecutionLogs.id }) - - if (deleteResult.length > 0) { - results.enhancedLogs.deleted++ - } else { - results.enhancedLogs.deleteFailed++ - logger.warn(`Failed to delete log ${log.id} after archiving: No rows deleted`) - } - } catch (deleteError) { - results.enhancedLogs.deleteFailed++ - logger.error(`Error deleting log ${log.id} after archiving:`, { deleteError }) - } - } catch (archiveError) { - results.enhancedLogs.archiveFailed++ - logger.error(`Failed to archive log ${log.id}:`, { archiveError }) - } - } - - batchesProcessed++ - hasMoreLogs = oldEnhancedLogs.length === BATCH_SIZE - - logger.info(`Processed logs batch ${batchesProcessed}: ${oldEnhancedLogs.length} logs`) + // Delete associated files from cloud storage + for (const log of batch) { + await deleteExecutionFiles(log.files, results) } + // Batch delete the log rows + const logIds = batch.map((log) => log.id) try { - const snapshotRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') + 1 // Keep snapshots 1 day longer - const cleanedSnapshots = await snapshotService.cleanupOrphanedSnapshots(snapshotRetentionDays) - results.snapshots.cleaned = cleanedSnapshots - logger.info(`Cleaned up ${cleanedSnapshots} orphaned snapshots`) - } catch (snapshotError) { - results.snapshots.cleanupFailed = 1 - logger.error('Error cleaning up orphaned snapshots:', { snapshotError }) + const deleted = await db + .delete(workflowExecutionLogs) + .where(inArray(workflowExecutionLogs.id, logIds)) + .returning({ id: workflowExecutionLogs.id }) + + results.deleted += deleted.length + } catch (deleteError) { + results.deleteFailed += logIds.length + logger.error(`Batch delete failed for ${tierLabel}:`, { deleteError }) } - const timeElapsed = (Date.now() - startTime) / 1000 - const reachedLimit = batchesProcessed >= MAX_BATCHES && hasMoreLogs + batchesProcessed++ + hasMore = batch.length === BATCH_SIZE + + logger.info( + `[${tierLabel}] Batch ${batchesProcessed}: ${batch.length} logs processed` + ) + } + + return results +} - return NextResponse.json({ - message: `Processed ${batchesProcessed} enhanced log batches (${results.enhancedLogs.total} logs, ${results.files.total} files) in ${timeElapsed.toFixed(2)}s${reachedLimit ? ' (batch limit reached)' : ''}`, - results, - complete: !hasMoreLogs, - batchLimitReached: reachedLimit, +async function runLogCleanup() { + const startTime = Date.now() + + const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') + const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') + + const freeRetentionDate = new Date(Date.now() - freeRetentionDays * 24 * 60 * 60 * 1000) + const paidRetentionDate = new Date(Date.now() - paidRetentionDays * 24 * 60 * 60 * 1000) + + logger.info('Starting log cleanup', { + freeRetentionDays, + paidRetentionDays, + freeRetentionDate: freeRetentionDate.toISOString(), + paidRetentionDate: paidRetentionDate.toISOString(), + }) + + // --- Group 1: Free workspaces (no paid subscription) --- + + const freeWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .leftJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + sqlIsPaid(subscription.plan) + ) + ) + .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) + + const freeIds = freeWorkspaceRows.map((w) => w.id) + logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) + const freeResults = await cleanupTier(freeIds, freeRetentionDate, 'free') + logger.info(`[free] Result: ${freeResults.deleted} deleted, ${freeResults.deleteFailed} failed out of ${freeResults.total} candidates`) + + // --- Group 2: Pro/Team workspaces (paid non-enterprise) --- + + const paidWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) + ) + ) + .where(isNull(workspace.archivedAt)) + + const paidIds = paidWorkspaceRows.map((w) => w.id) + logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) + const paidResults = await cleanupTier(paidIds, paidRetentionDate, 'paid') + logger.info(`[paid] Result: ${paidResults.deleted} deleted, ${paidResults.deleteFailed} failed out of ${paidResults.total} candidates`) + + // --- Group 3: Enterprise with custom logRetentionHours --- + // Enterprise with logRetentionHours = NULL → no cleanup (infinite retention) + + const enterpriseWorkspaceRows = await db + .select({ + id: workspace.id, + logRetentionHours: workspace.logRetentionHours, }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + eq(subscription.plan, 'enterprise') + ) + ) + .where( + and(isNull(workspace.archivedAt), isNotNull(workspace.logRetentionHours)) + ) + + const enterpriseGroups = new Map() + for (const ws of enterpriseWorkspaceRows) { + const hours = ws.logRetentionHours! + const group = enterpriseGroups.get(hours) ?? [] + group.push(ws.id) + enterpriseGroups.set(hours, group) + } + + logger.info(`[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct retention periods). Workspaces with NULL retention are skipped.`) + + const enterpriseResults = emptyTierResults() + for (const [hours, ids] of enterpriseGroups) { + const retentionDate = new Date(Date.now() - hours * 60 * 60 * 1000) + logger.info(`[enterprise-${hours}h] Processing ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}`) + const groupResults = await cleanupTier(ids, retentionDate, `enterprise-${hours}h`) + enterpriseResults.total += groupResults.total + enterpriseResults.deleted += groupResults.deleted + enterpriseResults.deleteFailed += groupResults.deleteFailed + enterpriseResults.filesTotal += groupResults.filesTotal + enterpriseResults.filesDeleted += groupResults.filesDeleted + enterpriseResults.filesDeleteFailed += groupResults.filesDeleteFailed + } + + // --- Snapshot cleanup --- + + try { + const allRetentionDays = [freeRetentionDays, paidRetentionDays] + for (const hours of enterpriseGroups.keys()) { + allRetentionDays.push(hours / 24) + } + const shortestRetentionDays = Math.min(...allRetentionDays) + const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots( + shortestRetentionDays + 1 + ) + logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`) + } catch (snapshotError) { + logger.error('Error cleaning up orphaned snapshots:', { snapshotError }) + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Log cleanup completed in ${timeElapsed.toFixed(2)}s`, { + free: { workspaces: freeIds.length, ...freeResults }, + paid: { workspaces: paidIds.length, ...paidResults }, + enterprise: { workspaces: enterpriseWorkspaceRows.length, groups: enterpriseGroups.size, ...enterpriseResults }, + }) +} + +export async function GET(request: NextRequest) { + try { + const authError = verifyCronAuth(request, 'logs cleanup') + if (authError) return authError + + await runLogCleanup() + + return NextResponse.json({ success: true }) } catch (error) { logger.error('Error in log cleanup process:', { error }) return NextResponse.json({ error: 'Failed to process log cleanup' }, { status: 500 }) diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts new file mode 100644 index 00000000000..d78b1e2df28 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -0,0 +1,245 @@ +import { db } from '@sim/db' +import { workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { getSession } from '@/lib/auth' +import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' +import { isEnterprisePlan } from '@/lib/billing/core/subscription' +import { + checkEnterprisePlan, + checkProPlan, + checkTeamPlan, +} from '@/lib/billing/subscriptions/utils' +import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('DataRetentionAPI') + +const MIN_HOURS = 24 +const MAX_HOURS = 43800 // 5 years + +const FREE_LOG_RETENTION_HOURS = 7 * 24 +const FREE_SOFT_DELETE_RETENTION_HOURS = 7 * 24 +const FREE_TASK_REDACTION_HOURS = null // never + +const PRO_LOG_RETENTION_HOURS = 30 * 24 +const PRO_SOFT_DELETE_RETENTION_HOURS = 30 * 24 +const PRO_TASK_REDACTION_HOURS = 30 * 24 + +interface PlanDefaults { + logRetentionHours: number + softDeleteRetentionHours: number + taskRedactionHours: number | null +} + +function getPlanDefaults(plan: 'free' | 'pro' | 'enterprise'): PlanDefaults { + switch (plan) { + case 'enterprise': + case 'pro': + return { + logRetentionHours: PRO_LOG_RETENTION_HOURS, + softDeleteRetentionHours: PRO_SOFT_DELETE_RETENTION_HOURS, + taskRedactionHours: PRO_TASK_REDACTION_HOURS, + } + default: + return { + logRetentionHours: FREE_LOG_RETENTION_HOURS, + softDeleteRetentionHours: FREE_SOFT_DELETE_RETENTION_HOURS, + taskRedactionHours: FREE_TASK_REDACTION_HOURS, + } + } +} + +async function resolveWorkspacePlan( + billedAccountUserId: string +): Promise<'free' | 'pro' | 'enterprise'> { + const sub = await getHighestPrioritySubscription(billedAccountUserId) + if (!sub) return 'free' + if (checkEnterprisePlan(sub)) return 'enterprise' + if (checkTeamPlan(sub) || checkProPlan(sub)) return 'pro' + return 'free' +} + +const updateRetentionSchema = z.object({ + logRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), + softDeleteRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), + taskRedactionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), +}) + +/** + * GET /api/workspaces/[id]/data-retention + * Returns the workspace's data retention config including plan defaults and + * whether the workspace is on an enterprise plan. + */ +export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workspaceId } = await params + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (!permission) { + return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 }) + } + + const [ws] = await db + .select({ + logRetentionHours: workspace.logRetentionHours, + softDeleteRetentionHours: workspace.softDeleteRetentionHours, + taskRedactionHours: workspace.taskRedactionHours, + billedAccountUserId: workspace.billedAccountUserId, + }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!ws) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + + const plan = await resolveWorkspacePlan(ws.billedAccountUserId) + const defaults = getPlanDefaults(plan) + const isEnterpriseWorkspace = plan === 'enterprise' + + return NextResponse.json({ + success: true, + data: { + plan, + isEnterprise: isEnterpriseWorkspace, + defaults, + configured: { + logRetentionHours: ws.logRetentionHours, + softDeleteRetentionHours: ws.softDeleteRetentionHours, + taskRedactionHours: ws.taskRedactionHours, + }, + effective: isEnterpriseWorkspace + ? { + logRetentionHours: ws.logRetentionHours, + softDeleteRetentionHours: ws.softDeleteRetentionHours, + taskRedactionHours: ws.taskRedactionHours, + } + : { + logRetentionHours: defaults.logRetentionHours, + softDeleteRetentionHours: defaults.softDeleteRetentionHours, + taskRedactionHours: defaults.taskRedactionHours, + }, + }, + }) + } catch (error) { + logger.error('Failed to get data retention settings', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * PUT /api/workspaces/[id]/data-retention + * Updates the workspace's data retention settings. + * Requires admin permission and enterprise plan. + */ +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workspaceId } = await params + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'admin') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const billedAccountUserId = await getWorkspaceBilledAccountUserId(workspaceId) + if (!billedAccountUserId) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + + const hasEnterprise = await isEnterprisePlan(billedAccountUserId) + if (!hasEnterprise) { + return NextResponse.json( + { error: 'Data Retention configuration is available on Enterprise plans only' }, + { status: 403 } + ) + } + + const body = await request.json() + const parsed = updateRetentionSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.errors[0]?.message ?? 'Invalid request body' }, + { status: 400 } + ) + } + + const updateData: Record = { updatedAt: new Date() } + + if (parsed.data.logRetentionHours !== undefined) { + updateData.logRetentionHours = parsed.data.logRetentionHours + } + if (parsed.data.softDeleteRetentionHours !== undefined) { + updateData.softDeleteRetentionHours = parsed.data.softDeleteRetentionHours + } + if (parsed.data.taskRedactionHours !== undefined) { + updateData.taskRedactionHours = parsed.data.taskRedactionHours + } + + const [updated] = await db + .update(workspace) + .set(updateData) + .where(eq(workspace.id, workspaceId)) + .returning({ + logRetentionHours: workspace.logRetentionHours, + softDeleteRetentionHours: workspace.softDeleteRetentionHours, + taskRedactionHours: workspace.taskRedactionHours, + }) + + if (!updated) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + + recordAudit({ + workspaceId, + actorId: session.user.id, + action: AuditAction.ORGANIZATION_UPDATED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: workspaceId, + actorName: session.user.name ?? undefined, + actorEmail: session.user.email ?? undefined, + description: 'Updated data retention settings', + metadata: { changes: parsed.data }, + request, + }) + + const defaults = getPlanDefaults('enterprise') + + return NextResponse.json({ + success: true, + data: { + plan: 'enterprise' as const, + isEnterprise: true, + defaults, + configured: { + logRetentionHours: updated.logRetentionHours, + softDeleteRetentionHours: updated.softDeleteRetentionHours, + taskRedactionHours: updated.taskRedactionHours, + }, + effective: { + logRetentionHours: updated.logRetentionHours ?? defaults.logRetentionHours, + softDeleteRetentionHours: + updated.softDeleteRetentionHours ?? defaults.softDeleteRetentionHours, + taskRedactionHours: updated.taskRedactionHours ?? defaults.taskRedactionHours, + }, + }, + }) + } catch (error) { + logger.error('Failed to update data retention settings', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index 2c0db1d6e1d..79f67bab10c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -25,6 +25,7 @@ const SECTION_TITLES: Record = { skills: 'Skills', 'workflow-mcp-servers': 'MCP Servers', 'credential-sets': 'Email Polling', + 'data-retention': 'Data Retention', 'recently-deleted': 'Recently Deleted', debug: 'Debug', } as const diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index de5096caefd..12c6948d4cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -169,6 +169,13 @@ const AuditLogs = dynamic( const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO), { loading: () => , }) +const DataRetentionSettings = dynamic( + () => + import('@/ee/data-retention/components/data-retention-settings').then( + (m) => m.DataRetentionSettings + ), + { loading: () => } +) const WhitelabelingSettings = dynamic( () => import('@/ee/whitelabeling/components/whitelabeling-settings').then( @@ -221,6 +228,7 @@ export function SettingsPage({ section }: SettingsPageProps) { {isBillingEnabled && effectiveSection === 'subscription' && } {isBillingEnabled && effectiveSection === 'team' && } {effectiveSection === 'sso' && } + {effectiveSection === 'data-retention' && } {effectiveSection === 'whitelabeling' && } {effectiveSection === 'byok' && } {effectiveSection === 'copilot' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index 382c087f90c..56226eb89ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -2,6 +2,7 @@ import { Card, ClipboardList, Connections, + Database, HexSimple, Key, KeySquare, @@ -42,6 +43,7 @@ export type SettingsSection = | 'workflow-mcp-servers' | 'inbox' | 'admin' + | 'data-retention' | 'mothership' | 'recently-deleted' @@ -178,6 +180,14 @@ export const allNavigationItems: NavigationItem[] = [ requiresEnterprise: true, selfHostedOverride: isSSOEnabled, }, + { + id: 'data-retention', + label: 'Data Retention', + icon: Database, + section: 'enterprise', + requiresEnterprise: true, + showWhenLocked: true, + }, { id: 'whitelabeling', label: 'Whitelabeling', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index 9c1bc3fafcb..947ce42a100 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -152,7 +152,11 @@ export function SettingsSidebar({ return false } - if (item.requiresEnterprise && (!hasEnterprisePlan || !isOrgAdminOrOwner)) { + if ( + item.requiresEnterprise && + (!hasEnterprisePlan || !isOrgAdminOrOwner) && + !item.showWhenLocked + ) { return false } diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx new file mode 100644 index 00000000000..b3db35fd5b8 --- /dev/null +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -0,0 +1,251 @@ +'use client' + +import { useCallback, useState } from 'react' +import { createLogger } from '@sim/logger' +import { Loader2 } from 'lucide-react' +import { useParams } from 'next/navigation' +import { Button } from '@/components/emcn' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { cn } from '@/lib/core/utils/cn' +import { + type DataRetentionResponse, + useUpdateWorkspaceRetention, + useWorkspaceRetention, +} from '@/ee/data-retention/hooks/data-retention' + +const logger = createLogger('DataRetentionSettings') + +const DAY_OPTIONS = [ + { value: '1', label: '1 day' }, + { value: '3', label: '3 days' }, + { value: '7', label: '7 days' }, + { value: '14', label: '14 days' }, + { value: '30', label: '30 days' }, + { value: '60', label: '60 days' }, + { value: '90', label: '90 days' }, + { value: '180', label: '180 days' }, + { value: '365', label: '1 year' }, + { value: '1825', label: '5 years' }, + { value: 'never', label: 'Forever' }, +] as const + +interface RetentionFieldProps { + label: string + description: string + value: string + onChange: (value: string) => void + disabled: boolean +} + +function RetentionField({ label, description, value, onChange, disabled }: RetentionFieldProps) { + return ( +
+
+ {label} +

{description}

+
+ {disabled ? ( + + {DAY_OPTIONS.find((o) => o.value === value)?.label ?? `${value} days`} + + ) : ( + + )} +
+ ) +} + +function hoursToDisplayDays(hours: number | null): string { + if (hours === null) return 'never' + return String(Math.round(hours / 24)) +} + +function daysToHours(days: string): number | null { + if (days === 'never') return null + return Number(days) * 24 +} + +function planLabel(plan: string): string { + switch (plan) { + case 'enterprise': + return 'Enterprise' + case 'pro': + return 'Pro' + default: + return 'Free' + } +} + +function LockedView({ data }: { data: DataRetentionResponse }) { + return ( +
+

+ Data retention policies control how long your workspace data is kept before automatic + cleanup. Custom retention periods are available on Enterprise plans. +

+
+ {}} + disabled + /> + {}} + disabled + /> + {}} + disabled + /> +
+

+ {planLabel(data.plan)} plan defaults. Upgrade to Enterprise to customize retention periods. +

+
+ ) +} + +function EditableView({ data, workspaceId }: { data: DataRetentionResponse; workspaceId: string }) { + const updateMutation = useUpdateWorkspaceRetention() + + const [logDays, setLogDays] = useState(hoursToDisplayDays(data.effective.logRetentionHours)) + const [softDeleteDays, setSoftDeleteDays] = useState( + hoursToDisplayDays(data.effective.softDeleteRetentionHours) + ) + const [taskRedactionDays, setTaskRedactionDays] = useState( + hoursToDisplayDays(data.effective.taskRedactionHours) + ) + + const [saveError, setSaveError] = useState(null) + const [saveSuccess, setSaveSuccess] = useState(false) + + const handleSave = useCallback(async () => { + setSaveError(null) + setSaveSuccess(false) + + try { + await updateMutation.mutateAsync({ + workspaceId, + settings: { + logRetentionHours: daysToHours(logDays), + softDeleteRetentionHours: daysToHours(softDeleteDays), + taskRedactionHours: daysToHours(taskRedactionDays), + }, + }) + setSaveSuccess(true) + setTimeout(() => setSaveSuccess(false), 3000) + } catch (error) { + logger.error('Failed to save data retention settings', { error }) + setSaveError(error instanceof Error ? error.message : 'Failed to save settings') + } + }, [workspaceId, logDays, softDeleteDays, taskRedactionDays]) + + return ( +
+

+ Configure how long your workspace data is retained before automatic cleanup. Values apply to + all workflows in this workspace. +

+
+ + + +
+ +
+ + {saveSuccess && ( + Settings saved successfully. + )} + {saveError && {saveError}} +
+
+ ) +} + +export function DataRetentionSettings() { + const params = useParams() + const workspaceId = params.workspaceId as string + + const { data, isLoading, error } = useWorkspaceRetention(workspaceId) + + if (isLoading) { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) + } + + if (error || !data) { + return ( +
+ Failed to load data retention settings. +
+ ) + } + + if (!data.isEnterprise) { + return + } + + return +} diff --git a/apps/sim/ee/data-retention/hooks/data-retention.ts b/apps/sim/ee/data-retention/hooks/data-retention.ts new file mode 100644 index 00000000000..dde645b353d --- /dev/null +++ b/apps/sim/ee/data-retention/hooks/data-retention.ts @@ -0,0 +1,76 @@ +'use client' + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +export interface RetentionValues { + logRetentionHours: number | null + softDeleteRetentionHours: number | null + taskRedactionHours: number | null +} + +export interface DataRetentionResponse { + plan: 'free' | 'pro' | 'enterprise' + isEnterprise: boolean + defaults: RetentionValues + configured: RetentionValues + effective: RetentionValues +} + +export const dataRetentionKeys = { + all: ['dataRetention'] as const, + settings: (workspaceId: string) => [...dataRetentionKeys.all, 'settings', workspaceId] as const, +} + +async function fetchDataRetention( + workspaceId: string, + signal?: AbortSignal +): Promise { + const response = await fetch(`/api/workspaces/${workspaceId}/data-retention`, { signal }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.error ?? 'Failed to fetch data retention settings') + } + + const { data } = await response.json() + return data as DataRetentionResponse +} + +export function useWorkspaceRetention(workspaceId: string | undefined) { + return useQuery({ + queryKey: dataRetentionKeys.settings(workspaceId ?? ''), + queryFn: ({ signal }) => fetchDataRetention(workspaceId as string, signal), + enabled: Boolean(workspaceId), + staleTime: 60 * 1000, + }) +} + +interface UpdateRetentionVariables { + workspaceId: string + settings: Partial +} + +export function useUpdateWorkspaceRetention() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ workspaceId, settings }: UpdateRetentionVariables) => { + const response = await fetch(`/api/workspaces/${workspaceId}/data-retention`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.error ?? 'Failed to update data retention settings') + } + + const { data } = await response.json() + return data as DataRetentionResponse + }, + onSettled: (_data, _error, { workspaceId }) => { + queryClient.invalidateQueries({ queryKey: dataRetentionKeys.settings(workspaceId) }) + }, + }) +} diff --git a/apps/sim/lib/billing/cleanup-dispatcher.ts b/apps/sim/lib/billing/cleanup-dispatcher.ts new file mode 100644 index 00000000000..4c20f8ca937 --- /dev/null +++ b/apps/sim/lib/billing/cleanup-dispatcher.ts @@ -0,0 +1,242 @@ +import { db } from '@sim/db' +import { subscription, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { tasks } from '@trigger.dev/sdk' +import { and, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' +import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' +import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' +import { getJobQueue } from '@/lib/core/async-jobs' +import { shouldExecuteInline } from '@/lib/core/async-jobs/config' +import { isTriggerAvailable } from '@/lib/knowledge/documents/service' + +const logger = createLogger('RetentionDispatcher') + +const BATCH_TRIGGER_CHUNK_SIZE = 1000 + +export type CleanupJobType = + | 'cleanup-logs' + | 'cleanup-soft-deletes' + | 'cleanup-tasks' + +export type WorkspaceRetentionColumn = + | 'logRetentionHours' + | 'softDeleteRetentionHours' + | 'taskCleanupHours' + +export type CleanupJobPayload = + | { tier: 'free' } + | { tier: 'paid' } + | { tier: 'enterprise'; workspaceId: string } + +const FREE_RETENTION_DAYS = 7 +const PAID_RETENTION_DAYS = 30 + +/** + * Default retention per tier + column. Single source of truth — used by both the + * API (to show effective values in the UI) and background tasks (to apply cleanup). + * + * - Free: 7-day log + soft-delete retention. No task cleanup, no PII redaction. + * - Paid: 30-day soft-delete retention only. Logs kept forever. No task cleanup, no PII redaction. + * - Enterprise: everything defaults to NULL (never). Must be explicitly configured + * per-workspace. PII redaction is enterprise-only. + */ +export function getRetentionDefaultHours( + tier: 'free' | 'paid' | 'enterprise', + column: WorkspaceRetentionColumn +): number | null { + if (tier === 'enterprise') return null + + switch (column) { + case 'logRetentionHours': + return tier === 'free' ? FREE_RETENTION_DAYS * 24 : null // paid: never + case 'softDeleteRetentionHours': + return (tier === 'free' ? FREE_RETENTION_DAYS : PAID_RETENTION_DAYS) * 24 + case 'taskCleanupHours': + return null + } +} + +/** + * Resolve workspace IDs for a given non-enterprise tier. Used inside background + * tasks at run time so they always see fresh data. + */ +export async function resolveTierWorkspaceIds(tier: 'free' | 'paid'): Promise { + if (tier === 'free') { + const rows = await db + .select({ id: workspace.id }) + .from(workspace) + .leftJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + sqlIsPaid(subscription.plan) + ) + ) + .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) + + return rows.map((r) => r.id) + } + + // paid (pro/team, excluding enterprise) + const rows = await db + .select({ id: workspace.id }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) + ) + ) + .where(isNull(workspace.archivedAt)) + + return rows.map((r) => r.id) +} + +type RunnerFn = (payload: CleanupJobPayload) => Promise + +async function getInlineRunner(jobType: CleanupJobType): Promise { + switch (jobType) { + case 'cleanup-logs': { + const { runCleanupLogs } = await import('@/background/cleanup-logs') + return runCleanupLogs + } + case 'cleanup-soft-deletes': { + const { runCleanupSoftDeletes } = await import('@/background/cleanup-soft-deletes') + return runCleanupSoftDeletes + } + case 'cleanup-tasks': { + const { runCleanupTasks } = await import('@/background/cleanup-tasks') + return runCleanupTasks + } + } +} + +/** + * When the job queue backend is "database" (no Trigger.dev, no BullMQ), the + * enqueued rows just sit in async_jobs forever. Run them inline as fire-and-forget + * promises, following the same pattern as the workflow execution API route. + */ +async function runInlineIfNeeded( + jobQueue: Awaited>, + jobType: CleanupJobType, + jobId: string, + payload: CleanupJobPayload +): Promise { + if (!shouldExecuteInline()) return + const runner = await getInlineRunner(jobType) + void (async () => { + try { + await jobQueue.startJob(jobId) + await runner(payload) + await jobQueue.completeJob(jobId, null) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error(`[${jobType}] Inline job ${jobId} failed`, { error: errorMessage }) + try { + await jobQueue.markJobFailed(jobId, errorMessage) + } catch (markErr) { + logger.error(`[${jobType}] Failed to mark job ${jobId} as failed`, { markErr }) + } + } + })() +} + +/** + * Dispatcher: enqueue cleanup jobs. + * + * - 1 free-tier job (always) + * - 1 paid-tier job (always) + * - 1 enterprise job per workspace with non-NULL retention column + * + * Uses Trigger.dev batchTrigger when available, otherwise parallel enqueue + * via the JobQueueBackend abstraction. On the database backend (no external worker), + * jobs run inline in the same process via fire-and-forget promises. + */ +export async function dispatchCleanupJobs( + jobType: CleanupJobType, + retentionColumn: WorkspaceRetentionColumn +): Promise<{ jobIds: string[]; jobCount: number; enterpriseCount: number }> { + const jobQueue = await getJobQueue() + const jobIds: string[] = [] + + // Free + paid tier jobs (always dispatched, task returns early if no workspaces) + const freePayload: CleanupJobPayload = { tier: 'free' } + const freeJobId = await jobQueue.enqueue(jobType, freePayload) + jobIds.push(freeJobId) + await runInlineIfNeeded(jobQueue, jobType, freeJobId, freePayload) + + const paidPayload: CleanupJobPayload = { tier: 'paid' } + const paidJobId = await jobQueue.enqueue(jobType, paidPayload) + jobIds.push(paidJobId) + await runInlineIfNeeded(jobQueue, jobType, paidJobId, paidPayload) + + // Enterprise: query workspaces with non-NULL retention column + const retentionCol = workspace[retentionColumn] + const enterpriseRows = await db + .select({ id: workspace.id }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + eq(subscription.plan, 'enterprise') + ) + ) + .where(and(isNull(workspace.archivedAt), isNotNull(retentionCol))) + + const enterpriseCount = enterpriseRows.length + + logger.info( + `[${jobType}] Dispatching: 1 free + 1 paid + ${enterpriseCount} enterprise jobs (column: ${retentionColumn})` + ) + + if (enterpriseCount === 0) { + return { jobIds, jobCount: jobIds.length, enterpriseCount: 0 } + } + + if (isTriggerAvailable()) { + // Trigger.dev: use batchTrigger, chunked + for (let i = 0; i < enterpriseRows.length; i += BATCH_TRIGGER_CHUNK_SIZE) { + const chunk = enterpriseRows.slice(i, i + BATCH_TRIGGER_CHUNK_SIZE) + const batchResult = await tasks.batchTrigger( + jobType, + chunk.map((row) => ({ + payload: { tier: 'enterprise' as const, workspaceId: row.id }, + options: { + tags: [`workspaceId:${row.id}`, `jobType:${jobType}`], + }, + })) + ) + jobIds.push(batchResult.batchId) + } + } else { + // Fallback: parallel enqueue via abstraction + const results = await Promise.allSettled( + enterpriseRows.map(async (row) => { + const payload: CleanupJobPayload = { tier: 'enterprise', workspaceId: row.id } + const jobId = await jobQueue.enqueue(jobType, payload) + await runInlineIfNeeded(jobQueue, jobType, jobId, payload) + return jobId + }) + ) + + let succeeded = 0 + let failed = 0 + for (const result of results) { + if (result.status === 'fulfilled') { + jobIds.push(result.value) + succeeded++ + } else { + failed++ + logger.error(`[${jobType}] Failed to enqueue enterprise job:`, { reason: result.reason }) + } + } + logger.info(`[${jobType}] Enterprise enqueue: ${succeeded} succeeded, ${failed} failed`) + } + + return { jobIds, jobCount: jobIds.length, enterpriseCount } +} diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 286fe624d39..af9ba0f920f 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -189,6 +189,7 @@ export const env = createEnv({ // Data Retention FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users + PAID_PLAN_LOG_RETENTION_DAYS: z.string().optional().default('30'), // Log retention days for paid (pro/team) plan users // Admission & Burst Protection ADMISSION_GATE_MAX_INFLIGHT: z.string().optional().default('500'), // Max concurrent in-flight execution requests per pod diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 3c9aba327ad..4be230ede75 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -140,6 +140,9 @@ export const workflowFolder = pgTable( ), parentSortIdx: index('workflow_folder_parent_sort_idx').on(table.parentId, table.sortOrder), archivedAtIdx: index('workflow_folder_archived_at_idx').on(table.archivedAt), + workspaceArchivedAtPartialIdx: index('workflow_folder_workspace_archived_partial_idx') + .on(table.workspaceId, table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) @@ -176,6 +179,9 @@ export const workflow = pgTable( .where(sql`${table.archivedAt} IS NULL`), folderSortIdx: index('workflow_folder_sort_idx').on(table.folderId, table.sortOrder), archivedAtIdx: index('workflow_archived_at_idx').on(table.archivedAt), + workspaceArchivedAtPartialIdx: index('workflow_workspace_archived_partial_idx') + .on(table.workspaceId, table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) @@ -543,7 +549,9 @@ export const workflowSchedule = pgTable( table.workflowId, table.deploymentVersionId ), - archivedAtIdx: index('workflow_schedule_archived_at_idx').on(table.archivedAt), + archivedAtPartialIdx: index('workflow_schedule_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), } } ) @@ -620,7 +628,9 @@ export const webhook = pgTable( ), // Optimize queries for credential set webhooks credentialSetIdIdx: index('webhook_credential_set_id_idx').on(table.credentialSetId), - archivedAtIdx: index('webhook_archived_at_idx').on(table.archivedAt), + archivedAtPartialIdx: index('webhook_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), } } ) @@ -899,7 +909,9 @@ export const chat = pgTable( identifierIdx: uniqueIndex('identifier_idx') .on(table.identifier) .where(sql`${table.archivedAt} IS NULL`), - archivedAtIdx: index('chat_archived_at_idx').on(table.archivedAt), + archivedAtPartialIdx: index('chat_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), } } ) @@ -941,7 +953,9 @@ export const form = pgTable( .where(sql`${table.archivedAt} IS NULL`), workflowIdIdx: index('form_workflow_id_idx').on(table.workflowId), userIdIdx: index('form_user_id_idx').on(table.userId), - archivedAtIdx: index('form_archived_at_idx').on(table.archivedAt), + archivedAtPartialIdx: index('form_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) @@ -1028,6 +1042,9 @@ export const workspace = pgTable('workspace', { inboxEnabled: boolean('inbox_enabled').notNull().default(false), inboxAddress: text('inbox_address'), inboxProviderId: text('inbox_provider_id'), + logRetentionHours: integer('log_retention_hours'), + softDeleteRetentionHours: integer('soft_delete_retention_hours'), + taskRedactionHours: integer('task_redaction_hours'), archivedAt: timestamp('archived_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -1054,6 +1071,9 @@ export const workspaceFile = pgTable( workspaceIdIdx: index('workspace_file_workspace_id_idx').on(table.workspaceId), keyIdx: index('workspace_file_key_idx').on(table.key), deletedAtIdx: index('workspace_file_deleted_at_idx').on(table.deletedAt), + workspaceDeletedAtPartialIdx: index('workspace_file_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), }) ) @@ -1090,6 +1110,9 @@ export const workspaceFiles = pgTable( contextIdx: index('workspace_files_context_idx').on(table.context), chatIdIdx: index('workspace_files_chat_id_idx').on(table.chatId), deletedAtIdx: index('workspace_files_deleted_at_idx').on(table.deletedAt), + workspaceDeletedAtPartialIdx: index('workspace_files_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), }) ) @@ -1190,6 +1213,9 @@ export const memory = pgTable( table.workspaceId, table.key ), + workspaceDeletedAtPartialIdx: index('memory_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), } } ) @@ -1232,6 +1258,9 @@ export const knowledgeBase = pgTable( userWorkspaceIdx: index('kb_user_workspace_idx').on(table.userId, table.workspaceId), // Index for soft delete filtering deletedAtIdx: index('kb_deleted_at_idx').on(table.deletedAt), + workspaceDeletedAtPartialIdx: index('kb_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), /** One active (non-deleted) name per workspace; matches user_table_definitions pattern */ workspaceNameActiveUnique: uniqueIndex('kb_workspace_name_active_unique') .on(table.workspaceId, table.name) @@ -1320,8 +1349,12 @@ export const document = pgTable( .where(sql`${table.deletedAt} IS NULL`), // Sync engine: load all active docs for a connector connectorIdIdx: index('doc_connector_id_idx').on(table.connectorId), - archivedAtIdx: index('doc_archived_at_idx').on(table.archivedAt), - deletedAtIdx: index('doc_deleted_at_idx').on(table.deletedAt), + archivedAtPartialIdx: index('doc_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), + deletedAtPartialIdx: index('doc_deleted_at_partial_idx') + .on(table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), // Text tag indexes tag1Idx: index('doc_tag1_idx').on(table.tag1), tag2Idx: index('doc_tag2_idx').on(table.tag2), @@ -2041,11 +2074,10 @@ export const mcpServers = pgTable( table.enabled ), - // Soft delete pattern - workspace + not deleted - workspaceDeletedIdx: index('mcp_servers_workspace_deleted_idx').on( - table.workspaceId, - table.deletedAt - ), + // Soft delete pattern - workspace + not deleted (partial: only deleted rows) + workspaceDeletedIdx: index('mcp_servers_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), }) ) @@ -2100,6 +2132,9 @@ export const workflowMcpServer = pgTable( workspaceIdIdx: index('workflow_mcp_server_workspace_id_idx').on(table.workspaceId), createdByIdx: index('workflow_mcp_server_created_by_idx').on(table.createdBy), deletedAtIdx: index('workflow_mcp_server_deleted_at_idx').on(table.deletedAt), + workspaceDeletedAtPartialIdx: index('workflow_mcp_server_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), }) ) @@ -2130,7 +2165,9 @@ export const workflowMcpTool = pgTable( serverWorkflowUnique: uniqueIndex('workflow_mcp_tool_server_workflow_unique') .on(table.serverId, table.workflowId) .where(sql`${table.archivedAt} IS NULL`), - archivedAtIdx: index('workflow_mcp_tool_archived_at_idx').on(table.archivedAt), + archivedAtPartialIdx: index('workflow_mcp_tool_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) @@ -2199,6 +2236,9 @@ export const a2aAgent = pgTable( .on(table.workspaceId, table.workflowId) .where(sql`${table.archivedAt} IS NULL`), archivedAtIdx: index('a2a_agent_archived_at_idx').on(table.archivedAt), + workspaceArchivedAtPartialIdx: index('a2a_agent_workspace_archived_partial_idx') + .on(table.workspaceId, table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) @@ -2350,6 +2390,10 @@ export const usageLog = pgTable( sourceIdx: index('usage_log_source_idx').on(table.source), workspaceIdIdx: index('usage_log_workspace_id_idx').on(table.workspaceId), workflowIdIdx: index('usage_log_workflow_id_idx').on(table.workflowId), + workspaceCreatedAtIdx: index('usage_log_workspace_created_at_idx').on( + table.workspaceId, + table.createdAt + ), }) ) @@ -2672,8 +2716,12 @@ export const knowledgeConnector = pgTable( (table) => ({ knowledgeBaseIdIdx: index('kc_knowledge_base_id_idx').on(table.knowledgeBaseId), statusNextSyncIdx: index('kc_status_next_sync_idx').on(table.status, table.nextSyncAt), - archivedAtIdx: index('kc_archived_at_idx').on(table.archivedAt), - deletedAtIdx: index('kc_deleted_at_idx').on(table.deletedAt), + archivedAtPartialIdx: index('kc_archived_at_partial_idx') + .on(table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), + deletedAtPartialIdx: index('kc_deleted_at_partial_idx') + .on(table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), }) ) @@ -2741,6 +2789,9 @@ export const userTableDefinitions = pgTable( .on(table.workspaceId, table.name) .where(sql`${table.archivedAt} IS NULL`), archivedAtIdx: index('user_table_def_archived_at_idx').on(table.archivedAt), + workspaceArchivedAtPartialIdx: index('user_table_def_workspace_archived_partial_idx') + .on(table.workspaceId, table.archivedAt) + .where(sql`${table.archivedAt} IS NOT NULL`), }) ) From 373a0f134742df60df14a02ceaa61997c64d3bfb Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 13 Apr 2026 10:27:27 -0700 Subject: [PATCH 02/15] Add task cleanup, switch to triggering off trigger.dev --- .../api/cron/cleanup-soft-deletes/route.ts | 303 +------------- apps/sim/app/api/cron/cleanup-tasks/route.ts | 25 ++ .../app/api/cron/redact-task-context/route.ts | 28 ++ apps/sim/app/api/logs/cleanup/route.ts | 250 +---------- .../workspaces/[id]/data-retention/route.ts | 23 +- apps/sim/background/cleanup-logs.ts | 137 ++++++ apps/sim/background/cleanup-soft-deletes.ts | 168 ++++++++ apps/sim/background/cleanup-tasks.ts | 234 +++++++++++ apps/sim/background/redact-task-context.ts | 389 ++++++++++++++++++ .../components/data-retention-settings.tsx | 20 +- .../ee/data-retention/hooks/data-retention.ts | 1 + .../lib/core/async-jobs/backends/bullmq.ts | 131 ++++++ .../core/async-jobs/backends/trigger-dev.ts | 4 + apps/sim/lib/core/async-jobs/types.ts | 4 + apps/sim/lib/core/bullmq/queues.ts | 246 +++++++++++ apps/sim/lib/retention/workspace-tiers.ts | 146 +++++++ packages/db/migrations/0190_lean_terror.sql | 5 + .../migrations/0191_parched_living_mummy.sql | 1 + packages/db/schema.ts | 6 + 19 files changed, 1578 insertions(+), 543 deletions(-) create mode 100644 apps/sim/app/api/cron/cleanup-tasks/route.ts create mode 100644 apps/sim/app/api/cron/redact-task-context/route.ts create mode 100644 apps/sim/background/cleanup-logs.ts create mode 100644 apps/sim/background/cleanup-soft-deletes.ts create mode 100644 apps/sim/background/cleanup-tasks.ts create mode 100644 apps/sim/background/redact-task-context.ts create mode 100644 apps/sim/lib/core/async-jobs/backends/bullmq.ts create mode 100644 apps/sim/lib/core/bullmq/queues.ts create mode 100644 apps/sim/lib/retention/workspace-tiers.ts create mode 100644 packages/db/migrations/0190_lean_terror.sql create mode 100644 packages/db/migrations/0191_parched_living_mummy.sql diff --git a/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts index dc28ee02f0b..cf33d024450 100644 --- a/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts +++ b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts @@ -1,314 +1,25 @@ -import { db } from '@sim/db' -import { - a2aAgent, - knowledgeBase, - mcpServers, - memory, - subscription, - userTableDefinitions, - workflow, - workflowFolder, - workflowMcpServer, - workspace, - workspaceFile, - workspaceFiles, -} from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray, isNotNull, isNull, lt, or, sql } from 'drizzle-orm' -import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' -import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' -import { env } from '@/lib/core/config/env' -import { isUsingCloudStorage, StorageService } from '@/lib/uploads' +import { getJobQueue } from '@/lib/core/async-jobs' export const dynamic = 'force-dynamic' const logger = createLogger('SoftDeleteCleanupAPI') -const BATCH_SIZE = 2000 -const MAX_BATCHES_PER_TABLE = 10 - -interface TableCleanupResult { - table: string - deleted: number - failed: number -} - -/** - * Batch-delete rows from a table where the soft-delete column is older than the retention date, - * scoped to the given workspace IDs. - */ -async function cleanupTable( - tableDef: PgTable, - softDeleteCol: PgColumn, - workspaceIdCol: PgColumn, - workspaceIds: string[], - retentionDate: Date, - tableName: string -): Promise { - const result: TableCleanupResult = { table: tableName, deleted: 0, failed: 0 } - - if (workspaceIds.length === 0) { - logger.info(`[${tableName}] Skipped — no workspaces in this tier`) - return result - } - - let batchesProcessed = 0 - let hasMore = true - - while (hasMore && batchesProcessed < MAX_BATCHES_PER_TABLE) { - try { - const deleted = await db - .delete(tableDef) - .where( - and( - inArray(workspaceIdCol, workspaceIds), - isNotNull(softDeleteCol), - lt(softDeleteCol, retentionDate) - ) - ) - .returning({ id: sql`id` }) - - result.deleted += deleted.length - hasMore = deleted.length === BATCH_SIZE - batchesProcessed++ - - if (deleted.length > 0) { - logger.info(`[${tableName}] Batch ${batchesProcessed}: deleted ${deleted.length} rows`) - } else { - logger.info(`[${tableName}] No expired soft-deleted rows found`) - } - } catch (error) { - result.failed++ - logger.error(`[${tableName}] Batch delete failed:`, { error }) - hasMore = false - } - } - - return result -} - -/** - * Clean up soft-deleted workspace files from cloud storage before hard-deleting. - */ -async function cleanupWorkspaceFileStorage( - workspaceIds: string[], - retentionDate: Date -): Promise<{ filesDeleted: number; filesFailed: number }> { - const stats = { filesDeleted: 0, filesFailed: 0 } - - if (!isUsingCloudStorage() || workspaceIds.length === 0) return stats - - // Fetch keys of files about to be deleted - const filesToDelete = await db - .select({ key: workspaceFiles.key }) - .from(workspaceFiles) - .where( - and( - inArray(workspaceFiles.workspaceId, workspaceIds), - isNotNull(workspaceFiles.deletedAt), - lt(workspaceFiles.deletedAt, retentionDate) - ) - ) - .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) - - for (const file of filesToDelete) { - try { - await StorageService.deleteFile({ key: file.key, context: 'workspace' }) - stats.filesDeleted++ - } catch (error) { - stats.filesFailed++ - logger.error(`Failed to delete storage file ${file.key}:`, { error }) - } - } - - return stats -} - -/** All tables to clean up with their soft-delete column and workspace column. */ -const CLEANUP_TARGETS = [ - { table: workflow, softDeleteCol: workflow.archivedAt, wsCol: workflow.workspaceId, name: 'workflow' }, - { table: workflowFolder, softDeleteCol: workflowFolder.archivedAt, wsCol: workflowFolder.workspaceId, name: 'workflowFolder' }, - { table: knowledgeBase, softDeleteCol: knowledgeBase.deletedAt, wsCol: knowledgeBase.workspaceId, name: 'knowledgeBase' }, - { table: userTableDefinitions, softDeleteCol: userTableDefinitions.archivedAt, wsCol: userTableDefinitions.workspaceId, name: 'userTableDefinitions' }, - { table: workspaceFile, softDeleteCol: workspaceFile.deletedAt, wsCol: workspaceFile.workspaceId, name: 'workspaceFile' }, - { table: workspaceFiles, softDeleteCol: workspaceFiles.deletedAt, wsCol: workspaceFiles.workspaceId, name: 'workspaceFiles' }, - { table: memory, softDeleteCol: memory.deletedAt, wsCol: memory.workspaceId, name: 'memory' }, - { table: mcpServers, softDeleteCol: mcpServers.deletedAt, wsCol: mcpServers.workspaceId, name: 'mcpServers' }, - { table: workflowMcpServer, softDeleteCol: workflowMcpServer.deletedAt, wsCol: workflowMcpServer.workspaceId, name: 'workflowMcpServer' }, - { table: a2aAgent, softDeleteCol: a2aAgent.archivedAt, wsCol: a2aAgent.workspaceId, name: 'a2aAgent' }, -] as const - -async function cleanupTier( - workspaceIds: string[], - retentionDate: Date, - tierLabel: string -): Promise<{ tables: TableCleanupResult[]; filesDeleted: number; filesFailed: number }> { - const tables: TableCleanupResult[] = [] - - if (workspaceIds.length === 0) { - return { tables, filesDeleted: 0, filesFailed: 0 } - } - - // Clean cloud storage files before hard-deleting file metadata rows - const fileStats = await cleanupWorkspaceFileStorage(workspaceIds, retentionDate) - - for (const target of CLEANUP_TARGETS) { - const result = await cleanupTable( - target.table, - target.softDeleteCol, - target.wsCol, - workspaceIds, - retentionDate, - `${tierLabel}/${target.name}` - ) - tables.push(result) - } - - return { tables, ...fileStats } -} - export async function GET(request: NextRequest) { try { const authError = verifyCronAuth(request, 'soft-delete cleanup') if (authError) return authError - const startTime = Date.now() - - const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') - const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') - - const freeRetentionDate = new Date(Date.now() - freeRetentionDays * 24 * 60 * 60 * 1000) - const paidRetentionDate = new Date(Date.now() - paidRetentionDays * 24 * 60 * 60 * 1000) - - logger.info('Starting soft-delete cleanup', { - freeRetentionDays, - paidRetentionDays, - freeRetentionDate: freeRetentionDate.toISOString(), - paidRetentionDate: paidRetentionDate.toISOString(), - }) - - // --- Group 1: Free workspaces --- - - const freeWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .leftJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - sqlIsPaid(subscription.plan) - ) - ) - .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) - - const freeIds = freeWorkspaceRows.map((w) => w.id) - logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) - const freeResults = await cleanupTier(freeIds, freeRetentionDate, 'free') - logger.info(`[free] Result: ${freeResults.tables.reduce((s, t) => s + t.deleted, 0)} total rows deleted across ${CLEANUP_TARGETS.length} tables`) - - // --- Group 2: Pro/Team workspaces --- - - const paidWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) - ) - ) - .where(isNull(workspace.archivedAt)) - - const paidIds = paidWorkspaceRows.map((w) => w.id) - logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) - const paidResults = await cleanupTier(paidIds, paidRetentionDate, 'paid') - logger.info(`[paid] Result: ${paidResults.tables.reduce((s, t) => s + t.deleted, 0)} total rows deleted across ${CLEANUP_TARGETS.length} tables`) - - // --- Group 3: Enterprise with custom softDeleteRetentionHours --- - - const enterpriseWorkspaceRows = await db - .select({ - id: workspace.id, - softDeleteRetentionHours: workspace.softDeleteRetentionHours, - }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - eq(subscription.plan, 'enterprise') - ) - ) - .where( - and(isNull(workspace.archivedAt), isNotNull(workspace.softDeleteRetentionHours)) - ) - - const enterpriseGroups = new Map() - for (const ws of enterpriseWorkspaceRows) { - const hours = ws.softDeleteRetentionHours! - const group = enterpriseGroups.get(hours) ?? [] - group.push(ws.id) - enterpriseGroups.set(hours, group) - } - - logger.info(`[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct retention periods). Workspaces with NULL retention are skipped.`) - - const enterpriseTables: TableCleanupResult[] = [] - let enterpriseFilesDeleted = 0 - let enterpriseFilesFailed = 0 - - for (const [hours, ids] of enterpriseGroups) { - const retentionDate = new Date(Date.now() - hours * 60 * 60 * 1000) - logger.info(`[enterprise-${hours}h] Processing ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}`) - const groupResults = await cleanupTier(ids, retentionDate, `enterprise-${hours}h`) - enterpriseTables.push(...groupResults.tables) - enterpriseFilesDeleted += groupResults.filesDeleted - enterpriseFilesFailed += groupResults.filesFailed - } - - const timeElapsed = (Date.now() - startTime) / 1000 + const jobQueue = await getJobQueue() + const jobId = await jobQueue.enqueue('cleanup-soft-deletes', {}) - const totalDeleted = (results: { tables: TableCleanupResult[] }) => - results.tables.reduce((sum, t) => sum + t.deleted, 0) + logger.info('Soft-delete cleanup job dispatched', { jobId }) - return NextResponse.json({ - message: `Soft-delete cleanup completed in ${timeElapsed.toFixed(2)}s`, - tiers: { - free: { - workspaces: freeIds.length, - retentionDays: freeRetentionDays, - totalDeleted: totalDeleted(freeResults), - filesDeleted: freeResults.filesDeleted, - filesFailed: freeResults.filesFailed, - tables: freeResults.tables, - }, - paid: { - workspaces: paidIds.length, - retentionDays: paidRetentionDays, - totalDeleted: totalDeleted(paidResults), - filesDeleted: paidResults.filesDeleted, - filesFailed: paidResults.filesFailed, - tables: paidResults.tables, - }, - enterprise: { - workspaces: enterpriseWorkspaceRows.length, - groups: enterpriseGroups.size, - totalDeleted: enterpriseTables.reduce((sum, t) => sum + t.deleted, 0), - filesDeleted: enterpriseFilesDeleted, - filesFailed: enterpriseFilesFailed, - tables: enterpriseTables, - }, - }, - }) + return NextResponse.json({ triggered: true, jobId }) } catch (error) { - logger.error('Error in soft-delete cleanup:', { error }) - return NextResponse.json({ error: 'Failed to process soft-delete cleanup' }, { status: 500 }) + logger.error('Failed to dispatch soft-delete cleanup job:', { error }) + return NextResponse.json({ error: 'Failed to dispatch soft-delete cleanup' }, { status: 500 }) } } diff --git a/apps/sim/app/api/cron/cleanup-tasks/route.ts b/apps/sim/app/api/cron/cleanup-tasks/route.ts new file mode 100644 index 00000000000..2304dc1d28d --- /dev/null +++ b/apps/sim/app/api/cron/cleanup-tasks/route.ts @@ -0,0 +1,25 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { getJobQueue } from '@/lib/core/async-jobs' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TaskCleanupAPI') + +export async function GET(request: NextRequest) { + try { + const authError = verifyCronAuth(request, 'task cleanup') + if (authError) return authError + + const jobQueue = await getJobQueue() + const jobId = await jobQueue.enqueue('cleanup-tasks', {}) + + logger.info('Task cleanup job dispatched', { jobId }) + + return NextResponse.json({ triggered: true, jobId }) + } catch (error) { + logger.error('Failed to dispatch task cleanup job:', { error }) + return NextResponse.json({ error: 'Failed to dispatch task cleanup' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/cron/redact-task-context/route.ts b/apps/sim/app/api/cron/redact-task-context/route.ts new file mode 100644 index 00000000000..e0c837a137c --- /dev/null +++ b/apps/sim/app/api/cron/redact-task-context/route.ts @@ -0,0 +1,28 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { getJobQueue } from '@/lib/core/async-jobs' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TaskRedactionAPI') + +export async function GET(request: NextRequest) { + try { + const authError = verifyCronAuth(request, 'task context redaction') + if (authError) return authError + + const jobQueue = await getJobQueue() + const jobId = await jobQueue.enqueue('redact-task-context', {}) + + logger.info('Task context redaction job dispatched', { jobId }) + + return NextResponse.json({ triggered: true, jobId }) + } catch (error) { + logger.error('Failed to dispatch task context redaction job:', { error }) + return NextResponse.json( + { error: 'Failed to dispatch task context redaction' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/logs/cleanup/route.ts b/apps/sim/app/api/logs/cleanup/route.ts index 16f9069aa3c..0305ae24a45 100644 --- a/apps/sim/app/api/logs/cleanup/route.ts +++ b/apps/sim/app/api/logs/cleanup/route.ts @@ -1,259 +1,25 @@ -import { db } from '@sim/db' -import { subscription, workflowExecutionLogs, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray, isNotNull, isNull, lt, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' -import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' -import { env } from '@/lib/core/config/env' -import { snapshotService } from '@/lib/logs/execution/snapshot/service' -import { isUsingCloudStorage, StorageService } from '@/lib/uploads' +import { getJobQueue } from '@/lib/core/async-jobs' export const dynamic = 'force-dynamic' const logger = createLogger('LogsCleanupAPI') -const BATCH_SIZE = 2000 -const MAX_BATCHES_PER_TIER = 10 - -interface TierResults { - total: number - deleted: number - deleteFailed: number - filesTotal: number - filesDeleted: number - filesDeleteFailed: number -} - -function emptyTierResults(): TierResults { - return { total: 0, deleted: 0, deleteFailed: 0, filesTotal: 0, filesDeleted: 0, filesDeleteFailed: 0 } -} - -async function deleteExecutionFiles( - files: unknown, - results: TierResults -): Promise { - if (!isUsingCloudStorage() || !files || !Array.isArray(files)) return - - for (const file of files) { - if (!file || typeof file !== 'object' || !file.key) continue - results.filesTotal++ - try { - await StorageService.deleteFile({ key: file.key, context: 'execution' }) - const { deleteFileMetadata } = await import('@/lib/uploads/server/metadata') - await deleteFileMetadata(file.key) - results.filesDeleted++ - } catch (fileError) { - results.filesDeleteFailed++ - logger.error(`Failed to delete file ${file.key}:`, { fileError }) - } - } -} - -/** - * Run batch cleanup for a set of workspace IDs with a given retention date. - * Selects logs to find files, deletes files from storage, then deletes log rows. - */ -async function cleanupTier( - workspaceIds: string[], - retentionDate: Date, - tierLabel: string -): Promise { - const results = emptyTierResults() - - if (workspaceIds.length === 0) return results - - let batchesProcessed = 0 - let hasMore = true - - while (hasMore && batchesProcessed < MAX_BATCHES_PER_TIER) { - // Select logs with files before deleting so we can clean up storage - const batch = await db - .select({ - id: workflowExecutionLogs.id, - files: workflowExecutionLogs.files, - }) - .from(workflowExecutionLogs) - .where( - and( - inArray(workflowExecutionLogs.workspaceId, workspaceIds), - lt(workflowExecutionLogs.startedAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - results.total += batch.length - - if (batch.length === 0) { - hasMore = false - break - } - - // Delete associated files from cloud storage - for (const log of batch) { - await deleteExecutionFiles(log.files, results) - } - - // Batch delete the log rows - const logIds = batch.map((log) => log.id) - try { - const deleted = await db - .delete(workflowExecutionLogs) - .where(inArray(workflowExecutionLogs.id, logIds)) - .returning({ id: workflowExecutionLogs.id }) - - results.deleted += deleted.length - } catch (deleteError) { - results.deleteFailed += logIds.length - logger.error(`Batch delete failed for ${tierLabel}:`, { deleteError }) - } - - batchesProcessed++ - hasMore = batch.length === BATCH_SIZE - - logger.info( - `[${tierLabel}] Batch ${batchesProcessed}: ${batch.length} logs processed` - ) - } - - return results -} - -async function runLogCleanup() { - const startTime = Date.now() - - const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') - const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') - - const freeRetentionDate = new Date(Date.now() - freeRetentionDays * 24 * 60 * 60 * 1000) - const paidRetentionDate = new Date(Date.now() - paidRetentionDays * 24 * 60 * 60 * 1000) - - logger.info('Starting log cleanup', { - freeRetentionDays, - paidRetentionDays, - freeRetentionDate: freeRetentionDate.toISOString(), - paidRetentionDate: paidRetentionDate.toISOString(), - }) - - // --- Group 1: Free workspaces (no paid subscription) --- - - const freeWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .leftJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - sqlIsPaid(subscription.plan) - ) - ) - .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) - - const freeIds = freeWorkspaceRows.map((w) => w.id) - logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) - const freeResults = await cleanupTier(freeIds, freeRetentionDate, 'free') - logger.info(`[free] Result: ${freeResults.deleted} deleted, ${freeResults.deleteFailed} failed out of ${freeResults.total} candidates`) - - // --- Group 2: Pro/Team workspaces (paid non-enterprise) --- - - const paidWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) - ) - ) - .where(isNull(workspace.archivedAt)) - - const paidIds = paidWorkspaceRows.map((w) => w.id) - logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) - const paidResults = await cleanupTier(paidIds, paidRetentionDate, 'paid') - logger.info(`[paid] Result: ${paidResults.deleted} deleted, ${paidResults.deleteFailed} failed out of ${paidResults.total} candidates`) - - // --- Group 3: Enterprise with custom logRetentionHours --- - // Enterprise with logRetentionHours = NULL → no cleanup (infinite retention) - - const enterpriseWorkspaceRows = await db - .select({ - id: workspace.id, - logRetentionHours: workspace.logRetentionHours, - }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - eq(subscription.plan, 'enterprise') - ) - ) - .where( - and(isNull(workspace.archivedAt), isNotNull(workspace.logRetentionHours)) - ) - - const enterpriseGroups = new Map() - for (const ws of enterpriseWorkspaceRows) { - const hours = ws.logRetentionHours! - const group = enterpriseGroups.get(hours) ?? [] - group.push(ws.id) - enterpriseGroups.set(hours, group) - } - - logger.info(`[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct retention periods). Workspaces with NULL retention are skipped.`) - - const enterpriseResults = emptyTierResults() - for (const [hours, ids] of enterpriseGroups) { - const retentionDate = new Date(Date.now() - hours * 60 * 60 * 1000) - logger.info(`[enterprise-${hours}h] Processing ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}`) - const groupResults = await cleanupTier(ids, retentionDate, `enterprise-${hours}h`) - enterpriseResults.total += groupResults.total - enterpriseResults.deleted += groupResults.deleted - enterpriseResults.deleteFailed += groupResults.deleteFailed - enterpriseResults.filesTotal += groupResults.filesTotal - enterpriseResults.filesDeleted += groupResults.filesDeleted - enterpriseResults.filesDeleteFailed += groupResults.filesDeleteFailed - } - - // --- Snapshot cleanup --- - - try { - const allRetentionDays = [freeRetentionDays, paidRetentionDays] - for (const hours of enterpriseGroups.keys()) { - allRetentionDays.push(hours / 24) - } - const shortestRetentionDays = Math.min(...allRetentionDays) - const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots( - shortestRetentionDays + 1 - ) - logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`) - } catch (snapshotError) { - logger.error('Error cleaning up orphaned snapshots:', { snapshotError }) - } - - const timeElapsed = (Date.now() - startTime) / 1000 - logger.info(`Log cleanup completed in ${timeElapsed.toFixed(2)}s`, { - free: { workspaces: freeIds.length, ...freeResults }, - paid: { workspaces: paidIds.length, ...paidResults }, - enterprise: { workspaces: enterpriseWorkspaceRows.length, groups: enterpriseGroups.size, ...enterpriseResults }, - }) -} - export async function GET(request: NextRequest) { try { const authError = verifyCronAuth(request, 'logs cleanup') if (authError) return authError - await runLogCleanup() + const jobQueue = await getJobQueue() + const jobId = await jobQueue.enqueue('cleanup-logs', {}) + + logger.info('Log cleanup job dispatched', { jobId }) - return NextResponse.json({ success: true }) + return NextResponse.json({ triggered: true, jobId }) } catch (error) { - logger.error('Error in log cleanup process:', { error }) - return NextResponse.json({ error: 'Failed to process log cleanup' }, { status: 500 }) + logger.error('Failed to dispatch log cleanup job:', { error }) + return NextResponse.json({ error: 'Failed to dispatch log cleanup' }, { status: 500 }) } } diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts index d78b1e2df28..4d46c327669 100644 --- a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -24,15 +24,18 @@ const MAX_HOURS = 43800 // 5 years const FREE_LOG_RETENTION_HOURS = 7 * 24 const FREE_SOFT_DELETE_RETENTION_HOURS = 7 * 24 const FREE_TASK_REDACTION_HOURS = null // never +const FREE_TASK_CLEANUP_HOURS = null // never const PRO_LOG_RETENTION_HOURS = 30 * 24 const PRO_SOFT_DELETE_RETENTION_HOURS = 30 * 24 const PRO_TASK_REDACTION_HOURS = 30 * 24 +const PRO_TASK_CLEANUP_HOURS = null // never interface PlanDefaults { logRetentionHours: number softDeleteRetentionHours: number taskRedactionHours: number | null + taskCleanupHours: number | null } function getPlanDefaults(plan: 'free' | 'pro' | 'enterprise'): PlanDefaults { @@ -43,12 +46,14 @@ function getPlanDefaults(plan: 'free' | 'pro' | 'enterprise'): PlanDefaults { logRetentionHours: PRO_LOG_RETENTION_HOURS, softDeleteRetentionHours: PRO_SOFT_DELETE_RETENTION_HOURS, taskRedactionHours: PRO_TASK_REDACTION_HOURS, + taskCleanupHours: PRO_TASK_CLEANUP_HOURS, } default: return { logRetentionHours: FREE_LOG_RETENTION_HOURS, softDeleteRetentionHours: FREE_SOFT_DELETE_RETENTION_HOURS, taskRedactionHours: FREE_TASK_REDACTION_HOURS, + taskCleanupHours: FREE_TASK_CLEANUP_HOURS, } } } @@ -67,6 +72,7 @@ const updateRetentionSchema = z.object({ logRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), softDeleteRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), taskRedactionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), + taskCleanupHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), }) /** @@ -93,6 +99,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ logRetentionHours: workspace.logRetentionHours, softDeleteRetentionHours: workspace.softDeleteRetentionHours, taskRedactionHours: workspace.taskRedactionHours, + taskCleanupHours: workspace.taskCleanupHours, billedAccountUserId: workspace.billedAccountUserId, }) .from(workspace) @@ -117,17 +124,20 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ logRetentionHours: ws.logRetentionHours, softDeleteRetentionHours: ws.softDeleteRetentionHours, taskRedactionHours: ws.taskRedactionHours, + taskCleanupHours: ws.taskCleanupHours, }, effective: isEnterpriseWorkspace ? { logRetentionHours: ws.logRetentionHours, softDeleteRetentionHours: ws.softDeleteRetentionHours, taskRedactionHours: ws.taskRedactionHours, + taskCleanupHours: ws.taskCleanupHours, } : { logRetentionHours: defaults.logRetentionHours, softDeleteRetentionHours: defaults.softDeleteRetentionHours, taskRedactionHours: defaults.taskRedactionHours, + taskCleanupHours: defaults.taskCleanupHours, }, }, }) @@ -189,6 +199,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ if (parsed.data.taskRedactionHours !== undefined) { updateData.taskRedactionHours = parsed.data.taskRedactionHours } + if (parsed.data.taskCleanupHours !== undefined) { + updateData.taskCleanupHours = parsed.data.taskCleanupHours + } const [updated] = await db .update(workspace) @@ -198,6 +211,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ logRetentionHours: workspace.logRetentionHours, softDeleteRetentionHours: workspace.softDeleteRetentionHours, taskRedactionHours: workspace.taskRedactionHours, + taskCleanupHours: workspace.taskCleanupHours, }) if (!updated) { @@ -229,12 +243,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ logRetentionHours: updated.logRetentionHours, softDeleteRetentionHours: updated.softDeleteRetentionHours, taskRedactionHours: updated.taskRedactionHours, + taskCleanupHours: updated.taskCleanupHours, }, effective: { - logRetentionHours: updated.logRetentionHours ?? defaults.logRetentionHours, - softDeleteRetentionHours: - updated.softDeleteRetentionHours ?? defaults.softDeleteRetentionHours, - taskRedactionHours: updated.taskRedactionHours ?? defaults.taskRedactionHours, + logRetentionHours: updated.logRetentionHours, + softDeleteRetentionHours: updated.softDeleteRetentionHours, + taskRedactionHours: updated.taskRedactionHours, + taskCleanupHours: updated.taskCleanupHours, }, }, }) diff --git a/apps/sim/background/cleanup-logs.ts b/apps/sim/background/cleanup-logs.ts new file mode 100644 index 00000000000..e95ee65ecb6 --- /dev/null +++ b/apps/sim/background/cleanup-logs.ts @@ -0,0 +1,137 @@ +import { db } from '@sim/db' +import { workflowExecutionLogs } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { and, inArray, lt } from 'drizzle-orm' +import { snapshotService } from '@/lib/logs/execution/snapshot/service' +import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' +import { isUsingCloudStorage, StorageService } from '@/lib/uploads' + +const logger = createLogger('CleanupLogs') + +const BATCH_SIZE = 2000 +const MAX_BATCHES_PER_TIER = 10 + +interface TierResults { + total: number + deleted: number + deleteFailed: number + filesTotal: number + filesDeleted: number + filesDeleteFailed: number +} + +function emptyTierResults(): TierResults { + return { total: 0, deleted: 0, deleteFailed: 0, filesTotal: 0, filesDeleted: 0, filesDeleteFailed: 0 } +} + +async function deleteExecutionFiles(files: unknown, results: TierResults): Promise { + if (!isUsingCloudStorage() || !files || !Array.isArray(files)) return + + for (const file of files) { + if (!file || typeof file !== 'object' || !file.key) continue + results.filesTotal++ + try { + await StorageService.deleteFile({ key: file.key, context: 'execution' }) + const { deleteFileMetadata } = await import('@/lib/uploads/server/metadata') + await deleteFileMetadata(file.key) + results.filesDeleted++ + } catch (fileError) { + results.filesDeleteFailed++ + logger.error(`Failed to delete file ${file.key}:`, { fileError }) + } + } +} + +async function cleanupTier( + workspaceIds: string[], + retentionDate: Date, + tierLabel: string +): Promise { + const results = emptyTierResults() + if (workspaceIds.length === 0) return results + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < MAX_BATCHES_PER_TIER) { + const batch = await db + .select({ + id: workflowExecutionLogs.id, + files: workflowExecutionLogs.files, + }) + .from(workflowExecutionLogs) + .where( + and( + inArray(workflowExecutionLogs.workspaceId, workspaceIds), + lt(workflowExecutionLogs.startedAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + results.total += batch.length + + if (batch.length === 0) { + hasMore = false + break + } + + for (const log of batch) { + await deleteExecutionFiles(log.files, results) + } + + const logIds = batch.map((log) => log.id) + try { + const deleted = await db + .delete(workflowExecutionLogs) + .where(inArray(workflowExecutionLogs.id, logIds)) + .returning({ id: workflowExecutionLogs.id }) + + results.deleted += deleted.length + } catch (deleteError) { + results.deleteFailed += logIds.length + logger.error(`Batch delete failed for ${tierLabel}:`, { deleteError }) + } + + batchesProcessed++ + hasMore = batch.length === BATCH_SIZE + + logger.info(`[${tierLabel}] Batch ${batchesProcessed}: ${batch.length} logs processed`) + } + + return results +} + +export const cleanupLogsTask = task({ + id: 'cleanup-logs', + run: async () => { + const startTime = Date.now() + + logger.info('Starting log cleanup task') + + const groups = await resolveRetentionGroups('logRetentionHours') + + for (const group of groups) { + const results = await cleanupTier(group.workspaceIds, group.retentionDate, group.tierLabel) + logger.info(`[${group.tierLabel}] Result: ${results.deleted} deleted, ${results.deleteFailed} failed out of ${results.total} candidates`) + } + + // Snapshot cleanup — use shortest retention + 1 day + try { + const shortestDays = Math.min( + ...groups.map((g) => (Date.now() - g.retentionDate.getTime()) / (24 * 60 * 60 * 1000)) + ) + if (Number.isFinite(shortestDays)) { + const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots( + Math.floor(shortestDays) + 1 + ) + logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`) + } + } catch (snapshotError) { + logger.error('Error cleaning up orphaned snapshots:', { snapshotError }) + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Log cleanup task completed in ${timeElapsed.toFixed(2)}s`) + }, +}) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts new file mode 100644 index 00000000000..3747bdfe81f --- /dev/null +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -0,0 +1,168 @@ +import { db } from '@sim/db' +import { + a2aAgent, + knowledgeBase, + mcpServers, + memory, + userTableDefinitions, + workflow, + workflowFolder, + workflowMcpServer, + workspaceFile, + workspaceFiles, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { and, inArray, isNotNull, lt, sql } from 'drizzle-orm' +import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' +import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' +import { isUsingCloudStorage, StorageService } from '@/lib/uploads' + +const logger = createLogger('CleanupSoftDeletes') + +const BATCH_SIZE = 2000 +const MAX_BATCHES_PER_TABLE = 10 + +interface TableCleanupResult { + table: string + deleted: number + failed: number +} + +async function cleanupTable( + tableDef: PgTable, + softDeleteCol: PgColumn, + workspaceIdCol: PgColumn, + workspaceIds: string[], + retentionDate: Date, + tableName: string +): Promise { + const result: TableCleanupResult = { table: tableName, deleted: 0, failed: 0 } + + if (workspaceIds.length === 0) { + logger.info(`[${tableName}] Skipped — no workspaces in this tier`) + return result + } + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < MAX_BATCHES_PER_TABLE) { + try { + const deleted = await db + .delete(tableDef) + .where( + and( + inArray(workspaceIdCol, workspaceIds), + isNotNull(softDeleteCol), + lt(softDeleteCol, retentionDate) + ) + ) + .returning({ id: sql`id` }) + + result.deleted += deleted.length + hasMore = deleted.length === BATCH_SIZE + batchesProcessed++ + + if (deleted.length > 0) { + logger.info(`[${tableName}] Batch ${batchesProcessed}: deleted ${deleted.length} rows`) + } else { + logger.info(`[${tableName}] No expired soft-deleted rows found`) + } + } catch (error) { + result.failed++ + logger.error(`[${tableName}] Batch delete failed:`, { error }) + hasMore = false + } + } + + return result +} + +async function cleanupWorkspaceFileStorage( + workspaceIds: string[], + retentionDate: Date +): Promise<{ filesDeleted: number; filesFailed: number }> { + const stats = { filesDeleted: 0, filesFailed: 0 } + + if (!isUsingCloudStorage() || workspaceIds.length === 0) return stats + + const filesToDelete = await db + .select({ key: workspaceFiles.key }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.workspaceId, workspaceIds), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, retentionDate) + ) + ) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + for (const file of filesToDelete) { + try { + await StorageService.deleteFile({ key: file.key, context: 'workspace' }) + stats.filesDeleted++ + } catch (error) { + stats.filesFailed++ + logger.error(`Failed to delete storage file ${file.key}:`, { error }) + } + } + + return stats +} + +const CLEANUP_TARGETS = [ + { table: workflow, softDeleteCol: workflow.archivedAt, wsCol: workflow.workspaceId, name: 'workflow' }, + { table: workflowFolder, softDeleteCol: workflowFolder.archivedAt, wsCol: workflowFolder.workspaceId, name: 'workflowFolder' }, + { table: knowledgeBase, softDeleteCol: knowledgeBase.deletedAt, wsCol: knowledgeBase.workspaceId, name: 'knowledgeBase' }, + { table: userTableDefinitions, softDeleteCol: userTableDefinitions.archivedAt, wsCol: userTableDefinitions.workspaceId, name: 'userTableDefinitions' }, + { table: workspaceFile, softDeleteCol: workspaceFile.deletedAt, wsCol: workspaceFile.workspaceId, name: 'workspaceFile' }, + { table: workspaceFiles, softDeleteCol: workspaceFiles.deletedAt, wsCol: workspaceFiles.workspaceId, name: 'workspaceFiles' }, + { table: memory, softDeleteCol: memory.deletedAt, wsCol: memory.workspaceId, name: 'memory' }, + { table: mcpServers, softDeleteCol: mcpServers.deletedAt, wsCol: mcpServers.workspaceId, name: 'mcpServers' }, + { table: workflowMcpServer, softDeleteCol: workflowMcpServer.deletedAt, wsCol: workflowMcpServer.workspaceId, name: 'workflowMcpServer' }, + { table: a2aAgent, softDeleteCol: a2aAgent.archivedAt, wsCol: a2aAgent.workspaceId, name: 'a2aAgent' }, +] as const + +export const cleanupSoftDeletesTask = task({ + id: 'cleanup-soft-deletes', + run: async () => { + const startTime = Date.now() + + logger.info('Starting soft-delete cleanup task') + + const groups = await resolveRetentionGroups('softDeleteRetentionHours') + + for (const group of groups) { + logger.info( + `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` + ) + + const fileStats = await cleanupWorkspaceFileStorage( + group.workspaceIds, + group.retentionDate + ) + + let totalDeleted = 0 + for (const target of CLEANUP_TARGETS) { + const result = await cleanupTable( + target.table, + target.softDeleteCol, + target.wsCol, + group.workspaceIds, + group.retentionDate, + `${group.tierLabel}/${target.name}` + ) + totalDeleted += result.deleted + } + + logger.info( + `[${group.tierLabel}] Complete: ${totalDeleted} rows deleted, ${fileStats.filesDeleted} files cleaned` + ) + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Soft-delete cleanup task completed in ${timeElapsed.toFixed(2)}s`) + }, +}) diff --git a/apps/sim/background/cleanup-tasks.ts b/apps/sim/background/cleanup-tasks.ts new file mode 100644 index 00000000000..cbe2eee491c --- /dev/null +++ b/apps/sim/background/cleanup-tasks.ts @@ -0,0 +1,234 @@ +import { db } from '@sim/db' +import { + copilotAsyncToolCalls, + copilotChats, + copilotFeedback, + copilotRunCheckpoints, + copilotRuns, + mothershipInboxTask, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { and, inArray, lt, sql } from 'drizzle-orm' +import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' +import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' + +const logger = createLogger('CleanupTasks') + +const BATCH_SIZE = 2000 +const MAX_BATCHES_PER_TABLE = 10 + +interface TableCleanupResult { + table: string + deleted: number + failed: number +} + +async function cleanupTable( + tableDef: PgTable, + workspaceIdCol: PgColumn, + createdAtCol: PgColumn, + workspaceIds: string[], + retentionDate: Date, + tableName: string +): Promise { + const result: TableCleanupResult = { table: tableName, deleted: 0, failed: 0 } + if (workspaceIds.length === 0) return result + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < MAX_BATCHES_PER_TABLE) { + try { + const deleted = await db + .delete(tableDef) + .where(and(inArray(workspaceIdCol, workspaceIds), lt(createdAtCol, retentionDate))) + .returning({ id: sql`id` }) + + result.deleted += deleted.length + hasMore = deleted.length === BATCH_SIZE + batchesProcessed++ + + if (deleted.length > 0) { + logger.info(`[${tableName}] Batch ${batchesProcessed}: deleted ${deleted.length} rows`) + } else { + logger.info(`[${tableName}] No expired rows found`) + } + } catch (error) { + result.failed++ + logger.error(`[${tableName}] Batch delete failed:`, { error }) + hasMore = false + } + } + + return result +} + +/** + * Delete copilot run checkpoints and async tool calls via join through copilotRuns. + * These tables don't have a direct workspaceId — we find qualifying run IDs first. + */ +async function cleanupRunChildren( + workspaceIds: string[], + retentionDate: Date, + tierLabel: string +): Promise { + const results: TableCleanupResult[] = [] + if (workspaceIds.length === 0) return results + + // Find run IDs in scope + const runIds = await db + .select({ id: copilotRuns.id }) + .from(copilotRuns) + .where(and(inArray(copilotRuns.workspaceId, workspaceIds), lt(copilotRuns.createdAt, retentionDate))) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + if (runIds.length === 0) { + results.push({ table: `${tierLabel}/copilotRunCheckpoints`, deleted: 0, failed: 0 }) + results.push({ table: `${tierLabel}/copilotAsyncToolCalls`, deleted: 0, failed: 0 }) + return results + } + + const ids = runIds.map((r) => r.id) + + // Delete checkpoints + const checkpointResult: TableCleanupResult = { + table: `${tierLabel}/copilotRunCheckpoints`, + deleted: 0, + failed: 0, + } + try { + const deleted = await db + .delete(copilotRunCheckpoints) + .where(inArray(copilotRunCheckpoints.runId, ids)) + .returning({ id: sql`id` }) + checkpointResult.deleted = deleted.length + logger.info(`[${tierLabel}/copilotRunCheckpoints] Deleted ${deleted.length} rows`) + } catch (error) { + checkpointResult.failed++ + logger.error(`[${tierLabel}/copilotRunCheckpoints] Delete failed:`, { error }) + } + results.push(checkpointResult) + + // Delete async tool calls + const toolCallResult: TableCleanupResult = { + table: `${tierLabel}/copilotAsyncToolCalls`, + deleted: 0, + failed: 0, + } + try { + const deleted = await db + .delete(copilotAsyncToolCalls) + .where(inArray(copilotAsyncToolCalls.runId, ids)) + .returning({ id: sql`id` }) + toolCallResult.deleted = deleted.length + logger.info(`[${tierLabel}/copilotAsyncToolCalls] Deleted ${deleted.length} rows`) + } catch (error) { + toolCallResult.failed++ + logger.error(`[${tierLabel}/copilotAsyncToolCalls] Delete failed:`, { error }) + } + results.push(toolCallResult) + + return results +} + +export const cleanupTasksTask = task({ + id: 'cleanup-tasks', + run: async () => { + const startTime = Date.now() + + logger.info('Starting task cleanup') + + const groups = await resolveRetentionGroups('taskCleanupHours') + + for (const group of groups) { + logger.info( + `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` + ) + + // Delete run children first (checkpoints, tool calls) since they reference runs + const runChildResults = await cleanupRunChildren( + group.workspaceIds, + group.retentionDate, + group.tierLabel + ) + for (const r of runChildResults) { + if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) + } + + // Delete feedback — no direct workspaceId, find via copilotChats + const feedbackResult: TableCleanupResult = { + table: `${group.tierLabel}/copilotFeedback`, + deleted: 0, + failed: 0, + } + try { + const chatIds = await db + .select({ id: copilotChats.id }) + .from(copilotChats) + .where( + and( + inArray(copilotChats.workspaceId, group.workspaceIds), + lt(copilotChats.createdAt, group.retentionDate) + ) + ) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + if (chatIds.length > 0) { + const deleted = await db + .delete(copilotFeedback) + .where(inArray(copilotFeedback.chatId, chatIds.map((c) => c.id))) + .returning({ id: sql`id` }) + feedbackResult.deleted = deleted.length + logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) + } else { + logger.info(`[${feedbackResult.table}] No expired rows found`) + } + } catch (error) { + feedbackResult.failed++ + logger.error(`[${feedbackResult.table}] Delete failed:`, { error }) + } + + // Delete copilot runs (has workspaceId directly, cascades checkpoints) + const runsResult = await cleanupTable( + copilotRuns, + copilotRuns.workspaceId, + copilotRuns.createdAt, + group.workspaceIds, + group.retentionDate, + `${group.tierLabel}/copilotRuns` + ) + + // Delete copilot chats (has workspaceId directly) + const chatsResult = await cleanupTable( + copilotChats, + copilotChats.workspaceId, + copilotChats.createdAt, + group.workspaceIds, + group.retentionDate, + `${group.tierLabel}/copilotChats` + ) + + // Delete mothership inbox tasks (has workspaceId directly) + const inboxResult = await cleanupTable( + mothershipInboxTask, + mothershipInboxTask.workspaceId, + mothershipInboxTask.createdAt, + group.workspaceIds, + group.retentionDate, + `${group.tierLabel}/mothershipInboxTask` + ) + + const totalDeleted = + runChildResults.reduce((s, r) => s + r.deleted, 0) + + runsResult.deleted + + chatsResult.deleted + + inboxResult.deleted + + logger.info(`[${group.tierLabel}] Complete: ${totalDeleted} total rows deleted`) + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Task cleanup completed in ${timeElapsed.toFixed(2)}s`) + }, +}) diff --git a/apps/sim/background/redact-task-context.ts b/apps/sim/background/redact-task-context.ts new file mode 100644 index 00000000000..0605b1940b1 --- /dev/null +++ b/apps/sim/background/redact-task-context.ts @@ -0,0 +1,389 @@ +import { db } from '@sim/db' +import { + copilotAsyncToolCalls, + copilotChats, + copilotFeedback, + copilotRunCheckpoints, + copilotRuns, + mothershipInboxTask, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { and, eq, inArray, isNull, lt } from 'drizzle-orm' +import { + SUPPORTED_PII_ENTITIES, + validatePII, +} from '@/lib/guardrails/validate_pii' +import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' + +const logger = createLogger('RedactTaskContext') + +const BATCH_SIZE = 100 +const ALL_ENTITY_TYPES = Object.keys(SUPPORTED_PII_ENTITIES) + +async function maskText(text: string | null, requestId: string): Promise { + if (!text || text.length === 0) return null + + const result = await validatePII({ + text, + entityTypes: ALL_ENTITY_TYPES, + mode: 'mask', + language: 'en', + requestId, + }) + + if (result.maskedText && result.detectedEntities.length > 0) { + return result.maskedText + } + return null +} + +async function maskJsonb( + value: unknown, + requestId: string +): Promise<{ masked: unknown; changed: boolean }> { + if (typeof value === 'string') { + const masked = await maskText(value, requestId) + if (masked !== null) return { masked, changed: true } + return { masked: value, changed: false } + } + + if (Array.isArray(value)) { + let anyChanged = false + const result = [] + for (let i = 0; i < value.length; i++) { + const { masked, changed } = await maskJsonb(value[i], `${requestId}-${i}`) + result.push(masked) + if (changed) anyChanged = true + } + return { masked: result, changed: anyChanged } + } + + if (value && typeof value === 'object') { + let anyChanged = false + const result: Record = {} + for (const [key, val] of Object.entries(value)) { + const { masked, changed } = await maskJsonb(val, `${requestId}-${key}`) + result[key] = masked + if (changed) anyChanged = true + } + return { masked: result, changed: anyChanged } + } + + return { masked: value, changed: false } +} + +interface RedactionStats { + table: string + processed: number + redacted: number + failed: number +} + +function emptyStats(table: string): RedactionStats { + return { table, processed: 0, redacted: 0, failed: 0 } +} + +async function redactCopilotChats( + workspaceIds: string[], + retentionDate: Date +): Promise { + const stats = emptyStats('copilotChats') + if (workspaceIds.length === 0) return stats + + const rows = await db + .select({ id: copilotChats.id, messages: copilotChats.messages }) + .from(copilotChats) + .where( + and( + inArray(copilotChats.workspaceId, workspaceIds), + isNull(copilotChats.redactedAt), + lt(copilotChats.createdAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + for (const row of rows) { + stats.processed++ + try { + const { masked, changed } = await maskJsonb(row.messages, `chat-${row.id}`) + await db + .update(copilotChats) + .set({ messages: changed ? masked : row.messages, redactedAt: new Date() }) + .where(eq(copilotChats.id, row.id)) + if (changed) stats.redacted++ + } catch (error) { + stats.failed++ + logger.error(`Failed to redact copilotChat ${row.id}:`, { error }) + } + } + + return stats +} + +async function redactMothershipInboxTasks( + workspaceIds: string[], + retentionDate: Date +): Promise { + const stats = emptyStats('mothershipInboxTask') + if (workspaceIds.length === 0) return stats + + const rows = await db + .select({ + id: mothershipInboxTask.id, + fromEmail: mothershipInboxTask.fromEmail, + fromName: mothershipInboxTask.fromName, + subject: mothershipInboxTask.subject, + bodyPreview: mothershipInboxTask.bodyPreview, + bodyText: mothershipInboxTask.bodyText, + bodyHtml: mothershipInboxTask.bodyHtml, + ccRecipients: mothershipInboxTask.ccRecipients, + }) + .from(mothershipInboxTask) + .where( + and( + inArray(mothershipInboxTask.workspaceId, workspaceIds), + isNull(mothershipInboxTask.redactedAt), + lt(mothershipInboxTask.createdAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + for (const row of rows) { + stats.processed++ + try { + const rid = `inbox-${row.id}` + const [mFromEmail, mFromName, mSubject, mBodyPreview, mBodyText, mBodyHtml, mCc] = + await Promise.all([ + maskText(row.fromEmail, `${rid}-fromEmail`), + maskText(row.fromName, `${rid}-fromName`), + maskText(row.subject, `${rid}-subject`), + maskText(row.bodyPreview, `${rid}-bodyPreview`), + maskText(row.bodyText, `${rid}-bodyText`), + maskText(row.bodyHtml, `${rid}-bodyHtml`), + maskText(row.ccRecipients, `${rid}-ccRecipients`), + ]) + + const hasChanges = + mFromEmail !== null || + mFromName !== null || + mSubject !== null || + mBodyPreview !== null || + mBodyText !== null || + mBodyHtml !== null || + mCc !== null + + const updateData: Record = { redactedAt: new Date() } + if (mFromEmail !== null) updateData.fromEmail = mFromEmail + if (mFromName !== null) updateData.fromName = mFromName + if (mSubject !== null) updateData.subject = mSubject + if (mBodyPreview !== null) updateData.bodyPreview = mBodyPreview + if (mBodyText !== null) updateData.bodyText = mBodyText + if (mBodyHtml !== null) updateData.bodyHtml = mBodyHtml + if (mCc !== null) updateData.ccRecipients = mCc + + await db + .update(mothershipInboxTask) + .set(updateData) + .where(eq(mothershipInboxTask.id, row.id)) + + if (hasChanges) stats.redacted++ + } catch (error) { + stats.failed++ + logger.error(`Failed to redact mothershipInboxTask ${row.id}:`, { error }) + } + } + + return stats +} + +async function redactCopilotFeedback( + workspaceIds: string[], + retentionDate: Date +): Promise { + const stats = emptyStats('copilotFeedback') + if (workspaceIds.length === 0) return stats + + const rows = await db + .select({ + id: copilotFeedback.feedbackId, + userQuery: copilotFeedback.userQuery, + agentResponse: copilotFeedback.agentResponse, + feedback: copilotFeedback.feedback, + }) + .from(copilotFeedback) + .innerJoin(copilotChats, eq(copilotFeedback.chatId, copilotChats.id)) + .where( + and( + inArray(copilotChats.workspaceId, workspaceIds), + isNull(copilotFeedback.redactedAt), + lt(copilotFeedback.createdAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + for (const row of rows) { + stats.processed++ + try { + const rid = `feedback-${row.id}` + const [mQuery, mResponse, mFeedback] = await Promise.all([ + maskText(row.userQuery, `${rid}-query`), + maskText(row.agentResponse, `${rid}-response`), + maskText(row.feedback, `${rid}-feedback`), + ]) + + const hasChanges = mQuery !== null || mResponse !== null || mFeedback !== null + const updateData: Record = { redactedAt: new Date() } + if (mQuery !== null) updateData.userQuery = mQuery + if (mResponse !== null) updateData.agentResponse = mResponse + if (mFeedback !== null) updateData.feedback = mFeedback + + await db + .update(copilotFeedback) + .set(updateData) + .where(eq(copilotFeedback.feedbackId, row.id)) + + if (hasChanges) stats.redacted++ + } catch (error) { + stats.failed++ + logger.error(`Failed to redact copilotFeedback ${row.id}:`, { error }) + } + } + + return stats +} + +async function redactCopilotRunCheckpoints( + workspaceIds: string[], + retentionDate: Date +): Promise { + const stats = emptyStats('copilotRunCheckpoints') + if (workspaceIds.length === 0) return stats + + const rows = await db + .select({ + id: copilotRunCheckpoints.id, + conversationSnapshot: copilotRunCheckpoints.conversationSnapshot, + }) + .from(copilotRunCheckpoints) + .innerJoin(copilotRuns, eq(copilotRunCheckpoints.runId, copilotRuns.id)) + .where( + and( + inArray(copilotRuns.workspaceId, workspaceIds), + isNull(copilotRunCheckpoints.redactedAt), + lt(copilotRunCheckpoints.createdAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + for (const row of rows) { + stats.processed++ + try { + const { masked, changed } = await maskJsonb(row.conversationSnapshot, `checkpoint-${row.id}`) + await db + .update(copilotRunCheckpoints) + .set({ + conversationSnapshot: changed ? masked : row.conversationSnapshot, + redactedAt: new Date(), + }) + .where(eq(copilotRunCheckpoints.id, row.id)) + if (changed) stats.redacted++ + } catch (error) { + stats.failed++ + logger.error(`Failed to redact copilotRunCheckpoint ${row.id}:`, { error }) + } + } + + return stats +} + +async function redactCopilotAsyncToolCalls( + workspaceIds: string[], + retentionDate: Date +): Promise { + const stats = emptyStats('copilotAsyncToolCalls') + if (workspaceIds.length === 0) return stats + + const rows = await db + .select({ + id: copilotAsyncToolCalls.id, + args: copilotAsyncToolCalls.args, + result: copilotAsyncToolCalls.result, + }) + .from(copilotAsyncToolCalls) + .innerJoin(copilotRuns, eq(copilotAsyncToolCalls.runId, copilotRuns.id)) + .where( + and( + inArray(copilotRuns.workspaceId, workspaceIds), + isNull(copilotAsyncToolCalls.redactedAt), + lt(copilotAsyncToolCalls.createdAt, retentionDate) + ) + ) + .limit(BATCH_SIZE) + + for (const row of rows) { + stats.processed++ + try { + const rid = `toolcall-${row.id}` + const [argsResult, resultResult] = await Promise.all([ + row.args ? maskJsonb(row.args, `${rid}-args`) : { masked: row.args, changed: false }, + row.result ? maskJsonb(row.result, `${rid}-result`) : { masked: row.result, changed: false }, + ]) + + const updateData: Record = { redactedAt: new Date() } + if (argsResult.changed) updateData.args = argsResult.masked + if (resultResult.changed) updateData.result = resultResult.masked + + await db + .update(copilotAsyncToolCalls) + .set(updateData) + .where(eq(copilotAsyncToolCalls.id, row.id)) + + if (argsResult.changed || resultResult.changed) stats.redacted++ + } catch (error) { + stats.failed++ + logger.error(`Failed to redact copilotAsyncToolCall ${row.id}:`, { error }) + } + } + + return stats +} + +const REDACTION_FUNCTIONS = [ + redactCopilotChats, + redactMothershipInboxTasks, + redactCopilotFeedback, + redactCopilotRunCheckpoints, + redactCopilotAsyncToolCalls, +] as const + +export const redactTaskContextTask = task({ + id: 'redact-task-context', + run: async () => { + const startTime = Date.now() + + logger.info('Starting task context redaction') + + const groups = await resolveRetentionGroups('taskRedactionHours') + + for (const group of groups) { + logger.info( + `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` + ) + + for (const redactFn of REDACTION_FUNCTIONS) { + const stats = await redactFn(group.workspaceIds, group.retentionDate) + if (stats.processed > 0) { + logger.info( + `[${group.tierLabel}/${stats.table}] Processed ${stats.processed}, redacted ${stats.redacted}, failed ${stats.failed}` + ) + } else { + logger.info(`[${group.tierLabel}/${stats.table}] No rows to process`) + } + } + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Task context redaction completed in ${timeElapsed.toFixed(2)}s`) + }, +}) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index b3db35fd5b8..d2a85ffead1 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -122,6 +122,13 @@ function LockedView({ data }: { data: DataRetentionResponse }) { onChange={() => {}} disabled /> + {}} + disabled + />

{planLabel(data.plan)} plan defaults. Upgrade to Enterprise to customize retention periods. @@ -140,6 +147,9 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work const [taskRedactionDays, setTaskRedactionDays] = useState( hoursToDisplayDays(data.effective.taskRedactionHours) ) + const [taskCleanupDays, setTaskCleanupDays] = useState( + hoursToDisplayDays(data.effective.taskCleanupHours) + ) const [saveError, setSaveError] = useState(null) const [saveSuccess, setSaveSuccess] = useState(false) @@ -155,6 +165,7 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work logRetentionHours: daysToHours(logDays), softDeleteRetentionHours: daysToHours(softDeleteDays), taskRedactionHours: daysToHours(taskRedactionDays), + taskCleanupHours: daysToHours(taskCleanupDays), }, }) setSaveSuccess(true) @@ -163,7 +174,7 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work logger.error('Failed to save data retention settings', { error }) setSaveError(error instanceof Error ? error.message : 'Failed to save settings') } - }, [workspaceId, logDays, softDeleteDays, taskRedactionDays]) + }, [workspaceId, logDays, softDeleteDays, taskRedactionDays, taskCleanupDays]) return (

@@ -193,6 +204,13 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work onChange={setTaskRedactionDays} disabled={false} /> +
diff --git a/apps/sim/ee/data-retention/hooks/data-retention.ts b/apps/sim/ee/data-retention/hooks/data-retention.ts index dde645b353d..a6180f07900 100644 --- a/apps/sim/ee/data-retention/hooks/data-retention.ts +++ b/apps/sim/ee/data-retention/hooks/data-retention.ts @@ -6,6 +6,7 @@ export interface RetentionValues { logRetentionHours: number | null softDeleteRetentionHours: number | null taskRedactionHours: number | null + taskCleanupHours: number | null } export interface DataRetentionResponse { diff --git a/apps/sim/lib/core/async-jobs/backends/bullmq.ts b/apps/sim/lib/core/async-jobs/backends/bullmq.ts new file mode 100644 index 00000000000..2345bd4ca5b --- /dev/null +++ b/apps/sim/lib/core/async-jobs/backends/bullmq.ts @@ -0,0 +1,131 @@ +import { createLogger } from '@sim/logger' +import type { Job as BullMQJob } from 'bullmq' +import { + type EnqueueOptions, + JOB_STATUS, + type Job, + type JobQueueBackend, + type JobStatus, + type JobType, +} from '@/lib/core/async-jobs/types' +import { type BullMQJobData, createBullMQJobData, getBullMQQueue } from '@/lib/core/bullmq' + +const logger = createLogger('BullMQJobQueue') + +function mapBullMQStatus(status: string): JobStatus { + switch (status) { + case 'active': + return JOB_STATUS.PROCESSING + case 'completed': + return JOB_STATUS.COMPLETED + case 'failed': + return JOB_STATUS.FAILED + default: + return JOB_STATUS.PENDING + } +} + +async function toJob( + queueType: JobType, + bullJob: BullMQJob> | null +): Promise { + if (!bullJob) { + return null + } + + const status = mapBullMQStatus(await bullJob.getState()) + + return { + id: bullJob.id ?? '', + type: queueType, + payload: bullJob.data.payload, + status, + createdAt: new Date(bullJob.timestamp), + startedAt: bullJob.processedOn ? new Date(bullJob.processedOn) : undefined, + completedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined, + attempts: bullJob.attemptsMade, + maxAttempts: bullJob.opts.attempts ?? 1, + error: bullJob.failedReason || undefined, + output: bullJob.returnvalue, + metadata: bullJob.data.metadata ?? {}, + } +} + +export class BullMQJobQueue implements JobQueueBackend { + async enqueue( + type: JobType, + payload: TPayload, + options?: EnqueueOptions + ): Promise { + const queue = getBullMQQueue(type) + + const job = await queue.add( + options?.name ?? type, + createBullMQJobData(payload, options?.metadata), + { + jobId: options?.jobId, + attempts: options?.maxAttempts, + priority: options?.priority, + delay: options?.delayMs, + } + ) + + logger.debug('Enqueued job via BullMQ', { + jobId: job.id, + type, + name: options?.name ?? type, + }) + + return String(job.id) + } + + async getJob(jobId: string): Promise { + const workflowJob = await getBullMQQueue('workflow-execution').getJob(jobId) + if (workflowJob) { + return toJob('workflow-execution', workflowJob) + } + + const webhookJob = await getBullMQQueue('webhook-execution').getJob(jobId) + if (webhookJob) { + return toJob('webhook-execution', webhookJob) + } + + const scheduleJob = await getBullMQQueue('schedule-execution').getJob(jobId) + if (scheduleJob) { + return toJob('schedule-execution', scheduleJob) + } + + const resumeJob = await getBullMQQueue('resume-execution').getJob(jobId) + if (resumeJob) { + return toJob('resume-execution', resumeJob) + } + + const cleanupLogsJob = await getBullMQQueue('cleanup-logs').getJob(jobId) + if (cleanupLogsJob) { + return toJob('cleanup-logs', cleanupLogsJob) + } + + const cleanupSoftDeletesJob = await getBullMQQueue('cleanup-soft-deletes').getJob(jobId) + if (cleanupSoftDeletesJob) { + return toJob('cleanup-soft-deletes', cleanupSoftDeletesJob) + } + + const cleanupTasksJob = await getBullMQQueue('cleanup-tasks').getJob(jobId) + if (cleanupTasksJob) { + return toJob('cleanup-tasks', cleanupTasksJob) + } + + const redactTaskContextJob = await getBullMQQueue('redact-task-context').getJob(jobId) + if (redactTaskContextJob) { + return toJob('redact-task-context', redactTaskContextJob) + } + + return null + } + + async startJob(_jobId: string): Promise {} + + async completeJob(_jobId: string, _output: unknown): Promise {} + + async markJobFailed(_jobId: string, _error: string): Promise {} +} diff --git a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts index c3863d418a8..7008be1327a 100644 --- a/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts +++ b/apps/sim/lib/core/async-jobs/backends/trigger-dev.ts @@ -20,6 +20,10 @@ const JOB_TYPE_TO_TASK_ID: Record = { 'schedule-execution': 'schedule-execution', 'webhook-execution': 'webhook-execution', 'resume-execution': 'resume-execution', + 'cleanup-logs': 'cleanup-logs', + 'cleanup-soft-deletes': 'cleanup-soft-deletes', + 'cleanup-tasks': 'cleanup-tasks', + 'redact-task-context': 'redact-task-context', } /** diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index 7531a46eca7..89670adc002 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -25,6 +25,10 @@ export type JobType = | 'schedule-execution' | 'webhook-execution' | 'resume-execution' + | 'cleanup-logs' + | 'cleanup-soft-deletes' + | 'cleanup-tasks' + | 'redact-task-context' export type AsyncExecutionCorrelationSource = 'workflow' | 'schedule' | 'webhook' diff --git a/apps/sim/lib/core/bullmq/queues.ts b/apps/sim/lib/core/bullmq/queues.ts new file mode 100644 index 00000000000..d4ea4b3c9a1 --- /dev/null +++ b/apps/sim/lib/core/bullmq/queues.ts @@ -0,0 +1,246 @@ +import { Queue, QueueEvents } from 'bullmq' +import type { JobMetadata, JobType } from '@/lib/core/async-jobs/types' +import { getBullMQConnectionOptions } from '@/lib/core/bullmq/connection' +import type { WorkspaceDispatchQueueName } from '@/lib/core/workspace-dispatch/types' + +export const KNOWLEDGE_CONNECTOR_SYNC_QUEUE = 'knowledge-connector-sync' as const +export const KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE = 'knowledge-process-document' as const +export const MOTHERSHIP_JOB_EXECUTION_QUEUE = 'mothership-job-execution' as const +export const WORKSPACE_NOTIFICATION_DELIVERY_QUEUE = 'workspace-notification-delivery' as const + +export interface BullMQJobData { + payload: TPayload + metadata?: JobMetadata +} + +let workflowQueueInstance: Queue | null = null +let webhookQueueInstance: Queue | null = null +let scheduleQueueInstance: Queue | null = null +let resumeQueueInstance: Queue | null = null +let knowledgeConnectorSyncQueueInstance: Queue | null = null +let knowledgeDocumentProcessingQueueInstance: Queue | null = null +let mothershipJobExecutionQueueInstance: Queue | null = null +let workspaceNotificationDeliveryQueueInstance: Queue | null = null +let cleanupLogsQueueInstance: Queue | null = null +let cleanupSoftDeletesQueueInstance: Queue | null = null +let cleanupTasksQueueInstance: Queue | null = null +let redactTaskContextQueueInstance: Queue | null = null +let workflowQueueEventsInstance: QueueEvents | null = null + +function getQueueDefaultOptions(type: JobType) { + switch (type) { + case 'workflow-execution': + return { + attempts: 3, + backoff: { type: 'exponential' as const, delay: 1000 }, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + } + case 'webhook-execution': + return { + attempts: 2, + backoff: { type: 'exponential' as const, delay: 2000 }, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 3 * 24 * 60 * 60 }, + } + case 'schedule-execution': + return { + attempts: 2, + backoff: { type: 'exponential' as const, delay: 5000 }, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 3 * 24 * 60 * 60 }, + } + case 'resume-execution': + return { + attempts: 1, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 3 * 24 * 60 * 60 }, + } + case 'cleanup-logs': + case 'cleanup-soft-deletes': + case 'cleanup-tasks': + case 'redact-task-context': + return { + attempts: 1, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + } + } +} + +function createQueue(type: JobType): Queue { + return new Queue(type, { + connection: getBullMQConnectionOptions(), + defaultJobOptions: getQueueDefaultOptions(type), + }) +} + +function createNamedQueue( + name: + | typeof KNOWLEDGE_CONNECTOR_SYNC_QUEUE + | typeof KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE + | typeof MOTHERSHIP_JOB_EXECUTION_QUEUE + | typeof WORKSPACE_NOTIFICATION_DELIVERY_QUEUE +): Queue { + switch (name) { + case KNOWLEDGE_CONNECTOR_SYNC_QUEUE: + return new Queue(name, { + connection: getBullMQConnectionOptions(), + defaultJobOptions: { + attempts: 3, + backoff: { type: 'exponential', delay: 5000 }, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + }, + }) + case KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE: + return new Queue(name, { + connection: getBullMQConnectionOptions(), + defaultJobOptions: { + attempts: 3, + backoff: { type: 'exponential', delay: 1000 }, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + }, + }) + case MOTHERSHIP_JOB_EXECUTION_QUEUE: + return new Queue(name, { + connection: getBullMQConnectionOptions(), + defaultJobOptions: { + attempts: 1, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + }, + }) + case WORKSPACE_NOTIFICATION_DELIVERY_QUEUE: + return new Queue(name, { + connection: getBullMQConnectionOptions(), + defaultJobOptions: { + attempts: 1, + removeOnComplete: { age: 24 * 60 * 60 }, + removeOnFail: { age: 7 * 24 * 60 * 60 }, + }, + }) + } +} + +export function getBullMQQueue(type: JobType): Queue { + switch (type) { + case 'workflow-execution': + if (!workflowQueueInstance) { + workflowQueueInstance = createQueue(type) + } + return workflowQueueInstance + case 'webhook-execution': + if (!webhookQueueInstance) { + webhookQueueInstance = createQueue(type) + } + return webhookQueueInstance + case 'schedule-execution': + if (!scheduleQueueInstance) { + scheduleQueueInstance = createQueue(type) + } + return scheduleQueueInstance + case 'resume-execution': + if (!resumeQueueInstance) { + resumeQueueInstance = createQueue(type) + } + return resumeQueueInstance + case 'cleanup-logs': + if (!cleanupLogsQueueInstance) { + cleanupLogsQueueInstance = createQueue(type) + } + return cleanupLogsQueueInstance + case 'cleanup-soft-deletes': + if (!cleanupSoftDeletesQueueInstance) { + cleanupSoftDeletesQueueInstance = createQueue(type) + } + return cleanupSoftDeletesQueueInstance + case 'cleanup-tasks': + if (!cleanupTasksQueueInstance) { + cleanupTasksQueueInstance = createQueue(type) + } + return cleanupTasksQueueInstance + case 'redact-task-context': + if (!redactTaskContextQueueInstance) { + redactTaskContextQueueInstance = createQueue(type) + } + return redactTaskContextQueueInstance + } +} + +export function getBullMQQueueByName(queueName: WorkspaceDispatchQueueName): Queue { + switch (queueName) { + case 'workflow-execution': + case 'webhook-execution': + case 'schedule-execution': + case 'resume-execution': + case 'cleanup-logs': + case 'cleanup-soft-deletes': + case 'cleanup-tasks': + case 'redact-task-context': + return getBullMQQueue(queueName) + case KNOWLEDGE_CONNECTOR_SYNC_QUEUE: + return getKnowledgeConnectorSyncQueue() + case KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE: + return getKnowledgeDocumentProcessingQueue() + case MOTHERSHIP_JOB_EXECUTION_QUEUE: + return getMothershipJobExecutionQueue() + case WORKSPACE_NOTIFICATION_DELIVERY_QUEUE: + return getWorkspaceNotificationDeliveryQueue() + } +} + +export function getWorkflowQueueEvents(): QueueEvents { + if (!workflowQueueEventsInstance) { + workflowQueueEventsInstance = new QueueEvents('workflow-execution', { + connection: getBullMQConnectionOptions(), + }) + } + + return workflowQueueEventsInstance +} + +export function getKnowledgeConnectorSyncQueue(): Queue { + if (!knowledgeConnectorSyncQueueInstance) { + knowledgeConnectorSyncQueueInstance = createNamedQueue(KNOWLEDGE_CONNECTOR_SYNC_QUEUE) + } + + return knowledgeConnectorSyncQueueInstance +} + +export function getKnowledgeDocumentProcessingQueue(): Queue { + if (!knowledgeDocumentProcessingQueueInstance) { + knowledgeDocumentProcessingQueueInstance = createNamedQueue(KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE) + } + + return knowledgeDocumentProcessingQueueInstance +} + +export function getMothershipJobExecutionQueue(): Queue { + if (!mothershipJobExecutionQueueInstance) { + mothershipJobExecutionQueueInstance = createNamedQueue(MOTHERSHIP_JOB_EXECUTION_QUEUE) + } + + return mothershipJobExecutionQueueInstance +} + +export function getWorkspaceNotificationDeliveryQueue(): Queue { + if (!workspaceNotificationDeliveryQueueInstance) { + workspaceNotificationDeliveryQueueInstance = createNamedQueue( + WORKSPACE_NOTIFICATION_DELIVERY_QUEUE + ) + } + + return workspaceNotificationDeliveryQueueInstance +} + +export function createBullMQJobData( + payload: TPayload, + metadata?: JobMetadata +): BullMQJobData { + return { + payload, + metadata: metadata ?? {}, + } +} diff --git a/apps/sim/lib/retention/workspace-tiers.ts b/apps/sim/lib/retention/workspace-tiers.ts new file mode 100644 index 00000000000..121fb948629 --- /dev/null +++ b/apps/sim/lib/retention/workspace-tiers.ts @@ -0,0 +1,146 @@ +import { db } from '@sim/db' +import { subscription, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' +import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' +import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' +import { env } from '@/lib/core/config/env' + +const logger = createLogger('WorkspaceTiers') + +export interface RetentionGroup { + workspaceIds: string[] + retentionDate: Date + tierLabel: string +} + +type RetentionColumn = + | 'logRetentionHours' + | 'softDeleteRetentionHours' + | 'taskRedactionHours' + | 'taskCleanupHours' + +/** + * Resolve all workspaces into retention groups based on their plan tier + * and any enterprise-configured retention overrides. + * + * Returns groups with computed retention dates, ready for batch processing. + * Enterprise workspaces with NULL retention are excluded (no cleanup). + */ +export async function resolveRetentionGroups( + retentionColumn: RetentionColumn +): Promise { + const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') + const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') + + const now = Date.now() + const freeRetentionDate = new Date(now - freeRetentionDays * 24 * 60 * 60 * 1000) + const paidRetentionDate = new Date(now - paidRetentionDays * 24 * 60 * 60 * 1000) + + logger.info(`Resolving retention groups for ${retentionColumn}`, { + freeRetentionDays, + paidRetentionDays, + freeRetentionDate: freeRetentionDate.toISOString(), + paidRetentionDate: paidRetentionDate.toISOString(), + }) + + const groups: RetentionGroup[] = [] + + // --- Free workspaces (no paid subscription) --- + + const freeWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .leftJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + sqlIsPaid(subscription.plan) + ) + ) + .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) + + const freeIds = freeWorkspaceRows.map((w) => w.id) + if (freeIds.length > 0) { + groups.push({ + workspaceIds: freeIds, + retentionDate: freeRetentionDate, + tierLabel: 'free', + }) + } + logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) + + // --- Pro/Team workspaces (paid non-enterprise) --- + + const paidWorkspaceRows = await db + .select({ id: workspace.id }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) + ) + ) + .where(isNull(workspace.archivedAt)) + + const paidIds = paidWorkspaceRows.map((w) => w.id) + if (paidIds.length > 0) { + groups.push({ + workspaceIds: paidIds, + retentionDate: paidRetentionDate, + tierLabel: 'paid', + }) + } + logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) + + // --- Enterprise with custom retention --- + // Enterprise with NULL retention column = skip (no cleanup) + + const retentionCol = workspace[retentionColumn] + + const enterpriseWorkspaceRows = await db + .select({ + id: workspace.id, + retentionHours: retentionCol, + }) + .from(workspace) + .innerJoin( + subscription, + and( + eq(subscription.referenceId, workspace.billedAccountUserId), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), + eq(subscription.plan, 'enterprise') + ) + ) + .where(and(isNull(workspace.archivedAt), isNotNull(retentionCol))) + + // Group by retention hours to batch workspaces with same retention + const enterpriseGroups = new Map() + for (const ws of enterpriseWorkspaceRows) { + const hours = ws.retentionHours as number + const group = enterpriseGroups.get(hours) ?? [] + group.push(ws.id) + enterpriseGroups.set(hours, group) + } + + logger.info( + `[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct periods). Workspaces with NULL are skipped.` + ) + + for (const [hours, ids] of enterpriseGroups) { + const retentionDate = new Date(now - hours * 60 * 60 * 1000) + groups.push({ + workspaceIds: ids, + retentionDate, + tierLabel: `enterprise-${hours}h`, + }) + logger.info( + `[enterprise-${hours}h] ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}` + ) + } + + return groups +} diff --git a/packages/db/migrations/0190_lean_terror.sql b/packages/db/migrations/0190_lean_terror.sql new file mode 100644 index 00000000000..9bd109d5ce8 --- /dev/null +++ b/packages/db/migrations/0190_lean_terror.sql @@ -0,0 +1,5 @@ +ALTER TABLE "copilot_async_tool_calls" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint +ALTER TABLE "copilot_chats" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint +ALTER TABLE "copilot_feedback" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint +ALTER TABLE "copilot_run_checkpoints" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint +ALTER TABLE "mothership_inbox_task" ADD COLUMN "redacted_at" timestamp; \ No newline at end of file diff --git a/packages/db/migrations/0191_parched_living_mummy.sql b/packages/db/migrations/0191_parched_living_mummy.sql new file mode 100644 index 00000000000..793dacdb215 --- /dev/null +++ b/packages/db/migrations/0191_parched_living_mummy.sql @@ -0,0 +1 @@ +ALTER TABLE "workspace" ADD COLUMN "task_cleanup_hours" integer; \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 4be230ede75..757a3b45a6e 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1045,6 +1045,7 @@ export const workspace = pgTable('workspace', { logRetentionHours: integer('log_retention_hours'), softDeleteRetentionHours: integer('soft_delete_retention_hours'), taskRedactionHours: integer('task_redaction_hours'), + taskCleanupHours: integer('task_cleanup_hours'), archivedAt: timestamp('archived_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -1616,6 +1617,7 @@ export const copilotChats = pgTable( config: jsonb('config'), resources: jsonb('resources').notNull().default('[]'), lastSeenAt: timestamp('last_seen_at'), + redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1780,6 +1782,7 @@ export const copilotRunCheckpoints = pgTable( conversationSnapshot: jsonb('conversation_snapshot').notNull().default('{}'), agentState: jsonb('agent_state').notNull().default('{}'), providerRequest: jsonb('provider_request').notNull().default('{}'), + redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1814,6 +1817,7 @@ export const copilotAsyncToolCalls = pgTable( claimedAt: timestamp('claimed_at'), claimedBy: text('claimed_by'), completedAt: timestamp('completed_at'), + redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1945,6 +1949,7 @@ export const copilotFeedback = pgTable( isPositive: boolean('is_positive').notNull(), feedback: text('feedback'), // Optional feedback text workflowYaml: text('workflow_yaml'), // Optional workflow YAML if edit/build workflow was triggered + redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -2940,6 +2945,7 @@ export const mothershipInboxTask = pgTable( rejectionReason: text('rejection_reason'), hasAttachments: boolean('has_attachments').notNull().default(false), ccRecipients: text('cc_recipients'), + redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), processingStartedAt: timestamp('processing_started_at'), completedAt: timestamp('completed_at'), From aeffefb6a4a39221e1c6c1a6e70e17f8a049e8d1 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 17 Apr 2026 14:23:41 -0700 Subject: [PATCH 03/15] fix migration --- .../api/cron/cleanup-soft-deletes/route.ts | 11 +- apps/sim/app/api/cron/cleanup-tasks/route.ts | 11 +- .../app/api/cron/redact-task-context/route.ts | 28 - apps/sim/app/api/logs/cleanup/route.ts | 11 +- .../workspaces/[id]/data-retention/route.ts | 51 +- apps/sim/background/cleanup-logs.ts | 99 +- apps/sim/background/cleanup-soft-deletes.ts | 112 +- apps/sim/background/cleanup-tasks.ts | 288 +- apps/sim/background/redact-task-context.ts | 389 - .../components/data-retention-settings.tsx | 22 +- .../ee/data-retention/hooks/data-retention.ts | 1 - .../lib/core/async-jobs/backends/bullmq.ts | 5 - .../core/async-jobs/backends/trigger-dev.ts | 1 - apps/sim/lib/core/async-jobs/types.ts | 1 - apps/sim/lib/core/bullmq/queues.ts | 8 - apps/sim/lib/core/config/env.ts | 3 - apps/sim/lib/retention/workspace-tiers.ts | 146 - packages/db/migrations/0190_lean_terror.sql | 5 - .../migrations/0191_parched_living_mummy.sql | 1 - .../db/migrations/meta/0190_snapshot.json | 14658 ---------------- packages/db/schema.ts | 6 - 21 files changed, 381 insertions(+), 15476 deletions(-) delete mode 100644 apps/sim/app/api/cron/redact-task-context/route.ts delete mode 100644 apps/sim/background/redact-task-context.ts delete mode 100644 apps/sim/lib/retention/workspace-tiers.ts delete mode 100644 packages/db/migrations/0190_lean_terror.sql delete mode 100644 packages/db/migrations/0191_parched_living_mummy.sql delete mode 100644 packages/db/migrations/meta/0190_snapshot.json diff --git a/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts index cf33d024450..5e75b0a4525 100644 --- a/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts +++ b/apps/sim/app/api/cron/cleanup-soft-deletes/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { getJobQueue } from '@/lib/core/async-jobs' +import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher' export const dynamic = 'force-dynamic' @@ -12,14 +12,13 @@ export async function GET(request: NextRequest) { const authError = verifyCronAuth(request, 'soft-delete cleanup') if (authError) return authError - const jobQueue = await getJobQueue() - const jobId = await jobQueue.enqueue('cleanup-soft-deletes', {}) + const result = await dispatchCleanupJobs('cleanup-soft-deletes', 'softDeleteRetentionHours') - logger.info('Soft-delete cleanup job dispatched', { jobId }) + logger.info('Soft-delete cleanup jobs dispatched', result) - return NextResponse.json({ triggered: true, jobId }) + return NextResponse.json({ triggered: true, ...result }) } catch (error) { - logger.error('Failed to dispatch soft-delete cleanup job:', { error }) + logger.error('Failed to dispatch soft-delete cleanup jobs:', { error }) return NextResponse.json({ error: 'Failed to dispatch soft-delete cleanup' }, { status: 500 }) } } diff --git a/apps/sim/app/api/cron/cleanup-tasks/route.ts b/apps/sim/app/api/cron/cleanup-tasks/route.ts index 2304dc1d28d..cf204c064e1 100644 --- a/apps/sim/app/api/cron/cleanup-tasks/route.ts +++ b/apps/sim/app/api/cron/cleanup-tasks/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { getJobQueue } from '@/lib/core/async-jobs' +import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher' export const dynamic = 'force-dynamic' @@ -12,14 +12,13 @@ export async function GET(request: NextRequest) { const authError = verifyCronAuth(request, 'task cleanup') if (authError) return authError - const jobQueue = await getJobQueue() - const jobId = await jobQueue.enqueue('cleanup-tasks', {}) + const result = await dispatchCleanupJobs('cleanup-tasks', 'taskCleanupHours') - logger.info('Task cleanup job dispatched', { jobId }) + logger.info('Task cleanup jobs dispatched', result) - return NextResponse.json({ triggered: true, jobId }) + return NextResponse.json({ triggered: true, ...result }) } catch (error) { - logger.error('Failed to dispatch task cleanup job:', { error }) + logger.error('Failed to dispatch task cleanup jobs:', { error }) return NextResponse.json({ error: 'Failed to dispatch task cleanup' }, { status: 500 }) } } diff --git a/apps/sim/app/api/cron/redact-task-context/route.ts b/apps/sim/app/api/cron/redact-task-context/route.ts deleted file mode 100644 index e0c837a137c..00000000000 --- a/apps/sim/app/api/cron/redact-task-context/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { verifyCronAuth } from '@/lib/auth/internal' -import { getJobQueue } from '@/lib/core/async-jobs' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('TaskRedactionAPI') - -export async function GET(request: NextRequest) { - try { - const authError = verifyCronAuth(request, 'task context redaction') - if (authError) return authError - - const jobQueue = await getJobQueue() - const jobId = await jobQueue.enqueue('redact-task-context', {}) - - logger.info('Task context redaction job dispatched', { jobId }) - - return NextResponse.json({ triggered: true, jobId }) - } catch (error) { - logger.error('Failed to dispatch task context redaction job:', { error }) - return NextResponse.json( - { error: 'Failed to dispatch task context redaction' }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/logs/cleanup/route.ts b/apps/sim/app/api/logs/cleanup/route.ts index 0305ae24a45..aa60f2b4e1e 100644 --- a/apps/sim/app/api/logs/cleanup/route.ts +++ b/apps/sim/app/api/logs/cleanup/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { getJobQueue } from '@/lib/core/async-jobs' +import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher' export const dynamic = 'force-dynamic' @@ -12,14 +12,13 @@ export async function GET(request: NextRequest) { const authError = verifyCronAuth(request, 'logs cleanup') if (authError) return authError - const jobQueue = await getJobQueue() - const jobId = await jobQueue.enqueue('cleanup-logs', {}) + const result = await dispatchCleanupJobs('cleanup-logs', 'logRetentionHours') - logger.info('Log cleanup job dispatched', { jobId }) + logger.info('Log cleanup jobs dispatched', result) - return NextResponse.json({ triggered: true, jobId }) + return NextResponse.json({ triggered: true, ...result }) } catch (error) { - logger.error('Failed to dispatch log cleanup job:', { error }) + logger.error('Failed to dispatch log cleanup jobs:', { error }) return NextResponse.json({ error: 'Failed to dispatch log cleanup' }, { status: 500 }) } } diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts index 4d46c327669..7b8ef9e0bd5 100644 --- a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' +import { getRetentionDefaultHours } from '@/lib/billing/cleanup-dispatcher' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { isEnterprisePlan } from '@/lib/billing/core/subscription' import { @@ -21,40 +22,19 @@ const logger = createLogger('DataRetentionAPI') const MIN_HOURS = 24 const MAX_HOURS = 43800 // 5 years -const FREE_LOG_RETENTION_HOURS = 7 * 24 -const FREE_SOFT_DELETE_RETENTION_HOURS = 7 * 24 -const FREE_TASK_REDACTION_HOURS = null // never -const FREE_TASK_CLEANUP_HOURS = null // never - -const PRO_LOG_RETENTION_HOURS = 30 * 24 -const PRO_SOFT_DELETE_RETENTION_HOURS = 30 * 24 -const PRO_TASK_REDACTION_HOURS = 30 * 24 -const PRO_TASK_CLEANUP_HOURS = null // never - interface PlanDefaults { - logRetentionHours: number - softDeleteRetentionHours: number - taskRedactionHours: number | null + logRetentionHours: number | null + softDeleteRetentionHours: number | null taskCleanupHours: number | null } function getPlanDefaults(plan: 'free' | 'pro' | 'enterprise'): PlanDefaults { - switch (plan) { - case 'enterprise': - case 'pro': - return { - logRetentionHours: PRO_LOG_RETENTION_HOURS, - softDeleteRetentionHours: PRO_SOFT_DELETE_RETENTION_HOURS, - taskRedactionHours: PRO_TASK_REDACTION_HOURS, - taskCleanupHours: PRO_TASK_CLEANUP_HOURS, - } - default: - return { - logRetentionHours: FREE_LOG_RETENTION_HOURS, - softDeleteRetentionHours: FREE_SOFT_DELETE_RETENTION_HOURS, - taskRedactionHours: FREE_TASK_REDACTION_HOURS, - taskCleanupHours: FREE_TASK_CLEANUP_HOURS, - } + const tier: 'free' | 'paid' | 'enterprise' = + plan === 'free' ? 'free' : plan === 'enterprise' ? 'enterprise' : 'paid' + return { + logRetentionHours: getRetentionDefaultHours(tier, 'logRetentionHours'), + softDeleteRetentionHours: getRetentionDefaultHours(tier, 'softDeleteRetentionHours'), + taskCleanupHours: getRetentionDefaultHours(tier, 'taskCleanupHours'), } } @@ -71,7 +51,6 @@ async function resolveWorkspacePlan( const updateRetentionSchema = z.object({ logRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), softDeleteRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), - taskRedactionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), taskCleanupHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(), }) @@ -98,7 +77,6 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ .select({ logRetentionHours: workspace.logRetentionHours, softDeleteRetentionHours: workspace.softDeleteRetentionHours, - taskRedactionHours: workspace.taskRedactionHours, taskCleanupHours: workspace.taskCleanupHours, billedAccountUserId: workspace.billedAccountUserId, }) @@ -123,20 +101,17 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ configured: { logRetentionHours: ws.logRetentionHours, softDeleteRetentionHours: ws.softDeleteRetentionHours, - taskRedactionHours: ws.taskRedactionHours, taskCleanupHours: ws.taskCleanupHours, }, effective: isEnterpriseWorkspace ? { logRetentionHours: ws.logRetentionHours, softDeleteRetentionHours: ws.softDeleteRetentionHours, - taskRedactionHours: ws.taskRedactionHours, taskCleanupHours: ws.taskCleanupHours, } : { logRetentionHours: defaults.logRetentionHours, softDeleteRetentionHours: defaults.softDeleteRetentionHours, - taskRedactionHours: defaults.taskRedactionHours, taskCleanupHours: defaults.taskCleanupHours, }, }, @@ -196,10 +171,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ if (parsed.data.softDeleteRetentionHours !== undefined) { updateData.softDeleteRetentionHours = parsed.data.softDeleteRetentionHours } - if (parsed.data.taskRedactionHours !== undefined) { - updateData.taskRedactionHours = parsed.data.taskRedactionHours - } - if (parsed.data.taskCleanupHours !== undefined) { +if (parsed.data.taskCleanupHours !== undefined) { updateData.taskCleanupHours = parsed.data.taskCleanupHours } @@ -210,7 +182,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ .returning({ logRetentionHours: workspace.logRetentionHours, softDeleteRetentionHours: workspace.softDeleteRetentionHours, - taskRedactionHours: workspace.taskRedactionHours, taskCleanupHours: workspace.taskCleanupHours, }) @@ -242,13 +213,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ configured: { logRetentionHours: updated.logRetentionHours, softDeleteRetentionHours: updated.softDeleteRetentionHours, - taskRedactionHours: updated.taskRedactionHours, taskCleanupHours: updated.taskCleanupHours, }, effective: { logRetentionHours: updated.logRetentionHours, softDeleteRetentionHours: updated.softDeleteRetentionHours, - taskRedactionHours: updated.taskRedactionHours, taskCleanupHours: updated.taskCleanupHours, }, }, diff --git a/apps/sim/background/cleanup-logs.ts b/apps/sim/background/cleanup-logs.ts index e95ee65ecb6..34f035d4a89 100644 --- a/apps/sim/background/cleanup-logs.ts +++ b/apps/sim/background/cleanup-logs.ts @@ -1,10 +1,14 @@ import { db } from '@sim/db' -import { workflowExecutionLogs } from '@sim/db/schema' +import { workflowExecutionLogs, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' -import { and, inArray, lt } from 'drizzle-orm' +import { and, eq, inArray, lt } from 'drizzle-orm' import { snapshotService } from '@/lib/logs/execution/snapshot/service' -import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' +import { + type CleanupJobPayload, + getRetentionDefaultHours, + resolveTierWorkspaceIds, +} from '@/lib/billing/cleanup-dispatcher' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' const logger = createLogger('CleanupLogs') @@ -102,36 +106,79 @@ async function cleanupTier( return results } -export const cleanupLogsTask = task({ - id: 'cleanup-logs', - run: async () => { - const startTime = Date.now() +async function resolvePayload(payload: CleanupJobPayload): Promise<{ + workspaceIds: string[] + retentionHours: number + tierLabel: string +} | null> { + if (payload.tier === 'free' || payload.tier === 'paid') { + const retentionHours = getRetentionDefaultHours(payload.tier, 'logRetentionHours') + if (retentionHours === null) { + logger.info(`[${payload.tier}] No default retention, skipping`) + return null + } + const workspaceIds = await resolveTierWorkspaceIds(payload.tier) + return { workspaceIds, retentionHours, tierLabel: payload.tier } + } + + // enterprise + const [ws] = await db + .select({ logRetentionHours: workspace.logRetentionHours }) + .from(workspace) + .where(eq(workspace.id, payload.workspaceId)) + .limit(1) - logger.info('Starting log cleanup task') + if (!ws?.logRetentionHours) { + logger.info(`[enterprise/${payload.workspaceId}] No retention configured, skipping`) + return null + } + + return { + workspaceIds: [payload.workspaceId], + retentionHours: ws.logRetentionHours, + tierLabel: `enterprise/${payload.workspaceId}`, + } +} - const groups = await resolveRetentionGroups('logRetentionHours') +export async function runCleanupLogs(payload: CleanupJobPayload): Promise { + const startTime = Date.now() - for (const group of groups) { - const results = await cleanupTier(group.workspaceIds, group.retentionDate, group.tierLabel) - logger.info(`[${group.tierLabel}] Result: ${results.deleted} deleted, ${results.deleteFailed} failed out of ${results.total} candidates`) - } + const resolved = await resolvePayload(payload) + if (!resolved) return + + const { workspaceIds, retentionHours, tierLabel } = resolved - // Snapshot cleanup — use shortest retention + 1 day + if (workspaceIds.length === 0) { + logger.info(`[${tierLabel}] No workspaces to process`) + return + } + + const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) + logger.info( + `[${tierLabel}] Cleaning ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` + ) + + const results = await cleanupTier(workspaceIds, retentionDate, tierLabel) + logger.info( + `[${tierLabel}] Result: ${results.deleted} deleted, ${results.deleteFailed} failed out of ${results.total} candidates` + ) + + // Snapshot cleanup runs only on the free job to avoid running it N times for N enterprise workspaces. + if (payload.tier === 'free') { try { - const shortestDays = Math.min( - ...groups.map((g) => (Date.now() - g.retentionDate.getTime()) / (24 * 60 * 60 * 1000)) - ) - if (Number.isFinite(shortestDays)) { - const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots( - Math.floor(shortestDays) + 1 - ) - logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`) - } + const retentionDays = Math.floor(retentionHours / 24) + const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots(retentionDays + 1) + logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`) } catch (snapshotError) { logger.error('Error cleaning up orphaned snapshots:', { snapshotError }) } + } + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`[${tierLabel}] Job completed in ${timeElapsed.toFixed(2)}s`) +} - const timeElapsed = (Date.now() - startTime) / 1000 - logger.info(`Log cleanup task completed in ${timeElapsed.toFixed(2)}s`) - }, +export const cleanupLogsTask = task({ + id: 'cleanup-logs', + run: runCleanupLogs, }) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 3747bdfe81f..501bc1ad4d2 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -8,14 +8,19 @@ import { workflow, workflowFolder, workflowMcpServer, + workspace, workspaceFile, workspaceFiles, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' -import { and, inArray, isNotNull, lt, sql } from 'drizzle-orm' +import { and, eq, inArray, isNotNull, lt, sql } from 'drizzle-orm' import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' -import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' +import { + type CleanupJobPayload, + getRetentionDefaultHours, + resolveTierWorkspaceIds, +} from '@/lib/billing/cleanup-dispatcher' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' const logger = createLogger('CleanupSoftDeletes') @@ -125,44 +130,81 @@ const CLEANUP_TARGETS = [ { table: a2aAgent, softDeleteCol: a2aAgent.archivedAt, wsCol: a2aAgent.workspaceId, name: 'a2aAgent' }, ] as const -export const cleanupSoftDeletesTask = task({ - id: 'cleanup-soft-deletes', - run: async () => { - const startTime = Date.now() +async function resolvePayload(payload: CleanupJobPayload): Promise<{ + workspaceIds: string[] + retentionHours: number + tierLabel: string +} | null> { + if (payload.tier === 'free' || payload.tier === 'paid') { + const retentionHours = getRetentionDefaultHours(payload.tier, 'softDeleteRetentionHours') + if (retentionHours === null) { + logger.info(`[${payload.tier}] No default retention, skipping`) + return null + } + const workspaceIds = await resolveTierWorkspaceIds(payload.tier) + return { workspaceIds, retentionHours, tierLabel: payload.tier } + } - logger.info('Starting soft-delete cleanup task') + const [ws] = await db + .select({ softDeleteRetentionHours: workspace.softDeleteRetentionHours }) + .from(workspace) + .where(eq(workspace.id, payload.workspaceId)) + .limit(1) - const groups = await resolveRetentionGroups('softDeleteRetentionHours') + if (!ws?.softDeleteRetentionHours) { + logger.info(`[enterprise/${payload.workspaceId}] No retention configured, skipping`) + return null + } - for (const group of groups) { - logger.info( - `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` - ) + return { + workspaceIds: [payload.workspaceId], + retentionHours: ws.softDeleteRetentionHours, + tierLabel: `enterprise/${payload.workspaceId}`, + } +} - const fileStats = await cleanupWorkspaceFileStorage( - group.workspaceIds, - group.retentionDate - ) +export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise { + const startTime = Date.now() - let totalDeleted = 0 - for (const target of CLEANUP_TARGETS) { - const result = await cleanupTable( - target.table, - target.softDeleteCol, - target.wsCol, - group.workspaceIds, - group.retentionDate, - `${group.tierLabel}/${target.name}` - ) - totalDeleted += result.deleted - } + const resolved = await resolvePayload(payload) + if (!resolved) return - logger.info( - `[${group.tierLabel}] Complete: ${totalDeleted} rows deleted, ${fileStats.filesDeleted} files cleaned` - ) - } + const { workspaceIds, retentionHours, tierLabel } = resolved + + if (workspaceIds.length === 0) { + logger.info(`[${tierLabel}] No workspaces to process`) + return + } + + const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) + logger.info( + `[${tierLabel}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` + ) + + const fileStats = await cleanupWorkspaceFileStorage(workspaceIds, retentionDate) + + let totalDeleted = 0 + for (const target of CLEANUP_TARGETS) { + const result = await cleanupTable( + target.table, + target.softDeleteCol, + target.wsCol, + workspaceIds, + retentionDate, + `${tierLabel}/${target.name}` + ) + totalDeleted += result.deleted + } + + logger.info( + `[${tierLabel}] Complete: ${totalDeleted} rows deleted, ${fileStats.filesDeleted} files cleaned` + ) + + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`[${tierLabel}] Job completed in ${timeElapsed.toFixed(2)}s`) +} - const timeElapsed = (Date.now() - startTime) / 1000 - logger.info(`Soft-delete cleanup task completed in ${timeElapsed.toFixed(2)}s`) - }, +export const cleanupSoftDeletesTask = task({ + id: 'cleanup-soft-deletes', + run: runCleanupSoftDeletes, }) diff --git a/apps/sim/background/cleanup-tasks.ts b/apps/sim/background/cleanup-tasks.ts index cbe2eee491c..9fb251b174a 100644 --- a/apps/sim/background/cleanup-tasks.ts +++ b/apps/sim/background/cleanup-tasks.ts @@ -6,12 +6,19 @@ import { copilotRunCheckpoints, copilotRuns, mothershipInboxTask, + workspace, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' -import { and, inArray, lt, sql } from 'drizzle-orm' +import { and, eq, inArray, lt, sql } from 'drizzle-orm' import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' -import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' +import { + type CleanupJobPayload, + getRetentionDefaultHours, + resolveTierWorkspaceIds, +} from '@/lib/billing/cleanup-dispatcher' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' +import { env } from '@/lib/core/config/env' const logger = createLogger('CleanupTasks') @@ -132,103 +139,216 @@ async function cleanupRunChildren( return results } -export const cleanupTasksTask = task({ - id: 'cleanup-tasks', - run: async () => { - const startTime = Date.now() +const COPILOT_CLEANUP_BATCH_SIZE = 1000 + +/** + * Call the copilot backend to delete chat data (memory_files, checkpoints, task_chains, etc.) + * before we delete the Sim DB rows. Chunked at 1000 per request. + */ +async function cleanupCopilotBackend( + chatIds: string[], + tierLabel: string +): Promise<{ deleted: number; failed: number }> { + const stats = { deleted: 0, failed: 0 } - logger.info('Starting task cleanup') + if (chatIds.length === 0 || !env.COPILOT_API_KEY) { + if (!env.COPILOT_API_KEY) { + logger.warn(`[${tierLabel}] COPILOT_API_KEY not set, skipping copilot backend cleanup`) + } + return stats + } - const groups = await resolveRetentionGroups('taskCleanupHours') + for (let i = 0; i < chatIds.length; i += COPILOT_CLEANUP_BATCH_SIZE) { + const chunk = chatIds.slice(i, i + COPILOT_CLEANUP_BATCH_SIZE) + try { + const response = await fetch(`${SIM_AGENT_API_URL}/api/tasks/cleanup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': env.COPILOT_API_KEY, + }, + body: JSON.stringify({ chatIds: chunk }), + }) - for (const group of groups) { + if (!response.ok) { + const errorBody = await response.text().catch(() => '') + logger.error(`[${tierLabel}] Copilot backend cleanup failed: ${response.status}`, { + errorBody, + chatCount: chunk.length, + }) + stats.failed += chunk.length + continue + } + + const result = await response.json() + stats.deleted += result.deleted ?? 0 logger.info( - `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` + `[${tierLabel}] Copilot backend cleanup: ${result.deleted} chats deleted (batch ${Math.floor(i / COPILOT_CLEANUP_BATCH_SIZE) + 1})` ) + } catch (error) { + stats.failed += chunk.length + logger.error(`[${tierLabel}] Copilot backend cleanup request failed:`, { error }) + } + } + + return stats +} + +async function resolvePayload(payload: CleanupJobPayload): Promise<{ + workspaceIds: string[] + retentionHours: number + tierLabel: string +} | null> { + if (payload.tier === 'free' || payload.tier === 'paid') { + const retentionHours = getRetentionDefaultHours(payload.tier, 'taskCleanupHours') + if (retentionHours === null) { + logger.info(`[${payload.tier}] No default retention, skipping`) + return null + } + const workspaceIds = await resolveTierWorkspaceIds(payload.tier) + return { workspaceIds, retentionHours, tierLabel: payload.tier } + } + + const [ws] = await db + .select({ taskCleanupHours: workspace.taskCleanupHours }) + .from(workspace) + .where(eq(workspace.id, payload.workspaceId)) + .limit(1) + + if (!ws?.taskCleanupHours) { + logger.info(`[enterprise/${payload.workspaceId}] No retention configured, skipping`) + return null + } + + return { + workspaceIds: [payload.workspaceId], + retentionHours: ws.taskCleanupHours, + tierLabel: `enterprise/${payload.workspaceId}`, + } +} - // Delete run children first (checkpoints, tool calls) since they reference runs - const runChildResults = await cleanupRunChildren( - group.workspaceIds, - group.retentionDate, - group.tierLabel +export async function runCleanupTasks(payload: CleanupJobPayload): Promise { + const startTime = Date.now() + + const resolved = await resolvePayload(payload) + if (!resolved) return + + const { workspaceIds, retentionHours, tierLabel } = resolved + + if (workspaceIds.length === 0) { + logger.info(`[${tierLabel}] No workspaces to process`) + return + } + + const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) + logger.info( + `[${tierLabel}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` + ) + + // Collect chat IDs before deleting so we can clean up the copilot backend after + const doomedChats = await db + .select({ id: copilotChats.id }) + .from(copilotChats) + .where( + and( + inArray(copilotChats.workspaceId, workspaceIds), + lt(copilotChats.createdAt, retentionDate) + ) ) - for (const r of runChildResults) { - if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) - } + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) - // Delete feedback — no direct workspaceId, find via copilotChats - const feedbackResult: TableCleanupResult = { - table: `${group.tierLabel}/copilotFeedback`, - deleted: 0, - failed: 0, - } - try { - const chatIds = await db - .select({ id: copilotChats.id }) - .from(copilotChats) - .where( - and( - inArray(copilotChats.workspaceId, group.workspaceIds), - lt(copilotChats.createdAt, group.retentionDate) - ) + const doomedChatIds = doomedChats.map((c) => c.id) + + // Delete run children first (checkpoints, tool calls) since they reference runs + const runChildResults = await cleanupRunChildren(workspaceIds, retentionDate, tierLabel) + for (const r of runChildResults) { + if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) + } + + // Delete feedback — no direct workspaceId, find via copilotChats + const feedbackResult: TableCleanupResult = { + table: `${tierLabel}/copilotFeedback`, + deleted: 0, + failed: 0, + } + try { + const chatIds = await db + .select({ id: copilotChats.id }) + .from(copilotChats) + .where( + and( + inArray(copilotChats.workspaceId, workspaceIds), + lt(copilotChats.createdAt, retentionDate) ) - .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) - - if (chatIds.length > 0) { - const deleted = await db - .delete(copilotFeedback) - .where(inArray(copilotFeedback.chatId, chatIds.map((c) => c.id))) - .returning({ id: sql`id` }) - feedbackResult.deleted = deleted.length - logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) - } else { - logger.info(`[${feedbackResult.table}] No expired rows found`) - } - } catch (error) { - feedbackResult.failed++ - logger.error(`[${feedbackResult.table}] Delete failed:`, { error }) + ) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + if (chatIds.length > 0) { + const deleted = await db + .delete(copilotFeedback) + .where(inArray(copilotFeedback.chatId, chatIds.map((c) => c.id))) + .returning({ id: sql`id` }) + feedbackResult.deleted = deleted.length + logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) + } else { + logger.info(`[${feedbackResult.table}] No expired rows found`) } + } catch (error) { + feedbackResult.failed++ + logger.error(`[${feedbackResult.table}] Delete failed:`, { error }) + } - // Delete copilot runs (has workspaceId directly, cascades checkpoints) - const runsResult = await cleanupTable( - copilotRuns, - copilotRuns.workspaceId, - copilotRuns.createdAt, - group.workspaceIds, - group.retentionDate, - `${group.tierLabel}/copilotRuns` - ) + // Delete copilot runs (has workspaceId directly, cascades checkpoints) + const runsResult = await cleanupTable( + copilotRuns, + copilotRuns.workspaceId, + copilotRuns.createdAt, + workspaceIds, + retentionDate, + `${tierLabel}/copilotRuns` + ) - // Delete copilot chats (has workspaceId directly) - const chatsResult = await cleanupTable( - copilotChats, - copilotChats.workspaceId, - copilotChats.createdAt, - group.workspaceIds, - group.retentionDate, - `${group.tierLabel}/copilotChats` - ) + // Delete copilot chats (has workspaceId directly) + const chatsResult = await cleanupTable( + copilotChats, + copilotChats.workspaceId, + copilotChats.createdAt, + workspaceIds, + retentionDate, + `${tierLabel}/copilotChats` + ) - // Delete mothership inbox tasks (has workspaceId directly) - const inboxResult = await cleanupTable( - mothershipInboxTask, - mothershipInboxTask.workspaceId, - mothershipInboxTask.createdAt, - group.workspaceIds, - group.retentionDate, - `${group.tierLabel}/mothershipInboxTask` - ) + // Delete mothership inbox tasks (has workspaceId directly) + const inboxResult = await cleanupTable( + mothershipInboxTask, + mothershipInboxTask.workspaceId, + mothershipInboxTask.createdAt, + workspaceIds, + retentionDate, + `${tierLabel}/mothershipInboxTask` + ) - const totalDeleted = - runChildResults.reduce((s, r) => s + r.deleted, 0) + - runsResult.deleted + - chatsResult.deleted + - inboxResult.deleted + const totalDeleted = + runChildResults.reduce((s, r) => s + r.deleted, 0) + + runsResult.deleted + + chatsResult.deleted + + inboxResult.deleted - logger.info(`[${group.tierLabel}] Complete: ${totalDeleted} total rows deleted`) + logger.info(`[${tierLabel}] Complete: ${totalDeleted} total rows deleted`) + + // Clean up copilot backend after Sim DB rows are gone (chat no longer accessible) + if (doomedChatIds.length > 0) { + const copilotResult = await cleanupCopilotBackend(doomedChatIds, tierLabel) + logger.info( + `[${tierLabel}] Copilot backend: ${copilotResult.deleted} deleted, ${copilotResult.failed} failed` + ) } - const timeElapsed = (Date.now() - startTime) / 1000 - logger.info(`Task cleanup completed in ${timeElapsed.toFixed(2)}s`) - }, + const timeElapsed = (Date.now() - startTime) / 1000 + logger.info(`Task cleanup completed in ${timeElapsed.toFixed(2)}s`) +} + +export const cleanupTasksTask = task({ + id: 'cleanup-tasks', + run: runCleanupTasks, }) diff --git a/apps/sim/background/redact-task-context.ts b/apps/sim/background/redact-task-context.ts deleted file mode 100644 index 0605b1940b1..00000000000 --- a/apps/sim/background/redact-task-context.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { db } from '@sim/db' -import { - copilotAsyncToolCalls, - copilotChats, - copilotFeedback, - copilotRunCheckpoints, - copilotRuns, - mothershipInboxTask, -} from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { task } from '@trigger.dev/sdk' -import { and, eq, inArray, isNull, lt } from 'drizzle-orm' -import { - SUPPORTED_PII_ENTITIES, - validatePII, -} from '@/lib/guardrails/validate_pii' -import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers' - -const logger = createLogger('RedactTaskContext') - -const BATCH_SIZE = 100 -const ALL_ENTITY_TYPES = Object.keys(SUPPORTED_PII_ENTITIES) - -async function maskText(text: string | null, requestId: string): Promise { - if (!text || text.length === 0) return null - - const result = await validatePII({ - text, - entityTypes: ALL_ENTITY_TYPES, - mode: 'mask', - language: 'en', - requestId, - }) - - if (result.maskedText && result.detectedEntities.length > 0) { - return result.maskedText - } - return null -} - -async function maskJsonb( - value: unknown, - requestId: string -): Promise<{ masked: unknown; changed: boolean }> { - if (typeof value === 'string') { - const masked = await maskText(value, requestId) - if (masked !== null) return { masked, changed: true } - return { masked: value, changed: false } - } - - if (Array.isArray(value)) { - let anyChanged = false - const result = [] - for (let i = 0; i < value.length; i++) { - const { masked, changed } = await maskJsonb(value[i], `${requestId}-${i}`) - result.push(masked) - if (changed) anyChanged = true - } - return { masked: result, changed: anyChanged } - } - - if (value && typeof value === 'object') { - let anyChanged = false - const result: Record = {} - for (const [key, val] of Object.entries(value)) { - const { masked, changed } = await maskJsonb(val, `${requestId}-${key}`) - result[key] = masked - if (changed) anyChanged = true - } - return { masked: result, changed: anyChanged } - } - - return { masked: value, changed: false } -} - -interface RedactionStats { - table: string - processed: number - redacted: number - failed: number -} - -function emptyStats(table: string): RedactionStats { - return { table, processed: 0, redacted: 0, failed: 0 } -} - -async function redactCopilotChats( - workspaceIds: string[], - retentionDate: Date -): Promise { - const stats = emptyStats('copilotChats') - if (workspaceIds.length === 0) return stats - - const rows = await db - .select({ id: copilotChats.id, messages: copilotChats.messages }) - .from(copilotChats) - .where( - and( - inArray(copilotChats.workspaceId, workspaceIds), - isNull(copilotChats.redactedAt), - lt(copilotChats.createdAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - for (const row of rows) { - stats.processed++ - try { - const { masked, changed } = await maskJsonb(row.messages, `chat-${row.id}`) - await db - .update(copilotChats) - .set({ messages: changed ? masked : row.messages, redactedAt: new Date() }) - .where(eq(copilotChats.id, row.id)) - if (changed) stats.redacted++ - } catch (error) { - stats.failed++ - logger.error(`Failed to redact copilotChat ${row.id}:`, { error }) - } - } - - return stats -} - -async function redactMothershipInboxTasks( - workspaceIds: string[], - retentionDate: Date -): Promise { - const stats = emptyStats('mothershipInboxTask') - if (workspaceIds.length === 0) return stats - - const rows = await db - .select({ - id: mothershipInboxTask.id, - fromEmail: mothershipInboxTask.fromEmail, - fromName: mothershipInboxTask.fromName, - subject: mothershipInboxTask.subject, - bodyPreview: mothershipInboxTask.bodyPreview, - bodyText: mothershipInboxTask.bodyText, - bodyHtml: mothershipInboxTask.bodyHtml, - ccRecipients: mothershipInboxTask.ccRecipients, - }) - .from(mothershipInboxTask) - .where( - and( - inArray(mothershipInboxTask.workspaceId, workspaceIds), - isNull(mothershipInboxTask.redactedAt), - lt(mothershipInboxTask.createdAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - for (const row of rows) { - stats.processed++ - try { - const rid = `inbox-${row.id}` - const [mFromEmail, mFromName, mSubject, mBodyPreview, mBodyText, mBodyHtml, mCc] = - await Promise.all([ - maskText(row.fromEmail, `${rid}-fromEmail`), - maskText(row.fromName, `${rid}-fromName`), - maskText(row.subject, `${rid}-subject`), - maskText(row.bodyPreview, `${rid}-bodyPreview`), - maskText(row.bodyText, `${rid}-bodyText`), - maskText(row.bodyHtml, `${rid}-bodyHtml`), - maskText(row.ccRecipients, `${rid}-ccRecipients`), - ]) - - const hasChanges = - mFromEmail !== null || - mFromName !== null || - mSubject !== null || - mBodyPreview !== null || - mBodyText !== null || - mBodyHtml !== null || - mCc !== null - - const updateData: Record = { redactedAt: new Date() } - if (mFromEmail !== null) updateData.fromEmail = mFromEmail - if (mFromName !== null) updateData.fromName = mFromName - if (mSubject !== null) updateData.subject = mSubject - if (mBodyPreview !== null) updateData.bodyPreview = mBodyPreview - if (mBodyText !== null) updateData.bodyText = mBodyText - if (mBodyHtml !== null) updateData.bodyHtml = mBodyHtml - if (mCc !== null) updateData.ccRecipients = mCc - - await db - .update(mothershipInboxTask) - .set(updateData) - .where(eq(mothershipInboxTask.id, row.id)) - - if (hasChanges) stats.redacted++ - } catch (error) { - stats.failed++ - logger.error(`Failed to redact mothershipInboxTask ${row.id}:`, { error }) - } - } - - return stats -} - -async function redactCopilotFeedback( - workspaceIds: string[], - retentionDate: Date -): Promise { - const stats = emptyStats('copilotFeedback') - if (workspaceIds.length === 0) return stats - - const rows = await db - .select({ - id: copilotFeedback.feedbackId, - userQuery: copilotFeedback.userQuery, - agentResponse: copilotFeedback.agentResponse, - feedback: copilotFeedback.feedback, - }) - .from(copilotFeedback) - .innerJoin(copilotChats, eq(copilotFeedback.chatId, copilotChats.id)) - .where( - and( - inArray(copilotChats.workspaceId, workspaceIds), - isNull(copilotFeedback.redactedAt), - lt(copilotFeedback.createdAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - for (const row of rows) { - stats.processed++ - try { - const rid = `feedback-${row.id}` - const [mQuery, mResponse, mFeedback] = await Promise.all([ - maskText(row.userQuery, `${rid}-query`), - maskText(row.agentResponse, `${rid}-response`), - maskText(row.feedback, `${rid}-feedback`), - ]) - - const hasChanges = mQuery !== null || mResponse !== null || mFeedback !== null - const updateData: Record = { redactedAt: new Date() } - if (mQuery !== null) updateData.userQuery = mQuery - if (mResponse !== null) updateData.agentResponse = mResponse - if (mFeedback !== null) updateData.feedback = mFeedback - - await db - .update(copilotFeedback) - .set(updateData) - .where(eq(copilotFeedback.feedbackId, row.id)) - - if (hasChanges) stats.redacted++ - } catch (error) { - stats.failed++ - logger.error(`Failed to redact copilotFeedback ${row.id}:`, { error }) - } - } - - return stats -} - -async function redactCopilotRunCheckpoints( - workspaceIds: string[], - retentionDate: Date -): Promise { - const stats = emptyStats('copilotRunCheckpoints') - if (workspaceIds.length === 0) return stats - - const rows = await db - .select({ - id: copilotRunCheckpoints.id, - conversationSnapshot: copilotRunCheckpoints.conversationSnapshot, - }) - .from(copilotRunCheckpoints) - .innerJoin(copilotRuns, eq(copilotRunCheckpoints.runId, copilotRuns.id)) - .where( - and( - inArray(copilotRuns.workspaceId, workspaceIds), - isNull(copilotRunCheckpoints.redactedAt), - lt(copilotRunCheckpoints.createdAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - for (const row of rows) { - stats.processed++ - try { - const { masked, changed } = await maskJsonb(row.conversationSnapshot, `checkpoint-${row.id}`) - await db - .update(copilotRunCheckpoints) - .set({ - conversationSnapshot: changed ? masked : row.conversationSnapshot, - redactedAt: new Date(), - }) - .where(eq(copilotRunCheckpoints.id, row.id)) - if (changed) stats.redacted++ - } catch (error) { - stats.failed++ - logger.error(`Failed to redact copilotRunCheckpoint ${row.id}:`, { error }) - } - } - - return stats -} - -async function redactCopilotAsyncToolCalls( - workspaceIds: string[], - retentionDate: Date -): Promise { - const stats = emptyStats('copilotAsyncToolCalls') - if (workspaceIds.length === 0) return stats - - const rows = await db - .select({ - id: copilotAsyncToolCalls.id, - args: copilotAsyncToolCalls.args, - result: copilotAsyncToolCalls.result, - }) - .from(copilotAsyncToolCalls) - .innerJoin(copilotRuns, eq(copilotAsyncToolCalls.runId, copilotRuns.id)) - .where( - and( - inArray(copilotRuns.workspaceId, workspaceIds), - isNull(copilotAsyncToolCalls.redactedAt), - lt(copilotAsyncToolCalls.createdAt, retentionDate) - ) - ) - .limit(BATCH_SIZE) - - for (const row of rows) { - stats.processed++ - try { - const rid = `toolcall-${row.id}` - const [argsResult, resultResult] = await Promise.all([ - row.args ? maskJsonb(row.args, `${rid}-args`) : { masked: row.args, changed: false }, - row.result ? maskJsonb(row.result, `${rid}-result`) : { masked: row.result, changed: false }, - ]) - - const updateData: Record = { redactedAt: new Date() } - if (argsResult.changed) updateData.args = argsResult.masked - if (resultResult.changed) updateData.result = resultResult.masked - - await db - .update(copilotAsyncToolCalls) - .set(updateData) - .where(eq(copilotAsyncToolCalls.id, row.id)) - - if (argsResult.changed || resultResult.changed) stats.redacted++ - } catch (error) { - stats.failed++ - logger.error(`Failed to redact copilotAsyncToolCall ${row.id}:`, { error }) - } - } - - return stats -} - -const REDACTION_FUNCTIONS = [ - redactCopilotChats, - redactMothershipInboxTasks, - redactCopilotFeedback, - redactCopilotRunCheckpoints, - redactCopilotAsyncToolCalls, -] as const - -export const redactTaskContextTask = task({ - id: 'redact-task-context', - run: async () => { - const startTime = Date.now() - - logger.info('Starting task context redaction') - - const groups = await resolveRetentionGroups('taskRedactionHours') - - for (const group of groups) { - logger.info( - `[${group.tierLabel}] Processing ${group.workspaceIds.length} workspaces` - ) - - for (const redactFn of REDACTION_FUNCTIONS) { - const stats = await redactFn(group.workspaceIds, group.retentionDate) - if (stats.processed > 0) { - logger.info( - `[${group.tierLabel}/${stats.table}] Processed ${stats.processed}, redacted ${stats.redacted}, failed ${stats.failed}` - ) - } else { - logger.info(`[${group.tierLabel}/${stats.table}] No rows to process`) - } - } - } - - const timeElapsed = (Date.now() - startTime) / 1000 - logger.info(`Task context redaction completed in ${timeElapsed.toFixed(2)}s`) - }, -}) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index d2a85ffead1..059bdad1ba4 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -115,13 +115,6 @@ function LockedView({ data }: { data: DataRetentionResponse }) { onChange={() => {}} disabled /> - {}} - disabled - /> @@ -197,14 +186,7 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work onChange={setSoftDeleteDays} disabled={false} /> - - = { 'cleanup-logs': 'cleanup-logs', 'cleanup-soft-deletes': 'cleanup-soft-deletes', 'cleanup-tasks': 'cleanup-tasks', - 'redact-task-context': 'redact-task-context', } /** diff --git a/apps/sim/lib/core/async-jobs/types.ts b/apps/sim/lib/core/async-jobs/types.ts index 89670adc002..06a3e21086b 100644 --- a/apps/sim/lib/core/async-jobs/types.ts +++ b/apps/sim/lib/core/async-jobs/types.ts @@ -28,7 +28,6 @@ export type JobType = | 'cleanup-logs' | 'cleanup-soft-deletes' | 'cleanup-tasks' - | 'redact-task-context' export type AsyncExecutionCorrelationSource = 'workflow' | 'schedule' | 'webhook' diff --git a/apps/sim/lib/core/bullmq/queues.ts b/apps/sim/lib/core/bullmq/queues.ts index d4ea4b3c9a1..f8950d7f838 100644 --- a/apps/sim/lib/core/bullmq/queues.ts +++ b/apps/sim/lib/core/bullmq/queues.ts @@ -24,7 +24,6 @@ let workspaceNotificationDeliveryQueueInstance: Queue | null = null let cleanupLogsQueueInstance: Queue | null = null let cleanupSoftDeletesQueueInstance: Queue | null = null let cleanupTasksQueueInstance: Queue | null = null -let redactTaskContextQueueInstance: Queue | null = null let workflowQueueEventsInstance: QueueEvents | null = null function getQueueDefaultOptions(type: JobType) { @@ -59,7 +58,6 @@ function getQueueDefaultOptions(type: JobType) { case 'cleanup-logs': case 'cleanup-soft-deletes': case 'cleanup-tasks': - case 'redact-task-context': return { attempts: 1, removeOnComplete: { age: 24 * 60 * 60 }, @@ -161,11 +159,6 @@ export function getBullMQQueue(type: JobType): Queue { cleanupTasksQueueInstance = createQueue(type) } return cleanupTasksQueueInstance - case 'redact-task-context': - if (!redactTaskContextQueueInstance) { - redactTaskContextQueueInstance = createQueue(type) - } - return redactTaskContextQueueInstance } } @@ -178,7 +171,6 @@ export function getBullMQQueueByName(queueName: WorkspaceDispatchQueueName): Que case 'cleanup-logs': case 'cleanup-soft-deletes': case 'cleanup-tasks': - case 'redact-task-context': return getBullMQQueue(queueName) case KNOWLEDGE_CONNECTOR_SYNC_QUEUE: return getKnowledgeConnectorSyncQueue() diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index af9ba0f920f..7d8de4a78d1 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -187,9 +187,6 @@ export const env = createEnv({ AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME: z.string().optional(), // Azure container for OpenGraph images AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME: z.string().optional(), // Azure container for workspace logos - // Data Retention - FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users - PAID_PLAN_LOG_RETENTION_DAYS: z.string().optional().default('30'), // Log retention days for paid (pro/team) plan users // Admission & Burst Protection ADMISSION_GATE_MAX_INFLIGHT: z.string().optional().default('500'), // Max concurrent in-flight execution requests per pod diff --git a/apps/sim/lib/retention/workspace-tiers.ts b/apps/sim/lib/retention/workspace-tiers.ts deleted file mode 100644 index 121fb948629..00000000000 --- a/apps/sim/lib/retention/workspace-tiers.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { db } from '@sim/db' -import { subscription, workspace } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' -import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' -import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' -import { env } from '@/lib/core/config/env' - -const logger = createLogger('WorkspaceTiers') - -export interface RetentionGroup { - workspaceIds: string[] - retentionDate: Date - tierLabel: string -} - -type RetentionColumn = - | 'logRetentionHours' - | 'softDeleteRetentionHours' - | 'taskRedactionHours' - | 'taskCleanupHours' - -/** - * Resolve all workspaces into retention groups based on their plan tier - * and any enterprise-configured retention overrides. - * - * Returns groups with computed retention dates, ready for batch processing. - * Enterprise workspaces with NULL retention are excluded (no cleanup). - */ -export async function resolveRetentionGroups( - retentionColumn: RetentionColumn -): Promise { - const freeRetentionDays = Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7') - const paidRetentionDays = Number(env.PAID_PLAN_LOG_RETENTION_DAYS || '30') - - const now = Date.now() - const freeRetentionDate = new Date(now - freeRetentionDays * 24 * 60 * 60 * 1000) - const paidRetentionDate = new Date(now - paidRetentionDays * 24 * 60 * 60 * 1000) - - logger.info(`Resolving retention groups for ${retentionColumn}`, { - freeRetentionDays, - paidRetentionDays, - freeRetentionDate: freeRetentionDate.toISOString(), - paidRetentionDate: paidRetentionDate.toISOString(), - }) - - const groups: RetentionGroup[] = [] - - // --- Free workspaces (no paid subscription) --- - - const freeWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .leftJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - sqlIsPaid(subscription.plan) - ) - ) - .where(and(isNull(subscription.id), isNull(workspace.archivedAt))) - - const freeIds = freeWorkspaceRows.map((w) => w.id) - if (freeIds.length > 0) { - groups.push({ - workspaceIds: freeIds, - retentionDate: freeRetentionDate, - tierLabel: 'free', - }) - } - logger.info(`[free] Found ${freeIds.length} workspaces, retention cutoff: ${freeRetentionDate.toISOString()}`) - - // --- Pro/Team workspaces (paid non-enterprise) --- - - const paidWorkspaceRows = await db - .select({ id: workspace.id }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - or(sqlIsPro(subscription.plan)!, sqlIsTeam(subscription.plan)!) - ) - ) - .where(isNull(workspace.archivedAt)) - - const paidIds = paidWorkspaceRows.map((w) => w.id) - if (paidIds.length > 0) { - groups.push({ - workspaceIds: paidIds, - retentionDate: paidRetentionDate, - tierLabel: 'paid', - }) - } - logger.info(`[paid] Found ${paidIds.length} workspaces, retention cutoff: ${paidRetentionDate.toISOString()}`) - - // --- Enterprise with custom retention --- - // Enterprise with NULL retention column = skip (no cleanup) - - const retentionCol = workspace[retentionColumn] - - const enterpriseWorkspaceRows = await db - .select({ - id: workspace.id, - retentionHours: retentionCol, - }) - .from(workspace) - .innerJoin( - subscription, - and( - eq(subscription.referenceId, workspace.billedAccountUserId), - inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES), - eq(subscription.plan, 'enterprise') - ) - ) - .where(and(isNull(workspace.archivedAt), isNotNull(retentionCol))) - - // Group by retention hours to batch workspaces with same retention - const enterpriseGroups = new Map() - for (const ws of enterpriseWorkspaceRows) { - const hours = ws.retentionHours as number - const group = enterpriseGroups.get(hours) ?? [] - group.push(ws.id) - enterpriseGroups.set(hours, group) - } - - logger.info( - `[enterprise] Found ${enterpriseWorkspaceRows.length} workspaces with custom retention (${enterpriseGroups.size} distinct periods). Workspaces with NULL are skipped.` - ) - - for (const [hours, ids] of enterpriseGroups) { - const retentionDate = new Date(now - hours * 60 * 60 * 1000) - groups.push({ - workspaceIds: ids, - retentionDate, - tierLabel: `enterprise-${hours}h`, - }) - logger.info( - `[enterprise-${hours}h] ${ids.length} workspaces, retention cutoff: ${retentionDate.toISOString()}` - ) - } - - return groups -} diff --git a/packages/db/migrations/0190_lean_terror.sql b/packages/db/migrations/0190_lean_terror.sql deleted file mode 100644 index 9bd109d5ce8..00000000000 --- a/packages/db/migrations/0190_lean_terror.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "copilot_async_tool_calls" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint -ALTER TABLE "copilot_chats" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint -ALTER TABLE "copilot_feedback" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint -ALTER TABLE "copilot_run_checkpoints" ADD COLUMN "redacted_at" timestamp;--> statement-breakpoint -ALTER TABLE "mothership_inbox_task" ADD COLUMN "redacted_at" timestamp; \ No newline at end of file diff --git a/packages/db/migrations/0191_parched_living_mummy.sql b/packages/db/migrations/0191_parched_living_mummy.sql deleted file mode 100644 index 793dacdb215..00000000000 --- a/packages/db/migrations/0191_parched_living_mummy.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "workspace" ADD COLUMN "task_cleanup_hours" integer; \ No newline at end of file diff --git a/packages/db/migrations/meta/0190_snapshot.json b/packages/db/migrations/meta/0190_snapshot.json deleted file mode 100644 index 409229b1427..00000000000 --- a/packages/db/migrations/meta/0190_snapshot.json +++ /dev/null @@ -1,14658 +0,0 @@ -{ - "id": "bb103638-c742-4300-a0fe-757734e707f1", - "prevId": "a7cc3b52-2dff-42e0-8196-f87e97f732bb", - "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": {} - } - }, - "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 - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'personal'" - }, - "last_used": { - "name": "last_used", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "api_key_workspace_type_idx": { - "name": "api_key_workspace_type_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "api_key_user_type_idx": { - "name": "api_key_user_type_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "api_key_user_id_user_id_fk": { - "name": "api_key_user_id_user_id_fk", - "tableFrom": "api_key", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "api_key_workspace_id_workspace_id_fk": { - "name": "api_key_workspace_id_workspace_id_fk", - "tableFrom": "api_key", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "api_key_created_by_user_id_fk": { - "name": "api_key_created_by_user_id_fk", - "tableFrom": "api_key", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "api_key_key_unique": { - "name": "api_key_key_unique", - "nullsNotDistinct": false, - "columns": ["key"] - } - }, - "policies": {}, - "checkConstraints": { - "workspace_type_check": { - "name": "workspace_type_check", - "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.async_jobs": { - "name": "async_jobs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "payload": { - "name": "payload", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "run_at": { - "name": "run_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "attempts": { - "name": "attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "max_attempts": { - "name": "max_attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 3 - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "output": { - "name": "output", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "async_jobs_status_started_at_idx": { - "name": "async_jobs_status_started_at_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "async_jobs_status_completed_at_idx": { - "name": "async_jobs_status_completed_at_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "completed_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.audit_log": { - "name": "audit_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_name": { - "name": "actor_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_email": { - "name": "actor_email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "resource_name": { - "name": "resource_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "audit_log_workspace_created_idx": { - "name": "audit_log_workspace_created_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_actor_created_idx": { - "name": "audit_log_actor_created_idx", - "columns": [ - { - "expression": "actor_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_resource_idx": { - "name": "audit_log_resource_idx", - "columns": [ - { - "expression": "resource_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "resource_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_action_idx": { - "name": "audit_log_action_idx", - "columns": [ - { - "expression": "action", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "audit_log_workspace_id_workspace_id_fk": { - "name": "audit_log_workspace_id_workspace_id_fk", - "tableFrom": "audit_log", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "audit_log_actor_id_user_id_fk": { - "name": "audit_log_actor_id_user_id_fk", - "tableFrom": "audit_log", - "tableTo": "user", - "columnsFrom": ["actor_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat": { - "name": "chat", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "customizations": { - "name": "customizations", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "auth_type": { - "name": "auth_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'public'" - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "allowed_emails": { - "name": "allowed_emails", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "output_configs": { - "name": "output_configs", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "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_idx": { - "name": "chat_archived_at_idx", - "columns": [ - { - "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 - }, - "messages": { - "name": "messages", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "model": { - "name": "model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'claude-3-7-sonnet-latest'" - }, - "conversation_id": { - "name": "conversation_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "preview_yaml": { - "name": "preview_yaml", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "plan_artifact": { - "name": "plan_artifact", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "resources": { - "name": "resources", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "last_seen_at": { - "name": "last_seen_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_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": {} - } - }, - "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_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_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.docs_embeddings": { - "name": "docs_embeddings", - "schema": "", - "columns": { - "chunk_id": { - "name": "chunk_id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "chunk_text": { - "name": "chunk_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_document": { - "name": "source_document", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_link": { - "name": "source_link", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "header_text": { - "name": "header_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "header_level": { - "name": "header_level", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token_count": { - "name": "token_count", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "embedding": { - "name": "embedding", - "type": "vector(1536)", - "primaryKey": false, - "notNull": true - }, - "embedding_model": { - "name": "embedding_model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text-embedding-3-small'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "chunk_text_tsv": { - "name": "chunk_text_tsv", - "type": "tsvector", - "primaryKey": false, - "notNull": false, - "generated": { - "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", - "type": "stored" - } - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "docs_emb_source_document_idx": { - "name": "docs_emb_source_document_idx", - "columns": [ - { - "expression": "source_document", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_header_level_idx": { - "name": "docs_emb_header_level_idx", - "columns": [ - { - "expression": "header_level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_source_header_idx": { - "name": "docs_emb_source_header_idx", - "columns": [ - { - "expression": "source_document", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "header_level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_model_idx": { - "name": "docs_emb_model_idx", - "columns": [ - { - "expression": "embedding_model", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_created_at_idx": { - "name": "docs_emb_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_embedding_vector_hnsw_idx": { - "name": "docs_embedding_vector_hnsw_idx", - "columns": [ - { - "expression": "embedding", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "vector_cosine_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "hnsw", - "with": { - "m": 16, - "ef_construction": 64 - } - }, - "docs_emb_metadata_gin_idx": { - "name": "docs_emb_metadata_gin_idx", - "columns": [ - { - "expression": "metadata", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "docs_emb_chunk_text_fts_idx": { - "name": "docs_emb_chunk_text_fts_idx", - "columns": [ - { - "expression": "chunk_text_tsv", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "docs_embedding_not_null_check": { - "name": "docs_embedding_not_null_check", - "value": "\"embedding\" IS NOT NULL" - }, - "docs_header_level_check": { - "name": "docs_header_level_check", - "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" - } - }, - "isRLSEnabled": false - }, - "public.document": { - "name": "document", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "knowledge_base_id": { - "name": "knowledge_base_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "filename": { - "name": "filename", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "file_url": { - "name": "file_url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "file_size": { - "name": "file_size", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "mime_type": { - "name": "mime_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chunk_count": { - "name": "chunk_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "token_count": { - "name": "token_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "character_count": { - "name": "character_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "processing_status": { - "name": "processing_status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "processing_started_at": { - "name": "processing_started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "processing_completed_at": { - "name": "processing_completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "processing_error": { - "name": "processing_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "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_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_archived_at_idx": { - "name": "doc_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_deleted_at_idx": { - "name": "doc_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag1_idx": { - "name": "doc_tag1_idx", - "columns": [ - { - "expression": "tag1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag2_idx": { - "name": "doc_tag2_idx", - "columns": [ - { - "expression": "tag2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag3_idx": { - "name": "doc_tag3_idx", - "columns": [ - { - "expression": "tag3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag4_idx": { - "name": "doc_tag4_idx", - "columns": [ - { - "expression": "tag4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag5_idx": { - "name": "doc_tag5_idx", - "columns": [ - { - "expression": "tag5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag6_idx": { - "name": "doc_tag6_idx", - "columns": [ - { - "expression": "tag6", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag7_idx": { - "name": "doc_tag7_idx", - "columns": [ - { - "expression": "tag7", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number1_idx": { - "name": "doc_number1_idx", - "columns": [ - { - "expression": "number1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number2_idx": { - "name": "doc_number2_idx", - "columns": [ - { - "expression": "number2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number3_idx": { - "name": "doc_number3_idx", - "columns": [ - { - "expression": "number3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number4_idx": { - "name": "doc_number4_idx", - "columns": [ - { - "expression": "number4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number5_idx": { - "name": "doc_number5_idx", - "columns": [ - { - "expression": "number5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_date1_idx": { - "name": "doc_date1_idx", - "columns": [ - { - "expression": "date1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_date2_idx": { - "name": "doc_date2_idx", - "columns": [ - { - "expression": "date2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean1_idx": { - "name": "doc_boolean1_idx", - "columns": [ - { - "expression": "boolean1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean2_idx": { - "name": "doc_boolean2_idx", - "columns": [ - { - "expression": "boolean2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean3_idx": { - "name": "doc_boolean3_idx", - "columns": [ - { - "expression": "boolean3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "document_knowledge_base_id_knowledge_base_id_fk": { - "name": "document_knowledge_base_id_knowledge_base_id_fk", - "tableFrom": "document", - "tableTo": "knowledge_base", - "columnsFrom": ["knowledge_base_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "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" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.embedding": { - "name": "embedding", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "knowledge_base_id": { - "name": "knowledge_base_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "document_id": { - "name": "document_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chunk_index": { - "name": "chunk_index", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "chunk_hash": { - "name": "chunk_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content_length": { - "name": "content_length", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "token_count": { - "name": "token_count", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "embedding": { - "name": "embedding", - "type": "vector(1536)", - "primaryKey": false, - "notNull": false - }, - "embedding_model": { - "name": "embedding_model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text-embedding-3-small'" - }, - "start_offset": { - "name": "start_offset", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "end_offset": { - "name": "end_offset", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "tag1": { - "name": "tag1", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag2": { - "name": "tag2", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag3": { - "name": "tag3", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag4": { - "name": "tag4", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag5": { - "name": "tag5", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag6": { - "name": "tag6", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag7": { - "name": "tag7", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "number1": { - "name": "number1", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number2": { - "name": "number2", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number3": { - "name": "number3", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number4": { - "name": "number4", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number5": { - "name": "number5", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "date1": { - "name": "date1", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "date2": { - "name": "date2", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "boolean1": { - "name": "boolean1", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean2": { - "name": "boolean2", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean3": { - "name": "boolean3", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "content_tsv": { - "name": "content_tsv", - "type": "tsvector", - "primaryKey": false, - "notNull": false, - "generated": { - "as": "to_tsvector('english', \"embedding\".\"content\")", - "type": "stored" - } - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "emb_kb_id_idx": { - "name": "emb_kb_id_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_doc_id_idx": { - "name": "emb_doc_id_idx", - "columns": [ - { - "expression": "document_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_doc_chunk_idx": { - "name": "emb_doc_chunk_idx", - "columns": [ - { - "expression": "document_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chunk_index", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_kb_model_idx": { - "name": "emb_kb_model_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "embedding_model", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_kb_enabled_idx": { - "name": "emb_kb_enabled_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "enabled", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_doc_enabled_idx": { - "name": "emb_doc_enabled_idx", - "columns": [ - { - "expression": "document_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "enabled", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "embedding_vector_hnsw_idx": { - "name": "embedding_vector_hnsw_idx", - "columns": [ - { - "expression": "embedding", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "vector_cosine_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "hnsw", - "with": { - "m": 16, - "ef_construction": 64 - } - }, - "emb_tag1_idx": { - "name": "emb_tag1_idx", - "columns": [ - { - "expression": "tag1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag2_idx": { - "name": "emb_tag2_idx", - "columns": [ - { - "expression": "tag2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag3_idx": { - "name": "emb_tag3_idx", - "columns": [ - { - "expression": "tag3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag4_idx": { - "name": "emb_tag4_idx", - "columns": [ - { - "expression": "tag4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag5_idx": { - "name": "emb_tag5_idx", - "columns": [ - { - "expression": "tag5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag6_idx": { - "name": "emb_tag6_idx", - "columns": [ - { - "expression": "tag6", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag7_idx": { - "name": "emb_tag7_idx", - "columns": [ - { - "expression": "tag7", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number1_idx": { - "name": "emb_number1_idx", - "columns": [ - { - "expression": "number1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number2_idx": { - "name": "emb_number2_idx", - "columns": [ - { - "expression": "number2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number3_idx": { - "name": "emb_number3_idx", - "columns": [ - { - "expression": "number3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number4_idx": { - "name": "emb_number4_idx", - "columns": [ - { - "expression": "number4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number5_idx": { - "name": "emb_number5_idx", - "columns": [ - { - "expression": "number5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_date1_idx": { - "name": "emb_date1_idx", - "columns": [ - { - "expression": "date1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_date2_idx": { - "name": "emb_date2_idx", - "columns": [ - { - "expression": "date2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean1_idx": { - "name": "emb_boolean1_idx", - "columns": [ - { - "expression": "boolean1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean2_idx": { - "name": "emb_boolean2_idx", - "columns": [ - { - "expression": "boolean2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean3_idx": { - "name": "emb_boolean3_idx", - "columns": [ - { - "expression": "boolean3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_content_fts_idx": { - "name": "emb_content_fts_idx", - "columns": [ - { - "expression": "content_tsv", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - } - }, - "foreignKeys": { - "embedding_knowledge_base_id_knowledge_base_id_fk": { - "name": "embedding_knowledge_base_id_knowledge_base_id_fk", - "tableFrom": "embedding", - "tableTo": "knowledge_base", - "columnsFrom": ["knowledge_base_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "embedding_document_id_document_id_fk": { - "name": "embedding_document_id_document_id_fk", - "tableFrom": "embedding", - "tableTo": "document", - "columnsFrom": ["document_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "embedding_not_null_check": { - "name": "embedding_not_null_check", - "value": "\"embedding\" IS NOT NULL" - } - }, - "isRLSEnabled": false - }, - "public.environment": { - "name": "environment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "variables": { - "name": "variables", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "environment_user_id_user_id_fk": { - "name": "environment_user_id_user_id_fk", - "tableFrom": "environment", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "environment_user_id_unique": { - "name": "environment_user_id_unique", - "nullsNotDistinct": false, - "columns": ["user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.form": { - "name": "form", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "customizations": { - "name": "customizations", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "auth_type": { - "name": "auth_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'public'" - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "allowed_emails": { - "name": "allowed_emails", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "show_branding": { - "name": "show_branding", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "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": { - "form_identifier_idx": { - "name": "form_identifier_idx", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"form\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_workflow_id_idx": { - "name": "form_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_user_id_idx": { - "name": "form_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_archived_at_idx": { - "name": "form_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "form_workflow_id_workflow_id_fk": { - "name": "form_workflow_id_workflow_id_fk", - "tableFrom": "form", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "form_user_id_user_id_fk": { - "name": "form_user_id_user_id_fk", - "tableFrom": "form", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.idempotency_key": { - "name": "idempotency_key", - "schema": "", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "result": { - "name": "result", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idempotency_key_created_at_idx": { - "name": "idempotency_key_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "inviter_id": { - "name": "inviter_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "invitation_email_idx": { - "name": "invitation_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_organization_id_idx": { - "name": "invitation_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "invitation_inviter_id_user_id_fk": { - "name": "invitation_inviter_id_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": ["inviter_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_organization_id_organization_id_fk": { - "name": "invitation_organization_id_organization_id_fk", - "tableFrom": "invitation", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.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_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.jwks": { - "name": "jwks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "public_key": { - "name": "public_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "private_key": { - "name": "private_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.knowledge_base": { - "name": "knowledge_base", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "token_count": { - "name": "token_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "embedding_model": { - "name": "embedding_model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text-embedding-3-small'" - }, - "embedding_dimension": { - "name": "embedding_dimension", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1536 - }, - "chunking_config": { - "name": "chunking_config", - "type": "json", - "primaryKey": false, - "notNull": true, - "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "kb_user_id_idx": { - "name": "kb_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_workspace_id_idx": { - "name": "kb_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_user_workspace_idx": { - "name": "kb_user_workspace_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_deleted_at_idx": { - "name": "kb_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "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_idx": { - "name": "kc_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kc_deleted_at_idx": { - "name": "kc_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "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_servers": { - "name": "mcp_servers", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "transport": { - "name": "transport", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "headers": { - "name": "headers", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "timeout": { - "name": "timeout", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 30000 - }, - "retries": { - "name": "retries", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 3 - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_connected": { - "name": "last_connected", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "connection_status": { - "name": "connection_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'disconnected'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status_config": { - "name": "status_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "tool_count": { - "name": "tool_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "last_tools_refresh": { - "name": "last_tools_refresh", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "total_requests": { - "name": "total_requests", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "last_used": { - "name": "last_used", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "mcp_servers_workspace_enabled_idx": { - "name": "mcp_servers_workspace_enabled_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "enabled", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "mcp_servers_workspace_deleted_idx": { - "name": "mcp_servers_workspace_deleted_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "mcp_servers_workspace_id_workspace_id_fk": { - "name": "mcp_servers_workspace_id_workspace_id_fk", - "tableFrom": "mcp_servers", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "mcp_servers_created_by_user_id_fk": { - "name": "mcp_servers_created_by_user_id_fk", - "tableFrom": "mcp_servers", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "member_user_id_unique": { - "name": "member_user_id_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_organization_id_idx": { - "name": "member_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "member_user_id_user_id_fk": { - "name": "member_user_id_user_id_fk", - "tableFrom": "member", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_organization_id_organization_id_fk": { - "name": "member_organization_id_organization_id_fk", - "tableFrom": "member", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.memory": { - "name": "memory", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "memory_key_idx": { - "name": "memory_key_idx", - "columns": [ - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "memory_workspace_idx": { - "name": "memory_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "memory_workspace_key_idx": { - "name": "memory_workspace_key_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "memory_workspace_id_workspace_id_fk": { - "name": "memory_workspace_id_workspace_id_fk", - "tableFrom": "memory", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.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.oauth_access_token": { - "name": "oauth_access_token", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "oauth_access_token_access_token_idx": { - "name": "oauth_access_token_access_token_idx", - "columns": [ - { - "expression": "access_token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "oauth_access_token_refresh_token_idx": { - "name": "oauth_access_token_refresh_token_idx", - "columns": [ - { - "expression": "refresh_token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_access_token_client_id_oauth_application_client_id_fk": { - "name": "oauth_access_token_client_id_oauth_application_client_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "oauth_application", - "columnsFrom": ["client_id"], - "columnsTo": ["client_id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_access_token_user_id_user_id_fk": { - "name": "oauth_access_token_user_id_user_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_access_token_access_token_unique": { - "name": "oauth_access_token_access_token_unique", - "nullsNotDistinct": false, - "columns": ["access_token"] - }, - "oauth_access_token_refresh_token_unique": { - "name": "oauth_access_token_refresh_token_unique", - "nullsNotDistinct": false, - "columns": ["refresh_token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_application": { - "name": "oauth_application", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "client_secret": { - "name": "client_secret", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "redirect_urls": { - "name": "redirect_urls", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "disabled": { - "name": "disabled", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "oauth_application_client_id_idx": { - "name": "oauth_application_client_id_idx", - "columns": [ - { - "expression": "client_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_application_user_id_user_id_fk": { - "name": "oauth_application_user_id_user_id_fk", - "tableFrom": "oauth_application", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_application_client_id_unique": { - "name": "oauth_application_client_id_unique", - "nullsNotDistinct": false, - "columns": ["client_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_consent": { - "name": "oauth_consent", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "scopes": { - "name": "scopes", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "consent_given": { - "name": "consent_given", - "type": "boolean", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "oauth_consent_user_client_idx": { - "name": "oauth_consent_user_client_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "client_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_consent_client_id_oauth_application_client_id_fk": { - "name": "oauth_consent_client_id_oauth_application_client_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "oauth_application", - "columnsFrom": ["client_id"], - "columnsTo": ["client_id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_consent_user_id_user_id_fk": { - "name": "oauth_consent_user_id_user_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organization": { - "name": "organization", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "whitelabel_settings": { - "name": "whitelabel_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.paused_executions": { - "name": "paused_executions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_snapshot": { - "name": "execution_snapshot", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "pause_points": { - "name": "pause_points", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "total_pause_count": { - "name": "total_pause_count", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "resumed_count": { - "name": "resumed_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'paused'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "paused_at": { - "name": "paused_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "paused_executions_workflow_id_idx": { - "name": "paused_executions_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "paused_executions_status_idx": { - "name": "paused_executions_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "paused_executions_execution_id_unique": { - "name": "paused_executions_execution_id_unique", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "paused_executions_workflow_id_workflow_id_fk": { - "name": "paused_executions_workflow_id_workflow_id_fk", - "tableFrom": "paused_executions", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pending_credential_draft": { - "name": "pending_credential_draft", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "credential_id": { - "name": "credential_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "pending_draft_user_provider_ws": { - "name": "pending_draft_user_provider_ws", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "pending_credential_draft_user_id_user_id_fk": { - "name": "pending_credential_draft_user_id_user_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "pending_credential_draft_workspace_id_workspace_id_fk": { - "name": "pending_credential_draft_workspace_id_workspace_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "pending_credential_draft_credential_id_credential_id_fk": { - "name": "pending_credential_draft_credential_id_credential_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "credential", - "columnsFrom": ["credential_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permission_group": { - "name": "permission_group", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "auto_add_new_members": { - "name": "auto_add_new_members", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "permission_group_created_by_idx": { - "name": "permission_group_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_org_name_unique": { - "name": "permission_group_org_name_unique", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_org_auto_add_unique": { - "name": "permission_group_org_auto_add_unique", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "auto_add_new_members = true", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permission_group_organization_id_organization_id_fk": { - "name": "permission_group_organization_id_organization_id_fk", - "tableFrom": "permission_group", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_created_by_user_id_fk": { - "name": "permission_group_created_by_user_id_fk", - "tableFrom": "permission_group", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permission_group_member": { - "name": "permission_group_member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "permission_group_id": { - "name": "permission_group_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "assigned_by": { - "name": "assigned_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "permission_group_member_group_id_idx": { - "name": "permission_group_member_group_id_idx", - "columns": [ - { - "expression": "permission_group_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_member_user_id_unique": { - "name": "permission_group_member_user_id_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permission_group_member_permission_group_id_permission_group_id_fk": { - "name": "permission_group_member_permission_group_id_permission_group_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "permission_group", - "columnsFrom": ["permission_group_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_member_user_id_user_id_fk": { - "name": "permission_group_member_user_id_user_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_member_assigned_by_user_id_fk": { - "name": "permission_group_member_assigned_by_user_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "user", - "columnsFrom": ["assigned_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permissions": { - "name": "permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "entity_type": { - "name": "entity_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission_type": { - "name": "permission_type", - "type": "permission_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "permissions_user_id_idx": { - "name": "permissions_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_entity_idx": { - "name": "permissions_entity_idx", - "columns": [ - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_type_idx": { - "name": "permissions_user_entity_type_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_permission_idx": { - "name": "permissions_user_entity_permission_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permission_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_idx": { - "name": "permissions_user_entity_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_unique_constraint": { - "name": "permissions_unique_constraint", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permissions_user_id_user_id_fk": { - "name": "permissions_user_id_user_id_fk", - "tableFrom": "permissions", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.rate_limit_bucket": { - "name": "rate_limit_bucket", - "schema": "", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "tokens": { - "name": "tokens", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "last_refill_at": { - "name": "last_refill_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.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 - }, - "error_notifications_enabled": { - "name": "error_notifications_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "snap_to_grid_size": { - "name": "snap_to_grid_size", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "show_action_bar": { - "name": "show_action_bar", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "copilot_enabled_models": { - "name": "copilot_enabled_models", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "copilot_auto_allowed_tools": { - "name": "copilot_auto_allowed_tools", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "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.skill": { - "name": "skill", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "skill_workspace_name_unique": { - "name": "skill_workspace_name_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "skill_workspace_id_workspace_id_fk": { - "name": "skill_workspace_id_workspace_id_fk", - "tableFrom": "skill", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "skill_user_id_user_id_fk": { - "name": "skill_user_id_user_id_fk", - "tableFrom": "skill", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sso_provider": { - "name": "sso_provider", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "issuer": { - "name": "issuer", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "domain": { - "name": "domain", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "oidc_config": { - "name": "oidc_config", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "saml_config": { - "name": "saml_config", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "sso_provider_provider_id_idx": { - "name": "sso_provider_provider_id_idx", - "columns": [ - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_domain_idx": { - "name": "sso_provider_domain_idx", - "columns": [ - { - "expression": "domain", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_user_id_idx": { - "name": "sso_provider_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_organization_id_idx": { - "name": "sso_provider_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sso_provider_user_id_user_id_fk": { - "name": "sso_provider_user_id_user_id_fk", - "tableFrom": "sso_provider", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "sso_provider_organization_id_organization_id_fk": { - "name": "sso_provider_organization_id_organization_id_fk", - "tableFrom": "sso_provider", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.subscription": { - "name": "subscription", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "plan": { - "name": "plan", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "stripe_customer_id": { - "name": "stripe_customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_subscription_id": { - "name": "stripe_subscription_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "period_start": { - "name": "period_start", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "period_end": { - "name": "period_end", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "cancel_at_period_end": { - "name": "cancel_at_period_end", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "seats": { - "name": "seats", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "trial_start": { - "name": "trial_start", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "trial_end": { - "name": "trial_end", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "json", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "subscription_reference_status_idx": { - "name": "subscription_reference_status_idx", - "columns": [ - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "check_enterprise_metadata": { - "name": "check_enterprise_metadata", - "value": "plan != 'enterprise' OR metadata IS NOT NULL" - } - }, - "isRLSEnabled": false - }, - "public.template_creators": { - "name": "template_creators", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "reference_type": { - "name": "reference_type", - "type": "template_creator_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "profile_image_url": { - "name": "profile_image_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "details": { - "name": "details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "verified": { - "name": "verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "template_creators_reference_idx": { - "name": "template_creators_reference_idx", - "columns": [ - { - "expression": "reference_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_creators_reference_id_idx": { - "name": "template_creators_reference_id_idx", - "columns": [ - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_creators_created_by_idx": { - "name": "template_creators_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "template_creators_created_by_user_id_fk": { - "name": "template_creators_created_by_user_id_fk", - "tableFrom": "template_creators", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.template_stars": { - "name": "template_stars", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "template_id": { - "name": "template_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "starred_at": { - "name": "starred_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "template_stars_user_id_idx": { - "name": "template_stars_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_id_idx": { - "name": "template_stars_template_id_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_user_template_idx": { - "name": "template_stars_user_template_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_user_idx": { - "name": "template_stars_template_user_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_starred_at_idx": { - "name": "template_stars_starred_at_idx", - "columns": [ - { - "expression": "starred_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_starred_at_idx": { - "name": "template_stars_template_starred_at_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "starred_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_user_template_unique": { - "name": "template_stars_user_template_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "template_stars_user_id_user_id_fk": { - "name": "template_stars_user_id_user_id_fk", - "tableFrom": "template_stars", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "template_stars_template_id_templates_id_fk": { - "name": "template_stars_template_id_templates_id_fk", - "tableFrom": "template_stars", - "tableTo": "templates", - "columnsFrom": ["template_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.templates": { - "name": "templates", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "details": { - "name": "details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "creator_id": { - "name": "creator_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "views": { - "name": "views", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "stars": { - "name": "stars", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "status": { - "name": "status", - "type": "template_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "tags": { - "name": "tags", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'::text[]" - }, - "required_credentials": { - "name": "required_credentials", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "state": { - "name": "state", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "og_image_url": { - "name": "og_image_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "templates_status_idx": { - "name": "templates_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_creator_id_idx": { - "name": "templates_creator_id_idx", - "columns": [ - { - "expression": "creator_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_views_idx": { - "name": "templates_views_idx", - "columns": [ - { - "expression": "views", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_stars_idx": { - "name": "templates_stars_idx", - "columns": [ - { - "expression": "stars", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_status_views_idx": { - "name": "templates_status_views_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "views", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_status_stars_idx": { - "name": "templates_status_stars_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stars", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_created_at_idx": { - "name": "templates_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_updated_at_idx": { - "name": "templates_updated_at_idx", - "columns": [ - { - "expression": "updated_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "templates_workflow_id_workflow_id_fk": { - "name": "templates_workflow_id_workflow_id_fk", - "tableFrom": "templates", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "templates_creator_id_template_creators_id_fk": { - "name": "templates_creator_id_template_creators_id_fk", - "tableFrom": "templates", - "tableTo": "template_creators", - "columnsFrom": ["creator_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.usage_log": { - "name": "usage_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "category": { - "name": "category", - "type": "usage_log_category", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "source": { - "name": "source", - "type": "usage_log_source", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "cost": { - "name": "cost", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "usage_log_user_created_at_idx": { - "name": "usage_log_user_created_at_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_source_idx": { - "name": "usage_log_source_idx", - "columns": [ - { - "expression": "source", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_workspace_id_idx": { - "name": "usage_log_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_workflow_id_idx": { - "name": "usage_log_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "usage_log_user_id_user_id_fk": { - "name": "usage_log_user_id_user_id_fk", - "tableFrom": "usage_log", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "usage_log_workspace_id_workspace_id_fk": { - "name": "usage_log_workspace_id_workspace_id_fk", - "tableFrom": "usage_log", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "usage_log_workflow_id_workflow_id_fk": { - "name": "usage_log_workflow_id_workflow_id_fk", - "tableFrom": "usage_log", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "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'" - }, - "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": {} - } - }, - "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 - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "user_table_rows_table_id_idx": { - "name": "user_table_rows_table_id_idx", - "columns": [ - { - "expression": "table_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_rows_data_gin_idx": { - "name": "user_table_rows_data_gin_idx", - "columns": [ - { - "expression": "data", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "user_table_rows_workspace_table_idx": { - "name": "user_table_rows_workspace_table_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "table_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "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": {} - } - }, - "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": {} - }, - "idx_webhook_on_workflow_id_block_id": { - "name": "idx_webhook_on_workflow_id_block_id", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_workflow_deployment_idx": { - "name": "webhook_workflow_deployment_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_credential_set_id_idx": { - "name": "webhook_credential_set_id_idx", - "columns": [ - { - "expression": "credential_set_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_archived_at_idx": { - "name": "webhook_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "webhook_workflow_id_workflow_id_fk": { - "name": "webhook_workflow_id_workflow_id_fk", - "tableFrom": "webhook", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "webhook_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "webhook", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "webhook_credential_set_id_credential_set_id_fk": { - "name": "webhook_credential_set_id_credential_set_id_fk", - "tableFrom": "webhook", - "tableTo": "credential_set", - "columnsFrom": ["credential_set_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow": { - "name": "workflow", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "folder_id": { - "name": "folder_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'#3972F6'" - }, - "last_synced": { - "name": "last_synced", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "is_deployed": { - "name": "is_deployed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "deployed_at": { - "name": "deployed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "is_public_api": { - "name": "is_public_api", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "run_count": { - "name": "run_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_run_at": { - "name": "last_run_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "variables": { - "name": "variables", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "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": {} - } - }, - "foreignKeys": { - "workflow_user_id_user_id_fk": { - "name": "workflow_user_id_user_id_fk", - "tableFrom": "workflow", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_workspace_id_workspace_id_fk": { - "name": "workflow_workspace_id_workspace_id_fk", - "tableFrom": "workflow", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_folder_id_workflow_folder_id_fk": { - "name": "workflow_folder_id_workflow_folder_id_fk", - "tableFrom": "workflow", - "tableTo": "workflow_folder", - "columnsFrom": ["folder_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_blocks": { - "name": "workflow_blocks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "position_x": { - "name": "position_x", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "position_y": { - "name": "position_y", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "horizontal_handles": { - "name": "horizontal_handles", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "is_wide": { - "name": "is_wide", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "advanced_mode": { - "name": "advanced_mode", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "trigger_mode": { - "name": "trigger_mode", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "locked": { - "name": "locked", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "height": { - "name": "height", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "sub_blocks": { - "name": "sub_blocks", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "outputs": { - "name": "outputs", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_blocks_workflow_id_idx": { - "name": "workflow_blocks_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_blocks_type_idx": { - "name": "workflow_blocks_type_idx", - "columns": [ - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_blocks_workflow_id_workflow_id_fk": { - "name": "workflow_blocks_workflow_id_workflow_id_fk", - "tableFrom": "workflow_blocks", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_checkpoints": { - "name": "workflow_checkpoints", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "message_id": { - "name": "message_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workflow_state": { - "name": "workflow_state", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_checkpoints_user_id_idx": { - "name": "workflow_checkpoints_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_workflow_id_idx": { - "name": "workflow_checkpoints_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_chat_id_idx": { - "name": "workflow_checkpoints_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_message_id_idx": { - "name": "workflow_checkpoints_message_id_idx", - "columns": [ - { - "expression": "message_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_user_workflow_idx": { - "name": "workflow_checkpoints_user_workflow_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_workflow_chat_idx": { - "name": "workflow_checkpoints_workflow_chat_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_created_at_idx": { - "name": "workflow_checkpoints_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_chat_created_at_idx": { - "name": "workflow_checkpoints_chat_created_at_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_checkpoints_user_id_user_id_fk": { - "name": "workflow_checkpoints_user_id_user_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_checkpoints_workflow_id_workflow_id_fk": { - "name": "workflow_checkpoints_workflow_id_workflow_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_checkpoints_chat_id_copilot_chats_id_fk": { - "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_deployment_version": { - "name": "workflow_deployment_version", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "state": { - "name": "state", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "workflow_deployment_version_workflow_version_unique": { - "name": "workflow_deployment_version_workflow_version_unique", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_deployment_version_workflow_active_idx": { - "name": "workflow_deployment_version_workflow_active_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "is_active", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_deployment_version_created_at_idx": { - "name": "workflow_deployment_version_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_deployment_version_workflow_id_workflow_id_fk": { - "name": "workflow_deployment_version_workflow_id_workflow_id_fk", - "tableFrom": "workflow_deployment_version", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_edges": { - "name": "workflow_edges", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_block_id": { - "name": "source_block_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "target_block_id": { - "name": "target_block_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_handle": { - "name": "source_handle", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "target_handle": { - "name": "target_handle", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_edges_workflow_id_idx": { - "name": "workflow_edges_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_edges_workflow_source_idx": { - "name": "workflow_edges_workflow_source_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_edges_workflow_target_idx": { - "name": "workflow_edges_workflow_target_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "target_block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_edges_workflow_id_workflow_id_fk": { - "name": "workflow_edges_workflow_id_workflow_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_edges_source_block_id_workflow_blocks_id_fk": { - "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow_blocks", - "columnsFrom": ["source_block_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_edges_target_block_id_workflow_blocks_id_fk": { - "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow_blocks", - "columnsFrom": ["target_block_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_execution_logs": { - "name": "workflow_execution_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state_snapshot_id": { - "name": "state_snapshot_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "deployment_version_id": { - "name": "deployment_version_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'running'" - }, - "trigger": { - "name": "trigger", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ended_at": { - "name": "ended_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "total_duration_ms": { - "name": "total_duration_ms", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "execution_data": { - "name": "execution_data", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "cost": { - "name": "cost", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "files": { - "name": "files", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_execution_logs_workflow_id_idx": { - "name": "workflow_execution_logs_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_state_snapshot_id_idx": { - "name": "workflow_execution_logs_state_snapshot_id_idx", - "columns": [ - { - "expression": "state_snapshot_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_deployment_version_id_idx": { - "name": "workflow_execution_logs_deployment_version_id_idx", - "columns": [ - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_trigger_idx": { - "name": "workflow_execution_logs_trigger_idx", - "columns": [ - { - "expression": "trigger", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_level_idx": { - "name": "workflow_execution_logs_level_idx", - "columns": [ - { - "expression": "level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_started_at_idx": { - "name": "workflow_execution_logs_started_at_idx", - "columns": [ - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_execution_id_unique": { - "name": "workflow_execution_logs_execution_id_unique", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_workflow_started_at_idx": { - "name": "workflow_execution_logs_workflow_started_at_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_workspace_started_at_idx": { - "name": "workflow_execution_logs_workspace_started_at_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_running_started_at_idx": { - "name": "workflow_execution_logs_running_started_at_idx", - "columns": [ - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'running'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_execution_logs_workflow_id_workflow_id_fk": { - "name": "workflow_execution_logs_workflow_id_workflow_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "workflow_execution_logs_workspace_id_workspace_id_fk": { - "name": "workflow_execution_logs_workspace_id_workspace_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { - "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow_execution_snapshots", - "columnsFrom": ["state_snapshot_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_execution_snapshots": { - "name": "workflow_execution_snapshots", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "state_hash": { - "name": "state_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state_data": { - "name": "state_data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_snapshots_workflow_id_idx": { - "name": "workflow_snapshots_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_hash_idx": { - "name": "workflow_snapshots_hash_idx", - "columns": [ - { - "expression": "state_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_workflow_hash_idx": { - "name": "workflow_snapshots_workflow_hash_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "state_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_created_at_idx": { - "name": "workflow_snapshots_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_execution_snapshots_workflow_id_workflow_id_fk": { - "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", - "tableFrom": "workflow_execution_snapshots", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_folder": { - "name": "workflow_folder", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_id": { - "name": "parent_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'#6B7280'" - }, - "is_expanded": { - "name": "is_expanded", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "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": {} - } - }, - "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": {} - } - }, - "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_idx": { - "name": "workflow_mcp_tool_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "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 - }, - "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 - }, - "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_idx": { - "name": "workflow_schedule_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_schedule_workflow_id_workflow_id_fk": { - "name": "workflow_schedule_workflow_id_workflow_id_fk", - "tableFrom": "workflow_schedule", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "workflow_schedule", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "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 - }, - "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": {}, - "foreignKeys": { - "workspace_owner_id_user_id_fk": { - "name": "workspace_owner_id_user_id_fk", - "tableFrom": "workspace", - "tableTo": "user", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_billed_account_user_id_user_id_fk": { - "name": "workspace_billed_account_user_id_user_id_fk", - "tableFrom": "workspace", - "tableTo": "user", - "columnsFrom": ["billed_account_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_byok_keys": { - "name": "workspace_byok_keys", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "encrypted_api_key": { - "name": "encrypted_api_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_byok_provider_unique": { - "name": "workspace_byok_provider_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_byok_workspace_idx": { - "name": "workspace_byok_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_byok_keys_workspace_id_workspace_id_fk": { - "name": "workspace_byok_keys_workspace_id_workspace_id_fk", - "tableFrom": "workspace_byok_keys", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_byok_keys_created_by_user_id_fk": { - "name": "workspace_byok_keys_created_by_user_id_fk", - "tableFrom": "workspace_byok_keys", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_environment": { - "name": "workspace_environment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "variables": { - "name": "variables", - "type": "json", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_environment_workspace_unique": { - "name": "workspace_environment_workspace_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_environment_workspace_id_workspace_id_fk": { - "name": "workspace_environment_workspace_id_workspace_id_fk", - "tableFrom": "workspace_environment", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_file": { - "name": "workspace_file", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "size": { - "name": "size", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "uploaded_by": { - "name": "uploaded_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "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": {} - } - }, - "foreignKeys": { - "workspace_file_workspace_id_workspace_id_fk": { - "name": "workspace_file_workspace_id_workspace_id_fk", - "tableFrom": "workspace_file", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_file_uploaded_by_user_id_fk": { - "name": "workspace_file_uploaded_by_user_id_fk", - "tableFrom": "workspace_file", - "tableTo": "user", - "columnsFrom": ["uploaded_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_file_key_unique": { - "name": "workspace_file_key_unique", - "nullsNotDistinct": false, - "columns": ["key"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_files": { - "name": "workspace_files", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "context": { - "name": "context", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "original_name": { - "name": "original_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "size": { - "name": "size", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "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_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_name_active_unique": { - "name": "workspace_files_workspace_name_active_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": 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_key_idx": { - "name": "workspace_files_key_idx", - "columns": [ - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_user_id_idx": { - "name": "workspace_files_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_workspace_id_idx": { - "name": "workspace_files_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_context_idx": { - "name": "workspace_files_context_idx", - "columns": [ - { - "expression": "context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "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": {} - } - }, - "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_chat_id_copilot_chats_id_fk": { - "name": "workspace_files_chat_id_copilot_chats_id_fk", - "tableFrom": "workspace_files", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_invitation": { - "name": "workspace_invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "inviter_id": { - "name": "inviter_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "status": { - "name": "status", - "type": "workspace_invitation_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permissions": { - "name": "permissions", - "type": "permission_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'admin'" - }, - "org_invitation_id": { - "name": "org_invitation_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "workspace_invitation_workspace_id_workspace_id_fk": { - "name": "workspace_invitation_workspace_id_workspace_id_fk", - "tableFrom": "workspace_invitation", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_invitation_inviter_id_user_id_fk": { - "name": "workspace_invitation_inviter_id_user_id_fk", - "tableFrom": "workspace_invitation", - "tableTo": "user", - "columnsFrom": ["inviter_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_invitation_token_unique": { - "name": "workspace_invitation_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_notification_delivery": { - "name": "workspace_notification_delivery", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "subscription_id": { - "name": "subscription_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "notification_delivery_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_attempt_at": { - "name": "last_attempt_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "next_attempt_at": { - "name": "next_attempt_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "response_status": { - "name": "response_status", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "response_body": { - "name": "response_body", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_notification_delivery_subscription_id_idx": { - "name": "workspace_notification_delivery_subscription_id_idx", - "columns": [ - { - "expression": "subscription_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_execution_id_idx": { - "name": "workspace_notification_delivery_execution_id_idx", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_status_idx": { - "name": "workspace_notification_delivery_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_next_attempt_idx": { - "name": "workspace_notification_delivery_next_attempt_idx", - "columns": [ - { - "expression": "next_attempt_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { - "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", - "tableFrom": "workspace_notification_delivery", - "tableTo": "workspace_notification_subscription", - "columnsFrom": ["subscription_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_notification_delivery_workflow_id_workflow_id_fk": { - "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", - "tableFrom": "workspace_notification_delivery", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_notification_subscription": { - "name": "workspace_notification_subscription", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "notification_type": { - "name": "notification_type", - "type": "notification_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "workflow_ids": { - "name": "workflow_ids", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'::text[]" - }, - "all_workflows": { - "name": "all_workflows", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "level_filter": { - "name": "level_filter", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY['info', 'error']::text[]" - }, - "trigger_filter": { - "name": "trigger_filter", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" - }, - "include_final_output": { - "name": "include_final_output", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_trace_spans": { - "name": "include_trace_spans", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_rate_limits": { - "name": "include_rate_limits", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_usage_data": { - "name": "include_usage_data", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "webhook_config": { - "name": "webhook_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "email_recipients": { - "name": "email_recipients", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "slack_config": { - "name": "slack_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "alert_config": { - "name": "alert_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "last_alert_at": { - "name": "last_alert_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "active": { - "name": "active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_notification_workspace_id_idx": { - "name": "workspace_notification_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_active_idx": { - "name": "workspace_notification_active_idx", - "columns": [ - { - "expression": "active", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_type_idx": { - "name": "workspace_notification_type_idx", - "columns": [ - { - "expression": "notification_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_notification_subscription_workspace_id_workspace_id_fk": { - "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", - "tableFrom": "workspace_notification_subscription", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_notification_subscription_created_by_user_id_fk": { - "name": "workspace_notification_subscription_created_by_user_id_fk", - "tableFrom": "workspace_notification_subscription", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.a2a_task_status": { - "name": "a2a_task_status", - "schema": "public", - "values": [ - "submitted", - "working", - "input-required", - "completed", - "failed", - "canceled", - "rejected", - "auth-required", - "unknown" - ] - }, - "public.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.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.notification_delivery_status": { - "name": "notification_delivery_status", - "schema": "public", - "values": ["pending", "in_progress", "success", "failed"] - }, - "public.notification_type": { - "name": "notification_type", - "schema": "public", - "values": ["webhook", "email", "slack"] - }, - "public.permission_type": { - "name": "permission_type", - "schema": "public", - "values": ["admin", "write", "read"] - }, - "public.template_creator_type": { - "name": "template_creator_type", - "schema": "public", - "values": ["user", "organization"] - }, - "public.template_status": { - "name": "template_status", - "schema": "public", - "values": ["pending", "approved", "rejected"] - }, - "public.usage_log_category": { - "name": "usage_log_category", - "schema": "public", - "values": ["model", "fixed"] - }, - "public.usage_log_source": { - "name": "usage_log_source", - "schema": "public", - "values": [ - "workflow", - "wand", - "copilot", - "workspace-chat", - "mcp_copilot", - "mothership_block", - "knowledge-base", - "voice-input" - ] - }, - "public.workspace_invitation_status": { - "name": "workspace_invitation_status", - "schema": "public", - "values": ["pending", "accepted", "rejected", "cancelled"] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 757a3b45a6e..44d373a8af0 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1044,7 +1044,6 @@ export const workspace = pgTable('workspace', { inboxProviderId: text('inbox_provider_id'), logRetentionHours: integer('log_retention_hours'), softDeleteRetentionHours: integer('soft_delete_retention_hours'), - taskRedactionHours: integer('task_redaction_hours'), taskCleanupHours: integer('task_cleanup_hours'), archivedAt: timestamp('archived_at'), createdAt: timestamp('created_at').notNull().defaultNow(), @@ -1617,7 +1616,6 @@ export const copilotChats = pgTable( config: jsonb('config'), resources: jsonb('resources').notNull().default('[]'), lastSeenAt: timestamp('last_seen_at'), - redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1782,7 +1780,6 @@ export const copilotRunCheckpoints = pgTable( conversationSnapshot: jsonb('conversation_snapshot').notNull().default('{}'), agentState: jsonb('agent_state').notNull().default('{}'), providerRequest: jsonb('provider_request').notNull().default('{}'), - redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1817,7 +1814,6 @@ export const copilotAsyncToolCalls = pgTable( claimedAt: timestamp('claimed_at'), claimedBy: text('claimed_by'), completedAt: timestamp('completed_at'), - redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -1949,7 +1945,6 @@ export const copilotFeedback = pgTable( isPositive: boolean('is_positive').notNull(), feedback: text('feedback'), // Optional feedback text workflowYaml: text('workflow_yaml'), // Optional workflow YAML if edit/build workflow was triggered - redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, @@ -2945,7 +2940,6 @@ export const mothershipInboxTask = pgTable( rejectionReason: text('rejection_reason'), hasAttachments: boolean('has_attachments').notNull().default(false), ccRecipients: text('cc_recipients'), - redactedAt: timestamp('redacted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), processingStartedAt: timestamp('processing_started_at'), completedAt: timestamp('completed_at'), From 4148d2b263c1f69ec30cc532f4e9dbad840c3289 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 17 Apr 2026 19:44:06 -0700 Subject: [PATCH 04/15] add bucket attachment cleanup --- apps/sim/background/cleanup-soft-deletes.ts | 34 +++ apps/sim/background/cleanup-tasks.ts | 82 +----- apps/sim/lib/cleanup/chat-cleanup.ts | 194 ++++++++++++++ .../lib/core/async-jobs/backends/bullmq.ts | 126 ---------- apps/sim/lib/core/bullmq/queues.ts | 238 ------------------ 5 files changed, 240 insertions(+), 434 deletions(-) create mode 100644 apps/sim/lib/cleanup/chat-cleanup.ts delete mode 100644 apps/sim/lib/core/async-jobs/backends/bullmq.ts delete mode 100644 apps/sim/lib/core/bullmq/queues.ts diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index 501bc1ad4d2..f8648089d11 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { a2aAgent, + copilotChats, knowledgeBase, mcpServers, memory, @@ -21,6 +22,7 @@ import { getRetentionDefaultHours, resolveTierWorkspaceIds, } from '@/lib/billing/cleanup-dispatcher' +import { prepareChatCleanup } from '@/lib/cleanup/chat-cleanup' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' const logger = createLogger('CleanupSoftDeletes') @@ -181,6 +183,33 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise `[${tierLabel}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` ) + // Find chats linked to workflows that are about to be cascade-deleted + const doomedWorkflows = await db + .select({ id: workflow.id }) + .from(workflow) + .where( + and( + inArray(workflow.workspaceId, workspaceIds), + isNotNull(workflow.archivedAt), + lt(workflow.archivedAt, retentionDate) + ) + ) + + const doomedWorkflowIds = doomedWorkflows.map((w) => w.id) + let chatCleanup: { execute: () => Promise } | null = null + + if (doomedWorkflowIds.length > 0) { + const doomedChats = await db + .select({ id: copilotChats.id }) + .from(copilotChats) + .where(inArray(copilotChats.workflowId, doomedWorkflowIds)) + + const doomedChatIds = doomedChats.map((c) => c.id) + if (doomedChatIds.length > 0) { + chatCleanup = await prepareChatCleanup(doomedChatIds, tierLabel) + } + } + const fileStats = await cleanupWorkspaceFileStorage(workspaceIds, retentionDate) let totalDeleted = 0 @@ -200,6 +229,11 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise `[${tierLabel}] Complete: ${totalDeleted} rows deleted, ${fileStats.filesDeleted} files cleaned` ) + // Clean up copilot backend + chat storage files after DB rows are gone + if (chatCleanup) { + await chatCleanup.execute() + } + const timeElapsed = (Date.now() - startTime) / 1000 logger.info(`[${tierLabel}] Job completed in ${timeElapsed.toFixed(2)}s`) } diff --git a/apps/sim/background/cleanup-tasks.ts b/apps/sim/background/cleanup-tasks.ts index 9fb251b174a..2b8558cd935 100644 --- a/apps/sim/background/cleanup-tasks.ts +++ b/apps/sim/background/cleanup-tasks.ts @@ -17,8 +17,7 @@ import { getRetentionDefaultHours, resolveTierWorkspaceIds, } from '@/lib/billing/cleanup-dispatcher' -import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' -import { env } from '@/lib/core/config/env' +import { prepareChatCleanup } from '@/lib/cleanup/chat-cleanup' const logger = createLogger('CleanupTasks') @@ -87,7 +86,7 @@ async function cleanupRunChildren( const runIds = await db .select({ id: copilotRuns.id }) .from(copilotRuns) - .where(and(inArray(copilotRuns.workspaceId, workspaceIds), lt(copilotRuns.createdAt, retentionDate))) + .where(and(inArray(copilotRuns.workspaceId, workspaceIds), lt(copilotRuns.updatedAt, retentionDate))) .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) if (runIds.length === 0) { @@ -139,61 +138,6 @@ async function cleanupRunChildren( return results } -const COPILOT_CLEANUP_BATCH_SIZE = 1000 - -/** - * Call the copilot backend to delete chat data (memory_files, checkpoints, task_chains, etc.) - * before we delete the Sim DB rows. Chunked at 1000 per request. - */ -async function cleanupCopilotBackend( - chatIds: string[], - tierLabel: string -): Promise<{ deleted: number; failed: number }> { - const stats = { deleted: 0, failed: 0 } - - if (chatIds.length === 0 || !env.COPILOT_API_KEY) { - if (!env.COPILOT_API_KEY) { - logger.warn(`[${tierLabel}] COPILOT_API_KEY not set, skipping copilot backend cleanup`) - } - return stats - } - - for (let i = 0; i < chatIds.length; i += COPILOT_CLEANUP_BATCH_SIZE) { - const chunk = chatIds.slice(i, i + COPILOT_CLEANUP_BATCH_SIZE) - try { - const response = await fetch(`${SIM_AGENT_API_URL}/api/tasks/cleanup`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': env.COPILOT_API_KEY, - }, - body: JSON.stringify({ chatIds: chunk }), - }) - - if (!response.ok) { - const errorBody = await response.text().catch(() => '') - logger.error(`[${tierLabel}] Copilot backend cleanup failed: ${response.status}`, { - errorBody, - chatCount: chunk.length, - }) - stats.failed += chunk.length - continue - } - - const result = await response.json() - stats.deleted += result.deleted ?? 0 - logger.info( - `[${tierLabel}] Copilot backend cleanup: ${result.deleted} chats deleted (batch ${Math.floor(i / COPILOT_CLEANUP_BATCH_SIZE) + 1})` - ) - } catch (error) { - stats.failed += chunk.length - logger.error(`[${tierLabel}] Copilot backend cleanup request failed:`, { error }) - } - } - - return stats -} - async function resolvePayload(payload: CleanupJobPayload): Promise<{ workspaceIds: string[] retentionHours: number @@ -252,13 +196,16 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise .where( and( inArray(copilotChats.workspaceId, workspaceIds), - lt(copilotChats.createdAt, retentionDate) + lt(copilotChats.updatedAt, retentionDate) ) ) .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) const doomedChatIds = doomedChats.map((c) => c.id) + // Prepare chat cleanup (collect file keys + copilot backend call) BEFORE DB deletion + const chatCleanup = await prepareChatCleanup(doomedChatIds, tierLabel) + // Delete run children first (checkpoints, tool calls) since they reference runs const runChildResults = await cleanupRunChildren(workspaceIds, retentionDate, tierLabel) for (const r of runChildResults) { @@ -278,7 +225,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise .where( and( inArray(copilotChats.workspaceId, workspaceIds), - lt(copilotChats.createdAt, retentionDate) + lt(copilotChats.updatedAt, retentionDate) ) ) .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) @@ -287,7 +234,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise const deleted = await db .delete(copilotFeedback) .where(inArray(copilotFeedback.chatId, chatIds.map((c) => c.id))) - .returning({ id: sql`id` }) + .returning({ id: copilotFeedback.feedbackId }) feedbackResult.deleted = deleted.length logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) } else { @@ -302,7 +249,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise const runsResult = await cleanupTable( copilotRuns, copilotRuns.workspaceId, - copilotRuns.createdAt, + copilotRuns.updatedAt, workspaceIds, retentionDate, `${tierLabel}/copilotRuns` @@ -312,7 +259,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise const chatsResult = await cleanupTable( copilotChats, copilotChats.workspaceId, - copilotChats.createdAt, + copilotChats.updatedAt, workspaceIds, retentionDate, `${tierLabel}/copilotChats` @@ -336,13 +283,8 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise logger.info(`[${tierLabel}] Complete: ${totalDeleted} total rows deleted`) - // Clean up copilot backend after Sim DB rows are gone (chat no longer accessible) - if (doomedChatIds.length > 0) { - const copilotResult = await cleanupCopilotBackend(doomedChatIds, tierLabel) - logger.info( - `[${tierLabel}] Copilot backend: ${copilotResult.deleted} deleted, ${copilotResult.failed} failed` - ) - } + // Clean up copilot backend + storage files after DB rows are gone + await chatCleanup.execute() const timeElapsed = (Date.now() - startTime) / 1000 logger.info(`Task cleanup completed in ${timeElapsed.toFixed(2)}s`) diff --git a/apps/sim/lib/cleanup/chat-cleanup.ts b/apps/sim/lib/cleanup/chat-cleanup.ts new file mode 100644 index 00000000000..d4a2363a9e6 --- /dev/null +++ b/apps/sim/lib/cleanup/chat-cleanup.ts @@ -0,0 +1,194 @@ +import { db } from '@sim/db' +import { copilotChats, workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, inArray, isNull } from 'drizzle-orm' +import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' +import { env } from '@/lib/core/config/env' +import type { StorageContext } from '@/lib/uploads' +import { isUsingCloudStorage, StorageService } from '@/lib/uploads' + +const logger = createLogger('ChatCleanup') + +const COPILOT_CLEANUP_BATCH_SIZE = 1000 + +interface FileRef { + key: string + context: StorageContext +} + +/** + * Collect all file storage keys associated with the given chat IDs. + * Two sources: + * 1. workspaceFiles rows with chatId FK — includes the storage context + * 2. fileAttachments[].key inside copilotChats.messages JSONB — these are + * uploaded with context='copilot' + */ +export async function collectChatFiles(chatIds: string[]): Promise { + const files: FileRef[] = [] + if (chatIds.length === 0) return files + + const seen = new Set() + + // Source 1: workspaceFiles linked via chatId (includes context column) + const linkedFiles = await db + .select({ key: workspaceFiles.key, context: workspaceFiles.context }) + .from(workspaceFiles) + .where(and(inArray(workspaceFiles.chatId, chatIds), isNull(workspaceFiles.deletedAt))) + + for (const f of linkedFiles) { + if (!seen.has(f.key)) { + seen.add(f.key) + files.push({ key: f.key, context: f.context as StorageContext }) + } + } + + // Source 2: fileAttachments in messages JSONB (always context='copilot') + const chatsWithMessages = await db + .select({ messages: copilotChats.messages }) + .from(copilotChats) + .where(inArray(copilotChats.id, chatIds)) + + for (const chat of chatsWithMessages) { + const messages = chat.messages as unknown[] + if (!Array.isArray(messages)) continue + for (const msg of messages) { + if (!msg || typeof msg !== 'object') continue + const attachments = (msg as Record).fileAttachments + if (!Array.isArray(attachments)) continue + for (const attachment of attachments) { + if ( + attachment && + typeof attachment === 'object' && + (attachment as Record).key + ) { + const key = (attachment as Record).key as string + if (!seen.has(key)) { + seen.add(key) + files.push({ key, context: 'copilot' }) + } + } + } + } + } + + return files +} + +/** + * Delete files from cloud storage using the correct context/bucket per file. + */ +export async function deleteStorageFiles( + files: FileRef[], + tierLabel: string +): Promise<{ filesDeleted: number; filesFailed: number }> { + const stats = { filesDeleted: 0, filesFailed: 0 } + if (files.length === 0 || !isUsingCloudStorage()) return stats + + for (const file of files) { + try { + logger.info(`[${tierLabel}] Deleting storage file: ${file.key} (context: ${file.context})`) + await StorageService.deleteFile({ key: file.key, context: file.context }) + stats.filesDeleted++ + } catch (error) { + stats.filesFailed++ + logger.error(`[${tierLabel}] Failed to delete storage file ${file.key}:`, { error }) + } + } + + return stats +} + +/** + * Call the copilot backend to delete chat data (memory_files, checkpoints, task_chains, etc.) + * Chunked at 1000 per request. + */ +export async function cleanupCopilotBackend( + chatIds: string[], + tierLabel: string +): Promise<{ deleted: number; failed: number }> { + const stats = { deleted: 0, failed: 0 } + + if (chatIds.length === 0 || !env.COPILOT_API_KEY) { + if (!env.COPILOT_API_KEY) { + logger.warn(`[${tierLabel}] COPILOT_API_KEY not set, skipping copilot backend cleanup`) + } + return stats + } + + for (let i = 0; i < chatIds.length; i += COPILOT_CLEANUP_BATCH_SIZE) { + const chunk = chatIds.slice(i, i + COPILOT_CLEANUP_BATCH_SIZE) + try { + const response = await fetch(`${SIM_AGENT_API_URL}/api/tasks/cleanup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': env.COPILOT_API_KEY, + }, + body: JSON.stringify({ chatIds: chunk }), + }) + + if (!response.ok) { + const errorBody = await response.text().catch(() => '') + logger.error(`[${tierLabel}] Copilot backend cleanup failed: ${response.status}`, { + errorBody, + chatCount: chunk.length, + }) + stats.failed += chunk.length + continue + } + + const result = await response.json() + stats.deleted += result.deleted ?? 0 + logger.info( + `[${tierLabel}] Copilot backend cleanup: ${result.deleted} chats deleted (batch ${Math.floor(i / COPILOT_CLEANUP_BATCH_SIZE) + 1})` + ) + } catch (error) { + stats.failed += chunk.length + logger.error(`[${tierLabel}] Copilot backend cleanup request failed:`, { error }) + } + } + + return stats +} + +/** + * Full chat cleanup: collect file refs, then (after DB deletion by caller) + * call copilot backend and delete storage files. + * + * Usage: + * const cleanup = await prepareChatCleanup(chatIds, tierLabel) + * // ... delete DB rows ... + * await cleanup.execute() + */ +export async function prepareChatCleanup( + chatIds: string[], + tierLabel: string +): Promise<{ execute: () => Promise }> { + // Collect file refs BEFORE DB deletion (keys + context are lost after cascade) + const files = await collectChatFiles(chatIds) + if (files.length > 0) { + logger.info(`[${tierLabel}] Collected ${files.length} files for cleanup`, { + files: files.map((f) => ({ key: f.key, context: f.context })), + }) + } + + return { + execute: async () => { + // Call copilot backend + if (chatIds.length > 0) { + const copilotResult = await cleanupCopilotBackend(chatIds, tierLabel) + logger.info( + `[${tierLabel}] Copilot backend: ${copilotResult.deleted} deleted, ${copilotResult.failed} failed` + ) + } + + // Delete storage files with correct context per file + if (files.length > 0) { + const fileStats = await deleteStorageFiles(files, tierLabel) + logger.info( + `[${tierLabel}] Storage cleanup: ${fileStats.filesDeleted} deleted, ${fileStats.filesFailed} failed` + ) + } + }, + } +} diff --git a/apps/sim/lib/core/async-jobs/backends/bullmq.ts b/apps/sim/lib/core/async-jobs/backends/bullmq.ts deleted file mode 100644 index 910788fc806..00000000000 --- a/apps/sim/lib/core/async-jobs/backends/bullmq.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { createLogger } from '@sim/logger' -import type { Job as BullMQJob } from 'bullmq' -import { - type EnqueueOptions, - JOB_STATUS, - type Job, - type JobQueueBackend, - type JobStatus, - type JobType, -} from '@/lib/core/async-jobs/types' -import { type BullMQJobData, createBullMQJobData, getBullMQQueue } from '@/lib/core/bullmq' - -const logger = createLogger('BullMQJobQueue') - -function mapBullMQStatus(status: string): JobStatus { - switch (status) { - case 'active': - return JOB_STATUS.PROCESSING - case 'completed': - return JOB_STATUS.COMPLETED - case 'failed': - return JOB_STATUS.FAILED - default: - return JOB_STATUS.PENDING - } -} - -async function toJob( - queueType: JobType, - bullJob: BullMQJob> | null -): Promise { - if (!bullJob) { - return null - } - - const status = mapBullMQStatus(await bullJob.getState()) - - return { - id: bullJob.id ?? '', - type: queueType, - payload: bullJob.data.payload, - status, - createdAt: new Date(bullJob.timestamp), - startedAt: bullJob.processedOn ? new Date(bullJob.processedOn) : undefined, - completedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined, - attempts: bullJob.attemptsMade, - maxAttempts: bullJob.opts.attempts ?? 1, - error: bullJob.failedReason || undefined, - output: bullJob.returnvalue, - metadata: bullJob.data.metadata ?? {}, - } -} - -export class BullMQJobQueue implements JobQueueBackend { - async enqueue( - type: JobType, - payload: TPayload, - options?: EnqueueOptions - ): Promise { - const queue = getBullMQQueue(type) - - const job = await queue.add( - options?.name ?? type, - createBullMQJobData(payload, options?.metadata), - { - jobId: options?.jobId, - attempts: options?.maxAttempts, - priority: options?.priority, - delay: options?.delayMs, - } - ) - - logger.debug('Enqueued job via BullMQ', { - jobId: job.id, - type, - name: options?.name ?? type, - }) - - return String(job.id) - } - - async getJob(jobId: string): Promise { - const workflowJob = await getBullMQQueue('workflow-execution').getJob(jobId) - if (workflowJob) { - return toJob('workflow-execution', workflowJob) - } - - const webhookJob = await getBullMQQueue('webhook-execution').getJob(jobId) - if (webhookJob) { - return toJob('webhook-execution', webhookJob) - } - - const scheduleJob = await getBullMQQueue('schedule-execution').getJob(jobId) - if (scheduleJob) { - return toJob('schedule-execution', scheduleJob) - } - - const resumeJob = await getBullMQQueue('resume-execution').getJob(jobId) - if (resumeJob) { - return toJob('resume-execution', resumeJob) - } - - const cleanupLogsJob = await getBullMQQueue('cleanup-logs').getJob(jobId) - if (cleanupLogsJob) { - return toJob('cleanup-logs', cleanupLogsJob) - } - - const cleanupSoftDeletesJob = await getBullMQQueue('cleanup-soft-deletes').getJob(jobId) - if (cleanupSoftDeletesJob) { - return toJob('cleanup-soft-deletes', cleanupSoftDeletesJob) - } - - const cleanupTasksJob = await getBullMQQueue('cleanup-tasks').getJob(jobId) - if (cleanupTasksJob) { - return toJob('cleanup-tasks', cleanupTasksJob) - } - - return null - } - - async startJob(_jobId: string): Promise {} - - async completeJob(_jobId: string, _output: unknown): Promise {} - - async markJobFailed(_jobId: string, _error: string): Promise {} -} diff --git a/apps/sim/lib/core/bullmq/queues.ts b/apps/sim/lib/core/bullmq/queues.ts deleted file mode 100644 index f8950d7f838..00000000000 --- a/apps/sim/lib/core/bullmq/queues.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Queue, QueueEvents } from 'bullmq' -import type { JobMetadata, JobType } from '@/lib/core/async-jobs/types' -import { getBullMQConnectionOptions } from '@/lib/core/bullmq/connection' -import type { WorkspaceDispatchQueueName } from '@/lib/core/workspace-dispatch/types' - -export const KNOWLEDGE_CONNECTOR_SYNC_QUEUE = 'knowledge-connector-sync' as const -export const KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE = 'knowledge-process-document' as const -export const MOTHERSHIP_JOB_EXECUTION_QUEUE = 'mothership-job-execution' as const -export const WORKSPACE_NOTIFICATION_DELIVERY_QUEUE = 'workspace-notification-delivery' as const - -export interface BullMQJobData { - payload: TPayload - metadata?: JobMetadata -} - -let workflowQueueInstance: Queue | null = null -let webhookQueueInstance: Queue | null = null -let scheduleQueueInstance: Queue | null = null -let resumeQueueInstance: Queue | null = null -let knowledgeConnectorSyncQueueInstance: Queue | null = null -let knowledgeDocumentProcessingQueueInstance: Queue | null = null -let mothershipJobExecutionQueueInstance: Queue | null = null -let workspaceNotificationDeliveryQueueInstance: Queue | null = null -let cleanupLogsQueueInstance: Queue | null = null -let cleanupSoftDeletesQueueInstance: Queue | null = null -let cleanupTasksQueueInstance: Queue | null = null -let workflowQueueEventsInstance: QueueEvents | null = null - -function getQueueDefaultOptions(type: JobType) { - switch (type) { - case 'workflow-execution': - return { - attempts: 3, - backoff: { type: 'exponential' as const, delay: 1000 }, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - } - case 'webhook-execution': - return { - attempts: 2, - backoff: { type: 'exponential' as const, delay: 2000 }, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 3 * 24 * 60 * 60 }, - } - case 'schedule-execution': - return { - attempts: 2, - backoff: { type: 'exponential' as const, delay: 5000 }, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 3 * 24 * 60 * 60 }, - } - case 'resume-execution': - return { - attempts: 1, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 3 * 24 * 60 * 60 }, - } - case 'cleanup-logs': - case 'cleanup-soft-deletes': - case 'cleanup-tasks': - return { - attempts: 1, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - } - } -} - -function createQueue(type: JobType): Queue { - return new Queue(type, { - connection: getBullMQConnectionOptions(), - defaultJobOptions: getQueueDefaultOptions(type), - }) -} - -function createNamedQueue( - name: - | typeof KNOWLEDGE_CONNECTOR_SYNC_QUEUE - | typeof KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE - | typeof MOTHERSHIP_JOB_EXECUTION_QUEUE - | typeof WORKSPACE_NOTIFICATION_DELIVERY_QUEUE -): Queue { - switch (name) { - case KNOWLEDGE_CONNECTOR_SYNC_QUEUE: - return new Queue(name, { - connection: getBullMQConnectionOptions(), - defaultJobOptions: { - attempts: 3, - backoff: { type: 'exponential', delay: 5000 }, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - }, - }) - case KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE: - return new Queue(name, { - connection: getBullMQConnectionOptions(), - defaultJobOptions: { - attempts: 3, - backoff: { type: 'exponential', delay: 1000 }, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - }, - }) - case MOTHERSHIP_JOB_EXECUTION_QUEUE: - return new Queue(name, { - connection: getBullMQConnectionOptions(), - defaultJobOptions: { - attempts: 1, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - }, - }) - case WORKSPACE_NOTIFICATION_DELIVERY_QUEUE: - return new Queue(name, { - connection: getBullMQConnectionOptions(), - defaultJobOptions: { - attempts: 1, - removeOnComplete: { age: 24 * 60 * 60 }, - removeOnFail: { age: 7 * 24 * 60 * 60 }, - }, - }) - } -} - -export function getBullMQQueue(type: JobType): Queue { - switch (type) { - case 'workflow-execution': - if (!workflowQueueInstance) { - workflowQueueInstance = createQueue(type) - } - return workflowQueueInstance - case 'webhook-execution': - if (!webhookQueueInstance) { - webhookQueueInstance = createQueue(type) - } - return webhookQueueInstance - case 'schedule-execution': - if (!scheduleQueueInstance) { - scheduleQueueInstance = createQueue(type) - } - return scheduleQueueInstance - case 'resume-execution': - if (!resumeQueueInstance) { - resumeQueueInstance = createQueue(type) - } - return resumeQueueInstance - case 'cleanup-logs': - if (!cleanupLogsQueueInstance) { - cleanupLogsQueueInstance = createQueue(type) - } - return cleanupLogsQueueInstance - case 'cleanup-soft-deletes': - if (!cleanupSoftDeletesQueueInstance) { - cleanupSoftDeletesQueueInstance = createQueue(type) - } - return cleanupSoftDeletesQueueInstance - case 'cleanup-tasks': - if (!cleanupTasksQueueInstance) { - cleanupTasksQueueInstance = createQueue(type) - } - return cleanupTasksQueueInstance - } -} - -export function getBullMQQueueByName(queueName: WorkspaceDispatchQueueName): Queue { - switch (queueName) { - case 'workflow-execution': - case 'webhook-execution': - case 'schedule-execution': - case 'resume-execution': - case 'cleanup-logs': - case 'cleanup-soft-deletes': - case 'cleanup-tasks': - return getBullMQQueue(queueName) - case KNOWLEDGE_CONNECTOR_SYNC_QUEUE: - return getKnowledgeConnectorSyncQueue() - case KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE: - return getKnowledgeDocumentProcessingQueue() - case MOTHERSHIP_JOB_EXECUTION_QUEUE: - return getMothershipJobExecutionQueue() - case WORKSPACE_NOTIFICATION_DELIVERY_QUEUE: - return getWorkspaceNotificationDeliveryQueue() - } -} - -export function getWorkflowQueueEvents(): QueueEvents { - if (!workflowQueueEventsInstance) { - workflowQueueEventsInstance = new QueueEvents('workflow-execution', { - connection: getBullMQConnectionOptions(), - }) - } - - return workflowQueueEventsInstance -} - -export function getKnowledgeConnectorSyncQueue(): Queue { - if (!knowledgeConnectorSyncQueueInstance) { - knowledgeConnectorSyncQueueInstance = createNamedQueue(KNOWLEDGE_CONNECTOR_SYNC_QUEUE) - } - - return knowledgeConnectorSyncQueueInstance -} - -export function getKnowledgeDocumentProcessingQueue(): Queue { - if (!knowledgeDocumentProcessingQueueInstance) { - knowledgeDocumentProcessingQueueInstance = createNamedQueue(KNOWLEDGE_DOCUMENT_PROCESSING_QUEUE) - } - - return knowledgeDocumentProcessingQueueInstance -} - -export function getMothershipJobExecutionQueue(): Queue { - if (!mothershipJobExecutionQueueInstance) { - mothershipJobExecutionQueueInstance = createNamedQueue(MOTHERSHIP_JOB_EXECUTION_QUEUE) - } - - return mothershipJobExecutionQueueInstance -} - -export function getWorkspaceNotificationDeliveryQueue(): Queue { - if (!workspaceNotificationDeliveryQueueInstance) { - workspaceNotificationDeliveryQueueInstance = createNamedQueue( - WORKSPACE_NOTIFICATION_DELIVERY_QUEUE - ) - } - - return workspaceNotificationDeliveryQueueInstance -} - -export function createBullMQJobData( - payload: TPayload, - metadata?: JobMetadata -): BullMQJobData { - return { - payload, - metadata: metadata ?? {}, - } -} From 8e0d8bc5a96b96ab3f76bc06a11521006e0e5bd9 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 17 Apr 2026 19:51:14 -0700 Subject: [PATCH 05/15] fix lint --- .../workspaces/[id]/data-retention/route.ts | 10 +- apps/sim/background/cleanup-logs.ts | 11 +- apps/sim/background/cleanup-soft-deletes.ts | 63 +++++- apps/sim/background/cleanup-tasks.ts | 193 +++++++++--------- .../components/data-retention-settings.tsx | 9 +- apps/sim/lib/billing/cleanup-dispatcher.ts | 5 +- 6 files changed, 169 insertions(+), 122 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts index 7b8ef9e0bd5..3fb2cd76b11 100644 --- a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -9,13 +9,9 @@ import { getSession } from '@/lib/auth' import { getRetentionDefaultHours } from '@/lib/billing/cleanup-dispatcher' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { isEnterprisePlan } from '@/lib/billing/core/subscription' -import { - checkEnterprisePlan, - checkProPlan, - checkTeamPlan, -} from '@/lib/billing/subscriptions/utils' -import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' +import { checkEnterprisePlan, checkProPlan, checkTeamPlan } from '@/lib/billing/subscriptions/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' +import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' const logger = createLogger('DataRetentionAPI') @@ -171,7 +167,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ if (parsed.data.softDeleteRetentionHours !== undefined) { updateData.softDeleteRetentionHours = parsed.data.softDeleteRetentionHours } -if (parsed.data.taskCleanupHours !== undefined) { + if (parsed.data.taskCleanupHours !== undefined) { updateData.taskCleanupHours = parsed.data.taskCleanupHours } diff --git a/apps/sim/background/cleanup-logs.ts b/apps/sim/background/cleanup-logs.ts index 34f035d4a89..013253b6c2c 100644 --- a/apps/sim/background/cleanup-logs.ts +++ b/apps/sim/background/cleanup-logs.ts @@ -3,12 +3,12 @@ import { workflowExecutionLogs, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' import { and, eq, inArray, lt } from 'drizzle-orm' -import { snapshotService } from '@/lib/logs/execution/snapshot/service' import { type CleanupJobPayload, getRetentionDefaultHours, resolveTierWorkspaceIds, } from '@/lib/billing/cleanup-dispatcher' +import { snapshotService } from '@/lib/logs/execution/snapshot/service' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' const logger = createLogger('CleanupLogs') @@ -26,7 +26,14 @@ interface TierResults { } function emptyTierResults(): TierResults { - return { total: 0, deleted: 0, deleteFailed: 0, filesTotal: 0, filesDeleted: 0, filesDeleteFailed: 0 } + return { + total: 0, + deleted: 0, + deleteFailed: 0, + filesTotal: 0, + filesDeleted: 0, + filesDeleteFailed: 0, + } } async function deleteExecutionFiles(files: unknown, results: TierResults): Promise { diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index f8648089d11..be05ce60bd0 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -120,16 +120,61 @@ async function cleanupWorkspaceFileStorage( } const CLEANUP_TARGETS = [ - { table: workflow, softDeleteCol: workflow.archivedAt, wsCol: workflow.workspaceId, name: 'workflow' }, - { table: workflowFolder, softDeleteCol: workflowFolder.archivedAt, wsCol: workflowFolder.workspaceId, name: 'workflowFolder' }, - { table: knowledgeBase, softDeleteCol: knowledgeBase.deletedAt, wsCol: knowledgeBase.workspaceId, name: 'knowledgeBase' }, - { table: userTableDefinitions, softDeleteCol: userTableDefinitions.archivedAt, wsCol: userTableDefinitions.workspaceId, name: 'userTableDefinitions' }, - { table: workspaceFile, softDeleteCol: workspaceFile.deletedAt, wsCol: workspaceFile.workspaceId, name: 'workspaceFile' }, - { table: workspaceFiles, softDeleteCol: workspaceFiles.deletedAt, wsCol: workspaceFiles.workspaceId, name: 'workspaceFiles' }, + { + table: workflow, + softDeleteCol: workflow.archivedAt, + wsCol: workflow.workspaceId, + name: 'workflow', + }, + { + table: workflowFolder, + softDeleteCol: workflowFolder.archivedAt, + wsCol: workflowFolder.workspaceId, + name: 'workflowFolder', + }, + { + table: knowledgeBase, + softDeleteCol: knowledgeBase.deletedAt, + wsCol: knowledgeBase.workspaceId, + name: 'knowledgeBase', + }, + { + table: userTableDefinitions, + softDeleteCol: userTableDefinitions.archivedAt, + wsCol: userTableDefinitions.workspaceId, + name: 'userTableDefinitions', + }, + { + table: workspaceFile, + softDeleteCol: workspaceFile.deletedAt, + wsCol: workspaceFile.workspaceId, + name: 'workspaceFile', + }, + { + table: workspaceFiles, + softDeleteCol: workspaceFiles.deletedAt, + wsCol: workspaceFiles.workspaceId, + name: 'workspaceFiles', + }, { table: memory, softDeleteCol: memory.deletedAt, wsCol: memory.workspaceId, name: 'memory' }, - { table: mcpServers, softDeleteCol: mcpServers.deletedAt, wsCol: mcpServers.workspaceId, name: 'mcpServers' }, - { table: workflowMcpServer, softDeleteCol: workflowMcpServer.deletedAt, wsCol: workflowMcpServer.workspaceId, name: 'workflowMcpServer' }, - { table: a2aAgent, softDeleteCol: a2aAgent.archivedAt, wsCol: a2aAgent.workspaceId, name: 'a2aAgent' }, + { + table: mcpServers, + softDeleteCol: mcpServers.deletedAt, + wsCol: mcpServers.workspaceId, + name: 'mcpServers', + }, + { + table: workflowMcpServer, + softDeleteCol: workflowMcpServer.deletedAt, + wsCol: workflowMcpServer.workspaceId, + name: 'workflowMcpServer', + }, + { + table: a2aAgent, + softDeleteCol: a2aAgent.archivedAt, + wsCol: a2aAgent.workspaceId, + name: 'a2aAgent', + }, ] as const async function resolvePayload(payload: CleanupJobPayload): Promise<{ diff --git a/apps/sim/background/cleanup-tasks.ts b/apps/sim/background/cleanup-tasks.ts index 2b8558cd935..f0a2369fbaa 100644 --- a/apps/sim/background/cleanup-tasks.ts +++ b/apps/sim/background/cleanup-tasks.ts @@ -86,7 +86,9 @@ async function cleanupRunChildren( const runIds = await db .select({ id: copilotRuns.id }) .from(copilotRuns) - .where(and(inArray(copilotRuns.workspaceId, workspaceIds), lt(copilotRuns.updatedAt, retentionDate))) + .where( + and(inArray(copilotRuns.workspaceId, workspaceIds), lt(copilotRuns.updatedAt, retentionDate)) + ) .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) if (runIds.length === 0) { @@ -172,25 +174,54 @@ async function resolvePayload(payload: CleanupJobPayload): Promise<{ } export async function runCleanupTasks(payload: CleanupJobPayload): Promise { - const startTime = Date.now() + const startTime = Date.now() - const resolved = await resolvePayload(payload) - if (!resolved) return + const resolved = await resolvePayload(payload) + if (!resolved) return - const { workspaceIds, retentionHours, tierLabel } = resolved + const { workspaceIds, retentionHours, tierLabel } = resolved - if (workspaceIds.length === 0) { - logger.info(`[${tierLabel}] No workspaces to process`) - return - } + if (workspaceIds.length === 0) { + logger.info(`[${tierLabel}] No workspaces to process`) + return + } - const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) - logger.info( - `[${tierLabel}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` + const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000) + logger.info( + `[${tierLabel}] Processing ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}` + ) + + // Collect chat IDs before deleting so we can clean up the copilot backend after + const doomedChats = await db + .select({ id: copilotChats.id }) + .from(copilotChats) + .where( + and( + inArray(copilotChats.workspaceId, workspaceIds), + lt(copilotChats.updatedAt, retentionDate) + ) ) + .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) + + const doomedChatIds = doomedChats.map((c) => c.id) + + // Prepare chat cleanup (collect file keys + copilot backend call) BEFORE DB deletion + const chatCleanup = await prepareChatCleanup(doomedChatIds, tierLabel) + + // Delete run children first (checkpoints, tool calls) since they reference runs + const runChildResults = await cleanupRunChildren(workspaceIds, retentionDate, tierLabel) + for (const r of runChildResults) { + if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) + } - // Collect chat IDs before deleting so we can clean up the copilot backend after - const doomedChats = await db + // Delete feedback — no direct workspaceId, find via copilotChats + const feedbackResult: TableCleanupResult = { + table: `${tierLabel}/copilotFeedback`, + deleted: 0, + failed: 0, + } + try { + const chatIds = await db .select({ id: copilotChats.id }) .from(copilotChats) .where( @@ -201,90 +232,66 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise ) .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) - const doomedChatIds = doomedChats.map((c) => c.id) - - // Prepare chat cleanup (collect file keys + copilot backend call) BEFORE DB deletion - const chatCleanup = await prepareChatCleanup(doomedChatIds, tierLabel) - - // Delete run children first (checkpoints, tool calls) since they reference runs - const runChildResults = await cleanupRunChildren(workspaceIds, retentionDate, tierLabel) - for (const r of runChildResults) { - if (r.deleted > 0) logger.info(`[${r.table}] ${r.deleted} deleted`) - } - - // Delete feedback — no direct workspaceId, find via copilotChats - const feedbackResult: TableCleanupResult = { - table: `${tierLabel}/copilotFeedback`, - deleted: 0, - failed: 0, - } - try { - const chatIds = await db - .select({ id: copilotChats.id }) - .from(copilotChats) + if (chatIds.length > 0) { + const deleted = await db + .delete(copilotFeedback) .where( - and( - inArray(copilotChats.workspaceId, workspaceIds), - lt(copilotChats.updatedAt, retentionDate) + inArray( + copilotFeedback.chatId, + chatIds.map((c) => c.id) ) ) - .limit(BATCH_SIZE * MAX_BATCHES_PER_TABLE) - - if (chatIds.length > 0) { - const deleted = await db - .delete(copilotFeedback) - .where(inArray(copilotFeedback.chatId, chatIds.map((c) => c.id))) - .returning({ id: copilotFeedback.feedbackId }) - feedbackResult.deleted = deleted.length - logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) - } else { - logger.info(`[${feedbackResult.table}] No expired rows found`) - } - } catch (error) { - feedbackResult.failed++ - logger.error(`[${feedbackResult.table}] Delete failed:`, { error }) + .returning({ id: copilotFeedback.feedbackId }) + feedbackResult.deleted = deleted.length + logger.info(`[${feedbackResult.table}] Deleted ${deleted.length} rows`) + } else { + logger.info(`[${feedbackResult.table}] No expired rows found`) } + } catch (error) { + feedbackResult.failed++ + logger.error(`[${feedbackResult.table}] Delete failed:`, { error }) + } - // Delete copilot runs (has workspaceId directly, cascades checkpoints) - const runsResult = await cleanupTable( - copilotRuns, - copilotRuns.workspaceId, - copilotRuns.updatedAt, - workspaceIds, - retentionDate, - `${tierLabel}/copilotRuns` - ) - - // Delete copilot chats (has workspaceId directly) - const chatsResult = await cleanupTable( - copilotChats, - copilotChats.workspaceId, - copilotChats.updatedAt, - workspaceIds, - retentionDate, - `${tierLabel}/copilotChats` - ) - - // Delete mothership inbox tasks (has workspaceId directly) - const inboxResult = await cleanupTable( - mothershipInboxTask, - mothershipInboxTask.workspaceId, - mothershipInboxTask.createdAt, - workspaceIds, - retentionDate, - `${tierLabel}/mothershipInboxTask` - ) - - const totalDeleted = - runChildResults.reduce((s, r) => s + r.deleted, 0) + - runsResult.deleted + - chatsResult.deleted + - inboxResult.deleted - - logger.info(`[${tierLabel}] Complete: ${totalDeleted} total rows deleted`) - - // Clean up copilot backend + storage files after DB rows are gone - await chatCleanup.execute() + // Delete copilot runs (has workspaceId directly, cascades checkpoints) + const runsResult = await cleanupTable( + copilotRuns, + copilotRuns.workspaceId, + copilotRuns.updatedAt, + workspaceIds, + retentionDate, + `${tierLabel}/copilotRuns` + ) + + // Delete copilot chats (has workspaceId directly) + const chatsResult = await cleanupTable( + copilotChats, + copilotChats.workspaceId, + copilotChats.updatedAt, + workspaceIds, + retentionDate, + `${tierLabel}/copilotChats` + ) + + // Delete mothership inbox tasks (has workspaceId directly) + const inboxResult = await cleanupTable( + mothershipInboxTask, + mothershipInboxTask.workspaceId, + mothershipInboxTask.createdAt, + workspaceIds, + retentionDate, + `${tierLabel}/mothershipInboxTask` + ) + + const totalDeleted = + runChildResults.reduce((s, r) => s + r.deleted, 0) + + runsResult.deleted + + chatsResult.deleted + + inboxResult.deleted + + logger.info(`[${tierLabel}] Complete: ${totalDeleted} total rows deleted`) + + // Clean up copilot backend + storage files after DB rows are gone + await chatCleanup.execute() const timeElapsed = (Date.now() - startTime) / 1000 logger.info(`Task cleanup completed in ${timeElapsed.toFixed(2)}s`) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index 059bdad1ba4..d9ba633188c 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -12,7 +12,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { cn } from '@/lib/core/utils/cn' import { type DataRetentionResponse, useUpdateWorkspaceRetention, @@ -186,7 +185,7 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work onChange={setSoftDeleteDays} disabled={false} /> -
-

- {planLabel(data.plan)} plan defaults. Upgrade to Enterprise to customize retention periods. + {PLAN_LABELS[data.plan]} plan defaults. Upgrade to Enterprise to customize retention + periods.

) @@ -160,7 +159,7 @@ function EditableView({ data, workspaceId }: { data: DataRetentionResponse; work setTimeout(() => setSaveSuccess(false), 3000) } catch (error) { logger.error('Failed to save data retention settings', { error }) - setSaveError(error instanceof Error ? error.message : 'Failed to save settings') + setSaveError(toError(error).message) } }, [workspaceId, logDays, softDeleteDays, taskCleanupDays]) diff --git a/apps/sim/ee/data-retention/hooks/data-retention.ts b/apps/sim/ee/data-retention/hooks/data-retention.ts index 610757c7607..f388934236b 100644 --- a/apps/sim/ee/data-retention/hooks/data-retention.ts +++ b/apps/sim/ee/data-retention/hooks/data-retention.ts @@ -1,6 +1,7 @@ 'use client' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import type { PlanCategory } from '@/lib/billing/plan-helpers' export interface RetentionValues { logRetentionHours: number | null @@ -9,7 +10,7 @@ export interface RetentionValues { } export interface DataRetentionResponse { - plan: 'free' | 'pro' | 'enterprise' + plan: PlanCategory isEnterprise: boolean defaults: RetentionValues configured: RetentionValues diff --git a/apps/sim/lib/billing/cleanup-dispatcher.ts b/apps/sim/lib/billing/cleanup-dispatcher.ts index 1610759dcd2..eac715a3909 100644 --- a/apps/sim/lib/billing/cleanup-dispatcher.ts +++ b/apps/sim/lib/billing/cleanup-dispatcher.ts @@ -1,13 +1,13 @@ import { db } from '@sim/db' import { subscription, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { tasks } from '@trigger.dev/sdk' -import { and, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' -import { sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' +import { and, eq, inArray, isNotNull, isNull } from 'drizzle-orm' +import { type PlanCategory, sqlIsPaid, sqlIsPro, sqlIsTeam } from '@/lib/billing/plan-helpers' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { getJobQueue } from '@/lib/core/async-jobs' import { shouldExecuteInline } from '@/lib/core/async-jobs/config' -import { toError } from '@/lib/core/utils/helpers' import { isTriggerAvailable } from '@/lib/knowledge/documents/service' const logger = createLogger('RetentionDispatcher') @@ -21,45 +21,47 @@ export type WorkspaceRetentionColumn = | 'softDeleteRetentionHours' | 'taskCleanupHours' +export type NonEnterprisePlan = Exclude + +const NON_ENTERPRISE_PLANS = ['free', 'pro', 'team'] as const satisfies readonly NonEnterprisePlan[] + export type CleanupJobPayload = - | { tier: 'free' } - | { tier: 'paid' } - | { tier: 'enterprise'; workspaceId: string } + | { plan: NonEnterprisePlan } + | { plan: 'enterprise'; workspaceId: string } -const FREE_RETENTION_DAYS = 7 -const PAID_RETENTION_DAYS = 30 +interface CleanupJobConfig { + column: WorkspaceRetentionColumn + defaults: Record +} + +const DAY = 24 /** - * Default retention per tier + column. Single source of truth — used by both the - * API (to show effective values in the UI) and background tasks (to apply cleanup). - * - * - Free: 7-day log + soft-delete retention. No task cleanup, no PII redaction. - * - Paid: 30-day soft-delete retention only. Logs kept forever. No task cleanup, no PII redaction. - * - Enterprise: everything defaults to NULL (never). Must be explicitly configured - * per-workspace. PII redaction is enterprise-only. + * Single source of truth for cleanup retention: which workspace column each job + * type inspects, and the default retention (in hours) per plan. Enterprise is + * always `null` here — enterprise tenants must set their own value per workspace. */ -export function getRetentionDefaultHours( - tier: 'free' | 'paid' | 'enterprise', - column: WorkspaceRetentionColumn -): number | null { - if (tier === 'enterprise') return null - - switch (column) { - case 'logRetentionHours': - return tier === 'free' ? FREE_RETENTION_DAYS * 24 : null // paid: never - case 'softDeleteRetentionHours': - return (tier === 'free' ? FREE_RETENTION_DAYS : PAID_RETENTION_DAYS) * 24 - case 'taskCleanupHours': - return null - } -} +export const CLEANUP_CONFIG = { + 'cleanup-logs': { + column: 'logRetentionHours', + defaults: { free: 7 * DAY, pro: null, team: null, enterprise: null }, + }, + 'cleanup-soft-deletes': { + column: 'softDeleteRetentionHours', + defaults: { free: 7 * DAY, pro: 30 * DAY, team: 30 * DAY, enterprise: null }, + }, + 'cleanup-tasks': { + column: 'taskCleanupHours', + defaults: { free: null, pro: null, team: null, enterprise: null }, + }, +} as const satisfies Record /** - * Resolve workspace IDs for a given non-enterprise tier. Used inside background - * tasks at run time so they always see fresh data. + * Bulk-lookup workspace IDs for a non-enterprise plan category. Enterprise is + * per-workspace (has explicit opt-in retention), so it's not handled here. */ -export async function resolveTierWorkspaceIds(tier: 'free' | 'paid'): Promise { - if (tier === 'free') { +export async function resolveWorkspaceIdsForPlan(plan: NonEnterprisePlan): Promise { + if (plan === 'free') { const rows = await db .select({ id: workspace.id }) .from(workspace) @@ -76,7 +78,7 @@ export async function resolveTierWorkspaceIds(tier: 'free' | 'paid'): Promise r.id) } - // paid (pro/team, excluding enterprise) + const planPredicate = plan === 'pro' ? sqlIsPro(subscription.plan) : sqlIsTeam(subscription.plan) const rows = await db .select({ id: workspace.id }) .from(workspace) @@ -85,7 +87,7 @@ export async function resolveTierWorkspaceIds(tier: 'free' | 'paid'): Promise r.id) } +export interface ResolvedCleanupScope { + workspaceIds: string[] + retentionHours: number + label: string +} + +/** + * Translate a queued cleanup payload into a concrete cleanup scope: the set of + * workspaces and the retention cutoff to apply. Returns `null` when the plan + * has no retention configured (default is null, or the enterprise workspace + * has not opted in). + */ +export async function resolveCleanupScope( + jobType: CleanupJobType, + payload: CleanupJobPayload +): Promise { + const config = CLEANUP_CONFIG[jobType] + + if (payload.plan !== 'enterprise') { + const retentionHours = config.defaults[payload.plan] + if (retentionHours === null) return null + const workspaceIds = await resolveWorkspaceIdsForPlan(payload.plan) + return { workspaceIds, retentionHours, label: payload.plan } + } + + const [ws] = await db + .select({ hours: workspace[config.column] }) + .from(workspace) + .where(eq(workspace.id, payload.workspaceId)) + .limit(1) + + if (!ws?.hours) return null + + return { + workspaceIds: [payload.workspaceId], + retentionHours: ws.hours, + label: `enterprise/${payload.workspaceId}`, + } +} + type RunnerFn = (payload: CleanupJobPayload) => Promise async function getInlineRunner(jobType: CleanupJobType): Promise { @@ -143,36 +185,33 @@ async function runInlineIfNeeded( } /** - * Dispatcher: enqueue cleanup jobs. + * Dispatcher: enqueue cleanup jobs driven by `CLEANUP_CONFIG`. * - * - 1 free-tier job (always) - * - 1 paid-tier job (always) - * - 1 enterprise job per workspace with non-NULL retention column + * - One job per non-enterprise plan with a non-null default + * - One enterprise job per workspace with a non-NULL retention value in the column * - * Uses Trigger.dev batchTrigger when available, otherwise parallel enqueue - * via the JobQueueBackend abstraction. On the database backend (no external worker), + * Uses Trigger.dev batchTrigger when available, otherwise parallel enqueue via + * the JobQueueBackend abstraction. On the database backend (no external worker), * jobs run inline in the same process via fire-and-forget promises. */ export async function dispatchCleanupJobs( - jobType: CleanupJobType, - retentionColumn: WorkspaceRetentionColumn + jobType: CleanupJobType ): Promise<{ jobIds: string[]; jobCount: number; enterpriseCount: number }> { + const config = CLEANUP_CONFIG[jobType] const jobQueue = await getJobQueue() const jobIds: string[] = [] - // Free + paid tier jobs (always dispatched, task returns early if no workspaces) - const freePayload: CleanupJobPayload = { tier: 'free' } - const freeJobId = await jobQueue.enqueue(jobType, freePayload) - jobIds.push(freeJobId) - await runInlineIfNeeded(jobQueue, jobType, freeJobId, freePayload) + const plansWithDefaults = NON_ENTERPRISE_PLANS.filter((plan) => config.defaults[plan] !== null) - const paidPayload: CleanupJobPayload = { tier: 'paid' } - const paidJobId = await jobQueue.enqueue(jobType, paidPayload) - jobIds.push(paidJobId) - await runInlineIfNeeded(jobQueue, jobType, paidJobId, paidPayload) + for (const plan of plansWithDefaults) { + const payload: CleanupJobPayload = { plan } + const jobId = await jobQueue.enqueue(jobType, payload) + jobIds.push(jobId) + await runInlineIfNeeded(jobQueue, jobType, jobId, payload) + } // Enterprise: query workspaces with non-NULL retention column - const retentionCol = workspace[retentionColumn] + const retentionCol = workspace[config.column] const enterpriseRows = await db .select({ id: workspace.id }) .from(workspace) @@ -188,8 +227,9 @@ export async function dispatchCleanupJobs( const enterpriseCount = enterpriseRows.length + const planLabels = plansWithDefaults.join('+') || 'none' logger.info( - `[${jobType}] Dispatching: 1 free + 1 paid + ${enterpriseCount} enterprise jobs (column: ${retentionColumn})` + `[${jobType}] Dispatching: plans=[${planLabels}] + ${enterpriseCount} enterprise jobs (column: ${config.column})` ) if (enterpriseCount === 0) { @@ -203,7 +243,7 @@ export async function dispatchCleanupJobs( const batchResult = await tasks.batchTrigger( jobType, chunk.map((row) => ({ - payload: { tier: 'enterprise' as const, workspaceId: row.id }, + payload: { plan: 'enterprise' as const, workspaceId: row.id }, options: { tags: [`workspaceId:${row.id}`, `jobType:${jobType}`], }, @@ -215,7 +255,7 @@ export async function dispatchCleanupJobs( // Fallback: parallel enqueue via abstraction const results = await Promise.allSettled( enterpriseRows.map(async (row) => { - const payload: CleanupJobPayload = { tier: 'enterprise', workspaceId: row.id } + const payload: CleanupJobPayload = { plan: 'enterprise', workspaceId: row.id } const jobId = await jobQueue.enqueue(jobType, payload) await runInlineIfNeeded(jobQueue, jobType, jobId, payload) return jobId diff --git a/apps/sim/lib/cleanup/batch-delete.ts b/apps/sim/lib/cleanup/batch-delete.ts new file mode 100644 index 00000000000..b894db6da4a --- /dev/null +++ b/apps/sim/lib/cleanup/batch-delete.ts @@ -0,0 +1,94 @@ +import { db } from '@sim/db' +import { createLogger } from '@sim/logger' +import { and, inArray, isNotNull, lt, sql } from 'drizzle-orm' +import type { PgColumn, PgTable } from 'drizzle-orm/pg-core' + +const logger = createLogger('BatchDelete') + +export const DEFAULT_BATCH_SIZE = 2000 +export const DEFAULT_MAX_BATCHES_PER_TABLE = 10 + +export interface TableCleanupResult { + table: string + deleted: number + failed: number +} + +export interface BatchDeleteOptions { + tableDef: PgTable + workspaceIdCol: PgColumn + timestampCol: PgColumn + workspaceIds: string[] + retentionDate: Date + tableName: string + /** When true, also requires `timestampCol IS NOT NULL` (soft-delete semantics). */ + requireTimestampNotNull?: boolean + batchSize?: number + maxBatches?: number +} + +/** + * Iteratively delete rows in a table matching a workspace + time-based predicate. + * + * Uses a SELECT-with-LIMIT → DELETE-by-ID pattern to keep each round bounded in + * memory and I/O (PostgreSQL DELETE does not support LIMIT directly). + */ +export async function batchDeleteByWorkspaceAndTimestamp({ + tableDef, + workspaceIdCol, + timestampCol, + workspaceIds, + retentionDate, + tableName, + requireTimestampNotNull = false, + batchSize = DEFAULT_BATCH_SIZE, + maxBatches = DEFAULT_MAX_BATCHES_PER_TABLE, +}: BatchDeleteOptions): Promise { + const result: TableCleanupResult = { table: tableName, deleted: 0, failed: 0 } + + if (workspaceIds.length === 0) { + logger.info(`[${tableName}] Skipped — no workspaces in scope`) + return result + } + + const predicates = [inArray(workspaceIdCol, workspaceIds), lt(timestampCol, retentionDate)] + if (requireTimestampNotNull) predicates.push(isNotNull(timestampCol)) + const whereClause = and(...predicates) + + let batchesProcessed = 0 + let hasMore = true + + while (hasMore && batchesProcessed < maxBatches) { + try { + const batch = await db + .select({ id: sql`id` }) + .from(tableDef) + .where(whereClause) + .limit(batchSize) + + if (batch.length === 0) { + logger.info(`[${tableName}] No expired rows found`) + hasMore = false + break + } + + const ids = batch.map((r) => r.id) + const deleted = await db + .delete(tableDef) + .where(inArray(sql`id`, ids)) + .returning({ id: sql`id` }) + + result.deleted += deleted.length + hasMore = batch.length === batchSize + batchesProcessed++ + + logger.info(`[${tableName}] Batch ${batchesProcessed}: deleted ${deleted.length} rows`) + } catch (error) { + result.failed++ + logger.error(`[${tableName}] Batch delete failed:`, { error }) + hasMore = false + } + } + + return result +} diff --git a/apps/sim/lib/cleanup/chat-cleanup.ts b/apps/sim/lib/cleanup/chat-cleanup.ts index d4a2363a9e6..c515c10dcfd 100644 --- a/apps/sim/lib/cleanup/chat-cleanup.ts +++ b/apps/sim/lib/cleanup/chat-cleanup.ts @@ -29,11 +29,16 @@ export async function collectChatFiles(chatIds: string[]): Promise { const seen = new Set() - // Source 1: workspaceFiles linked via chatId (includes context column) - const linkedFiles = await db - .select({ key: workspaceFiles.key, context: workspaceFiles.context }) - .from(workspaceFiles) - .where(and(inArray(workspaceFiles.chatId, chatIds), isNull(workspaceFiles.deletedAt))) + const [linkedFiles, chatsWithMessages] = await Promise.all([ + db + .select({ key: workspaceFiles.key, context: workspaceFiles.context }) + .from(workspaceFiles) + .where(and(inArray(workspaceFiles.chatId, chatIds), isNull(workspaceFiles.deletedAt))), + db + .select({ messages: copilotChats.messages }) + .from(copilotChats) + .where(inArray(copilotChats.id, chatIds)), + ]) for (const f of linkedFiles) { if (!seen.has(f.key)) { @@ -42,12 +47,6 @@ export async function collectChatFiles(chatIds: string[]): Promise { } } - // Source 2: fileAttachments in messages JSONB (always context='copilot') - const chatsWithMessages = await db - .select({ messages: copilotChats.messages }) - .from(copilotChats) - .where(inArray(copilotChats.id, chatIds)) - for (const chat of chatsWithMessages) { const messages = chat.messages as unknown[] if (!Array.isArray(messages)) continue @@ -79,21 +78,22 @@ export async function collectChatFiles(chatIds: string[]): Promise { */ export async function deleteStorageFiles( files: FileRef[], - tierLabel: string + label: string ): Promise<{ filesDeleted: number; filesFailed: number }> { const stats = { filesDeleted: 0, filesFailed: 0 } if (files.length === 0 || !isUsingCloudStorage()) return stats - for (const file of files) { - try { - logger.info(`[${tierLabel}] Deleting storage file: ${file.key} (context: ${file.context})`) - await StorageService.deleteFile({ key: file.key, context: file.context }) - stats.filesDeleted++ - } catch (error) { - stats.filesFailed++ - logger.error(`[${tierLabel}] Failed to delete storage file ${file.key}:`, { error }) - } - } + await Promise.all( + files.map(async (file) => { + try { + await StorageService.deleteFile({ key: file.key, context: file.context }) + stats.filesDeleted++ + } catch (error) { + stats.filesFailed++ + logger.error(`[${label}] Failed to delete storage file ${file.key}:`, { error }) + } + }) + ) return stats } @@ -104,13 +104,13 @@ export async function deleteStorageFiles( */ export async function cleanupCopilotBackend( chatIds: string[], - tierLabel: string + label: string ): Promise<{ deleted: number; failed: number }> { const stats = { deleted: 0, failed: 0 } if (chatIds.length === 0 || !env.COPILOT_API_KEY) { if (!env.COPILOT_API_KEY) { - logger.warn(`[${tierLabel}] COPILOT_API_KEY not set, skipping copilot backend cleanup`) + logger.warn(`[${label}] COPILOT_API_KEY not set, skipping copilot backend cleanup`) } return stats } @@ -129,7 +129,7 @@ export async function cleanupCopilotBackend( if (!response.ok) { const errorBody = await response.text().catch(() => '') - logger.error(`[${tierLabel}] Copilot backend cleanup failed: ${response.status}`, { + logger.error(`[${label}] Copilot backend cleanup failed: ${response.status}`, { errorBody, chatCount: chunk.length, }) @@ -140,11 +140,11 @@ export async function cleanupCopilotBackend( const result = await response.json() stats.deleted += result.deleted ?? 0 logger.info( - `[${tierLabel}] Copilot backend cleanup: ${result.deleted} chats deleted (batch ${Math.floor(i / COPILOT_CLEANUP_BATCH_SIZE) + 1})` + `[${label}] Copilot backend cleanup: ${result.deleted} chats deleted (batch ${Math.floor(i / COPILOT_CLEANUP_BATCH_SIZE) + 1})` ) } catch (error) { stats.failed += chunk.length - logger.error(`[${tierLabel}] Copilot backend cleanup request failed:`, { error }) + logger.error(`[${label}] Copilot backend cleanup request failed:`, { error }) } } @@ -156,18 +156,18 @@ export async function cleanupCopilotBackend( * call copilot backend and delete storage files. * * Usage: - * const cleanup = await prepareChatCleanup(chatIds, tierLabel) + * const cleanup = await prepareChatCleanup(chatIds, label) * // ... delete DB rows ... * await cleanup.execute() */ export async function prepareChatCleanup( chatIds: string[], - tierLabel: string + label: string ): Promise<{ execute: () => Promise }> { // Collect file refs BEFORE DB deletion (keys + context are lost after cascade) const files = await collectChatFiles(chatIds) if (files.length > 0) { - logger.info(`[${tierLabel}] Collected ${files.length} files for cleanup`, { + logger.info(`[${label}] Collected ${files.length} files for cleanup`, { files: files.map((f) => ({ key: f.key, context: f.context })), }) } @@ -176,17 +176,17 @@ export async function prepareChatCleanup( execute: async () => { // Call copilot backend if (chatIds.length > 0) { - const copilotResult = await cleanupCopilotBackend(chatIds, tierLabel) + const copilotResult = await cleanupCopilotBackend(chatIds, label) logger.info( - `[${tierLabel}] Copilot backend: ${copilotResult.deleted} deleted, ${copilotResult.failed} failed` + `[${label}] Copilot backend: ${copilotResult.deleted} deleted, ${copilotResult.failed} failed` ) } // Delete storage files with correct context per file if (files.length > 0) { - const fileStats = await deleteStorageFiles(files, tierLabel) + const fileStats = await deleteStorageFiles(files, label) logger.info( - `[${tierLabel}] Storage cleanup: ${fileStats.filesDeleted} deleted, ${fileStats.filesFailed} failed` + `[${label}] Storage cleanup: ${fileStats.filesDeleted} deleted, ${fileStats.filesFailed} failed` ) } }, From 990d56aad757a63bc3ad084c1387f068eb1701a0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 18 Apr 2026 19:26:41 -0700 Subject: [PATCH 15/15] Handle falsy check, correctly clean up file storage --- apps/sim/background/cleanup-soft-deletes.ts | 47 ++++++++++++++----- .../components/data-retention-settings.tsx | 9 +++- apps/sim/lib/billing/cleanup-dispatcher.ts | 2 +- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/apps/sim/background/cleanup-soft-deletes.ts b/apps/sim/background/cleanup-soft-deletes.ts index a32374bd380..59127d8a94b 100644 --- a/apps/sim/background/cleanup-soft-deletes.ts +++ b/apps/sim/background/cleanup-soft-deletes.ts @@ -22,6 +22,7 @@ import { DEFAULT_MAX_BATCHES_PER_TABLE, } from '@/lib/cleanup/batch-delete' import { prepareChatCleanup } from '@/lib/cleanup/chat-cleanup' +import type { StorageContext } from '@/lib/uploads' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' const logger = createLogger('CleanupSoftDeletes') @@ -34,26 +35,46 @@ async function cleanupWorkspaceFileStorage( if (!isUsingCloudStorage() || workspaceIds.length === 0) return stats - const filesToDelete = await db - .select({ key: workspaceFiles.key }) - .from(workspaceFiles) - .where( - and( - inArray(workspaceFiles.workspaceId, workspaceIds), - isNotNull(workspaceFiles.deletedAt), - lt(workspaceFiles.deletedAt, retentionDate) + const limit = DEFAULT_BATCH_SIZE * DEFAULT_MAX_BATCHES_PER_TABLE + + const [legacyFiles, multiContextFiles] = await Promise.all([ + db + .select({ key: workspaceFile.key }) + .from(workspaceFile) + .where( + and( + inArray(workspaceFile.workspaceId, workspaceIds), + isNotNull(workspaceFile.deletedAt), + lt(workspaceFile.deletedAt, retentionDate) + ) ) - ) - .limit(DEFAULT_BATCH_SIZE * DEFAULT_MAX_BATCHES_PER_TABLE) + .limit(limit), + db + .select({ key: workspaceFiles.key, context: workspaceFiles.context }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.workspaceId, workspaceIds), + isNotNull(workspaceFiles.deletedAt), + lt(workspaceFiles.deletedAt, retentionDate) + ) + ) + .limit(limit), + ]) + + const toDelete: Array<{ key: string; context: StorageContext }> = [ + ...legacyFiles.map((f) => ({ key: f.key, context: 'workspace' as StorageContext })), + ...multiContextFiles.map((f) => ({ key: f.key, context: f.context as StorageContext })), + ] await Promise.all( - filesToDelete.map(async (file) => { + toDelete.map(async ({ key, context }) => { try { - await StorageService.deleteFile({ key: file.key, context: 'workspace' }) + await StorageService.deleteFile({ key, context }) stats.filesDeleted++ } catch (error) { stats.filesFailed++ - logger.error(`Failed to delete storage file ${file.key}:`, { error }) + logger.error(`Failed to delete storage file ${key} (context: ${context}):`, { error }) } }) ) diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index d86eba2386c..1326fe477be 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -45,6 +45,11 @@ interface RetentionFieldProps { } function RetentionField({ label, description, value, onChange, disabled }: RetentionFieldProps) { + const standard = DAY_OPTIONS.find((o) => o.value === value) + const options = standard + ? DAY_OPTIONS + : [...DAY_OPTIONS, { value, label: `${value} days (custom)` } as const] + return (
@@ -53,7 +58,7 @@ function RetentionField({ label, description, value, onChange, disabled }: Reten
{disabled ? ( - {DAY_OPTIONS.find((o) => o.value === value)?.label ?? `${value} days`} + {standard?.label ?? `${value} days`} ) : (