chore: ZXDB env validation, MySQL setup, API & UI
This sanity commit wires up the initial ZXDB integration and a minimal UI to explore it. Key changes: - Add Zod-based env parsing (`src/env.ts`) validating `ZXDB_URL` as a mysql:// URL (t3.gg style). - Configure Drizzle ORM with mysql2 connection pool (`src/server/db.ts`) driven by `ZXDB_URL`. - Define minimal ZXDB schema models (`src/server/schema/zxdb.ts`): `entries` and helper `search_by_titles`. - Implement repository search with pagination using helper table (`src/server/repo/zxdb.ts`). - Expose Next.js API route `GET /api/zxdb/search` with Zod query validation and Node runtime (`src/app/api/zxdb/search/route.ts`). - Create new app section “ZXDB Explorer” at `/zxdb` with search UI, results table, and pagination (`src/app/zxdb/*`). - Add navbar link to ZXDB (`src/components/Navbar.tsx`). - Update example.env with readonly-role notes and example `ZXDB_URL`. - Add drizzle-kit config scaffold (`drizzle.config.ts`). - Update package.json deps: drizzle-orm, mysql2, zod; devDeps: drizzle-kit. Lockfile updated. - Extend .gitignore to exclude large ZXDB structure dump. Notes: - Ensure ZXDB data and helper tables are loaded (see `ZXDB/scripts/ZXDB_help_search.sql`). - This commit provides structure-only browsing; future work can enrich schema (authors, labels, publishers) and UI filters. Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
15
src/server/db.ts
Normal file
15
src/server/db.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import mysql from "mysql2/promise";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import { env } from "@/env";
|
||||
|
||||
// Create a singleton connection pool for the ZXDB database
|
||||
const pool = mysql.createPool({
|
||||
uri: env.ZXDB_URL,
|
||||
connectionLimit: 10,
|
||||
// Larger queries may be needed for ZXDB
|
||||
maxPreparedStatements: 256,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool);
|
||||
|
||||
export type Db = typeof db;
|
||||
74
src/server/repo/zxdb.ts
Normal file
74
src/server/repo/zxdb.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { entries, searchByTitles } from "@/server/schema/zxdb";
|
||||
|
||||
export interface SearchParams {
|
||||
q?: string;
|
||||
page?: number; // 1-based
|
||||
pageSize?: number; // default 20
|
||||
}
|
||||
|
||||
export interface SearchResultItem {
|
||||
id: number;
|
||||
title: string;
|
||||
isXrated: number;
|
||||
machinetypeId: number | null;
|
||||
languageId: string | null;
|
||||
}
|
||||
|
||||
export interface PagedResult<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export async function searchEntries(params: SearchParams): Promise<PagedResult<SearchResultItem>> {
|
||||
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.length === 0) {
|
||||
// Default listing: return first page by id desc (no guaranteed ordering field; using id)
|
||||
const [items, [{ total }]] = await Promise.all([
|
||||
db.select().from(entries).orderBy(desc(entries.id)).limit(pageSize).offset(offset),
|
||||
db.execute(sql`select count(*) as total from ${entries}`) as Promise<any>,
|
||||
]);
|
||||
return {
|
||||
items: items as any,
|
||||
page,
|
||||
pageSize,
|
||||
total: Number((total as any) ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
const pattern = `%${q.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
|
||||
// Count matches via helper table
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${searchByTitles.entryId})` })
|
||||
.from(searchByTitles)
|
||||
.where(like(searchByTitles.entryTitle, pattern));
|
||||
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
// Items using join to entries, distinct entry ids
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
languageId: entries.languageId,
|
||||
})
|
||||
.from(searchByTitles)
|
||||
.innerJoin(entries, eq(entries.id, searchByTitles.entryId))
|
||||
.where(like(searchByTitles.entryTitle, pattern))
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
18
src/server/schema/zxdb.ts
Normal file
18
src/server/schema/zxdb.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { mysqlTable, int, varchar, tinyint, char } from "drizzle-orm/mysql-core";
|
||||
|
||||
// Minimal subset needed for browsing/searching
|
||||
export const entries = mysqlTable("entries", {
|
||||
id: int("id").notNull().primaryKey(),
|
||||
title: varchar("title", { length: 250 }).notNull(),
|
||||
isXrated: tinyint("is_xrated").notNull(),
|
||||
machinetypeId: tinyint("machinetype_id"),
|
||||
languageId: char("language_id", { length: 2 }),
|
||||
});
|
||||
|
||||
// Helper table created by ZXDB_help_search.sql
|
||||
export const searchByTitles = mysqlTable("search_by_titles", {
|
||||
entryTitle: varchar("entry_title", { length: 250 }).notNull(),
|
||||
entryId: int("entry_id").notNull(),
|
||||
});
|
||||
|
||||
export type Entry = typeof entries.$inferSelect;
|
||||
Reference in New Issue
Block a user