From 84b4ac96f57579ece230702270f5f8221863be7c Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sat, 15 Aug 2026 12:13:12 -0400 Subject: [PATCH] next-site: D1 access layer, dynamic public index.json merge --- worker/src/db.ts | 175 ++++++++++++++++++++++++++++++++++++++ worker/src/index.ts | 23 +++++ worker/src/publicIndex.ts | 57 +++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 worker/src/db.ts create mode 100644 worker/src/publicIndex.ts diff --git a/worker/src/db.ts b/worker/src/db.ts new file mode 100644 index 0000000..138b61a --- /dev/null +++ b/worker/src/db.ts @@ -0,0 +1,175 @@ +import type { Env } from './env' + +export interface Override { + name?: string + description?: string + tagline?: string + note?: string + category?: string + accent?: string + status?: string + howItWorks?: string + features?: string[] + commands?: { cmd: string; desc: string }[] + limitations?: string + hidden: boolean + isDraft: boolean + manifest?: string + mainJs?: string +} + +interface Row { + id: string + name: string | null + description: string | null + tagline: string | null + note: string | null + category: string | null + accent: string | null + status: string | null + how_it_works: string | null + features: string | null + commands: string | null + limitations: string | null + hidden: number + is_draft: number + manifest: string | null + main_js: string | null +} + +function rowToOverride(row: Row): Override { + return { + name: row.name ?? undefined, + description: row.description ?? undefined, + tagline: row.tagline ?? undefined, + note: row.note ?? undefined, + category: row.category ?? undefined, + accent: row.accent ?? undefined, + status: row.status ?? undefined, + howItWorks: row.how_it_works ?? undefined, + features: row.features ? JSON.parse(row.features) : undefined, + commands: row.commands ? JSON.parse(row.commands) : undefined, + limitations: row.limitations ?? undefined, + hidden: !!row.hidden, + isDraft: !!row.is_draft, + manifest: row.manifest ?? undefined, + mainJs: row.main_js ?? undefined, + } +} + +export async function getOverride( + env: Env, + id: string, +): Promise { + const row = await env.DB.prepare( + 'SELECT * FROM plugin_overrides WHERE id = ?1', + ) + .bind(id) + .first() + return row ? rowToOverride(row) : null +} + +export async function getAllOverrides( + env: Env, +): Promise> { + const { results } = await env.DB.prepare( + 'SELECT * FROM plugin_overrides', + ).all() + const out: Record = {} + for (const row of results) out[row.id] = rowToOverride(row) + return out +} + +export async function getChannels( + env: Env, + id: string, +): Promise> { + const { results } = await env.DB.prepare( + 'SELECT channel, version FROM plugin_channels WHERE plugin_id = ?1', + ) + .bind(id) + .all<{ channel: string; version: string }>() + const out: Record = {} + for (const row of results) out[row.channel] = row.version + return out +} + +export async function getAllChannels( + env: Env, +): Promise>> { + const { results } = await env.DB.prepare( + 'SELECT plugin_id, channel, version FROM plugin_channels', + ).all<{ + plugin_id: string + channel: string + version: string + }>() + const out: Record> = {} + for (const row of results) { + out[row.plugin_id] ??= {} + out[row.plugin_id][row.channel] = row.version + } + return out +} + +export async function upsertOverride( + env: Env, + id: string, + patch: Partial, +): Promise { + const existing = await getOverride(env, id) + const merged: Override = { + hidden: false, + isDraft: false, + ...existing, + ...patch, + } + + await env.DB.prepare( + `INSERT INTO plugin_overrides ( + id, name, description, tagline, note, category, accent, status, how_it_works, + features, commands, limitations, hidden, is_draft, manifest, main_js, updated_at + ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, description = excluded.description, tagline = excluded.tagline, + note = excluded.note, category = excluded.category, accent = excluded.accent, + status = excluded.status, how_it_works = excluded.how_it_works, features = excluded.features, + commands = excluded.commands, limitations = excluded.limitations, hidden = excluded.hidden, + is_draft = excluded.is_draft, manifest = excluded.manifest, main_js = excluded.main_js, + updated_at = excluded.updated_at`, + ) + .bind( + id, + merged.name ?? null, + merged.description ?? null, + merged.tagline ?? null, + merged.note ?? null, + merged.category ?? null, + merged.accent ?? null, + merged.status ?? null, + merged.howItWorks ?? null, + merged.features ? JSON.stringify(merged.features) : null, + merged.commands ? JSON.stringify(merged.commands) : null, + merged.limitations ?? null, + merged.hidden ? 1 : 0, + merged.isDraft ? 1 : 0, + merged.manifest ?? null, + merged.mainJs ?? null, + new Date().toISOString(), + ) + .run() +} + +export async function setChannel( + env: Env, + id: string, + channel: string, + version: string, +): Promise { + await env.DB.prepare( + `INSERT INTO plugin_channels (plugin_id, channel, version, updated_at) VALUES (?1,?2,?3,?4) + ON CONFLICT(plugin_id, channel) DO UPDATE SET version = excluded.version, updated_at = excluded.updated_at`, + ) + .bind(id, channel, version, new Date().toISOString()) + .run() +} diff --git a/worker/src/index.ts b/worker/src/index.ts index 5b8406a..97d5235 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,7 +1,30 @@ +import { getAllChannels, getAllOverrides } from './db' +import { buildPublicIndex } from './publicIndex' import type { Env } from './env' +import type { BaseIndex } from './publicIndex' export default { async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url) + + if (url.pathname === '/index.json') { + const baseRes = await env.ASSETS.fetch( + new URL('/index.json', request.url), + ) + if (!baseRes.ok) return baseRes + + const base = await baseRes.json() + const [overrides, channels] = await Promise.all([ + getAllOverrides(env), + getAllChannels(env), + ]) + const merged = buildPublicIndex(base, overrides, channels) + + return new Response(JSON.stringify(merged), { + headers: { 'content-type': 'application/json' }, + }) + } + return env.ASSETS.fetch(request) }, } diff --git a/worker/src/publicIndex.ts b/worker/src/publicIndex.ts new file mode 100644 index 0000000..91cb303 --- /dev/null +++ b/worker/src/publicIndex.ts @@ -0,0 +1,57 @@ +import type { Override } from './db' + +export interface IndexVersion { + url: string + sha256: string + size: number + dependencies: Record +} + +export interface IndexPlugin { + name: string + description: string + author: string + icon?: string + channels: Record + versions: Record +} + +export interface BaseIndex { + format: number + name: string + description: string + icon?: string + plugins: Record +} + +/** + * Merges the git-tracked base index (real versions/hashes, never touched) with live D1 + * overrides: hidden plugins drop out, edited display fields win, and whatever version is + * mapped to the 'stable' channel becomes 'latest' - the only field of `channels` a public + * consumer (Next's installer) ever reads. alpha/beta stay in `channels` for the admin UI's + * own use but are meaningless to a public client. + */ +export function buildPublicIndex( + base: BaseIndex, + overrides: Record, + channels: Record>, +): BaseIndex { + const plugins: Record = {} + for (const [id, plugin] of Object.entries(base.plugins)) { + const override = overrides[id] + if (override?.hidden) continue + + const pluginChannels = channels[id] + const stable = pluginChannels?.stable + plugins[id] = { + ...plugin, + name: override?.name ?? plugin.name, + description: override?.description ?? plugin.description, + channels: { + ...plugin.channels, + ...(stable && stable in plugin.versions ? { latest: stable } : {}), + }, + } + } + return { ...base, plugins } +}