import type { SupabaseClient } from "@supabase/supabase-js"; import type { FolderCreateInput, FolderNotFoundError, FolderUpdateInput, ProjectCreateInput, ProjectNotFoundError, ProjectStore, ProjectUpdateInput, } from "./project-store.js"; import { failure, type Result, success } from "./result.js"; import type { Folder, Project } from "./types.js"; type ProjectRow = { id: string; user_id: string; name: string; description: string | null; is_global: boolean; created_at: string; updated_at: string; }; type FolderRow = { id: string; project_id: string; name: string; description: string | null; created_at: string; updated_at: string; }; function projectNotFoundError(projectId: string): ProjectNotFoundError { return { type: "PROJECT_NOT_FOUND", projectId }; } function folderNotFoundError(folderId: string): FolderNotFoundError { return { type: "FOLDER_NOT_FOUND", folderId }; } function rowToProject(row: ProjectRow, folders: Folder[]): Project { return { id: row.id, name: row.name, description: row.description, isGlobal: row.is_global, folders, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), }; } function rowToFolder(row: FolderRow): Folder { return { id: row.id, name: row.name, description: row.description, projectId: row.project_id, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), }; } export class SupabaseProjectStore implements ProjectStore { private client: SupabaseClient; private userId: string; constructor(client: SupabaseClient, userId: string) { this.client = client; this.userId = userId; } private async fetchFoldersForProject(projectId: string): Promise { const { data, error } = await this.client .from("folders") .select() .eq("project_id", projectId); if (error) { throw new Error(`Failed to fetch folders: ${error.message}`); } return (data as FolderRow[]).map(rowToFolder); } async create(input: ProjectCreateInput): Promise> { const { data, error } = await this.client .from("projects") .insert({ user_id: this.userId, name: input.name, description: input.description ?? null, is_global: input.isGlobal ?? false, }) .select() .single(); if (error) { throw new Error(`Failed to create project: ${error.message}`); } return success(rowToProject(data as ProjectRow, [])); } async get(id: string): Promise> { const { data, error } = await this.client .from("projects") .select() .eq("id", id) .eq("user_id", this.userId) .single(); if (error || !data) { return failure(projectNotFoundError(id)); } const folders = await this.fetchFoldersForProject(id); return success(rowToProject(data as ProjectRow, folders)); } async update( id: string, input: ProjectUpdateInput, ): Promise> { const updates: Record = {}; if (input.name !== undefined) { updates.name = input.name; } if (input.description !== undefined) { updates.description = input.description; } if (input.isGlobal !== undefined) { updates.is_global = input.isGlobal; } const { data, error } = await this.client .from("projects") .update(updates) .eq("id", id) .eq("user_id", this.userId) .select() .single(); if (error || !data) { return failure(projectNotFoundError(id)); } const folders = await this.fetchFoldersForProject(id); return success(rowToProject(data as ProjectRow, folders)); } async delete(id: string): Promise> { const { error, count } = await this.client .from("projects") .delete({ count: "exact" }) .eq("id", id) .eq("user_id", this.userId); if (error || count === 0) { return failure(projectNotFoundError(id)); } return success(undefined); } async list(): Promise> { const { data: projectRows, error } = await this.client .from("projects") .select() .eq("user_id", this.userId); if (error) { throw new Error(`Failed to list projects: ${error.message}`); } const projects: Project[] = []; for (const row of projectRows as ProjectRow[]) { const folders = await this.fetchFoldersForProject(row.id); projects.push(rowToProject(row, folders)); } return success(projects); } async addFolder( projectId: string, input: FolderCreateInput, ): Promise> { const projectResult = await this.get(projectId); if (!projectResult.success) { return failure(projectNotFoundError(projectId)); } const { data, error } = await this.client .from("folders") .insert({ project_id: projectId, name: input.name, description: input.description ?? null, }) .select() .single(); if (error) { throw new Error(`Failed to create folder: ${error.message}`); } return success(rowToFolder(data as FolderRow)); } async updateFolder( projectId: string, folderId: string, input: FolderUpdateInput, ): Promise> { const projectResult = await this.get(projectId); if (!projectResult.success) { return failure(projectNotFoundError(projectId)); } const updates: Record = {}; if (input.name !== undefined) { updates.name = input.name; } if (input.description !== undefined) { updates.description = input.description; } const { data, error } = await this.client .from("folders") .update(updates) .eq("id", folderId) .eq("project_id", projectId) .select() .single(); if (error || !data) { return failure(folderNotFoundError(folderId)); } return success(rowToFolder(data as FolderRow)); } async removeFolder( projectId: string, folderId: string, ): Promise> { const projectResult = await this.get(projectId); if (!projectResult.success) { return failure(projectNotFoundError(projectId)); } const { error, count } = await this.client .from("folders") .delete({ count: "exact" }) .eq("id", folderId) .eq("project_id", projectId); if (error || count === 0) { return failure(folderNotFoundError(folderId)); } return success(undefined); } async listFolders( projectId: string, ): Promise> { const projectResult = await this.get(projectId); if (!projectResult.success) { return failure(projectNotFoundError(projectId)); } const folders = await this.fetchFoldersForProject(projectId); return success(folders); } }