diff --git a/src/scripts/updateProjects.ts b/src/scripts/updateProjects.ts index b52f8a9..f30838c 100644 --- a/src/scripts/updateProjects.ts +++ b/src/scripts/updateProjects.ts @@ -5,17 +5,32 @@ import sharp from "sharp"; import { supabase } from '../lib/supabaseClient'; import { supabaseUrl } from '../lib/supabaseClient'; import { prisma } from "../lib/prisma.ts"; -import type { APIRoute } from 'astro'; // Function to sanitize filenames function sanitizeFileName(fileName: string): string { return fileName.replace(/[^a-z0-9-_.]/gi, '-'); // Replace invalid characters with hyphen } -// Function to download image from URL +// Function to download image from URL with timeout and error handling async function downloadImage(url: string): Promise { - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return Buffer.from(response.data, 'binary'); + try { + const response = await axios.get(url, { + responseType: 'arraybuffer', + timeout: 10000, // Set a timeout of 10 seconds + }); + return Buffer.from(response.data, 'binary'); + } catch (error) { + throw new Error(`Failed to download image: ${(error as Error).message}`); + } +} + +// Function to fetch fresh cover URL from Notion +async function getFreshCoverUrl(notion: Client, pageId: string): Promise { + const page = await notion.pages.retrieve({ page_id: pageId }) as projectRow; + const coverUrl = page.cover?.type === "external" + ? page.cover?.external.url + : page.cover?.file.url ?? ""; + return coverUrl; } // Function to delete image from Supabase Storage @@ -27,25 +42,31 @@ async function deleteImageFromSupabase(filePath: string): Promise { } } -// Function to upload image to Supabase Storage +// Function to upload image to Supabase Storage with error handling async function uploadImageToSupabase(imageBuffer: Buffer, filePath: string): Promise { - // Convert image to WebP format and compress it - const compressedImageBuffer = await sharp(imageBuffer) - .toFormat('webp', { quality: 30 }) - .toBuffer(); + try { + // Convert image to WebP format and compress it + const compressedImageBuffer = await sharp(imageBuffer) + .toFormat('webp', { quality: 30 }) + .toBuffer(); - // Attempt to delete the existing image first - await deleteImageFromSupabase(filePath); + // Attempt to delete the existing image first + await deleteImageFromSupabase(filePath); - // Upload the new image to Supabase - const { data, error } = await supabase.storage.from('images').upload(filePath, compressedImageBuffer); + // Upload the new image to Supabase + const { data, error } = await supabase.storage + .from('images') + .upload(filePath, compressedImageBuffer); - if (error) { - throw new Error(`Failed to upload image: ${error.message}`); - } + if (error) { + throw new Error(`Failed to upload image: ${error.message}`); + } - // Return the public URL of the uploaded image - return `${supabaseUrl}/storage/v1/object/public/images/${filePath}`; + // Return the public URL of the uploaded image + return `${supabaseUrl}/storage/v1/object/public/images/${filePath}`; + } catch (error) { + throw new Error(`Failed to process and upload image: ${(error as Error).message}`); + } } // Function to log messages @@ -62,11 +83,15 @@ function sendLog(controller: ReadableStreamDefaultController, message: string) { } // Update the updateProjects function -export async function updateProjects(controller: ReadableStreamDefaultController): Promise { +export async function updateProjects( + controller: ReadableStreamDefaultController +): Promise { const NOTION_TOKEN = process.env.NOTION_TOKEN || import.meta.env.NOTION_TOKEN; - const NOTION_PROJECTS_ID = process.env.NOTION_PROJECTS_ID || import.meta.env.NOTION_PROJECTS_ID; + const NOTION_PROJECTS_ID = + process.env.NOTION_PROJECTS_ID || import.meta.env.NOTION_PROJECTS_ID; if (!NOTION_TOKEN || !NOTION_PROJECTS_ID) { + sendLog(controller, "Missing Notion token or database ID."); throw new Error("Missing secret(s)"); } @@ -78,7 +103,7 @@ export async function updateProjects(controller: ReadableStreamDefaultController // Step 1: Get all project IDs from Notion const query = await notion.databases.query({ database_id: NOTION_PROJECTS_ID, - sorts: [{ property: 'Date', direction: 'descending' }] + sorts: [{ property: 'Date', direction: 'descending' }], }); const projectsRows = query.results as projectRow[]; @@ -92,7 +117,9 @@ export async function updateProjects(controller: ReadableStreamDefaultController const prismaProjectIds = prismaProjects.map(project => project.id); // Step 3: Identify projects to delete (those in Prisma but not in Notion) - const projectsToDelete = prismaProjects.filter(project => !notionProjectIds.includes(project.id)); + const projectsToDelete = prismaProjects.filter( + project => !notionProjectIds.includes(project.id) + ); // Step 4: Delete projects from Prisma and Supabase for (const project of projectsToDelete) { @@ -106,33 +133,67 @@ export async function updateProjects(controller: ReadableStreamDefaultController where: { id: project.id }, }); - sendLog(controller, `Deleted project ${project.id} from Prisma and its image from Supabase.`); + sendLog( + controller, + `Deleted project ${project.id} from Prisma and its image from Supabase.` + ); } - // Process the remaining projects (already in your existing logic) + // Process the remaining projects const projectPromises = projectsRows.map(async (row) => { - const title = row.properties.Name.title[0] ? row.properties.Name.title[0].plain_text : ""; - const dateStr = row.properties.Date.date ? row.properties.Date.date.start : ""; + const title = row.properties.Name.title[0] + ? row.properties.Name.title[0].plain_text + : ""; + const dateStr = row.properties.Date.date + ? row.properties.Date.date.start + : ""; const date = dateStr ? new Date(dateStr) : null; // Convert string to Date object - const description = row.properties.Description.rich_text[0] ? row.properties.Description.rich_text[0].plain_text : ""; - const coverUrl = row.cover?.type === "external" ? row.cover?.external.url : row.cover?.file.url ?? ""; - const team = row.properties.Team.rich_text[0] ? row.properties.Team.rich_text[0].plain_text : ""; - const tags = row.properties.Tags?.multi_select.map((tag) => tag.name) || []; // Ensure tags is always an array + const description = row.properties.Description.rich_text[0] + ? row.properties.Description.rich_text[0].plain_text + : ""; + const team = row.properties.Team.rich_text[0] + ? row.properties.Team.rich_text[0].plain_text + : ""; + const tags = + row.properties.Tags?.multi_select.map((tag) => tag.name) || []; // Ensure tags is always an array const id = row.id || ""; // Ensure the Notion ID is being used // Sanitize the title for cover path const sanitizedTitle = sanitizeFileName(title); - let coverPath = ''; - // If a cover URL exists, download and upload the image to Supabase + // Fetch fresh cover URL + let coverUrl = ''; + if (row.cover) { + try { + coverUrl = await getFreshCoverUrl(notion, row.id); + } catch (error) { + sendLog( + controller, + `Error fetching fresh cover URL for project ${title}: ${ + (error as Error).message + }` + ); + } + } + + // Proceed with image processing if coverUrl is available + let coverPath = ''; if (coverUrl) { try { sendLog(controller, `Downloading image for project: ${title}`); const imageBuffer = await downloadImage(coverUrl); // Download image - coverPath = await uploadImageToSupabase(imageBuffer, `projects/${sanitizedTitle}/cover.webp`); // Upload to Supabase + coverPath = await uploadImageToSupabase( + imageBuffer, + `projects/${sanitizedTitle}/cover.webp` + ); // Upload to Supabase sendLog(controller, `Uploaded cover image for project: ${title}`); } catch (error) { - sendLog(controller, `Error processing cover image for project ${title}: ${(error as Error).message}`); + sendLog( + controller, + `Error processing cover image for project ${title}: ${ + (error as Error).message + }` + ); } } @@ -194,6 +255,9 @@ export async function updateProjects(controller: ReadableStreamDefaultController sendLog(controller, "Projects data uploaded to Prisma database."); } catch (error) { console.error("Error retrieving or processing projects:", error); - sendLog(controller, `Error retrieving or processing projects: ${(error as Error).message}`); + sendLog( + controller, + `Error retrieving or processing projects: ${(error as Error).message}` + ); } -} \ No newline at end of file +}