diff options
Diffstat (limited to 'packages/trpc/models/rules.ts')
| -rw-r--r-- | packages/trpc/models/rules.ts | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/packages/trpc/models/rules.ts b/packages/trpc/models/rules.ts new file mode 100644 index 00000000..7b17fd8a --- /dev/null +++ b/packages/trpc/models/rules.ts @@ -0,0 +1,233 @@ +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { db as DONT_USE_DB } from "@karakeep/db"; +import { + ruleEngineActionsTable, + ruleEngineRulesTable, +} from "@karakeep/db/schema"; +import { + RuleEngineRule, + zNewRuleEngineRuleSchema, + zRuleEngineActionSchema, + zRuleEngineConditionSchema, + zRuleEngineEventSchema, + zUpdateRuleEngineRuleSchema, +} from "@karakeep/shared/types/rules"; + +import { AuthedContext } from ".."; +import { PrivacyAware } from "./privacy"; + +function dummy_fetchRule(ctx: AuthedContext, id: string) { + return DONT_USE_DB.query.ruleEngineRulesTable.findFirst({ + where: and( + eq(ruleEngineRulesTable.id, id), + eq(ruleEngineRulesTable.userId, ctx.user.id), + ), + with: { + actions: true, // Assuming actions are related; adjust if needed + }, + }); +} + +type FetchedRuleType = NonNullable<Awaited<ReturnType<typeof dummy_fetchRule>>>; + +export class RuleEngineRuleModel implements PrivacyAware { + protected constructor( + protected ctx: AuthedContext, + public rule: RuleEngineRule & { userId: string }, + ) {} + + private static fromData( + ctx: AuthedContext, + ruleData: FetchedRuleType, + ): RuleEngineRuleModel { + return new RuleEngineRuleModel(ctx, { + id: ruleData.id, + userId: ruleData.userId, + name: ruleData.name, + description: ruleData.description, + enabled: ruleData.enabled, + event: zRuleEngineEventSchema.parse(JSON.parse(ruleData.event)), + condition: zRuleEngineConditionSchema.parse( + JSON.parse(ruleData.condition), + ), + actions: ruleData.actions.map((a) => + zRuleEngineActionSchema.parse(JSON.parse(a.action)), + ), + }); + } + + static async fromId( + ctx: AuthedContext, + id: string, + ): Promise<RuleEngineRuleModel> { + const ruleData = await ctx.db.query.ruleEngineRulesTable.findFirst({ + where: and( + eq(ruleEngineRulesTable.id, id), + eq(ruleEngineRulesTable.userId, ctx.user.id), + ), + with: { + actions: true, // Assuming actions are related; adjust if needed + }, + }); + + if (!ruleData) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Rule not found", + }); + } + + return this.fromData(ctx, ruleData); + } + + ensureCanAccess(ctx: AuthedContext): void { + if (this.rule.userId != ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access resource", + }); + } + } + + static async create( + ctx: AuthedContext, + input: z.infer<typeof zNewRuleEngineRuleSchema>, + ): Promise<RuleEngineRuleModel> { + // Similar to lists create, but for rules + const insertedRule = await ctx.db.transaction(async (tx) => { + const [newRule] = await tx + .insert(ruleEngineRulesTable) + .values({ + name: input.name, + description: input.description, + enabled: input.enabled, + event: JSON.stringify(input.event), + condition: JSON.stringify(input.condition), + userId: ctx.user.id, + listId: + input.event.type === "addedToList" || + input.event.type === "removedFromList" + ? input.event.listId + : null, + tagId: + input.event.type === "tagAdded" || input.event.type === "tagRemoved" + ? input.event.tagId + : null, + }) + .returning(); + + if (input.actions.length > 0) { + await tx.insert(ruleEngineActionsTable).values( + input.actions.map((action) => ({ + ruleId: newRule.id, + userId: ctx.user.id, + action: JSON.stringify(action), + listId: + action.type === "addToList" || action.type === "removeFromList" + ? action.listId + : null, + tagId: + action.type === "addTag" || action.type === "removeTag" + ? action.tagId + : null, + })), + ); + } + return newRule; + }); + + // Fetch the full rule after insertion + return await RuleEngineRuleModel.fromId(ctx, insertedRule.id); + } + + async update( + input: z.infer<typeof zUpdateRuleEngineRuleSchema>, + ): Promise<void> { + if (this.rule.id !== input.id) { + throw new TRPCError({ code: "BAD_REQUEST", message: "ID mismatch" }); + } + + await this.ctx.db.transaction(async (tx) => { + const result = await tx + .update(ruleEngineRulesTable) + .set({ + name: input.name, + description: input.description, + enabled: input.enabled, + event: JSON.stringify(input.event), + condition: JSON.stringify(input.condition), + listId: + input.event.type === "addedToList" || + input.event.type === "removedFromList" + ? input.event.listId + : null, + tagId: + input.event.type === "tagAdded" || input.event.type === "tagRemoved" + ? input.event.tagId + : null, + }) + .where( + and( + eq(ruleEngineRulesTable.id, input.id), + eq(ruleEngineRulesTable.userId, this.ctx.user.id), + ), + ); + + if (result.changes === 0) { + throw new TRPCError({ code: "NOT_FOUND", message: "Rule not found" }); + } + + if (input.actions.length > 0) { + await tx + .delete(ruleEngineActionsTable) + .where(eq(ruleEngineActionsTable.ruleId, input.id)); + await tx.insert(ruleEngineActionsTable).values( + input.actions.map((action) => ({ + ruleId: input.id, + userId: this.ctx.user.id, + action: JSON.stringify(action), + listId: + action.type === "addToList" || action.type === "removeFromList" + ? action.listId + : null, + tagId: + action.type === "addTag" || action.type === "removeTag" + ? action.tagId + : null, + })), + ); + } + }); + + this.rule = await RuleEngineRuleModel.fromId(this.ctx, this.rule.id).then( + (r) => r.rule, + ); + } + + async delete(): Promise<void> { + const result = await this.ctx.db + .delete(ruleEngineRulesTable) + .where( + and( + eq(ruleEngineRulesTable.id, this.rule.id), + eq(ruleEngineRulesTable.userId, this.ctx.user.id), + ), + ); + + if (result.changes === 0) { + throw new TRPCError({ code: "NOT_FOUND", message: "Rule not found" }); + } + } + + static async getAll(ctx: AuthedContext): Promise<RuleEngineRuleModel[]> { + const rulesData = await ctx.db.query.ruleEngineRulesTable.findMany({ + where: eq(ruleEngineRulesTable.userId, ctx.user.id), + with: { actions: true }, + }); + + return rulesData.map((r) => this.fromData(ctx, r)); + } +} |
