Tom Butcher db60e43d73 Update project configuration and enhance API functionality
- Updated package.json to use npx for deploying and developing, and upgraded several dependencies including vitest and wrangler.
- Renamed the project in wrangler.jsonc to include the year 2026 and added new routes and triggers for scheduled tasks.
- Enhanced the API by adding new reload endpoints for pages, blogs, projects, and experiences, with appropriate CORS handling.
- Implemented checks in import functions for files, images, and videos to skip updates if no changes are detected.
- Improved error handling and logging across various routes and utilities.
- Refactored global headers to dynamically resolve CORS origins based on incoming requests.
2025-11-15 19:34:41 +00:00

225 lines
6.2 KiB
JavaScript

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,
};
}