auto-claude: subtask-4-3 - Create Supabase chat database operations module
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
import { createClient as createServerClient } from '@supabase/supabase-js';
|
||||
|
||||
// Type definitions for chat tables
|
||||
export type ChatRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
created_at: string;
|
||||
visitor_id: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
session_id: string;
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Input types for creating records
|
||||
export interface CreateChatSessionInput {
|
||||
visitor_id: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateChatMessageInput {
|
||||
session_id: string;
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// Create a Supabase client with service role key for server-side operations
|
||||
// This bypasses RLS for administrative operations
|
||||
function createServiceClient() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!supabaseUrl || !serviceRoleKey) {
|
||||
throw new Error(
|
||||
'Missing Supabase environment variables: NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY'
|
||||
);
|
||||
}
|
||||
|
||||
return createServerClient(supabaseUrl, serviceRoleKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Chat Session Operations
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Create a new chat session for a visitor
|
||||
*/
|
||||
export async function createChatSession(
|
||||
input: CreateChatSessionInput
|
||||
): Promise<ChatSession> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_sessions')
|
||||
.insert({
|
||||
visitor_id: input.visitor_id,
|
||||
metadata: input.metadata ?? {},
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to create chat session: ${error.message}`);
|
||||
}
|
||||
|
||||
return data as ChatSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chat session by ID
|
||||
*/
|
||||
export async function getChatSession(
|
||||
sessionId: string
|
||||
): Promise<ChatSession | null> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_sessions')
|
||||
.select()
|
||||
.eq('id', sessionId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
// No rows found
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to get chat session: ${error.message}`);
|
||||
}
|
||||
|
||||
return data as ChatSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a chat session for a visitor
|
||||
* Useful for maintaining session continuity
|
||||
*/
|
||||
export async function getOrCreateChatSession(
|
||||
visitorId: string,
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<ChatSession> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
// First, try to find an existing active session for this visitor
|
||||
// Get the most recent session from the last 24 hours
|
||||
const twentyFourHoursAgo = new Date(
|
||||
Date.now() - 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
|
||||
const { data: existingSession, error: findError } = await supabase
|
||||
.from('chat_sessions')
|
||||
.select()
|
||||
.eq('visitor_id', visitorId)
|
||||
.gte('created_at', twentyFourHoursAgo)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (existingSession && !findError) {
|
||||
return existingSession as ChatSession;
|
||||
}
|
||||
|
||||
// No existing session found, create a new one
|
||||
return createChatSession({
|
||||
visitor_id: visitorId,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session metadata
|
||||
*/
|
||||
export async function updateSessionMetadata(
|
||||
sessionId: string,
|
||||
metadata: Record<string, unknown>
|
||||
): Promise<ChatSession> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_sessions')
|
||||
.update({ metadata })
|
||||
.eq('id', sessionId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to update session metadata: ${error.message}`);
|
||||
}
|
||||
|
||||
return data as ChatSession;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Chat Message Operations
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Add a message to a chat session
|
||||
*/
|
||||
export async function addChatMessage(
|
||||
input: CreateChatMessageInput
|
||||
): Promise<ChatMessage> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_messages')
|
||||
.insert({
|
||||
session_id: input.session_id,
|
||||
role: input.role,
|
||||
content: input.content,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to add chat message: ${error.message}`);
|
||||
}
|
||||
|
||||
return data as ChatMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages for a chat session
|
||||
* Returns messages in chronological order
|
||||
*/
|
||||
export async function getChatMessages(
|
||||
sessionId: string
|
||||
): Promise<ChatMessage[]> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_messages')
|
||||
.select()
|
||||
.eq('session_id', sessionId)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to get chat messages: ${error.message}`);
|
||||
}
|
||||
|
||||
return (data ?? []) as ChatMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last N messages for a session
|
||||
* Useful for maintaining context window limits
|
||||
*/
|
||||
export async function getRecentMessages(
|
||||
sessionId: string,
|
||||
limit: number = 20
|
||||
): Promise<ChatMessage[]> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chat_messages')
|
||||
.select()
|
||||
.eq('session_id', sessionId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to get recent messages: ${error.message}`);
|
||||
}
|
||||
|
||||
// Reverse to get chronological order
|
||||
return ((data ?? []) as ChatMessage[]).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all messages for a session (clear history)
|
||||
*/
|
||||
export async function clearChatMessages(sessionId: string): Promise<void> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('chat_messages')
|
||||
.delete()
|
||||
.eq('session_id', sessionId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to clear chat messages: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat session and all its messages
|
||||
* Messages are deleted automatically via ON DELETE CASCADE
|
||||
*/
|
||||
export async function deleteChatSession(sessionId: string): Promise<void> {
|
||||
const supabase = createServiceClient();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('chat_sessions')
|
||||
.delete()
|
||||
.eq('id', sessionId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to delete chat session: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Utility Functions
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get conversation history formatted for AI context
|
||||
* Converts database messages to the format expected by chat completion APIs
|
||||
*/
|
||||
export async function getConversationHistory(
|
||||
sessionId: string,
|
||||
maxMessages: number = 20
|
||||
): Promise<Array<{ role: ChatRole; content: string }>> {
|
||||
const messages = await getRecentMessages(sessionId, maxMessages);
|
||||
|
||||
return messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a conversation turn (user message + assistant response)
|
||||
* Convenience function for typical chat flow
|
||||
*/
|
||||
export async function saveConversationTurn(
|
||||
sessionId: string,
|
||||
userMessage: string,
|
||||
assistantResponse: string
|
||||
): Promise<{ userMsg: ChatMessage; assistantMsg: ChatMessage }> {
|
||||
const userMsg = await addChatMessage({
|
||||
session_id: sessionId,
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
});
|
||||
|
||||
const assistantMsg = await addChatMessage({
|
||||
session_id: sessionId,
|
||||
role: 'assistant',
|
||||
content: assistantResponse,
|
||||
});
|
||||
|
||||
return { userMsg, assistantMsg };
|
||||
}
|
||||
|
||||
// Default export for convenience
|
||||
export default {
|
||||
// Session operations
|
||||
createChatSession,
|
||||
getChatSession,
|
||||
getOrCreateChatSession,
|
||||
updateSessionMetadata,
|
||||
deleteChatSession,
|
||||
// Message operations
|
||||
addChatMessage,
|
||||
getChatMessages,
|
||||
getRecentMessages,
|
||||
clearChatMessages,
|
||||
// Utility functions
|
||||
getConversationHistory,
|
||||
saveConversationTurn,
|
||||
};
|
||||
Reference in New Issue
Block a user