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 | |
| parent | chore(root): Add Supabase schema migration (diff) | |
| download | archived-imemio-3b50e39267af9971c32c1ef1bdbf8db46afd7bbd.tar.xz archived-imemio-3b50e39267af9971c32c1ef1bdbf8db46afd7bbd.zip | |
feat(sdk): Implement Supabase storage adapters
| -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 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 98 |
7 files changed, 577 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); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f176633..f0d6a79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,10 @@ importers: version: 5.9.3 packages/sdk: + dependencies: + '@supabase/supabase-js': + specifier: ^2.94.0 + version: 2.94.0 devDependencies: '@types/node': specifier: ^22.13.3 @@ -829,6 +833,30 @@ packages: cpu: [x64] os: [win32] + '@supabase/[email protected]': + resolution: {integrity: sha512-FPFx8DzEreSoLo2HVfwNY0p/uNQ9rONQp3VKw68UP8wg1YwXK5g+TM4d4U7LTGW4HqwG0rjUdQ1it7QPw09r2w==} + engines: {node: '>=20.0.0'} + + '@supabase/[email protected]': + resolution: {integrity: sha512-DAbIptT7e7hAvYHp4FhRH+LxxvKQ38QGxjaFHLoDoeQBqDaAbP/iu74dLOn6PIAnSRAqUkN2bKGs3awzNzBgKA==} + engines: {node: '>=20.0.0'} + + '@supabase/[email protected]': + resolution: {integrity: sha512-3YKoDJu8VxvlJdCe2U2edzSQ9uArR0OLM3A4eAsS4QnIqzs+HuY5ZnubeoWnn/zRNeTENMLSXXZHAeMBPkyXew==} + engines: {node: '>=20.0.0'} + + '@supabase/[email protected]': + resolution: {integrity: sha512-TTPVttf4yMZTd0Jo65rIn4eyTAlI7XlwgB6OVEnne4Sz4VOddXPavEw4xRISOKJZ1n8ULLRz03hilMtqnj9gNg==} + engines: {node: '>=20.0.0'} + + '@supabase/[email protected]': + resolution: {integrity: sha512-wLdfqKqSfdDgGbLqgsT8ssEELBaHJm1xwiymq3cvVgxcbjRR6ECtUGtA1kj0JvX/F9DiARbrk/zkIsQ+OaUVBg==} + engines: {node: '>=20.0.0'} + + '@supabase/[email protected]': + resolution: {integrity: sha512-KcqoA3ITW++CwoyCFxwV10npzR6wMfjKbMz87Q1PSuLw26SmHFQjjbBLvuZpzOrPoQ67on5W55irFsK8e0BhWg==} + engines: {node: '>=20.0.0'} + '@swc/[email protected]': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -989,6 +1017,9 @@ packages: '@types/[email protected]': resolution: {integrity: sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==} + '@types/[email protected]': + resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} + '@types/[email protected]': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -997,6 +1028,9 @@ packages: '@types/[email protected]': resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + '@types/[email protected]': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitest/[email protected]': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -1274,6 +1308,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1836,6 +1874,18 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -2287,6 +2337,44 @@ snapshots: '@rollup/[email protected]': optional: true + '@supabase/[email protected]': + dependencies: + tslib: 2.8.1 + + '@supabase/[email protected]': + dependencies: + tslib: 2.8.1 + + '@supabase/[email protected]': + dependencies: + tslib: 2.8.1 + + '@supabase/[email protected]': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/[email protected]': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/[email protected]': + dependencies: + '@supabase/auth-js': 2.94.0 + '@supabase/functions-js': 2.94.0 + '@supabase/postgrest-js': 2.94.0 + '@supabase/realtime-js': 2.94.0 + '@supabase/storage-js': 2.94.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@swc/[email protected]': dependencies: tslib: 2.8.1 @@ -2410,6 +2498,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/[email protected]': {} + '@types/[email protected](@types/[email protected])': dependencies: '@types/react': 19.2.10 @@ -2418,6 +2508,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/[email protected]': + dependencies: + '@types/node': 22.19.8 + '@vitest/[email protected]': dependencies: '@types/chai': 5.2.3 @@ -2761,6 +2855,8 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + [email protected]: {} + dependencies: safer-buffer: 2.1.2 @@ -3279,6 +3375,8 @@ snapshots: + [email protected]: {} + dependencies: zod: 3.25.76 |