aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers/rules.ts
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-04-27 00:02:20 +0100
committerGitHub <noreply@github.com>2025-04-27 00:02:20 +0100
commit136f126296af65f50da598d084d1485c0e40437a (patch)
tree2725c7932ebbcb9b48b5af98eb9b72329a400260 /packages/trpc/routers/rules.ts
parentca47be7fe7be128f459c37614a04902a873fe289 (diff)
downloadkarakeep-136f126296af65f50da598d084d1485c0e40437a.tar.zst
feat: Implement generic rule engine (#1318)
* Add schema for the new rule engine * Add rule engine backend logic * Implement the worker logic and event firing * Implement the UI changesfor the rule engine * Ensure that when a referenced list or tag are deleted, the corresponding event/action is * Dont show smart lists in rule engine events * Add privacy validations for attached tag and list ids * Move the rules logic into a models
Diffstat (limited to 'packages/trpc/routers/rules.ts')
-rw-r--r--packages/trpc/routers/rules.ts120
1 files changed, 120 insertions, 0 deletions
diff --git a/packages/trpc/routers/rules.ts b/packages/trpc/routers/rules.ts
new file mode 100644
index 00000000..5def8003
--- /dev/null
+++ b/packages/trpc/routers/rules.ts
@@ -0,0 +1,120 @@
+import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
+import { and, eq, inArray } from "drizzle-orm";
+import { z } from "zod";
+
+import { bookmarkTags } from "@karakeep/db/schema";
+import {
+ RuleEngineRule,
+ zNewRuleEngineRuleSchema,
+ zRuleEngineRuleSchema,
+ zUpdateRuleEngineRuleSchema,
+} from "@karakeep/shared/types/rules";
+
+import { AuthedContext, authedProcedure, router } from "../index";
+import { List } from "../models/lists";
+import { RuleEngineRuleModel } from "../models/rules";
+
+const ensureRuleOwnership = experimental_trpcMiddleware<{
+ ctx: AuthedContext;
+ input: { id: string };
+}>().create(async (opts) => {
+ const rule = await RuleEngineRuleModel.fromId(opts.ctx, opts.input.id);
+ return opts.next({
+ ctx: {
+ ...opts.ctx,
+ rule,
+ },
+ });
+});
+
+const ensureTagListOwnership = experimental_trpcMiddleware<{
+ ctx: AuthedContext;
+ input: Omit<RuleEngineRule, "id">;
+}>().create(async (opts) => {
+ const tagIds = [
+ ...(opts.input.event.type === "tagAdded" ||
+ opts.input.event.type === "tagRemoved"
+ ? [opts.input.event.tagId]
+ : []),
+ ...(opts.input.condition.type === "hasTag"
+ ? [opts.input.condition.tagId]
+ : []),
+ ...opts.input.actions.flatMap((a) =>
+ a.type == "addTag" || a.type == "removeTag" ? [a.tagId] : [],
+ ),
+ ];
+
+ const validateTags = async () => {
+ if (tagIds.length == 0) {
+ return;
+ }
+ const userTags = await opts.ctx.db.query.bookmarkTags.findMany({
+ where: and(
+ eq(bookmarkTags.userId, opts.ctx.user.id),
+ inArray(bookmarkTags.id, tagIds),
+ ),
+ columns: {
+ id: true,
+ },
+ });
+ if (tagIds.some((t) => userTags.find((u) => u.id == t) == null)) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Tag not found",
+ });
+ }
+ };
+
+ const listIds = [
+ ...(opts.input.event.type === "addedToList" ||
+ opts.input.event.type === "removedFromList"
+ ? [opts.input.event.listId]
+ : []),
+ ...opts.input.actions.flatMap((a) =>
+ a.type == "addToList" || a.type == "removeFromList" ? [a.listId] : [],
+ ),
+ ];
+
+ const [_tags, _lists] = await Promise.all([
+ validateTags(),
+ Promise.all(listIds.map((l) => List.fromId(opts.ctx, l))),
+ ]);
+ return opts.next();
+});
+
+export const rulesAppRouter = router({
+ create: authedProcedure
+ .input(zNewRuleEngineRuleSchema)
+ .output(zRuleEngineRuleSchema)
+ .use(ensureTagListOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const newRule = await RuleEngineRuleModel.create(ctx, input);
+ return newRule.rule;
+ }),
+ update: authedProcedure
+ .input(zUpdateRuleEngineRuleSchema)
+ .output(zRuleEngineRuleSchema)
+ .use(ensureRuleOwnership)
+ .use(ensureTagListOwnership)
+ .mutation(async ({ ctx, input }) => {
+ await ctx.rule.update(input);
+ return ctx.rule.rule;
+ }),
+ delete: authedProcedure
+ .input(z.object({ id: z.string() }))
+ .use(ensureRuleOwnership)
+ .mutation(async ({ ctx }) => {
+ await ctx.rule.delete();
+ }),
+ list: authedProcedure
+ .output(
+ z.object({
+ rules: z.array(zRuleEngineRuleSchema),
+ }),
+ )
+ .query(async ({ ctx }) => {
+ return {
+ rules: (await RuleEngineRuleModel.getAll(ctx)).map((r) => r.rule),
+ };
+ }),
+});