import type { SupabaseClient } from "@supabase/supabase-js"; import type { MemoryNotFoundError, MemoryStore } from "./memory-store.js"; import { failure, type Result, success } from "./result.js"; import type { Memory, MemoryCreateInput, MemoryFilter, MemoryUpdateInput, SearchOptions, SearchResult, Tag, } from "./types.js"; type MemoryRow = { id: string; user_id: string; project_id: string; folder_id: string | null; content: string; tags: Tag[]; metadata: Record; embedding: unknown; embedding_dimensions: number | null; created_at: string; updated_at: string; }; type SearchResultRow = { id: string; content: string; project_id: string; folder_id: string | null; tags: Tag[]; metadata: Record; similarity: number; created_at: string; updated_at: string; }; function memoryNotFoundError(memoryId: string): MemoryNotFoundError { return { type: "MEMORY_NOT_FOUND", memoryId }; } function rowToMemory(row: MemoryRow): Memory { return { id: row.id, content: row.content, projectId: row.project_id, folderId: row.folder_id, tags: row.tags ?? [], metadata: row.metadata ?? {}, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), }; } function rowToSearchResult(row: SearchResultRow): SearchResult { return { id: row.id, content: row.content, projectId: row.project_id, folderId: row.folder_id, tags: row.tags ?? [], metadata: row.metadata ?? {}, similarity: row.similarity, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), }; } export class SupabaseStore implements MemoryStore { private client: SupabaseClient; private userId: string; constructor(client: SupabaseClient, userId: string) { this.client = client; this.userId = userId; } async create(input: MemoryCreateInput): Promise { const insertData: Record = { user_id: this.userId, project_id: input.projectId, folder_id: input.folderId ?? null, content: input.content, tags: input.tags ?? [], metadata: input.metadata ?? {}, }; if (input.embedding) { insertData.embedding = input.embedding; insertData.embedding_dimensions = input.embeddingDimensions ?? input.embedding.length; } const { data, error } = await this.client .from("memories") .insert(insertData) .select() .single(); if (error) { throw new Error(`Failed to create memory: ${error.message}`); } return rowToMemory(data as MemoryRow); } async read(id: string): Promise> { const { data, error } = await this.client .from("memories") .select() .eq("id", id) .eq("user_id", this.userId) .single(); if (error || !data) { return failure(memoryNotFoundError(id)); } return success(rowToMemory(data as MemoryRow)); } async update( id: string, input: MemoryUpdateInput, ): Promise> { const updates: Record = {}; if (input.content !== undefined) { updates.content = input.content; } if (input.projectId !== undefined) { updates.project_id = input.projectId; } if (input.folderId !== undefined) { updates.folder_id = input.folderId; } if (input.tags !== undefined) { updates.tags = input.tags; } if (input.metadata !== undefined) { updates.metadata = input.metadata; } if (input.embedding !== undefined) { updates.embedding = input.embedding; updates.embedding_dimensions = input.embeddingDimensions ?? input.embedding.length; } const { data, error } = await this.client .from("memories") .update(updates) .eq("id", id) .eq("user_id", this.userId) .select() .single(); if (error || !data) { return failure(memoryNotFoundError(id)); } return success(rowToMemory(data as MemoryRow)); } async delete(id: string): Promise> { const { error, count } = await this.client .from("memories") .delete({ count: "exact" }) .eq("id", id) .eq("user_id", this.userId); if (error || count === 0) { return failure(memoryNotFoundError(id)); } return success(undefined); } async list(filter?: MemoryFilter): Promise { let query = this.client .from("memories") .select() .eq("user_id", this.userId); if (filter?.projectId) { query = query.eq("project_id", filter.projectId); } if (filter?.folderId) { query = query.eq("folder_id", filter.folderId); } if (filter?.tags && filter.tags.length > 0) { query = query.contains( "tags", filter.tags.map((tagId) => ({ id: tagId })), ); } const { data, error } = await query; if (error) { throw new Error(`Failed to list memories: ${error.message}`); } return (data as MemoryRow[]).map(rowToMemory); } async search( embedding: number[], options?: SearchOptions, ): Promise { const { data, error } = await this.client.rpc("search_memories", { query_embedding: embedding, match_threshold: options?.threshold ?? 0.7, match_count: options?.limit ?? 10, filter_project_id: options?.projectId ?? null, filter_folder_id: options?.folderId ?? null, }); if (error) { throw new Error(`Failed to search memories: ${error.message}`); } return (data as SearchResultRow[]).map(rowToSearchResult); } }