aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorAndrii Mokhovyk <andrii.mokhovyk@gmail.com>2026-01-18 16:36:49 +0200
committerGitHub <noreply@github.com>2026-01-18 14:36:49 +0000
commitc56cf4e24f6134547fb9c5b58eb20840f5083e9e (patch)
treeec9792cfcc6cbc6e45490d02e140b9241dca3fae /packages
parent1b98014d6cb0e3eb824d58ccbd35f39864e6ec88 (diff)
downloadkarakeep-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.ts25
-rw-r--r--packages/trpc/lib/__tests__/ruleEngine.test.ts33
-rw-r--r--packages/trpc/lib/ruleEngine.ts17
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,