Implement magazine reviews, label details, and year filtering
- Aggregate magazine references from all releases on the Entry detail page. - Display country names and external links (Wikipedia/Website) on the Label detail page. - Add a year filter to the ZXDB Explorer to search entries by release year. Signed-off: junie@lucy.xalior.com
This commit is contained in:
@@ -52,6 +52,7 @@ import {
|
||||
magrefs,
|
||||
searchByMagrefs,
|
||||
referencetypes,
|
||||
countries,
|
||||
} from "@/server/schema/zxdb";
|
||||
|
||||
export type EntrySearchScope = "title" | "title_aliases" | "title_aliases_origins";
|
||||
@@ -64,6 +65,8 @@ export interface SearchParams {
|
||||
genreId?: number;
|
||||
languageId?: string;
|
||||
machinetypeId?: number | number[];
|
||||
// Year filter
|
||||
year?: number;
|
||||
// Sorting
|
||||
sort?: "title" | "id_desc";
|
||||
// Search scope (defaults to titles only)
|
||||
@@ -206,6 +209,9 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
const ids = params.machinetypeId.map((id) => sql`${id}`);
|
||||
whereClauses.push(sql`${entries.machinetypeId} in (${sql.join(ids, sql`, `)})`);
|
||||
}
|
||||
if (typeof params.year === "number") {
|
||||
whereClauses.push(sql`${entries.id} in (select entry_id from releases where release_year = ${params.year})`);
|
||||
}
|
||||
|
||||
const whereExpr = whereClauses.length ? and(...whereClauses) : undefined;
|
||||
|
||||
@@ -250,9 +256,30 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
if (scope !== "title") {
|
||||
try {
|
||||
const union = buildEntrySearchUnion(pattern, scope);
|
||||
const whereClauses: Array<ReturnType<typeof sql>> = [
|
||||
sql`${entries.id} in (select entry_id from (${union}) as matches)`
|
||||
];
|
||||
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));
|
||||
} else if (Array.isArray(params.machinetypeId) && params.machinetypeId.length > 0) {
|
||||
const ids = params.machinetypeId.map((id) => sql`${id}`);
|
||||
whereClauses.push(sql`${entries.machinetypeId} in (${sql.join(ids, sql`, `)})`);
|
||||
}
|
||||
if (typeof params.year === "number") {
|
||||
whereClauses.push(sql`${entries.id} in (select entry_id from releases where release_year = ${params.year})`);
|
||||
}
|
||||
const whereExpr = and(...whereClauses);
|
||||
|
||||
const countRows = await db.execute(sql`
|
||||
select count(distinct entry_id) as total
|
||||
from (${union}) as matches
|
||||
select count(distinct id) as total
|
||||
from entries
|
||||
where ${whereExpr}
|
||||
`);
|
||||
type CountRow = { total: number | string };
|
||||
const total = Number((countRows as unknown as CountRow[])[0]?.total ?? 0);
|
||||
@@ -273,7 +300,7 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(sql`${entries.id} in (select entry_id from (${union}) as matches)`)
|
||||
.where(whereExpr)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(
|
||||
...(preferMachineOrder ? [preferMachineOrder] : []),
|
||||
@@ -289,10 +316,31 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
}
|
||||
|
||||
// Count matches via helper table
|
||||
const whereClauses: Array<ReturnType<typeof sql>> = [
|
||||
sql`lower(${searchByTitles.entryTitle}) like ${pattern}`
|
||||
];
|
||||
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));
|
||||
} else if (Array.isArray(params.machinetypeId) && params.machinetypeId.length > 0) {
|
||||
const ids = params.machinetypeId.map((id) => sql`${id}`);
|
||||
whereClauses.push(sql`${entries.machinetypeId} in (${sql.join(ids, sql`, `)})`);
|
||||
}
|
||||
if (typeof params.year === "number") {
|
||||
whereClauses.push(sql`${entries.id} in (select entry_id from releases where release_year = ${params.year})`);
|
||||
}
|
||||
const whereExpr = and(...whereClauses);
|
||||
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${searchByTitles.entryId})` })
|
||||
.from(searchByTitles)
|
||||
.where(sql`lower(${searchByTitles.entryTitle}) like ${pattern}`);
|
||||
.innerJoin(entries, eq(entries.id, searchByTitles.entryId))
|
||||
.where(whereExpr);
|
||||
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
@@ -314,7 +362,7 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(sql`lower(${searchByTitles.entryTitle}) like ${pattern}`)
|
||||
.where(whereExpr)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(
|
||||
...(preferMachineOrder ? [preferMachineOrder] : []),
|
||||
@@ -458,6 +506,26 @@ export interface EntryDetail {
|
||||
// Additional relationships surfaced on the entry detail page
|
||||
aliases?: { releaseSeq: number; languageId: string; title: string }[];
|
||||
webrefs?: { link: string; languageId: string; website: { id: number; name: string; link?: string | null } }[];
|
||||
magazineRefs?: {
|
||||
id: number;
|
||||
issueId: number;
|
||||
magazineId: number | null;
|
||||
magazineName: string | null;
|
||||
referencetypeId: number;
|
||||
referencetypeName: string | null;
|
||||
page: number;
|
||||
isOriginal: number;
|
||||
scoreGroup: string;
|
||||
issue: {
|
||||
dateYear: number | null;
|
||||
dateMonth: number | null;
|
||||
dateDay: number | null;
|
||||
volume: number | null;
|
||||
number: number | null;
|
||||
special: string | null;
|
||||
supplement: string | null;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
@@ -731,6 +799,24 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
notetypeName: string | null;
|
||||
text: string;
|
||||
}[] = [];
|
||||
let magazineRefRows: {
|
||||
id: number;
|
||||
issueId: number;
|
||||
magazineId: number | null;
|
||||
magazineName: string | null;
|
||||
referencetypeId: number;
|
||||
referencetypeName: string | null;
|
||||
page: number;
|
||||
isOriginal: number;
|
||||
scoreGroup: string;
|
||||
issueDateYear: number | null;
|
||||
issueDateMonth: number | null;
|
||||
issueDateDay: number | null;
|
||||
issueVolume: number | null;
|
||||
issueNumber: number | null;
|
||||
issueSpecial: string | null;
|
||||
issueSupplement: string | null;
|
||||
}[] = [];
|
||||
try {
|
||||
aliasRows = await db
|
||||
.select({ releaseSeq: aliases.releaseSeq, languageId: aliases.languageId, title: aliases.title })
|
||||
@@ -907,6 +993,43 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
noteRows = rows as typeof noteRows;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: magrefs.id,
|
||||
issueId: magrefs.issueId,
|
||||
magazineId: magazines.id,
|
||||
magazineName: magazines.name,
|
||||
referencetypeId: magrefs.referencetypeId,
|
||||
referencetypeName: referencetypes.name,
|
||||
page: magrefs.page,
|
||||
isOriginal: magrefs.isOriginal,
|
||||
scoreGroup: magrefs.scoreGroup,
|
||||
issueDateYear: issues.dateYear,
|
||||
issueDateMonth: issues.dateMonth,
|
||||
issueDateDay: issues.dateDay,
|
||||
issueVolume: issues.volume,
|
||||
issueNumber: issues.number,
|
||||
issueSpecial: issues.special,
|
||||
issueSupplement: issues.supplement,
|
||||
})
|
||||
.from(searchByMagrefs)
|
||||
.innerJoin(magrefs, eq(magrefs.id, searchByMagrefs.magrefId))
|
||||
.leftJoin(issues, eq(issues.id, magrefs.issueId))
|
||||
.leftJoin(magazines, eq(magazines.id, issues.magazineId))
|
||||
.leftJoin(referencetypes, eq(referencetypes.id, magrefs.referencetypeId))
|
||||
.where(eq(searchByMagrefs.entryId, id))
|
||||
.orderBy(
|
||||
asc(magazines.name),
|
||||
asc(issues.dateYear),
|
||||
asc(issues.dateMonth),
|
||||
asc(issues.id),
|
||||
asc(magrefs.page),
|
||||
asc(magrefs.id)
|
||||
);
|
||||
magazineRefRows = rows as typeof magazineRefRows;
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
id: base.id,
|
||||
title: base.title,
|
||||
@@ -1028,6 +1151,26 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
})),
|
||||
aliases: aliasRows.map((a) => ({ releaseSeq: Number(a.releaseSeq), languageId: a.languageId, title: a.title })),
|
||||
webrefs: webrefRows.map((w) => ({ link: w.link, languageId: w.languageId, website: { id: Number(w.websiteId), name: w.websiteName, link: w.websiteLink } })),
|
||||
magazineRefs: magazineRefRows.map((m) => ({
|
||||
id: m.id,
|
||||
issueId: Number(m.issueId),
|
||||
magazineId: m.magazineId != null ? Number(m.magazineId) : null,
|
||||
magazineName: m.magazineName ?? null,
|
||||
referencetypeId: Number(m.referencetypeId),
|
||||
referencetypeName: m.referencetypeName ?? null,
|
||||
page: Number(m.page),
|
||||
isOriginal: Number(m.isOriginal),
|
||||
scoreGroup: m.scoreGroup ?? "",
|
||||
issue: {
|
||||
dateYear: m.issueDateYear != null ? Number(m.issueDateYear) : null,
|
||||
dateMonth: m.issueDateMonth != null ? Number(m.issueDateMonth) : null,
|
||||
dateDay: m.issueDateDay != null ? Number(m.issueDateDay) : null,
|
||||
volume: m.issueVolume != null ? Number(m.issueVolume) : null,
|
||||
number: m.issueNumber != null ? Number(m.issueNumber) : null,
|
||||
special: m.issueSpecial ?? null,
|
||||
supplement: m.issueSupplement ?? null,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1035,6 +1178,12 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
|
||||
export interface LabelDetail extends LabelSummary {
|
||||
labeltypeName: string | null;
|
||||
countryId: string | null;
|
||||
countryName: string | null;
|
||||
country2Id: string | null;
|
||||
country2Name: string | null;
|
||||
linkWikipedia: string | null;
|
||||
linkSite: string | null;
|
||||
permissions: {
|
||||
website: { id: number; name: string; link?: string | null };
|
||||
type: { id: string; name: string | null };
|
||||
@@ -1099,9 +1248,17 @@ export async function getLabelById(id: number): Promise<LabelDetail | null> {
|
||||
name: labels.name,
|
||||
labeltypeId: labels.labeltypeId,
|
||||
labeltypeName: labeltypes.name,
|
||||
countryId: labels.countryId,
|
||||
countryName: sql<string>`c1.text`,
|
||||
country2Id: labels.country2Id,
|
||||
country2Name: sql<string>`c2.text`,
|
||||
linkWikipedia: labels.linkWikipedia,
|
||||
linkSite: labels.linkSite,
|
||||
})
|
||||
.from(labels)
|
||||
.leftJoin(labeltypes, eq(labeltypes.id, labels.labeltypeId))
|
||||
.leftJoin(sql`${countries} c1`, eq(sql`c1.id`, labels.countryId))
|
||||
.leftJoin(sql`${countries} c2`, eq(sql`c2.id`, labels.country2Id))
|
||||
.where(eq(labels.id, id))
|
||||
.limit(1);
|
||||
const base = rows[0];
|
||||
@@ -1165,6 +1322,12 @@ export async function getLabelById(id: number): Promise<LabelDetail | null> {
|
||||
name: base.name,
|
||||
labeltypeId: base.labeltypeId,
|
||||
labeltypeName: base.labeltypeName ?? null,
|
||||
countryId: base.countryId ?? null,
|
||||
countryName: base.countryName ?? null,
|
||||
country2Id: base.country2Id ?? null,
|
||||
country2Name: base.country2Name ?? null,
|
||||
linkWikipedia: base.linkWikipedia ?? null,
|
||||
linkSite: base.linkSite ?? null,
|
||||
permissions: permissionRows.map((p) => ({
|
||||
website: { id: Number(p.websiteId), name: p.websiteName, link: p.websiteLink ?? null },
|
||||
type: { id: p.permissiontypeId, name: p.permissiontypeName ?? null },
|
||||
|
||||
Reference in New Issue
Block a user