ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

在 Keystone 6 中使用 graphql-ts 扩展 GraphQL Schema:自定义 Query、Mutation 与类型实战

在 Keystone 6 中使用 graphql-ts 扩展 GraphQL Schema:自定义 Query、Mutation 与类型实战 后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载本文以仓库中的 extend-graphql-schema-graphql-ts 示例 为核心完整讲解如何通过graphql.extendGraphqlSchema配置项与graphql-ts/schema、graphql-ts/extend为 Keystone 自动生成的 GraphQL API 增加自定义查询、变更与全新对象类型。读完本文你将掌握g.extend的类型安全扩展写法、base.object复用现有类型、context.db与context.query的选型差异以及如何利用base.schema.extensions.scope区分外部 API 与内部 schema。示例项目概览一个最小可运行的 Schema 扩展工程该示例位于 examples/extend-graphql-schema-graphql-ts是一个独立的、可直接运行的 Keystone 6 工程其目录结构如下文件作用keystone.tsKeystone 入口配置声明 SQLite 数据库并在graphql段接入extendGraphqlSchemaschema.ts定义Post/Author两个列表并用g.extend编写全部自定义 Query 与 Mutationschema.graphqlKeystone 自动生成并提交入库的 GraphQL Schema 文件可直接核对扩展结果schema.prismaKeystone 依据列表定义自动生成的 Prisma 模型文件prisma.config.tsPrisma 配置schema 路径、迁移目录、数据源 URLpackage.json工程脚本与依赖声明README 中明确说明该项目建立在 Keystone 官方的 Blog 示例项目之上从 schema.ts 的列表定义可以确认它沿用了博客场景中作者Author— 文章Post的一对多关系模型并在此基础上演示如何为这套模型扩充聚合统计、批量发布等原生 CRUD 之外的能力。环境准备与快速启动按照 README 的 Instructions运行方式如下克隆 Keystone 仓库到本地在仓库根目录执行pnpm install安装依赖仓库使用 pnpm workspace 管理所有 packages 与 examples根目录存在 pnpm-workspace.yaml示例中的keystone-6/core依赖以workspace:^形式指向本地源码包进入示例目录并启动开发服务器cd examples/extend-graphql-schema-graphql-ts pnpm devpnpm dev实际执行的是keystone dev见 package.json它会启动开发服务器并在localhost:3000提供两个入口Admin UIhttp://localhost:3000用于在数据库中创建Post/Author数据方便后续验证自定义接口GraphQL Playgroundhttp://localhost:3000/api/graphql可直接运行 GraphQL 查询与变更是验证自定义 Query / Mutation 的最快途径。工程还提供了完整的构建与运行脚本pnpm build # keystone build生成 Prisma Client、GraphQL Schema 与类型产物 pnpm start # keystone start以生产模式启动 pnpm check # keystone postinstall校验工程配置数据库使用 SQLite数据文件默认为file:./keystone-example.db可通过环境变量DATABASE_URL覆盖见 keystone.ts 与 prisma.config.ts。接入方式graphql.extendGraphqlSchema配置项扩展逻辑的挂载点位于 Keystone 配置的graphql.extendGraphqlSchema选项。完整的配置入口如下// keystone.ts import { PrismaBetterSqlite3 } from prisma/adapter-better-sqlite3 import { config } from keystone-6/core import { lists, extendGraphqlSchema } from ./schema export default config({ db: { provider: sqlite, prismaClientOptions: () ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL || file:./keystone-example.db, }), }), }, graphql: { extendGraphqlSchema, }, lists, })从 Keystone 源码的类型定义看该配置项签名是extendGraphqlSchema?: (schema: GraphQLSchema) GraphQLSchema见 packages/core/src/types/config/index.ts——它接收一个由 Keystone 根据列表定义生成的GraphQLSchema并要求返回一个合法的 GraphQL Schema。在 packages/core/src/lib/graphql.ts 中createGraphQLSchema先基于列表构建基础 schema再执行// merge in the user defined graphQL API return config.graphql?.extendGraphqlSchema?.(graphQLSchema) ?? graphQLSchema也就是说无论你使用g.extend、graphql-tools/schema的mergeSchemas还是其他第三方 schema 工具只要传入一个「输入基础 schema、输出新 schema」的函数即可完成扩展。本示例选择的是 Keystone 官方推荐的graphql-ts方案即g.extend(base ({ query, mutation }))。数据模型Post 与 Author 列表扩展逻辑作用的对象是 schema.ts 中定义的两个列表export const lists { Post: list({ access: allowAll, fields: { title: text({ validation: { isRequired: true } }), status: select({ type: enum, options: [ { label: Draft, value: draft }, { label: Published, value: published }, { label: Banned, value: banned }, ], }), content: text(), publishDate: timestamp(), author: relationship({ ref: Author.posts, many: false }), }, }), Author: list({ access: allowAll, fields: { name: text({ validation: { isRequired: true } }), email: text({ isIndexed: unique, validation: { isRequired: true } }), posts: relationship({ ref: Post.author, many: true }), }, }), } satisfies Lists值得注意的点status是一个select枚举字段取值draft/published/banned后续publishPost、banPost两个自定义 Mutation 正是围绕它实现的satisfies Lists让lists接受Lists类型检查同时保留字面量类型推断便于g.extend内联扩展时对base.object(Post)等字符串引用保持精确对应的 Prisma 模型schema.prisma由 Keystone 自动生成Post.author与Author.posts通过关系字段Post_author关联。g.extend 的类型安全机制从gWithContext到g示例中扩展代码的头部有一行容易被忽略的关键声明import { gWithContext, list } from keystone-6/core import type { Context, Lists } from ./generated/keystone/types const g gWithContextContext() type gT gWithContext.inferT这里的Context来自 Keystone 生成的./generated/keystone/typeskeystone dev/build时自动生成它描述了每个列表的精确返回类型。gWithContext的底层实现在 packages/core/src/types/schema/gWithContext.ts它本质上是graphql-ts/schema的GWithContext、Keystone 标量集合与graphql-ts/extend的extend函数的组合export function gWithContextContext extends KeystoneContextany(): GWithContextContext typeof scalars { extend: typeof extend } { return { ...baseGWithContextContext(), ...scalars, extend, } }而 Keystone 默认导出的g则是预先绑定到 Keystone 默认KeystoneContext的单例见 packages/core/src/types/schema/g.tsexport const g gWithContextKeystoneContext()用法要点如果你的 resolver 只需使用 Keystone 标准的Context直接import { g } from keystone-6/core即可只有当你的应用给 context 注入了自定义字段、需要更精确的类型时才用gWithContextMyContext()重新绑定。本示例采用后者展示了面向自定义 context 的推荐写法。g.extend的回调参数base提供两个核心能力base.object(Post)从基础 schema 中按名称取出已有对象类型不存在则抛错用于复用 Keystone 生成的列表类型作为返回类型base.schema.extensions.scope读取基础 schema 上携带的public | internal标记来源见下文用于区分当前构建的是对外 API 还是内部 schema。自定义类型 Statistics聚合统计的三种字段写法示例用g.object定义了一个全新的Statistics对象类型演示定义新类型 在 resolver 中组合查询const Statistics g.object{ authorId: string }()({ name: Statistics, fields: { draft: g.field({ type: g.Int, resolve({ authorId }, args, context) { return context.query.Post.count({ where: { author: { id: { equals: authorId } }, status: { equals: draft } }, }) }, }), published: g.field({ type: g.Int, resolve({ authorId }, args, context) { return context.query.Post.count({ where: { author: { id: { equals: authorId } }, status: { equals: published } }, }) }, }), latest: g.field({ type: base.object(Post), async resolve({ authorId }, args, context) { const [post] await context.db.Post.findMany({ take: 1, orderBy: { publishDate: desc }, where: { author: { id: { equals: authorId } } }, }) return post }, }), }, })三个字段分别示范了三种典型写法draft/published使用context.query.Post.count统计某个作者对应状态的文章数。context.query返回的是普通数据对象适合用于Int这类标量结果latest使用context.db.Post.findMany取该作者publishDate最新的一篇文章返回类型声明为base.object(Post)即复用 Keystone 的Post类型。这里必须使用context.db——因为返回类型是 Keystone 的列表对象context.db提供的是符合 GraphQL 输出格式的内部对象若误用context.query会因字段格式不符而在客户端解析时出错源码注释明确警告了这一点g.object{ authorId: string }()的泛型参数声明了该类型的 source 形状——stats查询的 resolver 只返回{ authorId: id }真正的聚合计算被下放到各字段的 resolver 中执行这是 GraphQL 对象类型「按需懒解析」的典型用法。自定义 MutationpublishPost 与仅内部可见的 banPostreturn { mutation: { publishPost: g.field({ type: base.object(Post), args: { id: g.arg({ type: g.nonNull(g.ID) }) }, resolve(source, { id }, context) { return context.db.Post.updateOne({ where: { id }, data: { status: published, publishDate: new Date().toISOString() }, }) }, }), // only add this mutation to the internal schema (this is not usable from the API) ...(base.schema.extensions.scope internal ? { banPost: g.field({ type: base.object(Post), args: { id: g.arg({ type: g.nonNull(g.ID) }) }, resolve(source, { id }, context) { return context.db.Post.updateOne({ where: { id }, data: { status: banned }, }) }, }), } : {}), }, ... }publishPost接受一个非空的ID参数g.nonNull(g.ID)把对应文章的状态改为published并写入当前时间返回更新后的Post。它直接调用context.db.Post.updateOne与Statistics.latest同理因为返回类型是base.object(Post)。banPost演示了一个进阶技巧通过base.schema.extensions.scope internal判断当前构建的 schema 是内部还是公开版本从而把某些变更只暴露给内部 schema。其原理在 packages/core/src/lib/graphql.ts 与 packages/core/src/lib/graphql.tscreateGraphQLSchema会以scope: public | internal两种 scope 分别构建 schema并把 scope 写入GraphQLSchema的extensions。因此banPost会被合并进内部 schema供 Admin UI 等内部场景使用却不会出现在对外 API 中。这一点在 tests/api-tests/extend-graphql-schema.test.ts 中有直接验证it(Identifies whether the schema is public or internal, runner(async () { expect(observedSchemaExtensions.slice(-2)).toEqual([ { scope: public }, { scope: internal }, ]) }))自定义 QueryrecentPosts 与 statsquery: { recentPosts: g.field({ type: g.list(g.nonNull(base.object(Post))), args: { id: g.arg({ type: g.nonNull(g.ID) }), seconds: g.arg({ type: g.nonNull(g.Int), defaultValue: 600 }), }, resolve(source, { id, seconds }, context) { const cutoff new Date(Date.now() - seconds * 1000) return context.db.Post.findMany({ where: { author: { id: { equals: id } }, publishDate: { gt: cutoff } }, }) }, }), stats: g.field({ type: Statistics, args: { id: g.arg({ type: g.nonNull(g.ID) }) }, resolve(source, { id }) { return { authorId: id } }, }), },recentPosts返回「某个作者最近seconds秒内发布publishDate大于截止时间的文章列表」返回类型是g.list(g.nonNull(base.object(Post)))——非空元素的Post列表。seconds参数带defaultValue: 600调用方不传时默认取最近 10 分钟stats返回上节定义的Statistics类型resolver 只负责把id塞进 source{ authorId: id }具体聚合交给字段级 resolver。这两条查询是标准的 GraphQL 分层 resolver 实践外层查询只做参数透传真正的数据获取与过滤下沉到字段或依赖 Keystone 内置 API 完成。验证扩展结果自动生成的 schema.graphql运行pnpm dev后Keystone 会在工程根目录生成/更新schema.graphql。在该示例提交的 schema.graphql 中可以直观核对上述扩展是否生效type Mutation { createPost(data: PostCreateInput!): Post # ... 其余 CRUD 变更 publishPost(id: ID!): Post } type Query { # ... 其余列表查询 recentPosts(id: ID!, seconds: Int! 600): [Post!] stats(id: ID!): Statistics } type Statistics { draft: Int published: Int latest: Post }注意两点文件头部注释明确写着「This file is automatically generated by Keystone, do not modify it manually」因此扩展 API 的唯一正确入口是g.extend代码而不是直接编辑schema.graphqlMutation中只有publishPost没有banPost——与「内部 schema 专属」的设计一致Query中新增的recentPosts、stats以及Statistics类型与源码定义一一对应说明g.extend的扩展结果会被完整并入最终 schema。配套测试与延伸阅读仓库的 API 测试 tests/api-tests/extend-graphql-schema.test.ts 为extendGraphqlSchema提供了行为级佐证除了上文提到的 scope 断言外还覆盖了自定义 Query 正常执行double(x: 10)返回 20自定义 Mutation 正常执行triple(x: 10)返回 30扩展字段同样受 access control 约束quads因访问函数返回 false 而抛出Access deniedKeystone 内置 resolver 不受扩展影响createUser照常工作。这四条用例可以当作你为自定义扩展编写测试时的参照模板。若想进一步深入仓库内还有两处高价值资料GraphQL Schema 扩展指南系统讲解g.extend与第三方工具graphql-tools/schema的mergeSchemas两种路线并提示keystone-6/core3.0 之前由graphQLSchemaExtension导出的能力已改为直接使用第三方工具config.md 中 extendGraphqlSchema 章节给出配置项的类型签名与基础示例同目录下的姊妹示例 extend-graphql-schema-graphql-tools 与 extend-graphql-schema-nexus分别演示使用graphql-tools/schema和 Nexus 完成同样的扩展目标方便对比不同工具链的取舍。总的来说本示例是理解 Keystone GraphQL 扩展机制的理想起点g.extend提供了与graphql-ts/schema一脉相承的类型安全体验base.object打通了自定义类型与既有列表类型的复用而scope机制则让你能够精细控制扩展能力在对外 API 与内部 schema 之间的暴露范围。赞分享后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载相关推荐使用 Nexus 扩展 Keystone 的 GraphQL API自定义 Query/Mutation 实战指南使用 Nexus 扩展 Keystone 的 GraphQL API自定义 Query/Mutation 实战指南 导读 Keystone 会自动根据你的列表后端使用 graphql-tools/schema 扩展 Keystone GraphQL API自定义查询、变更与类型的完整实战使用 graphql tools/schema 扩展 Keystone GraphQL API自定义查询、变更与类型的完整实战 导读 Keystone 会后端免费 macOS 录屏教程QuickRecorder 7 种录制模式快速上手指南免费 macOS 录屏教程QuickRecorder 7 种录制模式快速上手指南 录制一堂网课、做一段产品演示或者想保存别人屏幕上的操作往往会被商业录屏软桌面应用音视频屏幕录制上一篇Chewie性能优化实战解决Android缓冲状态问题的终极方案下一篇Ruby定时任务的分布式锁基于Whenever的并发控制方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表