From 9a12851471aeb5f36e7aebd9c1652d0c2a565966 Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:16:51 +0100 Subject: [PATCH 1/2] fix: null render in sidebar provider (#337) --- .../Chat/sidebar-actions.stories.tsx | 19 +++++++++++++++---- apps/nextjs/src/lib/hooks/use-sidebar.tsx | 6 +----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx index 395fef328..496c40aae 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { within } from "@storybook/test"; import { SidebarActions } from "./sidebar-actions"; @@ -26,14 +27,24 @@ export const Default: Story = { }, }; +export const SharePending: Story = { + args: { + ...Default.args, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const deleteButton = canvas.getByRole("button", { name: "Share" }); + deleteButton.click(); + }, +}; + export const RemovePending: Story = { args: { ...Default.args, }, play: async ({ canvasElement }) => { - const deleteButton = canvasElement.querySelector("button:nth-child(2)"); - if (deleteButton instanceof HTMLElement) { - deleteButton.click(); - } + const canvas = within(canvasElement); + const deleteButton = canvas.getByRole("button", { name: "Delete" }); + deleteButton.click(); }, }; diff --git a/apps/nextjs/src/lib/hooks/use-sidebar.tsx b/apps/nextjs/src/lib/hooks/use-sidebar.tsx index 595e8ea6b..d573b85dc 100644 --- a/apps/nextjs/src/lib/hooks/use-sidebar.tsx +++ b/apps/nextjs/src/lib/hooks/use-sidebar.tsx @@ -27,7 +27,7 @@ interface SidebarProviderProps { } export function SidebarProvider({ children }: SidebarProviderProps) { - const [isSidebarOpen, setSidebarOpen] = React.useState(true); + const [isSidebarOpen, setSidebarOpen] = React.useState(false); const [isLoading, setLoading] = React.useState(true); React.useEffect(() => { @@ -46,10 +46,6 @@ export function SidebarProvider({ children }: SidebarProviderProps) { }); }; - if (isLoading) { - return null; - } - return ( Date: Wed, 6 Nov 2024 13:52:01 +0000 Subject: [PATCH 2/2] chore: delete old google drive export files cron job- AI-561 (#257) --- .../src/app/api/aila-download-all/route.ts | 2 +- .../nextjs/src/app/api/aila-download/route.ts | 1 + .../api/cron-jobs/expired-exports/route.ts | 140 ++++++++++++++++++ apps/nextjs/vercel.json | 8 +- packages/api/src/router/exports.ts | 1 + packages/logger/index.ts | 3 +- turbo.json | 3 +- 7 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts diff --git a/apps/nextjs/src/app/api/aila-download-all/route.ts b/apps/nextjs/src/app/api/aila-download-all/route.ts index 1ed389af7..37d46e7f6 100644 --- a/apps/nextjs/src/app/api/aila-download-all/route.ts +++ b/apps/nextjs/src/app/api/aila-download-all/route.ts @@ -130,7 +130,7 @@ async function getHandler(req: Request): Promise { } const lessonExport = await prisma.lessonExport.findFirst({ - where: { gdriveFileId: fileId, userId }, + where: { gdriveFileId: fileId, userId, expiredAt: null }, }); if (!lessonExport) { diff --git a/apps/nextjs/src/app/api/aila-download/route.ts b/apps/nextjs/src/app/api/aila-download/route.ts index 52d4cf5b3..64c9010bc 100644 --- a/apps/nextjs/src/app/api/aila-download/route.ts +++ b/apps/nextjs/src/app/api/aila-download/route.ts @@ -117,6 +117,7 @@ async function getHandler(req: Request): Promise { where: { gdriveFileId: fileId, userId, + expiredAt: null, }, cacheStrategy: { ttl: 60 * 5, swr: 60 * 2 }, }); diff --git a/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts b/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts new file mode 100644 index 000000000..114d7d276 --- /dev/null +++ b/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts @@ -0,0 +1,140 @@ +import { prisma } from "@oakai/db"; +import { googleDrive } from "@oakai/exports/src/gSuite/drive/client"; +import { aiLogger } from "@oakai/logger"; +import * as Sentry from "@sentry/node"; +import type { NextRequest } from "next/server"; +import { isTruthy } from "remeda"; + +const log = aiLogger("cron"); + +const requiredEnvVars = ["CRON_SECRET", "GOOGLE_DRIVE_OUTPUT_FOLDER_ID"]; + +requiredEnvVars.forEach((envVar) => { + if (!process.env[envVar]) { + throw new Error(`Environment variable ${envVar} is not set.`); + } +}); + +async function updateExpiredAt(fileIds: string[]) { + if (fileIds.length === 0) { + log.info("No file IDs to update."); + return; + } + + try { + const result = await prisma.lessonExport.updateMany({ + where: { + gdriveFileId: { + in: fileIds, + }, + }, + data: { + expiredAt: new Date(), + }, + }); + log.info(`Updated expiredAt for ${fileIds.length} files.`); + + if (result.count === fileIds.length) { + log.info("All files updated successfully."); + } else { + throw new Error( + `Expected to update ${fileIds.length} files, but only updated ${result.count}.`, + ); + } + } catch (error) { + log.error("Error updating expiredAt field in the database:", error); + throw error; + } +} + +async function deleteExpiredExports(fileIds: string[]) { + try { + for (const id of fileIds) { + await googleDrive.files.delete({ fileId: id }); + log.info("Deleted:", id); + } + } catch (error) { + log.error("Error deleting old files from folder:", error); + throw error; + } +} + +interface FetchExpiredExportsOptions { + folderId: string; + daysAgo: number; +} + +async function fetchExpiredExports({ + folderId, + daysAgo, +}: FetchExpiredExportsOptions) { + try { + const currentDate = new Date(); + const targetDate = new Date( + currentDate.setDate(currentDate.getDate() - daysAgo), + ).toISOString(); + + const query = `modifiedTime < '${targetDate}' and '${folderId}' in parents`; + + const res = await googleDrive.files.list({ + q: query, + fields: "files(id, name, modifiedTime, ownedByMe )", + pageSize: 1000, + }); + + const files = + res.data.files?.filter((file) => file.ownedByMe === true) || []; + + if (files.length === 0) { + log.info( + "No files found that are older than one month in the specified folder.", + ); + return null; + } + + log.info(`Found ${files.length} files older than one month in folder:`); + return files; + } catch (error) { + log.error("Error fetching old files from folder:", error); + return null; + } +} + +export async function GET(request: NextRequest) { + try { + const authHeader = request.headers.get("authorization"); + + const cronSecret = process.env.CRON_SECRET; + const folderId = process.env.GOOGLE_DRIVE_OUTPUT_FOLDER_ID; + + if (!cronSecret) { + log.error("Missing cron secret"); + return new Response("Missing cron secret", { status: 500 }); + } + if (!folderId) { + log.error("No folder ID provided."); + return new Response("No folder ID provided", { status: 500 }); + } + + if (authHeader !== `Bearer ${cronSecret}`) { + log.error("Authorization failed. Invalid token."); + return new Response("Unauthorized", { status: 401 }); + } + + const files = await fetchExpiredExports({ folderId, daysAgo: 14 }); + + if (!files || files.length === 0) { + return new Response("No expired files found", { status: 404 }); + } + + const validFileIds = files.map((file) => file.id).filter(isTruthy); + + await updateExpiredAt(validFileIds); + await deleteExpiredExports(validFileIds); + } catch (error) { + Sentry.captureException(error); + return new Response("Internal Server Error", { status: 500 }); + } + + return new Response(JSON.stringify({ success: true }), { status: 200 }); +} diff --git a/apps/nextjs/vercel.json b/apps/nextjs/vercel.json index 1c4945e35..8746c630a 100644 --- a/apps/nextjs/vercel.json +++ b/apps/nextjs/vercel.json @@ -8,5 +8,11 @@ "deploymentEnabled": { "release": false } - } + }, + "crons": [ + { + "path": "/api/cron-jobs/expired-exports", + "schedule": "0 3 * * *" + } + ] } diff --git a/packages/api/src/router/exports.ts b/packages/api/src/router/exports.ts index 8d1c53de4..24ef6a433 100644 --- a/packages/api/src/router/exports.ts +++ b/packages/api/src/router/exports.ts @@ -127,6 +127,7 @@ export async function ailaGetExportBySnapshotId({ where: { lessonSnapshotId: snapshotId, exportType, + expiredAt: null, }, orderBy: { createdAt: "desc", diff --git a/packages/logger/index.ts b/packages/logger/index.ts index 255319ba1..7336f9b7c 100644 --- a/packages/logger/index.ts +++ b/packages/logger/index.ts @@ -59,7 +59,8 @@ type ChildKey = | "transcripts" | "trpc" | "ui" - | "webhooks"; + | "webhooks" + | "cron"; const errorLogger = typeof window === "undefined" diff --git a/turbo.json b/turbo.json index 0cb26a988..45f42d4e1 100644 --- a/turbo.json +++ b/turbo.json @@ -133,6 +133,7 @@ "STRICT_CSP", "TELEMETRY_ENABLED", "UPSTASH_*", - "WOLFRAM_CLIENT_SECRET" + "WOLFRAM_CLIENT_SECRET", + "CRON_SECRET" ] }