No explicit any

This commit is contained in:
2025-12-17 20:10:00 +00:00
parent 18cf0cc140
commit 89001f53da
18 changed files with 257 additions and 205 deletions

View File

@@ -100,6 +100,12 @@ Comment what the code does, not what the agent has done. The documentation's pur
- Optional source lines and external links (e.g. wiki URLs). - Optional source lines and external links (e.g. wiki URLs).
- Delegate special-case parsing to functions in `src/utils/register_parsers/` (e.g. `reg_default.ts`, `reg_f0.ts`) when needed. - Delegate special-case parsing to functions in `src/utils/register_parsers/` (e.g. `reg_default.ts`, `reg_f0.ts`) when needed.
### TypeScript Patterns
- No explicity any types.
- Use `const` for constants.
- Use `type` for interfaces.
- No `enum`.
### React / Next.js Patterns ### React / Next.js Patterns
- **Server Components**: - **Server Components**:

View File

@@ -139,7 +139,7 @@ export default function ZxdbExplorer({
<button className="btn btn-primary" type="submit" disabled={loading}>Search</button> <button className="btn btn-primary" type="submit" disabled={loading}>Search</button>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={genreId as any} onChange={(e) => setGenreId(e.target.value === "" ? "" : Number(e.target.value))}> <select className="form-select" value={genreId} onChange={(e) => setGenreId(e.target.value === "" ? "" : Number(e.target.value))}>
<option value="">Genre</option> <option value="">Genre</option>
{genres.map((g) => ( {genres.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option> <option key={g.id} value={g.id}>{g.name}</option>
@@ -147,7 +147,7 @@ export default function ZxdbExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={languageId as any} onChange={(e) => setLanguageId(e.target.value)}> <select className="form-select" value={languageId} onChange={(e) => setLanguageId(e.target.value)}>
<option value="">Language</option> <option value="">Language</option>
{languages.map((l) => ( {languages.map((l) => (
<option key={l.id} value={l.id}>{l.name}</option> <option key={l.id} value={l.id}>{l.name}</option>
@@ -155,7 +155,7 @@ export default function ZxdbExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={machinetypeId as any} onChange={(e) => setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value))}> <select className="form-select" value={machinetypeId} onChange={(e) => setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value))}>
<option value="">Machine</option> <option value="">Machine</option>
{machines.map((m) => ( {machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option> <option key={m.id} value={m.id}>{m.name}</option>
@@ -163,7 +163,7 @@ export default function ZxdbExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={sort} onChange={(e) => setSort(e.target.value as any)}> <select className="form-select" value={sort} onChange={(e) => setSort(e.target.value as "title" | "id_desc")}>
<option value="title">Sort: Title</option> <option value="title">Sort: Title</option>
<option value="id_desc">Sort: Newest</option> <option value="id_desc">Sort: Newest</option>
</select> </select>

View File

@@ -194,7 +194,7 @@ export default function EntriesExplorer({
<button className="btn btn-primary" type="submit" disabled={loading}>Search</button> <button className="btn btn-primary" type="submit" disabled={loading}>Search</button>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={genreId as any} onChange={(e) => { setGenreId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}> <select className="form-select" value={genreId} onChange={(e) => { setGenreId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
<option value="">Genre</option> <option value="">Genre</option>
{genres.map((g) => ( {genres.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option> <option key={g.id} value={g.id}>{g.name}</option>
@@ -202,7 +202,7 @@ export default function EntriesExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={languageId as any} onChange={(e) => { setLanguageId(e.target.value); setPage(1); }}> <select className="form-select" value={languageId} onChange={(e) => { setLanguageId(e.target.value); setPage(1); }}>
<option value="">Language</option> <option value="">Language</option>
{languages.map((l) => ( {languages.map((l) => (
<option key={l.id} value={l.id}>{l.name}</option> <option key={l.id} value={l.id}>{l.name}</option>
@@ -210,7 +210,7 @@ export default function EntriesExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={machinetypeId as any} onChange={(e) => { setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}> <select className="form-select" value={machinetypeId} onChange={(e) => { setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
<option value="">Machine</option> <option value="">Machine</option>
{machines.map((m) => ( {machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option> <option key={m.id} value={m.id}>{m.name}</option>
@@ -218,7 +218,7 @@ export default function EntriesExplorer({
</select> </select>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={sort} onChange={(e) => { setSort(e.target.value as any); setPage(1); }}> <select className="form-select" value={sort} onChange={(e) => { setSort(e.target.value as "title" | "id_desc"); setPage(1); }}>
<option value="title">Sort: Title</option> <option value="title">Sort: Title</option>
<option value="id_desc">Sort: Newest</option> <option value="id_desc">Sort: Newest</option>
</select> </select>

View File

@@ -12,5 +12,5 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
const numericId = Number(id); const numericId = Number(id);
const data = await getEntryById(numericId); const data = await getEntryById(numericId);
// For simplicity, let the client render a Not Found state if null // For simplicity, let the client render a Not Found state if null
return <EntryDetailClient data={data as any} />; return <EntryDetailClient data={data} />;
} }

View File

@@ -13,7 +13,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const genreId = (Array.isArray(sp.genreId) ? sp.genreId[0] : sp.genreId) ?? ""; const genreId = (Array.isArray(sp.genreId) ? sp.genreId[0] : sp.genreId) ?? "";
const languageId = (Array.isArray(sp.languageId) ? sp.languageId[0] : sp.languageId) ?? ""; const languageId = (Array.isArray(sp.languageId) ? sp.languageId[0] : sp.languageId) ?? "";
const machinetypeId = (Array.isArray(sp.machinetypeId) ? sp.machinetypeId[0] : sp.machinetypeId) ?? ""; const machinetypeId = (Array.isArray(sp.machinetypeId) ? sp.machinetypeId[0] : sp.machinetypeId) ?? "";
const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) as any) ?? "id_desc"; const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) ?? "id_desc") as "title" | "id_desc";
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const [initial, genres, langs, machines] = await Promise.all([ const [initial, genres, langs, machines] = await Promise.all([
@@ -33,10 +33,10 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
return ( return (
<EntriesExplorer <EntriesExplorer
initial={initial as any} initial={initial}
initialGenres={genres as any} initialGenres={genres}
initialLanguages={langs as any} initialLanguages={langs}
initialMachines={machines as any} initialMachines={machines}
initialUrlState={{ q, page, genreId, languageId, machinetypeId, sort }} initialUrlState={{ q, page, genreId, languageId, machinetypeId, sort }}
/> />
); );

View File

@@ -12,5 +12,5 @@ export default async function Page({ params, searchParams }: { params: Promise<{
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const initial = await entriesByGenre(numericId, page, 20, q || undefined); const initial = await entriesByGenre(numericId, page, 20, q || undefined);
return <GenreDetailClient id={numericId} initial={initial as any} initialQ={q} />; return <GenreDetailClient id={numericId} initial={initial} initialQ={q} />;
} }

View File

@@ -10,5 +10,5 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const initial = await searchGenres({ q, page, pageSize: 20 }); const initial = await searchGenres({ q, page, pageSize: 20 });
return <GenresSearch initial={initial as any} initialQ={q} />; return <GenresSearch initial={initial} initialQ={q} />;
} }

View File

@@ -20,5 +20,5 @@ export default async function Page({ params, searchParams }: { params: Promise<{
]); ]);
// Let the client component handle the "not found" simple state // Let the client component handle the "not found" simple state
return <LabelDetailClient id={numericId} initial={{ label: label as any, authored: authored as any, published: published as any }} initialTab={tab} initialQ={q} />; return <LabelDetailClient id={numericId} initial={{ label: label, authored: authored, published: published }} initialTab={tab} initialQ={q} />;
} }

View File

@@ -11,5 +11,5 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const initial = await searchLabels({ q, page, pageSize: 20 }); const initial = await searchLabels({ q, page, pageSize: 20 });
return <LabelsSearch initial={initial as any} initialQ={q} />; return <LabelsSearch initial={initial} initialQ={q} />;
} }

View File

@@ -11,5 +11,5 @@ export default async function Page({ params, searchParams }: { params: Promise<{
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const initial = await entriesByLanguage(id, page, 20, q || undefined); const initial = await entriesByLanguage(id, page, 20, q || undefined);
return <LanguageDetailClient id={id} initial={initial as any} initialQ={q} />; return <LanguageDetailClient id={id} initial={initial} initialQ={q} />;
} }

View File

@@ -11,5 +11,5 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const initial = await searchLanguages({ q, page, pageSize: 20 }); const initial = await searchLanguages({ q, page, pageSize: 20 });
return <LanguagesSearch initial={initial as any} initialQ={q} />; return <LanguagesSearch initial={initial} initialQ={q} />;
} }

View File

@@ -11,5 +11,5 @@ export default async function Page({ params, searchParams }: { params: Promise<{
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const initial = await entriesByMachinetype(numericId, page, 20, q || undefined); const initial = await entriesByMachinetype(numericId, page, 20, q || undefined);
return <MachineTypeDetailClient id={numericId} initial={initial as any} initialQ={q} />; return <MachineTypeDetailClient id={numericId} initial={initial} initialQ={q} />;
} }

View File

@@ -10,5 +10,5 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1); const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
const initial = await searchMachinetypes({ q, page, pageSize: 20 }); const initial = await searchMachinetypes({ q, page, pageSize: 20 });
return <MachineTypesSearch initial={initial as any} initialQ={q} />; return <MachineTypesSearch initial={initial} initialQ={q} />;
} }

View File

@@ -286,7 +286,7 @@ export default function ReleasesExplorer({
<label className="form-check-label" htmlFor="demoCheck">Demo only</label> <label className="form-check-label" htmlFor="demoCheck">Demo only</label>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<select className="form-select" value={sort} onChange={(e) => { setSort(e.target.value as any); setPage(1); }}> <select className="form-select" value={sort} onChange={(e) => { setSort(e.target.value); setPage(1); }}>
<option value="year_desc">Sort: Newest</option> <option value="year_desc">Sort: Newest</option>
<option value="year_asc">Sort: Oldest</option> <option value="year_asc">Sort: Oldest</option>
<option value="title">Sort: Title</option> <option value="title">Sort: Title</option>

View File

@@ -13,7 +13,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? ""; const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
const yearStr = (Array.isArray(sp.year) ? sp.year[0] : sp.year) ?? ""; const yearStr = (Array.isArray(sp.year) ? sp.year[0] : sp.year) ?? "";
const year = yearStr ? Number(yearStr) : undefined; const year = yearStr ? Number(yearStr) : undefined;
const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) as any) ?? "year_desc"; const sort = ((Array.isArray(sp.sort) ? sp.sort[0] : sp.sort) ?? "year_desc") as "year_desc" | "year_asc" | "title" | "entry_id_desc";
const dLanguageId = (Array.isArray(sp.dLanguageId) ? sp.dLanguageId[0] : sp.dLanguageId) ?? ""; const dLanguageId = (Array.isArray(sp.dLanguageId) ? sp.dLanguageId[0] : sp.dLanguageId) ?? "";
const dMachinetypeIdStr = (Array.isArray(sp.dMachinetypeId) ? sp.dMachinetypeId[0] : sp.dMachinetypeId) ?? ""; const dMachinetypeIdStr = (Array.isArray(sp.dMachinetypeId) ? sp.dMachinetypeId[0] : sp.dMachinetypeId) ?? "";
const dMachinetypeId = dMachinetypeIdStr ? Number(dMachinetypeIdStr) : undefined; const dMachinetypeId = dMachinetypeIdStr ? Number(dMachinetypeIdStr) : undefined;
@@ -34,7 +34,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
return ( return (
<ReleasesExplorer <ReleasesExplorer
initial={initialPlain as any} initial={initialPlain}
initialUrlState={{ q, page, year: yearStr, sort, dLanguageId, dMachinetypeId: dMachinetypeIdStr, filetypeId: filetypeIdStr, schemetypeId, sourcetypeId, casetypeId, isDemo: isDemoStr }} initialUrlState={{ q, page, year: yearStr, sort, dLanguageId, dMachinetypeId: dMachinetypeIdStr, filetypeId: filetypeIdStr, schemetypeId, sourcetypeId, casetypeId, isDemo: isDemoStr }}
/> />
); );

View File

@@ -15,7 +15,8 @@ function formatErrors(errors: z.ZodFormattedError<Map<string, string>, string>)
return Object.entries(errors) return Object.entries(errors)
.map(([name, value]) => { .map(([name, value]) => {
if (value && "_errors" in value) { if (value && "_errors" in value) {
return `${name}: ${(value as any)._errors.join(", ")}`; const errs = (value as z.ZodFormattedError<string>)._errors;
return `${name}: ${errs.join(", ")}`;
} }
return `${name}: invalid`; return `${name}: invalid`;
}) })

View File

@@ -73,16 +73,22 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
if (q.length === 0) { if (q.length === 0) {
// Default listing: return first page by id desc (no guaranteed ordering field; using id) // Default listing: return first page by id desc (no guaranteed ordering field; using id)
// Apply optional filters even without q // Apply optional filters even without q
const whereClauses = [ const whereClauses: Array<ReturnType<typeof eq>> = [];
params.genreId ? eq(entries.genretypeId, params.genreId as any) : undefined, if (typeof params.genreId === "number") {
params.languageId ? eq(entries.languageId, params.languageId as any) : undefined, whereClauses.push(eq(entries.genretypeId, params.genreId));
params.machinetypeId ? eq(entries.machinetypeId, params.machinetypeId as any) : undefined, }
].filter(Boolean) as any[]; if (typeof params.languageId === "string") {
whereClauses.push(eq(entries.languageId, params.languageId));
}
if (typeof params.machinetypeId === "number") {
whereClauses.push(eq(entries.machinetypeId, params.machinetypeId));
}
const whereExpr = whereClauses.length ? and(...whereClauses) : undefined; const whereExpr = whereClauses.length ? and(...whereClauses) : undefined;
const [items, countRows] = await Promise.all([ const [items, countRows] = await Promise.all([
db (async () => {
let q1 = db
.select({ .select({
id: entries.id, id: entries.id,
title: entries.title, title: entries.title,
@@ -93,19 +99,21 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId));
.where(whereExpr as any) if (whereExpr) q1 = q1.where(whereExpr);
return q1
.orderBy(sort === "id_desc" ? desc(entries.id) : entries.title) .orderBy(sort === "id_desc" ? desc(entries.id) : entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset), .offset(offset);
})(),
db db
.select({ total: sql<number>`count(*)` }) .select({ total: sql<number>`count(*)` })
.from(entries) .from(entries)
.where(whereExpr as any) as unknown as Promise<{ total: number }[]>, .where(whereExpr ?? sql`true`),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
@@ -131,15 +139,15 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
}) })
.from(searchByTitles) .from(searchByTitles)
.innerJoin(entries, eq(entries.id, searchByTitles.entryId)) .innerJoin(entries, eq(entries.id, searchByTitles.entryId))
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(like(searchByTitles.entryTitle, pattern)) .where(like(searchByTitles.entryTitle, pattern))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(sort === "id_desc" ? desc(entries.id) : entries.title) .orderBy(sort === "id_desc" ? desc(entries.id) : entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export interface LabelSummary { export interface LabelSummary {
@@ -235,9 +243,9 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
issueId: entries.issueId, issueId: entries.issueId,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId as any)) .leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
.where(eq(entries.id, id)), .where(eq(entries.id, id)),
db db
.select({ id: labels.id, name: labels.name, labeltypeId: labels.labeltypeId }) .select({ id: labels.id, name: labels.name, labeltypeId: labels.labeltypeId })
@@ -279,13 +287,36 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
typeName: filetypes.name, typeName: filetypes.name,
}) })
.from(files) .from(files)
.innerJoin(filetypes, eq(filetypes.id, files.filetypeId as any)) .innerJoin(filetypes, eq(filetypes.id, files.filetypeId))
.where(eq(files.issueId as any, base.issueId as any))) as any; .where(eq(files.issueId, base.issueId)));
} }
let releaseRows: any[] = []; type ReleaseRow = { releaseSeq: number | string; year: number | string | null };
let downloadRows: any[] = []; type DownloadRow = {
let downloadFlatRows: any[] = []; id: number | string;
releaseSeq: number | string;
link: string;
size: number | string | null;
md5: string | null;
comments: string | null;
isDemo: number | boolean | null;
filetypeId: number | string;
filetypeName: string;
dlLangId: string | null;
dlLangName: string | null;
dlMachineId: number | string | null;
dlMachineName: string | null;
schemeId: string | null;
schemeName: string | null;
sourceId: string | null;
sourceName: string | null;
caseId: string | null;
caseName: string | null;
year: number | string | null;
};
let releaseRows: ReleaseRow[] = [];
let downloadRows: DownloadRow[] = [];
let downloadFlatRows: DownloadRow[] = [];
// Fetch releases for this entry (optional; ignore if table missing) // Fetch releases for this entry (optional; ignore if table missing)
try { try {
@@ -295,7 +326,7 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
year: releases.releaseYear, year: releases.releaseYear,
}) })
.from(releases) .from(releases)
.where(eq(releases.entryId as any, id as any))) as any; .where(eq(releases.entryId, id)));
} catch { } catch {
releaseRows = []; releaseRows = [];
} }
@@ -326,13 +357,13 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
year: downloads.releaseYear, year: downloads.releaseYear,
}) })
.from(downloads) .from(downloads)
.innerJoin(filetypes, eq(filetypes.id as any, downloads.filetypeId as any)) .innerJoin(filetypes, eq(filetypes.id, downloads.filetypeId))
.leftJoin(languages, eq(languages.id as any, downloads.languageId as any)) .leftJoin(languages, eq(languages.id, downloads.languageId))
.leftJoin(machinetypes, eq(machinetypes.id as any, downloads.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, downloads.machinetypeId))
.leftJoin(schemetypes, eq(schemetypes.id as any, downloads.schemetypeId as any)) .leftJoin(schemetypes, eq(schemetypes.id, downloads.schemetypeId))
.leftJoin(sourcetypes, eq(sourcetypes.id as any, downloads.sourcetypeId as any)) .leftJoin(sourcetypes, eq(sourcetypes.id, downloads.sourcetypeId))
.leftJoin(casetypes, eq(casetypes.id as any, downloads.casetypeId as any)) .leftJoin(casetypes, eq(casetypes.id, downloads.casetypeId))
.where(eq(downloads.entryId as any, id as any))) as any; .where(eq(downloads.entryId, id)));
} catch { } catch {
downloadRows = []; downloadRows = [];
} }
@@ -340,7 +371,7 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
// Flat list: same rows mapped, independent of releases // Flat list: same rows mapped, independent of releases
downloadFlatRows = downloadRows; downloadFlatRows = downloadRows;
const downloadsBySeq = new Map<number, any[]>(); const downloadsBySeq = new Map<number, DownloadRow[]>();
for (const row of downloadRows) { for (const row of downloadRows) {
const arr = downloadsBySeq.get(row.releaseSeq) ?? []; const arr = downloadsBySeq.get(row.releaseSeq) ?? [];
arr.push(row); arr.push(row);
@@ -350,14 +381,14 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
// Build a map of downloads grouped by release_seq // Build a map of downloads grouped by release_seq
// Then ensure we create "synthetic" release groups for any release_seq // Then ensure we create "synthetic" release groups for any release_seq
// that appears in downloads but has no corresponding releases row. // that appears in downloads but has no corresponding releases row.
const releasesData = releaseRows.map((r: any) => ({ const releasesData = releaseRows.map((r) => ({
releaseSeq: Number(r.releaseSeq), releaseSeq: Number(r.releaseSeq),
type: { id: null, name: null }, type: { id: null, name: null },
language: { id: null, name: null }, language: { id: null, name: null },
machinetype: { id: null, name: null }, machinetype: { id: null, name: null },
year: (r.year as any) ?? null, year: (r.year) ?? null,
comments: null, comments: null,
downloads: (downloadsBySeq.get(Number(r.releaseSeq)) ?? []).map((d: any) => ({ downloads: (downloadsBySeq.get(Number(r.releaseSeq)) ?? []).map((d) => ({
id: d.id, id: d.id,
link: d.link, link: d.link,
size: d.size ?? null, size: d.size ?? null,
@@ -365,12 +396,12 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
comments: d.comments ?? null, comments: d.comments ?? null,
isDemo: !!d.isDemo, isDemo: !!d.isDemo,
type: { id: d.filetypeId, name: d.filetypeName }, type: { id: d.filetypeId, name: d.filetypeName },
language: { id: (d.dlLangId as any) ?? null, name: (d.dlLangName as any) ?? null }, language: { id: (d.dlLangId) ?? null, name: (d.dlLangName) ?? null },
machinetype: { id: (d.dlMachineId as any) ?? null, name: (d.dlMachineName as any) ?? null }, machinetype: { id: (d.dlMachineId) ?? null, name: (d.dlMachineName) ?? null },
scheme: { id: (d.schemeId as any) ?? null, name: (d.schemeName as any) ?? null }, scheme: { id: (d.schemeId) ?? null, name: (d.schemeName) ?? null },
source: { id: (d.sourceId as any) ?? null, name: (d.sourceName as any) ?? null }, source: { id: (d.sourceId) ?? null, name: (d.sourceName) ?? null },
case: { id: (d.caseId as any) ?? null, name: (d.caseName as any) ?? null }, case: { id: (d.caseId) ?? null, name: (d.caseName) ?? null },
year: (d.year as any) ?? null, year: (d.year) ?? null,
})), })),
})); }));
@@ -382,17 +413,17 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
return { return {
id: base.id, id: base.id,
title: base.title, title: base.title,
isXrated: base.isXrated as any, isXrated: base.isXrated,
machinetype: { id: (base.machinetypeId as any) ?? null, name: (base.machinetypeName as any) ?? null }, machinetype: { id: (base.machinetypeId) ?? null, name: (base.machinetypeName) ?? null },
language: { id: (base.languageId as any) ?? null, name: (base.languageName as any) ?? null }, language: { id: (base.languageId) ?? null, name: (base.languageName) ?? null },
genre: { id: (base.genreId as any) ?? null, name: (base.genreName as any) ?? null }, genre: { id: (base.genreId) ?? null, name: (base.genreName) ?? null },
authors: authorRows as any, authors: authorRows,
publishers: publisherRows as any, publishers: publisherRows,
maxPlayers: (base.maxPlayers as any) ?? undefined, maxPlayers: (base.maxPlayers) ?? undefined,
availabletypeId: (base.availabletypeId as any) ?? undefined, availabletypeId: (base.availabletypeId) ?? undefined,
withoutLoadScreen: (base.withoutLoadScreen as any) ?? undefined, withoutLoadScreen: (base.withoutLoadScreen) ?? undefined,
withoutInlay: (base.withoutInlay as any) ?? undefined, withoutInlay: (base.withoutInlay) ?? undefined,
issueId: (base.issueId as any) ?? undefined, issueId: (base.issueId) ?? undefined,
files: files:
fileRows.length > 0 fileRows.length > 0
? fileRows.map((f) => ({ ? fileRows.map((f) => ({
@@ -405,7 +436,7 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
})) }))
: [], : [],
releases: releasesData, releases: releasesData,
downloadsFlat: downloadFlatRows.map((d: any) => ({ downloadsFlat: downloadFlatRows.map((d) => ({
id: d.id, id: d.id,
link: d.link, link: d.link,
size: d.size ?? null, size: d.size ?? null,
@@ -413,12 +444,12 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
comments: d.comments ?? null, comments: d.comments ?? null,
isDemo: !!d.isDemo, isDemo: !!d.isDemo,
type: { id: d.filetypeId, name: d.filetypeName }, type: { id: d.filetypeId, name: d.filetypeName },
language: { id: (d.dlLangId as any) ?? null, name: (d.dlLangName as any) ?? null }, language: { id: (d.dlLangId) ?? null, name: (d.dlLangName) ?? null },
machinetype: { id: (d.dlMachineId as any) ?? null, name: (d.dlMachineName as any) ?? null }, machinetype: { id: (d.dlMachineId) ?? null, name: (d.dlMachineName) ?? null },
scheme: { id: (d.schemeId as any) ?? null, name: (d.schemeName as any) ?? null }, scheme: { id: (d.schemeId) ?? null, name: (d.schemeName) ?? null },
source: { id: (d.sourceId as any) ?? null, name: (d.sourceName as any) ?? null }, source: { id: (d.sourceId) ?? null, name: (d.sourceName) ?? null },
case: { id: (d.caseId as any) ?? null, name: (d.caseName as any) ?? null }, case: { id: (d.caseId) ?? null, name: (d.caseName) ?? null },
year: (d.year as any) ?? null, year: (d.year) ?? null,
releaseSeq: Number(d.releaseSeq), releaseSeq: Number(d.releaseSeq),
})), })),
}; };
@@ -448,33 +479,33 @@ export async function searchLabels(params: LabelSearchParams): Promise<PagedResu
.from(labels) as unknown as Promise<{ total: number }[]>, .from(labels) as unknown as Promise<{ total: number }[]>,
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
// Using helper search_by_names for efficiency // Using helper search_by_names for efficiency
const pattern = `%${q}%`; const pattern = `%${q}%`;
const countRows = await db const countRows = await db
.select({ total: sql<number>`count(distinct ${sql.identifier("label_id")})` }) .select({ total: sql<number>`count(distinct ${sql.identifier("label_id")})` })
.from(sql`search_by_names` as any) .from(sql`search_by_names`)
.where(like(sql.identifier("label_name") as any, pattern)); .where(like(sql.identifier("label_name"), pattern));
const total = Number(countRows[0]?.total ?? 0); const total = Number(countRows[0]?.total ?? 0);
const items = await db const items = await db
.select({ id: labels.id, name: labels.name, labeltypeId: labels.labeltypeId }) .select({ id: labels.id, name: labels.name, labeltypeId: labels.labeltypeId })
.from(sql`search_by_names` as any) .from(sql`search_by_names`)
.innerJoin(labels, eq(labels.id as any, sql.identifier("label_id") as any)) .innerJoin(labels, eq(labels.id, sql.identifier("label_id")))
.where(like(sql.identifier("label_name") as any, pattern)) .where(like(sql.identifier("label_name"), pattern))
.groupBy(labels.id) .groupBy(labels.id)
.orderBy(labels.name) .orderBy(labels.name)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
export async function getLabelById(id: number): Promise<LabelDetail | null> { export async function getLabelById(id: number): Promise<LabelDetail | null> {
const rows = await db.select().from(labels).where(eq(labels.id, id)).limit(1); const rows = await db.select().from(labels).where(eq(labels.id, id)).limit(1);
return (rows[0] as any) ?? null; return (rows[0]) ?? null;
} }
export interface LabelContribsParams { export interface LabelContribsParams {
@@ -508,15 +539,15 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
}) })
.from(authors) .from(authors)
.innerJoin(entries, eq(entries.id, authors.entryId)) .innerJoin(entries, eq(entries.id, authors.entryId))
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(eq(authors.labelId, labelId)) .where(eq(authors.labelId, labelId))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
@@ -524,8 +555,8 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
.select({ total: sql<number>`count(distinct ${entries.id})` }) .select({ total: sql<number>`count(distinct ${entries.id})` })
.from(authors) .from(authors)
.innerJoin(entries, eq(entries.id, authors.entryId)) .innerJoin(entries, eq(entries.id, authors.entryId))
.where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any); .where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`));
const total = Number((countRows as any)[0]?.total ?? 0); const total = Number((countRows)[0]?.total ?? 0);
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -538,15 +569,15 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
}) })
.from(authors) .from(authors)
.innerJoin(entries, eq(entries.id, authors.entryId)) .innerJoin(entries, eq(entries.id, authors.entryId))
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any) .where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
export async function getLabelPublishedEntries(labelId: number, params: LabelContribsParams): Promise<PagedResult<SearchResultItem>> { export async function getLabelPublishedEntries(labelId: number, params: LabelContribsParams): Promise<PagedResult<SearchResultItem>> {
@@ -574,15 +605,15 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
}) })
.from(publishers) .from(publishers)
.innerJoin(entries, eq(entries.id, publishers.entryId)) .innerJoin(entries, eq(entries.id, publishers.entryId))
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(eq(publishers.labelId, labelId)) .where(eq(publishers.labelId, labelId))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
@@ -590,8 +621,8 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
.select({ total: sql<number>`count(distinct ${entries.id})` }) .select({ total: sql<number>`count(distinct ${entries.id})` })
.from(publishers) .from(publishers)
.innerJoin(entries, eq(entries.id, publishers.entryId)) .innerJoin(entries, eq(entries.id, publishers.entryId))
.where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any); .where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`));
const total = Number((countRows as any)[0]?.total ?? 0); const total = Number((countRows)[0]?.total ?? 0);
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -604,15 +635,15 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
}) })
.from(publishers) .from(publishers)
.innerJoin(entries, eq(entries.id, publishers.entryId)) .innerJoin(entries, eq(entries.id, publishers.entryId))
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any) .where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items: items, page, pageSize, total };
} }
// ----- Lookups lists and category browsing ----- // ----- Lookups lists and category browsing -----
@@ -646,10 +677,10 @@ export async function searchLanguages(params: SimpleSearchParams) {
if (!q) { if (!q) {
const [items, countRows] = await Promise.all([ const [items, countRows] = await Promise.all([
db.select().from(languages).orderBy(languages.name).limit(pageSize).offset(offset), db.select().from(languages).orderBy(languages.name).limit(pageSize).offset(offset),
db.select({ total: sql<number>`count(*)` }).from(languages) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(languages),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
const pattern = `%${q}%`; const pattern = `%${q}%`;
@@ -657,14 +688,14 @@ export async function searchLanguages(params: SimpleSearchParams) {
db db
.select() .select()
.from(languages) .from(languages)
.where(like(languages.name as any, pattern)) .where(like(languages.name, pattern))
.orderBy(languages.name) .orderBy(languages.name)
.limit(pageSize) .limit(pageSize)
.offset(offset), .offset(offset),
db.select({ total: sql<number>`count(*)` }).from(languages).where(like(languages.name as any, pattern)) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(languages).where(like(languages.name, pattern)),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export async function searchGenres(params: SimpleSearchParams) { export async function searchGenres(params: SimpleSearchParams) {
@@ -676,10 +707,10 @@ export async function searchGenres(params: SimpleSearchParams) {
if (!q) { if (!q) {
const [items, countRows] = await Promise.all([ const [items, countRows] = await Promise.all([
db.select().from(genretypes).orderBy(genretypes.name).limit(pageSize).offset(offset), db.select().from(genretypes).orderBy(genretypes.name).limit(pageSize).offset(offset),
db.select({ total: sql<number>`count(*)` }).from(genretypes) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(genretypes),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
const pattern = `%${q}%`; const pattern = `%${q}%`;
@@ -687,14 +718,14 @@ export async function searchGenres(params: SimpleSearchParams) {
db db
.select() .select()
.from(genretypes) .from(genretypes)
.where(like(genretypes.name as any, pattern)) .where(like(genretypes.name, pattern))
.orderBy(genretypes.name) .orderBy(genretypes.name)
.limit(pageSize) .limit(pageSize)
.offset(offset), .offset(offset),
db.select({ total: sql<number>`count(*)` }).from(genretypes).where(like(genretypes.name as any, pattern)) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(genretypes).where(like(genretypes.name, pattern)),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export async function searchMachinetypes(params: SimpleSearchParams) { export async function searchMachinetypes(params: SimpleSearchParams) {
@@ -706,10 +737,10 @@ export async function searchMachinetypes(params: SimpleSearchParams) {
if (!q) { if (!q) {
const [items, countRows] = await Promise.all([ const [items, countRows] = await Promise.all([
db.select().from(machinetypes).orderBy(machinetypes.name).limit(pageSize).offset(offset), db.select().from(machinetypes).orderBy(machinetypes.name).limit(pageSize).offset(offset),
db.select({ total: sql<number>`count(*)` }).from(machinetypes) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(machinetypes),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
const pattern = `%${q}%`; const pattern = `%${q}%`;
@@ -717,14 +748,14 @@ export async function searchMachinetypes(params: SimpleSearchParams) {
db db
.select() .select()
.from(machinetypes) .from(machinetypes)
.where(like(machinetypes.name as any, pattern)) .where(like(machinetypes.name, pattern))
.orderBy(machinetypes.name) .orderBy(machinetypes.name)
.limit(pageSize) .limit(pageSize)
.offset(offset), .offset(offset),
db.select({ total: sql<number>`count(*)` }).from(machinetypes).where(like(machinetypes.name as any, pattern)) as unknown as Promise<{ total: number }[]>, db.select({ total: sql<number>`count(*)` }).from(machinetypes).where(like(machinetypes.name, pattern)),
]); ]);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number((countRows as { total: number }[])[0]?.total ?? 0);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export async function entriesByGenre( export async function entriesByGenre(
@@ -737,10 +768,10 @@ export async function entriesByGenre(
const hasQ = !!(q && q.trim()); const hasQ = !!(q && q.trim());
if (!hasQ) { if (!hasQ) {
const countRows = (await db const countRows = await db
.select({ total: sql<number>`count(*)` }) .select({ total: sql<number>`count(*)` })
.from(entries) .from(entries)
.where(eq(entries.genretypeId, genreId as any))) as unknown as { total: number }[]; .where(eq(entries.genretypeId, genreId));
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -752,21 +783,21 @@ export async function entriesByGenre(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(eq(entries.genretypeId, genreId as any)) .where(eq(entries.genretypeId, genreId))
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) }; return { items, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
} }
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
const countRows = await db const countRows = await db
.select({ total: sql<number>`count(distinct ${entries.id})` }) .select({ total: sql<number>`count(distinct ${entries.id})` })
.from(entries) .from(entries)
.where(and(eq(entries.genretypeId, genreId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any); .where(and(eq(entries.genretypeId, genreId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`));
const total = Number((countRows as any)[0]?.total ?? 0); const total = Number(countRows[0]?.total ?? 0);
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -778,14 +809,14 @@ export async function entriesByGenre(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(and(eq(entries.genretypeId, genreId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any) .where(and(eq(entries.genretypeId, genreId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export async function entriesByLanguage( export async function entriesByLanguage(
@@ -798,10 +829,10 @@ export async function entriesByLanguage(
const hasQ = !!(q && q.trim()); const hasQ = !!(q && q.trim());
if (!hasQ) { if (!hasQ) {
const countRows = (await db const countRows = await db
.select({ total: sql<number>`count(*)` }) .select({ total: sql<number>`count(*)` })
.from(entries) .from(entries)
.where(eq(entries.languageId, langId as any))) as unknown as { total: number }[]; .where(eq(entries.languageId, langId));
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -813,21 +844,21 @@ export async function entriesByLanguage(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(eq(entries.languageId, langId as any)) .where(eq(entries.languageId, langId))
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) }; return { items, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
} }
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
const countRows = await db const countRows = await db
.select({ total: sql<number>`count(distinct ${entries.id})` }) .select({ total: sql<number>`count(distinct ${entries.id})` })
.from(entries) .from(entries)
.where(and(eq(entries.languageId, langId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any); .where(and(eq(entries.languageId, langId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`));
const total = Number((countRows as any)[0]?.total ?? 0); const total = Number(countRows[0]?.total ?? 0);
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -839,14 +870,14 @@ export async function entriesByLanguage(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(and(eq(entries.languageId, langId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any) .where(and(eq(entries.languageId, langId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
export async function entriesByMachinetype( export async function entriesByMachinetype(
@@ -859,10 +890,10 @@ export async function entriesByMachinetype(
const hasQ = !!(q && q.trim()); const hasQ = !!(q && q.trim());
if (!hasQ) { if (!hasQ) {
const countRows = (await db const countRows = await db
.select({ total: sql<number>`count(*)` }) .select({ total: sql<number>`count(*)` })
.from(entries) .from(entries)
.where(eq(entries.machinetypeId, mtId as any))) as unknown as { total: number }[]; .where(eq(entries.machinetypeId, mtId));
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -874,21 +905,21 @@ export async function entriesByMachinetype(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(eq(entries.machinetypeId, mtId as any)) .where(eq(entries.machinetypeId, mtId))
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) }; return { items, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
} }
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
const countRows = await db const countRows = await db
.select({ total: sql<number>`count(distinct ${entries.id})` }) .select({ total: sql<number>`count(distinct ${entries.id})` })
.from(entries) .from(entries)
.where(and(eq(entries.machinetypeId, mtId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any); .where(and(eq(entries.machinetypeId, mtId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`));
const total = Number((countRows as any)[0]?.total ?? 0); const total = Number(countRows[0]?.total ?? 0);
const items = await db const items = await db
.select({ .select({
id: entries.id, id: entries.id,
@@ -900,14 +931,14 @@ export async function entriesByMachinetype(
languageName: languages.name, languageName: languages.name,
}) })
.from(entries) .from(entries)
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any)) .leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
.leftJoin(languages, eq(languages.id, entries.languageId as any)) .leftJoin(languages, eq(languages.id, entries.languageId))
.where(and(eq(entries.machinetypeId, mtId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any) .where(and(eq(entries.machinetypeId, mtId), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
.groupBy(entries.id) .groupBy(entries.id)
.orderBy(entries.title) .orderBy(entries.title)
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
return { items: items as any, page, pageSize, total }; return { items, page, pageSize, total };
} }
// ----- Facets for search ----- // ----- Facets for search -----
@@ -917,7 +948,7 @@ export async function getEntryFacets(params: SearchParams): Promise<EntryFacets>
const pattern = q ? `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%` : null; const pattern = q ? `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%` : null;
// Build base WHERE SQL snippet considering q + filters // Build base WHERE SQL snippet considering q + filters
const whereParts: any[] = []; const whereParts: Array<ReturnType<typeof sql>> = [];
if (pattern) { if (pattern) {
whereParts.push(sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`); whereParts.push(sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`);
} }
@@ -925,7 +956,7 @@ export async function getEntryFacets(params: SearchParams): Promise<EntryFacets>
if (params.languageId) whereParts.push(sql`${entries.languageId} = ${params.languageId}`); if (params.languageId) whereParts.push(sql`${entries.languageId} = ${params.languageId}`);
if (params.machinetypeId) whereParts.push(sql`${entries.machinetypeId} = ${params.machinetypeId}`); if (params.machinetypeId) whereParts.push(sql`${entries.machinetypeId} = ${params.machinetypeId}`);
const whereSql = whereParts.length ? sql.join([sql`where `, sql.join(whereParts as any, sql` and `)], sql``) : sql``; const whereSql = whereParts.length ? sql.join([sql`where `, sql.join(whereParts, sql` and `)], sql``) : sql``;
// Genres facet // Genres facet
const genresRows = await db.execute(sql` const genresRows = await db.execute(sql`
@@ -935,7 +966,7 @@ export async function getEntryFacets(params: SearchParams): Promise<EntryFacets>
${whereSql} ${whereSql}
group by e.genretype_id, gt.text group by e.genretype_id, gt.text
order by count desc, name asc order by count desc, name asc
`) as any; `);
// Languages facet // Languages facet
const langRows = await db.execute(sql` const langRows = await db.execute(sql`
@@ -945,7 +976,7 @@ export async function getEntryFacets(params: SearchParams): Promise<EntryFacets>
${whereSql} ${whereSql}
group by e.language_id, l.text group by e.language_id, l.text
order by count desc, name asc order by count desc, name asc
`) as any; `);
// Machinetypes facet // Machinetypes facet
const mtRows = await db.execute(sql` const mtRows = await db.execute(sql`
@@ -955,12 +986,19 @@ export async function getEntryFacets(params: SearchParams): Promise<EntryFacets>
${whereSql} ${whereSql}
group by e.machinetype_id, m.text group by e.machinetype_id, m.text
order by count desc, name asc order by count desc, name asc
`) as any; `);
type FacetRow = { id: number | string | null; name: string | null; count: number | string };
return { return {
genres: (genresRows as any[]).map((r: any) => ({ id: Number(r.id), name: r.name ?? "(none)", count: Number(r.count) })).filter((r) => !!r.id), genres: (genresRows as unknown as FacetRow[])
languages: (langRows as any[]).map((r: any) => ({ id: String(r.id), name: r.name ?? "(none)", count: Number(r.count) })).filter((r) => !!r.id), .map((r) => ({ id: Number(r.id), name: r.name ?? "(none)", count: Number(r.count) }))
machinetypes: (mtRows as any[]).map((r: any) => ({ id: Number(r.id), name: r.name ?? "(none)", count: Number(r.count) })).filter((r) => !!r.id), .filter((r) => !!r.id),
languages: (langRows as unknown as FacetRow[])
.map((r) => ({ id: String(r.id), name: r.name ?? "(none)", count: Number(r.count) }))
.filter((r) => !!r.id),
machinetypes: (mtRows as unknown as FacetRow[])
.map((r) => ({ id: Number(r.id), name: r.name ?? "(none)", count: Number(r.count) }))
.filter((r) => !!r.id),
}; };
} }
@@ -996,20 +1034,20 @@ export async function searchReleases(params: ReleaseSearchParams): Promise<Paged
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
// Build WHERE conditions in Drizzle QB // Build WHERE conditions in Drizzle QB
const wherePartsQB: any[] = []; const wherePartsQB: Array<ReturnType<typeof sql>> = [];
if (q) { if (q) {
const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`; const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
wherePartsQB.push(sql`${releases.entryId} in (select ${searchByTitles.entryId} from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`); wherePartsQB.push(sql`${releases.entryId} in (select ${searchByTitles.entryId} from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`);
} }
if (params.year != null) { if (params.year != null) {
wherePartsQB.push(eq(releases.releaseYear as any, params.year as any)); wherePartsQB.push(eq(releases.releaseYear, params.year));
} }
// Optional filters via downloads table: use EXISTS for performance and correctness // Optional filters via downloads table: use EXISTS for performance and correctness
// IMPORTANT: when hand-writing SQL with an aliased table, we must render // IMPORTANT: when hand-writing SQL with an aliased table, we must render
// "from downloads as d" explicitly; using only the alias identifier ("d") // "from downloads as d" explicitly; using only the alias identifier ("d")
// would produce "from `d`" which MySQL interprets as a literal table. // would produce "from `d`" which MySQL interprets as a literal table.
const dlConds: any[] = []; const dlConds: Array<ReturnType<typeof sql>> = [];
if (params.dLanguageId) dlConds.push(sql`d.language_id = ${params.dLanguageId}`); if (params.dLanguageId) dlConds.push(sql`d.language_id = ${params.dLanguageId}`);
if (params.dMachinetypeId != null) dlConds.push(sql`d.machinetype_id = ${params.dMachinetypeId}`); if (params.dMachinetypeId != null) dlConds.push(sql`d.machinetype_id = ${params.dMachinetypeId}`);
if (params.filetypeId != null) dlConds.push(sql`d.filetype_id = ${params.filetypeId}`); if (params.filetypeId != null) dlConds.push(sql`d.filetype_id = ${params.filetypeId}`);
@@ -1024,34 +1062,41 @@ export async function searchReleases(params: ReleaseSearchParams): Promise<Paged
sql`d.release_seq = ${releases.releaseSeq}`, sql`d.release_seq = ${releases.releaseSeq}`,
...dlConds, ...dlConds,
]; ];
wherePartsQB.push( wherePartsQB.push(sql`exists (select 1 from ${downloads} as d where ${sql.join(baseConds, sql` and `)})`);
sql`exists (select 1 from ${downloads} as d where ${sql.join(baseConds as any, sql` and `)})`
);
} }
const whereExpr = wherePartsQB.length ? and(...(wherePartsQB as any)) : undefined; const whereExpr = wherePartsQB.length ? and(...wherePartsQB) : undefined;
// Count total // Count total
const countRows = (await db const countRows = await db
.select({ total: sql<number>`count(*)` }) .select({ total: sql<number>`count(*)` })
.from(releases) .from(releases)
.where(whereExpr as any)) as unknown as { total: number }[]; .where(whereExpr ?? sql`true`);
const total = Number(countRows?.[0]?.total ?? 0); const total = Number(countRows?.[0]?.total ?? 0);
// Rows via Drizzle QB to avoid tuple/field leakage // Rows via Drizzle QB to avoid tuple/field leakage
const orderByParts: any[] = []; let orderBy1;
let orderBy2;
let orderBy3;
switch (params.sort) { switch (params.sort) {
case "year_asc": case "year_asc":
orderByParts.push(asc(releases.releaseYear as any), asc(releases.entryId as any), asc(releases.releaseSeq as any)); orderBy1 = asc(releases.releaseYear);
orderBy2 = asc(releases.entryId);
orderBy3 = asc(releases.releaseSeq);
break; break;
case "title": case "title":
orderByParts.push(asc(entries.title as any), desc(releases.releaseYear as any), asc(releases.releaseSeq as any)); orderBy1 = asc(entries.title);
orderBy2 = desc(releases.releaseYear);
orderBy3 = asc(releases.releaseSeq);
break; break;
case "entry_id_desc": case "entry_id_desc":
orderByParts.push(desc(releases.entryId as any), desc(releases.releaseSeq as any)); orderBy1 = desc(releases.entryId);
orderBy2 = desc(releases.releaseSeq);
break; break;
case "year_desc": case "year_desc":
default: default:
orderByParts.push(desc(releases.releaseYear as any), desc(releases.entryId as any), desc(releases.releaseSeq as any)); orderBy1 = desc(releases.releaseYear);
orderBy2 = desc(releases.entryId);
orderBy3 = desc(releases.releaseSeq);
break; break;
} }
@@ -1063,14 +1108,14 @@ export async function searchReleases(params: ReleaseSearchParams): Promise<Paged
year: releases.releaseYear, year: releases.releaseYear,
}) })
.from(releases) .from(releases)
.leftJoin(entries, eq(entries.id as any, releases.entryId as any)) .leftJoin(entries, eq(entries.id, releases.entryId))
.where(whereExpr as any) .where(whereExpr ?? sql`true`)
.orderBy(...(orderByParts as any)) .orderBy(orderBy1!, ...(orderBy2 ? [orderBy2] : []), ...(orderBy3 ? [orderBy3] : []))
.limit(pageSize) .limit(pageSize)
.offset(offset); .offset(offset);
// Ensure plain primitives // Ensure plain primitives
const items: ReleaseListItem[] = rowsQB.map((r: any) => ({ const items: ReleaseListItem[] = rowsQB.map((r) => ({
entryId: Number(r.entryId), entryId: Number(r.entryId),
releaseSeq: Number(r.releaseSeq), releaseSeq: Number(r.releaseSeq),
entryTitle: r.entryTitle ?? "", entryTitle: r.entryTitle ?? "",

View File

@@ -10,13 +10,13 @@ export const parseDescriptionDefault = (reg: Register, description: string) => {
// Footnote multiline state // Footnote multiline state
let inFootnote = false; let inFootnote = false;
let footnoteBaseIndent = 0; let footnoteBaseIndent = 0;
let footnoteTarget: 'global' | 'access' | null = null; // let footnoteTarget: 'global' | 'access' | null = null;
let currentFootnote: Note | null = null; let currentFootnote: Note | null = null;
const endFootnoteIfActive = () => { const endFootnoteIfActive = () => {
inFootnote = false; inFootnote = false;
footnoteBaseIndent = 0; footnoteBaseIndent = 0;
footnoteTarget = null; // footnoteTarget = null;
currentFootnote = null; currentFootnote = null;
}; };
@@ -89,10 +89,10 @@ export const parseDescriptionDefault = (reg: Register, description: string) => {
const note: Note = { ref: noteMatch[1], text: noteMatch[2] }; const note: Note = { ref: noteMatch[1], text: noteMatch[2] };
if (currentAccess) { if (currentAccess) {
accessData.notes.push(note); accessData.notes.push(note);
footnoteTarget = 'access'; // footnoteTarget = 'access';
} else { } else {
reg.notes.push(note); reg.notes.push(note);
footnoteTarget = 'global'; // footnoteTarget = 'global';
} }
currentFootnote = note; currentFootnote = note;
inFootnote = true; inFootnote = true;