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