diff options
| -rw-r--r-- | packages/open-api/karakeep-openapi-spec.json | 3 | ||||
| -rw-r--r-- | packages/shared/types/tags.ts | 11 | ||||
| -rw-r--r-- | packages/shared/utils/tag.ts | 6 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 9 | ||||
| -rw-r--r-- | packages/trpc/routers/tags.test.ts | 34 |
5 files changed, 56 insertions, 7 deletions
diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json index 3641d166..8a2b8eb9 100644 --- a/packages/open-api/karakeep-openapi-spec.json +++ b/packages/open-api/karakeep-openapi-spec.json @@ -2321,8 +2321,7 @@ "type": "object", "properties": { "name": { - "type": "string", - "minLength": 1 + "type": "string" } }, "required": [ diff --git a/packages/shared/types/tags.ts b/packages/shared/types/tags.ts index c7a0103e..efb26bfa 100644 --- a/packages/shared/types/tags.ts +++ b/packages/shared/types/tags.ts @@ -1,7 +1,14 @@ import { z } from "zod"; +import { normalizeTagName } from "../utils/tag"; + +const zTagNameSchemaWithValidation = z + .string() + .transform((s) => normalizeTagName(s).trim()) + .pipe(z.string().min(1)); + export const zCreateTagRequestSchema = z.object({ - name: z.string().min(1), + name: zTagNameSchemaWithValidation, }); export const zAttachedByEnumSchema = z.enum(["ai", "human"]); @@ -23,7 +30,7 @@ export type ZGetTagResponse = z.infer<typeof zGetTagResponseSchema>; export const zUpdateTagRequestSchema = z.object({ tagId: z.string(), - name: z.string().optional(), + name: zTagNameSchemaWithValidation.optional(), }); export const zTagBasicSchema = z.object({ diff --git a/packages/shared/utils/tag.ts b/packages/shared/utils/tag.ts new file mode 100644 index 00000000..8e1bd105 --- /dev/null +++ b/packages/shared/utils/tag.ts @@ -0,0 +1,6 @@ +/** + * Ensures exactly ONE leading # + */ +export function normalizeTagName(raw: string): string { + return raw.trim().replace(/^#+/, ""); // strip every leading # +} diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 815cf90d..2a02a0cd 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -50,6 +50,7 @@ import { zSearchBookmarksRequestSchema, zUpdateBookmarksRequestSchema, } from "@karakeep/shared/types/bookmarks"; +import { normalizeTagName } from "@karakeep/shared/utils/tag"; import type { AuthedContext, Context } from "../index"; import { authedProcedure, router } from "../index"; @@ -867,9 +868,11 @@ export const bookmarksAppRouter = router({ }; } - const toAddTagNames = input.attach.flatMap((i) => - i.tagName ? [i.tagName] : [], - ); + const toAddTagNames = input.attach + .flatMap((i) => (i.tagName ? [i.tagName] : [])) + .map(normalizeTagName) // strip leading # + .filter((n) => n.length > 0); // drop empty results + const toAddTagIds = input.attach.flatMap((i) => i.tagId ? [i.tagId] : [], ); diff --git a/packages/trpc/routers/tags.test.ts b/packages/trpc/routers/tags.test.ts index c5f92cb2..1e7118d2 100644 --- a/packages/trpc/routers/tags.test.ts +++ b/packages/trpc/routers/tags.test.ts @@ -143,4 +143,38 @@ describe("Tags Routes", () => { const resUser2 = await apiUser2.list(); expect(resUser2.tags.some((tag) => tag.name === "user1Tag")).toBeFalsy(); // Should not see other user's tags }); + + test<CustomTestContext>("create strips extra leading hashes", async ({ + apiCallers, + db, + }) => { + const api = apiCallers[0].tags; + + const created = await api.create({ name: "##demo" }); + expect(created.name).toBe("demo"); + + // Confirm DB row too + const row = await db.query.bookmarkTags.findFirst({ + where: eq(bookmarkTags.id, created.id), + }); + expect(row?.name).toBe("demo"); + }); + + test<CustomTestContext>("update normalizes leading hashes", async ({ + apiCallers, + db, + }) => { + const api = apiCallers[0].tags; + + const created = await api.create({ name: "#foo" }); + const updated = await api.update({ tagId: created.id, name: "##bar" }); + + expect(updated.name).toBe("bar"); + + // Confirm DB row too + const row = await db.query.bookmarkTags.findFirst({ + where: eq(bookmarkTags.id, updated.id), + }); + expect(row?.name).toBe("bar"); + }); }); |
