import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; import { ResponseError } from "@homarr/common/server"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { anchorNotesListInputSchema, anchorNoteUpdateInputSchema, createIntegrationAsync, mockWidgetData, } from "@homarr/integrations"; import { anchorNoteRequestHandler, anchorNotesListRequestHandler } from "@homarr/request-handler/anchor-notes"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; const noteIdInput = z.object({ noteId: z.string(), }); const logger = createLogger({ module: "anchorNotesRouter" }); const assertMockNoteExists = (noteId: string) => { if (noteId === mockWidgetData.anchorNote.id) return; throw new TRPCError({ code: "NOT_FOUND", message: "Note not found" }); }; const isJsonDeltaString = (value: string) => { try { const parsed: unknown = JSON.parse(value); return typeof parsed === "object" && parsed !== null && Array.isArray((parsed as { ops?: unknown }).ops); } catch { return false; } }; const normalizeAnchorContent = (content: string | undefined) => { if (content === undefined) return undefined; const trimmed = content.trim(); if (!trimmed) { return JSON.stringify({ ops: [{ insert: "\n" }] }); } if (isJsonDeltaString(content)) return content; const normalizedText = content.endsWith("\n") ? content : `${content}\n`; return JSON.stringify({ ops: [{ insert: normalizedText }] }); }; export const anchorNotesRouter = createTRPCRouter({ listNotes: publicProcedure .concat(createOneIntegrationMiddleware("query", "anchor", "mock")) .input(anchorNotesListInputSchema) .query(async ({ ctx, input }) => { if (ctx.integration.kind === "mock") { const search = input.search?.toLowerCase(); const notes = mockWidgetData.anchorNotes.filter((note) => !search || note.title.toLowerCase().includes(search)); return notes.slice(0, input.limit ?? 50); } const handler = anchorNotesListRequestHandler.handler( { ...ctx.integration, kind: "anchor" }, { ...input, limit: input.limit ?? 50, }, ); const { data } = await handler.getDataAsync(); return data; }), getNote: publicProcedure .concat(createOneIntegrationMiddleware("query", "anchor", "mock")) .input(noteIdInput) .query(async ({ ctx, input }) => { if (ctx.integration.kind === "mock") { assertMockNoteExists(input.noteId); return mockWidgetData.anchorNote; } const handler = anchorNoteRequestHandler.handler( { ...ctx.integration, kind: "anchor" }, { noteId: input.noteId }, ); const { data } = await handler.getDataAsync(); return data; }), updateNote: protectedProcedure .concat(createOneIntegrationMiddleware("interact", "anchor", "mock")) .input(anchorNoteUpdateInputSchema) .mutation(async ({ ctx, input }) => { const normalizedContent = normalizeAnchorContent(input.content); const isContentNormalized = input.content !== undefined && normalizedContent !== undefined && input.content !== normalizedContent; if (ctx.integration.kind === "mock") { assertMockNoteExists(input.noteId); return { ...mockWidgetData.anchorNote, ...(input.title !== undefined ? { title: input.title } : {}), ...(normalizedContent !== undefined ? { content: normalizedContent } : {}), }; } const integrationInstance = await createIntegrationAsync({ ...ctx.integration, kind: "anchor" }); try { const updatedNote = await integrationInstance.updateNoteAsync({ noteId: input.noteId, ...(input.title !== undefined ? { title: input.title } : {}), ...(normalizedContent !== undefined ? { content: normalizedContent } : {}), }); return updatedNote; } catch (error) { if (error instanceof ResponseError && error.statusCode === 400) { logger.warn("Anchor update note failed validation", { noteId: input.noteId, integrationId: ctx.integration.id, contentLength: input.content?.length ?? 0, contentIsJsonDelta: input.content ? isJsonDeltaString(input.content) : false, contentNormalized: isContentNormalized, }); } if (error instanceof ResponseError && error.statusCode === 403) { throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission to edit this note", }); } if (error instanceof ResponseError && error.statusCode === 404) { throw new TRPCError({ code: "NOT_FOUND", message: "Note not found", }); } throw error; } }), });