diff options
| author | Andrii Mokhovyk <andrii.mokhovyk@gmail.com> | 2026-01-18 16:36:49 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-18 14:36:49 +0000 |
| commit | c56cf4e24f6134547fb9c5b58eb20840f5083e9e (patch) | |
| tree | ec9792cfcc6cbc6e45490d02e140b9241dca3fae /packages | |
| parent | 1b98014d6cb0e3eb824d58ccbd35f39864e6ec88 (diff) | |
| download | karakeep-c56cf4e24f6134547fb9c5b58eb20840f5083e9e.tar.zst | |
feat(rules): add "Title Contains" condition to Rule Engine (#1670) (#2354)
* feat(rules): add "Title Contains" condition to Rule Engine (#1670)
* feat(rules): hide title conditions for bookmark created trigger
* fix typecheck
Diffstat (limited to '')
| -rw-r--r-- | packages/shared/types/rules.ts | 25 | ||||
| -rw-r--r-- | packages/trpc/lib/__tests__/ruleEngine.test.ts | 33 | ||||
| -rw-r--r-- | packages/trpc/lib/ruleEngine.ts | 17 |
3 files changed, 75 insertions, 0 deletions
diff --git a/packages/shared/types/rules.ts b/packages/shared/types/rules.ts index 0daec524..fd99c266 100644 --- a/packages/shared/types/rules.ts +++ b/packages/shared/types/rules.ts @@ -59,6 +59,16 @@ const zUrlDoesNotContainCondition = z.object({ str: z.string(), }); +const zTitleContainsCondition = z.object({ + type: z.literal("titleContains"), + str: z.string(), +}); + +const zTitleDoesNotContainCondition = z.object({ + type: z.literal("titleDoesNotContain"), + str: z.string(), +}); + const zImportedFromFeedCondition = z.object({ type: z.literal("importedFromFeed"), feedId: z.string(), @@ -86,6 +96,8 @@ const nonRecursiveCondition = z.discriminatedUnion("type", [ zAlwaysTrueCondition, zUrlContainsCondition, zUrlDoesNotContainCondition, + zTitleContainsCondition, + zTitleDoesNotContainCondition, zImportedFromFeedCondition, zBookmarkTypeIsCondition, zHasTagCondition, @@ -105,6 +117,8 @@ export const zRuleEngineConditionSchema: z.ZodType<RuleEngineCondition> = zAlwaysTrueCondition, zUrlContainsCondition, zUrlDoesNotContainCondition, + zTitleContainsCondition, + zTitleDoesNotContainCondition, zImportedFromFeedCondition, zBookmarkTypeIsCondition, zHasTagCondition, @@ -244,6 +258,17 @@ const ruleValidaitorFn = ( return false; } return true; + case "titleContains": + case "titleDoesNotContain": + if (condition.str.length == 0) { + ctx.addIssue({ + code: "custom", + message: "You must specify a title for this condition type", + path: ["condition", "str"], + }); + return false; + } + return true; case "hasTag": if (condition.tagId.length == 0) { ctx.addIssue({ diff --git a/packages/trpc/lib/__tests__/ruleEngine.test.ts b/packages/trpc/lib/__tests__/ruleEngine.test.ts index 600d8aa9..d7f216e5 100644 --- a/packages/trpc/lib/__tests__/ruleEngine.test.ts +++ b/packages/trpc/lib/__tests__/ruleEngine.test.ts @@ -126,6 +126,7 @@ describe("RuleEngine", () => { .values({ userId, type: BookmarkTypes.LINK, + title: "Example Bookmark Title", favourited: false, archived: false, }) @@ -235,6 +236,38 @@ describe("RuleEngine", () => { expect(engine.doesBookmarkMatchConditions(condition)).toBe(true); }); + it("should return true for titleContains condition", () => { + const condition: RuleEngineCondition = { + type: "titleContains", + str: "Example", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(true); + }); + + it("should return false for titleContains condition mismatch", () => { + const condition: RuleEngineCondition = { + type: "titleContains", + str: "nonexistent", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(false); + }); + + it("should return false for titleDoesNotContain condition when title contains string", () => { + const condition: RuleEngineCondition = { + type: "titleDoesNotContain", + str: "Example", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(false); + }); + + it("should return true for titleDoesNotContain condition when title does not contain string", () => { + const condition: RuleEngineCondition = { + type: "titleDoesNotContain", + str: "nonexistent", + }; + expect(engine.doesBookmarkMatchConditions(condition)).toBe(true); + }); + it("should return true for importedFromFeed condition", () => { const condition: RuleEngineCondition = { type: "importedFromFeed", diff --git a/packages/trpc/lib/ruleEngine.ts b/packages/trpc/lib/ruleEngine.ts index 6b5f8fdf..233a6acf 100644 --- a/packages/trpc/lib/ruleEngine.ts +++ b/packages/trpc/lib/ruleEngine.ts @@ -22,6 +22,7 @@ async function fetchBookmark(db: AuthedContext["db"], bookmarkId: string) { link: { columns: { url: true, + title: true, }, }, text: true, @@ -61,6 +62,16 @@ export class RuleEngine { private rules: RuleEngineRule[], ) {} + private get bookmarkTitle(): string { + return ( + this.bookmark.title ?? + (this.bookmark.type === BookmarkTypes.LINK + ? this.bookmark.link?.title + : "") ?? + "" + ); + } + static async forBookmark(ctx: AuthedContext, bookmarkId: string) { const [bookmark, rules] = await Promise.all([ fetchBookmark(ctx.db, bookmarkId), @@ -90,6 +101,12 @@ export class RuleEngine { !(this.bookmark.link?.url ?? "").includes(condition.str) ); } + case "titleContains": { + return this.bookmarkTitle.includes(condition.str); + } + case "titleDoesNotContain": { + return !this.bookmarkTitle.includes(condition.str); + } case "importedFromFeed": { return this.bookmark.rssFeeds.some( (f) => f.rssFeedId === condition.feedId, |
