aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/models/importSessions.ts
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-10-04 13:40:24 +0100
committerGitHub <noreply@github.com>2025-10-04 13:40:24 +0100
commit4a580d713621f99abb8baabc9b847ce039d44842 (patch)
treea2aa6f3ae8045ad50a9316624e2a7028dd098c6b /packages/trpc/models/importSessions.ts
parent5e331a7d5b8d9666812170547574804d8b6da741 (diff)
downloadkarakeep-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.ts180
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",
+ });
+ }
+ }
+}