aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/open-api/karakeep-openapi-spec.json3
-rw-r--r--packages/shared/types/tags.ts11
-rw-r--r--packages/shared/utils/tag.ts6
-rw-r--r--packages/trpc/routers/bookmarks.ts9
-rw-r--r--packages/trpc/routers/tags.test.ts34
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");
+ });
});