From d9bc805b9ee09bb75bd916af027301828cc81e29 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 15 Nov 2025 16:18:20 +1100 Subject: [PATCH 1/3] Remove reply to emails --- .../jobs/account/mark-for-deletion/route.ts | 584 +++++++------- app/(api)/api/jobs/daily-summary/route.ts | 330 ++++---- .../api/jobs/user/mark-for-deletion/route.ts | 650 ++++++++------- app/(api)/api/webhook/auth/route.ts | 760 +++++++++--------- 4 files changed, 1158 insertions(+), 1166 deletions(-) diff --git a/app/(api)/api/jobs/account/mark-for-deletion/route.ts b/app/(api)/api/jobs/account/mark-for-deletion/route.ts index a024c88..3742cf9 100644 --- a/app/(api)/api/jobs/account/mark-for-deletion/route.ts +++ b/app/(api)/api/jobs/account/mark-for-deletion/route.ts @@ -3,337 +3,335 @@ import { serve } from "@upstash/workflow/nextjs"; import { and, eq, isNull, lte } from "drizzle-orm"; import { Resend } from "resend"; import { - DeletionNoticePlainText, - OrgDeletionNotice, + DeletionNoticePlainText, + OrgDeletionNotice, } from "@/components/emails/org-deletion-notice"; import { - SevenDayWarning, - sevenDayWarningPlainText, + SevenDayWarning, + sevenDayWarningPlainText, } from "@/components/emails/seven-day-warning"; import { opsOrganization } from "@/ops/drizzle/schema"; import { getOpsDatabase } from "@/ops/useOps"; -type ClerkOrgData = { - createdBy?: string; -}; +type ClerkOrgData = { createdBy?: string }; async function getOrgCreatorDetails(org: { - id: string; - name: string; - rawData: unknown; + id: string; + name: string; + rawData: unknown; }) { - const rawData = org.rawData as ClerkOrgData; - const createdByUserId = rawData?.createdBy; + const rawData = org.rawData as ClerkOrgData; + const createdByUserId = rawData?.createdBy; - if (!createdByUserId) { - throw new Error( - `No creator user ID found for organization ${org.name} (${org.id})`, - ); - } + if (!createdByUserId) { + throw new Error( + `No creator user ID found for organization ${org.name} (${org.id})`, + ); + } - try { - const clerk = await clerkClient(); - const creator = await clerk.users.getUser(createdByUserId); - const contactEmail = creator.emailAddresses[0]?.emailAddress; - const firstName = creator.firstName || undefined; + try { + const clerk = await clerkClient(); + const creator = await clerk.users.getUser(createdByUserId); + const contactEmail = creator.emailAddresses[0]?.emailAddress; + const firstName = creator.firstName || undefined; - if (!contactEmail) { - throw new Error( - `No email address found for creator of organization ${org.name} (${org.id})`, - ); - } + if (!contactEmail) { + throw new Error( + `No email address found for creator of organization ${org.name} (${org.id})`, + ); + } - return { contactEmail, firstName }; - } catch (error) { - console.error( - `[OrgDeletion] Failed to fetch creator details for user ${createdByUserId}:`, - error, - ); - throw new Error( - `Failed to fetch creator details for organization ${org.name} (${org.id}): ${error}`, - ); - } + return { contactEmail, firstName }; + } catch (error) { + console.error( + `[OrgDeletion] Failed to fetch creator details for user ${createdByUserId}:`, + error, + ); + throw new Error( + `Failed to fetch creator details for organization ${org.name} (${org.id}): ${error}`, + ); + } } export const { POST } = serve(async (context) => { - const resend = new Resend(process.env.RESEND_API_KEY); - console.log( - `[OrgDeletion] Resend API Key configured: ${process.env.RESEND_API_KEY ? "Yes" : "No"}`, - ); + const resend = new Resend(process.env.RESEND_API_KEY); + console.log( + `[OrgDeletion] Resend API Key configured: ${process.env.RESEND_API_KEY ? "Yes" : "No"}`, + ); - console.log( - "[OrgDeletion] Starting organization deletion job at", - new Date().toISOString(), - ); - const db = await getOpsDatabase(); - const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60); - console.log( - "[OrgDeletion] Looking for orgs inactive since:", - sixtyDaysAgo.toISOString(), - ); + console.log( + "[OrgDeletion] Starting organization deletion job at", + new Date().toISOString(), + ); + const db = await getOpsDatabase(); + const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60); + console.log( + "[OrgDeletion] Looking for orgs inactive since:", + sixtyDaysAgo.toISOString(), + ); - // Step 1: Mark organizations for deletion (60 days inactive) - const orgsToMark = await context.run("fetch-orgs-to-mark", async () => { - const orgs = await db - .select() - .from(opsOrganization) - .where( - and( - lte(opsOrganization.lastActiveAt, sixtyDaysAgo), - isNull(opsOrganization.markedForDeletionAt), - ), - ); + // Step 1: Mark organizations for deletion (60 days inactive) + const orgsToMark = await context.run("fetch-orgs-to-mark", async () => { + const orgs = await db + .select() + .from(opsOrganization) + .where( + and( + lte(opsOrganization.lastActiveAt, sixtyDaysAgo), + isNull(opsOrganization.markedForDeletionAt), + ), + ); - console.log( - `[OrgDeletion] Found ${orgs.length} organizations to mark for deletion`, - ); - if (orgs.length > 0) { - console.log( - "[OrgDeletion] Organizations to mark:", - orgs.map((o) => ({ - id: o.id, - name: o.name, - lastActiveAt: o.lastActiveAt?.toISOString(), - })), - ); - } else { - console.log( - "[OrgDeletion] No organizations found that need to be marked for deletion", - ); - } - return orgs; - }); + console.log( + `[OrgDeletion] Found ${orgs.length} organizations to mark for deletion`, + ); + if (orgs.length > 0) { + console.log( + "[OrgDeletion] Organizations to mark:", + orgs.map((o) => ({ + id: o.id, + name: o.name, + lastActiveAt: o.lastActiveAt?.toISOString(), + })), + ); + } else { + console.log( + "[OrgDeletion] No organizations found that need to be marked for deletion", + ); + } + return orgs; + }); - // Step 2: Send 60-day deletion notice and mark organizations - await context.run("mark-orgs-for-deletion", async () => { - if (orgsToMark.length === 0) { - console.log( - "[OrgDeletion] Skipping step 2: No organizations to mark for deletion", - ); - return; - } + // Step 2: Send 60-day deletion notice and mark organizations + await context.run("mark-orgs-for-deletion", async () => { + if (orgsToMark.length === 0) { + console.log( + "[OrgDeletion] Skipping step 2: No organizations to mark for deletion", + ); + return; + } - console.log( - `[OrgDeletion] Processing ${orgsToMark.length} organizations for 60-day deletion notices`, - ); - for (const org of orgsToMark) { - try { - // Get creator details - const { contactEmail, firstName } = await getOrgCreatorDetails(org); + console.log( + `[OrgDeletion] Processing ${orgsToMark.length} organizations for 60-day deletion notices`, + ); + for (const org of orgsToMark) { + try { + // Get creator details + const { contactEmail, firstName } = await getOrgCreatorDetails(org); - console.log( - `[OrgDeletion] Processing org ${org.name} (${org.id}), contact email: ${contactEmail}`, - ); - console.log( - `[OrgDeletion] Raw org data for ${org.name}:`, - JSON.stringify(org.rawData, null, 2), - ); + console.log( + `[OrgDeletion] Processing org ${org.name} (${org.id}), contact email: ${contactEmail}`, + ); + console.log( + `[OrgDeletion] Raw org data for ${org.name}:`, + JSON.stringify(org.rawData, null, 2), + ); - // Send 60-day deletion notice - console.log( - `[OrgDeletion] Sending 60-day notice email to ${contactEmail} for org ${org.name}`, - ); - const emailResult = await resend.emails.send({ - from: "Manage Team ", - to: contactEmail, - subject: "Organization Deletion Notice - 60 Days", - react: OrgDeletionNotice({ - firstName: firstName, - email: contactEmail, - organizationName: org.name, - }), - text: DeletionNoticePlainText({ - firstName: firstName, - email: contactEmail, - organizationName: org.name, - }), - }); - console.log( - `[OrgDeletion] Email send result for ${org.name}:`, - JSON.stringify(emailResult, null, 2), - ); + // Send 60-day deletion notice + console.log( + `[OrgDeletion] Sending 60-day notice email to ${contactEmail} for org ${org.name}`, + ); + const emailResult = await resend.emails.send({ + from: "Manage Team", + to: contactEmail, + subject: "Organization Deletion Notice - 60 Days", + react: OrgDeletionNotice({ + firstName: firstName, + email: contactEmail, + organizationName: org.name, + }), + text: DeletionNoticePlainText({ + firstName: firstName, + email: contactEmail, + organizationName: org.name, + }), + }); + console.log( + `[OrgDeletion] Email send result for ${org.name}:`, + JSON.stringify(emailResult, null, 2), + ); - // Mark organization for deletion - await db - .update(opsOrganization) - .set({ markedForDeletionAt: new Date() }) - .where(eq(opsOrganization.id, org.id)); + // Mark organization for deletion + await db + .update(opsOrganization) + .set({ markedForDeletionAt: new Date() }) + .where(eq(opsOrganization.id, org.id)); - console.log( - `[OrgDeletion] Successfully marked organization ${org.name} (${org.id}) for deletion`, - ); - } catch (error) { - console.error( - `[OrgDeletion] Failed to process organization ${org.name} (${org.id}):`, - error, - ); - } - } - }); + console.log( + `[OrgDeletion] Successfully marked organization ${org.name} (${org.id}) for deletion`, + ); + } catch (error) { + console.error( + `[OrgDeletion] Failed to process organization ${org.name} (${org.id}):`, + error, + ); + } + } + }); - // Step 3: Send 7-day warning to organizations marked 53 days ago - const orgsFor7DayWarning = await context.run( - "fetch-orgs-for-7-day-warning", - async () => { - const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53); - const orgs = await db - .select() - .from(opsOrganization) - .where( - and( - lte(opsOrganization.markedForDeletionAt, fiftyThreeDaysAgo), - isNull(opsOrganization.finalWarningAt), - ), - ); + // Step 3: Send 7-day warning to organizations marked 53 days ago + const orgsFor7DayWarning = await context.run( + "fetch-orgs-for-7-day-warning", + async () => { + const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53); + const orgs = await db + .select() + .from(opsOrganization) + .where( + and( + lte(opsOrganization.markedForDeletionAt, fiftyThreeDaysAgo), + isNull(opsOrganization.finalWarningAt), + ), + ); - console.log( - `[OrgDeletion] Found ${orgs.length} organizations for 7-day warning`, - ); - if (orgs.length > 0) { - console.log( - "[OrgDeletion] Organizations for 7-day warning:", - orgs.map((o) => ({ - id: o.id, - name: o.name, - markedForDeletionAt: o.markedForDeletionAt?.toISOString(), - })), - ); - } else { - console.log( - "[OrgDeletion] No organizations found that need 7-day warnings", - ); - } - return orgs; - }, - ); + console.log( + `[OrgDeletion] Found ${orgs.length} organizations for 7-day warning`, + ); + if (orgs.length > 0) { + console.log( + "[OrgDeletion] Organizations for 7-day warning:", + orgs.map((o) => ({ + id: o.id, + name: o.name, + markedForDeletionAt: o.markedForDeletionAt?.toISOString(), + })), + ); + } else { + console.log( + "[OrgDeletion] No organizations found that need 7-day warnings", + ); + } + return orgs; + }, + ); - await context.run("send-7-day-warning", async () => { - if (orgsFor7DayWarning.length === 0) { - console.log( - "[OrgDeletion] Skipping step 3: No organizations need 7-day warnings", - ); - return; - } + await context.run("send-7-day-warning", async () => { + if (orgsFor7DayWarning.length === 0) { + console.log( + "[OrgDeletion] Skipping step 3: No organizations need 7-day warnings", + ); + return; + } - console.log( - `[OrgDeletion] Processing ${orgsFor7DayWarning.length} organizations for 7-day warnings`, - ); - for (const org of orgsFor7DayWarning) { - try { - // Get creator details - const { contactEmail, firstName } = await getOrgCreatorDetails(org); + console.log( + `[OrgDeletion] Processing ${orgsFor7DayWarning.length} organizations for 7-day warnings`, + ); + for (const org of orgsFor7DayWarning) { + try { + // Get creator details + const { contactEmail, firstName } = await getOrgCreatorDetails(org); - console.log( - `[OrgDeletion] Sending 7-day warning to org ${org.name} (${org.id}), contact email: ${contactEmail}`, - ); + console.log( + `[OrgDeletion] Sending 7-day warning to org ${org.name} (${org.id}), contact email: ${contactEmail}`, + ); - // Send 7-day warning - console.log( - `[OrgDeletion] Sending 7-day warning email to ${contactEmail} for org ${org.name}`, - ); - const emailResult = await resend.emails.send({ - from: "Manage Team ", - to: contactEmail, - subject: "Final Warning - Organization Deletion in 7 Days", - react: SevenDayWarning({ - firstName: firstName, - email: contactEmail, - organizationName: org.name, - }), - text: sevenDayWarningPlainText({ - firstName: firstName, - email: contactEmail, - organizationName: org.name, - }), - }); - console.log( - `[OrgDeletion] 7-day warning email result for ${org.name}:`, - JSON.stringify(emailResult, null, 2), - ); + // Send 7-day warning + console.log( + `[OrgDeletion] Sending 7-day warning email to ${contactEmail} for org ${org.name}`, + ); + const emailResult = await resend.emails.send({ + from: "Manage Team ", + to: contactEmail, + subject: "Final Warning - Organization Deletion in 7 Days", + react: SevenDayWarning({ + firstName: firstName, + email: contactEmail, + organizationName: org.name, + }), + text: sevenDayWarningPlainText({ + firstName: firstName, + email: contactEmail, + organizationName: org.name, + }), + }); + console.log( + `[OrgDeletion] 7-day warning email result for ${org.name}:`, + JSON.stringify(emailResult, null, 2), + ); - // Mark final warning sent - await db - .update(opsOrganization) - .set({ finalWarningAt: new Date() }) - .where(eq(opsOrganization.id, org.id)); + // Mark final warning sent + await db + .update(opsOrganization) + .set({ finalWarningAt: new Date() }) + .where(eq(opsOrganization.id, org.id)); - console.log( - `[OrgDeletion] Successfully sent 7-day warning for organization ${org.name} (${org.id})`, - ); - } catch (error) { - console.error( - `[OrgDeletion] Failed to send 7-day warning for organization ${org.name} (${org.id}):`, - error, - ); - } - } - }); + console.log( + `[OrgDeletion] Successfully sent 7-day warning for organization ${org.name} (${org.id})`, + ); + } catch (error) { + console.error( + `[OrgDeletion] Failed to send 7-day warning for organization ${org.name} (${org.id}):`, + error, + ); + } + } + }); - // Step 4: Trigger deletion for organizations marked 60 days ago - const orgsToTriggerDeletion = await context.run( - "fetch-orgs-to-trigger-deletion", - async () => { - const sixtyDaysAgoForDeletion = new Date( - Date.now() - 1000 * 60 * 60 * 24 * 60, - ); - const orgs = await db - .select() - .from(opsOrganization) - .where( - lte(opsOrganization.markedForDeletionAt, sixtyDaysAgoForDeletion), - ); + // Step 4: Trigger deletion for organizations marked 60 days ago + const orgsToTriggerDeletion = await context.run( + "fetch-orgs-to-trigger-deletion", + async () => { + const sixtyDaysAgoForDeletion = new Date( + Date.now() - 1000 * 60 * 60 * 24 * 60, + ); + const orgs = await db + .select() + .from(opsOrganization) + .where( + lte(opsOrganization.markedForDeletionAt, sixtyDaysAgoForDeletion), + ); - console.log( - `[OrgDeletion] Found ${orgs.length} organizations ready for deletion`, - ); - if (orgs.length > 0) { - console.log( - "[OrgDeletion] Organizations ready for deletion:", - orgs.map((o) => ({ - id: o.id, - name: o.name, - markedForDeletionAt: o.markedForDeletionAt?.toISOString(), - finalWarningAt: o.finalWarningAt?.toISOString(), - })), - ); - } else { - console.log( - "[OrgDeletion] No organizations found that are ready for deletion", - ); - } - return orgs; - }, - ); + console.log( + `[OrgDeletion] Found ${orgs.length} organizations ready for deletion`, + ); + if (orgs.length > 0) { + console.log( + "[OrgDeletion] Organizations ready for deletion:", + orgs.map((o) => ({ + id: o.id, + name: o.name, + markedForDeletionAt: o.markedForDeletionAt?.toISOString(), + finalWarningAt: o.finalWarningAt?.toISOString(), + })), + ); + } else { + console.log( + "[OrgDeletion] No organizations found that are ready for deletion", + ); + } + return orgs; + }, + ); - await context.run("trigger-organization-deletions", async () => { - if (orgsToTriggerDeletion.length === 0) { - console.log( - "[OrgDeletion] Skipping step 4: No organizations ready for deletion", - ); - return; - } + await context.run("trigger-organization-deletions", async () => { + if (orgsToTriggerDeletion.length === 0) { + console.log( + "[OrgDeletion] Skipping step 4: No organizations ready for deletion", + ); + return; + } - console.log( - `[OrgDeletion] Triggering deletion for ${orgsToTriggerDeletion.length} organizations`, - ); - for (const org of orgsToTriggerDeletion) { - console.log( - `[OrgDeletion] Triggering deletion for org ${org.name} (${org.id}) via Clerk API`, - ); + console.log( + `[OrgDeletion] Triggering deletion for ${orgsToTriggerDeletion.length} organizations`, + ); + for (const org of orgsToTriggerDeletion) { + console.log( + `[OrgDeletion] Triggering deletion for org ${org.name} (${org.id}) via Clerk API`, + ); - // Delete organization from Clerk, which will trigger the webhook - // The webhook will handle database deletion and ops cleanup - const clerk = await clerkClient(); - await clerk.organizations.deleteOrganization(org.id); + // Delete organization from Clerk, which will trigger the webhook + // The webhook will handle database deletion and ops cleanup + const clerk = await clerkClient(); + await clerk.organizations.deleteOrganization(org.id); - console.log( - `[OrgDeletion] Successfully triggered deletion for organization ${org.name} (${org.id}). Webhook will handle cleanup.`, - ); - } - }); + console.log( + `[OrgDeletion] Successfully triggered deletion for organization ${org.name} (${org.id}). Webhook will handle cleanup.`, + ); + } + }); - console.log( - "[OrgDeletion] Organization deletion job completed at", - new Date().toISOString(), - ); + console.log( + "[OrgDeletion] Organization deletion job completed at", + new Date().toISOString(), + ); }); diff --git a/app/(api)/api/jobs/daily-summary/route.ts b/app/(api)/api/jobs/daily-summary/route.ts index 9131321..b047e11 100644 --- a/app/(api)/api/jobs/daily-summary/route.ts +++ b/app/(api)/api/jobs/daily-summary/route.ts @@ -2,8 +2,8 @@ import { serve } from "@upstash/workflow/nextjs"; import { gte } from "drizzle-orm"; import { Resend } from "resend"; import { - DailySummary, - dailySummaryPlainText, + DailySummary, + dailySummaryPlainText, } from "@/components/emails/daily-summary"; import { getTodayDataForUser } from "@/lib/utils/todayData"; import { getDatabaseForOwner } from "@/lib/utils/useDatabase"; @@ -11,177 +11,177 @@ import { opsUser } from "@/ops/drizzle/schema"; import { getOpsDatabase } from "@/ops/useOps"; function isCurrentlySevenAM(timezone: string): boolean { - try { - const now = new Date(); - const userTime = new Intl.DateTimeFormat("en-US", { - timeZone: timezone, - hour: "numeric", - hour12: false, - }).format(now); - - const hour = Number.parseInt(userTime.split(" ")[0] || userTime, 10); - return hour === 7; - } catch (error) { - console.error( - `[DailySummary] Error checking time for timezone ${timezone}:`, - error, - ); - return false; - } + try { + const now = new Date(); + const userTime = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "numeric", + hour12: false, + }).format(now); + + const hour = Number.parseInt(userTime.split(" ")[0] || userTime, 10); + return hour === 7; + } catch (error) { + console.error( + `[DailySummary] Error checking time for timezone ${timezone}:`, + error, + ); + return false; + } } export const { POST } = serve(async (context) => { - const resend = new Resend(process.env.RESEND_API_KEY); - console.log( - `[DailySummary] Starting daily summary job at ${new Date().toISOString()}`, - ); - - // Step 1: Get active users from ops database (active in last 7 days) and filter by 7AM timezone - const users = await context.run("fetch-users", async () => { - const opsDb = await getOpsDatabase(); - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - - const allUsers = await opsDb - .select() - .from(opsUser) - .where(gte(opsUser.lastActiveAt, sevenDaysAgo)); - console.log( - `[DailySummary] Found ${allUsers.length} active users (last 7 days)`, - ); - - // Filter users where it's currently 7AM in their timezone - const usersAt7AM = allUsers.filter((user) => { - const userTimezone = user.timeZone || "UTC"; - return isCurrentlySevenAM(userTimezone); - }); - - console.log( - `[DailySummary] Found ${usersAt7AM.length} users where it's currently 7AM`, - ); - return usersAt7AM; - }); - - // Step 2: Process users - await context.run("process-users", async () => { - console.log(`[DailySummary] Processing ${users.length} users`); - - for (const userData of users) { - try { - const userTimezone = userData.timeZone || "UTC"; - await processUserSummary( - userData.id, - userData.email, - userData.firstName, - userTimezone, - resend, - ); - } catch (error) { - console.error( - `[DailySummary] Error processing user ${userData.email}:`, - error, - ); - } - } - }); - - console.log( - `[DailySummary] Daily summary job completed at ${new Date().toISOString()}`, - ); + const resend = new Resend(process.env.RESEND_API_KEY); + console.log( + `[DailySummary] Starting daily summary job at ${new Date().toISOString()}`, + ); + + // Step 1: Get active users from ops database (active in last 7 days) and filter by 7AM timezone + const users = await context.run("fetch-users", async () => { + const opsDb = await getOpsDatabase(); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const allUsers = await opsDb + .select() + .from(opsUser) + .where(gte(opsUser.lastActiveAt, sevenDaysAgo)); + console.log( + `[DailySummary] Found ${allUsers.length} active users (last 7 days)`, + ); + + // Filter users where it's currently 7AM in their timezone + const usersAt7AM = allUsers.filter((user) => { + const userTimezone = user.timeZone || "UTC"; + return isCurrentlySevenAM(userTimezone); + }); + + console.log( + `[DailySummary] Found ${usersAt7AM.length} users where it's currently 7AM`, + ); + return usersAt7AM; + }); + + // Step 2: Process users + await context.run("process-users", async () => { + console.log(`[DailySummary] Processing ${users.length} users`); + + for (const userData of users) { + try { + const userTimezone = userData.timeZone || "UTC"; + await processUserSummary( + userData.id, + userData.email, + userData.firstName, + userTimezone, + resend, + ); + } catch (error) { + console.error( + `[DailySummary] Error processing user ${userData.email}:`, + error, + ); + } + } + }); + + console.log( + `[DailySummary] Daily summary job completed at ${new Date().toISOString()}`, + ); }); async function processUserSummary( - userId: string, - email: string, - firstName: string | undefined | null, - timezone: string, - resend: Resend, + userId: string, + email: string, + firstName: string | undefined | null, + timezone: string, + resend: Resend, ) { - try { - console.log( - `[DailySummary] Processing summary for user ${email} (${userId}) at timezone ${timezone}`, - ); - - // Get user's database - const db = await getDatabaseForOwner(userId); - - // Get today's data using the same logic as getTodayData - const today = new Date(); - const { dueToday, overDue, events } = await getTodayDataForUser( - db, - timezone, - today, - ); - - // Only send email if user has relevant content - const hasContent = - dueToday.length > 0 || overDue.length > 0 || events.length > 0; - - if (!hasContent) { - console.log( - `[DailySummary] No content for user ${email}, skipping email`, - ); - return; - } - - console.log( - `[DailySummary] Sending summary to ${email}: ${overDue.length} overdue, ${dueToday.length} due today, ${events.length} events`, - ); - - // Send daily summary email - await resend.emails.send({ - from: "Manage Daily Summary ", - to: email, - subject: `🌅 Your Daily Summary - ${getFormattedDate(today, timezone)} ✨`, - react: DailySummary({ - firstName: firstName || undefined, - email, - timezone, - date: today, - overdueTasks: overDue, - dueToday: dueToday, - events: events, - }), - text: dailySummaryPlainText({ - firstName: firstName || undefined, - email, - timezone, - date: today, - overdueTasks: overDue, - dueToday: dueToday, - events: events, - }), - }); - - console.log(`[DailySummary] Successfully sent summary email to ${email}`); - } catch (error) { - console.error( - `[DailySummary] Error processing summary for ${email}:`, - error, - ); - } + try { + console.log( + `[DailySummary] Processing summary for user ${email} (${userId}) at timezone ${timezone}`, + ); + + // Get user's database + const db = await getDatabaseForOwner(userId); + + // Get today's data using the same logic as getTodayData + const today = new Date(); + const { dueToday, overDue, events } = await getTodayDataForUser( + db, + timezone, + today, + ); + + // Only send email if user has relevant content + const hasContent = + dueToday.length > 0 || overDue.length > 0 || events.length > 0; + + if (!hasContent) { + console.log( + `[DailySummary] No content for user ${email}, skipping email`, + ); + return; + } + + console.log( + `[DailySummary] Sending summary to ${email}: ${overDue.length} overdue, ${dueToday.length} due today, ${events.length} events`, + ); + + // Send daily summary email + await resend.emails.send({ + from: "Manage Daily Summary", + to: email, + subject: `🌅 Your Daily Summary - ${getFormattedDate(today, timezone)} ✨`, + react: DailySummary({ + firstName: firstName || undefined, + email, + timezone, + date: today, + overdueTasks: overDue, + dueToday: dueToday, + events: events, + }), + text: dailySummaryPlainText({ + firstName: firstName || undefined, + email, + timezone, + date: today, + overdueTasks: overDue, + dueToday: dueToday, + events: events, + }), + }); + + console.log(`[DailySummary] Successfully sent summary email to ${email}`); + } catch (error) { + console.error( + `[DailySummary] Error processing summary for ${email}:`, + error, + ); + } } function getFormattedDate(date: Date, timezone: string): string { - try { - return new Intl.DateTimeFormat("en-US", { - timeZone: timezone, - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }).format(date); - } catch (error) { - console.error( - `[DailySummary] Error formatting date for timezone ${timezone}:`, - error, - ); - // Fallback to UTC if timezone is invalid - return new Intl.DateTimeFormat("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }).format(date); - } + try { + return new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }).format(date); + } catch (error) { + console.error( + `[DailySummary] Error formatting date for timezone ${timezone}:`, + error, + ); + // Fallback to UTC if timezone is invalid + return new Intl.DateTimeFormat("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }).format(date); + } } diff --git a/app/(api)/api/jobs/user/mark-for-deletion/route.ts b/app/(api)/api/jobs/user/mark-for-deletion/route.ts index 72d288b..ec9d80a 100644 --- a/app/(api)/api/jobs/user/mark-for-deletion/route.ts +++ b/app/(api)/api/jobs/user/mark-for-deletion/route.ts @@ -3,347 +3,345 @@ import { serve } from "@upstash/workflow/nextjs"; import { and, eq, isNull, lte } from "drizzle-orm"; import { Resend } from "resend"; import { - DeletionNoticePlainText, - OrgDeletionNotice, + DeletionNoticePlainText, + OrgDeletionNotice, } from "@/components/emails/org-deletion-notice"; import { - SevenDayWarning, - sevenDayWarningPlainText, + SevenDayWarning, + sevenDayWarningPlainText, } from "@/components/emails/seven-day-warning"; import { opsUser } from "@/ops/drizzle/schema"; import { getOpsDatabase } from "@/ops/useOps"; async function getUserDetails(user: { - id: string; - email: string; - firstName?: string | null; + id: string; + email: string; + firstName?: string | null; }) { - const contactEmail = user.email; - const firstName = user.firstName || undefined; + const contactEmail = user.email; + const firstName = user.firstName || undefined; - if (!contactEmail) { - throw new Error(`No email address found for user ${user.id}`); - } + if (!contactEmail) { + throw new Error(`No email address found for user ${user.id}`); + } - return { contactEmail, firstName }; + return { contactEmail, firstName }; } async function checkUserOrganizationMembership( - userId: string, + userId: string, ): Promise { - try { - const clerk = await clerkClient(); - // Get the user's organization memberships - const organizationMemberships = - await clerk.users.getOrganizationMembershipList({ - userId: userId, - }); - - console.log( - `[UserDeletion] User ${userId} has ${organizationMemberships.data.length} organization memberships`, - ); - - // Return true if user belongs to any organization - return organizationMemberships.data.length > 0; - } catch (error) { - console.error( - `[UserDeletion] Failed to fetch organization memberships for user ${userId}:`, - error, - ); - // If we can't determine organization membership, err on the side of caution - // and assume the user belongs to an organization (don't delete) - return true; - } + try { + const clerk = await clerkClient(); + // Get the user's organization memberships + const organizationMemberships = + await clerk.users.getOrganizationMembershipList({ userId: userId }); + + console.log( + `[UserDeletion] User ${userId} has ${organizationMemberships.data.length} organization memberships`, + ); + + // Return true if user belongs to any organization + return organizationMemberships.data.length > 0; + } catch (error) { + console.error( + `[UserDeletion] Failed to fetch organization memberships for user ${userId}:`, + error, + ); + // If we can't determine organization membership, err on the side of caution + // and assume the user belongs to an organization (don't delete) + return true; + } } export const { POST } = serve(async (context) => { - const resend = new Resend(process.env.RESEND_API_KEY); - console.log( - `[UserDeletion] Resend API Key configured: ${process.env.RESEND_API_KEY ? "Yes" : "No"}`, - ); - - console.log( - "[UserDeletion] Starting user deletion job at", - new Date().toISOString(), - ); - const db = await getOpsDatabase(); - const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60); - console.log( - "[UserDeletion] Looking for users inactive since:", - sixtyDaysAgo.toISOString(), - ); - - // Step 1: Mark users for deletion (60 days inactive, not part of any org) - const usersToMark = await context.run("fetch-users-to-mark", async () => { - const users = await db - .select() - .from(opsUser) - .where( - and( - lte(opsUser.lastActiveAt, sixtyDaysAgo), - isNull(opsUser.markedForDeletionAt), - ), - ); - - console.log( - `[UserDeletion] Found ${users.length} inactive users to check for organization membership`, - ); - - // Filter out users who belong to any organization - const eligibleUsers = []; - for (const user of users) { - const belongsToOrg = await checkUserOrganizationMembership(user.id); - if (!belongsToOrg) { - eligibleUsers.push(user); - } else { - console.log( - `[UserDeletion] Skipping user ${user.id} (${user.email}) - belongs to organization(s)`, - ); - } - } - - console.log( - `[UserDeletion] Found ${eligibleUsers.length} users eligible for deletion (not part of any organization)`, - ); - if (eligibleUsers.length > 0) { - console.log( - "[UserDeletion] Users to mark:", - eligibleUsers.map((u) => ({ - id: u.id, - email: u.email, - lastActiveAt: u.lastActiveAt?.toISOString(), - })), - ); - } else { - console.log( - "[UserDeletion] No eligible users found that need to be marked for deletion", - ); - } - return eligibleUsers; - }); - - // Step 2: Send 60-day deletion notice and mark users - await context.run("mark-users-for-deletion", async () => { - if (usersToMark.length === 0) { - console.log( - "[UserDeletion] Skipping step 2: No users to mark for deletion", - ); - return; - } - - console.log( - `[UserDeletion] Processing ${usersToMark.length} users for 60-day deletion notices`, - ); - for (const user of usersToMark) { - try { - // Get user details - const { contactEmail, firstName } = await getUserDetails(user); - - console.log( - `[UserDeletion] Processing user ${user.id}, contact email: ${contactEmail}`, - ); - - // Send 60-day deletion notice - console.log( - `[UserDeletion] Sending 60-day notice email to ${contactEmail} for user ${user.id}`, - ); - const emailResult = await resend.emails.send({ - from: "Manage Team ", - to: contactEmail, - subject: "Account Deletion Notice - 60 Days", - react: OrgDeletionNotice({ - firstName: firstName, - email: contactEmail, - // organizationName is undefined for user deletion - }), - text: DeletionNoticePlainText({ - firstName: firstName, - email: contactEmail, - // organizationName is undefined for user deletion - }), - }); - console.log( - `[UserDeletion] Email send result for user ${user.id}:`, - JSON.stringify(emailResult, null, 2), - ); - - // Mark user for deletion - await db - .update(opsUser) - .set({ markedForDeletionAt: new Date() }) - .where(eq(opsUser.id, user.id)); - - console.log( - `[UserDeletion] Successfully marked user ${user.id} (${contactEmail}) for deletion`, - ); - } catch (error) { - console.error( - `[UserDeletion] Failed to process user ${user.id}:`, - error, - ); - } - } - }); - - // Step 3: Send 7-day warning to users marked 53 days ago - const usersFor7DayWarning = await context.run( - "fetch-users-for-7-day-warning", - async () => { - const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53); - const users = await db - .select() - .from(opsUser) - .where( - and( - lte(opsUser.markedForDeletionAt, fiftyThreeDaysAgo), - isNull(opsUser.finalWarningAt), - ), - ); - - console.log( - `[UserDeletion] Found ${users.length} users for 7-day warning`, - ); - if (users.length > 0) { - console.log( - "[UserDeletion] Users for 7-day warning:", - users.map((u) => ({ - id: u.id, - email: u.email, - markedForDeletionAt: u.markedForDeletionAt?.toISOString(), - })), - ); - } else { - console.log("[UserDeletion] No users found that need 7-day warnings"); - } - return users; - }, - ); - - await context.run("send-7-day-warning", async () => { - if (usersFor7DayWarning.length === 0) { - console.log( - "[UserDeletion] Skipping step 3: No users need 7-day warnings", - ); - return; - } - - console.log( - `[UserDeletion] Processing ${usersFor7DayWarning.length} users for 7-day warnings`, - ); - for (const user of usersFor7DayWarning) { - try { - // Get user details - const { contactEmail, firstName } = await getUserDetails(user); - - console.log( - `[UserDeletion] Sending 7-day warning to user ${user.id}, contact email: ${contactEmail}`, - ); - - // Send 7-day warning - console.log( - `[UserDeletion] Sending 7-day warning email to ${contactEmail} for user ${user.id}`, - ); - const emailResult = await resend.emails.send({ - from: "Manage Team ", - to: contactEmail, - subject: "Final Warning - Account Deletion in 7 Days", - react: SevenDayWarning({ - firstName: firstName, - email: contactEmail, - // organizationName is undefined for user deletion - }), - text: sevenDayWarningPlainText({ - firstName: firstName, - email: contactEmail, - // organizationName is undefined for user deletion - }), - }); - console.log( - `[UserDeletion] 7-day warning email result for user ${user.id}:`, - JSON.stringify(emailResult, null, 2), - ); - - // Mark final warning sent - await db - .update(opsUser) - .set({ finalWarningAt: new Date() }) - .where(eq(opsUser.id, user.id)); - - console.log( - `[UserDeletion] Successfully sent 7-day warning for user ${user.id} (${contactEmail})`, - ); - } catch (error) { - console.error( - `[UserDeletion] Failed to send 7-day warning for user ${user.id}:`, - error, - ); - } - } - }); - - // Step 4: Trigger deletion for users marked 60 days ago - const usersToTriggerDeletion = await context.run( - "fetch-users-to-trigger-deletion", - async () => { - const sixtyDaysAgoForDeletion = new Date( - Date.now() - 1000 * 60 * 60 * 24 * 60, - ); - const users = await db - .select() - .from(opsUser) - .where(lte(opsUser.markedForDeletionAt, sixtyDaysAgoForDeletion)); - - console.log( - `[UserDeletion] Found ${users.length} users ready for deletion`, - ); - if (users.length > 0) { - console.log( - "[UserDeletion] Users ready for deletion:", - users.map((u) => ({ - id: u.id, - email: u.email, - markedForDeletionAt: u.markedForDeletionAt?.toISOString(), - finalWarningAt: u.finalWarningAt?.toISOString(), - })), - ); - } else { - console.log( - "[UserDeletion] No users found that are ready for deletion", - ); - } - return users; - }, - ); - - await context.run("trigger-user-deletions", async () => { - if (usersToTriggerDeletion.length === 0) { - console.log( - "[UserDeletion] Skipping step 4: No users ready for deletion", - ); - return; - } - - console.log( - `[UserDeletion] Triggering deletion for ${usersToTriggerDeletion.length} users`, - ); - for (const user of usersToTriggerDeletion) { - console.log( - `[UserDeletion] Triggering deletion for user ${user.id} (${user.email}) via Clerk API`, - ); - - // Delete user from Clerk, which will trigger the webhook - // The webhook will handle database deletion and ops cleanup - const clerk = await clerkClient(); - await clerk.users.deleteUser(user.id); - - console.log( - `[UserDeletion] Successfully triggered deletion for user ${user.id} (${user.email}). Webhook will handle cleanup.`, - ); - } - }); - - console.log( - "[UserDeletion] User deletion job completed at", - new Date().toISOString(), - ); + const resend = new Resend(process.env.RESEND_API_KEY); + console.log( + `[UserDeletion] Resend API Key configured: ${process.env.RESEND_API_KEY ? "Yes" : "No"}`, + ); + + console.log( + "[UserDeletion] Starting user deletion job at", + new Date().toISOString(), + ); + const db = await getOpsDatabase(); + const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60); + console.log( + "[UserDeletion] Looking for users inactive since:", + sixtyDaysAgo.toISOString(), + ); + + // Step 1: Mark users for deletion (60 days inactive, not part of any org) + const usersToMark = await context.run("fetch-users-to-mark", async () => { + const users = await db + .select() + .from(opsUser) + .where( + and( + lte(opsUser.lastActiveAt, sixtyDaysAgo), + isNull(opsUser.markedForDeletionAt), + ), + ); + + console.log( + `[UserDeletion] Found ${users.length} inactive users to check for organization membership`, + ); + + // Filter out users who belong to any organization + const eligibleUsers = []; + for (const user of users) { + const belongsToOrg = await checkUserOrganizationMembership(user.id); + if (!belongsToOrg) { + eligibleUsers.push(user); + } else { + console.log( + `[UserDeletion] Skipping user ${user.id} (${user.email}) - belongs to organization(s)`, + ); + } + } + + console.log( + `[UserDeletion] Found ${eligibleUsers.length} users eligible for deletion (not part of any organization)`, + ); + if (eligibleUsers.length > 0) { + console.log( + "[UserDeletion] Users to mark:", + eligibleUsers.map((u) => ({ + id: u.id, + email: u.email, + lastActiveAt: u.lastActiveAt?.toISOString(), + })), + ); + } else { + console.log( + "[UserDeletion] No eligible users found that need to be marked for deletion", + ); + } + return eligibleUsers; + }); + + // Step 2: Send 60-day deletion notice and mark users + await context.run("mark-users-for-deletion", async () => { + if (usersToMark.length === 0) { + console.log( + "[UserDeletion] Skipping step 2: No users to mark for deletion", + ); + return; + } + + console.log( + `[UserDeletion] Processing ${usersToMark.length} users for 60-day deletion notices`, + ); + for (const user of usersToMark) { + try { + // Get user details + const { contactEmail, firstName } = await getUserDetails(user); + + console.log( + `[UserDeletion] Processing user ${user.id}, contact email: ${contactEmail}`, + ); + + // Send 60-day deletion notice + console.log( + `[UserDeletion] Sending 60-day notice email to ${contactEmail} for user ${user.id}`, + ); + const emailResult = await resend.emails.send({ + from: "Manage Team", + to: contactEmail, + subject: "Account Deletion Notice - 60 Days", + react: OrgDeletionNotice({ + firstName: firstName, + email: contactEmail, + // organizationName is undefined for user deletion + }), + text: DeletionNoticePlainText({ + firstName: firstName, + email: contactEmail, + // organizationName is undefined for user deletion + }), + }); + console.log( + `[UserDeletion] Email send result for user ${user.id}:`, + JSON.stringify(emailResult, null, 2), + ); + + // Mark user for deletion + await db + .update(opsUser) + .set({ markedForDeletionAt: new Date() }) + .where(eq(opsUser.id, user.id)); + + console.log( + `[UserDeletion] Successfully marked user ${user.id} (${contactEmail}) for deletion`, + ); + } catch (error) { + console.error( + `[UserDeletion] Failed to process user ${user.id}:`, + error, + ); + } + } + }); + + // Step 3: Send 7-day warning to users marked 53 days ago + const usersFor7DayWarning = await context.run( + "fetch-users-for-7-day-warning", + async () => { + const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53); + const users = await db + .select() + .from(opsUser) + .where( + and( + lte(opsUser.markedForDeletionAt, fiftyThreeDaysAgo), + isNull(opsUser.finalWarningAt), + ), + ); + + console.log( + `[UserDeletion] Found ${users.length} users for 7-day warning`, + ); + if (users.length > 0) { + console.log( + "[UserDeletion] Users for 7-day warning:", + users.map((u) => ({ + id: u.id, + email: u.email, + markedForDeletionAt: u.markedForDeletionAt?.toISOString(), + })), + ); + } else { + console.log("[UserDeletion] No users found that need 7-day warnings"); + } + return users; + }, + ); + + await context.run("send-7-day-warning", async () => { + if (usersFor7DayWarning.length === 0) { + console.log( + "[UserDeletion] Skipping step 3: No users need 7-day warnings", + ); + return; + } + + console.log( + `[UserDeletion] Processing ${usersFor7DayWarning.length} users for 7-day warnings`, + ); + for (const user of usersFor7DayWarning) { + try { + // Get user details + const { contactEmail, firstName } = await getUserDetails(user); + + console.log( + `[UserDeletion] Sending 7-day warning to user ${user.id}, contact email: ${contactEmail}`, + ); + + // Send 7-day warning + console.log( + `[UserDeletion] Sending 7-day warning email to ${contactEmail} for user ${user.id}`, + ); + const emailResult = await resend.emails.send({ + from: "Manage Team", + to: contactEmail, + subject: "Final Warning - Account Deletion in 7 Days", + react: SevenDayWarning({ + firstName: firstName, + email: contactEmail, + // organizationName is undefined for user deletion + }), + text: sevenDayWarningPlainText({ + firstName: firstName, + email: contactEmail, + // organizationName is undefined for user deletion + }), + }); + console.log( + `[UserDeletion] 7-day warning email result for user ${user.id}:`, + JSON.stringify(emailResult, null, 2), + ); + + // Mark final warning sent + await db + .update(opsUser) + .set({ finalWarningAt: new Date() }) + .where(eq(opsUser.id, user.id)); + + console.log( + `[UserDeletion] Successfully sent 7-day warning for user ${user.id} (${contactEmail})`, + ); + } catch (error) { + console.error( + `[UserDeletion] Failed to send 7-day warning for user ${user.id}:`, + error, + ); + } + } + }); + + // Step 4: Trigger deletion for users marked 60 days ago + const usersToTriggerDeletion = await context.run( + "fetch-users-to-trigger-deletion", + async () => { + const sixtyDaysAgoForDeletion = new Date( + Date.now() - 1000 * 60 * 60 * 24 * 60, + ); + const users = await db + .select() + .from(opsUser) + .where(lte(opsUser.markedForDeletionAt, sixtyDaysAgoForDeletion)); + + console.log( + `[UserDeletion] Found ${users.length} users ready for deletion`, + ); + if (users.length > 0) { + console.log( + "[UserDeletion] Users ready for deletion:", + users.map((u) => ({ + id: u.id, + email: u.email, + markedForDeletionAt: u.markedForDeletionAt?.toISOString(), + finalWarningAt: u.finalWarningAt?.toISOString(), + })), + ); + } else { + console.log( + "[UserDeletion] No users found that are ready for deletion", + ); + } + return users; + }, + ); + + await context.run("trigger-user-deletions", async () => { + if (usersToTriggerDeletion.length === 0) { + console.log( + "[UserDeletion] Skipping step 4: No users ready for deletion", + ); + return; + } + + console.log( + `[UserDeletion] Triggering deletion for ${usersToTriggerDeletion.length} users`, + ); + for (const user of usersToTriggerDeletion) { + console.log( + `[UserDeletion] Triggering deletion for user ${user.id} (${user.email}) via Clerk API`, + ); + + // Delete user from Clerk, which will trigger the webhook + // The webhook will handle database deletion and ops cleanup + const clerk = await clerkClient(); + await clerk.users.deleteUser(user.id); + + console.log( + `[UserDeletion] Successfully triggered deletion for user ${user.id} (${user.email}). Webhook will handle cleanup.`, + ); + } + }); + + console.log( + "[UserDeletion] User deletion job completed at", + new Date().toISOString(), + ); }); diff --git a/app/(api)/api/webhook/auth/route.ts b/app/(api)/api/webhook/auth/route.ts index cdc7d9e..955d036 100644 --- a/app/(api)/api/webhook/auth/route.ts +++ b/app/(api)/api/webhook/auth/route.ts @@ -6,15 +6,15 @@ import { migrate } from "drizzle-orm/postgres-js/migrator"; import type { NextRequest } from "next/server"; import { Resend } from "resend"; import { - AccountDeleted, - accountDeletedPlainText, + AccountDeleted, + accountDeletedPlainText, } from "@/components/emails/account-deleted"; import * as schema from "@/drizzle/schema"; import { SearchService } from "@/lib/search"; import { - deleteDatabase, - getDatabaseForOwner, - getDatabaseName, + deleteDatabase, + getDatabaseForOwner, + getDatabaseName, } from "@/lib/utils/useDatabase"; import { addUserToTenantDb } from "@/lib/utils/useUser"; import { triggerBlobDeletionWorkflow } from "@/lib/utils/workflow"; @@ -22,417 +22,413 @@ import { opsOrganization, opsUser } from "@/ops/drizzle/schema"; import { addUserToOpsDb, getOpsDatabase } from "@/ops/useOps"; type ClerkOrgData = { - createdBy?: { - email?: string; - firstName?: string; - lastName?: string; - }; - adminEmails?: string[]; + createdBy?: { email?: string; firstName?: string; lastName?: string }; + adminEmails?: string[]; }; enum WebhookEventType { - organizationCreated = "organization.created", - organizationDeleted = "organization.deleted", - organizationUpdated = "organization.updated", - organizationInvitationAccepted = "organizationInvitation.accepted", - userCreated = "user.created", - userDeleted = "user.deleted", - userUpdated = "user.updated", + organizationCreated = "organization.created", + organizationDeleted = "organization.deleted", + organizationUpdated = "organization.updated", + organizationInvitationAccepted = "organizationInvitation.accepted", + userCreated = "user.created", + userDeleted = "user.deleted", + userUpdated = "user.updated", } async function createTenantDatabase(ownerId: string): Promise { - const databaseName = getDatabaseName(ownerId).match( - (value) => value, - () => { - throw new Error("Database name not found"); - }, - ); + const databaseName = getDatabaseName(ownerId).match( + (value) => value, + () => { + throw new Error("Database name not found"); + }, + ); - const ownerDb = drizzle({ - connection: { - url: `${process.env.DATABASE_URL}/manage`, - ssl: process.env.DATABASE_SSL === "true", - }, - schema, - }); + const ownerDb = drizzle({ + connection: { + url: `${process.env.DATABASE_URL}/manage`, + ssl: process.env.DATABASE_SSL === "true", + }, + schema, + }); - const checkDb = await ownerDb.execute( - sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`, - ); + const checkDb = await ownerDb.execute( + sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`, + ); - if (checkDb.count === 0) { - await ownerDb.execute(sql`CREATE DATABASE ${sql.identifier(databaseName)}`); - console.log(`Created database for tenant: ${databaseName}`); - } + if (checkDb.count === 0) { + await ownerDb.execute(sql`CREATE DATABASE ${sql.identifier(databaseName)}`); + console.log(`Created database for tenant: ${databaseName}`); + } - const tenantDb = drizzle({ - connection: { - url: `${process.env.DATABASE_URL}/${databaseName}`, - ssl: process.env.DATABASE_SSL === "true", - }, - schema, - }); + const tenantDb = drizzle({ + connection: { + url: `${process.env.DATABASE_URL}/${databaseName}`, + ssl: process.env.DATABASE_SSL === "true", + }, + schema, + }); - const migrationsFolder = path.resolve(process.cwd(), "drizzle"); - await migrate(tenantDb, { migrationsFolder }); - console.log(`Migrated database for tenant: ${databaseName}`); + const migrationsFolder = path.resolve(process.cwd(), "drizzle"); + await migrate(tenantDb, { migrationsFolder }); + console.log(`Migrated database for tenant: ${databaseName}`); } export async function POST(req: NextRequest) { - try { - const evt = await verifyWebhook(req); + try { + const evt = await verifyWebhook(req); - const { id } = evt.data; - const eventType = evt.type; - console.log("Webhook payload:", id, evt.data); + const { id } = evt.data; + const eventType = evt.type; + console.log("Webhook payload:", id, evt.data); - if (!id) { - console.error("Webhook received with no ID"); - return new Response("Webhook received with no ID", { status: 400 }); - } + if (!id) { + console.error("Webhook received with no ID"); + return new Response("Webhook received with no ID", { status: 400 }); + } - switch (eventType) { - case WebhookEventType.userCreated: - try { - const userData = evt.data; - await createTenantDatabase(id); - await Promise.all([ - addUserToTenantDb(userData), - addUserToOpsDb(userData), - ]); - console.log("User created - database and data synced successfully"); - } catch (err) { - console.error("Error creating user and database:", err); - } - break; - case WebhookEventType.userUpdated: - try { - const userData = evt.data; - await Promise.all([ - addUserToTenantDb(userData), - addUserToOpsDb(userData), - ]); - console.log("User updated - data synced successfully"); - } catch (err) { - console.error("Error syncing user data:", err); - } - break; - case WebhookEventType.organizationCreated: - try { - const orgData = evt.data; - await createTenantDatabase(id); - const db = await getOpsDatabase(); - await db - .insert(opsOrganization) - .values({ - id: orgData.id, - name: orgData.name, - rawData: orgData, - lastActiveAt: new Date(), - }) - .execute(); + switch (eventType) { + case WebhookEventType.userCreated: + try { + const userData = evt.data; + await createTenantDatabase(id); + await Promise.all([ + addUserToTenantDb(userData), + addUserToOpsDb(userData), + ]); + console.log("User created - database and data synced successfully"); + } catch (err) { + console.error("Error creating user and database:", err); + } + break; + case WebhookEventType.userUpdated: + try { + const userData = evt.data; + await Promise.all([ + addUserToTenantDb(userData), + addUserToOpsDb(userData), + ]); + console.log("User updated - data synced successfully"); + } catch (err) { + console.error("Error syncing user data:", err); + } + break; + case WebhookEventType.organizationCreated: + try { + const orgData = evt.data; + await createTenantDatabase(id); + const db = await getOpsDatabase(); + await db + .insert(opsOrganization) + .values({ + id: orgData.id, + name: orgData.name, + rawData: orgData, + lastActiveAt: new Date(), + }) + .execute(); - if (orgData.created_by) { - try { - const creatorData = await db - .select() - .from(opsUser) - .where(eq(opsUser.id, orgData.created_by)) - .limit(1); + if (orgData.created_by) { + try { + const creatorData = await db + .select() + .from(opsUser) + .where(eq(opsUser.id, orgData.created_by)) + .limit(1); - if (creatorData.length > 0) { - const creator = creatorData[0]; - const orgDb = await getDatabaseForOwner(id); - await orgDb - .insert(schema.user) - .values({ - id: creator.id, - email: creator.email, - firstName: creator.firstName, - lastName: creator.lastName, - imageUrl: creator.imageUrl, - rawData: creator.rawData, - lastActiveAt: new Date(), - }) - .onConflictDoUpdate({ - target: schema.user.id, - set: { - email: creator.email, - firstName: creator.firstName, - lastName: creator.lastName, - imageUrl: creator.imageUrl, - rawData: creator.rawData, - lastActiveAt: new Date(), - }, - }) - .execute(); - console.log( - `Added creator ${creator.id} to organization database`, - ); - } - } catch (creatorErr) { - console.error( - "Error adding creator to org database:", - creatorErr, - ); - } - } + if (creatorData.length > 0) { + const creator = creatorData[0]; + const orgDb = await getDatabaseForOwner(id); + await orgDb + .insert(schema.user) + .values({ + id: creator.id, + email: creator.email, + firstName: creator.firstName, + lastName: creator.lastName, + imageUrl: creator.imageUrl, + rawData: creator.rawData, + lastActiveAt: new Date(), + }) + .onConflictDoUpdate({ + target: schema.user.id, + set: { + email: creator.email, + firstName: creator.firstName, + lastName: creator.lastName, + imageUrl: creator.imageUrl, + rawData: creator.rawData, + lastActiveAt: new Date(), + }, + }) + .execute(); + console.log( + `Added creator ${creator.id} to organization database`, + ); + } + } catch (creatorErr) { + console.error( + "Error adding creator to org database:", + creatorErr, + ); + } + } - console.log( - "Organization created - database and data synced successfully", - ); - } catch (err) { - console.error("Error creating organization and database:", err); - } - break; - case WebhookEventType.organizationUpdated: - try { - const orgData = evt.data; - const db = await getOpsDatabase(); - await db - .insert(opsOrganization) - .values({ - id: orgData.id, - name: orgData.name, - rawData: orgData, - lastActiveAt: new Date(), - }) - .onConflictDoUpdate({ - target: opsOrganization.id, - set: { - name: orgData.name, - rawData: orgData, - lastActiveAt: new Date(), - markedForDeletionAt: null, - finalWarningAt: null, - }, - }) - .execute(); - console.log("Organization updated - data synced successfully"); - } catch (err) { - console.error("Error syncing org data:", err); - } - break; - case WebhookEventType.organizationInvitationAccepted: - try { - const invitationData = evt.data; - const orgId = invitationData.organization_id; - const emailAddress = invitationData.email_address; + console.log( + "Organization created - database and data synced successfully", + ); + } catch (err) { + console.error("Error creating organization and database:", err); + } + break; + case WebhookEventType.organizationUpdated: + try { + const orgData = evt.data; + const db = await getOpsDatabase(); + await db + .insert(opsOrganization) + .values({ + id: orgData.id, + name: orgData.name, + rawData: orgData, + lastActiveAt: new Date(), + }) + .onConflictDoUpdate({ + target: opsOrganization.id, + set: { + name: orgData.name, + rawData: orgData, + lastActiveAt: new Date(), + markedForDeletionAt: null, + finalWarningAt: null, + }, + }) + .execute(); + console.log("Organization updated - data synced successfully"); + } catch (err) { + console.error("Error syncing org data:", err); + } + break; + case WebhookEventType.organizationInvitationAccepted: + try { + const invitationData = evt.data; + const orgId = invitationData.organization_id; + const emailAddress = invitationData.email_address; - if (!orgId || !emailAddress) { - console.error("Missing organization or email in invitation data"); - break; - } + if (!orgId || !emailAddress) { + console.error("Missing organization or email in invitation data"); + break; + } - const db = await getOpsDatabase(); - const userData = await db - .select() - .from(opsUser) - .where(eq(opsUser.email, emailAddress)) - .limit(1); + const db = await getOpsDatabase(); + const userData = await db + .select() + .from(opsUser) + .where(eq(opsUser.email, emailAddress)) + .limit(1); - if (userData.length > 0) { - const user = userData[0]; - const orgDb = await getDatabaseForOwner(orgId); - await orgDb - .insert(schema.user) - .values({ - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl, - rawData: user.rawData, - lastActiveAt: new Date(), - }) - .onConflictDoUpdate({ - target: schema.user.id, - set: { - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - imageUrl: user.imageUrl, - rawData: user.rawData, - lastActiveAt: new Date(), - }, - }) - .execute(); - console.log( - `Added user ${user.id} to organization ${orgId} database after invitation acceptance`, - ); - } - } catch (err) { - console.error("Error adding user to org after invitation:", err); - } - break; - case WebhookEventType.userDeleted: - // For individual users, delete database immediately - // This happens when a user without an organization deletes their account - try { - await deleteDatabase(id); - console.log("User database deleted successfully"); - } catch (err) { - console.error("Error deleting user database:", err); - } + if (userData.length > 0) { + const user = userData[0]; + const orgDb = await getDatabaseForOwner(orgId); + await orgDb + .insert(schema.user) + .values({ + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl, + rawData: user.rawData, + lastActiveAt: new Date(), + }) + .onConflictDoUpdate({ + target: schema.user.id, + set: { + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + imageUrl: user.imageUrl, + rawData: user.rawData, + lastActiveAt: new Date(), + }, + }) + .execute(); + console.log( + `Added user ${user.id} to organization ${orgId} database after invitation acceptance`, + ); + } + } catch (err) { + console.error("Error adding user to org after invitation:", err); + } + break; + case WebhookEventType.userDeleted: + // For individual users, delete database immediately + // This happens when a user without an organization deletes their account + try { + await deleteDatabase(id); + console.log("User database deleted successfully"); + } catch (err) { + console.error("Error deleting user database:", err); + } - // Delete search index for user - try { - const userSearch = new SearchService(id, "me"); - await userSearch.deleteTenantIndex(); - console.log("User search index deleted successfully"); - } catch (err) { - console.error("Error deleting user search index:", err); - } + // Delete search index for user + try { + const userSearch = new SearchService(id, "me"); + await userSearch.deleteTenantIndex(); + console.log("User search index deleted successfully"); + } catch (err) { + console.error("Error deleting user search index:", err); + } - // Also delete user from ops database - try { - const db = await getOpsDatabase(); - await db.delete(opsUser).where(eq(opsUser.id, id)); - console.log("User deleted from ops database successfully"); - } catch (err) { - console.error("Error deleting user from ops database:", err); - } + // Also delete user from ops database + try { + const db = await getOpsDatabase(); + await db.delete(opsUser).where(eq(opsUser.id, id)); + console.log("User deleted from ops database successfully"); + } catch (err) { + console.error("Error deleting user from ops database:", err); + } - // Trigger blob deletion workflow - try { - await triggerBlobDeletionWorkflow(id); - console.log("User blob deletion workflow triggered successfully"); - } catch (err) { - console.error("Error triggering user blob deletion workflow:", err); - } - break; - case WebhookEventType.organizationDeleted: { - console.log(`[Webhook] Processing organization deletion for ID: ${id}`); + // Trigger blob deletion workflow + try { + await triggerBlobDeletionWorkflow(id); + console.log("User blob deletion workflow triggered successfully"); + } catch (err) { + console.error("Error triggering user blob deletion workflow:", err); + } + break; + case WebhookEventType.organizationDeleted: { + console.log(`[Webhook] Processing organization deletion for ID: ${id}`); - // First, get the organization info from ops database for email - let orgData = null; - try { - const db = await getOpsDatabase(); - const orgs = await db - .select() - .from(opsOrganization) - .where(eq(opsOrganization.id, id)); - orgData = orgs[0]; - if (orgData) { - console.log( - `[Webhook] Found organization data for ${orgData.name} (${id})`, - ); - } - } catch (err) { - console.error( - `[Webhook] Error fetching organization data for ID: ${id}:`, - err, - ); - } + // First, get the organization info from ops database for email + let orgData = null; + try { + const db = await getOpsDatabase(); + const orgs = await db + .select() + .from(opsOrganization) + .where(eq(opsOrganization.id, id)); + orgData = orgs[0]; + if (orgData) { + console.log( + `[Webhook] Found organization data for ${orgData.name} (${id})`, + ); + } + } catch (err) { + console.error( + `[Webhook] Error fetching organization data for ID: ${id}:`, + err, + ); + } - // Delete organization database immediately when Clerk deletes the organization - try { - await deleteDatabase(id); - console.log( - `[Webhook] Organization database deleted successfully for ID: ${id}`, - ); - } catch (err) { - console.error( - `[Webhook] Error deleting organization database for ID: ${id}:`, - err, - ); - } + // Delete organization database immediately when Clerk deletes the organization + try { + await deleteDatabase(id); + console.log( + `[Webhook] Organization database deleted successfully for ID: ${id}`, + ); + } catch (err) { + console.error( + `[Webhook] Error deleting organization database for ID: ${id}:`, + err, + ); + } - // Delete search index for organization - try { - const orgSearch = new SearchService(id, id); // slug is not relevant for deleting - await orgSearch.deleteTenantIndex(); - console.log( - `[Webhook] Organization search index deleted successfully for ID: ${id}`, - ); - } catch (err) { - console.error( - `[Webhook] Error deleting organization search index for ID: ${id}:`, - err, - ); - } + // Delete search index for organization + try { + const orgSearch = new SearchService(id, id); // slug is not relevant for deleting + await orgSearch.deleteTenantIndex(); + console.log( + `[Webhook] Organization search index deleted successfully for ID: ${id}`, + ); + } catch (err) { + console.error( + `[Webhook] Error deleting organization search index for ID: ${id}:`, + err, + ); + } - // Send deletion confirmation email if we have org data - if (orgData) { - try { - const rawData = orgData.rawData as ClerkOrgData; - const createdBy = rawData?.createdBy; - const contactEmail = createdBy?.email || rawData?.adminEmails?.[0]; + // Send deletion confirmation email if we have org data + if (orgData) { + try { + const rawData = orgData.rawData as ClerkOrgData; + const createdBy = rawData?.createdBy; + const contactEmail = createdBy?.email || rawData?.adminEmails?.[0]; - if (contactEmail) { - console.log( - `[Webhook] Sending deletion confirmation email to ${contactEmail} for org ${orgData.name}`, - ); + if (contactEmail) { + console.log( + `[Webhook] Sending deletion confirmation email to ${contactEmail} for org ${orgData.name}`, + ); - const resend = new Resend(process.env.RESEND_API_KEY); - await resend.emails.send({ - from: "Manage Team ", - to: contactEmail, - subject: "Organization Deleted", - react: AccountDeleted({ - firstName: createdBy?.firstName || undefined, - email: contactEmail, - organizationName: orgData.name, - }), - text: accountDeletedPlainText({ - firstName: createdBy?.firstName || undefined, - email: contactEmail, - organizationName: orgData.name, - }), - }); + const resend = new Resend(process.env.RESEND_API_KEY); + await resend.emails.send({ + from: "Manage Team", + to: contactEmail, + subject: "Organization Deleted", + react: AccountDeleted({ + firstName: createdBy?.firstName || undefined, + email: contactEmail, + organizationName: orgData.name, + }), + text: accountDeletedPlainText({ + firstName: createdBy?.firstName || undefined, + email: contactEmail, + organizationName: orgData.name, + }), + }); - console.log( - `[Webhook] Deletion confirmation email sent successfully for org ${orgData.name}`, - ); - } else { - console.log( - `[Webhook] No contact email found for org ${orgData.name}, skipping deletion confirmation email`, - ); - } - } catch (emailErr) { - console.error( - `[Webhook] Error sending deletion confirmation email for ID: ${id}:`, - emailErr, - ); - } - } + console.log( + `[Webhook] Deletion confirmation email sent successfully for org ${orgData.name}`, + ); + } else { + console.log( + `[Webhook] No contact email found for org ${orgData.name}, skipping deletion confirmation email`, + ); + } + } catch (emailErr) { + console.error( + `[Webhook] Error sending deletion confirmation email for ID: ${id}:`, + emailErr, + ); + } + } - // Delete organization from ops database - try { - const db = await getOpsDatabase(); - await db.delete(opsOrganization).where(eq(opsOrganization.id, id)); - console.log( - `[Webhook] Organization deleted from ops database successfully for ID: ${id}`, - ); - } catch (err) { - console.error( - `[Webhook] Error deleting organization from ops database for ID: ${id}:`, - err, - ); - } + // Delete organization from ops database + try { + const db = await getOpsDatabase(); + await db.delete(opsOrganization).where(eq(opsOrganization.id, id)); + console.log( + `[Webhook] Organization deleted from ops database successfully for ID: ${id}`, + ); + } catch (err) { + console.error( + `[Webhook] Error deleting organization from ops database for ID: ${id}:`, + err, + ); + } - // Trigger blob deletion workflow - try { - await triggerBlobDeletionWorkflow(id); - console.log( - `[Webhook] Organization blob deletion workflow triggered successfully for ID: ${id}`, - ); - } catch (err) { - console.error( - `[Webhook] Error triggering organization blob deletion workflow for ID: ${id}:`, - err, - ); - } - break; - } - default: - console.log("Unhandled webhook event type:", eventType); - break; - } + // Trigger blob deletion workflow + try { + await triggerBlobDeletionWorkflow(id); + console.log( + `[Webhook] Organization blob deletion workflow triggered successfully for ID: ${id}`, + ); + } catch (err) { + console.error( + `[Webhook] Error triggering organization blob deletion workflow for ID: ${id}:`, + err, + ); + } + break; + } + default: + console.log("Unhandled webhook event type:", eventType); + break; + } - return new Response("ok", { status: 200 }); - } catch (err) { - console.error("Error verifying webhook:", err); - return new Response("Error verifying webhook", { status: 400 }); - } + return new Response("ok", { status: 200 }); + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response("Error verifying webhook", { status: 400 }); + } } From a75b620464bafa0e2685098fccf0018bd1945c9f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 16 Nov 2025 14:50:58 +1100 Subject: [PATCH 2/3] Fix build --- bun.lock | 78 +++++------ components/core/notifications.tsx | 216 +++++++++++++++--------------- data/notification.ts | 9 +- lib/utils/error.ts | 38 +++--- lib/utils/turbowire.ts | 27 ++-- lib/utils/useNotification.ts | 50 +++---- package.json | 14 +- 7 files changed, 213 insertions(+), 219 deletions(-) diff --git a/bun.lock b/bun.lock index 3725915..f892714 100644 --- a/bun.lock +++ b/bun.lock @@ -31,14 +31,14 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.3", "@react-email/components": "^0.1.1", - "@sentry/nextjs": "^10.21.0", - "@sentry/node": "^10.21.0", + "@sentry/nextjs": "^10.25.0", + "@sentry/node": "^10.25.0", "@tanstack/react-query": "^5.71.10", "@trpc/client": "^11.0.2", "@trpc/server": "^11.0.2", "@trpc/tanstack-react-query": "^11.0.2", - "@turbowire/serverless": "^0.1.0", - "@turbowire/web": "^0.3.0", + "@turbowire/serverless": "^0.2.0", + "@turbowire/web": "^0.4.0", "@upstash/redis": "^1.35.3", "@upstash/search": "^0.1.5", "@upstash/workflow": "^0.2.16", @@ -82,12 +82,12 @@ "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.2", - "zod": "^3.24.2", - "zod-validation-error": "^3.4.0", + "zod": "^4.1.12", + "zod-validation-error": "^5.0.0", }, "devDependencies": { "@aws-sdk/s3-request-presigner": "^3.623.0", - "@biomejs/biome": "2.0.6", + "@biomejs/biome": "^2.3.5", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", "@types/mime-types": "^2.1.4", @@ -244,23 +244,23 @@ "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "@biomejs/biome": ["@biomejs/biome@2.0.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.6", "@biomejs/cli-darwin-x64": "2.0.6", "@biomejs/cli-linux-arm64": "2.0.6", "@biomejs/cli-linux-arm64-musl": "2.0.6", "@biomejs/cli-linux-x64": "2.0.6", "@biomejs/cli-linux-x64-musl": "2.0.6", "@biomejs/cli-win32-arm64": "2.0.6", "@biomejs/cli-win32-x64": "2.0.6" }, "bin": { "biome": "bin/biome" } }, "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.5", "@biomejs/cli-darwin-x64": "2.3.5", "@biomejs/cli-linux-arm64": "2.3.5", "@biomejs/cli-linux-arm64-musl": "2.3.5", "@biomejs/cli-linux-x64": "2.3.5", "@biomejs/cli-linux-x64-musl": "2.3.5", "@biomejs/cli-win32-arm64": "2.3.5", "@biomejs/cli-win32-x64": "2.3.5" }, "bin": { "biome": "bin/biome" } }, "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ=="], "@blocknote/core": ["@blocknote/core@0.27.2", "", { "dependencies": { "@emoji-mart/data": "^1.2.1", "@shikijs/types": "3.2.1", "@tiptap/core": "^2.11.5", "@tiptap/extension-bold": "^2.11.5", "@tiptap/extension-code": "^2.11.5", "@tiptap/extension-collaboration": "^2.11.5", "@tiptap/extension-collaboration-cursor": "^2.11.5", "@tiptap/extension-gapcursor": "^2.11.5", "@tiptap/extension-history": "^2.11.5", "@tiptap/extension-horizontal-rule": "^2.11.5", "@tiptap/extension-italic": "^2.11.5", "@tiptap/extension-link": "^2.11.5", "@tiptap/extension-paragraph": "^2.11.5", "@tiptap/extension-strike": "^2.11.5", "@tiptap/extension-table-cell": "^2.11.5", "@tiptap/extension-table-header": "^2.11.5", "@tiptap/extension-table-row": "^2.11.5", "@tiptap/extension-text": "^2.11.5", "@tiptap/extension-underline": "^2.11.5", "@tiptap/pm": "^2.11.5", "emoji-mart": "^5.6.0", "hast-util-from-dom": "^5.0.1", "prosemirror-dropcursor": "^1.8.1", "prosemirror-highlight": "^0.13.0", "prosemirror-model": "^1.24.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1", "rehype-format": "^5.0.1", "rehype-parse": "^9.0.1", "rehype-remark": "^10.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "uuid": "^8.3.2", "y-prosemirror": "1.3.0", "y-protocols": "^1.0.6", "yjs": "^13.6.15" }, "peerDependencies": { "@hocuspocus/provider": "^2.15.2" }, "optionalPeers": ["@hocuspocus/provider"] }, "sha512-v5W2SmBnEJaxGnPRG1t+qfc+OcRJVTfz2l1Xs4oacyZX8TwdpVpRxq2WVkZtvKfGzKYbIK0mpC0biBGSe6V6ig=="], @@ -754,17 +754,17 @@ "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.21.0", "", { "dependencies": { "@sentry/core": "10.21.0" } }, "sha512-QRHpCBheLd/88Z2m3ABMriV0MweW+pcGKuVsH61/UdziKcQLdoQpOSvGg0/0CuqFm2UjL7237ZzLdZrWaCOlfQ=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" } }, "sha512-wzg1ITZxrRtQouHPCgpt3tl1GiNAWFVy2RYK2KstFEhpYBAOUn9BAdP7KU9UyHBFKqbAvV4oGtAT8H2/Y4+leA=="], - "@sentry-internal/feedback": ["@sentry-internal/feedback@10.21.0", "", { "dependencies": { "@sentry/core": "10.21.0" } }, "sha512-6SnRR2FiW6TMwCE0PqbueHkkpeVnjOjz00R+/mX25Dp1U5BU5TzbXHzn9Y4wKnaD3Rzz4+nnzVkpHAOL3SppGw=="], + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" } }, "sha512-qlbT4tOd+WRyKpLdsbi26rkynGBoVabnY8/9rFnTxZ0WIUG5EFhJFqEeRLMyv+uk0uRFF3H0I9+u+qP/BKxIcQ=="], - "@sentry-internal/replay": ["@sentry-internal/replay@10.21.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.21.0", "@sentry/core": "10.21.0" } }, "sha512-5tfiKZJzZf9+Xk8SyvoC4ZEVLNmjBZZEaKhVyNo53CLWUWfWOqDc3DB9fj85i/yHFQ0ImdRnaPBc0CIeN00CcA=="], + "@sentry-internal/replay": ["@sentry-internal/replay@10.25.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-V/kKQn9T46HBTiP0bIThmpVr94K4vXwYM3/EHVpGSq4P9RynX06cgps8GLHq94+A0kX/DbK9igEMZmIuzS1q3A=="], - "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.21.0", "", { "dependencies": { "@sentry-internal/replay": "10.21.0", "@sentry/core": "10.21.0" } }, "sha512-TOLo5mAjJSOuJId8Po44d1hwJ5bIZDtRSoupWpYWqLw1tuUh1tc4vqID11ZXsw9pBzjVIK653BPDX/z/9+Um+Q=="], + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.25.0", "", { "dependencies": { "@sentry-internal/replay": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-zuj5jVNswZ/aA1nPPbU+VIFkQG0695lbyIfS1Skq+5o2FdRIS3MGnBXw1abI9h4pft8GLQWcKiBxISM7UpSz6w=="], "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.5.0", "", {}, "sha512-9sn9tJFtNnhSitPXW8hTuteefGMBbnPFyDER8dz+2sgdvcdq7T99lEwprMf8gUv5JCiDKIvtLe20Sf/4KPAahA=="], - "@sentry/browser": ["@sentry/browser@10.21.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.21.0", "@sentry-internal/feedback": "10.21.0", "@sentry-internal/replay": "10.21.0", "@sentry-internal/replay-canvas": "10.21.0", "@sentry/core": "10.21.0" } }, "sha512-z/63bUFBQkTfJ5ElhWTYvomz+gZ1GsoH16v4/RGoPY5qZgYxcVO3fkp0opnu3gcbXS0ZW7TLRiHpqhvipDdP6g=="], + "@sentry/browser": ["@sentry/browser@10.25.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.25.0", "@sentry-internal/feedback": "10.25.0", "@sentry-internal/replay": "10.25.0", "@sentry-internal/replay-canvas": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-UgSVT3RTM3vsK914TPuHVJQsjq5ooXVmjMtsWP3Ep+6f7N+1UVX4ZXsyyj5lDOcWdc79FgproD+MrEf9Cj6uBg=="], "@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@4.5.0", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "4.5.0", "@sentry/cli": "^2.51.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-LTgYe7qGgAP0BpsyCTpjk756l6wZUv3MtCE+G0qzlpsQ2AljYe2bN4qjDy0bQrsPo0QzNQm+S6d0zogcJj/tqw=="], @@ -786,19 +786,19 @@ "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ=="], - "@sentry/core": ["@sentry/core@10.21.0", "", {}, "sha512-/+gpOOb2Wr1UbW59WKqNAVVIqFz9FjtUJuPtVh4UanxGCfavMPaKpFzSlaEKJSKDkiCQgANP4O2y8Y5Bh3tvEA=="], + "@sentry/core": ["@sentry/core@10.25.0", "", {}, "sha512-mGi4BYIPwZjWdOXHrPoXz1AW4/cQbFoiuW/m+OOATmtSoGTDnWYwP+qZU7VLlL+v8ZEzxfPi2C1NPfJtPj7QWA=="], - "@sentry/nextjs": ["@sentry/nextjs@10.21.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.21.0", "@sentry/bundler-plugin-core": "^4.3.0", "@sentry/core": "10.21.0", "@sentry/node": "10.21.0", "@sentry/opentelemetry": "10.21.0", "@sentry/react": "10.21.0", "@sentry/vercel-edge": "10.21.0", "@sentry/webpack-plugin": "^4.3.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" } }, "sha512-Y2mCr7xobgc+Z8PAP46k07y9Dp2lW7orKms/VRjXRm9G+b67KDH88Crnk8Hdlo7R7WNwmRRvDnMzU2bphoeIug=="], + "@sentry/nextjs": ["@sentry/nextjs@10.25.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.25.0", "@sentry/bundler-plugin-core": "^4.3.0", "@sentry/core": "10.25.0", "@sentry/node": "10.25.0", "@sentry/opentelemetry": "10.25.0", "@sentry/react": "10.25.0", "@sentry/vercel-edge": "10.25.0", "@sentry/webpack-plugin": "^4.3.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" } }, "sha512-OQGjSNOS3UJqFyRlLA1mTEvHWo4ZYxCNDVsX9X4iPBYwjkFAfpmusvifvnBqAJ8um9bndTs2VnVDCWArjSY6vA=="], - "@sentry/node": ["@sentry/node@10.21.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.1.0", "@opentelemetry/core": "^2.1.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-amqplib": "0.51.0", "@opentelemetry/instrumentation-connect": "0.48.0", "@opentelemetry/instrumentation-dataloader": "0.22.0", "@opentelemetry/instrumentation-express": "0.53.0", "@opentelemetry/instrumentation-fs": "0.24.0", "@opentelemetry/instrumentation-generic-pool": "0.48.0", "@opentelemetry/instrumentation-graphql": "0.52.0", "@opentelemetry/instrumentation-hapi": "0.51.0", "@opentelemetry/instrumentation-http": "0.204.0", "@opentelemetry/instrumentation-ioredis": "0.52.0", "@opentelemetry/instrumentation-kafkajs": "0.14.0", "@opentelemetry/instrumentation-knex": "0.49.0", "@opentelemetry/instrumentation-koa": "0.52.0", "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", "@opentelemetry/instrumentation-mongodb": "0.57.0", "@opentelemetry/instrumentation-mongoose": "0.51.0", "@opentelemetry/instrumentation-mysql": "0.50.0", "@opentelemetry/instrumentation-mysql2": "0.51.0", "@opentelemetry/instrumentation-pg": "0.57.0", "@opentelemetry/instrumentation-redis": "0.53.0", "@opentelemetry/instrumentation-tedious": "0.23.0", "@opentelemetry/instrumentation-undici": "0.15.0", "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.15.0", "@sentry/core": "10.21.0", "@sentry/node-core": "10.21.0", "@sentry/opentelemetry": "10.21.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-z7g+rZIHOSzISGCYbpy8b6UxYd7kl0bjdTTjDC4rJCoofhO71By5tZum1HhcmYEWWDj7qc/Mbfmfv6rXoimT6A=="], + "@sentry/node": ["@sentry/node@10.25.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.1.0", "@opentelemetry/core": "^2.1.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-amqplib": "0.51.0", "@opentelemetry/instrumentation-connect": "0.48.0", "@opentelemetry/instrumentation-dataloader": "0.22.0", "@opentelemetry/instrumentation-express": "0.53.0", "@opentelemetry/instrumentation-fs": "0.24.0", "@opentelemetry/instrumentation-generic-pool": "0.48.0", "@opentelemetry/instrumentation-graphql": "0.52.0", "@opentelemetry/instrumentation-hapi": "0.51.0", "@opentelemetry/instrumentation-http": "0.204.0", "@opentelemetry/instrumentation-ioredis": "0.52.0", "@opentelemetry/instrumentation-kafkajs": "0.14.0", "@opentelemetry/instrumentation-knex": "0.49.0", "@opentelemetry/instrumentation-koa": "0.52.0", "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", "@opentelemetry/instrumentation-mongodb": "0.57.0", "@opentelemetry/instrumentation-mongoose": "0.51.0", "@opentelemetry/instrumentation-mysql": "0.50.0", "@opentelemetry/instrumentation-mysql2": "0.51.0", "@opentelemetry/instrumentation-pg": "0.57.0", "@opentelemetry/instrumentation-redis": "0.53.0", "@opentelemetry/instrumentation-tedious": "0.23.0", "@opentelemetry/instrumentation-undici": "0.15.0", "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.15.0", "@sentry/core": "10.25.0", "@sentry/node-core": "10.25.0", "@sentry/opentelemetry": "10.25.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-++mugiYF8X7CLtpymGN3N4J40SvQVIsVa6K7pURhooT4eX1QXYOBJSaOqvOXk5GN4qed5wETHNBkZuXSO0RARQ=="], - "@sentry/node-core": ["@sentry/node-core@10.21.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.21.0", "@sentry/opentelemetry": "10.21.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-vPn9sYMl2IB14lp6HP3nyJVM2VDDpclf7yvNWe/9yDY+ad1T/+8x5j501LjUaZDRR+7APM1Mb1S9YMAL3gTiwA=="], + "@sentry/node-core": ["@sentry/node-core@10.25.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.25.0", "@sentry/opentelemetry": "10.25.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-Hk0s7r9pkotZ1yfUc9+XX0ALDQ/bjaYsWF23O2q8Yfc4m8NcQio54ztAmdI+Yf+YiHLpt0x9Hlgwpl3AaRvwIA=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@10.21.0", "", { "dependencies": { "@sentry/core": "10.21.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-Yr4imXxkSLhJt2WHVXh31NpIe9ZgcnJTVVvzq/g6Ox40bj5+cdpFh6RTsLcsw5hvDC8a1KUvmdIhUTKAkEsqgA=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-AWCRzUIzvI+0RHXTmGvVx+MUtyyjwmC6F6d6XCnWhBKWGO52I+ucz1X8INIZxCrK05dpviFpeLZy+pzfgw892g=="], - "@sentry/react": ["@sentry/react@10.21.0", "", { "dependencies": { "@sentry/browser": "10.21.0", "@sentry/core": "10.21.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-BSCGKkepg9QPJRS8AUjtSAFd4lYJLmz3+P+oehViEHQDtRqqmXbVIBLhqwPc05KvRGIl4/kIDjyfDuHCFCJigQ=="], + "@sentry/react": ["@sentry/react@10.25.0", "", { "dependencies": { "@sentry/browser": "10.25.0", "@sentry/core": "10.25.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-LBQHgyPAFzuy99mEJF8ZF2AOxxJiGAmtu10eQhglhFgfJsU7JJVsee0h+vTSmvHMDtFrIwhZi3i1X5snZ/kzoA=="], - "@sentry/vercel-edge": ["@sentry/vercel-edge@10.21.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^2.1.0", "@sentry/core": "10.21.0" } }, "sha512-bQ77ObqX0i0UbznfwA5Ji+5pnECyc6xtrJmxrE8w/BZXCME4ZfTRbHGt9XRn7l5TMp0+gPnLih4PawJcMFJKeA=="], + "@sentry/vercel-edge": ["@sentry/vercel-edge@10.25.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^2.1.0", "@sentry/core": "10.25.0" } }, "sha512-TncKAvGo6YpFDKnf2k4l9Bdq8ZVdW+AlUClAloXs06sMLDZk9NDAmphT7lHdspx9kTsW/hTs29qEjk5H1AIZ8g=="], "@sentry/webpack-plugin": ["@sentry/webpack-plugin@4.5.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.5.0", "unplugin": "1.0.1", "uuid": "^9.0.0" }, "peerDependencies": { "webpack": ">=4.40.0" } }, "sha512-LtAYr54YFdOiklVpMWzYRwj17PQxE0KNffGa2qrMdH/Ays7iQ8j3z1t50wke4UoTrmeqz5kaCSZTJhZXv/XGwA=="], @@ -974,9 +974,9 @@ "@trpc/tanstack-react-query": ["@trpc/tanstack-react-query@11.0.2", "", { "peerDependencies": { "@tanstack/react-query": "^5.67.1", "@trpc/client": "11.0.2", "@trpc/server": "11.0.2", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-sufF2L6SvW0/9hQn1j3zblMpFD0HgzHegJRSA2W3Xkt6j7BXGUnOHPrJM7lGLMeO/oAfdhby9gfadAlX2H5WQg=="], - "@turbowire/serverless": ["@turbowire/serverless@0.1.0", "", {}, "sha512-TyVGo9kNtgJ6Ibin+CUED3oJAqIcSHorTLuPCQtlxGbDrVWHJdsSBInPxS55/W72jS2skiniatO+0avisPNB6A=="], + "@turbowire/serverless": ["@turbowire/serverless@0.2.0", "", { "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-fV6GXvngDRSBd3Uvo4vLFuTZSdpn8qExcRmpEp7vLx8eTUeW27vpvc2RSVyoKX9zTSPqdfF4hSIv8HOpmo0/5A=="], - "@turbowire/web": ["@turbowire/web@0.3.0", "", {}, "sha512-zLrG2Ahnqlk35Qc0Us2G3CaQC+FhA5PpCiM935np6tdrIQI01l19NRNfDYmJ9VFLocrtstCaXx6RE7n9aXt1CQ=="], + "@turbowire/web": ["@turbowire/web@0.4.0", "", { "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-8sCSyrsK/Csr6CKZ07dAhedYUveQOFA9/6+HD1vccBLBKPisvNvToCMFCp1/wc/3SmrNMCp9WAzeCFNwGxPT4g=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], @@ -1240,7 +1240,7 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -2538,11 +2538,11 @@ "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], - "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], + "zod-validation-error": ["zod-validation-error@5.0.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -2758,6 +2758,8 @@ "@upstash/qstash/neverthrow": ["neverthrow@7.2.0", "", {}, "sha512-iGBUfFB7yPczHHtA8dksKTJ9E8TESNTAx1UQWW6TzMF280vo9jdPYpLUXrMN1BCkPdHFdNG3fxOt2CUad8KhAw=="], + "@upstash/workflow/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -2830,6 +2832,10 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-react-hooks/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "eslint-plugin-react-hooks/zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], @@ -2842,8 +2848,6 @@ "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "jsondiffpatch/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "jsx-ast-utils/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -2870,8 +2874,6 @@ "object.groupby/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "ora/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -2884,8 +2886,6 @@ "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - "react-email/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "react-email/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "react-email/next": ["next@15.3.5", "", { "dependencies": { "@next/env": "15.3.5", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.5", "@next/swc-darwin-x64": "15.3.5", "@next/swc-linux-arm64-gnu": "15.3.5", "@next/swc-linux-arm64-musl": "15.3.5", "@next/swc-linux-x64-gnu": "15.3.5", "@next/swc-linux-x64-musl": "15.3.5", "@next/swc-win32-arm64-msvc": "15.3.5", "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw=="], diff --git a/components/core/notifications.tsx b/components/core/notifications.tsx index 96f8c54..4bf4f8b 100644 --- a/components/core/notifications.tsx +++ b/components/core/notifications.tsx @@ -5,6 +5,7 @@ import { TurboWire } from "@turbowire/web"; import { Bell, X } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { realtimeSchema } from "@/data/notification"; import { displayMutationError } from "@/lib/utils/error"; import { useTRPC } from "@/trpc/client"; import { Button } from "../ui/button"; @@ -13,120 +14,121 @@ import { NotificationItem } from "./notification-item"; import { Panel } from "./panel"; export function Notifications({ - notificationsWire, + notificationsWire, }: { - notificationsWire: string; + notificationsWire: string; }) { - const [open, setOpen] = useState(false); - const trpc = useTRPC(); - const { - data: notifications, - isLoading: notificationsLoading, - refetch: refetchNotifications, - } = useQuery(trpc.user.getUserNotifications.queryOptions()); - const { data: timezone, isLoading: timezoneLoading } = useQuery( - trpc.settings.getTimezone.queryOptions(), - ); + const [open, setOpen] = useState(false); + const trpc = useTRPC(); + const { + data: notifications, + isLoading: notificationsLoading, + refetch: refetchNotifications, + } = useQuery(trpc.user.getUserNotifications.queryOptions()); + const { data: timezone, isLoading: timezoneLoading } = useQuery( + trpc.settings.getTimezone.queryOptions(), + ); - const markNotificationsAsRead = useMutation( - trpc.user.markNotificationsAsRead.mutationOptions({ - onError: displayMutationError, - onSuccess: () => { - refetchNotifications(); - }, - }), - ); + const markNotificationsAsRead = useMutation( + trpc.user.markNotificationsAsRead.mutationOptions({ + onError: displayMutationError, + onSuccess: () => { + refetchNotifications(); + }, + }), + ); - const unreadCount = notifications?.filter((x) => !x.read).length; + const unreadCount = notifications?.filter((x) => !x.read).length; - useEffect(() => { - if (!notificationsWire) return; + useEffect(() => { + if (!notificationsWire) return; - const wire = new TurboWire(notificationsWire); - wire.connect((message) => { - try { - const data = JSON.parse(message); - refetchNotifications(); - if (data?.message) { - toast.info(data.message); - } - } catch (error) { - console.error(error); - } - }); + const wire = new TurboWire(notificationsWire, { schema: realtimeSchema }); - return () => { - wire?.disconnect(); - }; - }, [notificationsWire, refetchNotifications]); + wire.connect(); - return ( - <> - + wire.on("notification", ({ content }) => { + try { + if (!content) return; + refetchNotifications(); + toast.info(content); + } catch (error) { + console.error(error); + } + }); - - {notificationsLoading || timezoneLoading ? ( - - ) : ( -
-
-

