aboutsummaryrefslogtreecommitdiff
path: root/packages/web/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web/src/server')
-rw-r--r--packages/web/src/server/api/root.ts2
-rw-r--r--packages/web/src/server/api/routers/memory.ts142
-rw-r--r--packages/web/src/server/api/routers/project.ts79
3 files changed, 216 insertions, 7 deletions
diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts
index 919ba21..c382d36 100644
--- a/packages/web/src/server/api/root.ts
+++ b/packages/web/src/server/api/root.ts
@@ -1,12 +1,14 @@
import { apiKeyRouter } from "~/server/api/routers/api-key";
import { memoryRouter } from "~/server/api/routers/memory";
import { postRouter } from "~/server/api/routers/post";
+import { projectRouter } from "~/server/api/routers/project";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
export const appRouter = createTRPCRouter({
apiKey: apiKeyRouter,
memory: memoryRouter,
post: postRouter,
+ project: projectRouter,
});
// export type definition of API
diff --git a/packages/web/src/server/api/routers/memory.ts b/packages/web/src/server/api/routers/memory.ts
index e6d206b..eebe0f8 100644
--- a/packages/web/src/server/api/routers/memory.ts
+++ b/packages/web/src/server/api/routers/memory.ts
@@ -1,9 +1,21 @@
-import { SupabaseProjectStore, SupabaseStore } from "@imemio/sdk";
+import { EmbeddingService, SupabaseProjectStore, SupabaseStore } from "@imemio/sdk";
import type { SupabaseClient } from "@supabase/supabase-js";
+import { TRPCError } from "@trpc/server";
import { z } from "zod";
+import { env } from "~/env";
import { createClient } from "~/lib/supabase/server";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
+function getEmbeddingService(): EmbeddingService | null {
+ const openaiApiKey = env.OPENAI_API_KEY;
+
+ if (!openaiApiKey) {
+ return null;
+ }
+
+ return new EmbeddingService({ apiKey: openaiApiKey });
+}
+
const defaultProjectName = "default";
async function getOrCreateDefaultProject(
@@ -47,14 +59,53 @@ export const memoryRouter = createTRPCRouter({
return memories;
}),
- create: protectedProcedure
- .input(z.object({ content: z.string().min(1) }))
- .mutation(async ({ context, input }) => {
+ listWithProjects: protectedProcedure
+ .input(
+ z.object({
+ projectId: z.string().optional(),
+ sortOrder: z.enum(["asc", "desc"]).optional().default("desc"),
+ }),
+ )
+ .query(async ({ ctx: context, input }) => {
const supabaseClient = await createClient();
- const projectId = await getOrCreateDefaultProject(
+ const memoryStore = new SupabaseStore(supabaseClient, context.user.id);
+ const projectStore = new SupabaseProjectStore(
supabaseClient,
context.user.id,
);
+ const memories = await memoryStore.list(
+ input.projectId ? { projectId: input.projectId } : undefined,
+ );
+ const projectsResult = await projectStore.list();
+ const projects = projectsResult.success ? projectsResult.value : [];
+ const projectMap = new Map(projects.map((project) => [project.id, project]));
+ const memoriesWithProjects = memories.map((memory) => ({
+ ...memory,
+ project: projectMap.get(memory.projectId) ?? null,
+ }));
+ const sortedMemories = memoriesWithProjects.sort((memoryA, memoryB) => {
+ const comparison =
+ new Date(memoryA.createdAt).getTime() -
+ new Date(memoryB.createdAt).getTime();
+
+ return input.sortOrder === "desc" ? -comparison : comparison;
+ });
+
+ return { memories: sortedMemories, projects };
+ }),
+
+ create: protectedProcedure
+ .input(
+ z.object({
+ content: z.string().min(1),
+ projectId: z.string().optional(),
+ }),
+ )
+ .mutation(async ({ ctx: context, input }) => {
+ const supabaseClient = await createClient();
+ const projectId =
+ input.projectId ??
+ (await getOrCreateDefaultProject(supabaseClient, context.user.id));
const memoryStore = new SupabaseStore(supabaseClient, context.user.id);
const memory = await memoryStore.create({
content: input.content,
@@ -64,17 +115,94 @@ export const memoryRouter = createTRPCRouter({
return memory;
}),
+ update: protectedProcedure
+ .input(
+ z.object({
+ id: z.string(),
+ content: z.string().min(1).optional(),
+ projectId: z.string().optional(),
+ }),
+ )
+ .mutation(async ({ ctx: context, input }) => {
+ const supabaseClient = await createClient();
+ const memoryStore = new SupabaseStore(supabaseClient, context.user.id);
+ const updateData: { content?: string; projectId?: string } = {};
+
+ if (input.content !== undefined) {
+ updateData.content = input.content;
+ }
+
+ if (input.projectId !== undefined) {
+ updateData.projectId = input.projectId;
+ }
+
+ const updateResult = await memoryStore.update(input.id, updateData);
+
+ if (!updateResult.success) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: `Memory not found: ${input.id}`,
+ });
+ }
+
+ return updateResult.value;
+ }),
+
delete: protectedProcedure
.input(z.object({ id: z.string() }))
- .mutation(async ({ context, input }) => {
+ .mutation(async ({ ctx: context, input }) => {
const supabaseClient = await createClient();
const memoryStore = new SupabaseStore(supabaseClient, context.user.id);
const deleteResult = await memoryStore.delete(input.id);
if (!deleteResult.success) {
- throw new Error(`Memory not found: ${input.id}`);
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: `Memory not found: ${input.id}`,
+ });
}
return { success: true };
}),
+
+ search: protectedProcedure
+ .input(
+ z.object({
+ query: z.string().min(1),
+ projectId: z.string().optional(),
+ limit: z.number().optional().default(20),
+ }),
+ )
+ .query(async ({ ctx: context, input }) => {
+ const embeddingService = getEmbeddingService();
+
+ if (!embeddingService) {
+ throw new TRPCError({
+ code: "PRECONDITION_FAILED",
+ message:
+ "Search is not available. Configure OPENAI_API_KEY to enable semantic search.",
+ });
+ }
+
+ const supabaseClient = await createClient();
+ const memoryStore = new SupabaseStore(supabaseClient, context.user.id);
+ const projectStore = new SupabaseProjectStore(
+ supabaseClient,
+ context.user.id,
+ );
+ const queryEmbedding = await embeddingService.generate(input.query);
+ const searchResults = await memoryStore.search(queryEmbedding, {
+ projectId: input.projectId,
+ limit: input.limit,
+ });
+ const projectsResult = await projectStore.list();
+ const projects = projectsResult.success ? projectsResult.value : [];
+ const projectMap = new Map(projects.map((project) => [project.id, project]));
+ const resultsWithProjects = searchResults.map((result) => ({
+ ...result,
+ project: projectMap.get(result.projectId) ?? null,
+ }));
+
+ return { results: resultsWithProjects, projects };
+ }),
});
diff --git a/packages/web/src/server/api/routers/project.ts b/packages/web/src/server/api/routers/project.ts
new file mode 100644
index 0000000..c76070d
--- /dev/null
+++ b/packages/web/src/server/api/routers/project.ts
@@ -0,0 +1,79 @@
+import { SupabaseProjectStore } from "@imemio/sdk";
+import { z } from "zod";
+import { createClient } from "~/lib/supabase/server";
+import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
+
+const globalProjectName = "global";
+export const projectRouter = createTRPCRouter({
+ list: protectedProcedure.query(async ({ ctx: context }) => {
+ const supabaseClient = await createClient();
+ const projectStore = new SupabaseProjectStore(
+ supabaseClient,
+ context.user.id,
+ );
+ const result = await projectStore.list();
+
+ if (!result.success) {
+ return [];
+ }
+
+ return result.value;
+ }),
+
+ create: protectedProcedure
+ .input(
+ z.object({
+ name: z.string().min(1),
+ description: z.string().optional(),
+ isGlobal: z.boolean().optional().default(false),
+ }),
+ )
+ .mutation(async ({ ctx: context, input }) => {
+ const supabaseClient = await createClient();
+ const projectStore = new SupabaseProjectStore(
+ supabaseClient,
+ context.user.id,
+ );
+ const result = await projectStore.create({
+ name: input.name,
+ description: input.description,
+ isGlobal: input.isGlobal,
+ });
+
+ if (!result.success) {
+ throw new Error("Failed to create project");
+ }
+
+ return result.value;
+ }),
+
+ getOrCreateGlobal: protectedProcedure.query(async ({ ctx: context }) => {
+ const supabaseClient = await createClient();
+ const projectStore = new SupabaseProjectStore(
+ supabaseClient,
+ context.user.id,
+ );
+ const listResult = await projectStore.list();
+
+ if (listResult.success) {
+ const existingGlobal = listResult.value.find(
+ (project) => project.isGlobal,
+ );
+
+ if (existingGlobal) {
+ return existingGlobal;
+ }
+ }
+
+ const createResult = await projectStore.create({
+ name: globalProjectName,
+ isGlobal: true,
+ });
+
+ if (!createResult.success) {
+ throw new Error("Failed to create global project");
+ }
+
+ return createResult.value;
+ }),
+});