From 8f787763a22a30a9dd2f962a415b0c42239a9ed0 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 21 Jan 2026 23:07:46 +0530 Subject: [PATCH] changelog api --- apps/changelog-api/.gitignore | 33 +++++++ apps/changelog-api/README.md | 21 +++++ apps/changelog-api/package.json | 17 ++++ apps/changelog-api/src/index.ts | 83 +++++++++++++++++ apps/changelog-api/src/parser.ts | 144 ++++++++++++++++++++++++++++++ apps/changelog-api/tsconfig.json | 18 ++++ apps/changelog-api/wrangler.jsonc | 9 ++ 7 files changed, 325 insertions(+) create mode 100644 apps/changelog-api/.gitignore create mode 100644 apps/changelog-api/README.md create mode 100644 apps/changelog-api/package.json create mode 100644 apps/changelog-api/src/index.ts create mode 100644 apps/changelog-api/src/parser.ts create mode 100644 apps/changelog-api/tsconfig.json create mode 100644 apps/changelog-api/wrangler.jsonc diff --git a/apps/changelog-api/.gitignore b/apps/changelog-api/.gitignore new file mode 100644 index 00000000..e319e063 --- /dev/null +++ b/apps/changelog-api/.gitignore @@ -0,0 +1,33 @@ +# prod +dist/ + +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ +.wrangler + +# env +.env +.env.production +.dev.vars + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# misc +.DS_Store diff --git a/apps/changelog-api/README.md b/apps/changelog-api/README.md new file mode 100644 index 00000000..eba2b1e4 --- /dev/null +++ b/apps/changelog-api/README.md @@ -0,0 +1,21 @@ +```txt +npm install +npm run dev +``` + +```txt +npm run deploy +``` + +[For generating/synchronizing types based on your Worker configuration run](https://developers.cloudflare.com/workers/wrangler/commands/#types): + +```txt +npm run cf-typegen +``` + +Pass the `CloudflareBindings` as generics when instantiation `Hono`: + +```ts +// src/index.ts +const app = new Hono<{ Bindings: CloudflareBindings }>() +``` diff --git a/apps/changelog-api/package.json b/apps/changelog-api/package.json new file mode 100644 index 00000000..d8e26e17 --- /dev/null +++ b/apps/changelog-api/package.json @@ -0,0 +1,17 @@ +{ + "name": "@supermemory/changelog-api", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy --minify" + }, + "dependencies": { + "hono": "^4.11.4" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250620.0", + "typescript": "^5.8.3", + "wrangler": "^4.4.0" + } +} \ No newline at end of file diff --git a/apps/changelog-api/src/index.ts b/apps/changelog-api/src/index.ts new file mode 100644 index 00000000..21a6486a --- /dev/null +++ b/apps/changelog-api/src/index.ts @@ -0,0 +1,83 @@ +import { Hono } from "hono" +import { cors } from "hono/cors" +import { parseChangelog, type ChangelogResponse } from "./parser" + +const MDX_URL = + "https://raw.githubusercontent.com/supermemoryai/supermemory/main/apps/docs/changelog/developer-platform.mdx" + +const CACHE_KEY = "changelog-api-v1" +const CACHE_TTL_SECONDS = 3600 // 1 hour + +const app = new Hono() + +app.use( + "*", + cors({ + origin: "*", + allowMethods: ["GET", "OPTIONS"], + allowHeaders: ["Content-Type"], + }), +) + +async function fetchAndParseChangelog(): Promise { + const response = await fetch(MDX_URL) + if (!response.ok) { + throw new Error(`Failed to fetch changelog: ${response.status}`) + } + const content = await response.text() + return parseChangelog(content) +} + +async function getCachedChangelog( + cacheUrl: string, +): Promise { + const cache = caches.default + const cachedResponse = await cache.match(cacheUrl) + + if (cachedResponse) { + return cachedResponse.json() + } + + return null +} + +async function cacheChangelog( + cacheUrl: string, + data: ChangelogResponse, +): Promise { + const cache = caches.default + const response = new Response(JSON.stringify(data), { + headers: { + "Content-Type": "application/json", + "Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}`, + }, + }) + await cache.put(cacheUrl, response) +} + +app.get("/", async (c) => { + const cacheUrl = new URL(CACHE_KEY, c.req.url).toString() + + // Try to get from cache first + let changelog = await getCachedChangelog(cacheUrl) + + if (!changelog) { + // Fetch fresh data + changelog = await fetchAndParseChangelog() + // Cache it (don't await to not block response) + c.executionCtx.waitUntil(cacheChangelog(cacheUrl, changelog)) + } + + return c.json(changelog, { + headers: { + "Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=86400`, + }, + }) +}) + +// Health check endpoint +app.get("/health", (c) => { + return c.json({ status: "ok" }) +}) + +export default app diff --git a/apps/changelog-api/src/parser.ts b/apps/changelog-api/src/parser.ts new file mode 100644 index 00000000..af815c7a --- /dev/null +++ b/apps/changelog-api/src/parser.ts @@ -0,0 +1,144 @@ +export interface ChangelogItem { + title: string + description: string +} + +export interface ChangelogEntry { + date: string // "2025-12-30" + dateFormatted: string // "December 30, 2025" + items: ChangelogItem[] +} + +export interface ChangelogResponse { + title: string + description: string + lastUpdated: string // ISO date of most recent entry + entries: ChangelogEntry[] + total: number +} + +function parseFrontmatter(content: string): { + frontmatter: Record + body: string +} { + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) + if (!frontmatterMatch) { + return { frontmatter: {}, body: content } + } + + const frontmatterStr = frontmatterMatch[1] + const body = frontmatterMatch[2] + + const frontmatter: Record = {} + for (const line of frontmatterStr.split("\n")) { + const match = line.match(/^(\w+):\s*"?([^"]*)"?$/) + if (match) { + frontmatter[match[1]] = match[2] + } + } + + return { frontmatter, body } +} + +function parseDate(dateStr: string): { date: string; dateFormatted: string } { + // Parse "December 30, 2025" format + const months: Record = { + January: "01", + February: "02", + March: "03", + April: "04", + May: "05", + June: "06", + July: "07", + August: "08", + September: "09", + October: "10", + November: "11", + December: "12", + } + + const match = dateStr.match(/^(\w+)\s+(\d+),\s+(\d+)$/) + if (!match) { + return { date: "", dateFormatted: dateStr } + } + + const [, month, day, year] = match + const monthNum = months[month] + if (!monthNum) { + console.warn(`Unknown month "${month}" in date "${dateStr}", skipping entry`) + return { date: "", dateFormatted: dateStr } + } + const paddedDay = day.padStart(2, "0") + + return { + date: `${year}-${monthNum}-${paddedDay}`, + dateFormatted: dateStr, + } +} + +function parseItems(content: string): ChangelogItem[] { + const items: ChangelogItem[] = [] + + // Match lines starting with "- **Title:** Description" or "- **Title** Description" + const lines = content.split("\n") + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed.startsWith("- **")) continue + + // Match "- **Title:** Description" or "- **Title** Description" + const match = trimmed.match(/^-\s+\*\*([^*]+)\*\*:?\s*(.*)$/) + if (match) { + // Remove trailing colon from title if present (e.g., "MCP Context Prompt:" -> "MCP Context Prompt") + const title = match[1].trim().replace(/:$/, "") + items.push({ + title, + description: match[2].trim(), + }) + } + } + + return items +} + +export function parseChangelog(content: string): ChangelogResponse { + const { frontmatter, body } = parseFrontmatter(content) + + const entries: ChangelogEntry[] = [] + + // Split by date headers (## Month Day, Year) + const sections = body.split(/^##\s+/m).filter((s) => s.trim()) + + for (const section of sections) { + const lines = section.split("\n") + const dateStr = lines[0].trim() + + // Skip if not a valid date header + if (!dateStr.match(/^\w+\s+\d+,\s+\d+$/)) continue + + const { date, dateFormatted } = parseDate(dateStr) + const items = parseItems(section) + + if (items.length > 0 && date) { + entries.push({ + date, + dateFormatted, + items, + }) + } + } + + // Sort entries by date descending (most recent first) + entries.sort((a, b) => b.date.localeCompare(a.date)) + + const totalItems = entries.reduce((sum, entry) => sum + entry.items.length, 0) + const lastUpdated = entries[0]?.date || "" + + return { + title: frontmatter.title || "Changelog", + description: frontmatter.description || "", + lastUpdated, + entries, + total: totalItems, + } +} diff --git a/apps/changelog-api/tsconfig.json b/apps/changelog-api/tsconfig.json new file mode 100644 index 00000000..e001d62e --- /dev/null +++ b/apps/changelog-api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "types": ["@cloudflare/workers-types"], + "esModuleInterop": true, + "resolveJsonModule": true, + "outDir": "dist", + "rootDir": "src", + "baseUrl": "." + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/apps/changelog-api/wrangler.jsonc b/apps/changelog-api/wrangler.jsonc new file mode 100644 index 00000000..3445b98f --- /dev/null +++ b/apps/changelog-api/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "changelog-api", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + "observability": { + "enabled": true + } +}