query-service
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServices: Server-side Prisma Queries for Server Components
服务:供Server Components使用的服务端Prisma查询
You implement server-only service functions that run Prisma SELECT and COUNT queries for Next.js Server Components (RSC).
These services live in , are domain-scoped, have easy-to-understand names, and are re-exported via . They use the Prisma type patterns defined in the prisma types skill(Prisma.validator + GetPayload) for return types.
@src/services/@src/services/index.ts你需要实现仅服务端运行的服务函数,为Next.js Server Components(RSC)执行Prisma SELECT和COUNT查询。
这些服务存放在目录下,按领域划分,拥有易懂的命名,并通过重新导出。它们使用prisma types skill中定义的Prisma类型模式(Prisma.validator + GetPayload)来定义返回类型。
@src/services/@src/services/index.tsWhen to use this skill
何时使用该skill
Use this skill when the user asks to:
- add/refactor server-side Prisma reads used in Server Components
- implement list pages with pagination, filtering, sorting, and total counts
- organize server data access in a folder
services/ - ensure return types match the project’s patterns
types/<domain>/...
当用户要求以下操作时使用本skill:
- 添加/重构Server Components中使用的服务端Prisma读取操作
- 实现带有分页、筛选、排序和总计数的列表页面
- 在services/目录中组织服务端数据访问
- 确保返回类型符合项目的模式
types/<domain>/...
Folder & naming conventions
目录与命名规范
- Services live here:
@src/services/<domain>/... - File names are domain-specific and descriptive:
@src/services/users/get-users.ts@src/services/events/get-event-previews.ts@src/services/workspaces/get-workspace-members.ts
- Exported in:
@src/services/index.ts - Each service function name is a clear verb phrase:
- ,
getUsers,getWorkspaceMembers,getEventPreviewscountUsers
- 服务存放路径:
@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.ts - 每个服务函数名称为清晰的动词短语:
- ,
getUsers,getWorkspaceMembers,getEventPreviewscountUsers
Hard rules
硬性规则
- Server-only: add at the top of each service file.
"use server" - Read-only responsibilities: these services are for SELECT/COUNT for server rendering.
- Mutations belong in tRPC controllers (unless the user explicitly wants a server action for a mutation).
- Type-safe outputs: the returned model shapes must come from the Prisma type-settings skill:
- Define reusable /
selectobjects inincludetypes/<domain>/... - Derive payload types via
Prisma.<Model>GetPayload<...>
- Define reusable
- Parallelize data + count: for paginated lists, fetch and
findManyincount.Promise.all - Avoid overfetching: always use (preferred) or strict
select.include - Deterministic ordering: if sorting by a non-unique field (e.g. ), add a tie-breaker order by
createdAtto keep pagination stable.id - Validate/whitelist sorting keys: never pass arbitrary directly into
sortBywithout a whitelist.orderBy - Performance-aware counts: use for simple counts; if count becomes complex and slow, consider raw SQL read optimization per Prisma querying skill (but keep this as an exception and keep it parameterized).
db.<model>.count({ where })
- 仅服务端运行:在每个服务文件顶部添加。
"use server" - 只读职责:这些服务仅用于服务端渲染的SELECT/COUNT操作。
- 变更操作属于tRPC控制器的职责(除非用户明确要求使用server action处理变更)。
- 类型安全输出:返回的模型结构必须来自Prisma类型设置skill:
- 在中定义可复用的
types/<domain>/.../select对象include - 通过推导负载类型
Prisma.<Model>GetPayload<...>
- 在
- 数据与计数并行获取:对于分页列表,使用同时执行
Promise.all和findMany查询。count - 避免过度获取:始终使用(优先选择)或严格的
select。include - 确定的排序规则:如果按非唯一字段(如)排序,需添加
createdAt作为排序的平局决胜项,以保持分页稳定性。id - 验证/白名单排序键:永远不要将任意直接传入
sortBy,必须经过白名单验证。orderBy - 性能优先的计数查询:简单计数使用;如果计数查询变得复杂且缓慢,可参考Prisma查询skill使用原生SQL进行只读优化(但仅作为例外情况,且需保持参数化)。
db.<model>.count({ where })
Service function structure
服务函数结构
Each service file should typically contain:
"use server"- import from
db~/server/db - An interface (only if needed)
Options - builder (search/filter)
where - builder with a whitelist
orderBy Promise.all([findMany, count])- Return a typed result object
每个服务文件通常应包含:
- 声明
"use server" - 从导入
~/server/dbdb - 一个接口(仅在需要时添加)
Options - 构建器(搜索/筛选)
where - 带白名单的构建器
orderBy - 并行查询
Promise.all([findMany, count]) - 返回强类型的结果对象
Recommended return shape for list endpoints
列表接口推荐返回结构
ts
{
items: T[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
}Use domain-appropriate names (, ) if that improves clarity, but keep the structure consistent.
userseventsts
{
items: T[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
}如果使用领域专属名称(如、)能提升清晰度,可替换,但需保持结构一致。
userseventsitemsTyping: reference the Prisma type-settings skill
类型定义:参考Prisma类型设置skill
Do not hand-write response shapes for Prisma models. Instead:
不要手动编写Prisma模型的响应结构,应遵循以下步骤:
1) Define a reusable select in @src/types/<domain>/...
@src/types/<domain>/...1) 在@src/types/<domain>/...
中定义可复用的select对象
@src/types/<domain>/...Example:
src/types/users/user.select.tssrc/types/users/user.types.ts
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,
});You can read more about this in the types skill
ts
// 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/types/users/user.select.tssrc/types/users/user.types.ts
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,
});你可以在types skill中了解更多相关内容
ts
// src/types/users/user.types.ts
import { Prisma } from "@prisma/client";
import { userListSelect } from "./user.select";
export type UserListItem = Prisma.UserGetPayload<{
select: typeof userListSelect;
}>;2) Use those exports in the service and type the return
2) 在服务中使用这些导出并为返回值定义类型
ts
// 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),
};
}ts
// 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"; // 白名单
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 } },
],
}
: {};
// 稳定排序:请求字段 + id平局决胜项
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),
};
}Index exports
索引导出
Every domain service must be exported via :
src/services/index.tsts
// src/services/index.ts
export * from "./users/get-users";
export * from "./events/get-event-previews";If you also keep per-domain files, export them from the root.
index.ts每个领域的服务必须通过导出:
src/services/index.tsts
// src/services/index.ts
export * from "./users/get-users";
export * from "./events/get-event-previews";如果也有按领域划分的文件,需从根索引文件导出这些文件。
index.tsUsage in Next.js Server Components
在Next.js Server Components中的使用
In a Server Component:
tsx
import { 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>
);
}在Server Component中:
tsx
import { 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>总计:{data.totalCount}</div>
{/* 渲染data.users */}
</div>
);
}Advanced guidance
进阶指南
Sorting whitelist (required)
排序白名单(必填)
Never do:
ts
orderBy: { [sortBy]: sortOrder }unless is a union of known keys (or validated via a whitelist map).
Preferred pattern:
sortByts
const SORT_KEYS = {
createdAt: "createdAt",
name: "name",
email: "email",
} as const;
type SortBy = keyof typeof SORT_KEYS;严禁这样做:
ts
orderBy: { [sortBy]: sortOrder }除非是已知键的联合类型(或通过白名单映射验证)。推荐模式:
sortByts
const SORT_KEYS = {
createdAt: "createdAt",
name: "name",
email: "email",
} as const;
type SortBy = keyof typeof SORT_KEYS;Search performance
搜索性能
For large tables:
- Ensure indexes exist for high-selectivity filters.
- Consider full-text search or trigram indexes in Postgres if search becomes slow (only if the user asks for scaling guidance).
contains
对于大型数据表:
- 确保高选择性筛选条件对应有索引。
- 如果搜索变慢,可考虑在Postgres中使用全文搜索或 trigram 索引(仅当用户询问扩展方案时)。
contains
When count is too slow
当计数查询过慢时
If becomes a bottleneck (complex filters/joins), you may:
count- keep in Prisma
findMany - use parameterized raw SQL for the count query (read-only) per the Prisma querying skill
- document why this exception is used
如果成为性能瓶颈(复杂筛选/关联查询),你可以:
count- 继续使用Prisma执行
findMany - 参考Prisma查询skill使用参数化原生SQL执行计数查询(只读)
- 记录使用该例外方案的原因
Output format when implementing a new service
实现新服务时的输出格式
When asked to create a service, output:
- select + payload type (if missing)
types/<domain>/... - implementation
services/<domain>/<service>.ts - export
services/index.ts - Example Server Component usage
当被要求创建服务时,需输出以下内容:
- 中的select对象 + 负载类型(如果不存在)
types/<domain>/... - 实现代码
services/<domain>/<service>.ts - 导出代码
services/index.ts - Server Components使用示例
Cross-skill references
跨skill参考
- Prisma type-settings skill: all service return shapes that include Prisma model data must be typed via +
Prisma.validator()inGetPayload.types/<domain>/... - Prisma database-querying skill: raw SQL is acceptable for SELECT/COUNT only when Prisma cannot express the query efficiently; mutations stay in Prisma Client or tRPC controllers unless specified.
- Prisma类型设置skill:所有包含Prisma模型数据的服务返回结构,必须通过中的
types/<domain>/...+Prisma.validator()定义类型。GetPayload - Prisma数据库查询skill:仅当Prisma无法高效表达查询时,才允许为SELECT/COUNT使用原生SQL;变更操作需保留在Prisma Client或tRPC控制器中,除非另有说明。