You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 17:42:14