From bf5bf996c63cc3af92bc0f302ec37f7dbbc9e94a Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 14 Sep 2025 08:27:44 +0000 Subject: refactor: strongly type the search plugin interface --- packages/plugins-search-meilisearch/src/index.ts | 18 ++++++++++++++++-- packages/shared/search.ts | 22 +++++++++++++++++++--- packages/trpc/routers/bookmarks.ts | 11 ++++++----- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/plugins-search-meilisearch/src/index.ts b/packages/plugins-search-meilisearch/src/index.ts index d54fb6bc..77d5a57e 100644 --- a/packages/plugins-search-meilisearch/src/index.ts +++ b/packages/plugins-search-meilisearch/src/index.ts @@ -3,6 +3,7 @@ import { MeiliSearch } from "meilisearch"; import type { BookmarkSearchDocument, + FilterQuery, SearchIndexClient, SearchOptions, SearchResponse, @@ -11,6 +12,19 @@ import { PluginProvider } from "@karakeep/shared/plugins"; import { envConfig } from "./env"; +function filterToMeiliSearchFilter(filter: FilterQuery): string { + switch (filter.type) { + case "eq": + return `${filter.field} = "${filter.value}"`; + case "in": + return `${filter.field} IN [${filter.values.join(",")}]`; + default: { + const exhaustiveCheck: never = filter; + throw new Error(`Unhandled color case: ${exhaustiveCheck}`); + } + } +} + class MeiliSearchIndexClient implements SearchIndexClient { constructor(private index: Index) {} @@ -28,10 +42,10 @@ class MeiliSearchIndexClient implements SearchIndexClient { async search(options: SearchOptions): Promise { const result = await this.index.search(options.query, { - filter: options.filter, + filter: options.filter?.map((f) => filterToMeiliSearchFilter(f)), limit: options.limit, offset: options.offset, - sort: options.sort, + sort: options.sort?.map((s) => `${s.field}:${s.order}`), attributesToRetrieve: ["id"], showRankingScore: true, }); diff --git a/packages/shared/search.ts b/packages/shared/search.ts index 5158f30f..d23ab29f 100644 --- a/packages/shared/search.ts +++ b/packages/shared/search.ts @@ -24,18 +24,34 @@ export const zBookmarkSearchDocument = z.object({ export type BookmarkSearchDocument = z.infer; +export type SortOrder = "asc" | "desc"; +export type SortableAttributes = "createdAt"; + +export type FilterableAttributes = "userId" | "id"; +export type FilterQuery = + | { + type: "eq"; + field: FilterableAttributes; + value: string; + } + | { + type: "in"; + field: FilterableAttributes; + values: string[]; + }; + export interface SearchResult { id: string; score?: number; } export interface SearchOptions { - // TODO: Make query, filter and sort strongly typed query: string; - filter?: string[]; + // Diffeernt filters are ANDed together + filter?: FilterQuery[]; limit?: number; offset?: number; - sort?: string[]; + sort?: { field: SortableAttributes; order: SortOrder }[]; } export interface SearchResponse { diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 31ffef4a..efd295f7 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -38,7 +38,7 @@ import { triggerSearchReindex, triggerWebhook, } from "@karakeep/shared/queues"; -import { getSearchClient } from "@karakeep/shared/search"; +import { FilterQuery, getSearchClient } from "@karakeep/shared/search"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; import { BookmarkTypes, @@ -765,17 +765,18 @@ export const bookmarksAppRouter = router({ } const parsedQuery = parseSearchQuery(input.text); - let filter: string[]; + let filter: FilterQuery[]; if (parsedQuery.matcher) { const bookmarkIds = await getBookmarkIdsFromMatcher( ctx, parsedQuery.matcher, ); filter = [ - `userId = '${ctx.user.id}' AND id IN [${bookmarkIds.join(",")}]`, + { type: "in", field: "id", values: bookmarkIds }, + { type: "eq", field: "userId", value: ctx.user.id }, ]; } else { - filter = [`userId = '${ctx.user.id}'`]; + filter = [{ type: "eq", field: "userId", value: ctx.user.id }]; } /** @@ -786,7 +787,7 @@ export const bookmarksAppRouter = router({ const resp = await client.search({ query: parsedQuery.text, filter, - sort: [`createdAt:${createdAtSortOrder}`], + sort: [{ field: "createdAt", order: createdAtSortOrder }], limit: input.limit, ...(input.cursor ? { -- cgit v1.2.3-70-g09d2