import { getItemByNotionId, getPlainText, transformContent, toCamelCase, } from "./notion.js"; import _ from "lodash"; import diff from "microdiff"; import { getNavigation } from "../utils/navigation.js"; import dayjs from "dayjs"; export async function getProjects(env, cached = false) { try { // If cached=true, try to get from Cloudflare Cache API first if (cached) { try { const cache = caches.default; const cacheKey = new Request( `https://${env.CACHE_URL || "cache"}/Projects` ); const cachedResponse = await cache.match(cacheKey); if (cachedResponse) { const cachedData = await cachedResponse.json(); console.log("Projects retrieved from Cloudflare cache"); return cachedData; } } catch (cacheError) { console.log("Cache miss or error, falling back to KV:", cacheError); } } // Fall back to KV storage const kvData = await env.CONTENT_KV.get(env.PROJECTS_KEY, { type: "json", }); // If we were trying to use cache but it failed, store the KV data in cache if (cached && kvData) { try { const cache = caches.default; const cacheKey = new Request( `https://${env.CACHE_URL || "cache"}/Projects` ); const response = new Response(JSON.stringify(kvData), { headers: { "Content-Type": "application/json", "Cache-Control": "max-age=60", // 1 minute TTL ETag: `"Projects-${Date.now()}"`, // Add ETag for cache validation }, }); await cache.put(cacheKey, response); console.log("Projects stored in Cloudflare cache after KV fallback"); } catch (cacheError) { console.warn("Error storing in cache after KV fallback:", cacheError); } } return kvData || []; } catch (error) { console.log("Error fetching Projects cache:", error); return null; } } export async function storeProjects(env, Projects) { try { // Always store in KV first await env.CONTENT_KV.put(env.PROJECTS_KEY, JSON.stringify(Projects)); console.log("Projects stored in KV."); // Purge the Cloudflare Cache API try { const cache = caches.default; const cacheKey = new Request( `https://${env.CACHE_URL || "cache"}/Projects` ); await cache.delete(cacheKey); console.log("Projects cache purged successfully."); } catch (cacheError) { console.warn( "Error purging Cloudflare cache, but KV was updated successfully:", cacheError ); } } catch (error) { console.error("Error storing Projects cache:", error); throw error; } } export function diffProjects(newList, oldList) { // Keys to check for changes const keysToCheck = [ "name", "date", "image", "content", "published", "externalLink", "type", "tools", "client", ]; // Helper: index by notionId using lodash const oldById = _.keyBy(oldList || [], "notionId"); const newById = _.keyBy(newList || [], "notionId"); // toAdd: in newList but not in oldList const toAdd = newList.filter((p) => !oldById[p.notionId]); // toDelete: in oldList but not in newList const toDelete = oldList.filter((p) => !newById[p.notionId]); // toUpdate: in both, but with different content (using microdiff) const toUpdate = newList.filter((p) => { const old = oldById[p.notionId]; if (!old) return false; // Prepare objects for comparison const { notionId, ...restNew } = p; const { notionId: _, ...restOld } = old; // Use microdiff to get differences const differences = diff(restOld, restNew); // Filter differences to only include keys we care about const relevantDifferences = differences.filter((change) => { // Check if the changed path starts with any of our keysToCheck const path = change.path.join("."); return keysToCheck.some((key) => path.startsWith(key)); }); return relevantDifferences.length > 0; }); return { toAdd, toUpdate, toDelete }; } // Transform Notion project data to desired format export async function transformNotionProject(env, notionProject, settingsData) { const navigationItems = await getNavigation(env); const properties = notionProject.properties; console.log("Notion Project:", notionProject); // Extract theme information let theme = undefined; // default if (properties.Theme?.relation?.[0]?.id) { const themeId = properties.Theme.relation[0].id; console.log(settingsData.themes); const themeData = getItemByNotionId(settingsData.themes, themeId); if (themeData?.name) { theme = themeData.name; } } const published = properties["Published"]?.checkbox || false; const slug = properties["Slug"]?.formula?.string || "unknown"; const status = toCamelCase(properties["Status"]?.status?.name || null); const date = properties["Date"]?.date?.start || null; // Extract image URL (handle both external and file images) const imageFile = notionProject?.cover; const image = imageFile?.external?.url || imageFile?.file?.url || null; const subTitle = getPlainText(properties["Subtitle"]); // Extract name from title const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; // Extract external link const externalLink = properties["External Link"]?.url || null; // Extract type (select) const type = properties["Type"]?.select?.name || null; // Extract tools (multi-select) const tools = (properties["Tools"]?.multi_select || []).map( (item) => item.name ); // Extract client (text) const client = getPlainText(properties["Client"]); // Fetch and transform the actual page content from Notion blocks const content = [{ type: "image", url: image, caption: subTitle }]; const projectContent = await transformContent( env, notionProject.id, navigationItems ); content.push(...projectContent); if (published == false) { return null; } return { notionId: notionProject.id, name: name, date: date, image: image, subTitle: subTitle, content, published, externalLink: externalLink, type: type, tools: tools, client: client, slug: slug, status: status, theme: theme, }; }