GraphQL客户端:增强数据遍历修改函数以支持片段等复杂查询
支持GraphQL片段的响应数据遍历修改方案
需求背景
在客户端使用GraphQL时,需要基于GraphQL Schema和具体查询结构遍历并修改响应数据,目标是根据Schema类型信息动态转换数据(例如将字符串格式的日期转为Date对象),预期调用方式如下:
traverse(schema, query, data, { GraphQLScalar: (type, value) => { if (type.name === 'Date') { value = new Date(value) } } })
当前已实现可处理无片段的简单查询的函数,但无法应对包含片段(FragmentSpread、InlineFragment)的复杂查询场景,现有实现代码如下:
import * as GraphQL from "graphql"; import graphqlSchema from "..."; type Jsonify<T> = T extends { toJSON(): infer U } ? U : T const schema = GraphQL.buildSchema(graphqlSchema); export function reviveGraphQLResponse<T extends object>(node: GraphQL.DocumentNode, value: Jsonify<T>): T { const newValue = structuredClone(value); for (const definition of node.definitions) { if (definition.kind !== GraphQL.Kind.OPERATION_DEFINITION) { continue; } reviveGraphQLSelectionSet({ schema, value: newValue, selections: definition.selectionSet.selections, parent: schema.getType("Query") as GraphQL.GraphQLObjectType, }); } return newValue; } function reviveGraphQLSelectionSet(params: { schema: GraphQL.GraphQLSchema; value: any; parent: GraphQL.GraphQLObjectType; selections: readonly GraphQL.SelectionNode[]; }) { if (Array.isArray(params.value)) { for (const item of params.value) { reviveGraphQLSelectionSet({ schema: params.schema, value: item, parent: params.parent, selections: params.selections, }); } return; } for (const selection of params.selections) { if (selection.kind !== GraphQL.Kind.FIELD) { continue; } const fieldName = selection.name.value; if (params.value === null || fieldName in params.value) { continue; } const parentFields = params.parent.getFields(); const parentRawType = parentFields[fieldName].type; const parentTypeName = GraphQL.getNamedType(parentRawType).name; const parentType = schema.getType(parentTypeName) as GraphQL.GraphQLObjectType; if (selection.selectionSet) { reviveGraphQLSelectionSet({ schema: params.schema, value: params.value[fieldName], parent: parentType, selections: selection.selectionSet.selections, }); continue; } const field = parentFields[fieldName]; const fieldTypeName = GraphQL.getNamedType(field.type).name; if (fieldTypeName === "Date") { params.value[fieldName] = new Date(params.value[fieldName]); } } return; }
增强方案(支持片段)
1. 核心改进思路
- 提前收集查询文档中的所有片段定义,建立快速查找映射
- 扩展选择集处理逻辑,新增对
FragmentSpread和InlineFragment的递归解析 - 添加类型兼容性检查,确保片段仅应用于匹配的对象类型/接口
- 替换硬编码的转换逻辑为可配置的回调函数,贴合预期调用方式
2. 修改后的完整实现
import * as GraphQL from "graphql"; import graphqlSchema from "..."; type Jsonify<T> = T extends { toJSON(): infer U } ? U : T; type TraverseConfig = { GraphQLScalar?: (type: GraphQL.GraphQLScalarType, value: any) => any; }; const schema = GraphQL.buildSchema(graphqlSchema); export function traverse<T extends object>( schema: GraphQL.GraphQLSchema, query: GraphQL.DocumentNode, data: Jsonify<T>, config: TraverseConfig = {} ): T { const newValue = structuredClone(data); const fragmentMap = getFragmentMap(query); for (const definition of query.definitions) { if (definition.kind !== GraphQL.Kind.OPERATION_DEFINITION) continue; // 自动识别根类型(Query/Mutation/Subscription) const rootType = schema.getType(definition.operation) as GraphQL.GraphQLObjectType; if (!rootType) continue; reviveGraphQLSelectionSet({ schema, value: newValue, parent: rootType, selections: definition.selectionSet.selections, fragmentMap, config, }); } return newValue; } // 辅助函数:提取查询中的所有片段定义 function getFragmentMap(document: GraphQL.DocumentNode): Record<string, GraphQL.FragmentDefinitionNode> { const fragmentMap: Record<string, GraphQL.FragmentDefinitionNode> = {}; for (const def of document.definitions) { if (def.kind === GraphQL.Kind.FRAGMENT_DEFINITION) { fragmentMap[def.name.value] = def; } } return fragmentMap; } // 辅助函数:验证类型兼容性(支持接口、直接类型匹配) function isTypeCompatible(parentType: GraphQL.GraphQLObjectType, targetType: GraphQL.GraphQLObjectType | GraphQL.GraphQLInterfaceType): boolean { // 直接类型匹配 if (parentType.name === targetType.name) return true; // 检查父类型是否实现了目标接口 if (targetType instanceof GraphQL.GraphQLInterfaceType) { return parentType.getInterfaces().some(iface => iface.name === targetType.name); } return false; } // 核心遍历处理函数 function reviveGraphQLSelectionSet(params: { schema: GraphQL.GraphQLSchema; value: any; parent: GraphQL.GraphQLObjectType; selections: readonly GraphQL.SelectionNode[]; fragmentMap: Record<string, GraphQL.FragmentDefinitionNode>; config: TraverseConfig; }) { const { schema, value, parent, selections, fragmentMap, config } = params; // 处理数组类型数据 if (Array.isArray(value)) { value.forEach(item => { reviveGraphQLSelectionSet({ ...params, value: item }); }); return; } if (value === null) return; for (const selection of selections) { switch (selection.kind) { case GraphQL.Kind.FIELD: { const fieldName = selection.name.value; // 跳过不存在的字段,避免报错 if (!(fieldName in value)) continue; const parentFields = parent.getFields(); const field = parentFields[fieldName]; if (!field) continue; const fieldType = GraphQL.getNamedType(field.type); // 处理嵌套对象/数组的选择集 if (selection.selectionSet && fieldType instanceof GraphQL.GraphQLObjectType) { reviveGraphQLSelectionSet({ ...params, value: value[fieldName], parent: fieldType, selections: selection.selectionSet.selections, }); } // 使用自定义回调处理Scalar类型 else if (fieldType instanceof GraphQL.GraphQLScalarType && config.GraphQLScalar) { value[fieldName] = config.GraphQLScalar(fieldType, value[fieldName]); } break; } case GraphQL.Kind.FRAGMENT_SPREAD: { const fragment = fragmentMap[selection.name.value]; if (!fragment) continue; const fragmentType = schema.getType(fragment.typeCondition.name.value) as GraphQL.GraphQLObjectType | GraphQL.GraphQLInterfaceType; if (!fragmentType || !isTypeCompatible(parent, fragmentType)) continue; // 递归处理片段中的选择集 reviveGraphQLSelectionSet({ ...params, selections: fragment.selectionSet.selections, }); break; } case GraphQL.Kind.INLINE_FRAGMENT: { // 验证内联片段的类型条件(如果有) if (selection.typeCondition) { const fragmentType = schema.getType(selection.typeCondition.name.value) as GraphQL.GraphQLObjectType | GraphQL.GraphQLInterfaceType; if (!fragmentType || !isTypeCompatible(parent, fragmentType)) continue; } // 递归处理内联片段中的选择集 reviveGraphQLSelectionSet({ ...params, selections: selection.selectionSet.selections, }); break; } } } }
关键优化点
- 完整片段支持:覆盖
FragmentSpread和InlineFragment两种片段类型的解析 - 类型安全校验:确保片段仅应用于符合类型约束的对象
- 可扩展性:通过配置回调支持任意Scalar类型的自定义转换
- 健壮性:跳过不存在的字段,避免空值报错
- 灵活性:自动识别根操作类型(Query/Mutation),无需硬编码
内容的提问来源于stack exchange,提问作者Eliya Cohen
相关产品推荐
相关产品推荐

