ZXDB: Releases browser filters, schema lists, and fixes

- UI: Add /zxdb hub cards for Entries and Releases; implement Releases browser
  with URL‑synced filters (q, year, sort, DL language/machine, file/scheme/source/case, demo)
  and a paginated table (Entry ID, Title, Release #, Year).
- API: Add GET /api/zxdb/releases/search (Zod‑validated, Node runtime) supporting
  title, year, sort, and downloads‑based filters; return paged JSON.
- Repo: Rewrite searchReleases to Drizzle QB; correct ORDER BY on releases.release_year;
  implement EXISTS on downloads using explicit "from downloads as d"; return JSON‑safe rows.
- Schema: Align Drizzle models with ZXDB for releases/downloads; add lookups
  availabletypes, currencies, roletypes, and roles relation.
- API (lookups): Add GET /api/zxdb/{availabletypes,currencies,roletypes} for dropdowns.
- Stability: JSON‑clone SSR payloads before passing to Client Components to avoid
  RowDataPacket serialization errors.

Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
2025-12-16 23:00:38 +00:00
parent fd4c0f8963
commit f563b41792
16 changed files with 1147 additions and 62 deletions

View File

@@ -1,4 +1,4 @@
import { mysqlTable, int, varchar, tinyint, char, smallint } from "drizzle-orm/mysql-core";
import { mysqlTable, int, varchar, tinyint, char, smallint, decimal } from "drizzle-orm/mysql-core";
// Minimal subset needed for browsing/searching
export const entries = mysqlTable("entries", {
@@ -80,6 +80,21 @@ export const genretypes = mysqlTable("genretypes", {
name: varchar("text", { length: 50 }).notNull(),
});
// Additional lookups
export const availabletypes = mysqlTable("availabletypes", {
id: char("id", { length: 1 }).notNull().primaryKey(),
// DB column `text`
name: varchar("text", { length: 50 }).notNull(),
});
export const currencies = mysqlTable("currencies", {
id: char("id", { length: 3 }).notNull().primaryKey(),
name: varchar("name", { length: 50 }).notNull(),
symbol: varchar("symbol", { length: 20 }),
// Stored as tinyint(1) 0/1
prefix: tinyint("prefix").notNull(),
});
// ----- Files and Filetypes (for downloads/assets) -----
export const filetypes = mysqlTable("filetypes", {
id: tinyint("id").notNull().primaryKey(),
@@ -100,14 +115,6 @@ export const files = mysqlTable("files", {
comments: varchar("comments", { length: 250 }),
});
// ----- Releases / Downloads (linked assets per release) -----
// Lookups used by releases/downloads
export const releasetypes = mysqlTable("releasetypes", {
id: char("id", { length: 1 }).notNull().primaryKey(),
// column name in DB is `text`
name: varchar("text", { length: 50 }).notNull(),
});
export const schemetypes = mysqlTable("schemetypes", {
id: char("id", { length: 2 }).notNull().primaryKey(),
name: varchar("text", { length: 50 }).notNull(),
@@ -123,6 +130,11 @@ export const casetypes = mysqlTable("casetypes", {
name: varchar("text", { length: 50 }).notNull(),
});
export const roletypes = mysqlTable("roletypes", {
id: char("id", { length: 1 }).notNull().primaryKey(),
name: varchar("text", { length: 50 }).notNull(),
});
export const hosts = mysqlTable("hosts", {
id: tinyint("id").notNull().primaryKey(),
title: varchar("title", { length: 150 }).notNull(),
@@ -135,13 +147,17 @@ export const hosts = mysqlTable("hosts", {
export const releases = mysqlTable("releases", {
entryId: int("entry_id").notNull(),
releaseSeq: smallint("release_seq").notNull(),
releasetypeId: char("releasetype_id", { length: 1 }),
languageId: char("language_id", { length: 2 }),
machinetypeId: tinyint("machinetype_id"),
labelId: int("label_id"), // developer
publisherId: int("publisher_label_id"),
releaseYear: smallint("release_year"),
comments: varchar("comments", { length: 250 }),
releaseMonth: smallint("release_month"),
releaseDay: smallint("release_day"),
currencyId: char("currency_id", { length: 3 }),
releasePrice: decimal("release_price", { precision: 9, scale: 2 }),
budgetPrice: decimal("budget_price", { precision: 9, scale: 2 }),
microdrivePrice: decimal("microdrive_price", { precision: 9, scale: 2 }),
diskPrice: decimal("disk_price", { precision: 9, scale: 2 }),
cartridgePrice: decimal("cartridge_price", { precision: 9, scale: 2 }),
bookIsbn: varchar("book_isbn", { length: 50 }),
bookPages: smallint("book_pages"),
});
// Downloads are linked to a release via (entry_id, release_seq)
@@ -167,3 +183,10 @@ export const downloads = mysqlTable("downloads", {
releaseYear: smallint("release_year"),
comments: varchar("comments", { length: 250 }),
});
// Roles relation (composite PK in DB)
export const roles = mysqlTable("roles", {
entryId: int("entry_id").notNull(),
labelId: int("label_id").notNull(),
roletypeId: char("roletype_id", { length: 1 }).notNull(),
});