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

@@ -18,6 +18,37 @@ export type EntryDetailData = {
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 default function EntryDetailClient({ data }: { data: EntryDetailData }) {
@@ -173,6 +204,126 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData }) {
<hr />
<div>
<h5>Files</h5>
{(!data.files || data.files.length === 0) && <div className="text-secondary">No files linked</div>}
{data.files && data.files.length > 0 && (
<div className="table-responsive">
<table className="table table-sm table-striped align-middle">
<thead>
<tr>
<th>Type</th>
<th>Link</th>
<th style={{ width: 120 }} className="text-end">Size</th>
<th style={{ width: 260 }}>MD5</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
{data.files.map((f) => {
const isHttp = f.link.startsWith("http://") || f.link.startsWith("https://");
return (
<tr key={f.id}>
<td><span className="badge text-bg-secondary">{f.type.name}</span></td>
<td>
{isHttp ? (
<a href={f.link} target="_blank" rel="noopener noreferrer">{f.link}</a>
) : (
<span>{f.link}</span>
)}
</td>
<td className="text-end">{f.size != null ? new Intl.NumberFormat().format(f.size) : "-"}</td>
<td><code>{f.md5 ?? "-"}</code></td>
<td>{f.comments ?? ""}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
<hr />
<div>
<h5>Downloads</h5>
{(!data.releases || data.releases.length === 0) && <div className="text-secondary">No downloads</div>}
{data.releases && data.releases.length > 0 && (
<div className="vstack gap-3">
{data.releases.map((r) => (
<div key={r.releaseSeq} className="card">
<div className="card-header d-flex align-items-center gap-2 flex-wrap">
<span className="badge text-bg-secondary">Release #{r.releaseSeq}</span>
{r.type.name && <span className="badge text-bg-primary">{r.type.name}</span>}
{r.language.name && <span className="badge text-bg-info">{r.language.name}</span>}
{r.machinetype.name && <span className="badge text-bg-warning text-dark">{r.machinetype.name}</span>}
{r.year && <span className="badge text-bg-light text-dark">{r.year}</span>}
{r.comments && <span className="text-secondary">{r.comments}</span>}
</div>
<div className="card-body">
{r.downloads.length === 0 ? (
<div className="text-secondary">No downloads in this release</div>
) : (
<div className="table-responsive">
<table className="table table-sm table-striped align-middle">
<thead>
<tr>
<th>Type</th>
<th>Link</th>
<th style={{ width: 120 }} className="text-end">Size</th>
<th style={{ width: 260 }}>MD5</th>
<th>Flags</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{r.downloads.map((d) => {
const isHttp = d.link.startsWith("http://") || d.link.startsWith("https://");
return (
<tr key={d.id}>
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
<td>
{isHttp ? (
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
) : (
<span>{d.link}</span>
)}
</td>
<td className="text-end">{d.size != null ? new Intl.NumberFormat().format(d.size) : "-"}</td>
<td><code>{d.md5 ?? "-"}</code></td>
<td>
<div className="d-flex flex-wrap gap-1">
{d.isDemo && <span className="badge rounded-pill text-bg-warning text-dark">Demo</span>}
{d.language.name && <span className="badge rounded-pill text-bg-info">{d.language.name}</span>}
{d.machinetype.name && <span className="badge rounded-pill text-bg-primary">{d.machinetype.name}</span>}
</div>
</td>
<td>
<div className="d-flex flex-wrap gap-1">
{d.scheme.name && <span className="badge text-bg-light text-dark">{d.scheme.name}</span>}
{d.source.name && <span className="badge text-bg-light text-dark">{d.source.name}</span>}
{d.case.name && <span className="badge text-bg-light text-dark">{d.case.name}</span>}
{d.year && <span className="badge text-bg-light text-dark">{d.year}</span>}
</div>
{d.comments && <div className="text-secondary small mt-1">{d.comments}</div>}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
<hr />
<div className="d-flex align-items-center gap-2">
<Link className="btn btn-sm btn-outline-secondary" href={`/zxdb/entries/${data.id}`}>Permalink</Link>
<Link className="btn btn-sm btn-outline-primary" href="/zxdb">Back to Explorer</Link>