This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+123
-104
@@ -7,47 +7,50 @@ import { prisma } from "./prisma";
|
||||
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 }),
|
||||
* }
|
||||
*/
|
||||
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;
|
||||
/** 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 }),
|
||||
* }
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
interface AdminListResult<TRow> {
|
||||
rows: TRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
lastPage: number;
|
||||
locale: string;
|
||||
permissions: PermissionSet;
|
||||
rank: number;
|
||||
rows: TRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
lastPage: number;
|
||||
locale: string;
|
||||
permissions: PermissionSet;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,17 +58,22 @@ interface AdminListResult<TRow> {
|
||||
* 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;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,72 +82,83 @@ function buildFieldCondition(fieldPath: string, search: string): Record<string,
|
||||
* Prisma query with pagination, and row mapping.
|
||||
*/
|
||||
export async function fetchAdminList<TRow = PrismaModel>(
|
||||
config: AdminListConfig<TRow>,
|
||||
paramsPromise: Promise<{ locale: string }>,
|
||||
searchParamsPromise: Promise<Record<string, string>>,
|
||||
config: AdminListConfig<TRow>,
|
||||
paramsPromise: Promise<{ locale: string }>,
|
||||
searchParamsPromise: Promise<Record<string, string>>,
|
||||
): Promise<AdminListResult<TRow>> {
|
||||
const { locale } = await paramsPromise;
|
||||
const { session, permissions } = await getAdminContext();
|
||||
const { locale } = await paramsPromise;
|
||||
const { session, permissions } = await getAdminContext();
|
||||
|
||||
if (!canAccess(permissions, config.permission, session.user.rank)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
if (!canAccess(permissions, config.permission, session.user.rank)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const rawParams = await searchParamsPromise;
|
||||
const sp = new URLSearchParams(rawParams);
|
||||
const parsed = parseListParams(sp);
|
||||
const perPage = config.defaultPerPage ? Number(rawParams.perPage) || config.defaultPerPage : parsed.perPage;
|
||||
const { search, page, sort, order } = parsed;
|
||||
const rawParams = await searchParamsPromise;
|
||||
const sp = new URLSearchParams(rawParams);
|
||||
const parsed = parseListParams(sp);
|
||||
const perPage = config.defaultPerPage
|
||||
? Number(rawParams.perPage) || config.defaultPerPage
|
||||
: parsed.perPage;
|
||||
const { search, page, sort, order } = parsed;
|
||||
|
||||
// Build WHERE clause
|
||||
const searchWhere =
|
||||
search && config.searchFields.length > 0
|
||||
? { OR: config.searchFields.map((field) => buildFieldCondition(field, search)) }
|
||||
: {};
|
||||
// Build WHERE clause
|
||||
const searchWhere =
|
||||
search && config.searchFields.length > 0
|
||||
? {
|
||||
OR: config.searchFields.map((field) =>
|
||||
buildFieldCondition(field, search),
|
||||
),
|
||||
}
|
||||
: {};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
};
|
||||
const where = {
|
||||
...(config.baseWhere ?? {}),
|
||||
...filterWhere,
|
||||
...searchWhere,
|
||||
};
|
||||
|
||||
// Build orderBy
|
||||
const orderBy = sort ? { [sort]: order } : (config.defaultSort ?? { id: "desc" as const });
|
||||
// Build orderBy
|
||||
const orderBy = sort
|
||||
? { [sort]: order }
|
||||
: (config.defaultSort ?? { id: "desc" as const });
|
||||
|
||||
// Access Prisma model dynamically
|
||||
const delegate = (prisma as PrismaModel)[config.model];
|
||||
// Access Prisma model dynamically
|
||||
const delegate = (prisma as PrismaModel)[config.model];
|
||||
|
||||
const queryArgs: PrismaModel = {
|
||||
where,
|
||||
orderBy,
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage,
|
||||
};
|
||||
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;
|
||||
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 [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, page, perPage);
|
||||
const rows = config.mapRow ? rawRows.map(config.mapRow) : rawRows;
|
||||
|
||||
return {
|
||||
rows,
|
||||
...pagination,
|
||||
locale,
|
||||
permissions,
|
||||
rank: session.user.rank,
|
||||
};
|
||||
return {
|
||||
rows,
|
||||
...pagination,
|
||||
locale,
|
||||
permissions,
|
||||
rank: session.user.rank,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user