Show downloads even without releases rows

Add synthetic release groups in getEntryById so downloads
are displayed even when there are no matching rows in
`releases` for a given entry. Group by `release_seq`,
attach downloads, and sort groups for stable order.

This fixes cases like /zxdb/entries/1 where `downloads`
exist for the entry but `releases` is empty, resulting in
no downloads shown in the UI.

Signed-off-by: Junie@devbox
This commit is contained in:
2025-12-16 21:47:17 +00:00
parent 285c7da87c
commit fd4c0f8963
3 changed files with 147 additions and 104 deletions

View File

@@ -168,6 +168,23 @@ export interface EntryDetail {
comments: string | null;
type: { id: number; name: string };
}[];
// Flat downloads by entry_id (no dependency on releases)
downloadsFlat?: {
id: number;
link: string;
size: number | null;
md5: string | null;
comments: string | null;
isDemo: boolean;
type: { id: number; name: string };
language: { id: string | null; name: string | null };
machinetype: { id: number | null; name: string | null };
scheme: { id: string | null; name: string | null };
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
releaseSeq: number;
}[];
releases?: {
releaseSeq: number;
type: { id: string | null; name: string | null };
@@ -194,18 +211,6 @@ export interface EntryDetail {
}
export async function getEntryById(id: number): Promise<EntryDetail | null> {
// Helper: check if releases/downloads lookup tables exist; cache result per process
// This prevents runtime crashes on environments where ZXDB schema is older/minimal.
async function hasReleaseSchema() {
try {
const rows = await db.execute<{ cnt: number }>(sql`select count(*) as cnt from information_schema.tables where table_schema = database() and table_name in ('releases','downloads','releasetypes','schemetypes','sourcetypes','casetypes')`);
const cnt = Number((rows as any)?.[0]?.cnt ?? 0);
// require at least releases + downloads; lookups are optional
return cnt >= 2;
} catch {
return false;
}
}
// Run base row + contributors in parallel to reduce latency
const [rows, authorRows, publisherRows] = await Promise.all([
@@ -277,9 +282,10 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
let releaseRows: any[] = [];
let downloadRows: any[] = [];
const schemaOk = await hasReleaseSchema();
if (schemaOk) {
// Fetch releases for this entry (lightweight)
let downloadFlatRows: any[] = [];
// Fetch releases for this entry (optional; ignore if table missing)
try {
releaseRows = (await db
.select({
releaseSeq: releases.releaseSeq,
@@ -297,8 +303,12 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
.leftJoin(languages, eq(languages.id as any, releases.languageId as any))
.leftJoin(machinetypes, eq(machinetypes.id as any, releases.machinetypeId as any))
.where(eq(releases.entryId as any, id as any))) as any;
} catch {
releaseRows = [];
}
// Fetch downloads for this entry, join lookups
// Fetch downloads for this entry, join lookups (do not gate behind schema checks)
try {
downloadRows = (await db
.select({
id: downloads.id,
@@ -330,8 +340,13 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
.leftJoin(sourcetypes, eq(sourcetypes.id as any, downloads.sourcetypeId as any))
.leftJoin(casetypes, eq(casetypes.id as any, downloads.casetypeId as any))
.where(eq(downloads.entryId as any, id as any))) as any;
} catch {
downloadRows = [];
}
// Flat list: same rows mapped, independent of releases
downloadFlatRows = downloadRows;
const downloadsBySeq = new Map<number, any[]>();
for (const row of downloadRows) {
const arr = downloadsBySeq.get(row.releaseSeq) ?? [];
@@ -339,6 +354,9 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
downloadsBySeq.set(row.releaseSeq, arr);
}
// Build a map of downloads grouped by release_seq
// Then ensure we create "synthetic" release groups for any release_seq
// that appears in downloads but has no corresponding releases row.
const releasesData = releaseRows.map((r: any) => ({
releaseSeq: Number(r.releaseSeq),
type: { id: (r.releasetypeId as any) ?? null, name: (r.releasetypeName as any) ?? null },
@@ -363,6 +381,11 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
})),
}));
// No synthetic release groups: only real releases are returned
// Sort releases by sequence for stable UI order
releasesData.sort((a, b) => a.releaseSeq - b.releaseSeq);
return {
id: base.id,
title: base.title,
@@ -389,6 +412,22 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
}))
: [],
releases: releasesData,
downloadsFlat: downloadFlatRows.map((d: any) => ({
id: d.id,
link: d.link,
size: d.size ?? null,
md5: d.md5 ?? null,
comments: d.comments ?? null,
isDemo: !!d.isDemo,
type: { id: d.filetypeId, name: d.filetypeName },
language: { id: (d.dlLangId as any) ?? null, name: (d.dlLangName as any) ?? null },
machinetype: { id: (d.dlMachineId as any) ?? null, name: (d.dlMachineName as any) ?? null },
scheme: { id: (d.schemeId as any) ?? null, name: (d.schemeName as any) ?? null },
source: { id: (d.sourceId as any) ?? null, name: (d.sourceName as any) ?? null },
case: { id: (d.caseId as any) ?? null, name: (d.caseName as any) ?? null },
year: (d.year as any) ?? null,
releaseSeq: Number(d.releaseSeq),
})),
};
}