Loading...
Loading...
Create and maintain server-side Prisma “service” functions for SELECT and COUNT used by Next.js Server Components, organized by domain under services/, exported via index.ts, and returning strongly typed Prisma payload types (referencing the Prisma type-settings skill).
npx skill4agent add madsnyl/t3-template query-service@src/services/@src/services/index.tsservices/types/<domain>/...@src/services/<domain>/...@src/services/users/get-users.ts@src/services/events/get-event-previews.ts@src/services/workspaces/get-workspace-members.ts@src/services/index.tsgetUsersgetWorkspaceMembersgetEventPreviewscountUsers"use server"selectincludetypes/<domain>/...Prisma.<Model>GetPayload<...>findManycountPromise.allselectincludecreatedAtidsortByorderBydb.<model>.count({ where })"use server"db~/server/dbOptionswhereorderByPromise.all([findMany, count]){
items: T[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
}usersevents@src/types/<domain>/...src/types/users/user.select.tssrc/types/users/user.types.ts// src/types/users/user.select.ts
import { Prisma } from "@prisma/client";
export const userListSelect = Prisma.validator<Prisma.UserSelect>()({
id: true,
name: true,
email: true,
emailVerified: true,
isAdmin: true,
createdAt: true,
});// src/types/users/user.types.ts
import { Prisma } from "@prisma/client";
import { userListSelect } from "./user.select";
export type UserListItem = Prisma.UserGetPayload<{
select: typeof userListSelect;
}>;// src/services/users/get-users.ts
"use server";
import { db } from "~/server/db";
import { userListSelect } from "~/types/users/user.select";
import type { UserListItem } from "~/types/users/user.types";
interface GetUsersOptions {
page?: number;
pageSize?: number;
search?: string;
sortBy?: "createdAt" | "name" | "email"; // whitelist
sortOrder?: "asc" | "desc";
}
type GetUsersResult = {
users: UserListItem[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
};
export async function getUsers(options: GetUsersOptions = {}): Promise<GetUsersResult> {
const {
page = 1,
pageSize = 10,
search = "",
sortBy = "createdAt",
sortOrder = "desc",
} = options;
const safePage = Math.max(1, page);
const safePageSize = Math.min(Math.max(1, pageSize), 200);
const skip = (safePage - 1) * safePageSize;
const where = search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ email: { contains: search, mode: "insensitive" as const } },
],
}
: {};
// Stable ordering: requested field + id tie-breaker
const orderBy = [{ [sortBy]: sortOrder } as const, { id: "asc" as const }];
const [users, totalCount] = await Promise.all([
db.user.findMany({
where,
select: userListSelect,
orderBy,
skip,
take: safePageSize,
}),
db.user.count({ where }),
]);
return {
users,
totalCount,
page: safePage,
pageSize: safePageSize,
totalPages: Math.ceil(totalCount / safePageSize),
};
}src/services/index.ts// src/services/index.ts
export * from "./users/get-users";
export * from "./events/get-event-previews";index.tsimport { getUsers } from "~/services";
export default async function UsersPage({ searchParams }: { searchParams: Record<string, string | string[]> }) {
const page = Number(searchParams.page ?? 1);
const data = await getUsers({
page,
pageSize: 20,
search: typeof searchParams.q === "string" ? searchParams.q : "",
sortBy: "createdAt",
sortOrder: "desc",
});
return (
<div>
<div>Total: {data.totalCount}</div>
{/* render data.users */}
</div>
);
}orderBy: { [sortBy]: sortOrder }sortByconst SORT_KEYS = {
createdAt: "createdAt",
name: "name",
email: "email",
} as const;
type SortBy = keyof typeof SORT_KEYS;containscountfindManytypes/<domain>/...services/<domain>/<service>.tsservices/index.tsPrisma.validator()GetPayloadtypes/<domain>/...