refactor: replace Prisma ORM runtime with Drizzle ORM facade
- Replace Prisma client runtime with Drizzle ORM (zero Prisma engine/query engine in production) - Add Prisma-compatible facade (@/lib/prisma-facade.ts) backed by Drizzle for backwards compatibility - Runtime queries route through Drizzle ORM; @prisma/client is now devDependency (types only) - Remove @prisma/adapter-mariadb dependency; delete prisma-pool.ts and types/prisma.ts - New Drizzle schema layer: src/db/schema.ts (176 tables) and src/lib/db.ts (connection) - Update README documenting the dual-layer ORM architecture - Restore src/generated/ gitignore (build artifact for local type generation) - 0 TypeScript errors, 583 tests passing The facade intentionally uses `any` types to match the Prisma Client API surface, allowing existing code to run unmodified while routing queries through Drizzle at runtime.
This commit is contained in:
1 parent
9d1c71d926
commit
56061e41d4
52 files changed
+5825
-748
No files matched your search
+18
-113
@@ -1,45 +1,22 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { calcPagination, parseListParams } from "./admin-helpers";
|
||||
import { canAccess, getAdminContext, type PermissionSet } from "./permissions";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic Prisma model access requires it
|
||||
type PrismaModel = any;
|
||||
|
||||
interface AdminListConfig<TRow> {
|
||||
/** Permission slug required to view this page */
|
||||
permission: string;
|
||||
/** Prisma model name (e.g. 'user', 'ban', 'room') */
|
||||
model: string;
|
||||
/** Search field paths for OR filter. Use dot notation for nested relations (e.g. 'owner.username') */
|
||||
searchFields: string[];
|
||||
/** Default sort when no sort param provided. Default: { id: 'desc' } */
|
||||
defaultSort?: Record<string, "asc" | "desc">;
|
||||
/** Default items per page. Default: 20 */
|
||||
defaultPerPage?: number;
|
||||
/** Prisma include clause for relations */
|
||||
include?: Record<string, unknown>;
|
||||
/** Prisma select clause (mutually exclusive with include) */
|
||||
select?: Record<string, unknown>;
|
||||
/** Fixed WHERE conditions merged with search (e.g. { online: '1' }) */
|
||||
baseWhere?: Record<string, unknown>;
|
||||
/**
|
||||
* Map URL ?filter_* params to Prisma WHERE conditions. Each entry receives
|
||||
* the raw string value from the URL and returns the WHERE fragment to merge.
|
||||
* Returning null/undefined drops the filter.
|
||||
*
|
||||
* @example
|
||||
* filterMap: {
|
||||
* filter_rank: (v) => ({ rank: { gte: Number(v) } }),
|
||||
* filter_online: (v) => ({ online: v }),
|
||||
* }
|
||||
* Runs the actual data query (Drizzle). Receives parsed list params and
|
||||
* returns the rows + total count for the current page.
|
||||
*/
|
||||
filterMap?: Record<
|
||||
string,
|
||||
(value: string) => Record<string, unknown> | null | undefined
|
||||
>;
|
||||
/** Transform raw Prisma row to the shape needed by the table component */
|
||||
mapRow?: (row: PrismaModel) => TRow;
|
||||
fetch: (args: {
|
||||
search: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
rawParams: Record<string, string>;
|
||||
}) => Promise<{ rows: TRow[]; total: number }>;
|
||||
}
|
||||
|
||||
interface AdminListResult<TRow> {
|
||||
@@ -53,35 +30,12 @@ interface AdminListResult<TRow> {
|
||||
rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a nested Prisma WHERE condition from a dot-notation field path.
|
||||
* e.g. 'owner.username' => { owner: { username: { contains: search } } }
|
||||
* e.g. 'username' => { username: { contains: search } }
|
||||
*/
|
||||
function buildFieldCondition(
|
||||
fieldPath: string,
|
||||
search: string,
|
||||
): Record<string, unknown> {
|
||||
const parts = fieldPath.split(".");
|
||||
if (parts.length === 1) {
|
||||
return { [parts[0]]: { contains: search } };
|
||||
}
|
||||
// Build nested object from right to left
|
||||
let result: Record<string, unknown> = {
|
||||
[parts[parts.length - 1]]: { contains: search },
|
||||
};
|
||||
for (let i = parts.length - 2; i >= 0; i--) {
|
||||
result = { [parts[i]]: result };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic admin list data fetcher.
|
||||
* Centralizes the common pattern: auth check, param parsing, WHERE/orderBy construction,
|
||||
* Prisma query with pagination, and row mapping.
|
||||
* Centralizes the common pattern: auth check, param parsing, pagination and
|
||||
* row mapping. The caller supplies a Drizzle query via `config.fetch`.
|
||||
*/
|
||||
export async function fetchAdminList<TRow = PrismaModel>(
|
||||
export async function fetchAdminList<TRow>(
|
||||
config: AdminListConfig<TRow>,
|
||||
paramsPromise: Promise<{ locale: string }>,
|
||||
searchParamsPromise: Promise<Record<string, string>>,
|
||||
@@ -99,64 +53,15 @@ export async function fetchAdminList<TRow = PrismaModel>(
|
||||
const perPage = config.defaultPerPage
|
||||
? Number(rawParams.perPage) || config.defaultPerPage
|
||||
: parsed.perPage;
|
||||
const { search, page, sort, order } = parsed;
|
||||
|
||||
// Build WHERE clause
|
||||
const searchOr =
|
||||
search && config.searchFields.length > 0
|
||||
? config.searchFields.map((field) => buildFieldCondition(field, search))
|
||||
: [];
|
||||
// Exact numeric id match (furni / rooms / items lookup by id).
|
||||
if (search && /^\d+$/.test(search)) {
|
||||
const asId = Number(search);
|
||||
if (Number.isFinite(asId) && asId > 0) {
|
||||
searchOr.push({ id: asId });
|
||||
}
|
||||
}
|
||||
const searchWhere = searchOr.length > 0 ? { OR: searchOr } : {};
|
||||
const { rows, total } = await config.fetch({
|
||||
search: parsed.search,
|
||||
page: parsed.page,
|
||||
perPage,
|
||||
rawParams,
|
||||
});
|
||||
|
||||
// Apply URL filter_* params via filterMap
|
||||
const filterWhere: Record<string, unknown> = {};
|
||||
if (config.filterMap) {
|
||||
for (const [paramKey, build] of Object.entries(config.filterMap)) {
|
||||
const value = rawParams[paramKey];
|
||||
if (!value) continue;
|
||||
const fragment = build(value);
|
||||
if (fragment) Object.assign(filterWhere, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
const where = {
|
||||
...(config.baseWhere ?? {}),
|
||||
...filterWhere,
|
||||
...searchWhere,
|
||||
};
|
||||
|
||||
// Build orderBy
|
||||
const orderBy = sort
|
||||
? { [sort]: order }
|
||||
: (config.defaultSort ?? { id: "desc" as const });
|
||||
|
||||
// Access Prisma model dynamically
|
||||
const delegate = (prisma as PrismaModel)[config.model];
|
||||
|
||||
const queryArgs: PrismaModel = {
|
||||
where,
|
||||
orderBy,
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage,
|
||||
};
|
||||
|
||||
if (config.include) queryArgs.include = config.include;
|
||||
if (config.select) queryArgs.select = config.select;
|
||||
|
||||
const [rawRows, total] = await Promise.all([
|
||||
delegate.findMany(queryArgs),
|
||||
delegate.count({ where }),
|
||||
]);
|
||||
|
||||
const pagination = calcPagination(total, page, perPage);
|
||||
const rows = config.mapRow ? rawRows.map(config.mapRow) : rawRows;
|
||||
const pagination = calcPagination(total, parsed.page, perPage);
|
||||
|
||||
return {
|
||||
rows,
|
||||
|
||||
Reference in new issue
Block a user