diff --git a/AGENTS.md b/AGENTS.md index 129b83f..5678266 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,11 @@ This document provides an overview of the Next Explorer project, its structure, ## Project Overview -Next Explorer is a web application for browsing and exploring the registers of the Spectrum Next computer. It is built with Next.js (App Router), React, and TypeScript. +Next Explorer is a web application for exploring the Spectrum Next ecosystem. It is built with Next.js (App Router), React, and TypeScript. -The application reads register data from `data/nextreg.txt`, parses it on the server, and displays it in a user-friendly interface. Users can search for specific registers and view their details, including per-register notes and source snippets. +It has two main areas: +- Registers: parsed from `data/nextreg.txt`, browsable with real-time filtering and deep links. +- ZXDB Explorer: a deep, cross‑linked browser for ZXDB entries, labels, genres, languages, and machine types backed by MySQL using Drizzle ORM. ## Project Structure @@ -64,6 +66,17 @@ next-explorer/ - `RegisterBrowser.tsx`: Client Component implementing search/filter and listing. - `RegisterDetail.tsx`: Client Component that renders a single register’s details, including modes, notes, and source modal. - `[hex]/page.tsx`: Dynamic route that renders details for a specific register by hex address. +- `src/app/zxdb/`: ZXDB Explorer routes and client components. + - `page.tsx` + `ZxdbExplorer.tsx`: Search + filters with server-rendered initial content and ISR. + - `entries/[id]/page.tsx` + `EntryDetail.tsx`: Entry details (SSR initial data). + - `labels/page.tsx`, `labels/[id]/page.tsx` + client: Labels search and detail. + - `genres/`, `languages/`, `machinetypes/`: Category hubs and detail pages. +- `src/app/api/zxdb/`: Zod‑validated API routes (Node runtime) for search and category browsing. +- `src/server/`: + - `env.ts`: Zod env parsing/validation (t3.gg style). Validates `ZXDB_URL` (mysql://). + - `server/db.ts`: Drizzle over `mysql2` singleton pool. + - `server/schema/zxdb.ts`: Minimal Drizzle models (entries, labels, helper tables, lookups). + - `server/repo/zxdb.ts`: Repository queries for search, details, categories, and facets. - **`src/components/`**: Shared UI components such as `Navbar` and `ThemeDropdown`. - **`src/services/register.service.ts`**: Service layer responsible for loading and caching parsed register data. - **`src/utils/register_parser.ts` & `src/utils/register_parsers/`**: Parsing logic for `nextreg.txt`, including mode/bitfield handling and any register-specific parsing extensions. @@ -91,23 +104,34 @@ Comment what the code does, not what the agent has done. The documentation's pur - **Server Components**: - `src/app/registers/page.tsx` and `src/app/registers/[hex]/page.tsx` are Server Components. - - They call `getRegisters()` on the server and pass the resulting data down to client components as props. + - ZXDB pages under `/zxdb` server‑render initial content for fast first paint, with ISR (`export const revalidate = 3600`) on non‑search pages. + - Server components call repository functions directly on the server and pass data to client components for presentation. - **Client Components**: - `RegisterBrowser.tsx`: - Marked with `'use client'`. - Uses React state to manage search input and filtered results. - - Renders a list or grid of registers. - `RegisterDetail.tsx`: - Marked with `'use client'`. - Renders a single register with tabs for different access modes. - - Uses a modal to show the original source lines for the register. + - ZXDB client components (e.g., `ZxdbExplorer.tsx`, `EntryDetail.tsx`, `labels/*`) receive initial data from the server and keep interactions on the client without blocking the first paint. - **Dynamic Routing**: - - `src/app/registers/[hex]/page.tsx`: - - Resolves the `[hex]` URL segment. - - Looks up the corresponding register by `hex_address`. - - Calls `notFound()` when no matching register exists. + - Pages and API routes must await dynamic params in Next.js 15: + - Pages: `export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; }` + - API: `export async function GET(req, ctx: { params: Promise<{ id: string }> }) { const raw = await ctx.params; /* validate with Zod */ }` + - `src/app/registers/[hex]/page.tsx` resolves the `[hex]` segment and calls `notFound()` if absent. + +### ZXDB Integration + +- Database connection via `mysql2` pool wrapped by Drizzle (`src/server/db.ts`). +- Env validation via Zod (`src/env.ts`) ensures `ZXDB_URL` is a valid `mysql://` URL. +- Minimal Drizzle schema models used for fast search and lookups (`src/server/schema/zxdb.ts`). +- Repository consolidates SQL with typed results (`src/server/repo/zxdb.ts`). +- API routes under `/api/zxdb/*` validate inputs with Zod and run on Node runtime. +- UI under `/zxdb` is deeply cross‑linked and server‑renders initial data for performance. Links use Next `Link` to enable prefetching. + - Helper SQL `ZXDB/scripts/ZXDB_help_search.sql` must be run to create `search_by_*` tables for efficient searches. + - Lookup tables use column `text` for display names; the Drizzle schema maps it as `name`. ### Working Patterns*** @@ -120,4 +144,8 @@ Comment what the code does, not what the agent has done. The documentation's pur - git commit messages: - Use imperative mood (e.g., "Add feature X", "Fix bug Y"). - Include relevant issue numbers if applicable. - - Sign-off commit message as Junie@ \ No newline at end of file + - Sign-off commit message as Junie@ + +### References + +- ZXDB setup and API usage: `docs/ZXDB.md` \ No newline at end of file diff --git a/COMMIT_EDITMSG b/COMMIT_EDITMSG index 468f607..c795392 100644 --- a/COMMIT_EDITMSG +++ b/COMMIT_EDITMSG @@ -1,28 +1,15 @@ -perf(zxdb): server-render index pages with ISR and initial data +docs: add ZXDB guide; refresh README & AGENTS -Why -- Reduce time-to-first-content on ZXDB index pages by eliminating the initial client-side fetch and enabling incremental static regeneration. +Expand and update documentation to reflect the current app (Registers + ZXDB Explorer), with clear setup and usage instructions. -What -- Main Explorer (/zxdb): - - Server-renders first page of results and lookup lists (genres, languages, machinetypes) and passes them as initial props. - - Keeps client interactivity for subsequent searches/filters. -- Labels index (/zxdb/labels): - - Server-renders first page of empty search and passes as initial props to skip the first fetch. -- Category lists: - - Genres (/zxdb/genres), Languages (/zxdb/languages), Machine Types (/zxdb/machinetypes) now server-render their lists and export revalidate=3600. - - Refactored list components to accept server-provided items; removed on-mount fetching. -- Links & prefetch: - - Replaced remaining anchors with Next Link to enable prefetch where applicable. - -Tech details -- Added revalidate=3600 to the index pages for ISR. -- Updated ZxdbExplorer to accept initial results and initial filter lists; skips first client fetch when initial props are present. -- Updated LabelsSearch to accept initial payload and skip first fetch in default state. -- Updated GenreList, LanguageList, MachineTypeList to be presentational components receiving items from server pages. +Changes +- README: add project overview including ZXDB Explorer; routes tour; ZXDB setup (DB import, helper search tables, readonly role); environment configuration; selected API endpoints; implementation notes (Next 15 async params, Node runtime for mysql2, SSR/ISR usage); links to AGENTS.md and docs/ZXDB.md. +- docs/ZXDB.md (new): deep-dive guide covering database preparation, helper tables, environment, Explorer UI, API reference under /api/zxdb, performance approach (helper tables, parallel queries, ISR), troubleshooting, and roadmap. +- AGENTS.md: refresh Project Overview/Structure with ZXDB routes and server/client boundaries; document Next.js 15 dynamic params async pattern for pages and API routes; note Drizzle+mysql2, Node runtime, and lookup `text`→`name` mapping; keep commit workflow guidance. +- example.env: add reference to docs/ZXDB.md and clarify mysql:// format and setup pointers. Notes -- Low-churn list APIs already emit Cache-Control for CDN; list pages now render instantly from server. -- Further polish (breadcrumbs, facet counts UI) can build on this foundation without reintroducing initial network waits. +- Documentation focuses on the current state of the codebase (what the code does), not a log of agent actions. +- Helper SQL at ZXDB/scripts/ZXDB_help_search.sql is required for performant searches. -Signed-off-by: Junie@lucy.xalior.com \ No newline at end of file +Signed-off-by: Junie@lucy.xalior.com diff --git a/README.md b/README.md index 0c5c686..30ac99a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ Spectrum Next Explorer -A Next.js application for exploring the Spectrum Next hardware. It includes a Register Explorer with real‑time search and deep‑linkable queries. +A Next.js application for exploring the Spectrum Next ecosystem. It ships with: -Features -- Register Explorer parsed from `data/nextreg.txt` -- Real‑time filtering with query‑string deep links (e.g. `/registers?q=vram`) +- Register Explorer: parsed from `data/nextreg.txt`, with real‑time search and deep links +- ZXDB Explorer: a deep, cross‑linked browser for entries, labels, genres, languages, and machine types backed by a MySQL ZXDB instance - Bootstrap 5 theme with light/dark support Quick start -- Prerequisites: Node.js 20+, pnpm (recommended) +- Prerequisites: Node.js 20+, pnpm (recommended), access to a MySQL server for ZXDB (optional for Registers) - Install dependencies: - `pnpm install` - Run in development (Turbopack, port 4000): @@ -26,11 +25,53 @@ Project scripts (package.json) - `deploy-test`: push to `test.explorer.specnext.dev` - `deploy-prod`: push to `explorer.specnext.dev` -Documentation -- Docs index: `docs/index.md` -- Getting Started: `docs/getting-started.md` -- Architecture: `docs/architecture.md` -- Register Explorer: `docs/registers.md` +Routes +- `/` — Home +- `/registers` — Register Explorer +- `/zxdb` — ZXDB Explorer (search + filters) +- `/zxdb/entries/[id]` — Entry detail +- `/zxdb/labels` and `/zxdb/labels/[id]` — Labels search and detail +- `/zxdb/genres` and `/zxdb/genres/[id]` — Genres list and entries +- `/zxdb/languages` and `/zxdb/languages/[id]` — Languages list and entries +- `/zxdb/machinetypes` and `/zxdb/machinetypes/[id]` — Machine types list and entries + +ZXDB setup (database, env, and helper tables) +The Registers section works without any database. The ZXDB Explorer requires a MySQL ZXDB database and one environment variable. + +1) Prepare the database (outside this app) +- Import ZXDB data into MySQL. If you want only structure, use `ZXDB/ZXDB_mysql_STRUCTURE_ONLY.sql` in this repo. For data, import ZXDB via your normal process. +- Create the helper search tables (required for fast search): + - Run `ZXDB/scripts/ZXDB_help_search.sql` against your ZXDB database. +- Create a read‑only role/user (recommended): + - Example (see `bin/import_mysql.sh`): + - Create role `zxdb_readonly` + - Grant `SELECT, SHOW VIEW` on database `zxdb` + +2) Configure environment +- Copy `.env` from `example.env`. +- Set `ZXDB_URL` to a MySQL URL, e.g. `mysql://zxdb_readonly:password@hostname:3306/zxdb`. +- On startup, `src/env.ts` validates env vars (t3.gg pattern with Zod) and will fail fast if invalid. + +3) Run the app +- `pnpm dev` → open http://localhost:4000 and navigate to `/zxdb`. + +API (selected endpoints) +- `GET /api/zxdb/search?q=...&page=1&pageSize=20&genreId=...&languageId=...&machinetypeId=...&sort=title&facets=1` +- `GET /api/zxdb/entries/[id]` +- `GET /api/zxdb/labels/search?q=...` +- `GET /api/zxdb/labels/[id]?page=1&pageSize=20` +- `GET /api/zxdb/genres` and `/api/zxdb/genres/[id]?page=1` +- `GET /api/zxdb/languages` and `/api/zxdb/languages/[id]?page=1` +- `GET /api/zxdb/machinetypes` and `/api/zxdb/machinetypes/[id]?page=1` + +Implementation notes +- Next.js 15 dynamic params: pages and API routes that consume `params` must await it, e.g. `export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; }` +- ZXDB integration uses Drizzle ORM over `mysql2` with a singleton pool at `src/server/db.ts`; API routes declare `export const runtime = "nodejs"`. +- Entry and detail pages server‑render initial content and use ISR (`revalidate = 3600`) for fast time‑to‑content; index pages avoid a blocking first client fetch. + +Further reading +- ZXDB details and API usage: `docs/ZXDB.md` +- Agent/developer workflow and commit guidelines: `AGENTS.md` License - See `LICENSE.txt` for details. diff --git a/docs/ZXDB.md b/docs/ZXDB.md new file mode 100644 index 0000000..5c1783a --- /dev/null +++ b/docs/ZXDB.md @@ -0,0 +1,112 @@ +# ZXDB Guide + +This document explains how the ZXDB Explorer works in this project, how to set up the database connection, and how to use the built‑in API and UI for software discovery. + +## What is ZXDB? + +ZXDB ( https://github.com/zxdb/ZXDB )is a community‑maintained database of ZX Spectrum software, publications, and related entities. In this project, we connect to a MySQL ZXDB instance in read‑only mode and expose a fast, cross‑linked explorer UI under `/zxdb`. + +## Prerequisites + +- MySQL server with ZXDB data (or at minimum the tables; data is needed to browse). +- Ability to run the helper SQL that builds search tables (required for efficient LIKE searches). +- A read‑only MySQL user for the app (recommended). + +## Database setup + +1. Import ZXDB data into MySQL. + - For structure only (no data): use `ZXDB/ZXDB_mysql_STRUCTURE_ONLY.sql` in this repo. + - For actual data, follow your usual ZXDB data import process. + +2. Create helper search tables (required). + - Run `ZXDB/scripts/ZXDB_help_search.sql` on your ZXDB database. + - This creates `search_by_titles`, `search_by_names`, `search_by_authors`, and `search_by_publishers` tables. + +3. Create a read‑only role/user (recommended). + - Example (see `bin/import_mysql.sh`): + - Create role `zxdb_readonly`. + - Grant `SELECT, SHOW VIEW` on your `zxdb` database to the user. + +## Environment configuration + +Set the connection string in `.env`: + +``` +ZXDB_URL=mysql://zxdb_readonly:password@hostname:3306/zxdb +``` + +Notes: +- The URL must start with `mysql://`. Env is validated at boot by `src/env.ts` (Zod), failing fast if misconfigured. +- The app uses a singleton `mysql2` pool (`src/server/db.ts`) and Drizzle ORM for typed queries. + +## Running + +``` +pnpm install +pnpm dev +# open http://localhost:4000 and navigate to /zxdb +``` + +## Explorer UI overview + +- `/zxdb` — Search entries by title and filter by genre, language, and machine type; sort and paginate results. +- `/zxdb/entries/[id]` — Entry details with badges for genre/language/machine, and linked authors/publishers. +- `/zxdb/labels` and `/zxdb/labels/[id]` — Browse/search labels (people/companies) and view authored/published entries. +- `/zxdb/genres`, `/zxdb/languages`, `/zxdb/machinetypes` — Category hubs with linked detail pages listing entries. + +Cross‑linking: All entities are permalinks using stable IDs. Navigation uses Next `Link` so pages are prefetched. + +Performance: Detail and index pages are server‑rendered with initial data and use ISR (`revalidate = 3600`) to reduce time‑to‑first‑content. Queries select only required columns and leverage helper tables for text search. + +## HTTP API reference (selected) + +All endpoints are under `/api/zxdb` and validate inputs with Zod. Responses are JSON. + +- Search entries + - `GET /api/zxdb/search` + - Query params: + - `q` — string (free‑text search; normalized via helper tables) + - `page`, `pageSize` — pagination (default pageSize=20, max=100) + - `genreId`, `languageId`, `machinetypeId` — optional filters + - `sort` — `title` or `id_desc` + - `facets` — boolean; if truthy, includes facet counts for genres/languages/machines + +- Entry detail + - `GET /api/zxdb/entries/[id]` + - Returns: entry core fields, joined genre/language/machinetype names, authors and publishers. + +- Labels + - `GET /api/zxdb/labels/search?q=...` + - `GET /api/zxdb/labels/[id]?page=1&pageSize=20` — includes `authored` and `published` lists. + +- Categories + - `GET /api/zxdb/genres` and `/api/zxdb/genres/[id]?page=1` + - `GET /api/zxdb/languages` and `/api/zxdb/languages/[id]?page=1` + - `GET /api/zxdb/machinetypes` and `/api/zxdb/machinetypes/[id]?page=1` + +Runtime: API routes declare `export const runtime = "nodejs"` to support `mysql2`. + +## Implementation notes + +- Drizzle models map ZXDB lookup table column `text` to property `name` for ergonomics (e.g., `languages.text` → `name`). +- Next.js 15 dynamic params must be awaited in App Router pages and API routes. Example: + ```ts + export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + // ... + } + ``` +- Repository queries parallelize independent calls with `Promise.all` for lower latency. + +## Troubleshooting + +- 400 from dynamic API routes: ensure you await `ctx.params` before Zod validation. +- Unknown column errors for lookup names: ZXDB tables use column `text` for names; Drizzle schema must select `text` as `name`. +- Slow entry page: confirm server‑rendering is active and ISR is set; client components should not fetch on the first paint when initial props are provided. +- MySQL auth or network errors: verify `ZXDB_URL` and that your user has read permissions. + +## Roadmap + +- Facet counts displayed in the `/zxdb` filter UI. +- Breadcrumbs and additional a11y polish. +- Media assets and download links per release (future). diff --git a/example.env b/example.env index 2442bd5..1f25098 100644 --- a/example.env +++ b/example.env @@ -2,4 +2,6 @@ # Example using a readonly user created by ZXDB scripts # CREATE ROLE 'zxdb_readonly'; # GRANT SELECT, SHOW VIEW ON `zxdb`.* TO 'zxdb_readonly'; +# See docs/ZXDB.md for full setup instructions (DB import, helper tables, +# readonly role, and environment validation notes). ZXDB_URL=mysql://zxdb_readonly:password@hostname:3306/zxdb diff --git a/src/app/zxdb/ZxdbExplorer.tsx b/src/app/zxdb/ZxdbExplorer.tsx index 118adc6..fb3e68f 100644 --- a/src/app/zxdb/ZxdbExplorer.tsx +++ b/src/app/zxdb/ZxdbExplorer.tsx @@ -121,7 +121,7 @@ export default function ZxdbExplorer({ } return ( -
+

