docs: add ZXDB guide; refresh README & AGENTS

Expand and update documentation to reflect the current app (Registers + ZXDB Explorer), with clear setup and usage instructions.

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
- 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
This commit is contained in:
2025-12-12 16:17:35 +00:00
parent 3ef3a16bc0
commit ddbf72ea52
10 changed files with 409 additions and 56 deletions

View File

@@ -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, crosslinked 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 registers 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/`: Zodvalidated 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` serverrender initial content for fast first paint, with ISR (`export const revalidate = 3600`) on nonsearch 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 crosslinked and serverrenders 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@<hostname>
- Sign-off commit message as Junie@<hostname>
### References
- ZXDB setup and API usage: `docs/ZXDB.md`

View File

@@ -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
Signed-off-by: Junie@lucy.xalior.com

View File

@@ -1,14 +1,13 @@
Spectrum Next Explorer
A Next.js application for exploring the Spectrum Next hardware. It includes a Register Explorer with realtime search and deeplinkable queries.
A Next.js application for exploring the Spectrum Next ecosystem. It ships with:
Features
- Register Explorer parsed from `data/nextreg.txt`
- Realtime filtering with querystring deep links (e.g. `/registers?q=vram`)
- Register Explorer: parsed from `data/nextreg.txt`, with realtime search and deep links
- ZXDB Explorer: a deep, crosslinked 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 readonly 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 serverrender initial content and use ISR (`revalidate = 3600`) for fast timetocontent; 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.

112
docs/ZXDB.md Normal file
View File

@@ -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 builtin API and UI for software discovery.
## What is ZXDB?
ZXDB ( https://github.com/zxdb/ZXDB )is a communitymaintained database of ZX Spectrum software, publications, and related entities. In this project, we connect to a MySQL ZXDB instance in readonly mode and expose a fast, crosslinked 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 readonly 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 readonly 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.
Crosslinking: All entities are permalinks using stable IDs. Navigation uses Next `Link` so pages are prefetched.
Performance: Detail and index pages are serverrendered with initial data and use ISR (`revalidate = 3600`) to reduce timetofirstcontent. 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 (freetext 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 serverrendering 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).

View File

@@ -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

View File

@@ -121,7 +121,7 @@ export default function ZxdbExplorer({
}
return (
<div className="container">
<div>
<h1 className="mb-3">ZXDB Explorer</h1>
<form className="row gy-2 gx-2 align-items-center" onSubmit={onSubmit}>
<div className="col-sm-8 col-md-6 col-lg-4">

View File

@@ -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 <GenreList items={items as any} />;
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 <GenresSearch initial={initial as any} initialQ={q} />;
}

View File

@@ -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<T> = { items: T[]; page: number; pageSize: number; total: number };
export default function LanguagesSearch({ initial, initialQ }: { initial?: Paged<Language>; initialQ?: string }) {
const router = useRouter();
const [q, setQ] = useState(initialQ ?? "");
const [data, setData] = useState<Paged<Language> | 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 (
<div>
<h1>Languages</h1>
<form className="row gy-2 gx-2 align-items-center" onSubmit={submit}>
<div className="col-sm-8 col-md-6 col-lg-4">
<input className="form-control" placeholder="Search languages…" value={q} onChange={(e) => setQ(e.target.value)} />
</div>
<div className="col-auto">
<button className="btn btn-primary">Search</button>
</div>
</form>
<div className="mt-3">
{data && data.items.length === 0 && <div className="alert alert-warning">No languages found.</div>}
{data && data.items.length > 0 && (
<ul className="list-group">
{data.items.map((l) => (
<li key={l.id} className="list-group-item d-flex justify-content-between align-items-center">
<Link href={`/zxdb/languages/${l.id}`}>{l.name}</Link>
<span className="badge text-bg-light">{l.id}</span>
</li>
))}
</ul>
)}
</div>
<div className="d-flex align-items-center gap-2 mt-2">
<span>Page {data?.page ?? 1} / {totalPages}</span>
<div className="ms-auto d-flex gap-2">
<Link
className={`btn btn-outline-secondary ${!data || (data.page <= 1) ? "disabled" : ""}`}
aria-disabled={!data || data.page <= 1}
href={`/zxdb/languages?${(() => { 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
</Link>
<Link
className={`btn btn-outline-secondary ${!data || (data.page >= 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
</Link>
</div>
</div>
</div>
);
}

View File

@@ -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 <LanguageList items={items as any} />;
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 <LanguagesSearch initial={initial as any} initialQ={q} />;
}

View File

@@ -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<number>`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<number>`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<number>`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<number>`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<number>`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<number>`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<PagedResult<SearchResultItem>> {
const offset = (page - 1) * pageSize;
const countRows = (await db