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).
- 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
- **Server Components**:

View File

@@ -139,7 +139,7 @@ export default function ZxdbExplorer({
<button className="btn btn-primary" type="submit" disabled={loading}>Search</button>
</div>
<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>
{genres.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option>
@@ -147,7 +147,7 @@ export default function ZxdbExplorer({
</select>
</div>
<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>
{languages.map((l) => (
<option key={l.id} value={l.id}>{l.name}</option>
@@ -155,7 +155,7 @@ export default function ZxdbExplorer({
</select>
</div>
<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>
{machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
@@ -163,7 +163,7 @@ export default function ZxdbExplorer({
</select>
</div>
<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="id_desc">Sort: Newest</option>
</select>

View File

@@ -194,7 +194,7 @@ export default function EntriesExplorer({
<button className="btn btn-primary" type="submit" disabled={loading}>Search</button>
</div>
<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>
{genres.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option>
@@ -202,7 +202,7 @@ export default function EntriesExplorer({
</select>
</div>
<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>
{languages.map((l) => (
<option key={l.id} value={l.id}>{l.name}</option>
@@ -210,7 +210,7 @@ export default function EntriesExplorer({
</select>
</div>
<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>
{machines.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
@@ -218,7 +218,7 @@ export default function EntriesExplorer({
</select>
</div>
<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="id_desc">Sort: Newest</option>
</select>

View File

@@ -12,5 +12,5 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
const numericId = Number(id);
const data = await getEntryById(numericId);
// 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 languageId = (Array.isArray(sp.languageId) ? sp.languageId[0] : sp.languageId) ?? "";
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 [initial, genres, langs, machines] = await Promise.all([
@@ -33,10 +33,10 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
return (
<EntriesExplorer
initial={initial as any}
initialGenres={genres as any}
initialLanguages={langs as any}
initialMachines={machines as any}
initial={initial}
initialGenres={genres}
initialLanguages={langs}
initialMachines={machines}
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 q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
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 page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
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
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 page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
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 q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
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 page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
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 q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
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 page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
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>
</div>
<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_asc">Sort: Oldest</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 yearStr = (Array.isArray(sp.year) ? sp.year[0] : sp.year) ?? "";
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 dMachinetypeIdStr = (Array.isArray(sp.dMachinetypeId) ? sp.dMachinetypeId[0] : sp.dMachinetypeId) ?? "";
const dMachinetypeId = dMachinetypeIdStr ? Number(dMachinetypeIdStr) : undefined;
@@ -34,7 +34,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
return (
<ReleasesExplorer
initial={initialPlain as any}
initial={initialPlain}
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)
.map(([name, 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`;
})

View File

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

View File

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