import { type SQL, sql } from 'drizzle-orm' import { bigint, boolean, check, customType, decimal, index, integer, json, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid, vector, } from 'drizzle-orm/pg-core' import { DEFAULT_FREE_CREDITS, TAG_SLOTS } from './consts' // Custom tsvector type for full-text search export const tsvector = customType<{ data: string }>({ dataType() { return `tsvector` }, }) export const user = pgTable('user', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), emailVerified: boolean('email_verified').notNull(), image: text('image'), createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), stripeCustomerId: text('stripe_customer_id'), isSuperUser: boolean('is_super_user').notNull().default(false), }) export const session = pgTable( 'session', { id: text('id').primaryKey(), expiresAt: timestamp('expires_at').notNull(), token: text('token').notNull().unique(), createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), activeOrganizationId: text('active_organization_id').references(() => organization.id, { onDelete: 'set null', }), }, (table) => ({ userIdIdx: index('session_user_id_idx').on(table.userId), tokenIdx: index('session_token_idx').on(table.token), }) ) export const account = pgTable( 'account', { id: text('id').primaryKey(), accountId: text('account_id').notNull(), providerId: text('provider_id').notNull(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), accessToken: text('access_token'), refreshToken: text('refresh_token'), idToken: text('id_token'), accessTokenExpiresAt: timestamp('access_token_expires_at'), refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), scope: text('scope'), password: text('password'), createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), }, (table) => ({ userIdIdx: index('account_user_id_idx').on(table.userId), }) ) export const verification = pgTable( 'verification', { id: text('id').primaryKey(), identifier: text('identifier').notNull(), value: text('value').notNull(), expiresAt: timestamp('expires_at').notNull(), createdAt: timestamp('created_at'), updatedAt: timestamp('updated_at'), }, (table) => ({ identifierIdx: index('verification_identifier_idx').on(table.identifier), }) ) export const workflowFolder = pgTable( 'workflow_folder', { id: text('id').primaryKey(), name: text('name').notNull(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), parentId: text('parent_id'), // Self-reference will be handled by foreign key constraint color: text('color').default('#6B7280'), isExpanded: boolean('is_expanded').notNull().default(true), sortOrder: integer('sort_order').notNull().default(0), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ userIdx: index('workflow_folder_user_idx').on(table.userId), workspaceParentIdx: index('workflow_folder_workspace_parent_idx').on( table.workspaceId, table.parentId ), parentSortIdx: index('workflow_folder_parent_sort_idx').on(table.parentId, table.sortOrder), }) ) export const workflow = pgTable( 'workflow', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), folderId: text('folder_id').references(() => workflowFolder.id, { onDelete: 'set null' }), name: text('name').notNull(), description: text('description'), color: text('color').notNull().default('#3972F6'), lastSynced: timestamp('last_synced').notNull(), createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), isDeployed: boolean('is_deployed').notNull().default(false), deployedAt: timestamp('deployed_at'), runCount: integer('run_count').notNull().default(0), lastRunAt: timestamp('last_run_at'), variables: json('variables').default('{}'), }, (table) => ({ userIdIdx: index('workflow_user_id_idx').on(table.userId), workspaceIdIdx: index('workflow_workspace_id_idx').on(table.workspaceId), userWorkspaceIdx: index('workflow_user_workspace_idx').on(table.userId, table.workspaceId), }) ) export const workflowBlocks = pgTable( 'workflow_blocks', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), type: text('type').notNull(), // 'starter', 'agent', 'api', 'function' name: text('name').notNull(), positionX: decimal('position_x').notNull(), positionY: decimal('position_y').notNull(), enabled: boolean('enabled').notNull().default(true), horizontalHandles: boolean('horizontal_handles').notNull().default(true), isWide: boolean('is_wide').notNull().default(false), advancedMode: boolean('advanced_mode').notNull().default(false), triggerMode: boolean('trigger_mode').notNull().default(false), height: decimal('height').notNull().default('0'), subBlocks: jsonb('sub_blocks').notNull().default('{}'), outputs: jsonb('outputs').notNull().default('{}'), data: jsonb('data').default('{}'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_blocks_workflow_id_idx').on(table.workflowId), workflowTypeIdx: index('workflow_blocks_workflow_type_idx').on(table.workflowId, table.type), }) ) export const workflowEdges = pgTable( 'workflow_edges', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), sourceBlockId: text('source_block_id') .notNull() .references(() => workflowBlocks.id, { onDelete: 'cascade' }), targetBlockId: text('target_block_id') .notNull() .references(() => workflowBlocks.id, { onDelete: 'cascade' }), sourceHandle: text('source_handle'), targetHandle: text('target_handle'), createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_edges_workflow_id_idx').on(table.workflowId), workflowSourceIdx: index('workflow_edges_workflow_source_idx').on( table.workflowId, table.sourceBlockId ), workflowTargetIdx: index('workflow_edges_workflow_target_idx').on( table.workflowId, table.targetBlockId ), }) ) export const workflowSubflows = pgTable( 'workflow_subflows', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), type: text('type').notNull(), // 'loop' or 'parallel' config: jsonb('config').notNull().default('{}'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_subflows_workflow_id_idx').on(table.workflowId), workflowTypeIdx: index('workflow_subflows_workflow_type_idx').on(table.workflowId, table.type), }) ) export const waitlist = pgTable('waitlist', { id: text('id').primaryKey(), email: text('email').notNull().unique(), status: text('status').notNull().default('pending'), // pending, approved, rejected createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const workflowExecutionSnapshots = pgTable( 'workflow_execution_snapshots', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), stateHash: text('state_hash').notNull(), stateData: jsonb('state_data').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_snapshots_workflow_id_idx').on(table.workflowId), stateHashIdx: index('workflow_snapshots_hash_idx').on(table.stateHash), workflowHashUnique: uniqueIndex('workflow_snapshots_workflow_hash_idx').on( table.workflowId, table.stateHash ), createdAtIdx: index('workflow_snapshots_created_at_idx').on(table.createdAt), }) ) export const workflowExecutionLogs = pgTable( 'workflow_execution_logs', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), executionId: text('execution_id').notNull(), stateSnapshotId: text('state_snapshot_id') .notNull() .references(() => workflowExecutionSnapshots.id), level: text('level').notNull(), // 'info', 'error' trigger: text('trigger').notNull(), // 'api', 'webhook', 'schedule', 'manual', 'chat' startedAt: timestamp('started_at').notNull(), endedAt: timestamp('ended_at'), totalDurationMs: integer('total_duration_ms'), executionData: jsonb('execution_data').notNull().default('{}'), cost: jsonb('cost'), files: jsonb('files'), // File metadata for execution files createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_execution_logs_workflow_id_idx').on(table.workflowId), executionIdIdx: index('workflow_execution_logs_execution_id_idx').on(table.executionId), stateSnapshotIdIdx: index('workflow_execution_logs_state_snapshot_id_idx').on( table.stateSnapshotId ), triggerIdx: index('workflow_execution_logs_trigger_idx').on(table.trigger), levelIdx: index('workflow_execution_logs_level_idx').on(table.level), startedAtIdx: index('workflow_execution_logs_started_at_idx').on(table.startedAt), executionIdUnique: uniqueIndex('workflow_execution_logs_execution_id_unique').on( table.executionId ), // Composite index for the new join-based query pattern workflowStartedAtIdx: index('workflow_execution_logs_workflow_started_at_idx').on( table.workflowId, table.startedAt ), }) ) export const pausedExecutions = pgTable( 'paused_executions', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), executionId: text('execution_id').notNull(), executionSnapshot: jsonb('execution_snapshot').notNull(), pausePoints: jsonb('pause_points').notNull(), totalPauseCount: integer('total_pause_count').notNull(), resumedCount: integer('resumed_count').notNull().default(0), status: text('status').notNull().default('paused'), metadata: jsonb('metadata').notNull().default(sql`'{}'::jsonb`), pausedAt: timestamp('paused_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), expiresAt: timestamp('expires_at'), }, (table) => ({ workflowIdx: index('paused_executions_workflow_id_idx').on(table.workflowId), statusIdx: index('paused_executions_status_idx').on(table.status), executionUnique: uniqueIndex('paused_executions_execution_id_unique').on(table.executionId), }) ) export const resumeQueue = pgTable( 'resume_queue', { id: text('id').primaryKey(), pausedExecutionId: text('paused_execution_id') .notNull() .references(() => pausedExecutions.id, { onDelete: 'cascade' }), parentExecutionId: text('parent_execution_id').notNull(), newExecutionId: text('new_execution_id').notNull(), contextId: text('context_id').notNull(), resumeInput: jsonb('resume_input'), status: text('status').notNull().default('pending'), queuedAt: timestamp('queued_at').notNull().defaultNow(), claimedAt: timestamp('claimed_at'), completedAt: timestamp('completed_at'), failureReason: text('failure_reason'), }, (table) => ({ parentStatusIdx: index('resume_queue_parent_status_idx').on( table.parentExecutionId, table.status, table.queuedAt ), newExecutionIdx: index('resume_queue_new_execution_idx').on(table.newExecutionId), }) ) export const environment = pgTable('environment', { id: text('id').primaryKey(), // Use the user id as the key userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }) .unique(), // One environment per user variables: json('variables').notNull(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const workspaceEnvironment = pgTable( 'workspace_environment', { id: text('id').primaryKey(), workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), variables: json('variables').notNull().default('{}'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ workspaceUnique: uniqueIndex('workspace_environment_workspace_unique').on(table.workspaceId), }) ) export const settings = pgTable('settings', { id: text('id').primaryKey(), // Use the user id as the key userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }) .unique(), // One settings record per user // General settings theme: text('theme').notNull().default('system'), autoConnect: boolean('auto_connect').notNull().default(true), autoFillEnvVars: boolean('auto_fill_env_vars').notNull().default(true), // DEPRECATED: autofill feature removed autoPan: boolean('auto_pan').notNull().default(true), consoleExpandedByDefault: boolean('console_expanded_by_default').notNull().default(true), // Privacy settings telemetryEnabled: boolean('telemetry_enabled').notNull().default(true), // Email preferences emailPreferences: json('email_preferences').notNull().default('{}'), // Billing usage notifications preference billingUsageNotificationsEnabled: boolean('billing_usage_notifications_enabled') .notNull() .default(true), // UI preferences showFloatingControls: boolean('show_floating_controls').notNull().default(true), showTrainingControls: boolean('show_training_controls').notNull().default(false), superUserModeEnabled: boolean('super_user_mode_enabled').notNull().default(true), // Copilot preferences - maps model_id to enabled/disabled boolean copilotEnabledModels: jsonb('copilot_enabled_models').notNull().default('{}'), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const workflowSchedule = pgTable( 'workflow_schedule', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), blockId: text('block_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), cronExpression: text('cron_expression'), nextRunAt: timestamp('next_run_at'), lastRanAt: timestamp('last_ran_at'), triggerType: text('trigger_type').notNull(), // "manual", "webhook", "schedule" timezone: text('timezone').notNull().default('UTC'), failedCount: integer('failed_count').notNull().default(0), // Track consecutive failures status: text('status').notNull().default('active'), // 'active' or 'disabled' lastFailedAt: timestamp('last_failed_at'), // When the schedule last failed createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => { return { workflowBlockUnique: uniqueIndex('workflow_schedule_workflow_block_unique').on( table.workflowId, table.blockId ), } } ) export const webhook = pgTable( 'webhook', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), blockId: text('block_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), // ID of the webhook trigger block (nullable for legacy starter block webhooks) path: text('path').notNull(), provider: text('provider'), // e.g., "whatsapp", "github", etc. providerConfig: json('provider_config'), // Store provider-specific configuration isActive: boolean('is_active').notNull().default(true), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => { return { // Ensure webhook paths are unique pathIdx: uniqueIndex('path_idx').on(table.path), } } ) export const workflowLogWebhook = pgTable( 'workflow_log_webhook', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), url: text('url').notNull(), secret: text('secret'), includeFinalOutput: boolean('include_final_output').notNull().default(false), includeTraceSpans: boolean('include_trace_spans').notNull().default(false), includeRateLimits: boolean('include_rate_limits').notNull().default(false), includeUsageData: boolean('include_usage_data').notNull().default(false), levelFilter: text('level_filter') .array() .notNull() .default(sql`ARRAY['info', 'error']::text[]`), triggerFilter: text('trigger_filter') .array() .notNull() .default(sql`ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]`), active: boolean('active').notNull().default(true), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ workflowIdIdx: index('workflow_log_webhook_workflow_id_idx').on(table.workflowId), activeIdx: index('workflow_log_webhook_active_idx').on(table.active), }) ) export const webhookDeliveryStatusEnum = pgEnum('webhook_delivery_status', [ 'pending', 'in_progress', 'success', 'failed', ]) export const workflowLogWebhookDelivery = pgTable( 'workflow_log_webhook_delivery', { id: text('id').primaryKey(), subscriptionId: text('subscription_id') .notNull() .references(() => workflowLogWebhook.id, { onDelete: 'cascade' }), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), executionId: text('execution_id').notNull(), status: webhookDeliveryStatusEnum('status').notNull().default('pending'), attempts: integer('attempts').notNull().default(0), lastAttemptAt: timestamp('last_attempt_at'), nextAttemptAt: timestamp('next_attempt_at'), responseStatus: integer('response_status'), responseBody: text('response_body'), errorMessage: text('error_message'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ subscriptionIdIdx: index('workflow_log_webhook_delivery_subscription_id_idx').on( table.subscriptionId ), executionIdIdx: index('workflow_log_webhook_delivery_execution_id_idx').on(table.executionId), statusIdx: index('workflow_log_webhook_delivery_status_idx').on(table.status), nextAttemptIdx: index('workflow_log_webhook_delivery_next_attempt_idx').on(table.nextAttemptAt), }) ) export const apiKey = pgTable( 'api_key', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), // Only set for workspace keys createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), // Who created the workspace key name: text('name').notNull(), key: text('key').notNull().unique(), type: text('type').notNull().default('personal'), // 'personal' or 'workspace' lastUsed: timestamp('last_used'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), expiresAt: timestamp('expires_at'), }, (table) => ({ // Ensure workspace keys have a workspace_id and personal keys don't workspaceTypeCheck: check( 'workspace_type_check', sql`(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)` ), }) ) export const marketplace = pgTable('marketplace', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), state: json('state').notNull(), name: text('name').notNull(), description: text('description'), authorId: text('author_id') .notNull() .references(() => user.id), authorName: text('author_name').notNull(), views: integer('views').notNull().default(0), category: text('category'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const userStats = pgTable('user_stats', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }) .unique(), // One record per user totalManualExecutions: integer('total_manual_executions').notNull().default(0), totalApiCalls: integer('total_api_calls').notNull().default(0), totalWebhookTriggers: integer('total_webhook_triggers').notNull().default(0), totalScheduledExecutions: integer('total_scheduled_executions').notNull().default(0), totalChatExecutions: integer('total_chat_executions').notNull().default(0), totalTokensUsed: integer('total_tokens_used').notNull().default(0), totalCost: decimal('total_cost').notNull().default('0'), currentUsageLimit: decimal('current_usage_limit').default(DEFAULT_FREE_CREDITS.toString()), // Default $10 for free plan, null for team/enterprise usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(), // Billing period tracking currentPeriodCost: decimal('current_period_cost').notNull().default('0'), // Usage in current billing period lastPeriodCost: decimal('last_period_cost').default('0'), // Usage from previous billing period billedOverageThisPeriod: decimal('billed_overage_this_period').notNull().default('0'), // Amount of overage already billed via threshold billing // Pro usage snapshot when joining a team (to prevent double-billing) proPeriodCostSnapshot: decimal('pro_period_cost_snapshot').default('0'), // Snapshot of Pro usage when joining team // Copilot usage tracking totalCopilotCost: decimal('total_copilot_cost').notNull().default('0'), currentPeriodCopilotCost: decimal('current_period_copilot_cost').notNull().default('0'), lastPeriodCopilotCost: decimal('last_period_copilot_cost').default('0'), totalCopilotTokens: integer('total_copilot_tokens').notNull().default(0), totalCopilotCalls: integer('total_copilot_calls').notNull().default(0), // Storage tracking (for free/pro users) storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), lastActive: timestamp('last_active').notNull().defaultNow(), billingBlocked: boolean('billing_blocked').notNull().default(false), }) export const customTools = pgTable( 'custom_tools', { id: text('id').primaryKey(), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), userId: text('user_id').references(() => user.id, { onDelete: 'set null' }), title: text('title').notNull(), schema: json('schema').notNull(), code: text('code').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ workspaceIdIdx: index('custom_tools_workspace_id_idx').on(table.workspaceId), }) ) export const subscription = pgTable( 'subscription', { id: text('id').primaryKey(), plan: text('plan').notNull(), referenceId: text('reference_id').notNull(), stripeCustomerId: text('stripe_customer_id'), stripeSubscriptionId: text('stripe_subscription_id'), status: text('status'), periodStart: timestamp('period_start'), periodEnd: timestamp('period_end'), cancelAtPeriodEnd: boolean('cancel_at_period_end'), seats: integer('seats'), trialStart: timestamp('trial_start'), trialEnd: timestamp('trial_end'), metadata: json('metadata'), }, (table) => ({ referenceStatusIdx: index('subscription_reference_status_idx').on( table.referenceId, table.status ), enterpriseMetadataCheck: check( 'check_enterprise_metadata', sql`plan != 'enterprise' OR metadata IS NOT NULL` ), }) ) export const userRateLimits = pgTable('user_rate_limits', { referenceId: text('reference_id').primaryKey(), // Can be userId or organizationId for pooling syncApiRequests: integer('sync_api_requests').notNull().default(0), // Sync API requests counter asyncApiRequests: integer('async_api_requests').notNull().default(0), // Async API requests counter apiEndpointRequests: integer('api_endpoint_requests').notNull().default(0), // External API endpoint requests counter windowStart: timestamp('window_start').notNull().defaultNow(), lastRequestAt: timestamp('last_request_at').notNull().defaultNow(), isRateLimited: boolean('is_rate_limited').notNull().default(false), rateLimitResetAt: timestamp('rate_limit_reset_at'), }) export const chat = pgTable( 'chat', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), identifier: text('identifier').notNull(), title: text('title').notNull(), description: text('description'), isActive: boolean('is_active').notNull().default(true), customizations: json('customizations').default('{}'), // For UI customization options // Authentication options authType: text('auth_type').notNull().default('public'), // 'public', 'password', 'email', 'sso' password: text('password'), // Stored hashed, populated when authType is 'password' allowedEmails: json('allowed_emails').default('[]'), // Array of allowed emails or domains when authType is 'email' or 'sso' // Output configuration outputConfigs: json('output_configs').default('[]'), // Array of {blockId, path} objects createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => { return { // Ensure identifiers are unique identifierIdx: uniqueIndex('identifier_idx').on(table.identifier), } } ) export const organization = pgTable('organization', { id: text('id').primaryKey(), name: text('name').notNull(), slug: text('slug').notNull(), logo: text('logo'), metadata: json('metadata'), orgUsageLimit: decimal('org_usage_limit'), storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), // Storage tracking for team/enterprise createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export const member = pgTable( 'member', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), organizationId: text('organization_id') .notNull() .references(() => organization.id, { onDelete: 'cascade' }), role: text('role').notNull(), // 'admin' or 'member' - team-level permissions only createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ userIdIdx: index('member_user_id_idx').on(table.userId), organizationIdIdx: index('member_organization_id_idx').on(table.organizationId), }) ) export const invitation = pgTable( 'invitation', { id: text('id').primaryKey(), email: text('email').notNull(), inviterId: text('inviter_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), organizationId: text('organization_id') .notNull() .references(() => organization.id, { onDelete: 'cascade' }), role: text('role').notNull(), status: text('status').notNull(), expiresAt: timestamp('expires_at').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ emailIdx: index('invitation_email_idx').on(table.email), organizationIdIdx: index('invitation_organization_id_idx').on(table.organizationId), }) ) export const workspace = pgTable('workspace', { id: text('id').primaryKey(), name: text('name').notNull(), ownerId: text('owner_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), billedAccountUserId: text('billed_account_user_id') .notNull() .references(() => user.id, { onDelete: 'no action' }), allowPersonalApiKeys: boolean('allow_personal_api_keys').notNull().default(true), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const workspaceFile = pgTable( 'workspace_file', { id: text('id').primaryKey(), workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), name: text('name').notNull(), key: text('key').notNull().unique(), size: integer('size').notNull(), type: text('type').notNull(), uploadedBy: text('uploaded_by') .notNull() .references(() => user.id, { onDelete: 'cascade' }), uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), }, (table) => ({ workspaceIdIdx: index('workspace_file_workspace_id_idx').on(table.workspaceId), keyIdx: index('workspace_file_key_idx').on(table.key), }) ) export const workspaceFiles = pgTable( 'workspace_files', { id: text('id').primaryKey(), key: text('key').notNull().unique(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), context: text('context').notNull(), // 'workspace', 'copilot', 'chat', 'knowledge-base', 'profile-pictures', 'general', 'execution' originalName: text('original_name').notNull(), contentType: text('content_type').notNull(), size: integer('size').notNull(), uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), }, (table) => ({ keyIdx: index('workspace_files_key_idx').on(table.key), userIdIdx: index('workspace_files_user_id_idx').on(table.userId), workspaceIdIdx: index('workspace_files_workspace_id_idx').on(table.workspaceId), contextIdx: index('workspace_files_context_idx').on(table.context), }) ) export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read']) export const workspaceInvitationStatusEnum = pgEnum('workspace_invitation_status', [ 'pending', 'accepted', 'rejected', 'cancelled', ]) export type WorkspaceInvitationStatus = (typeof workspaceInvitationStatusEnum.enumValues)[number] export const workspaceInvitation = pgTable('workspace_invitation', { id: text('id').primaryKey(), workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), email: text('email').notNull(), inviterId: text('inviter_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), role: text('role').notNull().default('member'), status: workspaceInvitationStatusEnum('status').notNull().default('pending'), token: text('token').notNull().unique(), permissions: permissionTypeEnum('permissions').notNull().default('admin'), orgInvitationId: text('org_invitation_id'), expiresAt: timestamp('expires_at').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }) export const permissions = pgTable( 'permissions', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), entityType: text('entity_type').notNull(), // 'workspace', 'workflow', 'organization', etc. entityId: text('entity_id').notNull(), // ID of the workspace, workflow, etc. permissionType: permissionTypeEnum('permission_type').notNull(), // Use enum instead of text createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access pattern - get all permissions for a user userIdIdx: index('permissions_user_id_idx').on(table.userId), // Entity-based queries - get all users with permissions on an entity entityIdx: index('permissions_entity_idx').on(table.entityType, table.entityId), // User + entity type queries - get user's permissions for all workspaces userEntityTypeIdx: index('permissions_user_entity_type_idx').on(table.userId, table.entityType), // Specific permission checks - does user have specific permission on entity userEntityPermissionIdx: index('permissions_user_entity_permission_idx').on( table.userId, table.entityType, table.permissionType ), // User + specific entity queries - get user's permissions for specific entity userEntityIdx: index('permissions_user_entity_idx').on( table.userId, table.entityType, table.entityId ), // Uniqueness constraint - prevent duplicate permission rows (one permission per user/entity) uniquePermissionConstraint: uniqueIndex('permissions_unique_constraint').on( table.userId, table.entityType, table.entityId ), }) ) export const memory = pgTable( 'memory', { id: text('id').primaryKey(), workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'cascade' }), key: text('key').notNull(), // Identifier for the memory within its context type: text('type').notNull(), // 'agent' or 'raw' data: json('data').notNull(), // Stores either agent message data or raw data createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), deletedAt: timestamp('deleted_at'), }, (table) => { return { // Add index on key for faster lookups keyIdx: index('memory_key_idx').on(table.key), // Add index on workflowId for faster filtering workflowIdx: index('memory_workflow_idx').on(table.workflowId), // Compound unique index to ensure keys are unique per workflow uniqueKeyPerWorkflowIdx: uniqueIndex('memory_workflow_key_idx').on( table.workflowId, table.key ), } } ) export const knowledgeBase = pgTable( 'knowledge_base', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id), name: text('name').notNull(), description: text('description'), // Token tracking for usage tokenCount: integer('token_count').notNull().default(0), // Embedding configuration embeddingModel: text('embedding_model').notNull().default('text-embedding-3-small'), embeddingDimension: integer('embedding_dimension').notNull().default(1536), // Chunking configuration stored as JSON for flexibility chunkingConfig: json('chunking_config') .notNull() .default('{"maxSize": 1024, "minSize": 1, "overlap": 200}'), // Soft delete support deletedAt: timestamp('deleted_at'), // Metadata and timestamps createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access patterns userIdIdx: index('kb_user_id_idx').on(table.userId), workspaceIdIdx: index('kb_workspace_id_idx').on(table.workspaceId), // Composite index for user's workspaces 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), }) ) export const document = pgTable( 'document', { id: text('id').primaryKey(), knowledgeBaseId: text('knowledge_base_id') .notNull() .references(() => knowledgeBase.id, { onDelete: 'cascade' }), // File information filename: text('filename').notNull(), fileUrl: text('file_url').notNull(), fileSize: integer('file_size').notNull(), // Size in bytes mimeType: text('mime_type').notNull(), // e.g., 'application/pdf', 'text/plain' // Content statistics chunkCount: integer('chunk_count').notNull().default(0), tokenCount: integer('token_count').notNull().default(0), characterCount: integer('character_count').notNull().default(0), // Processing status processingStatus: text('processing_status').notNull().default('pending'), // 'pending', 'processing', 'completed', 'failed' processingStartedAt: timestamp('processing_started_at'), processingCompletedAt: timestamp('processing_completed_at'), processingError: text('processing_error'), // Document state enabled: boolean('enabled').notNull().default(true), // Enable/disable from knowledge base deletedAt: timestamp('deleted_at'), // Soft delete // Document tags for filtering (inherited by all chunks) tag1: text('tag1'), tag2: text('tag2'), tag3: text('tag3'), tag4: text('tag4'), tag5: text('tag5'), tag6: text('tag6'), tag7: text('tag7'), // Timestamps uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), }, (table) => ({ // Primary access pattern - documents by knowledge base knowledgeBaseIdIdx: index('doc_kb_id_idx').on(table.knowledgeBaseId), // Search by filename (for search functionality) filenameIdx: index('doc_filename_idx').on(table.filename), // Order by upload date (for listing documents) kbUploadedAtIdx: index('doc_kb_uploaded_at_idx').on(table.knowledgeBaseId, table.uploadedAt), // Processing status filtering processingStatusIdx: index('doc_processing_status_idx').on( table.knowledgeBaseId, table.processingStatus ), // Tag indexes for filtering tag1Idx: index('doc_tag1_idx').on(table.tag1), tag2Idx: index('doc_tag2_idx').on(table.tag2), tag3Idx: index('doc_tag3_idx').on(table.tag3), tag4Idx: index('doc_tag4_idx').on(table.tag4), tag5Idx: index('doc_tag5_idx').on(table.tag5), tag6Idx: index('doc_tag6_idx').on(table.tag6), tag7Idx: index('doc_tag7_idx').on(table.tag7), }) ) export const knowledgeBaseTagDefinitions = pgTable( 'knowledge_base_tag_definitions', { id: text('id').primaryKey(), knowledgeBaseId: text('knowledge_base_id') .notNull() .references(() => knowledgeBase.id, { onDelete: 'cascade' }), tagSlot: text('tag_slot', { enum: TAG_SLOTS, }).notNull(), displayName: text('display_name').notNull(), fieldType: text('field_type').notNull().default('text'), // 'text', future: 'date', 'number', 'range' createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Ensure unique tag slot per knowledge base kbTagSlotIdx: uniqueIndex('kb_tag_definitions_kb_slot_idx').on( table.knowledgeBaseId, table.tagSlot ), // Ensure unique display name per knowledge base kbDisplayNameIdx: uniqueIndex('kb_tag_definitions_kb_display_name_idx').on( table.knowledgeBaseId, table.displayName ), // Index for querying by knowledge base kbIdIdx: index('kb_tag_definitions_kb_id_idx').on(table.knowledgeBaseId), }) ) export const embedding = pgTable( 'embedding', { id: text('id').primaryKey(), knowledgeBaseId: text('knowledge_base_id') .notNull() .references(() => knowledgeBase.id, { onDelete: 'cascade' }), documentId: text('document_id') .notNull() .references(() => document.id, { onDelete: 'cascade' }), // Chunk information chunkIndex: integer('chunk_index').notNull(), chunkHash: text('chunk_hash').notNull(), content: text('content').notNull(), contentLength: integer('content_length').notNull(), tokenCount: integer('token_count').notNull(), // Vector embeddings - optimized for text-embedding-3-small with HNSW support embedding: vector('embedding', { dimensions: 1536 }), // For text-embedding-3-small embeddingModel: text('embedding_model').notNull().default('text-embedding-3-small'), // Chunk boundaries and overlap startOffset: integer('start_offset').notNull(), endOffset: integer('end_offset').notNull(), // Tag columns inherited from document for efficient filtering tag1: text('tag1'), tag2: text('tag2'), tag3: text('tag3'), tag4: text('tag4'), tag5: text('tag5'), tag6: text('tag6'), tag7: text('tag7'), // Chunk state - enable/disable from knowledge base enabled: boolean('enabled').notNull().default(true), // Full-text search support - generated tsvector column contentTsv: tsvector('content_tsv').generatedAlwaysAs( (): SQL => sql`to_tsvector('english', ${embedding.content})` ), // Timestamps createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary vector search pattern kbIdIdx: index('emb_kb_id_idx').on(table.knowledgeBaseId), // Document-level access docIdIdx: index('emb_doc_id_idx').on(table.documentId), // Chunk ordering within documents docChunkIdx: uniqueIndex('emb_doc_chunk_idx').on(table.documentId, table.chunkIndex), // Model-specific queries for A/B testing or migrations kbModelIdx: index('emb_kb_model_idx').on(table.knowledgeBaseId, table.embeddingModel), // Enabled state filtering indexes (for chunk enable/disable functionality) kbEnabledIdx: index('emb_kb_enabled_idx').on(table.knowledgeBaseId, table.enabled), docEnabledIdx: index('emb_doc_enabled_idx').on(table.documentId, table.enabled), // Vector similarity search indexes (HNSW) - optimized for small embeddings embeddingVectorHnswIdx: index('embedding_vector_hnsw_idx') .using('hnsw', table.embedding.op('vector_cosine_ops')) .with({ m: 16, ef_construction: 64, }), // Tag indexes for efficient filtering tag1Idx: index('emb_tag1_idx').on(table.tag1), tag2Idx: index('emb_tag2_idx').on(table.tag2), tag3Idx: index('emb_tag3_idx').on(table.tag3), tag4Idx: index('emb_tag4_idx').on(table.tag4), tag5Idx: index('emb_tag5_idx').on(table.tag5), tag6Idx: index('emb_tag6_idx').on(table.tag6), tag7Idx: index('emb_tag7_idx').on(table.tag7), // Full-text search index contentFtsIdx: index('emb_content_fts_idx').using('gin', table.contentTsv), // Ensure embedding exists (simplified since we only support one model) embeddingNotNullCheck: check('embedding_not_null_check', sql`"embedding" IS NOT NULL`), }) ) export const docsEmbeddings = pgTable( 'docs_embeddings', { chunkId: uuid('chunk_id').primaryKey().defaultRandom(), chunkText: text('chunk_text').notNull(), sourceDocument: text('source_document').notNull(), sourceLink: text('source_link').notNull(), headerText: text('header_text').notNull(), headerLevel: integer('header_level').notNull(), tokenCount: integer('token_count').notNull(), // Vector embedding - optimized for text-embedding-3-small with HNSW support embedding: vector('embedding', { dimensions: 1536 }).notNull(), embeddingModel: text('embedding_model').notNull().default('text-embedding-3-small'), // Metadata for flexible filtering metadata: jsonb('metadata').notNull().default('{}'), // Full-text search support - generated tsvector column chunkTextTsv: tsvector('chunk_text_tsv').generatedAlwaysAs( (): SQL => sql`to_tsvector('english', ${docsEmbeddings.chunkText})` ), // Timestamps createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Source document queries sourceDocumentIdx: index('docs_emb_source_document_idx').on(table.sourceDocument), // Header level filtering headerLevelIdx: index('docs_emb_header_level_idx').on(table.headerLevel), // Combined source and header queries sourceHeaderIdx: index('docs_emb_source_header_idx').on( table.sourceDocument, table.headerLevel ), // Model-specific queries modelIdx: index('docs_emb_model_idx').on(table.embeddingModel), // Timestamp queries createdAtIdx: index('docs_emb_created_at_idx').on(table.createdAt), // Vector similarity search indexes (HNSW) - optimized for documentation embeddings embeddingVectorHnswIdx: index('docs_embedding_vector_hnsw_idx') .using('hnsw', table.embedding.op('vector_cosine_ops')) .with({ m: 16, ef_construction: 64, }), // GIN index for JSONB metadata queries metadataGinIdx: index('docs_emb_metadata_gin_idx').using('gin', table.metadata), // Full-text search index chunkTextFtsIdx: index('docs_emb_chunk_text_fts_idx').using('gin', table.chunkTextTsv), // Constraints embeddingNotNullCheck: check('docs_embedding_not_null_check', sql`"embedding" IS NOT NULL`), headerLevelCheck: check( 'docs_header_level_check', sql`"header_level" >= 1 AND "header_level" <= 6` ), }) ) export const copilotChats = pgTable( 'copilot_chats', { id: uuid('id').primaryKey().defaultRandom(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), title: text('title'), messages: jsonb('messages').notNull().default('[]'), model: text('model').notNull().default('claude-3-7-sonnet-latest'), conversationId: text('conversation_id'), previewYaml: text('preview_yaml'), // YAML content for pending workflow preview createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access patterns userIdIdx: index('copilot_chats_user_id_idx').on(table.userId), workflowIdIdx: index('copilot_chats_workflow_id_idx').on(table.workflowId), userWorkflowIdx: index('copilot_chats_user_workflow_idx').on(table.userId, table.workflowId), // Ordering indexes createdAtIdx: index('copilot_chats_created_at_idx').on(table.createdAt), updatedAtIdx: index('copilot_chats_updated_at_idx').on(table.updatedAt), }) ) export const workflowCheckpoints = pgTable( 'workflow_checkpoints', { id: uuid('id').primaryKey().defaultRandom(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), chatId: uuid('chat_id') .notNull() .references(() => copilotChats.id, { onDelete: 'cascade' }), messageId: text('message_id'), // ID of the user message that triggered this checkpoint workflowState: json('workflow_state').notNull(), // JSON workflow state createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access patterns userIdIdx: index('workflow_checkpoints_user_id_idx').on(table.userId), workflowIdIdx: index('workflow_checkpoints_workflow_id_idx').on(table.workflowId), chatIdIdx: index('workflow_checkpoints_chat_id_idx').on(table.chatId), messageIdIdx: index('workflow_checkpoints_message_id_idx').on(table.messageId), // Combined indexes for common queries userWorkflowIdx: index('workflow_checkpoints_user_workflow_idx').on( table.userId, table.workflowId ), workflowChatIdx: index('workflow_checkpoints_workflow_chat_idx').on( table.workflowId, table.chatId ), // Ordering indexes createdAtIdx: index('workflow_checkpoints_created_at_idx').on(table.createdAt), chatCreatedAtIdx: index('workflow_checkpoints_chat_created_at_idx').on( table.chatId, table.createdAt ), }) ) export const templateStatusEnum = pgEnum('template_status', ['pending', 'approved', 'rejected']) export const templateCreatorTypeEnum = pgEnum('template_creator_type', ['user', 'organization']) export const templateCreators = pgTable( 'template_creators', { id: text('id').primaryKey(), referenceType: templateCreatorTypeEnum('reference_type').notNull(), referenceId: text('reference_id').notNull(), name: text('name').notNull(), profileImageUrl: text('profile_image_url'), details: jsonb('details'), createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ referenceUniqueIdx: uniqueIndex('template_creators_reference_idx').on( table.referenceType, table.referenceId ), referenceIdIdx: index('template_creators_reference_id_idx').on(table.referenceId), createdByIdx: index('template_creators_created_by_idx').on(table.createdBy), }) ) export const templates = pgTable( 'templates', { id: text('id').primaryKey(), workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'set null' }), name: text('name').notNull(), details: jsonb('details'), creatorId: text('creator_id').references(() => templateCreators.id, { onDelete: 'set null' }), views: integer('views').notNull().default(0), stars: integer('stars').notNull().default(0), status: templateStatusEnum('status').notNull().default('pending'), tags: text('tags').array().notNull().default(sql`'{}'::text[]`), // Array of tags requiredCredentials: jsonb('required_credentials').notNull().default('[]'), // Array of credential requirements state: jsonb('state').notNull(), // Store the workflow state directly createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access patterns statusIdx: index('templates_status_idx').on(table.status), creatorIdIdx: index('templates_creator_id_idx').on(table.creatorId), // Sorting indexes for popular/trending templates viewsIdx: index('templates_views_idx').on(table.views), starsIdx: index('templates_stars_idx').on(table.stars), // Composite indexes for common queries statusViewsIdx: index('templates_status_views_idx').on(table.status, table.views), statusStarsIdx: index('templates_status_stars_idx').on(table.status, table.stars), // Temporal indexes createdAtIdx: index('templates_created_at_idx').on(table.createdAt), updatedAtIdx: index('templates_updated_at_idx').on(table.updatedAt), }) ) export const templateStars = pgTable( 'template_stars', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), templateId: text('template_id') .notNull() .references(() => templates.id, { onDelete: 'cascade' }), starredAt: timestamp('starred_at').notNull().defaultNow(), createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ // Primary access patterns userIdIdx: index('template_stars_user_id_idx').on(table.userId), templateIdIdx: index('template_stars_template_id_idx').on(table.templateId), // Composite indexes for common queries userTemplateIdx: index('template_stars_user_template_idx').on(table.userId, table.templateId), templateUserIdx: index('template_stars_template_user_idx').on(table.templateId, table.userId), // Temporal indexes for analytics starredAtIdx: index('template_stars_starred_at_idx').on(table.starredAt), templateStarredAtIdx: index('template_stars_template_starred_at_idx').on( table.templateId, table.starredAt ), // Uniqueness constraint - prevent duplicate stars uniqueUserTemplateConstraint: uniqueIndex('template_stars_user_template_unique').on( table.userId, table.templateId ), }) ) export const copilotFeedback = pgTable( 'copilot_feedback', { feedbackId: uuid('feedback_id').primaryKey().defaultRandom(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), chatId: uuid('chat_id') .notNull() .references(() => copilotChats.id, { onDelete: 'cascade' }), userQuery: text('user_query').notNull(), agentResponse: text('agent_response').notNull(), isPositive: boolean('is_positive').notNull(), feedback: text('feedback'), // Optional feedback text workflowYaml: text('workflow_yaml'), // Optional workflow YAML if edit/build workflow was triggered createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Access patterns userIdIdx: index('copilot_feedback_user_id_idx').on(table.userId), chatIdIdx: index('copilot_feedback_chat_id_idx').on(table.chatId), userChatIdx: index('copilot_feedback_user_chat_idx').on(table.userId, table.chatId), // Query patterns isPositiveIdx: index('copilot_feedback_is_positive_idx').on(table.isPositive), // Ordering indexes createdAtIdx: index('copilot_feedback_created_at_idx').on(table.createdAt), }) ) // Tracks immutable deployment versions for each workflow export const workflowDeploymentVersion = pgTable( 'workflow_deployment_version', { id: text('id').primaryKey(), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), version: integer('version').notNull(), name: text('name'), state: json('state').notNull(), isActive: boolean('is_active').notNull().default(false), createdAt: timestamp('created_at').notNull().defaultNow(), createdBy: text('created_by'), }, (table) => ({ workflowIdIdx: index('workflow_deployment_version_workflow_id_idx').on(table.workflowId), workflowVersionUnique: uniqueIndex('workflow_deployment_version_workflow_version_unique').on( table.workflowId, table.version ), workflowActiveIdx: index('workflow_deployment_version_workflow_active_idx').on( table.workflowId, table.isActive ), createdAtIdx: index('workflow_deployment_version_created_at_idx').on(table.createdAt), }) ) // Idempotency keys for preventing duplicate processing across all webhooks and triggers export const idempotencyKey = pgTable( 'idempotency_key', { key: text('key').notNull(), namespace: text('namespace').notNull().default('default'), result: json('result').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ // Primary key is combination of key and namespace keyNamespacePk: uniqueIndex('idempotency_key_namespace_unique').on(table.key, table.namespace), // Index for cleanup operations by creation time createdAtIdx: index('idempotency_key_created_at_idx').on(table.createdAt), // Index for namespace-based queries namespaceIdx: index('idempotency_key_namespace_idx').on(table.namespace), }) ) export const mcpServers = pgTable( 'mcp_servers', { id: text('id').primaryKey(), workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), // Track who created the server, but workspace owns it createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), name: text('name').notNull(), description: text('description'), transport: text('transport').notNull(), url: text('url'), headers: json('headers').default('{}'), timeout: integer('timeout').default(30000), retries: integer('retries').default(3), enabled: boolean('enabled').notNull().default(true), lastConnected: timestamp('last_connected'), connectionStatus: text('connection_status').default('disconnected'), lastError: text('last_error'), toolCount: integer('tool_count').default(0), lastToolsRefresh: timestamp('last_tools_refresh'), totalRequests: integer('total_requests').default(0), lastUsed: timestamp('last_used'), deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ // Primary access pattern - active servers by workspace workspaceEnabledIdx: index('mcp_servers_workspace_enabled_idx').on( table.workspaceId, table.enabled ), // Soft delete pattern - workspace + not deleted workspaceDeletedIdx: index('mcp_servers_workspace_deleted_idx').on( table.workspaceId, table.deletedAt ), }) ) // SSO Provider table export const ssoProvider = pgTable( 'sso_provider', { id: text('id').primaryKey(), issuer: text('issuer').notNull(), domain: text('domain').notNull(), oidcConfig: text('oidc_config'), samlConfig: text('saml_config'), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), providerId: text('provider_id').notNull(), organizationId: text('organization_id').references(() => organization.id, { onDelete: 'cascade', }), }, (table) => ({ providerIdIdx: index('sso_provider_provider_id_idx').on(table.providerId), domainIdx: index('sso_provider_domain_idx').on(table.domain), userIdIdx: index('sso_provider_user_id_idx').on(table.userId), organizationIdIdx: index('sso_provider_organization_id_idx').on(table.organizationId), }) )