diff options
| author | Fuwn <[email protected]> | 2026-02-03 05:18:44 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-03 19:34:35 -0800 |
| commit | 3b50e39267af9971c32c1ef1bdbf8db46afd7bbd (patch) | |
| tree | 5ab4d3790851613be0f96f0c855137ba77580cc7 /packages | |
| parent | chore(root): Add Supabase schema migration (diff) | |
| download | archived-imemio-3b50e39267af9971c32c1ef1bdbf8db46afd7bbd.tar.xz archived-imemio-3b50e39267af9971c32c1ef1bdbf8db46afd7bbd.zip | |
feat(sdk): Implement Supabase storage adapters
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/sdk/package.json | 3 | ||||
| -rw-r--r-- | packages/sdk/src/in-memory-project-store.ts | 13 | ||||
| -rw-r--r-- | packages/sdk/src/index.ts | 6 | ||||
| -rw-r--r-- | packages/sdk/src/supabase-client.ts | 7 | ||||
| -rw-r--r-- | packages/sdk/src/supabase-project-store.ts | 291 | ||||
| -rw-r--r-- | packages/sdk/src/supabase-store.ts | 167 |
6 files changed, 479 insertions, 8 deletions
diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 432c4e4..bfa4ada 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -24,5 +24,8 @@ "@types/node": "^22.13.3", "typescript": "^5.7.3", "vitest": "^3.0.5" + }, + "dependencies": { + "@supabase/supabase-js": "^2.94.0" } } diff --git a/packages/sdk/src/in-memory-project-store.ts b/packages/sdk/src/in-memory-project-store.ts index 09c2a3a..6208327 100644 --- a/packages/sdk/src/in-memory-project-store.ts +++ b/packages/sdk/src/in-memory-project-store.ts @@ -113,7 +113,6 @@ export class InMemoryProjectStore implements ProjectStore { createdAt: now, updatedAt: now, }; - const updatedProject: Project = { ...project, folders: [...project.folders, folder], @@ -136,15 +135,14 @@ export class InMemoryProjectStore implements ProjectStore { return failure(projectNotFoundError(projectId)); } - const folderIndex = project.folders.findIndex( + const existingFolder = project.folders.find( (folder) => folder.id === folderId, ); - if (folderIndex === -1) { + if (!existingFolder) { return failure(folderNotFoundError(folderId)); } - const existingFolder = project.folders[folderIndex]; const updatedFolder: Folder = { ...existingFolder, name: input.name ?? existingFolder.name, @@ -154,10 +152,9 @@ export class InMemoryProjectStore implements ProjectStore { : existingFolder.description, updatedAt: new Date(), }; - - const updatedFolders = [...project.folders]; - updatedFolders[folderIndex] = updatedFolder; - + const updatedFolders = project.folders.map((folder) => + folder.id === folderId ? updatedFolder : folder, + ); const updatedProject: Project = { ...project, folders: updatedFolders, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 71ed7eb..b61164e 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -30,3 +30,9 @@ export type { export { success, failure } from "./result.js"; export { InMemoryStore } from "./in-memory-store.js"; export { InMemoryProjectStore } from "./in-memory-project-store.js"; +export { + createSupabaseClient, + type SupabaseClient, +} from "./supabase-client.js"; +export { SupabaseStore } from "./supabase-store.js"; +export { SupabaseProjectStore } from "./supabase-project-store.js"; diff --git a/packages/sdk/src/supabase-client.ts b/packages/sdk/src/supabase-client.ts new file mode 100644 index 0000000..33f96b3 --- /dev/null +++ b/packages/sdk/src/supabase-client.ts @@ -0,0 +1,7 @@ +import { createClient, type SupabaseClient } from "@supabase/supabase-js"; + +export type { SupabaseClient }; + +export function createSupabaseClient(url: string, key: string): SupabaseClient { + return createClient(url, key); +} diff --git a/packages/sdk/src/supabase-project-store.ts b/packages/sdk/src/supabase-project-store.ts new file mode 100644 index 0000000..3116eaf --- /dev/null +++ b/packages/sdk/src/supabase-project-store.ts @@ -0,0 +1,291 @@ +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<Folder[]> { + 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<Result<Project, never>> { + 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<Result<Project, ProjectNotFoundError>> { + 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<Result<Project, ProjectNotFoundError>> { + const updates: Record<string, unknown> = {}; + + 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<Result<void, ProjectNotFoundError>> { + 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<Result<Project[], never>> { + 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<Result<Folder, ProjectNotFoundError>> { + 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<Result<Folder, ProjectNotFoundError | FolderNotFoundError>> { + const projectResult = await this.get(projectId); + + if (!projectResult.success) { + return failure(projectNotFoundError(projectId)); + } + + const updates: Record<string, unknown> = {}; + + 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<Result<void, ProjectNotFoundError | FolderNotFoundError>> { + 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<Result<Folder[], ProjectNotFoundError>> { + const projectResult = await this.get(projectId); + + if (!projectResult.success) { + return failure(projectNotFoundError(projectId)); + } + + const folders = await this.fetchFoldersForProject(projectId); + + return success(folders); + } +} diff --git a/packages/sdk/src/supabase-store.ts b/packages/sdk/src/supabase-store.ts new file mode 100644 index 0000000..acb9364 --- /dev/null +++ b/packages/sdk/src/supabase-store.ts @@ -0,0 +1,167 @@ +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, + Tag, +} from "./types.js"; + +type MemoryRow = { + id: string; + user_id: string; + project_id: string; + folder_id: string | null; + content: string; + tags: Tag[]; + metadata: Record<string, unknown>; + embedding: unknown; + 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), + }; +} + +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<Memory> { + const { data, error } = await this.client + .from("memories") + .insert({ + user_id: this.userId, + project_id: input.projectId, + folder_id: input.folderId ?? null, + content: input.content, + tags: input.tags ?? [], + metadata: input.metadata ?? {}, + }) + .select() + .single(); + + if (error) { + throw new Error(`Failed to create memory: ${error.message}`); + } + + return rowToMemory(data as MemoryRow); + } + + async read(id: string): Promise<Result<Memory, MemoryNotFoundError>> { + 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<Result<Memory, MemoryNotFoundError>> { + const updates: Record<string, unknown> = {}; + + if (input.content !== undefined) { + updates.content = input.content; + } + + 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; + } + + 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<Result<void, MemoryNotFoundError>> { + 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<Memory[]> { + 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((id) => ({ id })), + ); + } + + const { data, error } = await query; + + if (error) { + throw new Error(`Failed to list memories: ${error.message}`); + } + + return (data as MemoryRow[]).map(rowToMemory); + } +} |