- 后端
【免费下载链接】keystone
The superpowered headless CMS for Node.js — built with GraphQL and React
导读
在 Headless CMS 的日常使用中,"一次查询返回多少条记录"是一个容易被忽视却直接影响性能与安全的配置。本指南基于 Keystone 官方示例项目 examples/limits 展开,演示如何通过graphql.maxTake为列表的 GraphQL 查询设置"单次最多返回记录数"上限,并深入源码剖析该限制在 Keystone 底层是如何被生成进 Schema、又如何被强制执行与报错的。读完本文,你将掌握maxTake的配置写法、它的默认值行为、超出上限时的报错形态,以及如何用示例自带的批量种子脚本做压测验证。
示例项目概览:一个只展示 limits 能力的最小工程
examples/limits/README.md 开门见山地说明:这个项目专门用来演示在 Keystone 中给 GraphQL Schema 添加不同类型上限(limits)的能力。它是刻意做小的"最小可运行工程",方便你专注观察maxTake一个特性。
项目的核心文件布局如下:
- keystone.ts:Keystone 入口配置,使用 SQLite(better-sqlite3 adapter)作为数据库;
- schema.ts:定义了唯一的
Post列表,并在其中配置graphql.maxTake: 20; - seed-data.ts:批量造数脚本,默认写入10 万条
Post记录,用于验证上限效果; - package.json:提供
dev、seed-data等 npm scripts; - schema.graphql:Keystone 根据配置自动生成的 GraphQL Schema 快照。
可以看出,这个工程刻意将业务复杂度降到最低——只有一个Post列表、只有一个title字段——全部注意力都集中在maxTake这一个配置点上。
快速启动:clone、安装、运行
按照 examples/limits/README.md 中的说明,运行步骤如下:
- 在本地 clone Keystone 仓库后,在仓库根目录执行
pnpm install安装全部 workspace 依赖; - 进入示例目录并启动开发服务:
pnpm dev- 启动成功后:
- Admin UI运行在
http://localhost:3000,你可以在界面中直接创建数据库记录; - GraphQL Playground运行在
http://localhost:3000/api/graphql,可以直接执行查询(query)和变更(mutation)。
可选:灌入示例数据
示例自带了一个非常"激进"的造数脚本,用于把数据量撑到能触发上限的程度。使用方法:
- 至少先运行一次
pnpm dev,确保数据库已初始化; - 执行
pnpm seed-data灌入示例数据; - 重新运行
pnpm dev,此时 Admin UI 中即包含样例数据。
值得说明的是,seed-data.ts 的实现是把1e5(即 10 万)条Post循环写入数据库:
for (let i = 0; i < 1e5; ++i) { console.log(`...Post.createOne ${i}`) await context.db.Post.createOne({ data: { title: `Post #${i}, run ${run}` } }) }这个脚本刻意不追求速度(每条都await且打印日志),目的就是让你能直观地感受到:当列表里躺着 10 万条记录时,如果不设上限,一个不带take的查询会造成多大的数据量压力——这正是maxTake要解决的问题。
核心配置:一行代码为列表加上查询上限
示例中最关键的部分在 schema.ts:
import { list } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' import { text } from '@keystone-6/core/fields' import type { Lists } from './generated/keystone/types' export const lists = { Post: list({ access: allowAll, fields: { title: text({ validation: { isRequired: true } }), }, graphql: { // enforce that only 20 posts can be retrieved in a Query // this additionally defaults the GraphQL schema 'take' value to 20 maxTake: 20, }, }), } satisfies Lists要点拆解:
graphql是list()配置中的一个可选子配置块,maxTake是该配置块中的上限声明;- 注释里写得很清楚:
maxTake: 20做了两件事——一是强制任何一次列表查询最多只返回 20 条;二是把 GraphQL Schema 中take参数的默认值也同时设为 20; - 从源码类型定义看,
maxTake声明为可选数字maxTake?: number,定义于 packages/core/src/types/config/lists.ts,并会在列表级配置合并时被读取(见 packages/core/src/types/config/index.ts)。
配置的默认值继承关系
maxTake不仅支持在单个列表上配置,也支持作为全局默认值配置。在 packages/core/src/schema.ts 中可以看到这一合并逻辑:
maxTake: list.graphql?.maxTake ?? config.listDefaults?.graphql?.maxTake,也就是说,若列表自身没有声明maxTake,则会回退使用config.listDefaults.graphql.maxTake这一全局默认值。这种"列表级覆盖、默认值兜底"的设计,让你既能全站统一设限,又能针对热点列表单独收紧。
上限如何进入 GraphQL Schema:自动生成的类型快照
配置maxTake之后,Keystone 在构建时会把上限写进自动生成的 GraphQL Schema。在 examples/limits/schema.graphql 中可以看到posts查询的完整签名:
type Query { post(where: PostWhereUniqueInput!): Post posts( where: PostWhereInput! = {} orderBy: [PostOrderByInput!]! = [] take: Int! = 20 skip: Int! = 0 cursor: PostWhereUniqueInput ): [Post!] postsCount(where: PostWhereInput! = {}): Int }请特别注意take: Int! = 20这一节:它同时体现了两层含义——
- 类型是非空的
Int!:take不再是可以省略的可选参数,而是必须存在的参数; - 默认值是
20:这正是maxTake配置被写入 Schema 的结果,来自 packages/core/src/lib/core/initialise-lists.ts 的生成逻辑:
let take: any = g.arg({ type: g.Int }) if (listConfig.graphql?.maxTake !== undefined && listConfig.graphql.maxTake !== Infinity) { take = g.arg({ type: g.nonNull(g.Int), // WARNING: used by queries/resolvers.ts to enforce the limit defaultValue: listConfig.graphql.maxTake, }) }从源码可以推断:只有显式配置了maxTake(且不是Infinity)时,take参数才会被生成为带默认值的非空参数;如果没有配置上限,take就是普通的可空Int参数。这一点也解释了为什么 schema.graphql 快照里posts的take是Int! = 20。
上限如何被执行:resolver 层的强制校验
Schema 里的默认值只是"软默认",真正硬性的强制校验发生在查询解析(resolver)阶段。在 packages/core/src/lib/core/queries/resolvers.ts 的findMany实现中:
export async function findMany(...): Promise<BaseItem[]> { const maxTake = (list.graphql.types.findManyArgs.take.defaultValue ?? Infinity) as number if (Math.abs(take ?? Infinity) > maxTake) { throw limitsExceededError({ list: list.listKey, type: 'maxTake', limit: maxTake }) } ... }这里有几个值得深挖的实现细节:
- 上限值从哪里来:
findMany直接从 Schema 层已经生成好的findManyArgs.take.defaultValue读取上限,也就是说运行时强制的上限值与 Schema 中暴露的默认值天然保持一致,避免了两处配置漂移; - 绝对值校验:代码用的是
Math.abs(take ?? Infinity) > maxTake,这意味着负数take(如-30,Prisma 语义下表示反向取数)同样受上限约束,Math.abs(-30) = 30 > 20一样会触发报错; - 无上限时的行为:当
maxTake未配置(defaultValue为undefined)时,上限回退为Infinity,此时任何take值都不会触发该校验。
超出上限时的报错形态
触发上限后会抛出limitsExceededError,其定义在 packages/core/src/lib/core/graphql-errors.ts:
export const limitsExceededError = (_: { type: string; limit: number; list: string }) => new GraphQLError('Your request exceeded server limits', { extensions: { code: 'KS_LIMITS_EXCEEDED', }, })也就是说,当你在 GraphQL Playground 中执行query { posts(take: 100) { id title } }时,会收到一条错误码为KS_LIMITS_EXCEEDED、消息为Your request exceeded server limits的 GraphQL 错误响应,而不是静默截断或返回部分数据。这种"硬失败"设计避免了"客户端以为拿到了全部数据、实际却被悄悄截断"的隐患。
数据库与运行环境说明
keystone.ts 中的数据库配置采用 SQLite 加 better-sqlite3 适配器:
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { config } from '@keystone-6/core' import { lists } from './schema' import type { TypeInfo } from './generated/keystone/types' export default config<TypeInfo>({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', }), }), }, lists, })两点提示:
- 数据库文件默认为
./keystone-example.db,可通过环境变量DATABASE_URL覆盖; - package.json 中的 scripts 与本例配套:
dev(开发模式)、start(生产启动)、build(构建)、check(postinstall 校验)、seed-data(通过tsx执行造数脚本)。其中@keystone-6/core以workspace:^引入,说明该示例设计为在 Keystone 仓库(monorepo)内直接运行,与根目录pnpm install的安装方式相呼应。
实操验证:完整走一遍上限触发链路
结合示例自带的造数脚本,可以完整复现"数据量大 → 查询被上限拦截"的链路:
- 初始化:在示例目录运行
pnpm dev,让 Keystone 完成数据库迁移与 Schema 生成; - 造数:运行
pnpm seed-data,写入 10 万条Post(脚本逐条await,可观察进度输出); - 重启服务:再次
pnpm dev,让服务带着海量数据运行; - 验证默认上限:在 GraphQL Playground 执行不带
take的查询query { posts { id title } },由于 Schema 默认take = 20,最多只会返回 20 条记录; - 验证强制拦截:执行
query { posts(take: 100) { id title } },响应将带有KS_LIMITS_EXCEEDED错误码; - 对照实验(可选):把 schema.ts 中的
maxTake改大或删除后重新运行pnpm dev,观察 schema.graphql 中posts的take类型与默认值如何随之变化——这能直观印证上一节介绍的 Schema 生成逻辑。
小结
graphql.maxTake是 Keystone 为 GraphQL 列表查询提供的一道简单而有效的防线:配置上它,Schema 中take参数自动带上默认值,resolver 层则对所有查询(包括负值take)做绝对值上限校验,超出即抛出KS_LIMITS_EXCEEDED错误。从 examples/limits 这个最小示例出发,你可以把这套机制无缝迁移到自己的真实业务中——尤其是那些记录量可能快速膨胀的列表,例如文章、日志或用户操作记录。它不能替代分页,但能保证"不管客户端怎么写查询,服务端都不会被一次请求拖垮"。
- 后端
【免费下载链接】keystone
The superpowered headless CMS for Node.js — built with GraphQL and React
相关推荐
kerberoast完全指南:从原理到实战的Kerberos攻击工具包详解
kerberoast完全指南:从原理到实战的Kerberos攻击工具包详解 kerberoast是一套针对微软Kerberos协议实现的攻击工具集,旨在帮助安全
网络安全StarRocks SHOW PROFILELIST 指南:查询 Profile 记录列表的查看与实战
StarRocks SHOW PROFILELIST 指南:查询 Profile 记录列表的查看与实战 SHOW PROFILELIST 是 StarRocks
数据库OLAP数据仓库大数据湖仓一体数据分析告别多表查询烦恼:APIJSON联表查询实战指南
告别多表查询烦恼:APIJSON联表查询实战指南 你是否还在为复杂的SQL联表查询而头疼?是否在面对一对一、一对多、多对多关系时感到无从下手?本文将带你一文掌握
后端ORM低代码
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考