next-site: D1 access layer, dynamic public index.json merge
This commit is contained in:
parent
ef25876bfb
commit
84b4ac96f5
3 changed files with 255 additions and 0 deletions
175
worker/src/db.ts
Normal file
175
worker/src/db.ts
Normal file
|
|
@ -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<Override | null> {
|
||||
const row = await env.DB.prepare(
|
||||
'SELECT * FROM plugin_overrides WHERE id = ?1',
|
||||
)
|
||||
.bind(id)
|
||||
.first<Row>()
|
||||
return row ? rowToOverride(row) : null
|
||||
}
|
||||
|
||||
export async function getAllOverrides(
|
||||
env: Env,
|
||||
): Promise<Record<string, Override>> {
|
||||
const { results } = await env.DB.prepare(
|
||||
'SELECT * FROM plugin_overrides',
|
||||
).all<Row>()
|
||||
const out: Record<string, Override> = {}
|
||||
for (const row of results) out[row.id] = rowToOverride(row)
|
||||
return out
|
||||
}
|
||||
|
||||
export async function getChannels(
|
||||
env: Env,
|
||||
id: string,
|
||||
): Promise<Record<string, string>> {
|
||||
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<string, string> = {}
|
||||
for (const row of results) out[row.channel] = row.version
|
||||
return out
|
||||
}
|
||||
|
||||
export async function getAllChannels(
|
||||
env: Env,
|
||||
): Promise<Record<string, Record<string, string>>> {
|
||||
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<string, Record<string, string>> = {}
|
||||
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<Override>,
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<Response> {
|
||||
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<BaseIndex>()
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
57
worker/src/publicIndex.ts
Normal file
57
worker/src/publicIndex.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { Override } from './db'
|
||||
|
||||
export interface IndexVersion {
|
||||
url: string
|
||||
sha256: string
|
||||
size: number
|
||||
dependencies: Record<string, { version?: string; optional?: boolean }>
|
||||
}
|
||||
|
||||
export interface IndexPlugin {
|
||||
name: string
|
||||
description: string
|
||||
author: string
|
||||
icon?: string
|
||||
channels: Record<string, string>
|
||||
versions: Record<string, IndexVersion>
|
||||
}
|
||||
|
||||
export interface BaseIndex {
|
||||
format: number
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
plugins: Record<string, IndexPlugin>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, Override>,
|
||||
channels: Record<string, Record<string, string>>,
|
||||
): BaseIndex {
|
||||
const plugins: Record<string, IndexPlugin> = {}
|
||||
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 }
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue