query-service

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Services: 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
@src/services/
, are domain-scoped, have easy-to-understand names, and are re-exported via
@src/services/index.ts
. They use the Prisma type patterns defined in the prisma types skill(Prisma.validator + GetPayload) for return types.
你需要实现仅服务端运行的服务函数,为Next.js Server Components(RSC)执行Prisma SELECT和COUNT查询。
这些服务存放在
@src/services/
目录下,按领域划分,拥有易懂的命名,并通过
@src/services/index.ts
重新导出。它们使用prisma types skill中定义的Prisma类型模式(Prisma.validator + GetPayload)来定义返回类型。

When 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
    services/
    folder
  • ensure return types match the project’s
    types/<domain>/...
    patterns
当用户要求以下操作时使用本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
      ,
      getEventPreviews
      ,
      countUsers
  • 服务存放路径:
    @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
      ,
      getEventPreviews
      ,
      countUsers

Hard rules

硬性规则

  1. Server-only: add
    "use server"
    at the top of each service file.
  2. 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).
  3. Type-safe outputs: the returned model shapes must come from the Prisma type-settings skill:
    • Define reusable
      select
      /
      include
      objects in
      types/<domain>/...
    • Derive payload types via
      Prisma.<Model>GetPayload<...>
  4. Parallelize data + count: for paginated lists, fetch
    findMany
    and
    count
    in
    Promise.all
    .
  5. Avoid overfetching: always use
    select
    (preferred) or strict
    include
    .
  6. Deterministic ordering: if sorting by a non-unique field (e.g.
    createdAt
    ), add a tie-breaker order by
    id
    to keep pagination stable.
  7. Validate/whitelist sorting keys: never pass arbitrary
    sortBy
    directly into
    orderBy
    without a whitelist.
  8. Performance-aware counts: use
    db.<model>.count({ where })
    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).
  1. 仅服务端运行:在每个服务文件顶部添加
    "use server"
  2. 只读职责:这些服务仅用于服务端渲染的SELECT/COUNT操作。
    • 变更操作属于tRPC控制器的职责(除非用户明确要求使用server action处理变更)。
  3. 类型安全输出:返回的模型结构必须来自Prisma类型设置skill
    • types/<domain>/...
      中定义可复用的
      select
      /
      include
      对象
    • 通过
      Prisma.<Model>GetPayload<...>
      推导负载类型
  4. 数据与计数并行获取:对于分页列表,使用
    Promise.all
    同时执行
    findMany
    count
    查询。
  5. 避免过度获取:始终使用
    select
    (优先选择)或严格的
    include
  6. 确定的排序规则:如果按非唯一字段(如
    createdAt
    )排序,需添加
    id
    作为排序的平局决胜项,以保持分页稳定性。
  7. 验证/白名单排序键:永远不要将任意
    sortBy
    直接传入
    orderBy
    ,必须经过白名单验证。
  8. 性能优先的计数查询:简单计数使用
    db.<model>.count({ where })
    ;如果计数查询变得复杂且缓慢,可参考Prisma查询skill使用原生SQL进行只读优化(但仅作为例外情况,且需保持参数化)。

Service function structure

服务函数结构

Each service file should typically contain:
  • "use server"
  • db
    import from
    ~/server/db
  • An
    Options
    interface (only if needed)
  • where
    builder (search/filter)
  • orderBy
    builder with a whitelist
  • Promise.all([findMany, count])
  • Return a typed result object
每个服务文件通常应包含:
  • "use server"
    声明
  • ~/server/db
    导入
    db
  • 一个
    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 (
users
,
events
) if that improves clarity, but keep the structure consistent.
ts
{
  items: T[];
  totalCount: number;
  page: number;
  pageSize: number;
  totalPages: number;
}
如果使用领域专属名称(如
users
events
)能提升清晰度,可替换
items
,但需保持结构一致。

Typing: 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>/...

1) 在
@src/types/<domain>/...
中定义可复用的select对象

Example:
  • src/types/users/user.select.ts
  • src/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.ts
  • src/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.ts
:
ts
// src/services/index.ts
export * from "./users/get-users";
export * from "./events/get-event-previews";
If you also keep per-domain
index.ts
files, export them from the root.
每个领域的服务必须通过
src/services/index.ts
导出:
ts
// src/services/index.ts
export * from "./users/get-users";
export * from "./events/get-event-previews";
如果也有按领域划分的
index.ts
文件,需从根索引文件导出这些文件。

Usage 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
sortBy
is a union of known keys (or validated via a whitelist map). Preferred pattern:
ts
const SORT_KEYS = {
  createdAt: "createdAt",
  name: "name",
  email: "email",
} as const;

type SortBy = keyof typeof SORT_KEYS;
严禁这样做:
ts
orderBy: { [sortBy]: sortOrder }
除非
sortBy
是已知键的联合类型(或通过白名单映射验证)。推荐模式:
ts
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
    contains
    search becomes slow (only if the user asks for scaling guidance).
对于大型数据表:
  • 确保高选择性筛选条件对应有索引。
  • 如果
    contains
    搜索变慢,可考虑在Postgres中使用全文搜索或 trigram 索引(仅当用户询问扩展方案时)。

When count is too slow

当计数查询过慢时

If
count
becomes a bottleneck (complex filters/joins), you may:
  • keep
    findMany
    in Prisma
  • 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:
  1. types/<domain>/...
    select + payload type (if missing)
  2. services/<domain>/<service>.ts
    implementation
  3. services/index.ts
    export
  4. Example Server Component usage
当被要求创建服务时,需输出以下内容:
  1. types/<domain>/...
    中的select对象 + 负载类型(如果不存在)
  2. services/<domain>/<service>.ts
    实现代码
  3. services/index.ts
    导出代码
  4. 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()
    +
    GetPayload
    in
    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控制器中,除非另有说明。