ARTICLE DETAIL

资讯详情

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

TypeGraphQL Resolver 完全指南:用 TypeScript 类与方法定义 Query、Mutation 与字段解析器

TypeGraphQL Resolver 完全指南:用 TypeScript 类与方法定义 Query、Mutation 与字段解析器 后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载TypeGraphQL 不仅允许我们像定义普通类一样声明 GraphQL 的对象类型还允许我们以类似经典 REST 框架如 Java Spring、.NET Web API、TypeScript routing-controllers 为主线结合本仓库src/decorators与examples中的源码实现系统讲解 Resolver 类的定义、参数声明、输入类型、字段解析器以及 Resolver 继承等完整实战内容。读完本文你将掌握 TypeGraphQL 中解析器Resolver的完整建模方式并能直接照搬到自己的 GraphQL 服务中。Queries and Mutations查询与变更的声明方式Resolver 类GraphQL 世界的“控制器”首先我们创建一个 Resolver 类并用Resolver()装饰器进行标注。这个类在 TypeGraphQL 中的作用就像经典 REST 框架中的控制器Controller负责承载并组织所有 GraphQL 的查询、变更与字段解析方法Resolver() class RecipeResolver {}从本仓库的 Resolver.ts 源码可以看到Resolver()有多个重载可以不带参数直接标注一个“只负责提供解析方法、不绑定特定对象类型”的 Resolver 类也可以传入对象类型或类型函数Resolver(typeFunc)/Resolver(objectType)。不带参数时TypeGraphQL 会在没有提供对象类型的情况下抛出一个明确错误No provided object type in Resolver decorator for class ...!。所有 Resolver 类元数据最终都会通过getMetadataStorage().collectResolverClassMetadata(...)收集到全局元数据存储中。Resolver 类可以配合 DI依赖注入框架使用详见 dependency-injection.md将服务、仓储Repository等类依赖注入进来也可以直接在类内部保存数据。TypeGraphQL 保证每个 Resolver 类在应用中是单实例的Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; }定义 QueryQuery装饰器与返回类型在 Resolver 类中我们可以编写普通类方法作为 GraphQL 查询的处理器。例如添加一个recipes查询返回所有食谱的集合Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; async recipes() { // fake async in this example return await this.recipesCollection; } }要让这个方法真正成为 GraphQL 查询还需要做两件事添加Query装饰器将类方法标记为 GraphQL 查询提供返回类型。由于方法是异步的TypeScript 的反射元数据design:returntype会把这个方法的返回类型记录为Promise而不是我们想要的Recipe[]因此必须在装饰器参数中显式声明returns [Recipe]告诉 TypeGraphQL 该方法解析为Recipe对象类型的数组Resolver() class RecipeResolver { private recipesCollection: Recipe[] []; Query(returns [Recipe]) async recipes() { return await this.recipesCollection; } }从源码看Query.ts 支持三种调用形式无参数Query()、仅传选项Query(options)、传返回类型函数加选项Query(returnTypeFunc, options?)。装饰器内部通过getResolverMetadata(...)解析返回类型借助 findType.ts 读取design:returntype反射元数据并调用getMetadataStorage().collectQueryHandlerMetadata(metadata)收集查询处理器元数据。resolver-metadata.ts 中还支持通过options配置nameSchema 中的字段名、description描述、deprecationReason废弃原因与complexity复杂度等高级选项。参数声明内联Arg()与ArgsType()参数类通常查询都会带有参数——可能是资源的 id、搜索关键字或分页设置。TypeGraphQL 提供了两种声明参数的方式。方式一内联使用Arg()装饰器。由于反射系统的限制参数名需要在装饰器参数中重复书写。同时我们还可以通过defaultValue选项设置默认值该默认值会直接反映到 GraphQL Schema 中Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes( Arg(title, { nullable: true }) title?: string, Arg(servings, { defaultValue: 2 }) servings: number, ): PromiseRecipe[] { // ... } }查看 Arg.ts 源码Arg需要至少传入name还可以传入返回类型函数returnTypeFunc与ArgOptions包含nullable、defaultValue、description、deprecationReason以及校验相关选项validate等最终以kind: arg的形式收集为处理器参数元数据。方式二使用ArgsType()参数类。当参数数量较多比如分页 过滤的多个参数时内联参数会让方法签名臃肿不堪。此时可以定义一个参数类——它看起来与对象类型类很像但顶层装饰器是ArgsType()ArgsType() class GetRecipesArgs { Field(type Int, { nullable: true }) skip?: number; Field(type Int, { nullable: true }) take?: number; Field({ nullable: true }) title?: string; }对于可选字段我们既可以在Field()装饰器中通过defaultValue选项设置默认值也可以直接使用属性初始化器如take 25。两种情况 TypeGraphQL 都会将其反映到 Schema 中设置默认值并使字段变为可空。此外参数类的声明方式还允许我们进行数据校验详见 validation.md与class-validator配合使用。同时参数类中还可以定义辅助字段与方法。但请注意在参数类或输入类中定义构造函数是严格禁止的——因为 TypeGraphQL 会在底层自行创建参数类与输入类的实例我们应该避免手动管理它们的构造过程import { Min, Max } from class-validator; ArgsType() class GetRecipesArgs { Field(type Int, { defaultValue: 0 }) Min(0) skip: number; Field(type Int) Min(1) Max(50) take 25; Field({ nullable: true }) title?: string; // helpers - index calculations get startIndex(): number { return this.skip; } get endIndex(): number { return this.skip this.take; } }然后在 Resolver 方法中把参数类作为方法参数类型即可。我们还可以使用解构语法将参数类中的单个字段直接解构为独立变量而不必持有整个参数对象Resolver() class RecipeResolver { // ... Query(returns [Recipe]) async recipes(Args() { title, startIndex, endIndex }: GetRecipesArgs) { // sample implementation let recipes this.recipesCollection; if (title) { recipes recipes.filter(recipe recipe.title title); } return recipes.slice(startIndex, endIndex); } }以上声明会在 Schema 的 SDL 中生成如下内容type Query { recipes(skip: Int 0, take: Int 25, title: String): [Recipe!] }注意观察skip与take的默认值0、25来自上面的defaultValue选项与属性初始化器title因nullable: true而成为可空参数返回值[Recipe!]表示数组本身不可空、元素不可空。Input 类型为 Mutation 定义输入数据GraphQL 的 Mutation 创建方式与 Query 类似声明类方法、使用Mutation装饰器、创建参数、提供返回类型如需要等。但 Mutation 通常使用input类型作为输入载体因此 TypeGraphQL 允许我们像定义对象类型一样创建输入类型只是顶层装饰器换成了InputType()InputType() class AddRecipeInput {}为了确保不会意外更改属性类型我们可以利用 TypeScript 的类型检查系统让输入类实现PartialRecipeInputType() class AddRecipeInput implements PartialRecipe {}接着用Field()装饰器声明需要的输入字段InputType({ description: New recipe data }) class AddRecipeInput implements PartialRecipe { Field() title: string; Field({ nullable: true }) description?: string; }从 InputType.ts 源码可以看到InputType支持传入name与descriptionInputTypeOptions最终通过collectInputMetadata收集输入类元数据。仓库中的真实示例 recipe.input.ts 正是这种写法RecipeInput实现PartialRecipe其中title必填、description可空。之后我们就可以在 Mutation 中直接使用AddRecipeInput类型了——既可以内联使用通过Arg()也可以像上面的查询示例那样作为参数类的字段使用。Mutation 中还经常需要访问上下文Context。为此我们可以使用Ctx()装饰器并配合自定义的Context接口获得类型提示Resolver() class RecipeResolver { // ... Mutation() addRecipe(Arg(data) newRecipeData: AddRecipeInput, Ctx() ctx: Context): Recipe { // sample implementation const recipe RecipesUtils.create(newRecipeData, ctx.user); this.recipesCollection.push(recipe); return recipe; } }因为该方法是同步的、且显式返回Recipe所以这里可以省略Mutation()的类型标注TypeGraphQL 能直接读取反射元数据。在 Ctx.ts 的源码中Ctx还支持传入propertyName用于只注入上下文中的某个属性例如Ctx(user) user: User。上面的声明会在 Schema 中生成input AddRecipeInput { title: String! description: String }type Mutation { addRecipe(data: AddRecipeInput!): Recipe! }参数装饰器带来的整洁与可测试性通过使用各种参数装饰器Arg、Args、Ctx等我们可以彻底摆脱像root这样无用的参数——否则这些参数会污染方法签名并且必须通过给参数名加_前缀来忽略。同时装饰器让我们得以在 GraphQL 层与业务代码之间实现清晰的分离Resolver 及其方法表现得就像普通服务Service一样可以非常方便地进行单元测试。Field resolvers字段解析器查询和变更并不是解析器的全部。我们经常需要为对象类型的某个字段编写解析器——例如user类型有一个posts字段它需要从数据库中按关联关系拉取数据。TypeGraphQL 中的字段解析器与查询、变更非常相似——同样作为 Resolver 类上的方法但有几点不同。首先通过向Resolver装饰器传入对象类型声明我们要为哪个对象类型的字段提供解析Resolver(of Recipe) class RecipeResolver { // queries and mutations }然后创建一个将成为字段解析器的类方法。在下面的例子中Recipe对象类型有一个averageRating字段需要根据ratings数组计算平均值Resolver(of Recipe) class RecipeResolver { // queries and mutations averageRating(recipe: Recipe) { // ... } }接着用FieldResolver()装饰器将该方法标记为字段解析器。由于字段类型已经在Recipe类定义中声明过了这里无需重新定义返回类型。同时我们用Root装饰器标注方法参数以便注入父对象即当前被解析的recipe对象Resolver(of Recipe) class RecipeResolver { // queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }从 FieldResolver.ts 源码看FieldResolver会尽力通过findType读取design:returntype来推断返回类型若推断失败则静默跳过交由对象类型类中的字段定义兜底并以kind: external收集为外部字段解析器元数据同时支持nameSchema 字段名、description、deprecationReason、complexity等高级选项。为了获得更强的类型安全我们还可以让 Resolver 类实现ResolverInterfaceRecipe接口。ResolverInterface.ts 是一个小而实用的辅助类型它检查字段解析器方法的返回类型如averageRating(...)是否与Recipe类上对应属性averageRating的类型一致并且要求方法第一个参数就是对应的对象类型Recipe类Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { // ... } }下面是averageRating字段解析器的完整实现Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { // queries and mutations FieldResolver() averageRating(Root() recipe: Recipe) { const ratingsSum recipe.ratings.reduce((a, b) a b, 0); return recipe.ratings.length ? ratingsSum / recipe.ratings.length : null; } }仓库中 recipe.resolver.ts 提供了一个更完整的实战版本ratingsCount字段解析器同时使用Root()注入父对象、Arg()声明参数minRate带默认值0充分展示了字段解析器也可以拥有自己的参数。内联字段解析器简单的派生字段对于像averageRating这样的简单解析或像别名alias一样行为的已废弃字段我们也可以直接在对象类型类内部以内联方式创建字段解析器ObjectType() class Recipe { Field() title: string; Field({ deprecationReason: Use title instead }) get name(): string { return this.title; } Field(type [Rate]) ratings: Rate[]; Field(type Float, { nullable: true }) averageRating(Arg(since) sinceDate: Date): number | null { const ratings this.ratings.filter(rate rate.date sinceDate); if (!ratings.length) return null; const ratingsSum ratings.reduce((a, b) a b, 0); return ratingsSum / ratings.length; } }可以看到内联字段解析器同样支持Field的deprecationReason废弃原因选项以及Arg参数。但请注意取舍如果代码比较复杂、且带有副作用如调用外部 API、从数据库取数应该使用 Resolver 类方法的形式这样可以利用依赖注入机制对测试非常友好。例如import { Repository } from typeorm; Resolver(of Recipe) class RecipeResolver implements ResolverInterfaceRecipe { constructor( private userRepository: RepositoryUser, // dependency injection ) {} FieldResolver() async author(Root() recipe: Recipe) { const author await this.userRepository.findById(recipe.userId); if (!author) throw new SomethingWentWrongError(); return author; } }此外还有一个值得一提的特性如果某个字段解析器的字段名在对应的对象类型中不存在TypeGraphQL 会自动在 Schema 中以该名字创建一个新字段。这个特性非常适合纯粹可计算的字段例如由ratings数组派生的averageRating可以避免把这些派生字段写进类签名、污染对象类型类。Resolver Inheritance解析器继承Resolver 类的继承属于进阶主题TypeGraphQL 支持通过面向对象的继承机制复用和扩展解析方法具体细节见 inheritance.md 中的 “Resolvers inheritance” 章节resolvers-inheritance 示例 提供了person与recipe两套可运行的继承示例代码。其核心思路是父类中的Query、Mutation、FieldResolver等方法在子类中依然有效子类可以在此基础上新增解析方法或重写行为。更多实战示例本文中的代码示例仅用于教学演示。仓库中 examples 目录提供了大量更真实、更完整的可运行示例适合继续深入学习simple-usage最简单的 Query / Mutation / FieldResolver / InputType 组合用法覆盖了本文介绍的全部核心概念simple-subscriptions 与 redis-subscriptions基于Subscription的订阅解析器middlewares-custom-decorators解析器方法与中间件、自定义装饰器的配合resolvers-inheritanceResolver 继承的具体落地实现automatic-validation 与 custom-validation参数类与输入类的校验实战。以 simple-usage/recipe.resolver.ts 为例一个完整覆盖查询、变更与字段解析器的 Resolver 类大致长这样recipe(title)查询带Arg参数与可空返回类型、recipes()查询返回数组、addRecipe(recipe)变更接收InputType输入类以及ratingsCount字段解析器同时使用Root与Arg。它同时实现了ResolverInterfaceRecipe以获得编译期类型检查并配合Query/Mutation的_returns参数下划线前缀用于规避未使用变量警告显式声明返回类型。小结在 TypeGraphQL 中一切解析逻辑都可以收敛到“类 装饰器”这一套统一的心智模型中Resolver()声明 Resolver 类相当于 REST 控制器可配合 DI 容器、保证单实例Query()/Mutation()将类方法声明为查询与变更处理器通过returns [...]显式提供返回类型Arg()与ArgsType()/Args()分别支持内联参数与参数类两种声明方式参数类支持默认值、校验与辅助方法InputType()为 Mutation 提供输入类型可配合PartialT获得类型检查Ctx()注入上下文FieldResolver()Root()实现对象类型字段的解析ResolverInterfaceT提供编译期类型保障简单派生字段可内联在对象类型类中带副作用的复杂解析则应放在 Resolver 类方法中以便依赖注入与测试。理解这些核心机制再加上src/decorators中每个装饰器的源码实现作为参照你就能自如地设计任何规模的 GraphQL Schema 与解析层了。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐TypeGraphQL Resolvers 完全指南用 TypeScript 类与装饰器构建 Query、Mutation 与 Field ResolverTypeGraphQL Resolvers 完全指南用 TypeScript 类与装饰器构建 Query、Mutation 与 Field Resolver后端GraphQLAPI设计TypeGraphQL 继承指南复用类型定义与 Resolver 基类的完整实践TypeGraphQL 继承指南复用类型定义与 Resolver 基类的完整实践 TypeGraphQL 的核心思想是用 TypeScript 类来定义 G后端GraphQLAPI设计TypeGraphQL 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 ResolverTypeGraphQL 入门用 TypeScript 类与装饰器构建 GraphQL Schema 与 Resolver TypeGraphQL 是一个面向后端GraphQLAPI设计上一篇UniGetUI 命令行接口CLI完全指南动词命令语法、自动化 IPC 传输与退出码详解下一篇Beads 语义查重实战bd find-duplicates 命令的机械相似度与 AI 判定机制解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表