
后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载本指南讲解 TypeGraphQL 的 Query Complexity查询复杂度能力你可以在Field、FieldResolver、Mutation、Subscription装饰器上为每个字段声明成本再借助graphql-query-complexity在任意 GraphQL 服务器中分析查询 AST、估算单次操作的总体复杂度从而拒绝超阈值的高开销查询保护服务器免受资源耗尽与 DDoS 攻击。读完本文你将掌握字段复杂度的两种定义方式数字与估算函数、TypeGraphQL 内部如何把复杂度写入 schema以及在 Apollo Server 中落地校验插件的完整流程。为什么要限制查询复杂度一个看似普通的 GraphQL 查询可能给服务器带来巨大负担。例如客户端请求一个列表字段而每个列表项又带若干个嵌套子字段一次操作就可能触发成千上万次数据库查询攻击者可以借此发起 DDoS 攻击。TypeGraphQL 官方文档docs/complexity.md明确指出单条 GraphQL 查询可能产生巨大的服务器工作量如数千次数据库操作并被利用来发动 DDoS 攻击因此需要跟踪并限制每次 GraphQL 操作所能做的事情。与其依赖超时或请求频率等外围手段更精准的做法是基于成本分析cost analysis先为每个字段定义成本complexity cost再分析查询的 AST抽象语法树估算整条查询的总成本。TypeGraphQL 将成本分析交给graphql-query-complexity库完成你只需要做两件事在 TypeGraphQL 装饰器中定义各字段的复杂度在所使用的任意 GraphQL 服务器中接入graphql-query-complexity。第一步在装饰器中为字段定义复杂度TypeGraphQL 允许把complexity作为选项传给字段/查询/变更/订阅的装饰器。复杂度可以是一个数字也可以是一个估算函数。ObjectType() class MyObject { Field({ complexity: 2 }) publicField: string; Field({ complexity: ({ args, childComplexity }) childComplexity 1 }) complexField: string; }数字形式Field({ complexity: 2 })表示该字段的成本恒为 2。函数形式({ args, childComplexity }) childComplexity 1函数的入参由graphql-query-complexity提供childComplexity子节点嵌套字段的复杂度之和args该字段查询/变更接收的参数可用来按参数动态计算成本。支持复杂度选项的装饰器官方文档说明complexity可以作为选项传给以下任意装饰器装饰器说明Field对象类型字段FieldResolver字段解析器Mutation变更操作Subscription订阅操作默认值与优先级规则可省略如果字段的复杂度值就是 1可以完全省略complexity选项simpleEstimator的默认行为也与此一致见下文。优先级对同一个属性如果FieldResolver与Field都定义了复杂度那么FieldResolver上传递的复杂度优先。这一点在示例代码中也有明确体现见 examples/query-complexity/recipe.resolver.ts 中的注释 Complexity in field resolver overrides complexity of equivalent field type。源码视角复杂度如何被收集并写入 schema从 TypeGraphQL 源码可以确认complexity选项的完整流转链路类型定义src/typings/Complexity.ts定义了export type Complexity ComplexityEstimator | number;即复杂度本质上就是graphql-query-complexity的估算器类型或一个数字。元数据收集在 src/decorators/Field.ts 中options.complexity同时被写入类字段元数据collectClassFieldMetadata和内部字段解析器元数据collectFieldResolverMetadatasrc/decorators/FieldResolver.ts 同样收集该选项。写入 GraphQL 字段配置在 src/schema/schema-generator.ts 中生成对象类型字段时会把complexity: field.complexity放入字段配置的extensions对象extensions: { complexity: field.complexity, ...field.extensions, ...fieldResolverMetadata?.extensions, },这正是graphql-query-complexity的fieldExtensionsEstimator()能够读取到各字段复杂度值的原因——TypeGraphQL 在构建 schema 时把复杂度作为字段扩展extensions挂在 GraphQL 字段配置上估算器在运行时解析这些扩展即可。接口类型字段与查询/变更/订阅处理器的生成逻辑同文件约 L478-L481、L668-L672也采用相同的extensions.complexity写入方式。第二步在服务器中集成 graphql-query-complexity定义好字段复杂度后下一步是把graphql-query-complexity集成进你实际使用的 GraphQL 服务器。该库支持多种服务器接入方式官方文档提到可参照express-graphql的用法TypeGraphQL 官方示例使用 Apollo Server。方案一Apollo Server 插件推荐与官方示例一致在 examples/query-complexity/index.ts 中通过 Apollo Server 的plugins机制在每个请求解析操作时调用getComplexity计算复杂度并对比阈值MAX_COMPLEXITY超限即抛错拒绝查询import { ApolloServer } from apollo/server; import { startStandaloneServer } from apollo/server/standalone; import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from graphql-query-complexity; import { buildSchema } from type-graphql; import { RecipeResolver } from ./recipe.resolver; // Maximum allowed complexity const MAX_COMPLEXITY 20; async function bootstrap() { // Build TypeGraphQL executable schema const schema await buildSchema({ resolvers: [RecipeResolver], emitSchemaFile: path.resolve(__dirname, schema.graphql), }); // Create GraphQL server const server new ApolloServer({ schema, // Create a plugin to allow query complexity calculation for every request plugins: [ { requestDidStart: async () ({ async didResolveOperation({ request, document }) { const complexity getComplexity({ // GraphQL schema schema, // To calculate query complexity properly, // check only the requested operation, // not the whole document that may contain multiple operations operationName: request.operationName, // GraphQL query document query: document, // GraphQL query variables variables: request.variables, // Add any number of estimators. The estimators are invoked in order, // the first numeric value returned by an estimator is used as the field complexity. // If no estimator returns a value, an exception is raised estimators: [ // Using fieldExtensionsEstimator is mandatory to make it work with type-graphql fieldExtensionsEstimator(), // This will assign each field a complexity of 1 // if no other estimator returned a value simpleEstimator({ defaultComplexity: 1 }), ], }); // React to the calculated complexity, // like compare it with max and throw error when the threshold is reached if (complexity MAX_COMPLEXITY) { throw new Error( Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}, ); } console.log(Used query complexity points:, complexity); }, }), }, ], }); // Start server const { url } await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(GraphQL server ready at ${url}); } bootstrap().catch(console.error);getComplexity关键参数说明参数作用schema由buildSchema构建出的可执行 GraphQL schema估算器需据此遍历字段operationName只计算文档中被请求的那一个操作避免一次文档含多个操作时误算queryGraphQL 查询文档variables查询变量。变量在graphql-js的 visitor 中不可直接获取因此需显式传入estimators估算器数组按顺序调用第一个返回数值的估算器决定该字段的复杂度若所有估算器都不返回值则抛出异常estimators 的配置要点fieldExtensionsEstimator()是必选项它是 TypeGraphQL 与graphql-query-complexity协作的桥梁——只有通过它TypeGraphQL 写入字段extensions.complexity的复杂度值才会被读取。官方文档与示例代码均强调 Using fieldExtensionsEstimator is mandatory to make it work with type-graphql。simpleEstimator({ defaultComplexity: 1 })作为兜底为所有没有显式标注复杂度的字段分配默认值 1这与 TypeGraphQL 复杂度为 1 时可省略选项 的约定一致。你也可以继续追加自定义估算器如按类型估算的directiveEstimator等第一个返回数值的估算器生效。方案二graphql-server 的 validationRules旧版示例版本化文档中保留了另一种接入形态通过graphql-server的validationRules配置在每次请求时运行queryComplexity()校验规则const server new GraphQLServer({ schema }); const serverOptions: Options { port: 4000, endpoint: /graphql, playground: /playground, validationRules: req [ queryComplexity({ // The maximum allowed query complexity, queries above this threshold will be rejected maximumComplexity: 8, // The query variables. This is needed because the variables are not available // in the visitor of the graphql-js library variables: req.query.variables, // Optional callback function to retrieve the determined query complexity. // Will be invoked whether the query is rejected or not. // This can be used for logging or to implement rate limiting onComplete: (complexity: number) { console.log(Query Complexity:, complexity); }, estimators: [ // Using fieldConfigEstimator is mandatory to make it work with type-graphql fieldConfigEstimator(), // This will assign each field a complexity of 1 if no other estimator // returned a value. We can define the default value for field not explicitly annotated simpleEstimator({ defaultComplexity: 1, }), ], }), ], }; server.start(serverOptions, ({ port, playground }) { console.log( Server is running, GraphQL Playground available at http://localhost:${port}${playground}, ); });注意此方案中估算器名称为fieldConfigEstimator()旧版 API 命名而当前graphql-query-complexity推荐使用fieldExtensionsEstimator()且默认复杂度阈值maximumComplexity: 8仅为示例值请按业务实际调整。完整实战示例为 Recipe 查询设置复杂度仓库的 examples/query-complexity 目录提供了一个可直接运行的完整示例recipe.type.ts、recipe.resolver.ts、index.ts。对象类型数字与函数两种复杂度用法examples/query-complexity/recipe.type.ts 演示了三种情形ObjectType() export class Recipe { /* By default, every field gets a complexity of 1 */ Field() title!: string; /* Which can be customized by passing the complexity parameter */ Field(_type Int, { complexity: 2 }) ratingsCount!: number; Field(_type Float, { nullable: true, complexity: 10, }) get averageRating(): number | null { const ratingsCount this.ratings.length; if (ratingsCount 0) { return null; } const ratingsSum this.ratings.reduce((a, b) a b, 0); return ratingsSum / ratingsCount; } // Internal property, not exposed in schema ratings!: number[]; }title未标注复杂度按约定取默认值 1ratingsCount复杂度为 2averageRating是一个计算型 getter 字段涉及数组求和与除法成本较高复杂度设为 10。Resolver按参数与子节点复杂度动态计算examples/query-complexity/recipe.resolver.ts 展示了最实用的动态复杂度计算——列表查询的复杂度应随参数count与每个子项的成本相乘Resolver(_of Recipe) export class RecipeResolver implements ResolverInterfaceRecipe { private readonly items: Recipe[] createRecipeSamples(); Query(_returns [Recipe], { /* Pass also a calculation function in the complexity option to determine a custom complexity. This function provides the complexity of the child nodes as well as the field input arguments. That way a more realistic estimation of individual field complexity values is made, e.g. by multiplying childComplexity by the number of items in array */ complexity: ({ childComplexity, args }) args.count * childComplexity, }) async recipes(Arg(count) count: number): PromiseRecipe[] { return this.items.slice(0, count); } /* Complexity in field resolver overrides complexity of equivalent field type */ FieldResolver({ complexity: 5 }) ratingsCount(Root() recipe: Recipe): number { return recipe.ratings.length; } }关键点recipes查询的复杂度 args.count × childComplexity。请求recipes(count: 3)且每个子项包含averageRating成本 10时该查询的复杂度即3 × 10 30超过MAX_COMPLEXITY 20会被拒绝。ratingsCount通过FieldResolver({ complexity: 5 })覆盖了对象类型中Field定义的 2体现了FieldResolver 优先规则。用示例查询验证阈值行为examples/query-complexity/examples.graphql 给出了两条对比查询query GetRecipesWithComplexityError { recipes(count: 3) { title averageRating } } query GetRecipesWithoutComplexityError { recipes(count: 2) { title ratingsCount } }GetRecipesWithComplexityErrorrecipes(count: 3)的averageRating成本 10子项复杂度约 10总复杂度3 × 10 30 20请求被拒绝并抛出Sorry, too complicated query!错误。GetRecipesWithoutComplexityErrorrecipes(count: 2)子项包含title1与ratingsCountFieldResolver 覆盖为 5总复杂度2 × (1 5) 12 ≤ 20请求正常通过。测试用例佐证复杂度的计算是可验证的仓库的 tests/functional/query-complexity.ts 对复杂度计算做了单元级验证可直接证明字段复杂度 默认值 查询总复杂度的算法测试定义了一个complexity: 10的字段complexResolverMethod查询sampleQuery { complexResolverMethod }经fieldExtensionsEstimator()与simpleEstimator({ defaultComplexity: 1 })计算总复杂度期望值等于 11即字段成本 10 根查询的默认成本 1。该测试同样覆盖了 Subscriptions订阅操作场景说明Subscription上的复杂度选项同样会被估算器正确计入。注意事项与最佳实践复杂度只是估算不是真实耗时它衡量的是查询代价上限的近似值适合作为防滥用阈值不应替代真实的性能分析与监控。operationName务必传入避免多操作文档中无关操作被一并计入。阈值要结合业务数据规模设定例如列表查询中count的合理上限、嵌套深度等示例中的 8/20 均为演示值。日志与限流可使用onComplete回调validationRules 方案或在didResolveOperation中记录每次查询的复杂度点数用于监控、告警或实现基于复杂度的限流。不同服务器接入方式可互相替换无论 Apollo Server、express-graphql 还是其他兼容graphql-js的服务器核心都是schema estimators 复杂度阈值这一套组合TypeGraphQL 侧的定义方式完全不变。小结通过 TypeGraphQL 的complexity装饰器选项配合graphql-query-complexity你可以用极小的成本为 GraphQL API 建立一套查询成本预算机制数字定义简单字段成本、函数按参数与子节点动态计算列表类成本FieldResolver优先级覆盖保证灵活性fieldExtensionsEstimator负责读取 TypeGraphQL 写入 schema 的复杂度扩展最终在服务器侧统一校验并拒绝超阈值查询。参考仓库中的 examples/query-complexity 完整示例与 tests/functional/query-complexity.ts 测试即可快速在自己项目中落地这一防护方案。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐gqlgen 查询复杂度限制用成本计算防御 GraphQL 恶意查询gqlgen 查询复杂度限制用成本计算防御 GraphQL 恶意查询 导读 GraphQL 允许客户端一次请求嵌套极深、规模巨大的数据这种强大能力同样意味着后端GraphQL代码生成深度评测Ascend AI处理器Dhrystone整数运算性能全面分析深度评测Ascend AI处理器Dhrystone整数运算性能全面分析 在AI计算和深度学习应用场景中整数运算性能是衡量处理器核心能力的关键指标。本文基于P算子库人工智能CANNTypeGraphQL查询复杂度估算自定义成本函数TypeGraphQL查询复杂度估算自定义成本函数 GraphQL查询可能引发服务器资源耗尽风险尤其当面对嵌套层级深或数据量大的请求时。TypeGraphQ后端GraphQLAPI设计上一篇如何快速集成Flagsmith移动端SDKiOS和Android完整指南下一篇iOS动画终极指南如何创建惊艳的按钮动画与过渡效果创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考