Notifications

-
- {unreadCount ? ( - - ) : null} - -
-
-
- {notifications?.length && timezone ? ( -
- {notifications.map((notification) => ( - - ))} -
- ) : ( -
- You have no new notifications. -
- )} -
-
- )} -
- - ); + return () => { + wire?.disconnect(); + }; + }, [notificationsWire, refetchNotifications]); + + return ( + <> + + + + {notificationsLoading || timezoneLoading ? ( + + ) : ( +
+
+

Notifications

+
+ {unreadCount ? ( + + ) : null} + +
+
+
+ {notifications?.length && timezone ? ( +
+ {notifications.map((notification) => ( + + ))} +
+ ) : ( +
+ You have no new notifications. +
+ )} +
+
+ )} +
+ + ); } diff --git a/data/notification.ts b/data/notification.ts index dba1819..2564e6b 100644 --- a/data/notification.ts +++ b/data/notification.ts @@ -1,4 +1,5 @@ -export enum notificationType { - assign = "assign", - comment = "comment", -} +import z from "zod"; + +export const realtimeSchema = { + notification: z.object({ content: z.string().nullable() }), +}; diff --git a/lib/utils/error.ts b/lib/utils/error.ts index 8be7e97..1d02ac1 100644 --- a/lib/utils/error.ts +++ b/lib/utils/error.ts @@ -1,26 +1,26 @@ -import type { AppRouter } from "@/trpc/routers/_app"; import type { TRPCClientErrorLike } from "@trpc/client"; import { toast } from "sonner"; +import type { AppRouter } from "@/trpc/routers/_app"; export function displayMutationError(error: TRPCClientErrorLike) { - const fieldErrors = error.data?.zodError?.fieldErrors; - if (fieldErrors) { - for (const error in fieldErrors) { - toast.error(fieldErrors[error]); - return; - } - } + const fieldErrors = error.data?.zodError?.fieldErrors; + if (fieldErrors) { + for (const error in fieldErrors) { + toast.error(error); + return; + } + } - const formErrors = error.data?.zodError?.formErrors; - if (formErrors) { - for (const error of formErrors) { - toast.error(error); - return; - } - } + const formErrors = error.data?.zodError?.formErrors; + if (formErrors) { + for (const error of formErrors) { + toast.error(error); + return; + } + } - toast.error( - error.message || - "Oops! Something went wrong. Please try again or contact support.", - ); + toast.error( + error.message || + "Oops! Something went wrong. Please try again or contact support.", + ); } diff --git a/lib/utils/turbowire.ts b/lib/utils/turbowire.ts index 1cc3a76..71a72d4 100644 --- a/lib/utils/turbowire.ts +++ b/lib/utils/turbowire.ts @@ -1,19 +1,20 @@ -import { TurboWireHub } from "@turbowire/serverless"; +import { createTurboWireHub } from "@turbowire/serverless"; +import { realtimeSchema } from "@/data/notification"; -export type Event = "notifications"; - -export async function getSignedWireUrl(room: Event, userId: string) { - const turbowire = new TurboWireHub(process.env.TURBOWIRE_DOMAIN!); - return turbowire.getSignedWire(`${room}/${userId}`); +export async function getSignedWireUrl(room: string, userId: string) { + const turbowire = createTurboWireHub(process.env.TURBOWIRE_DOMAIN!, { + schema: realtimeSchema, + }); + return turbowire.getSignedWire(`${room}/${userId}`); } export async function broadcastEvent( - room: Event, - actor: string, - message: Record | null = null, + room: string, + userId: string, + content: string | null = null, ) { - const turbowire = new TurboWireHub(process.env.TURBOWIRE_DOMAIN!, { - broadcastUrl: process.env.TURBOWIRE_BROADCAST_URL, - }); - await turbowire.broadcast(`${room}/${actor}`, JSON.stringify(message)); + const turbowire = createTurboWireHub(process.env.TURBOWIRE_DOMAIN!, { + schema: realtimeSchema, + }); + await turbowire.broadcast(`${room}/${userId}`).notification({ content }); } diff --git a/lib/utils/useNotification.ts b/lib/utils/useNotification.ts index 2840a7a..035a9d7 100644 --- a/lib/utils/useNotification.ts +++ b/lib/utils/useNotification.ts @@ -3,37 +3,27 @@ import { broadcastEvent } from "./turbowire"; import { database } from "./useDatabase"; export async function notifyUser( - userId: string, - message: string | null = null, - options: { - type: "task" | "project" | "event" | "comment" | "mention" | null; - target: string | null; - fromUser: string | null; - } = { - type: null, - target: null, - fromUser: null, - }, + userId: string, + message: string | null = null, + options: { + type: "task" | "project" | "event" | "comment" | "mention" | null; + target: string | null; + fromUser: string | null; + } = { type: null, target: null, fromUser: null }, ) { - if (!userId) return; + if (!userId) return; - const db = await database(); + const db = await database(); - if (message) { - await Promise.all([ - db - .insert(notification) - .values({ - ...options, - toUser: userId, - message, - }) - .execute(), - broadcastEvent("notifications", userId, { - message: message, - }), - ]); - } else { - await broadcastEvent("notifications", userId); - } + if (message) { + await Promise.all([ + db + .insert(notification) + .values({ ...options, toUser: userId, message }) + .execute(), + broadcastEvent("notifications", userId, message), + ]); + } else { + await broadcastEvent("notifications", userId); + } } diff --git a/package.json b/package.json index 0fd2eea..6664074 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.3", "@react-email/components": "^0.1.1", - "@sentry/nextjs": "^10.21.0", - "@sentry/node": "^10.21.0", + "@sentry/nextjs": "^10.25.0", + "@sentry/node": "^10.25.0", "@tanstack/react-query": "^5.71.10", "@trpc/client": "^11.0.2", "@trpc/server": "^11.0.2", "@trpc/tanstack-react-query": "^11.0.2", - "@turbowire/serverless": "^0.1.0", - "@turbowire/web": "^0.3.0", + "@turbowire/serverless": "^0.2.0", + "@turbowire/web": "^0.4.0", "@upstash/redis": "^1.35.3", "@upstash/search": "^0.1.5", "@upstash/workflow": "^0.2.16", @@ -94,12 +94,12 @@ "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.2", - "zod": "^3.24.2", - "zod-validation-error": "^3.4.0" + "zod": "^4.1.12", + "zod-validation-error": "^5.0.0" }, "devDependencies": { "@aws-sdk/s3-request-presigner": "^3.623.0", - "@biomejs/biome": "2.0.6", + "@biomejs/biome": "^2.3.5", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", "@types/mime-types": "^2.1.4", From 5fe308fc39c7c36261820d235eedb328a706eff2 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 16 Nov 2025 14:52:35 +1100 Subject: [PATCH 3/3] Update next --- bun.lock | 40 ++++++++++++++++++++-------------------- package.json | 12 ++++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bun.lock b/bun.lock index f892714..bc5771a 100644 --- a/bun.lock +++ b/bun.lock @@ -55,12 +55,12 @@ "dotenv": "^16.4.7", "drizzle-orm": "^0.44.6", "es-toolkit": "^1.39.8", - "eslint-config-next": "16.0.0", + "eslint-config-next": "16.0.3", "ical-generator": "^8.0.1", "lucide-react": "^0.503.0", "mime-types": "^2.1.35", "neverthrow": "^8.2.0", - "next": "16.0.0", + "next": "16.0.3", "next-themes": "^0.3.0", "node-ical": "^0.20.1", "nuqs": "^2.4.1", @@ -92,8 +92,8 @@ "@tailwindcss/typography": "^0.5.13", "@types/mime-types": "^2.1.4", "@types/node": "20.1.0", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", + "@types/react": "19.2.5", + "@types/react-dom": "19.2.3", "drizzle-kit": "^0.31.5", "encoding": "^0.1.13", "typescript": "5.7.2", @@ -104,8 +104,8 @@ "@sentry/cli", ], "overrides": { - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", + "@types/react": "19.2.5", + "@types/react-dom": "19.2.3", }, "packages": { "@ai-sdk/openai": ["@ai-sdk/openai@1.3.22", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw=="], @@ -468,25 +468,25 @@ "@neondatabase/serverless": ["@neondatabase/serverless@1.0.2", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw=="], - "@next/env": ["@next/env@16.0.0", "", {}, "sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA=="], + "@next/env": ["@next/env@16.0.3", "", {}, "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.0", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-IB7RzmmtrPOrpAgEBR1PIQPD0yea5lggh5cq54m51jHjjljU80Ia+czfxJYMlSDl1DPvpzb8S9TalCc0VMo9Hw=="], + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.3", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/CntqDCnk5w2qIwMiF0a9r6+9qunZzFmU0cBX4T82LOflE72zzH6gnOjCwUXYKOBlQi8OpP/rMj8cBIr18x4TA=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hB4GZnJGKa8m4efvTGNyii6qs76vTNl+3dKHTCAUaksN6KjYy4iEO3Q5ira405NW2PKb3EcqWiRaL9DrYJfMHg=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-E2IHMdE+C1k+nUgndM13/BY/iJY9KGCphCftMh7SXWcaQqExq/pJU/1Hgn8n/tFwSoLoYC/yUghOv97tAsIxqg=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-O1cJbT+lZp+cTjYyZGiDwsOjO3UHHzSqobkPNipdlnnuPb1swfcuY6r3p8dsKU4hAIEO4cO67ZCfVVH/M1ETXA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1022,9 +1022,9 @@ "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="], - "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], @@ -1426,7 +1426,7 @@ "eslint": ["eslint@9.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.1.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.22.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ=="], - "eslint-config-next": ["eslint-config-next@16.0.0", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-DWKT1YAO9ex2rK0/EeiPpKU++ghTiG59z6m08/ReLRECOYIaEv17maSCYT8zmFQLwIrY5lhJ+iaJPQdT4sJd4g=="], + "eslint-config-next": ["eslint-config-next@16.0.3", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-5F6qDjcZldf0Y0ZbqvWvap9xzYUxyDf7/of37aeyhvkrQokj/4bT1JYWZdlWUr283aeVa+s52mPq9ogmGg+5dw=="], "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], @@ -1942,7 +1942,7 @@ "neverthrow": ["neverthrow@8.2.0", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.24.0" } }, "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ=="], - "next": ["next@16.0.0", "", { "dependencies": { "@next/env": "16.0.0", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.0", "@next/swc-darwin-x64": "16.0.0", "@next/swc-linux-arm64-gnu": "16.0.0", "@next/swc-linux-arm64-musl": "16.0.0", "@next/swc-linux-x64-gnu": "16.0.0", "@next/swc-linux-x64-musl": "16.0.0", "@next/swc-win32-arm64-msvc": "16.0.0", "@next/swc-win32-x64-msvc": "16.0.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg=="], + "next": ["next@16.0.3", "", { "dependencies": { "@next/env": "16.0.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.3", "@next/swc-darwin-x64": "16.0.3", "@next/swc-linux-arm64-gnu": "16.0.3", "@next/swc-linux-arm64-musl": "16.0.3", "@next/swc-linux-x64-gnu": "16.0.3", "@next/swc-linux-x64-musl": "16.0.3", "@next/swc-win32-arm64-msvc": "16.0.3", "@next/swc-win32-x64-msvc": "16.0.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w=="], "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], diff --git a/package.json b/package.json index 6664074..187b749 100644 --- a/package.json +++ b/package.json @@ -67,12 +67,12 @@ "dotenv": "^16.4.7", "drizzle-orm": "^0.44.6", "es-toolkit": "^1.39.8", - "eslint-config-next": "16.0.0", + "eslint-config-next": "16.0.3", "ical-generator": "^8.0.1", "lucide-react": "^0.503.0", "mime-types": "^2.1.35", "neverthrow": "^8.2.0", - "next": "16.0.0", + "next": "16.0.3", "next-themes": "^0.3.0", "node-ical": "^0.20.1", "nuqs": "^2.4.1", @@ -104,15 +104,15 @@ "@tailwindcss/typography": "^0.5.13", "@types/mime-types": "^2.1.4", "@types/node": "20.1.0", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", + "@types/react": "19.2.5", + "@types/react-dom": "19.2.3", "drizzle-kit": "^0.31.5", "encoding": "^0.1.13", "typescript": "5.7.2" }, "overrides": { - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2" + "@types/react": "19.2.5", + "@types/react-dom": "19.2.3" }, "trustedDependencies": [ "@sentry/cli"