diff options
Diffstat (limited to 'packages/trpc/models/backups.ts')
| -rw-r--r-- | packages/trpc/models/backups.ts | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/packages/trpc/models/backups.ts b/packages/trpc/models/backups.ts new file mode 100644 index 00000000..c7ab99ba --- /dev/null +++ b/packages/trpc/models/backups.ts @@ -0,0 +1,172 @@ +import { TRPCError } from "@trpc/server"; +import { and, desc, eq, lt } from "drizzle-orm"; +import { z } from "zod"; + +import { assets, backupsTable } from "@karakeep/db/schema"; +import { BackupQueue } from "@karakeep/shared-server"; +import { deleteAsset } from "@karakeep/shared/assetdb"; +import { zBackupSchema } from "@karakeep/shared/types/backups"; + +import { AuthedContext } from ".."; + +export class Backup { + private constructor( + private ctx: AuthedContext, + private backup: z.infer<typeof zBackupSchema>, + ) {} + + static async fromId(ctx: AuthedContext, backupId: string): Promise<Backup> { + const backup = await ctx.db.query.backupsTable.findFirst({ + where: and( + eq(backupsTable.id, backupId), + eq(backupsTable.userId, ctx.user.id), + ), + }); + + if (!backup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Backup not found", + }); + } + + return new Backup(ctx, backup); + } + + private static fromData( + ctx: AuthedContext, + backup: z.infer<typeof zBackupSchema>, + ): Backup { + return new Backup(ctx, backup); + } + + static async getAll(ctx: AuthedContext): Promise<Backup[]> { + const backups = await ctx.db.query.backupsTable.findMany({ + where: eq(backupsTable.userId, ctx.user.id), + orderBy: [desc(backupsTable.createdAt)], + }); + + return backups.map((b) => new Backup(ctx, b)); + } + + static async create(ctx: AuthedContext): Promise<Backup> { + const [backup] = await ctx.db + .insert(backupsTable) + .values({ + userId: ctx.user.id, + size: 0, + bookmarkCount: 0, + status: "pending", + }) + .returning(); + return new Backup(ctx, backup!); + } + + async triggerBackgroundJob({ + delayMs, + idempotencyKey, + }: { delayMs?: number; idempotencyKey?: string } = {}): Promise<void> { + await BackupQueue.enqueue( + { + userId: this.ctx.user.id, + backupId: this.backup.id, + }, + { + delayMs, + idempotencyKey, + }, + ); + } + + /** + * Generic update method for backup records + */ + async update( + data: Partial<{ + size: number; + bookmarkCount: number; + status: "pending" | "success" | "failure"; + assetId: string | null; + errorMessage: string | null; + }>, + ): Promise<void> { + await this.ctx.db + .update(backupsTable) + .set(data) + .where( + and( + eq(backupsTable.id, this.backup.id), + eq(backupsTable.userId, this.ctx.user.id), + ), + ); + + // Update local state + this.backup = { ...this.backup, ...data }; + } + + async delete(): Promise<void> { + if (this.backup.assetId) { + // Delete asset + await deleteAsset({ + userId: this.ctx.user.id, + assetId: this.backup.assetId, + }); + } + + await this.ctx.db.transaction(async (db) => { + // Delete asset first + if (this.backup.assetId) { + await db + .delete(assets) + .where( + and( + eq(assets.id, this.backup.assetId), + eq(assets.userId, this.ctx.user.id), + ), + ); + } + + // Delete backup record + await db + .delete(backupsTable) + .where( + and( + eq(backupsTable.id, this.backup.id), + eq(backupsTable.userId, this.ctx.user.id), + ), + ); + }); + } + + /** + * Finds backups older than the retention period + */ + static async findOldBackups( + ctx: AuthedContext, + retentionDays: number, + ): Promise<Backup[]> { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + + const oldBackups = await ctx.db.query.backupsTable.findMany({ + where: and( + eq(backupsTable.userId, ctx.user.id), + lt(backupsTable.createdAt, cutoffDate), + ), + }); + + return oldBackups.map((backup) => Backup.fromData(ctx, backup)); + } + + asPublic(): z.infer<typeof zBackupSchema> { + return this.backup; + } + + get id() { + return this.backup.id; + } + + get assetId() { + return this.backup.assetId; + } +} |
