aboutsummaryrefslogtreecommitdiff
path: root/packages/sdk
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sdk')
-rw-r--r--packages/sdk/package.json3
-rw-r--r--packages/sdk/src/in-memory-project-store.ts13
-rw-r--r--packages/sdk/src/index.ts6
-rw-r--r--packages/sdk/src/supabase-client.ts7
-rw-r--r--packages/sdk/src/supabase-project-store.ts291
-rw-r--r--packages/sdk/src/supabase-store.ts167
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);
+ }
+}