ZXDB Explorer

diff --git a/src/app/zxdb/genres/page.tsx b/src/app/zxdb/genres/page.tsx index 345b810..c328127 100644 --- a/src/app/zxdb/genres/page.tsx +++ b/src/app/zxdb/genres/page.tsx @@ -1,11 +1,14 @@ -import GenreList from "./GenreList"; -import { listGenres } from "@/server/repo/zxdb"; +import GenresSearch from "./GenresSearch"; +import { searchGenres } from "@/server/repo/zxdb"; export const metadata = { title: "ZXDB Genres" }; -export const revalidate = 3600; +export const dynamic = "force-dynamic"; -export default async function Page() { - const items = await listGenres(); - return ; +export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { + const sp = await searchParams; + 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 ; } diff --git a/src/app/zxdb/languages/LanguagesSearch.tsx b/src/app/zxdb/languages/LanguagesSearch.tsx new file mode 100644 index 0000000..232d5f0 --- /dev/null +++ b/src/app/zxdb/languages/LanguagesSearch.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +type Language = { id: string; name: string }; +type Paged = { items: T[]; page: number; pageSize: number; total: number }; + +export default function LanguagesSearch({ initial, initialQ }: { initial?: Paged; initialQ?: string }) { + const router = useRouter(); + const [q, setQ] = useState(initialQ ?? ""); + const [data, setData] = useState | null>(initial ?? null); + const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); + + useEffect(() => { + if (initial) setData(initial); + }, [initial]); + + useEffect(() => { + setQ(initialQ ?? ""); + }, [initialQ]); + + function submit(e: React.FormEvent) { + e.preventDefault(); + const params = new URLSearchParams(); + if (q) params.set("q", q); + params.set("page", "1"); + router.push(`/zxdb/languages?${params.toString()}`); + } + + return ( +
+

Languages

+ +
+ setQ(e.target.value)} /> +
+
+ +
+ + +
+ {data && data.items.length === 0 &&
No languages found.
} + {data && data.items.length > 0 && ( +
    + {data.items.map((l) => ( +
  • + {l.name} + {l.id} +
  • + ))} +
+ )} +
+ +
+ Page {data?.page ?? 1} / {totalPages} +
+ { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.max(1, (data?.page ?? 1) - 1))); return p.toString(); })()}`} + > + Prev + + = totalPages) ? "disabled" : ""}`} + aria-disabled={!data || data.page >= totalPages} + href={`/zxdb/languages?${(() => { const p = new URLSearchParams(); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, (data?.page ?? 1) + 1))); return p.toString(); })()}`} + > + Next + +
+
+
+ ); +} diff --git a/src/app/zxdb/languages/page.tsx b/src/app/zxdb/languages/page.tsx index 5bee4bd..8afb3d5 100644 --- a/src/app/zxdb/languages/page.tsx +++ b/src/app/zxdb/languages/page.tsx @@ -1,11 +1,15 @@ -import LanguageList from "./LanguageList"; -import { listLanguages } from "@/server/repo/zxdb"; +import LanguagesSearch from "./LanguagesSearch"; +import { searchLanguages } from "@/server/repo/zxdb"; export const metadata = { title: "ZXDB Languages" }; -export const revalidate = 3600; +// Depends on searchParams (?q=, ?page=). Force dynamic so each request renders correctly. +export const dynamic = "force-dynamic"; -export default async function Page() { - const items = await listLanguages(); - return ; +export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { + const sp = await searchParams; + 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 ; } diff --git a/src/server/repo/zxdb.ts b/src/server/repo/zxdb.ts index e2fa645..916747c 100644 --- a/src/server/repo/zxdb.ts +++ b/src/server/repo/zxdb.ts @@ -311,6 +311,103 @@ export async function listMachinetypes() { return db.select().from(machinetypes).orderBy(machinetypes.name); } +// Search with pagination for lookups +export interface SimpleSearchParams { + q?: string; + page?: number; + pageSize?: number; +} + +export async function searchLanguages(params: SimpleSearchParams) { + const q = (params.q ?? "").trim(); + const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100)); + const page = Math.max(1, params.page ?? 1); + const offset = (page - 1) * pageSize; + + if (!q) { + const [items, countRows] = await Promise.all([ + db.select().from(languages).orderBy(languages.name).limit(pageSize).offset(offset), + db.select({ total: sql`count(*)` }).from(languages) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; + } + + const pattern = `%${q}%`; + const [items, countRows] = await Promise.all([ + db + .select() + .from(languages) + .where(like(languages.name as any, pattern)) + .orderBy(languages.name) + .limit(pageSize) + .offset(offset), + db.select({ total: sql`count(*)` }).from(languages).where(like(languages.name as any, pattern)) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; +} + +export async function searchGenres(params: SimpleSearchParams) { + const q = (params.q ?? "").trim(); + const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100)); + const page = Math.max(1, params.page ?? 1); + const offset = (page - 1) * pageSize; + + if (!q) { + const [items, countRows] = await Promise.all([ + db.select().from(genretypes).orderBy(genretypes.name).limit(pageSize).offset(offset), + db.select({ total: sql`count(*)` }).from(genretypes) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; + } + + const pattern = `%${q}%`; + const [items, countRows] = await Promise.all([ + db + .select() + .from(genretypes) + .where(like(genretypes.name as any, pattern)) + .orderBy(genretypes.name) + .limit(pageSize) + .offset(offset), + db.select({ total: sql`count(*)` }).from(genretypes).where(like(genretypes.name as any, pattern)) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; +} + +export async function searchMachinetypes(params: SimpleSearchParams) { + const q = (params.q ?? "").trim(); + const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100)); + const page = Math.max(1, params.page ?? 1); + const offset = (page - 1) * pageSize; + + if (!q) { + const [items, countRows] = await Promise.all([ + db.select().from(machinetypes).orderBy(machinetypes.name).limit(pageSize).offset(offset), + db.select({ total: sql`count(*)` }).from(machinetypes) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; + } + + const pattern = `%${q}%`; + const [items, countRows] = await Promise.all([ + db + .select() + .from(machinetypes) + .where(like(machinetypes.name as any, pattern)) + .orderBy(machinetypes.name) + .limit(pageSize) + .offset(offset), + db.select({ total: sql`count(*)` }).from(machinetypes).where(like(machinetypes.name as any, pattern)) as unknown as Promise<{ total: number }[]>, + ]); + const total = Number(countRows?.[0]?.total ?? 0); + return { items: items as any, page, pageSize, total }; +} + export async function entriesByGenre(genreId: number, page: number, pageSize: number): Promise> { const offset = (page - 1) * pageSize; const countRows = (await db