From bc4ce29fc628a77bacafcda00e02ab8be7f487be Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 7 Jul 2025 09:52:26 -0600 Subject: [PATCH] moving over slack processing work --- .../helpers/data-transformers.test.ts | 322 ++ .../helpers/data-transformers.ts | 109 + .../message-post-processing/helpers/index.ts | 4 + .../helpers/message-fetchers.test.ts | 252 ++ .../helpers/message-fetchers.ts | 151 + .../helpers/slack-notifier.ts | 274 ++ .../tasks/message-post-processing/index.ts | 6 + .../message-post-processing.int.test.ts | 217 + .../message-post-processing.test.ts | 318 ++ .../message-post-processing.ts | 341 ++ .../tasks/message-post-processing/types.ts | 81 + .../format-follow-up-message-step.ts | 2 +- .../src/workflows/post-processing-workflow.ts | 2 +- .../drizzle/0073_lovely_white_tiger.sql | 2 +- packages/database/src/schema.ts | 3540 +++++++++-------- pnpm-lock.yaml | 61 +- 16 files changed, 3907 insertions(+), 1775 deletions(-) create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/index.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.test.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/index.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/message-post-processing.int.test.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/message-post-processing.ts create mode 100644 apps/trigger/src/tasks/message-post-processing/types.ts diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts new file mode 100644 index 000000000..ea71d8a79 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts @@ -0,0 +1,322 @@ +import type { CoreMessage } from 'ai'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { MessageContext } from '../types'; +import { + buildConversationHistory, + buildWorkflowInput, + concatenateDatasets, + formatPreviousMessages, +} from './data-transformers'; + +// Mock console.error to avoid noise in tests +beforeEach(() => { + vi.spyOn(console, 'error').mockImplementation(() => {}); +}); + +describe('data-transformers', () => { + describe('buildConversationHistory', () => { + it('should combine multiple message arrays correctly', () => { + const messages = [ + { + id: '1', + rawLlmMessages: [ + { role: 'user', content: 'Hello' } as CoreMessage, + { role: 'assistant', content: 'Hi there' } as CoreMessage, + ], + createdAt: new Date('2024-01-01T00:00:00Z'), + }, + { + id: '2', + rawLlmMessages: [ + { role: 'user', content: 'How are you?' } as CoreMessage, + { role: 'assistant', content: 'I am fine' } as CoreMessage, + ], + createdAt: new Date('2024-01-01T00:01:00Z'), + }, + ]; + + const result = buildConversationHistory(messages); + + expect(result).toHaveLength(4); + expect(result?.[0]).toEqual({ role: 'user', content: 'Hello' }); + expect(result?.[3]).toEqual({ role: 'assistant', content: 'I am fine' }); + }); + + it('should handle empty messages array', () => { + const result = buildConversationHistory([]); + expect(result).toBeUndefined(); + }); + + it('should skip messages with null rawLlmMessages', () => { + const messages = [ + { + id: '1', + rawLlmMessages: null as any, + createdAt: new Date(), + }, + { + id: '2', + rawLlmMessages: [{ role: 'user', content: 'Test' } as CoreMessage], + createdAt: new Date(), + }, + ]; + + const result = buildConversationHistory(messages); + expect(result).toHaveLength(1); + expect(result?.[0]).toEqual({ role: 'user', content: 'Test' }); + }); + + it('should handle non-array rawLlmMessages gracefully', () => { + const messages = [ + { + id: '1', + rawLlmMessages: 'invalid data' as any, + createdAt: new Date(), + }, + { + id: '2', + rawLlmMessages: [{ role: 'user', content: 'Valid message' } as CoreMessage], + createdAt: new Date(), + }, + ]; + + const result = buildConversationHistory(messages); + expect(result).toHaveLength(1); + expect(result?.[0]).toEqual({ role: 'user', content: 'Valid message' }); + }); + }); + + describe('formatPreviousMessages', () => { + it('should extract string representation correctly', () => { + const results = [ + { + postProcessingMessage: { assumptions: ['Test assumption'] }, + createdAt: new Date(), + }, + { + postProcessingMessage: { message: 'Direct string message' }, + createdAt: new Date(), + }, + ]; + + const formatted = formatPreviousMessages(results); + + expect(formatted).toHaveLength(2); + expect(formatted[0]).toContain('assumptions'); + expect(formatted[1]).toContain('Direct string message'); + }); + + it('should handle complex nested objects', () => { + const results = [ + { + postProcessingMessage: { + initial: { + assumptions: ['Complex assumption'], + flagForReview: true, + nested: { + deep: 'value', + }, + }, + }, + createdAt: new Date(), + }, + ]; + + const formatted = formatPreviousMessages(results); + expect(formatted[0]).toContain('Complex assumption'); + expect(formatted[0]).toContain('flagForReview'); + expect(formatted[0]).toContain('deep'); + }); + + it('should return empty array for no messages', () => { + const formatted = formatPreviousMessages([]); + expect(formatted).toEqual([]); + }); + + it('should filter out empty strings from errors', () => { + const results = [ + { + postProcessingMessage: {}, // This will cause an error/empty result + createdAt: new Date(), + }, + { + postProcessingMessage: { message: 'Valid message' }, + createdAt: new Date(), + }, + ]; + + const formatted = formatPreviousMessages(results); + expect(formatted).toHaveLength(2); + expect(formatted[1]).toContain('Valid message'); + }); + }); + + describe('concatenateDatasets', () => { + it('should join with correct separator', () => { + const datasets = [ + { + id: '1', + name: 'Dataset 1', + ymlFile: 'content1', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds1', + }, + { + id: '2', + name: 'Dataset 2', + ymlFile: 'content2', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds2', + }, + ]; + + const result = concatenateDatasets(datasets); + expect(result).toBe('content1\n---\ncontent2'); + }); + + it('should filter null ymlFile entries', () => { + const datasets = [ + { + id: '1', + name: 'Dataset 1', + ymlFile: 'content1', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds1', + }, + { + id: '2', + name: 'Dataset 2', + ymlFile: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds2', + }, + ]; + + const result = concatenateDatasets(datasets); + expect(result).toBe('content1'); + }); + + it('should return empty string for no datasets', () => { + const result = concatenateDatasets([]); + expect(result).toBe(''); + }); + + it('should return empty string if all datasets have null ymlFile', () => { + const datasets = [ + { + id: '1', + name: 'Dataset 1', + ymlFile: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds1', + }, + ]; + + const result = concatenateDatasets(datasets); + expect(result).toBe(''); + }); + }); + + describe('buildWorkflowInput', () => { + const baseMessageContext: MessageContext = { + id: 'msg-123', + chatId: 'chat-123', + createdBy: 'user-123', + createdAt: new Date(), + userName: 'John Doe', + organizationId: 'org-123', + }; + + const baseConversationMessages = [ + { + id: '1', + rawLlmMessages: [{ role: 'user', content: 'Hello' }] as any, + createdAt: new Date(), + }, + ]; + + const basePreviousResults: any[] = []; + + const baseDatasets = [ + { + id: '1', + name: 'Dataset 1', + ymlFile: 'yaml content', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds1', + }, + ]; + + it('should build complete workflow input for initial message', () => { + const result = buildWorkflowInput( + baseMessageContext, + baseConversationMessages, + basePreviousResults, + baseDatasets + ); + + expect(result).toEqual({ + conversationHistory: [{ role: 'user', content: 'Hello' }], + userName: 'John Doe', + messageId: 'msg-123', + userId: 'user-123', + chatId: 'chat-123', + isFollowUp: false, + previousMessages: [], + datasets: 'yaml content', + }); + }); + + it('should build workflow input for follow-up message', () => { + const previousResults = [ + { + postProcessingMessage: { assumptions: ['Previous assumption'] }, + createdAt: new Date(), + }, + ]; + + const result = buildWorkflowInput( + baseMessageContext, + baseConversationMessages, + previousResults, + baseDatasets + ); + + expect(result.isFollowUp).toBe(true); + expect(result.previousMessages).toHaveLength(1); + expect(result.previousMessages[0]).toContain('Previous assumption'); + }); + + it('should handle null userName', () => { + const messageContextWithNullUser = { + ...baseMessageContext, + userName: null, + }; + + const result = buildWorkflowInput( + messageContextWithNullUser, + baseConversationMessages, + basePreviousResults, + baseDatasets + ); + expect(result.userName).toBe('Unknown User'); + }); + + it('should handle empty conversation history', () => { + const result = buildWorkflowInput(baseMessageContext, [], basePreviousResults, baseDatasets); + expect(result.conversationHistory).toBeUndefined(); + }); + }); +}); diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.ts b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.ts new file mode 100644 index 000000000..230cc34b2 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.ts @@ -0,0 +1,109 @@ +import type { PermissionedDataset } from '@buster/access-controls'; +import type { MessageHistory } from '@buster/ai/utils/memory/types'; +import type { PostProcessingWorkflowInput } from '@buster/ai/workflows/post-processing-workflow'; +import type { CoreMessage } from 'ai'; +import type { ConversationMessage, MessageContext, PostProcessingResult } from '../types'; + +/** + * Combine raw LLM messages from multiple messages into a single conversation history + */ +export function buildConversationHistory( + messages: ConversationMessage[] +): MessageHistory | undefined { + if (messages.length === 0) { + return undefined; + } + + const allMessages: CoreMessage[] = []; + + for (const message of messages) { + if (!message.rawLlmMessages) { + continue; + } + + try { + // rawLlmMessages from the database is already CoreMessage[] + if (Array.isArray(message.rawLlmMessages)) { + allMessages.push(...message.rawLlmMessages); + } + } catch (_error) { + // Skip messages that can't be parsed + // Continue with other messages + } + } + + if (allMessages.length === 0) { + return undefined; + } + + // Return as MessageHistory which is CoreMessage[] + return allMessages as MessageHistory; +} + +/** + * Extract post-processing messages as string array + */ +export function formatPreviousMessages(results: PostProcessingResult[]): string[] { + return results + .map((result) => { + try { + if (typeof result.postProcessingMessage === 'string') { + return result.postProcessingMessage; + } + // Convert object to formatted string + return JSON.stringify(result.postProcessingMessage, null, 2); + } catch (_error) { + // Skip messages that can't be formatted + return ''; + } + }) + .filter((msg) => msg.length > 0); +} + +/** + * Concatenate dataset YAML files + */ +export function concatenateDatasets(datasets: PermissionedDataset[]): string { + const validDatasets = datasets.filter( + (dataset) => dataset.ymlFile !== null && dataset.ymlFile !== undefined + ); + + if (validDatasets.length === 0) { + return ''; + } + + return validDatasets.map((dataset) => dataset.ymlFile).join('\n---\n'); +} + +/** + * Build complete workflow input from collected data + */ +export function buildWorkflowInput( + messageContext: MessageContext, + conversationMessages: ConversationMessage[], + previousPostProcessingResults: PostProcessingResult[], + datasets: PermissionedDataset[] +): PostProcessingWorkflowInput { + // Build conversation history from all messages + const conversationHistory = buildConversationHistory(conversationMessages); + + // Determine if this is a follow-up + const isFollowUp = previousPostProcessingResults.length > 0; + + // Format previous messages + const previousMessages = formatPreviousMessages(previousPostProcessingResults); + + // Concatenate datasets + const datasetsYaml = concatenateDatasets(datasets); + + return { + conversationHistory, + userName: messageContext.userName || 'Unknown User', + messageId: messageContext.id, + userId: messageContext.createdBy, + chatId: messageContext.chatId, + isFollowUp, + previousMessages, + datasets: datasetsYaml, + }; +} diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/index.ts b/apps/trigger/src/tasks/message-post-processing/helpers/index.ts new file mode 100644 index 000000000..4d8f03559 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/index.ts @@ -0,0 +1,4 @@ +// Export all helper functions +export * from './message-fetchers'; +export * from './data-transformers'; +export * from './slack-notifier'; diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.test.ts b/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.test.ts new file mode 100644 index 000000000..dcaf5abb3 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.test.ts @@ -0,0 +1,252 @@ +import * as accessControls from '@buster/access-controls'; +import * as database from '@buster/database'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { DataFetchError, MessageNotFoundError } from '../types'; +import { + fetchConversationHistory, + fetchMessageWithContext, + fetchPreviousPostProcessingMessages, + fetchUserDatasets, +} from './message-fetchers'; + +// Mock the database module +vi.mock('@buster/database', () => ({ + getDb: vi.fn(), + and: vi.fn((...args) => ({ type: 'and', args })), + eq: vi.fn((a, b) => ({ type: 'eq', a, b })), + lt: vi.fn((a, b) => ({ type: 'lt', a, b })), + isNull: vi.fn((a) => ({ type: 'isNull', a })), + isNotNull: vi.fn((a) => ({ type: 'isNotNull', a })), + messages: { id: 'messages.id', chatId: 'messages.chatId', createdBy: 'messages.createdBy' }, + chats: { id: 'chats.id', organizationId: 'chats.organizationId' }, + users: { id: 'users.id', name: 'users.name' }, +})); + +// Mock access controls +vi.mock('@buster/access-controls', () => ({ + getPermissionedDatasets: vi.fn(), +})); + +describe('message-fetchers', () => { + let mockDb: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockDb = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + leftJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + }; + vi.mocked(database.getDb).mockReturnValue(mockDb); + }); + + describe('fetchMessageWithContext', () => { + it('should return message with user and chat data', async () => { + const messageData = { + id: '123e4567-e89b-12d3-a456-426614174000', + chatId: '223e4567-e89b-12d3-a456-426614174000', + createdBy: '323e4567-e89b-12d3-a456-426614174000', + createdAt: '2024-01-01T00:00:00Z', + userName: 'John Doe', + organizationId: '423e4567-e89b-12d3-a456-426614174000', + }; + + mockDb.limit.mockResolvedValue([messageData]); + + const result = await fetchMessageWithContext(messageData.id); + + expect(result).toEqual({ + id: messageData.id, + chatId: messageData.chatId, + createdBy: messageData.createdBy, + createdAt: new Date(messageData.createdAt), + userName: messageData.userName, + organizationId: messageData.organizationId, + }); + + expect(mockDb.select).toHaveBeenCalled(); + expect(mockDb.where).toHaveBeenCalled(); + expect(mockDb.limit).toHaveBeenCalledWith(1); + }); + + it('should throw MessageNotFoundError when message not found', async () => { + mockDb.limit.mockResolvedValue([]); + + await expect(fetchMessageWithContext('non-existent-id')).rejects.toThrow( + MessageNotFoundError + ); + }); + + it('should handle null user name', async () => { + const messageData = { + id: '123e4567-e89b-12d3-a456-426614174000', + chatId: '223e4567-e89b-12d3-a456-426614174000', + createdBy: '323e4567-e89b-12d3-a456-426614174000', + createdAt: '2024-01-01T00:00:00Z', + userName: null, + organizationId: '423e4567-e89b-12d3-a456-426614174000', + }; + + mockDb.limit.mockResolvedValue([messageData]); + + const result = await fetchMessageWithContext(messageData.id); + expect(result.userName).toBeNull(); + }); + + it('should wrap database errors in DataFetchError', async () => { + const dbError = new Error('Database connection failed'); + mockDb.limit.mockRejectedValue(dbError); + + await expect(fetchMessageWithContext('123')).rejects.toThrow(DataFetchError); + }); + }); + + describe('fetchConversationHistory', () => { + it('should return messages in chronological order', async () => { + const messages = [ + { + id: '1', + rawLlmMessages: [{ role: 'user', content: 'Hello' }], + createdAt: '2024-01-01T00:00:00Z', + }, + { + id: '2', + rawLlmMessages: [{ role: 'assistant', content: 'Hi there' }], + createdAt: '2024-01-01T00:01:00Z', + }, + ]; + + mockDb.orderBy.mockResolvedValue(messages); + + const result = await fetchConversationHistory('chat-id'); + + expect(result).toHaveLength(2); + expect(result[0]?.id).toBe('1'); + expect(result[1]?.id).toBe('2'); + expect(mockDb.orderBy).toHaveBeenCalled(); + }); + + it('should return empty array for non-existent chat', async () => { + mockDb.orderBy.mockResolvedValue([]); + + const result = await fetchConversationHistory('non-existent-chat'); + expect(result).toEqual([]); + }); + + it('should handle messages with null rawLlmMessages', async () => { + const messages = [ + { + id: '1', + rawLlmMessages: null, + createdAt: '2024-01-01T00:00:00Z', + }, + ]; + + mockDb.orderBy.mockResolvedValue(messages); + + const result = await fetchConversationHistory('chat-id'); + expect(result[0]?.rawLlmMessages).toBeNull(); + }); + }); + + describe('fetchPreviousPostProcessingMessages', () => { + const beforeTimestamp = new Date('2024-01-01T12:00:00Z'); + + it('should return only messages with postProcessingMessage', async () => { + const messages = [ + { + postProcessingMessage: { assumptions: ['test'] }, + createdAt: '2024-01-01T10:00:00Z', + }, + { + postProcessingMessage: { followUp: { suggestions: ['ask more'] } }, + createdAt: '2024-01-01T11:00:00Z', + }, + ]; + + mockDb.orderBy.mockResolvedValue(messages); + + const result = await fetchPreviousPostProcessingMessages('chat-id', beforeTimestamp); + + expect(result).toHaveLength(2); + expect(result[0]?.postProcessingMessage).toHaveProperty('assumptions'); + expect(result[1]?.postProcessingMessage).toHaveProperty('followUp'); + }); + + it('should order by createdAt ascending', async () => { + const messages = [ + { + postProcessingMessage: { id: 1 }, + createdAt: '2024-01-01T10:00:00Z', + }, + { + postProcessingMessage: { id: 2 }, + createdAt: '2024-01-01T11:00:00Z', + }, + ]; + + mockDb.orderBy.mockResolvedValue(messages); + + const result = await fetchPreviousPostProcessingMessages('chat-id', beforeTimestamp); + + expect(result[0]!.createdAt < result[1]!.createdAt).toBe(true); + }); + + it('should return empty array when no results', async () => { + mockDb.orderBy.mockResolvedValue([]); + + const result = await fetchPreviousPostProcessingMessages('chat-id', beforeTimestamp); + expect(result).toEqual([]); + }); + }); + + describe('fetchUserDatasets', () => { + it('should return datasets with non-null ymlFile', async () => { + const datasets = [ + { + id: '1', + name: 'Dataset 1', + ymlFile: 'content1', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds1', + }, + { + id: '2', + name: 'Dataset 2', + ymlFile: 'content2', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + dataSourceId: 'ds2', + }, + ]; + + vi.mocked(accessControls.getPermissionedDatasets).mockResolvedValue(datasets); + + const result = await fetchUserDatasets('user-id'); + + expect(result).toEqual(datasets); + expect(accessControls.getPermissionedDatasets).toHaveBeenCalledWith('user-id', 0, 1000); + }); + + it('should handle empty dataset list', async () => { + vi.mocked(accessControls.getPermissionedDatasets).mockResolvedValue([]); + + const result = await fetchUserDatasets('user-id'); + expect(result).toEqual([]); + }); + + it('should wrap errors in DataFetchError', async () => { + const error = new Error('Access denied'); + vi.mocked(accessControls.getPermissionedDatasets).mockRejectedValue(error); + + await expect(fetchUserDatasets('user-id')).rejects.toThrow(DataFetchError); + }); + }); +}); diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.ts b/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.ts new file mode 100644 index 000000000..cac36fc05 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/message-fetchers.ts @@ -0,0 +1,151 @@ +import { getPermissionedDatasets } from '@buster/access-controls'; +import { + and, + chats, + desc, + eq, + getDb, + isNotNull, + isNull, + lt, + messages, + users, +} from '@buster/database'; +import type { CoreMessage } from 'ai'; +import { + type ConversationMessage, + DataFetchError, + type MessageContext, + MessageNotFoundError, + type PostProcessingResult, +} from '../types'; + +/** + * Fetch current message with user and chat info + */ +export async function fetchMessageWithContext(messageId: string): Promise { + const db = getDb(); + + try { + const result = await db + .select({ + id: messages.id, + chatId: messages.chatId, + createdBy: messages.createdBy, + createdAt: messages.createdAt, + userName: users.name, + organizationId: chats.organizationId, + }) + .from(messages) + .innerJoin(chats, eq(messages.chatId, chats.id)) + .leftJoin(users, eq(messages.createdBy, users.id)) + .where(and(eq(messages.id, messageId), isNull(messages.deletedAt))) + .limit(1); + + const messageData = result[0]; + if (!messageData) { + throw new MessageNotFoundError(messageId); + } + + return { + id: messageData.id, + chatId: messageData.chatId, + createdBy: messageData.createdBy, + createdAt: new Date(messageData.createdAt), + userName: messageData.userName, + organizationId: messageData.organizationId, + }; + } catch (error) { + if (error instanceof MessageNotFoundError) { + throw error; + } + throw new DataFetchError( + `Failed to fetch message context for ${messageId}`, + error instanceof Error ? { cause: error } : undefined + ); + } +} + +/** + * Fetch all messages for conversation history + */ +export async function fetchConversationHistory(chatId: string): Promise { + const db = getDb(); + + try { + const result = await db + .select({ + id: messages.id, + rawLlmMessages: messages.rawLlmMessages, + createdAt: messages.createdAt, + }) + .from(messages) + .where(and(eq(messages.chatId, chatId), isNull(messages.deletedAt))) + .orderBy(messages.createdAt); + + return result.map((msg) => ({ + id: msg.id, + rawLlmMessages: msg.rawLlmMessages as CoreMessage[], + createdAt: new Date(msg.createdAt), + })); + } catch (error) { + throw new DataFetchError( + `Failed to fetch conversation history for chat ${chatId}`, + error instanceof Error ? { cause: error } : undefined + ); + } +} + +/** + * Fetch previous post-processing results + */ +export async function fetchPreviousPostProcessingMessages( + chatId: string, + beforeTimestamp: Date +): Promise { + const db = getDb(); + + try { + const result = await db + .select({ + postProcessingMessage: messages.postProcessingMessage, + createdAt: messages.createdAt, + }) + .from(messages) + .where( + and( + eq(messages.chatId, chatId), + isNotNull(messages.postProcessingMessage), + isNull(messages.deletedAt), + lt(messages.createdAt, beforeTimestamp.toISOString()) + ) + ) + .orderBy(messages.createdAt); + + return result.map((msg) => ({ + postProcessingMessage: msg.postProcessingMessage as Record, + createdAt: new Date(msg.createdAt), + })); + } catch (error) { + throw new DataFetchError( + `Failed to fetch previous post-processing messages for chat ${chatId}`, + error instanceof Error ? { cause: error } : undefined + ); + } +} + +/** + * Fetch user's permissioned datasets + */ +export async function fetchUserDatasets(userId: string) { + try { + // Using the existing access control function + const datasets = await getPermissionedDatasets(userId, 0, 1000); + return datasets; + } catch (error) { + throw new DataFetchError( + `Failed to fetch datasets for user ${userId}`, + error instanceof Error ? { cause: error } : undefined + ); + } +} diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts b/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts new file mode 100644 index 000000000..c672f0032 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts @@ -0,0 +1,274 @@ +import { and, eq, getDb, getSecretByName, isNull, slackIntegrations } from '@buster/database'; +import { logger } from '@trigger.dev/sdk/v3'; + +export interface SlackNotificationParams { + organizationId: string; + userName: string | null; + summaryTitle?: string | undefined; + summaryMessage?: string | undefined; + formattedMessage?: string | null | undefined; + toolCalled: string; + message?: string | undefined; +} + +export interface SlackNotificationResult { + sent: boolean; + error?: string; +} + +interface SlackBlock { + type: string; + text?: { + type: string; + text: string; + verbatim?: boolean; + }; +} + +interface SlackMessage { + blocks?: SlackBlock[]; + text?: string; +} + +/** + * Send a Slack notification based on post-processing results + */ +export async function sendSlackNotification( + params: SlackNotificationParams +): Promise { + try { + // Step 1: Check if organization has active Slack integration + const db = getDb(); + const [integration] = await db + .select() + .from(slackIntegrations) + .where( + and( + eq(slackIntegrations.organizationId, params.organizationId), + eq(slackIntegrations.status, 'active'), + isNull(slackIntegrations.deletedAt) + ) + ) + .limit(1); + + if (!integration) { + logger.log('No active Slack integration found', { organizationId: params.organizationId }); + return { sent: false, error: 'No active Slack integration' }; + } + + if (!integration.defaultChannel) { + logger.log('No default channel configured for Slack integration', { + organizationId: params.organizationId, + integrationId: integration.id, + }); + return { sent: false, error: 'No default channel configured' }; + } + + // Step 2: Check if we should send a notification + const shouldSendNotification = shouldSendSlackNotification(params); + if (!shouldSendNotification) { + logger.log('Notification conditions not met', { params }); + return { sent: false, error: 'Notification conditions not met' }; + } + + // Step 3: Retrieve access token from vault + if (!integration.tokenVaultKey) { + logger.error('No token vault key found for integration', { + integrationId: integration.id, + organizationId: params.organizationId, + }); + return { sent: false, error: 'No token vault key found' }; + } + + const tokenSecret = await getSecretByName(integration.tokenVaultKey); + if (!tokenSecret) { + logger.error('Failed to retrieve token from vault', { + tokenVaultKey: integration.tokenVaultKey, + organizationId: params.organizationId, + }); + return { sent: false, error: 'Failed to retrieve access token' }; + } + + // Step 4: Format the Slack message + const slackMessage = formatSlackMessage(params); + + // Step 5: Send the message via Slack API + const result = await sendSlackMessage( + tokenSecret.secret, + integration.defaultChannel.id, + slackMessage + ); + + if (result.success) { + logger.log('Successfully sent Slack notification', { + organizationId: params.organizationId, + channelId: integration.defaultChannel.id, + messageTs: result.messageTs, + }); + return { sent: true }; + } + + logger.error('Failed to send Slack notification', { + organizationId: params.organizationId, + channelId: integration.defaultChannel.id, + error: result.error, + }); + return { sent: false, error: result.error || 'Failed to send message' }; + } catch (error) { + logger.error('Error in sendSlackNotification', { + error: error instanceof Error ? error.message : 'Unknown error', + organizationId: params.organizationId, + }); + return { + sent: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }; + } +} + +/** + * Determine if we should send a Slack notification based on the parameters + */ +function shouldSendSlackNotification(params: SlackNotificationParams): boolean { + // Condition 1: formattedMessage is present (from format-message steps) + if (params.formattedMessage) { + return true; + } + + // Condition 2: summaryTitle and summaryMessage are present (legacy) + if (params.summaryTitle && params.summaryMessage) { + return true; + } + + // Condition 3: toolCalled is 'flagChat' and message is present (legacy) + if (params.toolCalled === 'flagChat' && params.message) { + return true; + } + + return false; +} + +/** + * Format the Slack message based on the notification type + */ +function formatSlackMessage(params: SlackNotificationParams): SlackMessage { + const userName = params.userName || 'Unknown User'; + + // Case 1: Formatted message from workflow (highest priority) + if (params.formattedMessage) { + return { + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `Buster flagged a chat for review:\n**`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: params.formattedMessage, + verbatim: false, + }, + }, + ], + }; + } + + // Case 2: Summary notification (summaryTitle and summaryMessage present) + if (params.summaryTitle && params.summaryMessage) { + return { + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `Buster flagged a chat for review:\n**`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: params.summaryMessage, + verbatim: false, + }, + }, + ], + }; + } + + // Case 3: Flagged chat notification (toolCalled is 'flagChat' and message present) + if (params.toolCalled === 'flagChat' && params.message) { + return { + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `Buster flagged a chat for review:\n**`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: params.message, + verbatim: false, + }, + }, + ], + }; + } + + // This shouldn't happen if shouldSendSlackNotification is working correctly + throw new Error('Invalid notification parameters'); +} + +/** + * Send a message to Slack using the Web API + */ +async function sendSlackMessage( + accessToken: string, + channelId: string, + message: SlackMessage +): Promise<{ success: boolean; messageTs?: string; error?: string }> { + try { + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: channelId, + blocks: message.blocks, + text: message.text || ' ', // Fallback text required by Slack + }), + }); + + const data = (await response.json()) as { ok: boolean; ts?: string; error?: string }; + + if (data.ok) { + return { + success: true, + ...(data.ts && { messageTs: data.ts }), + }; + } + + return { + success: false, + error: data.error || 'Failed to send message', + }; + } catch (error) { + logger.error('Failed to send Slack message', { + error: error instanceof Error ? error.message : 'Unknown error', + }); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to send message', + }; + } +} diff --git a/apps/trigger/src/tasks/message-post-processing/index.ts b/apps/trigger/src/tasks/message-post-processing/index.ts new file mode 100644 index 000000000..539e63a1d --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/index.ts @@ -0,0 +1,6 @@ +export { messagePostProcessingTask } from './message-post-processing'; +export type { MessagePostProcessingTask } from './message-post-processing'; +export type { + TaskInput as MessagePostProcessingTaskInput, + TaskOutput as MessagePostProcessingTaskOutput, +} from './types'; diff --git a/apps/trigger/src/tasks/message-post-processing/message-post-processing.int.test.ts b/apps/trigger/src/tasks/message-post-processing/message-post-processing.int.test.ts new file mode 100644 index 000000000..7e21e35cd --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/message-post-processing.int.test.ts @@ -0,0 +1,217 @@ +import { eq, getDb, messages } from '@buster/database'; +import { + cleanupTestChats, + cleanupTestMessages, + createTestChat, + createTestMessage, + createTestUser, +} from '@buster/test-utils'; +import { tasks } from '@trigger.dev/sdk/v3'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import type { messagePostProcessingTask } from './message-post-processing'; + +// Skip integration tests if TEST_DATABASE_URL is not set +const skipIntegrationTests = !process.env.DATABASE_URL; + +describe.skipIf(skipIntegrationTests)('messagePostProcessingTask integration', () => { + let testUserId: string; + let testChatId: string; + let testMessageId: string; + let testOrgId: string; + + beforeAll(async () => { + // Use specific test user with datasets and permissions + testUserId = 'c2dd64cd-f7f3-4884-bc91-d46ae431901e'; + + const testChatResult = await createTestChat(); + testChatId = testChatResult.chatId; + testOrgId = testChatResult.organizationId; + }); + + afterAll(async () => { + // Cleanup test data + if (testChatId) { + // Note: cleanupTestMessages expects message IDs, not chat IDs + // For now, we'll just clean up the chat which should cascade delete messages + await cleanupTestChats([testChatId]); + } + }); + + it('should successfully process new message (not follow-up)', async () => { + // Use prepopulated message ID + const messageId = 'a3206f20-35d1-4a6c-84a7-48f8f222c39f'; + + // Execute task + const result = await tasks.triggerAndPoll( + 'message-post-processing', + { messageId }, + { pollIntervalMs: 2000 } + ); + + // Verify result structure + expect(result).toBeDefined(); + expect(result.status).toBe('COMPLETED'); + expect(result.output).toBeDefined(); + expect(result.output?.success).toBe(true); + expect(result.output?.messageId).toBe(messageId); + expect(result.output?.result?.success).toBe(true); + expect(result.output?.result?.workflowCompleted).toBe(true); + + // Verify database was updated + const db = getDb(); + const updatedMessage = await db + .select({ postProcessingMessage: messages.postProcessingMessage }) + .from(messages) + .where(eq(messages.id, messageId)) + .limit(1); + + expect(updatedMessage[0]?.postProcessingMessage).toBeDefined(); + + // Cleanup - reset postProcessingMessage to null + await db + .update(messages) + .set({ postProcessingMessage: null }) + .where(eq(messages.id, messageId)); + }); + + it('should successfully process follow-up message', async () => { + // Create first message with post-processing result + const firstMessageId = await createTestMessage(testChatId, testUserId, { + requestMessage: 'Tell me about databases', + rawLlmMessages: [ + { role: 'user' as const, content: 'Tell me about databases' }, + { role: 'assistant' as const, content: 'Databases are organized collections of data.' }, + ], + }); + + // Manually add post-processing result to first message + const db = getDb(); + await db + .update(messages) + .set({ + postProcessingMessage: { + initial: { + assumptions: ['User wants general database information'], + flagForReview: false, + }, + }, + }) + .where(eq(messages.id, firstMessageId)); + + // Create follow-up message + const followUpMessageId = await createTestMessage(testChatId, testUserId, { + requestMessage: 'What about NoSQL databases?', + rawLlmMessages: [ + { role: 'user' as const, content: 'What about NoSQL databases?' }, + { role: 'assistant' as const, content: 'NoSQL databases are non-relational databases.' }, + ], + }); + + // Execute task for follow-up + const result = await tasks.triggerAndPoll( + 'message-post-processing', + { messageId: followUpMessageId }, + { pollIntervalMs: 2000 } + ); + + // Verify it's a follow-up result + expect(result).toBeDefined(); + expect(result.status).toBe('COMPLETED'); + expect(result.output?.success).toBe(true); + expect(result.output?.messageId).toBe(followUpMessageId); + expect(result.output?.result?.success).toBe(true); + expect(result.output?.result?.workflowCompleted).toBe(true); + }); + + it('should handle message with no conversation history', async () => { + // Use prepopulated message ID + const messageId = 'a3206f20-35d1-4a6c-84a7-48f8f222c39f'; + + // Execute task + const result = await tasks.triggerAndPoll( + 'message-post-processing', + { messageId }, + { pollIntervalMs: 2000 } + ); + + // Should still process successfully + expect(result).toBeDefined(); + expect(result.status).toBe('COMPLETED'); + expect(result.output?.success).toBe(true); + expect(result.output?.messageId).toBe(messageId); + + // Cleanup - reset postProcessingMessage to null + const db = getDb(); + await db + .update(messages) + .set({ postProcessingMessage: null }) + .where(eq(messages.id, messageId)); + }); + + it('should fail gracefully when message does not exist', async () => { + const nonExistentId = '00000000-0000-0000-0000-000000000000'; + + const result = await tasks.triggerAndPoll( + 'message-post-processing', + { messageId: nonExistentId }, + { pollIntervalMs: 2000 } + ); + + expect(result.status).toBe('COMPLETED'); + expect(result.output?.success).toBe(false); + expect(result.output?.error?.code).toBe('MESSAGE_NOT_FOUND'); + }); + + it('should complete within timeout', async () => { + // Use prepopulated message ID + const messageId = 'a3206f20-35d1-4a6c-84a7-48f8f222c39f'; + + const startTime = Date.now(); + + await tasks.triggerAndPoll( + 'message-post-processing', + { messageId }, + { pollIntervalMs: 2000 } + ); + + const duration = Date.now() - startTime; + + // Should complete within 60 seconds (task timeout) + expect(duration).toBeLessThan(60000); + + // Cleanup - reset postProcessingMessage to null + const db = getDb(); + await db + .update(messages) + .set({ postProcessingMessage: null }) + .where(eq(messages.id, messageId)); + }); + + it('should handle large conversation histories', async () => { + // Create many messages in the chat + const largeHistory = []; + for (let i = 0; i < 50; i++) { + largeHistory.push( + { role: 'user' as const, content: `Question ${i}` }, + { role: 'assistant' as const, content: `Answer ${i}` } + ); + } + + const largeMessageId = await createTestMessage(testChatId, testUserId, { + requestMessage: 'Large history test', + rawLlmMessages: largeHistory, + }); + + // Should still process successfully + const result = await tasks.triggerAndPoll( + 'message-post-processing', + { messageId: largeMessageId }, + { pollIntervalMs: 2000 } + ); + + expect(result).toBeDefined(); + expect(result.status).toBe('COMPLETED'); + expect(result.output?.success).toBe(true); + expect(result.output?.messageId).toBe(largeMessageId); + }); +}); diff --git a/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts b/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts new file mode 100644 index 000000000..ea5a336bb --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts @@ -0,0 +1,318 @@ +import postProcessingWorkflow from '@buster/ai/workflows/post-processing-workflow'; +import * as database from '@buster/database'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as helpers from './helpers'; +import { messagePostProcessingTask } from './message-post-processing'; +import { DataFetchError, MessageNotFoundError } from './types'; + +// Extract the run function from the task +const runTask = (messagePostProcessingTask as any).run; + +// Mock dependencies +vi.mock('./helpers', () => ({ + fetchMessageWithContext: vi.fn(), + fetchConversationHistory: vi.fn(), + fetchPreviousPostProcessingMessages: vi.fn(), + fetchUserDatasets: vi.fn(), + buildWorkflowInput: vi.fn(), + validateMessageId: vi.fn((id) => id), + validateWorkflowOutput: vi.fn((output) => output), +})); + +vi.mock('@buster/database', () => ({ + getDb: vi.fn(), + eq: vi.fn((a, b) => ({ type: 'eq', a, b })), + messages: { id: 'messages.id' }, +})); + +vi.mock('@buster/ai/workflows/post-processing-workflow', () => ({ + default: { + createRun: vi.fn(), + }, +})); + +// Mock Trigger.dev logger +vi.mock('@trigger.dev/sdk/v3', () => ({ + logger: { + log: vi.fn(), + error: vi.fn(), + }, + schemaTask: vi.fn((config) => ({ + ...config, + run: config.run, + })), +})); + +describe('messagePostProcessingTask', () => { + let mockDb: any; + let mockWorkflowRun: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockDb = { + update: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }; + vi.mocked(database.getDb).mockReturnValue(mockDb); + + // Setup workflow mock + mockWorkflowRun = { + start: vi.fn(), + }; + vi.mocked(postProcessingWorkflow.createRun).mockReturnValue(mockWorkflowRun); + }); + + it('should process message successfully for initial message', async () => { + const messageId = '123e4567-e89b-12d3-a456-426614174000'; + const messageContext = { + id: messageId, + chatId: 'chat-123', + createdBy: 'user-123', + createdAt: new Date(), + userName: 'John Doe', + organizationId: 'org-123', + }; + + const conversationMessages = [ + { + id: '1', + rawLlmMessages: [{ role: 'user' as const, content: 'Hello' }], + createdAt: new Date(), + }, + ]; + + const workflowOutput = { + initial: { + assumptions: ['Test assumption'], + flagForReview: false, + }, + }; + + // Setup mocks + vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue(messageContext); + vi.mocked(helpers.fetchConversationHistory).mockResolvedValue(conversationMessages); + vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]); + vi.mocked(helpers.fetchUserDatasets).mockResolvedValue([]); + vi.mocked(helpers.buildWorkflowInput).mockReturnValue({ + conversationHistory: [{ role: 'user', content: 'Hello' }], + userName: 'John Doe', + messageId, + userId: 'user-123', + chatId: 'chat-123', + isFollowUp: false, + previousMessages: [], + datasets: '', + }); + mockWorkflowRun.start.mockResolvedValue({ + status: 'success', + result: workflowOutput, + }); + + // Execute task + const result = await runTask({ messageId }); + + // Verify results + expect(result).toEqual({ + success: true, + messageId, + result: { + success: true, + messageId, + executionTimeMs: expect.any(Number), + workflowCompleted: true, + }, + }); + expect(helpers.fetchMessageWithContext).toHaveBeenCalledWith(messageId); + expect(helpers.fetchConversationHistory).toHaveBeenCalledWith('chat-123'); + expect(helpers.fetchPreviousPostProcessingMessages).toHaveBeenCalledWith( + 'chat-123', + messageContext.createdAt + ); + expect(helpers.fetchUserDatasets).toHaveBeenCalledWith('user-123'); + expect(postProcessingWorkflow.createRun).toHaveBeenCalled(); + expect(mockWorkflowRun.start).toHaveBeenCalled(); + expect(mockDb.update).toHaveBeenCalledWith(database.messages); + expect(mockDb.set).toHaveBeenCalledWith({ + postProcessingMessage: workflowOutput, + updatedAt: expect.any(String), + }); + }); + + it('should process follow-up message correctly', async () => { + const messageId = '123e4567-e89b-12d3-a456-426614174000'; + const previousResults = [ + { + postProcessingMessage: { assumptions: ['Previous assumption'] }, + createdAt: new Date(), + }, + ]; + + const workflowOutput = { + followUp: { + suggestions: ['Ask about X'], + analysis: 'Based on previous conversation...', + }, + }; + + // Setup mocks for follow-up scenario + vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({ + id: messageId, + chatId: 'chat-123', + createdBy: 'user-123', + createdAt: new Date(), + userName: 'John Doe', + organizationId: 'org-123', + }); + vi.mocked(helpers.fetchConversationHistory).mockResolvedValue([]); + vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue(previousResults); + vi.mocked(helpers.fetchUserDatasets).mockResolvedValue([]); + vi.mocked(helpers.buildWorkflowInput).mockReturnValue({ + conversationHistory: undefined, + userName: 'John Doe', + messageId, + userId: 'user-123', + chatId: 'chat-123', + isFollowUp: true, + previousMessages: ['{"assumptions":["Previous assumption"]}'], + datasets: '', + }); + mockWorkflowRun.start.mockResolvedValue({ + status: 'success', + result: workflowOutput, + }); + + const result = await runTask({ messageId }); + + expect(result).toEqual({ + success: true, + messageId, + result: { + success: true, + messageId, + executionTimeMs: expect.any(Number), + workflowCompleted: true, + }, + }); + expect(helpers.buildWorkflowInput).toHaveBeenCalledWith( + expect.objectContaining({ id: messageId }), + [], + previousResults, + [] + ); + }); + + it('should return error result when workflow returns no output', async () => { + const messageId = '123e4567-e89b-12d3-a456-426614174000'; + + vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({ + id: messageId, + chatId: 'chat-123', + createdBy: 'user-123', + createdAt: new Date(), + userName: 'John Doe', + organizationId: 'org-123', + }); + vi.mocked(helpers.fetchConversationHistory).mockResolvedValue([]); + vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]); + vi.mocked(helpers.fetchUserDatasets).mockResolvedValue([]); + vi.mocked(helpers.buildWorkflowInput).mockReturnValue({ + conversationHistory: undefined, + userName: 'John Doe', + messageId, + userId: 'user-123', + chatId: 'chat-123', + isFollowUp: false, + previousMessages: [], + datasets: '', + }); + mockWorkflowRun.start.mockResolvedValue({ + status: 'failed', + result: null, + }); + + const result = await runTask({ messageId }); + + expect(result).toEqual({ + success: false, + messageId, + error: { + code: 'WORKFLOW_EXECUTION_ERROR', + message: 'Post-processing workflow returned no output', + details: { + operation: 'message_post_processing_task_execution', + messageId, + }, + }, + }); + }); + + it('should return error result for message not found', async () => { + const messageId = 'non-existent-id'; + const error = new MessageNotFoundError(messageId); + + vi.mocked(helpers.fetchMessageWithContext).mockRejectedValue(error); + + const result = await runTask({ messageId }); + + expect(result).toEqual({ + success: false, + messageId, + error: { + code: 'MESSAGE_NOT_FOUND', + message: `Message not found: ${messageId}`, + details: { + operation: 'message_post_processing_task_execution', + messageId, + }, + }, + }); + }); + + it('should return error result for database update failure', async () => { + const messageId = '123e4567-e89b-12d3-a456-426614174000'; + const dbError = new Error('Database update failed'); + + vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({ + id: messageId, + chatId: 'chat-123', + createdBy: 'user-123', + createdAt: new Date(), + userName: 'John Doe', + organizationId: 'org-123', + }); + vi.mocked(helpers.fetchConversationHistory).mockResolvedValue([]); + vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]); + vi.mocked(helpers.fetchUserDatasets).mockResolvedValue([]); + vi.mocked(helpers.buildWorkflowInput).mockReturnValue({ + conversationHistory: undefined, + userName: 'John Doe', + messageId, + userId: 'user-123', + chatId: 'chat-123', + isFollowUp: false, + previousMessages: [], + datasets: '', + }); + mockWorkflowRun.start.mockResolvedValue({ + status: 'success', + result: { initial: { assumptions: [], flagForReview: false } }, + }); + mockDb.where.mockRejectedValue(dbError); + + const result = await runTask({ messageId }); + + expect(result).toEqual({ + success: false, + messageId, + error: { + code: 'DATABASE_ERROR', + message: 'Database update failed', + details: { + operation: 'message_post_processing_task_execution', + messageId, + }, + }, + }); + }); +}); diff --git a/apps/trigger/src/tasks/message-post-processing/message-post-processing.ts b/apps/trigger/src/tasks/message-post-processing/message-post-processing.ts new file mode 100644 index 000000000..dd48279d8 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/message-post-processing.ts @@ -0,0 +1,341 @@ +import postProcessingWorkflow, { + type PostProcessingWorkflowOutput, +} from '@buster/ai/workflows/post-processing-workflow'; +import { eq, getDb, messages } from '@buster/database'; +import { logger, schemaTask } from '@trigger.dev/sdk/v3'; +import { initLogger, wrapTraced } from 'braintrust'; +import { z } from 'zod'; +import { + buildWorkflowInput, + fetchConversationHistory, + fetchMessageWithContext, + fetchPreviousPostProcessingMessages, + fetchUserDatasets, + sendSlackNotification, +} from './helpers'; +import { + DataFetchError, + MessageNotFoundError, + TaskInputSchema, + type TaskOutputSchema, +} from './types'; +import type { TaskInput, TaskOutput } from './types'; + +// Schema for the subset of fields we want to save to the database +const PostProcessingDbDataSchema = z.object({ + summaryMessage: z.string().optional(), + summaryTitle: z.string().optional(), + formattedMessage: z.string().nullable().optional(), + assumptions: z + .array( + z.object({ + descriptiveTitle: z.string(), + classification: z.enum([ + 'fieldMapping', + 'tableRelationship', + 'dataQuality', + 'dataFormat', + 'dataAvailability', + 'timePeriodInterpretation', + 'timePeriodGranularity', + 'metricInterpretation', + 'segmentInterpretation', + 'quantityInterpretation', + 'requestScope', + 'metricDefinition', + 'segmentDefinition', + 'businessLogic', + 'policyInterpretation', + 'optimization', + 'aggregation', + 'filtering', + 'sorting', + 'grouping', + 'calculationMethod', + 'dataRelevance', + ]), + explanation: z.string(), + label: z.enum(['timeRelated', 'vagueRequest', 'major', 'minor']), + }) + ) + .optional(), + message: z.string().optional(), + toolCalled: z.string(), + userName: z.string().nullable().optional(), +}); + +type PostProcessingDbData = z.infer; + +/** + * Extract only the specific fields we want to save to the database + */ +function extractDbFields( + workflowOutput: PostProcessingWorkflowOutput, + userName: string | null +): PostProcessingDbData { + const extracted = { + summaryMessage: workflowOutput.summaryMessage, + summaryTitle: workflowOutput.summaryTitle, + formattedMessage: workflowOutput.formattedMessage, + assumptions: workflowOutput.assumptions, + message: workflowOutput.message, + toolCalled: workflowOutput.toolCalled || 'unknown', // Provide default if missing + userName, + }; + + // Validate the extracted data matches our schema + return PostProcessingDbDataSchema.parse(extracted); +} + +/** + * Message Post-Processing Task + * + * Processes messages after creation to extract insights, assumptions, + * and generate follow-up suggestions using AI workflows. + */ +export const messagePostProcessingTask: ReturnType< + typeof schemaTask<'message-post-processing', typeof TaskInputSchema, TaskOutput> +> = schemaTask<'message-post-processing', typeof TaskInputSchema, TaskOutput>({ + id: 'message-post-processing', + schema: TaskInputSchema, + maxDuration: 300, // 300 seconds timeout + run: async (payload: TaskInput): Promise => { + const startTime = Date.now(); + + if (!process.env.BRAINTRUST_KEY) { + throw new Error('BRAINTRUST_KEY is not set'); + } + + // Initialize Braintrust logging for observability + initLogger({ + apiKey: process.env.BRAINTRUST_KEY, + projectName: process.env.ENVIRONMENT || 'development', + }); + + try { + logger.log('Starting message post-processing task', { + messageId: payload.messageId, + }); + + // Step 1: Fetch message context (this will throw if message not found) + const messageContext = await fetchMessageWithContext(payload.messageId); + logger.log('Fetched message context', { + chatId: messageContext.chatId, + userId: messageContext.createdBy, + organizationId: messageContext.organizationId, + }); + + // Step 2: Fetch all required data concurrently + const [conversationMessages, previousPostProcessingResults, datasets] = await Promise.all([ + fetchConversationHistory(messageContext.chatId), + fetchPreviousPostProcessingMessages(messageContext.chatId, messageContext.createdAt), + fetchUserDatasets(messageContext.createdBy), + ]); + + logger.log('Fetched required data', { + messageId: payload.messageId, + conversationMessagesCount: conversationMessages.length, + previousPostProcessingCount: previousPostProcessingResults.length, + datasetsCount: datasets.length, + }); + + // Step 3: Build workflow input + const workflowInput = buildWorkflowInput( + messageContext, + conversationMessages, + previousPostProcessingResults, + datasets + ); + + logger.log('Built workflow input', { + messageId: payload.messageId, + isFollowUp: workflowInput.isFollowUp, + previousMessagesCount: workflowInput.previousMessages.length, + hasConversationHistory: !!workflowInput.conversationHistory, + datasetsLength: workflowInput.datasets.length, + }); + + // Step 4: Execute post-processing workflow + logger.log('Starting post-processing workflow execution', { + messageId: payload.messageId, + }); + + const tracedWorkflow = wrapTraced( + async () => { + const run = postProcessingWorkflow.createRun(); + return await run.start({ + inputData: workflowInput, + }); + }, + { + name: 'Message Post-Processing Workflow', + } + ); + + const workflowResult = await tracedWorkflow(); + + if (!workflowResult || workflowResult.status !== 'success' || !workflowResult.result) { + throw new Error('Post-processing workflow returned no output'); + } + + // Handle branch results - the result will have one of the branch step IDs as a key + let validatedOutput: PostProcessingWorkflowOutput; + const branchResult = workflowResult.result as any; // Type assertion needed for branch results + + if ('format-follow-up-message' in branchResult && branchResult['format-follow-up-message']) { + validatedOutput = branchResult['format-follow-up-message'] as PostProcessingWorkflowOutput; + } else if ( + 'format-initial-message' in branchResult && + branchResult['format-initial-message'] + ) { + validatedOutput = branchResult['format-initial-message'] as PostProcessingWorkflowOutput; + } else { + logger.error('Unexpected workflow result structure', { + messageId: payload.messageId, + resultKeys: Object.keys(branchResult), + result: branchResult, + }); + throw new Error('Post-processing workflow returned unexpected result structure'); + } + + logger.log('Validated output', { + messageId: payload.messageId, + summaryTitle: validatedOutput.summaryTitle, + summaryMessage: validatedOutput.summaryMessage, + flagChatMessage: validatedOutput.flagChatMessage, + flagChatTitle: validatedOutput.flagChatTitle, + toolCalled: validatedOutput.toolCalled, + assumptions: validatedOutput.assumptions, + message: validatedOutput.message, + }); + + // Step 5: Store result in database + logger.log('Storing post-processing result in database', { + messageId: payload.messageId, + }); + + const db = getDb(); + + const dbData = extractDbFields(validatedOutput, messageContext.userName); + await db + .update(messages) + .set({ + postProcessingMessage: dbData, + updatedAt: new Date().toISOString(), + }) + .where(eq(messages.id, payload.messageId)); + + // Step 6: Send Slack notification if conditions are met + logger.log('Checking Slack notification conditions', { + messageId: payload.messageId, + organizationId: messageContext.organizationId, + summaryTitle: dbData.summaryTitle, + summaryMessage: dbData.summaryMessage, + formattedMessage: dbData.formattedMessage, + toolCalled: dbData.toolCalled, + }); + + const slackResult = await sendSlackNotification({ + organizationId: messageContext.organizationId, + userName: messageContext.userName, + summaryTitle: dbData.summaryTitle, + summaryMessage: dbData.summaryMessage, + formattedMessage: dbData.formattedMessage, + toolCalled: dbData.toolCalled, + message: dbData.message, + }); + + if (slackResult.sent) { + logger.log('Slack notification sent successfully', { + messageId: payload.messageId, + organizationId: messageContext.organizationId, + }); + } else { + logger.log('Slack notification not sent', { + messageId: payload.messageId, + organizationId: messageContext.organizationId, + reason: slackResult.error, + }); + } + + logger.log('Message post-processing completed successfully', { + messageId: payload.messageId, + executionTimeMs: Date.now() - startTime, + }); + + // Wait 500ms to allow Braintrust to clean up its trace before completing + await new Promise((resolve) => setTimeout(resolve, 500)); + + return { + success: true, + messageId: payload.messageId, + result: { + success: true, + messageId: payload.messageId, + executionTimeMs: Date.now() - startTime, + workflowCompleted: true, + }, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + + logger.error('Post-processing task execution failed', { + messageId: payload.messageId, + error: errorMessage, + executionTimeMs: Date.now() - startTime, + }); + + return { + success: false, + messageId: payload.messageId, + error: { + code: getErrorCode(error), + message: errorMessage, + details: { + operation: 'message_post_processing_task_execution', + messageId: payload.messageId, + }, + }, + }; + } + }, +}); + +/** + * Get error code from error object for consistent error handling + */ +function getErrorCode(error: unknown): string { + if (error instanceof MessageNotFoundError) { + return 'MESSAGE_NOT_FOUND'; + } + + if (error instanceof DataFetchError) { + return 'DATA_FETCH_ERROR'; + } + + if (error instanceof Error) { + // Validation errors + if (error.name === 'ZodError' || error.name === 'ValidationError') { + return 'VALIDATION_ERROR'; + } + + // Workflow errors + if (error.message.includes('workflow')) { + return 'WORKFLOW_EXECUTION_ERROR'; + } + + // Database errors + if (error.message.includes('database') || error.message.includes('Database')) { + return 'DATABASE_ERROR'; + } + + // Permission errors + if (error.message.includes('permission') || error.message.includes('access')) { + return 'ACCESS_DENIED'; + } + } + + return 'UNKNOWN_ERROR'; +} + +export type MessagePostProcessingTask = typeof messagePostProcessingTask; diff --git a/apps/trigger/src/tasks/message-post-processing/types.ts b/apps/trigger/src/tasks/message-post-processing/types.ts new file mode 100644 index 000000000..af7c97748 --- /dev/null +++ b/apps/trigger/src/tasks/message-post-processing/types.ts @@ -0,0 +1,81 @@ +import type { CoreMessage } from 'ai'; +import { z } from 'zod'; + +// Input schema - simple UUID validation +export const UUIDSchema = z.string().uuid('Must be a valid UUID'); + +export const TaskInputSchema = z.object({ + messageId: UUIDSchema, +}); + +// Task execution result for internal monitoring +export const TaskExecutionResultSchema = z.object({ + success: z.boolean(), + messageId: z.string(), + executionTimeMs: z.number(), + workflowCompleted: z.boolean(), + error: z + .object({ + code: z.string(), + message: z.string(), + details: z.record(z.any()).optional(), + }) + .optional(), +}); + +// Main output schema - what Trigger.dev expects +export const TaskOutputSchema = z.object({ + success: z.boolean(), + messageId: z.string(), + result: TaskExecutionResultSchema.optional(), + error: z + .object({ + code: z.string(), + message: z.string(), + details: z.record(z.any()).optional(), + }) + .optional(), +}); + +// Database output schemas +export const MessageContextSchema = z.object({ + id: z.string(), + chatId: z.string(), + createdBy: z.string(), + createdAt: z.date(), + userName: z.string().nullable(), + organizationId: z.string(), +}); + +export const ConversationMessageSchema = z.object({ + id: z.string(), + rawLlmMessages: z.custom(), + createdAt: z.date(), +}); + +export const PostProcessingResultSchema = z.object({ + postProcessingMessage: z.record(z.unknown()), + createdAt: z.date(), +}); + +// Infer TypeScript types from schemas +export type TaskInput = z.infer; +export type TaskOutput = z.infer; +export type MessageContext = z.infer; +export type ConversationMessage = z.infer; +export type PostProcessingResult = z.infer; + +// Error types +export class MessageNotFoundError extends Error { + constructor(messageId: string) { + super(`Message not found: ${messageId}`); + this.name = 'MessageNotFoundError'; + } +} + +export class DataFetchError extends Error { + constructor(message: string, options?: { cause?: Error }) { + super(message, options); + this.name = 'DataFetchError'; + } +} diff --git a/packages/ai/src/steps/post-processing/format-follow-up-message-step.ts b/packages/ai/src/steps/post-processing/format-follow-up-message-step.ts index 61e0d7310..7d9c0c4eb 100644 --- a/packages/ai/src/steps/post-processing/format-follow-up-message-step.ts +++ b/packages/ai/src/steps/post-processing/format-follow-up-message-step.ts @@ -268,4 +268,4 @@ export const formatFollowUpMessageStep = createStep({ inputSchema, outputSchema: formatFollowUpMessageOutputSchema, execute: formatFollowUpMessageStepExecution, -}); +}); \ No newline at end of file diff --git a/packages/ai/src/workflows/post-processing-workflow.ts b/packages/ai/src/workflows/post-processing-workflow.ts index b5ea98772..1a213a62b 100644 --- a/packages/ai/src/workflows/post-processing-workflow.ts +++ b/packages/ai/src/workflows/post-processing-workflow.ts @@ -53,4 +53,4 @@ const postProcessingWorkflow = createWorkflow({ export default postProcessingWorkflow; // Re-export schemas for external use -export { postProcessingWorkflowInputSchema, postProcessingWorkflowOutputSchema }; +export { postProcessingWorkflowInputSchema, postProcessingWorkflowOutputSchema }; \ No newline at end of file diff --git a/packages/database/drizzle/0073_lovely_white_tiger.sql b/packages/database/drizzle/0073_lovely_white_tiger.sql index 94cd09bc1..3319237f9 100644 --- a/packages/database/drizzle/0073_lovely_white_tiger.sql +++ b/packages/database/drizzle/0073_lovely_white_tiger.sql @@ -1 +1 @@ -ALTER TABLE "slack_integrations" ADD COLUMN "default_channel" jsonb DEFAULT '{}'::jsonb; \ No newline at end of file +ALTER TABLE "slack_integrations" ADD COLUMN "default_channel" jsonb DEFAULT 'null'::jsonb; \ No newline at end of file diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index bc69a394c..64651d0cd 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -1,1949 +1,1951 @@ import { sql } from 'drizzle-orm'; import { - bigint, - boolean, - check, - doublePrecision, - foreignKey, - index, - integer, - jsonb, - pgEnum, - pgPolicy, - pgTable, - primaryKey, - text, - timestamp, - unique, - uniqueIndex, - uuid, - varchar, + bigint, + boolean, + check, + doublePrecision, + foreignKey, + index, + integer, + jsonb, + pgEnum, + pgPolicy, + pgTable, + primaryKey, + text, + timestamp, + unique, + uniqueIndex, + uuid, + varchar, } from 'drizzle-orm/pg-core'; export const assetPermissionRoleEnum = pgEnum('asset_permission_role_enum', [ - 'owner', - 'editor', - 'viewer', - 'full_access', - 'can_edit', - 'can_filter', - 'can_view', + 'owner', + 'editor', + 'viewer', + 'full_access', + 'can_edit', + 'can_filter', + 'can_view', ]); export const assetTypeEnum = pgEnum('asset_type_enum', [ - 'dashboard', - 'thread', - 'collection', - 'chat', - 'metric_file', - 'dashboard_file', + 'dashboard', + 'thread', + 'collection', + 'chat', + 'metric_file', + 'dashboard_file', ]); export const dataSourceOnboardingStatusEnum = pgEnum('data_source_onboarding_status_enum', [ - 'notStarted', - 'inProgress', - 'completed', - 'failed', + 'notStarted', + 'inProgress', + 'completed', + 'failed', ]); export const datasetTypeEnum = pgEnum('dataset_type_enum', ['table', 'view', 'materializedView']); export const identityTypeEnum = pgEnum('identity_type_enum', ['user', 'team', 'organization']); export const messageFeedbackEnum = pgEnum('message_feedback_enum', ['positive', 'negative']); export const sharingSettingEnum = pgEnum('sharing_setting_enum', [ - 'none', - 'team', - 'organization', - 'public', + 'none', + 'team', + 'organization', + 'public', ]); export const storedValuesStatusEnum = pgEnum('stored_values_status_enum', [ - 'syncing', - 'success', - 'failed', + 'syncing', + 'success', + 'failed', ]); export const teamRoleEnum = pgEnum('team_role_enum', ['manager', 'member']); export const userOrganizationRoleEnum = pgEnum('user_organization_role_enum', [ - 'workspace_admin', - 'data_admin', - 'querier', - 'restricted_querier', - 'viewer', + 'workspace_admin', + 'data_admin', + 'querier', + 'restricted_querier', + 'viewer', ]); export const userOrganizationStatusEnum = pgEnum('user_organization_status_enum', [ - 'active', - 'inactive', - 'pending', - 'guest', + 'active', + 'inactive', + 'pending', + 'guest', ]); export const verificationEnum = pgEnum('verification_enum', [ - 'verified', - 'backlogged', - 'inReview', - 'requested', - 'notRequested', + 'verified', + 'backlogged', + 'inReview', + 'requested', + 'notRequested', ]); export const tableTypeEnum = pgEnum('table_type_enum', [ - 'TABLE', - 'VIEW', - 'MATERIALIZED_VIEW', - 'EXTERNAL_TABLE', - 'TEMPORARY_TABLE', + 'TABLE', + 'VIEW', + 'MATERIALIZED_VIEW', + 'EXTERNAL_TABLE', + 'TEMPORARY_TABLE', ]); export const slackIntegrationStatusEnum = pgEnum('slack_integration_status_enum', [ - 'pending', - 'active', - 'failed', - 'revoked', + 'pending', + 'active', + 'failed', + 'revoked', ]); export const apiKeys = pgTable( - 'api_keys', - { - id: uuid().defaultRandom().primaryKey().notNull(), - ownerId: uuid('owner_id').notNull(), - key: text().notNull(), - organizationId: uuid('organization_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'api_keys_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.ownerId], - foreignColumns: [users.id], - name: 'api_keys_owner_id_fkey', - }).onUpdate('cascade'), - unique('api_keys_key_key').on(table.key), - ], + 'api_keys', + { + id: uuid().defaultRandom().primaryKey().notNull(), + ownerId: uuid('owner_id').notNull(), + key: text().notNull(), + organizationId: uuid('organization_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'api_keys_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.ownerId], + foreignColumns: [users.id], + name: 'api_keys_owner_id_fkey', + }).onUpdate('cascade'), + unique('api_keys_key_key').on(table.key), + ] ); export const teams = pgTable( - 'teams', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - organizationId: uuid('organization_id').notNull(), - sharingSetting: sharingSettingEnum('sharing_setting').default('none').notNull(), - editSql: boolean('edit_sql').default(false).notNull(), - uploadCsv: boolean('upload_csv').default(false).notNull(), - exportAssets: boolean('export_assets').default(false).notNull(), - emailSlackEnabled: boolean('email_slack_enabled').default(false).notNull(), - createdBy: uuid('created_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'teams_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'teams_created_by_fkey', - }).onUpdate('cascade'), - unique('teams_name_key').on(table.name), - ], + 'teams', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + organizationId: uuid('organization_id').notNull(), + sharingSetting: sharingSettingEnum('sharing_setting').default('none').notNull(), + editSql: boolean('edit_sql').default(false).notNull(), + uploadCsv: boolean('upload_csv').default(false).notNull(), + exportAssets: boolean('export_assets').default(false).notNull(), + emailSlackEnabled: boolean('email_slack_enabled').default(false).notNull(), + createdBy: uuid('created_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'teams_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'teams_created_by_fkey', + }).onUpdate('cascade'), + unique('teams_name_key').on(table.name), + ] ); export const permissionGroups = pgTable( - 'permission_groups', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'permission_groups_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'permission_groups_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'permission_groups_updated_by_fkey', - }).onUpdate('cascade'), - ], + 'permission_groups', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'permission_groups_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'permission_groups_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'permission_groups_updated_by_fkey', + }).onUpdate('cascade'), + ] ); export const terms = pgTable( - 'terms', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - definition: text(), - sqlSnippet: text('sql_snippet'), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'terms_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'terms_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'terms_updated_by_fkey', - }).onUpdate('cascade'), - ], + 'terms', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + definition: text(), + sqlSnippet: text('sql_snippet'), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'terms_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'terms_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'terms_updated_by_fkey', + }).onUpdate('cascade'), + ] ); export const collections = pgTable( - 'collections', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - description: text(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - organizationId: uuid('organization_id').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'collections_organization_id_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'collections_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'collections_updated_by_fkey', - }).onUpdate('cascade'), - ], + 'collections', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + description: text(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + organizationId: uuid('organization_id').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'collections_organization_id_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'collections_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'collections_updated_by_fkey', + }).onUpdate('cascade'), + ] ); export const dashboards = pgTable( - 'dashboards', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - description: text(), - config: jsonb().notNull(), - publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), - publiclyEnabledBy: uuid('publicly_enabled_by'), - publicExpiryDate: timestamp('public_expiry_date', { - withTimezone: true, - mode: 'string', - }), - passwordSecretId: uuid('password_secret_id'), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - organizationId: uuid('organization_id').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'dashboards_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'dashboards_organization_id_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'dashboards_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'dashboards_updated_by_fkey', - }).onUpdate('cascade'), - ], + 'dashboards', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + description: text(), + config: jsonb().notNull(), + publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), + publiclyEnabledBy: uuid('publicly_enabled_by'), + publicExpiryDate: timestamp('public_expiry_date', { + withTimezone: true, + mode: 'string', + }), + passwordSecretId: uuid('password_secret_id'), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + organizationId: uuid('organization_id').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'dashboards_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'dashboards_organization_id_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'dashboards_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'dashboards_updated_by_fkey', + }).onUpdate('cascade'), + ] ); export const dashboardVersions = pgTable( - 'dashboard_versions', - { - id: uuid().defaultRandom().primaryKey().notNull(), - dashboardId: uuid('dashboard_id').notNull(), - config: jsonb().notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.dashboardId], - foreignColumns: [dashboards.id], - name: 'dashboard_versions_dashboard_id_fkey', - }).onDelete('cascade'), - ], + 'dashboard_versions', + { + id: uuid().defaultRandom().primaryKey().notNull(), + dashboardId: uuid('dashboard_id').notNull(), + config: jsonb().notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.dashboardId], + foreignColumns: [dashboards.id], + name: 'dashboard_versions_dashboard_id_fkey', + }).onDelete('cascade'), + ] ); export const dataSources = pgTable( - 'data_sources', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - type: text().notNull(), - secretId: uuid('secret_id').notNull(), - onboardingStatus: dataSourceOnboardingStatusEnum('onboarding_status') - .default('notStarted') - .notNull(), - onboardingError: text('onboarding_error'), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - env: varchar().default('dev').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'data_sources_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'data_sources_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'data_sources_updated_by_fkey', - }).onUpdate('cascade'), - unique('data_sources_name_organization_id_env_key').on( - table.name, - table.organizationId, - table.env, - ), - ], + 'data_sources', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + type: text().notNull(), + secretId: uuid('secret_id').notNull(), + onboardingStatus: dataSourceOnboardingStatusEnum('onboarding_status') + .default('notStarted') + .notNull(), + onboardingError: text('onboarding_error'), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + env: varchar().default('dev').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'data_sources_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'data_sources_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'data_sources_updated_by_fkey', + }).onUpdate('cascade'), + unique('data_sources_name_organization_id_env_key').on( + table.name, + table.organizationId, + table.env + ), + ] ); export const datasetColumns = pgTable( - 'dataset_columns', - { - id: uuid().primaryKey().notNull(), - datasetId: uuid('dataset_id').notNull(), - name: text().notNull(), - type: text().notNull(), - description: text(), - nullable: boolean().notNull(), - createdAt: timestamp('created_at', { - withTimezone: true, - mode: 'string', - }).notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - storedValues: boolean('stored_values').default(false), - storedValuesStatus: storedValuesStatusEnum('stored_values_status'), - storedValuesError: text('stored_values_error'), - // You can use { mode: "bigint" } if numbers are exceeding js number limitations - storedValuesCount: bigint('stored_values_count', { mode: 'number' }), - storedValuesLastSynced: timestamp('stored_values_last_synced', { - withTimezone: true, - mode: 'string', - }), - semanticType: text('semantic_type'), - dimType: text('dim_type'), - expr: text(), - }, - (table) => [unique('unique_dataset_column_name').on(table.datasetId, table.name)], + 'dataset_columns', + { + id: uuid().primaryKey().notNull(), + datasetId: uuid('dataset_id').notNull(), + name: text().notNull(), + type: text().notNull(), + description: text(), + nullable: boolean().notNull(), + createdAt: timestamp('created_at', { + withTimezone: true, + mode: 'string', + }).notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + storedValues: boolean('stored_values').default(false), + storedValuesStatus: storedValuesStatusEnum('stored_values_status'), + storedValuesError: text('stored_values_error'), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + storedValuesCount: bigint('stored_values_count', { mode: 'number' }), + storedValuesLastSynced: timestamp('stored_values_last_synced', { + withTimezone: true, + mode: 'string', + }), + semanticType: text('semantic_type'), + dimType: text('dim_type'), + expr: text(), + }, + (table) => [unique('unique_dataset_column_name').on(table.datasetId, table.name)] ); export const sqlEvaluations = pgTable('sql_evaluations', { - id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(), - evaluationObj: jsonb('evaluation_obj').notNull(), - evaluationSummary: text('evaluation_summary').notNull(), - score: text().notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(), + evaluationObj: jsonb('evaluation_obj').notNull(), + evaluationSummary: text('evaluation_summary').notNull(), + score: text().notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), }); export const assetSearch = pgTable( - 'asset_search', - { - id: uuid().defaultRandom().primaryKey().notNull(), - assetId: uuid('asset_id').notNull(), - assetType: text('asset_type').notNull(), - content: text().notNull(), - organizationId: uuid('organization_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - uniqueIndex('asset_search_asset_id_asset_type_idx').using( - 'btree', - table.assetId.asc().nullsLast().op('text_ops'), - table.assetType.asc().nullsLast().op('text_ops'), - ), - index('pgroonga_content_index').using( - 'pgroonga', - table.content.asc().nullsLast().op('pgroonga_text_full_text_search_ops_v2'), - ), - ], + 'asset_search', + { + id: uuid().defaultRandom().primaryKey().notNull(), + assetId: uuid('asset_id').notNull(), + assetType: text('asset_type').notNull(), + content: text().notNull(), + organizationId: uuid('organization_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + uniqueIndex('asset_search_asset_id_asset_type_idx').using( + 'btree', + table.assetId.asc().nullsLast().op('text_ops'), + table.assetType.asc().nullsLast().op('text_ops') + ), + index('pgroonga_content_index').using( + 'pgroonga', + table.content.asc().nullsLast().op('pgroonga_text_full_text_search_ops_v2') + ), + ] ); export const datasetGroups = pgTable( - 'dataset_groups', - { - id: uuid().defaultRandom().primaryKey().notNull(), - organizationId: uuid('organization_id').notNull(), - name: varchar().notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - index('dataset_groups_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('dataset_groups_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'dataset_groups_organization_id_fkey', - }).onDelete('cascade'), - pgPolicy('dataset_groups_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - ], + 'dataset_groups', + { + id: uuid().defaultRandom().primaryKey().notNull(), + organizationId: uuid('organization_id').notNull(), + name: varchar().notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + index('dataset_groups_deleted_at_idx').using( + 'btree', + table.deletedAt.asc().nullsLast().op('timestamptz_ops') + ), + index('dataset_groups_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'dataset_groups_organization_id_fkey', + }).onDelete('cascade'), + pgPolicy('dataset_groups_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + ] ); export const datasetPermissions = pgTable( - 'dataset_permissions', - { - id: uuid().defaultRandom().primaryKey().notNull(), - organizationId: uuid('organization_id').notNull(), - datasetId: uuid('dataset_id').notNull(), - permissionId: uuid('permission_id').notNull(), - permissionType: varchar('permission_type').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - index('dataset_permissions_dataset_id_idx').using( - 'btree', - table.datasetId.asc().nullsLast().op('uuid_ops'), - ), - index('dataset_permissions_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('dataset_permissions_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - index('dataset_permissions_permission_lookup_idx').using( - 'btree', - table.permissionId.asc().nullsLast().op('uuid_ops'), - table.permissionType.asc().nullsLast().op('text_ops'), - ), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'dataset_permissions_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'dataset_permissions_dataset_id_fkey', - }).onDelete('cascade'), - unique('dataset_permissions_dataset_id_permission_id_permission_typ_key').on( - table.datasetId, - table.permissionId, - table.permissionType, - ), - pgPolicy('dataset_permissions_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - check( - 'dataset_permissions_permission_type_check', - sql`(permission_type)::text = ANY ((ARRAY['user'::character varying, 'dataset_group'::character varying, 'permission_group'::character varying])::text[])`, - ), - ], + 'dataset_permissions', + { + id: uuid().defaultRandom().primaryKey().notNull(), + organizationId: uuid('organization_id').notNull(), + datasetId: uuid('dataset_id').notNull(), + permissionId: uuid('permission_id').notNull(), + permissionType: varchar('permission_type').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + index('dataset_permissions_dataset_id_idx').using( + 'btree', + table.datasetId.asc().nullsLast().op('uuid_ops') + ), + index('dataset_permissions_deleted_at_idx').using( + 'btree', + table.deletedAt.asc().nullsLast().op('timestamptz_ops') + ), + index('dataset_permissions_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + index('dataset_permissions_permission_lookup_idx').using( + 'btree', + table.permissionId.asc().nullsLast().op('uuid_ops'), + table.permissionType.asc().nullsLast().op('text_ops') + ), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'dataset_permissions_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'dataset_permissions_dataset_id_fkey', + }).onDelete('cascade'), + unique('dataset_permissions_dataset_id_permission_id_permission_typ_key').on( + table.datasetId, + table.permissionId, + table.permissionType + ), + pgPolicy('dataset_permissions_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + check( + 'dataset_permissions_permission_type_check', + sql`(permission_type)::text = ANY ((ARRAY['user'::character varying, 'dataset_group'::character varying, 'permission_group'::character varying])::text[])` + ), + ] ); export const dieselSchemaMigrations = pgTable( - '__diesel_schema_migrations', - { - version: varchar({ length: 50 }).primaryKey().notNull(), - runOn: timestamp('run_on', { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - }, - (_table) => [ - pgPolicy('diesel_schema_migrations_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - ], + '__diesel_schema_migrations', + { + version: varchar({ length: 50 }).primaryKey().notNull(), + runOn: timestamp('run_on', { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + }, + (_table) => [ + pgPolicy('diesel_schema_migrations_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + ] ); export const datasetGroupsPermissions = pgTable( - 'dataset_groups_permissions', - { - id: uuid().defaultRandom().primaryKey().notNull(), - datasetGroupId: uuid('dataset_group_id').notNull(), - permissionId: uuid('permission_id').notNull(), - permissionType: varchar('permission_type').notNull(), - organizationId: uuid('organization_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - index('dataset_groups_permissions_dataset_group_id_idx').using( - 'btree', - table.datasetGroupId.asc().nullsLast().op('uuid_ops'), - ), - index('dataset_groups_permissions_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - index('dataset_groups_permissions_permission_id_idx').using( - 'btree', - table.permissionId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.datasetGroupId], - foreignColumns: [datasetGroups.id], - name: 'dataset_groups_permissions_dataset_group_id_fkey', - }), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'dataset_groups_permissions_organization_id_fkey', - }), - unique('unique_dataset_group_permission').on( - table.datasetGroupId, - table.permissionId, - table.permissionType, - ), - ], + 'dataset_groups_permissions', + { + id: uuid().defaultRandom().primaryKey().notNull(), + datasetGroupId: uuid('dataset_group_id').notNull(), + permissionId: uuid('permission_id').notNull(), + permissionType: varchar('permission_type').notNull(), + organizationId: uuid('organization_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + index('dataset_groups_permissions_dataset_group_id_idx').using( + 'btree', + table.datasetGroupId.asc().nullsLast().op('uuid_ops') + ), + index('dataset_groups_permissions_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + index('dataset_groups_permissions_permission_id_idx').using( + 'btree', + table.permissionId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.datasetGroupId], + foreignColumns: [datasetGroups.id], + name: 'dataset_groups_permissions_dataset_group_id_fkey', + }), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'dataset_groups_permissions_organization_id_fkey', + }), + unique('unique_dataset_group_permission').on( + table.datasetGroupId, + table.permissionId, + table.permissionType + ), + ] ); export const threadsDeprecated = pgTable( - 'threads_deprecated', - { - id: uuid().defaultRandom().primaryKey().notNull(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), - publiclyEnabledBy: uuid('publicly_enabled_by'), - publicExpiryDate: timestamp('public_expiry_date', { - withTimezone: true, - mode: 'string', - }), - passwordSecretId: uuid('password_secret_id'), - stateMessageId: uuid('state_message_id'), - parentThreadId: uuid('parent_thread_id'), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - organizationId: uuid('organization_id').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'threads_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'threads_updated_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'threads_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.parentThreadId], - foreignColumns: [table.id], - name: 'threads_parent_thread_id_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'threads_organization_id_fkey', - }), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'threads_deprecated_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'threads_deprecated_updated_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'threads_deprecated_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - ], + 'threads_deprecated', + { + id: uuid().defaultRandom().primaryKey().notNull(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), + publiclyEnabledBy: uuid('publicly_enabled_by'), + publicExpiryDate: timestamp('public_expiry_date', { + withTimezone: true, + mode: 'string', + }), + passwordSecretId: uuid('password_secret_id'), + stateMessageId: uuid('state_message_id'), + parentThreadId: uuid('parent_thread_id'), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + organizationId: uuid('organization_id').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'threads_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'threads_updated_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'threads_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.parentThreadId], + foreignColumns: [table.id], + name: 'threads_parent_thread_id_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'threads_organization_id_fkey', + }), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'threads_deprecated_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'threads_deprecated_updated_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'threads_deprecated_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + ] ); export const messagesDeprecated = pgTable( - 'messages_deprecated', - { - id: uuid().defaultRandom().primaryKey().notNull(), - threadId: uuid('thread_id').notNull(), - sentBy: uuid('sent_by').notNull(), - message: text().notNull(), - responses: jsonb(), - code: text(), - context: jsonb(), - title: text(), - feedback: messageFeedbackEnum(), - verification: verificationEnum().default('notRequested').notNull(), - datasetId: uuid('dataset_id'), - chartConfig: jsonb('chart_config').default({}), - chartRecommendations: jsonb('chart_recommendations').default({}), - timeFrame: text('time_frame'), - dataMetadata: jsonb('data_metadata'), - draftSessionId: uuid('draft_session_id'), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - draftState: jsonb('draft_state'), - summaryQuestion: text('summary_question'), - sqlEvaluationId: uuid('sql_evaluation_id'), - }, - (table) => [ - foreignKey({ - columns: [table.sentBy], - foreignColumns: [users.id], - name: 'messages_sent_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'messages_dataset_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.sentBy], - foreignColumns: [users.id], - name: 'messages_deprecated_sent_by_fkey', - }).onUpdate('cascade'), - ], + 'messages_deprecated', + { + id: uuid().defaultRandom().primaryKey().notNull(), + threadId: uuid('thread_id').notNull(), + sentBy: uuid('sent_by').notNull(), + message: text().notNull(), + responses: jsonb(), + code: text(), + context: jsonb(), + title: text(), + feedback: messageFeedbackEnum(), + verification: verificationEnum().default('notRequested').notNull(), + datasetId: uuid('dataset_id'), + chartConfig: jsonb('chart_config').default({}), + chartRecommendations: jsonb('chart_recommendations').default({}), + timeFrame: text('time_frame'), + dataMetadata: jsonb('data_metadata'), + draftSessionId: uuid('draft_session_id'), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + draftState: jsonb('draft_state'), + summaryQuestion: text('summary_question'), + sqlEvaluationId: uuid('sql_evaluation_id'), + }, + (table) => [ + foreignKey({ + columns: [table.sentBy], + foreignColumns: [users.id], + name: 'messages_sent_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'messages_dataset_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.sentBy], + foreignColumns: [users.id], + name: 'messages_deprecated_sent_by_fkey', + }).onUpdate('cascade'), + ] ); export const datasets = pgTable( - 'datasets', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - databaseName: text('database_name').notNull(), - whenToUse: text('when_to_use'), - whenNotToUse: text('when_not_to_use'), - type: datasetTypeEnum().notNull(), - definition: text().notNull(), - schema: text().notNull(), - enabled: boolean().default(false).notNull(), - imported: boolean().default(false).notNull(), - dataSourceId: uuid('data_source_id').notNull(), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - model: text(), - ymlFile: text('yml_file'), - databaseIdentifier: text('database_identifier'), - }, - (table) => [ - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'datasets_data_source_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'datasets_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'datasets_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'datasets_updated_by_fkey', - }).onUpdate('cascade'), - unique('datasets_database_name_data_source_id_key').on(table.databaseName, table.dataSourceId), - ], + 'datasets', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + databaseName: text('database_name').notNull(), + whenToUse: text('when_to_use'), + whenNotToUse: text('when_not_to_use'), + type: datasetTypeEnum().notNull(), + definition: text().notNull(), + schema: text().notNull(), + enabled: boolean().default(false).notNull(), + imported: boolean().default(false).notNull(), + dataSourceId: uuid('data_source_id').notNull(), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + model: text(), + ymlFile: text('yml_file'), + databaseIdentifier: text('database_identifier'), + }, + (table) => [ + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'datasets_data_source_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'datasets_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'datasets_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'datasets_updated_by_fkey', + }).onUpdate('cascade'), + unique('datasets_database_name_data_source_id_key').on(table.databaseName, table.dataSourceId), + ] ); export const users = pgTable( - 'users', - { - id: uuid().defaultRandom().primaryKey().notNull(), - email: text().notNull(), - name: text(), - config: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - attributes: jsonb().default({}).notNull(), - avatarUrl: text('avatar_url'), - }, - (table) => [unique('users_email_key').on(table.email)], + 'users', + { + id: uuid().defaultRandom().primaryKey().notNull(), + email: text().notNull(), + name: text(), + config: jsonb().default({}).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + attributes: jsonb().default({}).notNull(), + avatarUrl: text('avatar_url'), + }, + (table) => [unique('users_email_key').on(table.email)] ); export const messages = pgTable( - 'messages', - { - id: uuid().defaultRandom().primaryKey().notNull(), - requestMessage: text('request_message'), - responseMessages: jsonb('response_messages').notNull(), - reasoning: jsonb().notNull(), - title: text().notNull(), - rawLlmMessages: jsonb('raw_llm_messages').notNull(), - finalReasoningMessage: text('final_reasoning_message'), - chatId: uuid('chat_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - feedback: text(), - isCompleted: boolean('is_completed').default(false).notNull(), - }, - (table) => [ - index('messages_chat_id_idx').using('btree', table.chatId.asc().nullsLast().op('uuid_ops')), - index('messages_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('messages_created_by_idx').using( - 'btree', - table.createdBy.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.chatId], - foreignColumns: [chats.id], - name: 'messages_chat_id_fkey', - }), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'messages_created_by_fkey', - }).onUpdate('cascade'), - ], + 'messages', + { + id: uuid().defaultRandom().primaryKey().notNull(), + requestMessage: text('request_message'), + responseMessages: jsonb('response_messages').notNull(), + reasoning: jsonb().notNull(), + title: text().notNull(), + rawLlmMessages: jsonb('raw_llm_messages').notNull(), + finalReasoningMessage: text('final_reasoning_message'), + chatId: uuid('chat_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + feedback: text(), + isCompleted: boolean('is_completed').default(false).notNull(), + }, + (table) => [ + index('messages_chat_id_idx').using('btree', table.chatId.asc().nullsLast().op('uuid_ops')), + index('messages_created_at_idx').using( + 'btree', + table.createdAt.asc().nullsLast().op('timestamptz_ops') + ), + index('messages_created_by_idx').using( + 'btree', + table.createdBy.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.chatId], + foreignColumns: [chats.id], + name: 'messages_chat_id_fkey', + }), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'messages_created_by_fkey', + }).onUpdate('cascade'), + ] ); export const messagesToFiles = pgTable( - 'messages_to_files', - { - id: uuid().primaryKey().notNull(), - messageId: uuid('message_id').notNull(), - fileId: uuid('file_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - isDuplicate: boolean('is_duplicate').default(false).notNull(), - versionNumber: integer('version_number').default(1).notNull(), - }, - (table) => [ - index('messages_files_file_id_idx').using( - 'btree', - table.fileId.asc().nullsLast().op('uuid_ops'), - ), - index('messages_files_message_id_idx').using( - 'btree', - table.messageId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.messageId], - foreignColumns: [messages.id], - name: 'messages_to_files_message_id_fkey', - }), - unique('messages_to_files_message_id_file_id_key').on(table.messageId, table.fileId), - ], + 'messages_to_files', + { + id: uuid().primaryKey().notNull(), + messageId: uuid('message_id').notNull(), + fileId: uuid('file_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + isDuplicate: boolean('is_duplicate').default(false).notNull(), + versionNumber: integer('version_number').default(1).notNull(), + }, + (table) => [ + index('messages_files_file_id_idx').using( + 'btree', + table.fileId.asc().nullsLast().op('uuid_ops') + ), + index('messages_files_message_id_idx').using( + 'btree', + table.messageId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.messageId], + foreignColumns: [messages.id], + name: 'messages_to_files_message_id_fkey', + }), + unique('messages_to_files_message_id_file_id_key').on(table.messageId, table.fileId), + ] ); export const dashboardFiles = pgTable( - 'dashboard_files', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: varchar().notNull(), - fileName: varchar('file_name').notNull(), - content: jsonb().notNull(), - filter: varchar(), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), - publiclyEnabledBy: uuid('publicly_enabled_by'), - publicExpiryDate: timestamp('public_expiry_date', { - withTimezone: true, - mode: 'string', - }), - versionHistory: jsonb('version_history').default({}).notNull(), - publicPassword: text('public_password'), - }, - (table) => [ - index('dashboard_files_created_by_idx').using( - 'btree', - table.createdBy.asc().nullsLast().op('uuid_ops'), - ), - index('dashboard_files_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('dashboard_files_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'dashboard_files_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'dashboard_files_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - ], + 'dashboard_files', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: varchar().notNull(), + fileName: varchar('file_name').notNull(), + content: jsonb().notNull(), + filter: varchar(), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), + publiclyEnabledBy: uuid('publicly_enabled_by'), + publicExpiryDate: timestamp('public_expiry_date', { + withTimezone: true, + mode: 'string', + }), + versionHistory: jsonb('version_history').default({}).notNull(), + publicPassword: text('public_password'), + }, + (table) => [ + index('dashboard_files_created_by_idx').using( + 'btree', + table.createdBy.asc().nullsLast().op('uuid_ops') + ), + index('dashboard_files_deleted_at_idx').using( + 'btree', + table.deletedAt.asc().nullsLast().op('timestamptz_ops') + ), + index('dashboard_files_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'dashboard_files_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'dashboard_files_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + ] ); export const chats = pgTable( - 'chats', - { - id: uuid().defaultRandom().primaryKey().notNull(), - title: text().notNull(), - organizationId: uuid('organization_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), - publiclyEnabledBy: uuid('publicly_enabled_by'), - publicExpiryDate: timestamp('public_expiry_date', { - withTimezone: true, - mode: 'string', - }), - mostRecentFileId: uuid('most_recent_file_id'), - mostRecentFileType: varchar('most_recent_file_type', { length: 255 }), - mostRecentVersionNumber: integer('most_recent_version_number'), - }, - (table) => [ - index('chats_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('chats_created_by_idx').using('btree', table.createdBy.asc().nullsLast().op('uuid_ops')), - index('chats_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - index('idx_chats_most_recent_file_id').using( - 'btree', - table.mostRecentFileId.asc().nullsLast().op('uuid_ops'), - ), - index('idx_chats_most_recent_file_type').using( - 'btree', - table.mostRecentFileType.asc().nullsLast().op('text_ops'), - ), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'chats_organization_id_fkey', - }), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'chats_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'chats_updated_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'chats_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - ], + 'chats', + { + id: uuid().defaultRandom().primaryKey().notNull(), + title: text().notNull(), + organizationId: uuid('organization_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), + publiclyEnabledBy: uuid('publicly_enabled_by'), + publicExpiryDate: timestamp('public_expiry_date', { + withTimezone: true, + mode: 'string', + }), + mostRecentFileId: uuid('most_recent_file_id'), + mostRecentFileType: varchar('most_recent_file_type', { length: 255 }), + mostRecentVersionNumber: integer('most_recent_version_number'), + }, + (table) => [ + index('chats_created_at_idx').using( + 'btree', + table.createdAt.asc().nullsLast().op('timestamptz_ops') + ), + index('chats_created_by_idx').using('btree', table.createdBy.asc().nullsLast().op('uuid_ops')), + index('chats_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + index('idx_chats_most_recent_file_id').using( + 'btree', + table.mostRecentFileId.asc().nullsLast().op('uuid_ops') + ), + index('idx_chats_most_recent_file_type').using( + 'btree', + table.mostRecentFileType.asc().nullsLast().op('text_ops') + ), + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'chats_organization_id_fkey', + }), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'chats_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'chats_updated_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'chats_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + ] ); export const organizations = pgTable( - 'organizations', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: text().notNull(), - domain: text(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - paymentRequired: boolean('payment_required').default(false).notNull(), - }, - (table) => [unique('organizations_name_key').on(table.name)], + 'organizations', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: text().notNull(), + domain: text(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + paymentRequired: boolean('payment_required').default(false).notNull(), + }, + (table) => [unique('organizations_name_key').on(table.name)] ); export const storedValuesSyncJobs = pgTable( - 'stored_values_sync_jobs', - { - id: uuid().defaultRandom().primaryKey().notNull(), - dataSourceId: uuid('data_source_id').notNull(), - databaseName: text('database_name').notNull(), - schemaName: text('schema_name').notNull(), - tableName: text('table_name').notNull(), - columnName: text('column_name').notNull(), - lastSyncedAt: timestamp('last_synced_at', { - withTimezone: true, - mode: 'string', - }), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - status: text().notNull(), - errorMessage: text('error_message'), - }, - (table) => [ - index('idx_stored_values_sync_jobs_data_source_id').using( - 'btree', - table.dataSourceId.asc().nullsLast().op('uuid_ops'), - ), - index('idx_stored_values_sync_jobs_db_schema_table_column').using( - 'btree', - table.databaseName.asc().nullsLast().op('text_ops'), - table.schemaName.asc().nullsLast().op('text_ops'), - table.tableName.asc().nullsLast().op('text_ops'), - table.columnName.asc().nullsLast().op('text_ops'), - ), - index('idx_stored_values_sync_jobs_status').using( - 'btree', - table.status.asc().nullsLast().op('text_ops'), - ), - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'stored_values_sync_jobs_data_source_id_fkey', - }).onDelete('cascade'), - ], + 'stored_values_sync_jobs', + { + id: uuid().defaultRandom().primaryKey().notNull(), + dataSourceId: uuid('data_source_id').notNull(), + databaseName: text('database_name').notNull(), + schemaName: text('schema_name').notNull(), + tableName: text('table_name').notNull(), + columnName: text('column_name').notNull(), + lastSyncedAt: timestamp('last_synced_at', { + withTimezone: true, + mode: 'string', + }), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + status: text().notNull(), + errorMessage: text('error_message'), + }, + (table) => [ + index('idx_stored_values_sync_jobs_data_source_id').using( + 'btree', + table.dataSourceId.asc().nullsLast().op('uuid_ops') + ), + index('idx_stored_values_sync_jobs_db_schema_table_column').using( + 'btree', + table.databaseName.asc().nullsLast().op('text_ops'), + table.schemaName.asc().nullsLast().op('text_ops'), + table.tableName.asc().nullsLast().op('text_ops'), + table.columnName.asc().nullsLast().op('text_ops') + ), + index('idx_stored_values_sync_jobs_status').using( + 'btree', + table.status.asc().nullsLast().op('text_ops') + ), + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'stored_values_sync_jobs_data_source_id_fkey', + }).onDelete('cascade'), + ] ); export const metricFiles = pgTable( - 'metric_files', - { - id: uuid().defaultRandom().primaryKey().notNull(), - name: varchar().notNull(), - fileName: varchar('file_name').notNull(), - content: jsonb().notNull(), - verification: verificationEnum().default('notRequested').notNull(), - evaluationObj: jsonb('evaluation_obj'), - evaluationSummary: text('evaluation_summary'), - evaluationScore: doublePrecision('evaluation_score'), - organizationId: uuid('organization_id').notNull(), - createdBy: uuid('created_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), - publiclyEnabledBy: uuid('publicly_enabled_by'), - publicExpiryDate: timestamp('public_expiry_date', { - withTimezone: true, - mode: 'string', - }), - versionHistory: jsonb('version_history').default({}).notNull(), - dataMetadata: jsonb('data_metadata'), - publicPassword: text('public_password'), - dataSourceId: uuid('data_source_id').notNull(), - }, - (table) => [ - index('metric_files_created_by_idx').using( - 'btree', - table.createdBy.asc().nullsLast().op('uuid_ops'), - ), - index('metric_files_data_metadata_idx').using( - 'gin', - table.dataMetadata.asc().nullsLast().op('jsonb_ops'), - ), - index('metric_files_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('metric_files_organization_id_idx').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'metric_files_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.publiclyEnabledBy], - foreignColumns: [users.id], - name: 'metric_files_publicly_enabled_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'fk_data_source', - }), - ], + 'metric_files', + { + id: uuid().defaultRandom().primaryKey().notNull(), + name: varchar().notNull(), + fileName: varchar('file_name').notNull(), + content: jsonb().notNull(), + verification: verificationEnum().default('notRequested').notNull(), + evaluationObj: jsonb('evaluation_obj'), + evaluationSummary: text('evaluation_summary'), + evaluationScore: doublePrecision('evaluation_score'), + organizationId: uuid('organization_id').notNull(), + createdBy: uuid('created_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + publiclyAccessible: boolean('publicly_accessible').default(false).notNull(), + publiclyEnabledBy: uuid('publicly_enabled_by'), + publicExpiryDate: timestamp('public_expiry_date', { + withTimezone: true, + mode: 'string', + }), + versionHistory: jsonb('version_history').default({}).notNull(), + dataMetadata: jsonb('data_metadata'), + publicPassword: text('public_password'), + dataSourceId: uuid('data_source_id').notNull(), + }, + (table) => [ + index('metric_files_created_by_idx').using( + 'btree', + table.createdBy.asc().nullsLast().op('uuid_ops') + ), + index('metric_files_data_metadata_idx').using( + 'gin', + table.dataMetadata.asc().nullsLast().op('jsonb_ops') + ), + index('metric_files_deleted_at_idx').using( + 'btree', + table.deletedAt.asc().nullsLast().op('timestamptz_ops') + ), + index('metric_files_organization_id_idx').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'metric_files_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.publiclyEnabledBy], + foreignColumns: [users.id], + name: 'metric_files_publicly_enabled_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'fk_data_source', + }), + ] ); export const permissionGroupsToUsers = pgTable( - 'permission_groups_to_users', - { - permissionGroupId: uuid('permission_group_id').notNull(), - userId: uuid('user_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - }, - (table) => [ - index('permission_groups_to_users_user_id_idx').using( - 'btree', - table.userId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.permissionGroupId], - foreignColumns: [permissionGroups.id], - name: 'permission_groups_to_users_permission_group_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'permission_groups_to_users_user_id_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.permissionGroupId, table.userId], - name: 'permission_groups_to_users_pkey', - }), - pgPolicy('permission_groups_to_users_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - ], + 'permission_groups_to_users', + { + permissionGroupId: uuid('permission_group_id').notNull(), + userId: uuid('user_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + }, + (table) => [ + index('permission_groups_to_users_user_id_idx').using( + 'btree', + table.userId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.permissionGroupId], + foreignColumns: [permissionGroups.id], + name: 'permission_groups_to_users_permission_group_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: 'permission_groups_to_users_user_id_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.permissionGroupId, table.userId], + name: 'permission_groups_to_users_pkey', + }), + pgPolicy('permission_groups_to_users_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + ] ); export const entityRelationship = pgTable( - 'entity_relationship', - { - primaryDatasetId: uuid('primary_dataset_id').notNull(), - foreignDatasetId: uuid('foreign_dataset_id').notNull(), - relationshipType: text('relationship_type').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - }, - (table) => [ - primaryKey({ - columns: [table.primaryDatasetId, table.foreignDatasetId], - name: 'entity_relationship_pkey', - }), - ], + 'entity_relationship', + { + primaryDatasetId: uuid('primary_dataset_id').notNull(), + foreignDatasetId: uuid('foreign_dataset_id').notNull(), + relationshipType: text('relationship_type').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + }, + (table) => [ + primaryKey({ + columns: [table.primaryDatasetId, table.foreignDatasetId], + name: 'entity_relationship_pkey', + }), + ] ); export const metricFilesToDatasets = pgTable( - 'metric_files_to_datasets', - { - metricFileId: uuid('metric_file_id').notNull(), - datasetId: uuid('dataset_id').notNull(), - metricVersionNumber: integer('metric_version_number').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.metricFileId], - foreignColumns: [metricFiles.id], - name: 'fk_metric_file', - }).onDelete('cascade'), - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'fk_dataset', - }).onDelete('cascade'), - primaryKey({ - columns: [table.metricFileId, table.datasetId, table.metricVersionNumber], - name: 'metric_files_to_datasets_pkey', - }), - ], + 'metric_files_to_datasets', + { + metricFileId: uuid('metric_file_id').notNull(), + datasetId: uuid('dataset_id').notNull(), + metricVersionNumber: integer('metric_version_number').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.metricFileId], + foreignColumns: [metricFiles.id], + name: 'fk_metric_file', + }).onDelete('cascade'), + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'fk_dataset', + }).onDelete('cascade'), + primaryKey({ + columns: [table.metricFileId, table.datasetId, table.metricVersionNumber], + name: 'metric_files_to_datasets_pkey', + }), + ] ); export const termsToDatasets = pgTable( - 'terms_to_datasets', - { - termId: uuid('term_id').notNull(), - datasetId: uuid('dataset_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.termId], - foreignColumns: [terms.id], - name: 'terms_to_datasets_term_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'terms_to_datasets_dataset_id_fkey', - }).onDelete('cascade'), - primaryKey({ - columns: [table.termId, table.datasetId], - name: 'terms_to_datasets_pkey', - }), - ], + 'terms_to_datasets', + { + termId: uuid('term_id').notNull(), + datasetId: uuid('dataset_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.termId], + foreignColumns: [terms.id], + name: 'terms_to_datasets_term_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'terms_to_datasets_dataset_id_fkey', + }).onDelete('cascade'), + primaryKey({ + columns: [table.termId, table.datasetId], + name: 'terms_to_datasets_pkey', + }), + ] ); export const datasetsToPermissionGroups = pgTable( - 'datasets_to_permission_groups', - { - datasetId: uuid('dataset_id').notNull(), - permissionGroupId: uuid('permission_group_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'datasets_to_permission_groups_dataset_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.permissionGroupId], - foreignColumns: [permissionGroups.id], - name: 'datasets_to_permission_groups_permission_group_id_fkey', - }).onDelete('cascade'), - primaryKey({ - columns: [table.datasetId, table.permissionGroupId], - name: 'datasets_to_permission_groups_pkey', - }), - pgPolicy('datasets_to_permission_groups_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - ], + 'datasets_to_permission_groups', + { + datasetId: uuid('dataset_id').notNull(), + permissionGroupId: uuid('permission_group_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'datasets_to_permission_groups_dataset_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.permissionGroupId], + foreignColumns: [permissionGroups.id], + name: 'datasets_to_permission_groups_permission_group_id_fkey', + }).onDelete('cascade'), + primaryKey({ + columns: [table.datasetId, table.permissionGroupId], + name: 'datasets_to_permission_groups_pkey', + }), + pgPolicy('datasets_to_permission_groups_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + ] ); export const datasetsToDatasetGroups = pgTable( - 'datasets_to_dataset_groups', - { - datasetId: uuid('dataset_id').notNull(), - datasetGroupId: uuid('dataset_group_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - index('datasets_to_dataset_groups_dataset_group_id_idx').using( - 'btree', - table.datasetGroupId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.datasetId], - foreignColumns: [datasets.id], - name: 'datasets_to_dataset_groups_dataset_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.datasetGroupId], - foreignColumns: [datasetGroups.id], - name: 'datasets_to_dataset_groups_dataset_group_id_fkey', - }).onDelete('cascade'), - primaryKey({ - columns: [table.datasetId, table.datasetGroupId], - name: 'datasets_to_dataset_groups_pkey', - }), - pgPolicy('datasets_to_dataset_groups_policy', { - as: 'permissive', - for: 'all', - to: ['authenticated'], - using: sql`true`, - }), - ], + 'datasets_to_dataset_groups', + { + datasetId: uuid('dataset_id').notNull(), + datasetGroupId: uuid('dataset_group_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + index('datasets_to_dataset_groups_dataset_group_id_idx').using( + 'btree', + table.datasetGroupId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasets.id], + name: 'datasets_to_dataset_groups_dataset_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.datasetGroupId], + foreignColumns: [datasetGroups.id], + name: 'datasets_to_dataset_groups_dataset_group_id_fkey', + }).onDelete('cascade'), + primaryKey({ + columns: [table.datasetId, table.datasetGroupId], + name: 'datasets_to_dataset_groups_pkey', + }), + pgPolicy('datasets_to_dataset_groups_policy', { + as: 'permissive', + for: 'all', + to: ['authenticated'], + using: sql`true`, + }), + ] ); export const threadsToDashboards = pgTable( - 'threads_to_dashboards', - { - threadId: uuid('thread_id').notNull(), - dashboardId: uuid('dashboard_id').notNull(), - addedBy: uuid('added_by').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.threadId], - foreignColumns: [threadsDeprecated.id], - name: 'threads_to_dashboards_thread_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.dashboardId], - foreignColumns: [dashboards.id], - name: 'threads_to_dashboards_dashboard_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.addedBy], - foreignColumns: [users.id], - name: 'threads_to_dashboards_added_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.threadId, table.dashboardId], - name: 'threads_to_dashboards_pkey', - }), - ], + 'threads_to_dashboards', + { + threadId: uuid('thread_id').notNull(), + dashboardId: uuid('dashboard_id').notNull(), + addedBy: uuid('added_by').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.threadId], + foreignColumns: [threadsDeprecated.id], + name: 'threads_to_dashboards_thread_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.dashboardId], + foreignColumns: [dashboards.id], + name: 'threads_to_dashboards_dashboard_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.addedBy], + foreignColumns: [users.id], + name: 'threads_to_dashboards_added_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.threadId, table.dashboardId], + name: 'threads_to_dashboards_pkey', + }), + ] ); export const userFavorites = pgTable( - 'user_favorites', - { - userId: uuid('user_id').notNull(), - assetId: uuid('asset_id').notNull(), - assetType: assetTypeEnum('asset_type').notNull(), - orderIndex: integer('order_index').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'user_favorites_user_id_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.userId, table.assetId, table.assetType], - name: 'user_favorites_pkey', - }), - ], + 'user_favorites', + { + userId: uuid('user_id').notNull(), + assetId: uuid('asset_id').notNull(), + assetType: assetTypeEnum('asset_type').notNull(), + orderIndex: integer('order_index').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: 'user_favorites_user_id_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.userId, table.assetId, table.assetType], + name: 'user_favorites_pkey', + }), + ] ); export const teamsToUsers = pgTable( - 'teams_to_users', - { - teamId: uuid('team_id').notNull(), - userId: uuid('user_id').notNull(), - role: teamRoleEnum().default('member').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.teamId], - foreignColumns: [teams.id], - name: 'teams_to_users_team_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'teams_to_users_user_id_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.teamId, table.userId], - name: 'teams_to_users_pkey', - }), - ], + 'teams_to_users', + { + teamId: uuid('team_id').notNull(), + userId: uuid('user_id').notNull(), + role: teamRoleEnum().default('member').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.teamId], + foreignColumns: [teams.id], + name: 'teams_to_users_team_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: 'teams_to_users_user_id_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.teamId, table.userId], + name: 'teams_to_users_pkey', + }), + ] ); export const metricFilesToDashboardFiles = pgTable( - 'metric_files_to_dashboard_files', - { - metricFileId: uuid('metric_file_id').notNull(), - dashboardFileId: uuid('dashboard_file_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - }, - (table) => [ - index('metric_files_to_dashboard_files_dashboard_id_idx').using( - 'btree', - table.dashboardFileId.asc().nullsLast().op('uuid_ops'), - ), - index('metric_files_to_dashboard_files_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamptz_ops'), - ), - index('metric_files_to_dashboard_files_metric_id_idx').using( - 'btree', - table.metricFileId.asc().nullsLast().op('uuid_ops'), - ), - foreignKey({ - columns: [table.metricFileId], - foreignColumns: [metricFiles.id], - name: 'metric_files_to_dashboard_files_metric_file_id_fkey', - }), - foreignKey({ - columns: [table.dashboardFileId], - foreignColumns: [dashboardFiles.id], - name: 'metric_files_to_dashboard_files_dashboard_file_id_fkey', - }), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'metric_files_to_dashboard_files_created_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.metricFileId, table.dashboardFileId], - name: 'metric_files_to_dashboard_files_pkey', - }), - ], + 'metric_files_to_dashboard_files', + { + metricFileId: uuid('metric_file_id').notNull(), + dashboardFileId: uuid('dashboard_file_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + }, + (table) => [ + index('metric_files_to_dashboard_files_dashboard_id_idx').using( + 'btree', + table.dashboardFileId.asc().nullsLast().op('uuid_ops') + ), + index('metric_files_to_dashboard_files_deleted_at_idx').using( + 'btree', + table.deletedAt.asc().nullsLast().op('timestamptz_ops') + ), + index('metric_files_to_dashboard_files_metric_id_idx').using( + 'btree', + table.metricFileId.asc().nullsLast().op('uuid_ops') + ), + foreignKey({ + columns: [table.metricFileId], + foreignColumns: [metricFiles.id], + name: 'metric_files_to_dashboard_files_metric_file_id_fkey', + }), + foreignKey({ + columns: [table.dashboardFileId], + foreignColumns: [dashboardFiles.id], + name: 'metric_files_to_dashboard_files_dashboard_file_id_fkey', + }), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'metric_files_to_dashboard_files_created_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.metricFileId, table.dashboardFileId], + name: 'metric_files_to_dashboard_files_pkey', + }), + ] ); export const collectionsToAssets = pgTable( - 'collections_to_assets', - { - collectionId: uuid('collection_id').notNull(), - assetId: uuid('asset_id').notNull(), - assetType: assetTypeEnum('asset_type').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'collections_to_assets_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'collections_to_assets_updated_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.collectionId, table.assetId, table.assetType], - name: 'collections_to_assets_pkey', - }), - ], + 'collections_to_assets', + { + collectionId: uuid('collection_id').notNull(), + assetId: uuid('asset_id').notNull(), + assetType: assetTypeEnum('asset_type').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'collections_to_assets_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'collections_to_assets_updated_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.collectionId, table.assetId, table.assetType], + name: 'collections_to_assets_pkey', + }), + ] ); export const permissionGroupsToIdentities = pgTable( - 'permission_groups_to_identities', - { - permissionGroupId: uuid('permission_group_id').notNull(), - identityId: uuid('identity_id').notNull(), - identityType: identityTypeEnum('identity_type').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'permission_groups_to_identities_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'permission_groups_to_identities_updated_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.permissionGroupId, table.identityId, table.identityType], - name: 'permission_groups_to_identities_pkey', - }), - ], + 'permission_groups_to_identities', + { + permissionGroupId: uuid('permission_group_id').notNull(), + identityId: uuid('identity_id').notNull(), + identityType: identityTypeEnum('identity_type').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'permission_groups_to_identities_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'permission_groups_to_identities_updated_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.permissionGroupId, table.identityId, table.identityType], + name: 'permission_groups_to_identities_pkey', + }), + ] ); export const assetPermissions = pgTable( - 'asset_permissions', - { - identityId: uuid('identity_id').notNull(), - identityType: identityTypeEnum('identity_type').notNull(), - assetId: uuid('asset_id').notNull(), - assetType: assetTypeEnum('asset_type').notNull(), - role: assetPermissionRoleEnum().notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'asset_permissions_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'asset_permissions_updated_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.identityId, table.identityType, table.assetId, table.assetType], - name: 'asset_permissions_pkey', - }), - ], + 'asset_permissions', + { + identityId: uuid('identity_id').notNull(), + identityType: identityTypeEnum('identity_type').notNull(), + assetId: uuid('asset_id').notNull(), + assetType: assetTypeEnum('asset_type').notNull(), + role: assetPermissionRoleEnum().notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'asset_permissions_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'asset_permissions_updated_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.identityId, table.identityType, table.assetId, table.assetType], + name: 'asset_permissions_pkey', + }), + ] ); export const usersToOrganizations = pgTable( - 'users_to_organizations', - { - userId: uuid('user_id').notNull(), - organizationId: uuid('organization_id').notNull(), - role: userOrganizationRoleEnum().default('querier').notNull(), - sharingSetting: sharingSettingEnum('sharing_setting').default('none').notNull(), - editSql: boolean('edit_sql').default(false).notNull(), - uploadCsv: boolean('upload_csv').default(false).notNull(), - exportAssets: boolean('export_assets').default(false).notNull(), - emailSlackEnabled: boolean('email_slack_enabled').default(false).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - createdBy: uuid('created_by').notNull(), - updatedBy: uuid('updated_by').notNull(), - deletedBy: uuid('deleted_by'), - status: userOrganizationStatusEnum().default('active').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'users_to_organizations_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'users_to_organizations_user_id_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.createdBy], - foreignColumns: [users.id], - name: 'users_to_organizations_created_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.updatedBy], - foreignColumns: [users.id], - name: 'users_to_organizations_updated_by_fkey', - }).onUpdate('cascade'), - foreignKey({ - columns: [table.deletedBy], - foreignColumns: [users.id], - name: 'users_to_organizations_deleted_by_fkey', - }).onUpdate('cascade'), - primaryKey({ - columns: [table.userId, table.organizationId], - name: 'users_to_organizations_pkey', - }), - ], + 'users_to_organizations', + { + userId: uuid('user_id').notNull(), + organizationId: uuid('organization_id').notNull(), + role: userOrganizationRoleEnum().default('querier').notNull(), + sharingSetting: sharingSettingEnum('sharing_setting').default('none').notNull(), + editSql: boolean('edit_sql').default(false).notNull(), + uploadCsv: boolean('upload_csv').default(false).notNull(), + exportAssets: boolean('export_assets').default(false).notNull(), + emailSlackEnabled: boolean('email_slack_enabled').default(false).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + createdBy: uuid('created_by').notNull(), + updatedBy: uuid('updated_by').notNull(), + deletedBy: uuid('deleted_by'), + status: userOrganizationStatusEnum().default('active').notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'users_to_organizations_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: 'users_to_organizations_user_id_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.createdBy], + foreignColumns: [users.id], + name: 'users_to_organizations_created_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.updatedBy], + foreignColumns: [users.id], + name: 'users_to_organizations_updated_by_fkey', + }).onUpdate('cascade'), + foreignKey({ + columns: [table.deletedBy], + foreignColumns: [users.id], + name: 'users_to_organizations_deleted_by_fkey', + }).onUpdate('cascade'), + primaryKey({ + columns: [table.userId, table.organizationId], + name: 'users_to_organizations_pkey', + }), + ] ); export const databaseMetadata = pgTable( - 'database_metadata', - { - id: uuid().defaultRandom().primaryKey().notNull(), - dataSourceId: uuid('data_source_id').notNull(), - name: text().notNull(), - owner: text(), - comment: text(), - created: timestamp({ withTimezone: true, mode: 'string' }), - lastModified: timestamp('last_modified', { - withTimezone: true, - mode: 'string', - }), - metadata: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'database_metadata_data_source_id_fkey', - }).onDelete('cascade'), - unique('database_metadata_data_source_id_name_key').on(table.dataSourceId, table.name), - index('database_metadata_data_source_id_idx').using( - 'btree', - table.dataSourceId.asc().nullsLast().op('uuid_ops'), - ), - ], + 'database_metadata', + { + id: uuid().defaultRandom().primaryKey().notNull(), + dataSourceId: uuid('data_source_id').notNull(), + name: text().notNull(), + owner: text(), + comment: text(), + created: timestamp({ withTimezone: true, mode: 'string' }), + lastModified: timestamp('last_modified', { + withTimezone: true, + mode: 'string', + }), + metadata: jsonb().default({}).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'database_metadata_data_source_id_fkey', + }).onDelete('cascade'), + unique('database_metadata_data_source_id_name_key').on(table.dataSourceId, table.name), + index('database_metadata_data_source_id_idx').using( + 'btree', + table.dataSourceId.asc().nullsLast().op('uuid_ops') + ), + ] ); export const schemaMetadata = pgTable( - 'schema_metadata', - { - id: uuid().defaultRandom().primaryKey().notNull(), - dataSourceId: uuid('data_source_id').notNull(), - databaseId: uuid('database_id'), // Optional for MySQL - name: text().notNull(), - databaseName: text('database_name').notNull(), - owner: text(), - comment: text(), - created: timestamp({ withTimezone: true, mode: 'string' }), - lastModified: timestamp('last_modified', { - withTimezone: true, - mode: 'string', - }), - metadata: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'schema_metadata_data_source_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.databaseId], - foreignColumns: [databaseMetadata.id], - name: 'schema_metadata_database_id_fkey', - }).onDelete('cascade'), - unique('schema_metadata_data_source_id_database_id_name_key').on( - table.dataSourceId, - table.databaseId, - table.name, - ), - index('schema_metadata_data_source_id_idx').using( - 'btree', - table.dataSourceId.asc().nullsLast().op('uuid_ops'), - ), - index('schema_metadata_database_id_idx').using( - 'btree', - table.databaseId.asc().nullsLast().op('uuid_ops'), - ), - ], + 'schema_metadata', + { + id: uuid().defaultRandom().primaryKey().notNull(), + dataSourceId: uuid('data_source_id').notNull(), + databaseId: uuid('database_id'), // Optional for MySQL + name: text().notNull(), + databaseName: text('database_name').notNull(), + owner: text(), + comment: text(), + created: timestamp({ withTimezone: true, mode: 'string' }), + lastModified: timestamp('last_modified', { + withTimezone: true, + mode: 'string', + }), + metadata: jsonb().default({}).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'schema_metadata_data_source_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.databaseId], + foreignColumns: [databaseMetadata.id], + name: 'schema_metadata_database_id_fkey', + }).onDelete('cascade'), + unique('schema_metadata_data_source_id_database_id_name_key').on( + table.dataSourceId, + table.databaseId, + table.name + ), + index('schema_metadata_data_source_id_idx').using( + 'btree', + table.dataSourceId.asc().nullsLast().op('uuid_ops') + ), + index('schema_metadata_database_id_idx').using( + 'btree', + table.databaseId.asc().nullsLast().op('uuid_ops') + ), + ] ); export const tableMetadata = pgTable( - 'table_metadata', - { - id: uuid().defaultRandom().primaryKey().notNull(), - dataSourceId: uuid('data_source_id').notNull(), - databaseId: uuid('database_id'), // Optional for some databases - schemaId: uuid('schema_id').notNull(), - name: text().notNull(), - schemaName: text('schema_name').notNull(), - databaseName: text('database_name').notNull(), - type: tableTypeEnum().notNull(), - rowCount: bigint('row_count', { mode: 'number' }), - sizeBytes: bigint('size_bytes', { mode: 'number' }), - comment: text(), - created: timestamp({ withTimezone: true, mode: 'string' }), - lastModified: timestamp('last_modified', { - withTimezone: true, - mode: 'string', - }), - clusteringKeys: jsonb('clustering_keys').default([]).notNull(), - columns: jsonb().default([]).notNull(), // Array of Column objects - metadata: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.dataSourceId], - foreignColumns: [dataSources.id], - name: 'table_metadata_data_source_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.databaseId], - foreignColumns: [databaseMetadata.id], - name: 'table_metadata_database_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.schemaId], - foreignColumns: [schemaMetadata.id], - name: 'table_metadata_schema_id_fkey', - }).onDelete('cascade'), - unique('table_metadata_data_source_id_schema_id_name_key').on( - table.dataSourceId, - table.schemaId, - table.name, - ), - index('table_metadata_data_source_id_idx').using( - 'btree', - table.dataSourceId.asc().nullsLast().op('uuid_ops'), - ), - index('table_metadata_database_id_idx').using( - 'btree', - table.databaseId.asc().nullsLast().op('uuid_ops'), - ), - index('table_metadata_schema_id_idx').using( - 'btree', - table.schemaId.asc().nullsLast().op('uuid_ops'), - ), - ], + 'table_metadata', + { + id: uuid().defaultRandom().primaryKey().notNull(), + dataSourceId: uuid('data_source_id').notNull(), + databaseId: uuid('database_id'), // Optional for some databases + schemaId: uuid('schema_id').notNull(), + name: text().notNull(), + schemaName: text('schema_name').notNull(), + databaseName: text('database_name').notNull(), + type: tableTypeEnum().notNull(), + rowCount: bigint('row_count', { mode: 'number' }), + sizeBytes: bigint('size_bytes', { mode: 'number' }), + comment: text(), + created: timestamp({ withTimezone: true, mode: 'string' }), + lastModified: timestamp('last_modified', { + withTimezone: true, + mode: 'string', + }), + clusteringKeys: jsonb('clustering_keys').default([]).notNull(), + columns: jsonb().default([]).notNull(), // Array of Column objects + metadata: jsonb().default({}).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.dataSourceId], + foreignColumns: [dataSources.id], + name: 'table_metadata_data_source_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.databaseId], + foreignColumns: [databaseMetadata.id], + name: 'table_metadata_database_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.schemaId], + foreignColumns: [schemaMetadata.id], + name: 'table_metadata_schema_id_fkey', + }).onDelete('cascade'), + unique('table_metadata_data_source_id_schema_id_name_key').on( + table.dataSourceId, + table.schemaId, + table.name + ), + index('table_metadata_data_source_id_idx').using( + 'btree', + table.dataSourceId.asc().nullsLast().op('uuid_ops') + ), + index('table_metadata_database_id_idx').using( + 'btree', + table.databaseId.asc().nullsLast().op('uuid_ops') + ), + index('table_metadata_schema_id_idx').using( + 'btree', + table.schemaId.asc().nullsLast().op('uuid_ops') + ), + ] ); // Slack integrations table export const slackIntegrations = pgTable( - 'slack_integrations', - { - id: uuid().defaultRandom().primaryKey().notNull(), - organizationId: uuid('organization_id').notNull(), - userId: uuid('user_id').notNull(), + 'slack_integrations', + { + id: uuid().defaultRandom().primaryKey().notNull(), + organizationId: uuid('organization_id').notNull(), + userId: uuid('user_id').notNull(), - // OAuth state fields (for pending integrations) - oauthState: varchar('oauth_state', { length: 255 }).unique(), - oauthExpiresAt: timestamp('oauth_expires_at', { withTimezone: true, mode: 'string' }), - oauthMetadata: jsonb('oauth_metadata').default({}), + // OAuth state fields (for pending integrations) + oauthState: varchar('oauth_state', { length: 255 }).unique(), + oauthExpiresAt: timestamp('oauth_expires_at', { withTimezone: true, mode: 'string' }), + oauthMetadata: jsonb('oauth_metadata').default({}), - // Slack workspace info (populated after successful OAuth) - teamId: varchar('team_id', { length: 255 }), - teamName: varchar('team_name', { length: 255 }), - teamDomain: varchar('team_domain', { length: 255 }), - enterpriseId: varchar('enterprise_id', { length: 255 }), + // Slack workspace info (populated after successful OAuth) + teamId: varchar('team_id', { length: 255 }), + teamName: varchar('team_name', { length: 255 }), + teamDomain: varchar('team_domain', { length: 255 }), + enterpriseId: varchar('enterprise_id', { length: 255 }), - // Bot info - botUserId: varchar('bot_user_id', { length: 255 }), - scope: text(), + // Bot info + botUserId: varchar('bot_user_id', { length: 255 }), + scope: text(), - // Token reference (actual token in Supabase Vault) - tokenVaultKey: varchar('token_vault_key', { length: 255 }).unique(), + // Token reference (actual token in Supabase Vault) + tokenVaultKey: varchar('token_vault_key', { length: 255 }).unique(), - // Metadata - installedBySlackUserId: varchar('installed_by_slack_user_id', { length: 255 }), - installedAt: timestamp('installed_at', { withTimezone: true, mode: 'string' }), - lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }), - status: slackIntegrationStatusEnum().default('pending').notNull(), + // Metadata + installedBySlackUserId: varchar('installed_by_slack_user_id', { length: 255 }), + installedAt: timestamp('installed_at', { withTimezone: true, mode: 'string' }), + lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }), + status: slackIntegrationStatusEnum().default('pending').notNull(), - // Default channel configuration - defaultChannel: jsonb('default_channel').default({}), + // Default channel configuration + defaultChannel: jsonb('default_channel') + .$type<{ id: string; name: string } | Record>() + .default({}), - // Timestamps - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'slack_integrations_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'slack_integrations_user_id_fkey', - }), - unique('slack_integrations_org_team_key').on(table.organizationId, table.teamId), - index('idx_slack_integrations_org_id').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops'), - ), - index('idx_slack_integrations_team_id').using( - 'btree', - table.teamId.asc().nullsLast().op('text_ops'), - ), - index('idx_slack_integrations_oauth_state').using( - 'btree', - table.oauthState.asc().nullsLast().op('text_ops'), - ), - index('idx_slack_integrations_oauth_expires').using( - 'btree', - table.oauthExpiresAt.asc().nullsLast().op('timestamptz_ops'), - ), - check( - 'slack_integrations_status_check', - sql`(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)`, - ), - ], + // Timestamps + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + name: 'slack_integrations_organization_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.userId], + foreignColumns: [users.id], + name: 'slack_integrations_user_id_fkey', + }), + unique('slack_integrations_org_team_key').on(table.organizationId, table.teamId), + index('idx_slack_integrations_org_id').using( + 'btree', + table.organizationId.asc().nullsLast().op('uuid_ops') + ), + index('idx_slack_integrations_team_id').using( + 'btree', + table.teamId.asc().nullsLast().op('text_ops') + ), + index('idx_slack_integrations_oauth_state').using( + 'btree', + table.oauthState.asc().nullsLast().op('text_ops') + ), + index('idx_slack_integrations_oauth_expires').using( + 'btree', + table.oauthExpiresAt.asc().nullsLast().op('timestamptz_ops') + ), + check( + 'slack_integrations_status_check', + sql`(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)` + ), + ] ); // Slack message tracking table (optional) export const slackMessageTracking = pgTable( - 'slack_message_tracking', - { - id: uuid().defaultRandom().primaryKey().notNull(), - integrationId: uuid('integration_id').notNull(), + 'slack_message_tracking', + { + id: uuid().defaultRandom().primaryKey().notNull(), + integrationId: uuid('integration_id').notNull(), - // Internal reference - internalMessageId: uuid('internal_message_id').notNull().unique(), + // Internal reference + internalMessageId: uuid('internal_message_id').notNull().unique(), - // Slack references - slackChannelId: varchar('slack_channel_id', { length: 255 }).notNull(), - slackMessageTs: varchar('slack_message_ts', { length: 255 }).notNull(), - slackThreadTs: varchar('slack_thread_ts', { length: 255 }), + // Slack references + slackChannelId: varchar('slack_channel_id', { length: 255 }).notNull(), + slackMessageTs: varchar('slack_message_ts', { length: 255 }).notNull(), + slackThreadTs: varchar('slack_thread_ts', { length: 255 }), - // Metadata - messageType: varchar('message_type', { length: 50 }).notNull(), // 'message', 'reply', 'update' - content: text(), - senderInfo: jsonb('sender_info'), + // Metadata + messageType: varchar('message_type', { length: 50 }).notNull(), // 'message', 'reply', 'update' + content: text(), + senderInfo: jsonb('sender_info'), - // Timestamps - sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.integrationId], - foreignColumns: [slackIntegrations.id], - name: 'slack_message_tracking_integration_id_fkey', - }).onDelete('cascade'), - index('idx_message_tracking_integration').using( - 'btree', - table.integrationId.asc().nullsLast().op('uuid_ops'), - ), - index('idx_message_tracking_channel').using( - 'btree', - table.slackChannelId.asc().nullsLast().op('text_ops'), - ), - index('idx_message_tracking_thread').using( - 'btree', - table.slackThreadTs.asc().nullsLast().op('text_ops'), - ), - ], + // Timestamps + sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.integrationId], + foreignColumns: [slackIntegrations.id], + name: 'slack_message_tracking_integration_id_fkey', + }).onDelete('cascade'), + index('idx_message_tracking_integration').using( + 'btree', + table.integrationId.asc().nullsLast().op('uuid_ops') + ), + index('idx_message_tracking_channel').using( + 'btree', + table.slackChannelId.asc().nullsLast().op('text_ops') + ), + index('idx_message_tracking_thread').using( + 'btree', + table.slackThreadTs.asc().nullsLast().op('text_ops') + ), + ] ); // Join table between messages and slack messages export const messagesToSlackMessages = pgTable( - 'messages_to_slack_messages', - { - messageId: uuid('message_id').notNull(), - slackMessageId: uuid('slack_message_id').notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - // Foreign keys - foreignKey({ - columns: [table.messageId], - foreignColumns: [messages.id], - name: 'messages_to_slack_messages_message_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.slackMessageId], - foreignColumns: [slackMessageTracking.id], - name: 'messages_to_slack_messages_slack_message_id_fkey', - }).onDelete('cascade'), - // Composite primary key - primaryKey({ - columns: [table.messageId, table.slackMessageId], - name: 'messages_to_slack_messages_pkey', - }), - // Indexes for query performance - index('messages_to_slack_messages_message_id_idx').using( - 'btree', - table.messageId.asc().nullsLast().op('uuid_ops'), - ), - index('messages_to_slack_messages_slack_message_id_idx').using( - 'btree', - table.slackMessageId.asc().nullsLast().op('uuid_ops'), - ), - ], + 'messages_to_slack_messages', + { + messageId: uuid('message_id').notNull(), + slackMessageId: uuid('slack_message_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), + }, + (table) => [ + // Foreign keys + foreignKey({ + columns: [table.messageId], + foreignColumns: [messages.id], + name: 'messages_to_slack_messages_message_id_fkey', + }).onDelete('cascade'), + foreignKey({ + columns: [table.slackMessageId], + foreignColumns: [slackMessageTracking.id], + name: 'messages_to_slack_messages_slack_message_id_fkey', + }).onDelete('cascade'), + // Composite primary key + primaryKey({ + columns: [table.messageId, table.slackMessageId], + name: 'messages_to_slack_messages_pkey', + }), + // Indexes for query performance + index('messages_to_slack_messages_message_id_idx').using( + 'btree', + table.messageId.asc().nullsLast().op('uuid_ops') + ), + index('messages_to_slack_messages_slack_message_id_idx').using( + 'btree', + table.slackMessageId.asc().nullsLast().op('uuid_ops') + ), + ] ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b40d69f1..6c306935e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: 'catalog:' version: 4.3.16(react@18.3.1)(zod@3.25.67) braintrust: - specifier: ^0.0.206 - version: 0.0.206(@aws-sdk/credential-provider-web-identity@3.840.0)(react@18.3.1)(sswr@2.2.0(svelte@5.34.9))(svelte@5.34.9)(vue@3.5.17(typescript@5.8.3))(zod@3.25.67) + specifier: ^0.0.209 + version: 0.0.209(@aws-sdk/credential-provider-web-identity@3.840.0)(react@18.3.1)(sswr@2.2.0(svelte@5.34.9))(svelte@5.34.9)(vue@3.5.17(typescript@5.8.3))(zod@3.25.67) vitest: specifier: 'catalog:' version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.3(@types/node@24.0.10)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) @@ -1880,6 +1880,9 @@ packages: '@braintrust/core@0.0.88': resolution: {integrity: sha512-asVr//nyiXvnagf2Av+k3Ggv2UFiygwvlzreI8rS87+9DYRlw0ofy13gSxr7a0ycd0yfRomdVSEpDRlEzpQm5w==} + '@braintrust/core@0.0.89': + resolution: {integrity: sha512-BBLVfFxM6/d4B+i4LUTDW/FvZa4C0HN1/Cqo1W1vOflxnCJ8QVXFSqEUl+MhIU9+cJV9vwjTUVukmyscMT24hA==} + '@bugsnag/cuid@3.2.1': resolution: {integrity: sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==} @@ -5952,6 +5955,12 @@ packages: peerDependencies: zod: ^3.0.0 + braintrust@0.0.209: + resolution: {integrity: sha512-acsjb06ttD/gllfb59idiq1lDAdvsoHcHSJPkmddSIRPORy3vIYt3kfKXiW+WlhwZs2pl3lN8X8pTVuLyj5NNw==} + hasBin: true + peerDependencies: + zod: ^3.0.0 + brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} @@ -6004,7 +6013,6 @@ packages: bun@1.2.18: resolution: {integrity: sha512-OR+EpNckoJN4tHMVZPaTPxDj2RgpJgJwLruTIFYbO3bQMguLd0YrmkWKYqsiihcLgm2ehIjF/H1RLfZiRa7+qQ==} - cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -11821,6 +11829,11 @@ snapshots: openapi3-ts: 4.5.0 zod: 3.25.67 + '@asteasolutions/zod-to-openapi@6.4.0(zod@3.25.75)': + dependencies: + openapi3-ts: 4.5.0 + zod: 3.25.75 + '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 @@ -13407,6 +13420,12 @@ snapshots: uuid: 9.0.1 zod: 3.25.67 + '@braintrust/core@0.0.89': + dependencies: + '@asteasolutions/zod-to-openapi': 6.4.0(zod@3.25.75) + uuid: 9.0.1 + zod: 3.25.75 + '@bugsnag/cuid@3.2.1': {} '@bundled-es-modules/cookie@2.0.1': @@ -18204,6 +18223,42 @@ snapshots: - svelte - vue + braintrust@0.0.209(@aws-sdk/credential-provider-web-identity@3.840.0)(react@18.3.1)(sswr@2.2.0(svelte@5.34.9))(svelte@5.34.9)(vue@3.5.17(typescript@5.8.3))(zod@3.25.67): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@braintrust/core': 0.0.89 + '@next/env': 14.2.30 + '@vercel/functions': 1.6.0(@aws-sdk/credential-provider-web-identity@3.840.0) + ai: 3.4.33(react@18.3.1)(sswr@2.2.0(svelte@5.34.9))(svelte@5.34.9)(vue@3.5.17(typescript@5.8.3))(zod@3.25.67) + argparse: 2.0.1 + chalk: 4.1.2 + cli-progress: 3.12.0 + cors: 2.8.5 + dotenv: 16.6.1 + esbuild: 0.25.5 + eventsource-parser: 1.1.2 + express: 4.21.2 + graceful-fs: 4.2.11 + http-errors: 2.0.0 + minimatch: 9.0.5 + mustache: 4.2.0 + pluralize: 8.0.0 + simple-git: 3.28.0 + slugify: 1.6.6 + source-map: 0.7.4 + uuid: 9.0.1 + zod: 3.25.67 + zod-to-json-schema: 3.24.6(zod@3.25.67) + transitivePeerDependencies: + - '@aws-sdk/credential-provider-web-identity' + - openai + - react + - solid-js + - sswr + - supports-color + - svelte + - vue + brorand@1.1.0: {} browser-assert@1.2.1: {}