149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
}),
|
|
});
|