import { query } from "./_generated/server"; import type { Doc, Id } from "./_generated/dataModel"; import { v } from "convex/values"; import { optionalAuth } from "./helpers"; import { prioritizeRecentCreditTransactions } from "../lib/credits-activity"; import { MONTHLY_TIER_CREDITS, normalizeBillingTier } from "../lib/tier-credits"; const DEFAULT_TIER = "free" as const; const DEFAULT_SUBSCRIPTION_STATUS = "active" as const; const DASHBOARD_MEDIA_PREVIEW_LIMIT = 8; const MEDIA_LIBRARY_DEFAULT_LIMIT = 200; const MEDIA_LIBRARY_MIN_LIMIT = 1; const MEDIA_LIBRARY_MAX_LIMIT = 500; type MediaPreviewItem = { storageId: Id<"_storage">; previewStorageId?: Id<"_storage">; filename?: string; mimeType?: string; width?: number; height?: number; previewWidth?: number; previewHeight?: number; sourceCanvasId: Id<"canvases">; sourceNodeId: Id<"nodes">; createdAt: number; }; function readImageMediaPreview(node: Doc<"nodes">): MediaPreviewItem | null { if (node.type !== "image") { return null; } const data = (node.data as Record | undefined) ?? {}; const storageId = data.storageId; if (typeof storageId !== "string" || storageId.length === 0) { return null; } const previewStorageId = typeof data.previewStorageId === "string" && data.previewStorageId.length > 0 ? (data.previewStorageId as Id<"_storage">) : undefined; const filename = typeof data.filename === "string" ? data.filename : typeof data.originalFilename === "string" ? data.originalFilename : undefined; const mimeType = typeof data.mimeType === "string" ? data.mimeType : undefined; const width = typeof data.width === "number" && Number.isFinite(data.width) ? data.width : undefined; const height = typeof data.height === "number" && Number.isFinite(data.height) ? data.height : undefined; const previewWidth = typeof data.previewWidth === "number" && Number.isFinite(data.previewWidth) ? data.previewWidth : undefined; const previewHeight = typeof data.previewHeight === "number" && Number.isFinite(data.previewHeight) ? data.previewHeight : undefined; return { storageId: storageId as Id<"_storage">, previewStorageId, filename, mimeType, width, height, previewWidth, previewHeight, sourceCanvasId: node.canvasId, sourceNodeId: node._id, createdAt: node._creationTime, }; } function buildMediaPreview(nodes: Array>, limit: number): MediaPreviewItem[] { const candidates = nodes .map(readImageMediaPreview) .filter((item): item is MediaPreviewItem => item !== null) .sort((a, b) => b.createdAt - a.createdAt); const deduped = new Map, MediaPreviewItem>(); for (const item of candidates) { if (deduped.has(item.storageId)) { continue; } deduped.set(item.storageId, item); if (deduped.size >= limit) { break; } } return [...deduped.values()]; } function normalizeMediaLibraryLimit(limit: number | undefined): number { if (typeof limit !== "number" || !Number.isFinite(limit)) { return MEDIA_LIBRARY_DEFAULT_LIMIT; } return Math.min(MEDIA_LIBRARY_MAX_LIMIT, Math.max(MEDIA_LIBRARY_MIN_LIMIT, Math.floor(limit))); } export const getSnapshot = query({ args: {}, handler: async (ctx) => { const user = await optionalAuth(ctx); if (!user) { return { balance: { balance: 0, reserved: 0, available: 0, monthlyAllocation: 0 }, subscription: { tier: DEFAULT_TIER, status: DEFAULT_SUBSCRIPTION_STATUS, currentPeriodEnd: undefined, }, usageStats: { monthlyUsage: 0, totalGenerations: 0, monthlyCredits: MONTHLY_TIER_CREDITS[DEFAULT_TIER], }, recentTransactions: [], canvases: [], mediaPreview: [], generatedAt: Date.now(), }; } const [balanceRow, subscriptionRow, usageTransactions, recentTransactionsRaw, canvases] = await Promise.all([ ctx.db .query("creditBalances") .withIndex("by_user", (q) => q.eq("userId", user.userId)) .unique(), ctx.db .query("subscriptions") .withIndex("by_user", (q) => q.eq("userId", user.userId)) .order("desc") .first(), ctx.db .query("creditTransactions") .withIndex("by_user_type", (q) => q.eq("userId", user.userId).eq("type", "usage")) .order("desc") .collect(), ctx.db .query("creditTransactions") .withIndex("by_user", (q) => q.eq("userId", user.userId)) .order("desc") .take(80), ctx.db .query("canvases") .withIndex("by_owner_updated", (q) => q.eq("ownerId", user.userId)) .order("desc") .collect(), ]); const imageNodesByCanvas = await Promise.all( canvases.map((canvas) => ctx.db .query("nodes") .withIndex("by_canvas_type", (q) => q.eq("canvasId", canvas._id).eq("type", "image")) .order("desc") .collect(), ), ); const mediaPreview = buildMediaPreview(imageNodesByCanvas.flat(), DASHBOARD_MEDIA_PREVIEW_LIMIT); const tier = normalizeBillingTier(subscriptionRow?.tier); const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime(); let monthlyUsage = 0; let totalGenerations = 0; for (const transaction of usageTransactions) { if (transaction._creationTime < monthStart) { break; } if (transaction.status === "committed") { monthlyUsage += Math.abs(transaction.amount); totalGenerations += 1; } } const balance = { balance: balanceRow?.balance ?? 0, reserved: balanceRow?.reserved ?? 0, available: (balanceRow?.balance ?? 0) - (balanceRow?.reserved ?? 0), monthlyAllocation: balanceRow?.monthlyAllocation ?? MONTHLY_TIER_CREDITS[tier], }; return { balance, subscription: { tier: subscriptionRow?.tier ?? DEFAULT_TIER, status: subscriptionRow?.status ?? DEFAULT_SUBSCRIPTION_STATUS, currentPeriodEnd: subscriptionRow?.currentPeriodEnd, }, usageStats: { monthlyUsage, totalGenerations, monthlyCredits: MONTHLY_TIER_CREDITS[tier], }, recentTransactions: prioritizeRecentCreditTransactions(recentTransactionsRaw, 20), canvases, mediaPreview, generatedAt: Date.now(), }; }, }); export const listMediaLibrary = query({ args: { limit: v.optional(v.number()), }, handler: async (ctx, { limit }) => { const user = await optionalAuth(ctx); if (!user) { return []; } const normalizedLimit = normalizeMediaLibraryLimit(limit); const canvases = await ctx.db .query("canvases") .withIndex("by_owner_updated", (q) => q.eq("ownerId", user.userId)) .order("desc") .collect(); if (canvases.length === 0) { return []; } const imageNodesByCanvas = await Promise.all( canvases.map((canvas) => ctx.db .query("nodes") .withIndex("by_canvas_type", (q) => q.eq("canvasId", canvas._id).eq("type", "image")) .order("desc") .collect(), ), ); return buildMediaPreview(imageNodesByCanvas.flat(), normalizedLimit); }, });