Handle missing ZXDB releases/downloads schema gracefully

Prevent runtime crashes when `releases`, `downloads`, or related lookup tables
(`releasetypes`, `schemetypes`, `sourcetypes`, `casetypes`) are absent in the
connected ZXDB MySQL database.

- Repo: gate releases/downloads queries behind a schema capability check using
  `information_schema.tables`; if missing, skip queries and return empty arrays.
- Keeps entry detail page functional on legacy/minimal DB exports while fully
  utilizing rich data when available.

Refs: runtime error "Table 'zxdb.releasetypes' doesn't exist"

Signed-off-by: Junie@quinn
This commit is contained in:
2025-12-16 18:41:14 +00:00
parent f507d51c61
commit 285c7da87c
5 changed files with 434 additions and 1 deletions

View File

@@ -9,6 +9,14 @@ import {
languages,
machinetypes,
genretypes,
files,
filetypes,
releases,
downloads,
releasetypes,
schemetypes,
sourcetypes,
casetypes,
} from "@/server/schema/zxdb";
export interface SearchParams {
@@ -152,9 +160,53 @@ export interface EntryDetail {
withoutLoadScreen?: number;
withoutInlay?: number;
issueId?: number | null;
files?: {
id: number;
link: string;
size: number | null;
md5: string | null;
comments: string | null;
type: { id: number; name: string };
}[];
releases?: {
releaseSeq: number;
type: { id: string | null; name: string | null };
language: { id: string | null; name: string | null };
machinetype: { id: number | null; name: string | null };
year: number | null;
comments: string | null;
downloads: {
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;
}[];
}[];
}
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([
db
@@ -196,6 +248,121 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
const base = rows[0];
if (!base) return null;
// Fetch related files if the entry is associated with an issue
let fileRows: {
id: number;
link: string;
size: number | null;
md5: string | null;
comments: string | null;
typeId: number;
typeName: string;
}[] = [];
if (base.issueId != null) {
fileRows = (await db
.select({
id: files.id,
link: files.fileLink,
size: files.fileSize,
md5: files.fileMd5,
comments: files.comments,
typeId: filetypes.id,
typeName: filetypes.name,
})
.from(files)
.innerJoin(filetypes, eq(filetypes.id, files.filetypeId as any))
.where(eq(files.issueId as any, base.issueId as any))) as any;
}
let releaseRows: any[] = [];
let downloadRows: any[] = [];
const schemaOk = await hasReleaseSchema();
if (schemaOk) {
// Fetch releases for this entry (lightweight)
releaseRows = (await db
.select({
releaseSeq: releases.releaseSeq,
releasetypeId: releases.releasetypeId,
releasetypeName: releasetypes.name,
languageId: releases.languageId,
languageName: languages.name,
machinetypeId: releases.machinetypeId,
machinetypeName: machinetypes.name,
year: releases.releaseYear,
comments: releases.comments,
})
.from(releases)
.leftJoin(releasetypes, eq(releasetypes.id as any, releases.releasetypeId as any))
.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;
// Fetch downloads for this entry, join lookups
downloadRows = (await db
.select({
id: downloads.id,
releaseSeq: downloads.releaseSeq,
link: downloads.fileLink,
size: downloads.fileSize,
md5: downloads.fileMd5,
comments: downloads.comments,
isDemo: downloads.isDemo,
filetypeId: filetypes.id,
filetypeName: filetypes.name,
dlLangId: downloads.languageId,
dlLangName: languages.name,
dlMachineId: downloads.machinetypeId,
dlMachineName: machinetypes.name,
schemeId: schemetypes.id,
schemeName: schemetypes.name,
sourceId: sourcetypes.id,
sourceName: sourcetypes.name,
caseId: casetypes.id,
caseName: casetypes.name,
year: downloads.releaseYear,
})
.from(downloads)
.innerJoin(filetypes, eq(filetypes.id as any, downloads.filetypeId as any))
.leftJoin(languages, eq(languages.id as any, downloads.languageId as any))
.leftJoin(machinetypes, eq(machinetypes.id as any, downloads.machinetypeId as any))
.leftJoin(schemetypes, eq(schemetypes.id as any, downloads.schemetypeId as any))
.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;
}
const downloadsBySeq = new Map<number, any[]>();
for (const row of downloadRows) {
const arr = downloadsBySeq.get(row.releaseSeq) ?? [];
arr.push(row);
downloadsBySeq.set(row.releaseSeq, arr);
}
const releasesData = releaseRows.map((r: any) => ({
releaseSeq: Number(r.releaseSeq),
type: { id: (r.releasetypeId as any) ?? null, name: (r.releasetypeName as any) ?? null },
language: { id: (r.languageId as any) ?? null, name: (r.languageName as any) ?? null },
machinetype: { id: (r.machinetypeId as any) ?? null, name: (r.machinetypeName as any) ?? null },
year: (r.year as any) ?? null,
comments: (r.comments as any) ?? null,
downloads: (downloadsBySeq.get(Number(r.releaseSeq)) ?? []).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,
})),
}));
return {
id: base.id,
title: base.title,
@@ -210,6 +377,18 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
withoutLoadScreen: (base.withoutLoadScreen as any) ?? undefined,
withoutInlay: (base.withoutInlay as any) ?? undefined,
issueId: (base.issueId as any) ?? undefined,
files:
fileRows.length > 0
? fileRows.map((f) => ({
id: f.id,
link: f.link,
size: f.size ?? null,
md5: f.md5 ?? null,
comments: f.comments ?? null,
type: { id: f.typeId, name: f.typeName },
}))
: [],
releases: releasesData,
};
}