From 9fd26b472b18924ab11afcebace90329b0fe3abf Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 12 Jan 2025 20:03:47 +0000 Subject: feat: Add ability to filter by bookmark type --- packages/shared/searchQueryParser.test.ts | 55 ++++++++++++++++++++++++++++++ packages/shared/searchQueryParser.ts | 28 +++++++++++++++ packages/shared/types/search.ts | 14 ++++++++ packages/trpc/lib/__tests__/search.test.ts | 48 ++++++++++++++++++++++++++ packages/trpc/lib/search.ts | 10 ++++++ 5 files changed, 155 insertions(+) (limited to 'packages') diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts index 5af7ca2f..7430d58f 100644 --- a/packages/shared/searchQueryParser.test.ts +++ b/packages/shared/searchQueryParser.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vitest"; import { parseSearchQuery } from "./searchQueryParser"; +import { BookmarkTypes } from "./types/bookmarks"; describe("Search Query Parser", () => { test("simple is queries", () => { @@ -68,6 +69,60 @@ describe("Search Query Parser", () => { inList: false, }, }); + expect(parseSearchQuery("is:link")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: false, + }, + }); + expect(parseSearchQuery("-is:link")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: true, + }, + }); + expect(parseSearchQuery("is:text")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.TEXT, + inverse: false, + }, + }); + expect(parseSearchQuery("-is:text")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.TEXT, + inverse: true, + }, + }); + expect(parseSearchQuery("is:media")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.ASSET, + inverse: false, + }, + }); + expect(parseSearchQuery("-is:media")).toEqual({ + result: "full", + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.ASSET, + inverse: true, + }, + }); }); test("simple string queries", () => { diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts index e52af274..4f68523b 100644 --- a/packages/shared/searchQueryParser.ts +++ b/packages/shared/searchQueryParser.ts @@ -15,6 +15,7 @@ import { } from "typescript-parsec"; import { z } from "zod"; +import { BookmarkTypes } from "./types/bookmarks"; import { Matcher } from "./types/search"; enum TokenType { @@ -136,6 +137,33 @@ MATCHER.setPattern( text: "", matcher: { type: "inlist", inList: !minus }, }; + case "link": + return { + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: !!minus, + }, + }; + case "text": + return { + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.TEXT, + inverse: !!minus, + }, + }; + case "media": + return { + text: "", + matcher: { + type: "type", + typeName: BookmarkTypes.ASSET, + inverse: !!minus, + }, + }; default: // If the token is not known, emit it as pure text return { diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts index 9d97fdd8..19c5d0e2 100644 --- a/packages/shared/types/search.ts +++ b/packages/shared/types/search.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { BookmarkTypes } from "./bookmarks"; + const zTagNameMatcher = z.object({ type: z.literal("tagName"), tagName: z.string(), @@ -50,6 +52,16 @@ const zIsInListMatcher = z.object({ inList: z.boolean(), }); +const zTypeMatcher = z.object({ + type: z.literal("type"), + typeName: z.enum([ + BookmarkTypes.LINK, + BookmarkTypes.TEXT, + BookmarkTypes.ASSET, + ]), + inverse: z.boolean(), +}); + const zNonRecursiveMatcher = z.union([ zTagNameMatcher, zListNameMatcher, @@ -60,6 +72,7 @@ const zNonRecursiveMatcher = z.union([ zDateBeforeMatcher, zIsTaggedMatcher, zIsInListMatcher, + zTypeMatcher, ]); type NonRecursiveMatcher = z.infer; @@ -79,6 +92,7 @@ export const zMatcherSchema: z.ZodType = z.lazy(() => { zDateBeforeMatcher, zIsTaggedMatcher, zIsInListMatcher, + zTypeMatcher, z.object({ type: z.literal("and"), matchers: z.array(zMatcherSchema), diff --git a/packages/trpc/lib/__tests__/search.test.ts b/packages/trpc/lib/__tests__/search.test.ts index 468aef83..8a6f1949 100644 --- a/packages/trpc/lib/__tests__/search.test.ts +++ b/packages/trpc/lib/__tests__/search.test.ts @@ -280,6 +280,54 @@ describe("getBookmarkIdsFromMatcher", () => { expect(result).toEqual(["b1", "b2"]); }); + it("should handle type matcher", async () => { + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: false, + }), + ).toEqual(["b1", "b2", "b4"]); + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.TEXT, + inverse: false, + }), + ).toEqual(["b3", "b5"]); + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.ASSET, + inverse: false, + }), + ).toEqual(["b6"]); + }); + + it("should handle type matcher with inverse=true", async () => { + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.LINK, + inverse: true, + }), + ).toEqual(["b3", "b5", "b6"]); + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.TEXT, + inverse: true, + }), + ).toEqual(["b1", "b2", "b4", "b6"]); + expect( + await getBookmarkIdsFromMatcher(mockCtx, { + type: "type", + typeName: BookmarkTypes.ASSET, + inverse: true, + }), + ).toEqual(["b1", "b2", "b3", "b4", "b5"]); + }); + it("should handle dateBefore matcher with inverse=true", async () => { const matcher: Matcher = { type: "dateBefore", diff --git a/packages/trpc/lib/search.ts b/packages/trpc/lib/search.ts index 74aaf51b..eac323df 100644 --- a/packages/trpc/lib/search.ts +++ b/packages/trpc/lib/search.ts @@ -7,6 +7,7 @@ import { like, lt, lte, + ne, notExists, notLike, } from "drizzle-orm"; @@ -233,6 +234,15 @@ async function getIds( ), ); } + case "type": { + const comp = matcher.inverse + ? ne(bookmarks.type, matcher.typeName) + : eq(bookmarks.type, matcher.typeName); + return db + .select({ id: bookmarks.id }) + .from(bookmarks) + .where(and(eq(bookmarks.userId, userId), comp)); + } case "and": { const vals = await Promise.all( matcher.matchers.map((m) => getIds(db, userId, m)), -- cgit v1.2.3-70-g09d2