diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-10-04 13:40:24 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-04 13:40:24 +0100 |
| commit | 4a580d713621f99abb8baabc9b847ce039d44842 (patch) | |
| tree | a2aa6f3ae8045ad50a9316624e2a7028dd098c6b /packages/trpc/models/importSessions.ts | |
| parent | 5e331a7d5b8d9666812170547574804d8b6da741 (diff) | |
| download | karakeep-4a580d713621f99abb8baabc9b847ce039d44842.tar.zst | |
feat: Revamp import experience (#2001)
* WIP: import v2
* remove new session button
* don't redirect after import
* store and lint to root list
* models + tests
* redesign the progress
* simplify the import session for ow
* drop status from session schema
* split the import session page
* i18n
* fix test
* remove pagination
* fix some colors in darkmode
* one last fix
* add privacy filter
* privacy check
* fix interactivity of import progress
* fix test
Diffstat (limited to 'packages/trpc/models/importSessions.ts')
| -rw-r--r-- | packages/trpc/models/importSessions.ts | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/packages/trpc/models/importSessions.ts b/packages/trpc/models/importSessions.ts new file mode 100644 index 00000000..270c2bce --- /dev/null +++ b/packages/trpc/models/importSessions.ts @@ -0,0 +1,180 @@ +import { TRPCError } from "@trpc/server"; +import { and, count, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { + bookmarkLinks, + bookmarks, + importSessionBookmarks, + importSessions, +} from "@karakeep/db/schema"; +import { + zCreateImportSessionRequestSchema, + ZImportSession, + ZImportSessionWithStats, +} from "@karakeep/shared/types/importSessions"; + +import type { AuthedContext } from "../index"; +import { PrivacyAware } from "./privacy"; + +export class ImportSession implements PrivacyAware { + protected constructor( + protected ctx: AuthedContext, + public session: ZImportSession, + ) {} + + static async fromId( + ctx: AuthedContext, + importSessionId: string, + ): Promise<ImportSession> { + const session = await ctx.db.query.importSessions.findFirst({ + where: and( + eq(importSessions.id, importSessionId), + eq(importSessions.userId, ctx.user.id), + ), + }); + + if (!session) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Import session not found", + }); + } + + return new ImportSession(ctx, session); + } + + static async create( + ctx: AuthedContext, + input: z.infer<typeof zCreateImportSessionRequestSchema>, + ): Promise<ImportSession> { + const [session] = await ctx.db + .insert(importSessions) + .values({ + name: input.name, + userId: ctx.user.id, + rootListId: input.rootListId, + }) + .returning(); + + return new ImportSession(ctx, session); + } + + static async getAll(ctx: AuthedContext): Promise<ImportSession[]> { + const sessions = await ctx.db.query.importSessions.findMany({ + where: eq(importSessions.userId, ctx.user.id), + orderBy: (importSessions, { desc }) => [desc(importSessions.createdAt)], + limit: 50, + }); + + return sessions.map((session) => new ImportSession(ctx, session)); + } + + static async getAllWithStats( + ctx: AuthedContext, + ): Promise<ZImportSessionWithStats[]> { + const sessions = await this.getAll(ctx); + + return await Promise.all( + sessions.map(async (session) => { + return await session.getWithStats(); + }), + ); + } + + ensureCanAccess(ctx: AuthedContext): void { + if (this.session.userId !== ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access this import session", + }); + } + } + + async attachBookmark(bookmarkId: string): Promise<void> { + await this.ctx.db.insert(importSessionBookmarks).values({ + importSessionId: this.session.id, + bookmarkId, + }); + } + + async getWithStats(): Promise<ZImportSessionWithStats> { + // Get bookmark counts by status + const statusCounts = await this.ctx.db + .select({ + crawlStatus: bookmarkLinks.crawlStatus, + taggingStatus: bookmarks.taggingStatus, + count: count(), + }) + .from(importSessionBookmarks) + .innerJoin( + importSessions, + eq(importSessions.id, importSessionBookmarks.importSessionId), + ) + .leftJoin(bookmarks, eq(bookmarks.id, importSessionBookmarks.bookmarkId)) + .leftJoin( + bookmarkLinks, + eq(bookmarkLinks.id, importSessionBookmarks.bookmarkId), + ) + .where( + and( + eq(importSessionBookmarks.importSessionId, this.session.id), + eq(importSessions.userId, this.ctx.user.id), + ), + ) + .groupBy(bookmarkLinks.crawlStatus, bookmarks.taggingStatus); + + const stats = { + totalBookmarks: 0, + completedBookmarks: 0, + failedBookmarks: 0, + pendingBookmarks: 0, + processingBookmarks: 0, + }; + + statusCounts.forEach((statusCount) => { + stats.totalBookmarks += statusCount.count; + if ( + statusCount.crawlStatus === "success" && + statusCount.taggingStatus === "success" + ) { + stats.completedBookmarks += statusCount.count; + } else if ( + statusCount.crawlStatus === "failure" || + statusCount.taggingStatus === "failure" + ) { + stats.failedBookmarks += statusCount.count; + } else if ( + statusCount.crawlStatus === "pending" || + statusCount.taggingStatus === "pending" + ) { + stats.pendingBookmarks += statusCount.count; + } + }); + + return { + ...this.session, + status: stats.pendingBookmarks > 0 ? "in_progress" : "completed", + ...stats, + }; + } + + async delete(): Promise<void> { + // Delete the session (cascade will handle the bookmarks) + const result = await this.ctx.db + .delete(importSessions) + .where( + and( + eq(importSessions.id, this.session.id), + eq(importSessions.userId, this.ctx.user.id), + ), + ); + + if (result.changes === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Import session not found", + }); + } + } +} |
