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:
1
.junie/guidelines.md
Symbolic link
1
.junie/guidelines.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../AGENTS.md
|
||||||
14
COMMIT_EDITMSG
Normal file
14
COMMIT_EDITMSG
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -18,6 +18,37 @@ export type EntryDetailData = {
|
|||||||
withoutLoadScreen?: number;
|
withoutLoadScreen?: number;
|
||||||
withoutInlay?: number;
|
withoutInlay?: number;
|
||||||
issueId?: number | null;
|
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 }) {
|
export default function EntryDetailClient({ data }: { data: EntryDetailData }) {
|
||||||
@@ -173,6 +204,126 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData }) {
|
|||||||
|
|
||||||
<hr />
|
<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">
|
<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-secondary" href={`/zxdb/entries/${data.id}`}>Permalink</Link>
|
||||||
<Link className="btn btn-sm btn-outline-primary" href="/zxdb">Back to Explorer</Link>
|
<Link className="btn btn-sm btn-outline-primary" href="/zxdb">Back to Explorer</Link>
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import {
|
|||||||
languages,
|
languages,
|
||||||
machinetypes,
|
machinetypes,
|
||||||
genretypes,
|
genretypes,
|
||||||
|
files,
|
||||||
|
filetypes,
|
||||||
|
releases,
|
||||||
|
downloads,
|
||||||
|
releasetypes,
|
||||||
|
schemetypes,
|
||||||
|
sourcetypes,
|
||||||
|
casetypes,
|
||||||
} from "@/server/schema/zxdb";
|
} from "@/server/schema/zxdb";
|
||||||
|
|
||||||
export interface SearchParams {
|
export interface SearchParams {
|
||||||
@@ -152,9 +160,53 @@ export interface EntryDetail {
|
|||||||
withoutLoadScreen?: number;
|
withoutLoadScreen?: number;
|
||||||
withoutInlay?: number;
|
withoutInlay?: number;
|
||||||
issueId?: number | null;
|
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> {
|
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
|
// Run base row + contributors in parallel to reduce latency
|
||||||
const [rows, authorRows, publisherRows] = await Promise.all([
|
const [rows, authorRows, publisherRows] = await Promise.all([
|
||||||
db
|
db
|
||||||
@@ -196,6 +248,121 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
|||||||
const base = rows[0];
|
const base = rows[0];
|
||||||
if (!base) return null;
|
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 {
|
return {
|
||||||
id: base.id,
|
id: base.id,
|
||||||
title: base.title,
|
title: base.title,
|
||||||
@@ -210,6 +377,18 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
|||||||
withoutLoadScreen: (base.withoutLoadScreen as any) ?? undefined,
|
withoutLoadScreen: (base.withoutLoadScreen as any) ?? undefined,
|
||||||
withoutInlay: (base.withoutInlay as any) ?? undefined,
|
withoutInlay: (base.withoutInlay as any) ?? undefined,
|
||||||
issueId: (base.issueId 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mysqlTable, int, varchar, tinyint, char } from "drizzle-orm/mysql-core";
|
import { mysqlTable, int, varchar, tinyint, char, smallint } from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
// Minimal subset needed for browsing/searching
|
// Minimal subset needed for browsing/searching
|
||||||
export const entries = mysqlTable("entries", {
|
export const entries = mysqlTable("entries", {
|
||||||
@@ -79,3 +79,91 @@ export const genretypes = mysqlTable("genretypes", {
|
|||||||
// Column name in DB is `text`
|
// Column name in DB is `text`
|
||||||
name: varchar("text", { length: 50 }).notNull(),
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- Files and Filetypes (for downloads/assets) -----
|
||||||
|
export const filetypes = mysqlTable("filetypes", {
|
||||||
|
id: tinyint("id").notNull().primaryKey(),
|
||||||
|
// Column name in DB is `text`
|
||||||
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const files = mysqlTable("files", {
|
||||||
|
id: int("id").notNull().primaryKey(),
|
||||||
|
labelId: int("label_id"),
|
||||||
|
issueId: int("issue_id"),
|
||||||
|
toolId: int("tool_id"),
|
||||||
|
fileLink: varchar("file_link", { length: 250 }).notNull(),
|
||||||
|
fileDate: varchar("file_date", { length: 50 }),
|
||||||
|
fileSize: int("file_size"),
|
||||||
|
fileMd5: varchar("file_md5", { length: 32 }),
|
||||||
|
filetypeId: tinyint("filetype_id").notNull(),
|
||||||
|
comments: varchar("comments", { length: 250 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----- Releases / Downloads (linked assets per release) -----
|
||||||
|
// Lookups used by releases/downloads
|
||||||
|
export const releasetypes = mysqlTable("releasetypes", {
|
||||||
|
id: char("id", { length: 1 }).notNull().primaryKey(),
|
||||||
|
// column name in DB is `text`
|
||||||
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemetypes = mysqlTable("schemetypes", {
|
||||||
|
id: char("id", { length: 2 }).notNull().primaryKey(),
|
||||||
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sourcetypes = mysqlTable("sourcetypes", {
|
||||||
|
id: char("id", { length: 1 }).notNull().primaryKey(),
|
||||||
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const casetypes = mysqlTable("casetypes", {
|
||||||
|
id: char("id", { length: 1 }).notNull().primaryKey(),
|
||||||
|
name: varchar("text", { length: 50 }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hosts = mysqlTable("hosts", {
|
||||||
|
id: tinyint("id").notNull().primaryKey(),
|
||||||
|
title: varchar("title", { length: 150 }).notNull(),
|
||||||
|
link: varchar("link", { length: 150 }).notNull(),
|
||||||
|
admin: varchar("admin", { length: 150 }).notNull(),
|
||||||
|
magazineId: smallint("magazine_id"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// `releases` are identified by (entry_id, release_seq)
|
||||||
|
export const releases = mysqlTable("releases", {
|
||||||
|
entryId: int("entry_id").notNull(),
|
||||||
|
releaseSeq: smallint("release_seq").notNull(),
|
||||||
|
releasetypeId: char("releasetype_id", { length: 1 }),
|
||||||
|
languageId: char("language_id", { length: 2 }),
|
||||||
|
machinetypeId: tinyint("machinetype_id"),
|
||||||
|
labelId: int("label_id"), // developer
|
||||||
|
publisherId: int("publisher_label_id"),
|
||||||
|
releaseYear: smallint("release_year"),
|
||||||
|
comments: varchar("comments", { length: 250 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Downloads are linked to a release via (entry_id, release_seq)
|
||||||
|
export const downloads = mysqlTable("downloads", {
|
||||||
|
id: int("id").notNull().primaryKey(),
|
||||||
|
entryId: int("entry_id").notNull(),
|
||||||
|
releaseSeq: smallint("release_seq").notNull().default(0),
|
||||||
|
fileLink: varchar("file_link", { length: 250 }).notNull(),
|
||||||
|
fileDate: varchar("file_date", { length: 50 }),
|
||||||
|
fileSize: int("file_size"),
|
||||||
|
fileMd5: varchar("file_md5", { length: 32 }),
|
||||||
|
filetypeId: tinyint("filetype_id").notNull(),
|
||||||
|
scrBorder: tinyint("scr_border").notNull().default(7),
|
||||||
|
languageId: char("language_id", { length: 2 }),
|
||||||
|
isDemo: tinyint("is_demo").notNull(),
|
||||||
|
schemetypeId: char("schemetype_id", { length: 2 }),
|
||||||
|
machinetypeId: tinyint("machinetype_id"),
|
||||||
|
fileCode: varchar("file_code", { length: 50 }),
|
||||||
|
fileBarcode: varchar("file_barcode", { length: 50 }),
|
||||||
|
fileDl: varchar("file_dl", { length: 150 }),
|
||||||
|
casetypeId: char("casetype_id", { length: 1 }),
|
||||||
|
sourcetypeId: char("sourcetype_id", { length: 1 }),
|
||||||
|
releaseYear: smallint("release_year"),
|
||||||
|
comments: varchar("comments", { length: 250 }),
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user