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

Hasura与Apollo GraphQL Schema拼接冲突及类型适配问题咨询

技术栈

"@nestjs/apollo": "^10.0.19"
"@nestjs/graphql": "^10.0.21"
"typeorm": "^0.3.7"
"graphql": "^16.5.0"
"@graphql-tools/stitch": "^8.7.6"
hasura graphql engine: 2.9.0

背景说明

我用NestJS+Apollo做GraphQL后端,采用代码优先方案,通过TypeORM操作Postgres数据库。在NestJS里给Todo表实现了自定义CRUD mutations,同时用Hasura自动生成只读GraphQL查询操作同一张表。为了让Hasura返回字符串类型的数值,设置了HASURA_GRAPHQL_STRINGFY_NUMERIC_TYPES=true,但该配置也将double precision类型转为字符串。

通过Schema Stitching将Hasura与Apollo的Schema拼接,用NestJS暴露合并后的代理Schema。TypeORM实体由typeorm-model-generator自动生成,在@Field()装饰器中设置GraphQLJSON,让jsonb和interval类型以JSON格式返回。

Todo实体定义

@ObjectType()
export class Todo {
    @Field(() => Int)
    @PrimaryGeneratedColumn({ type: "integer", name: "id" })
    id: number;

    @Field(() => Float, { nullable: true })
    @Column("double precision", {
        name: "quantity",
        nullable: true,
        precision: 53,
    })
    quantity: number | null;

    @Field(() => GraphQLJSON, { nullable: true })
    @Column("jsonb", { name: "custom_data", nullable: true })
    customData: object | null;

    @Field(() => GraphQLJSON, { nullable: true })
    @Column("interval", {
        name: "duration",
        nullable: true,
        default: () => "'00:05:00'",
    })
    duration: any | null;

    @Field({ nullable: true })
    @Column("timestamp with time zone", {
        name: "created_at",
        nullable: true,
        default: () => "CURRENT_TIMESTAMP",
    })
    createdAt: Date | null;
}

初始Schema拼接代码

const createRemoteSchema = async (url: string) => {
    const executor = async ({ document, variables }) => {
        const query = print(document);
        const fetchResult = await fetch(url, {
            method: 'POST',
            headers: {'Content-Type': 'application/json',},
            body: JSON.stringify({ query, variables }),
        });
        return fetchResult.json();
    };
    return {
        schema: await introspectSchema(executor),
        executor: executor,
    };
};

// ...

GraphQLModule.forRootAsync<ApolloDriverConfig>({
    driver: ApolloDriver,
    inject: [ConfigService],
    useFactory: async (configService: ConfigService) => {
        const hasuraSchema = await createRemoteSchema(url);
        return {
            driver: ApolloDriver,
            debug: false,
            playground: true,
            introspection: true,
            autoSchemaFile: true,
            context: ({ req }) => ({ headers: req.headers }),
            transformSchema: async (schema: GraphQLSchema) => {
                return stitchSchemas({ subschemas: [hasuraSchema, apolloSchema] });
            },
            formatError: (error) => errorFormatter(error),
        };
    },
}),

// ...

启动时的Schema冲突警告

Definitions of field "Todo.duration" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.duration'].validationLevel = "error|off"
Definitions of field "Todo.quantity" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.quantity'].validationLevel = "error|off"
Definitions of field "Todo.createdAt" implement inconsistent named types across subschemas. This will be an automatic error in future versions. To disable this warning or elevate it to an error, set typeMergingOptions.validationScopes['Todo.createdAt'].validationLevel = "error|off"

尝试用TransformObjectFields修正类型

const createRemoteSchema = async (url: string) => {
    const executor = async ({ document, variables }) => {
        const query = print(document);
        const fetchResult = await fetch(url, {
            method: 'POST',
            headers: {'Content-Type': 'application/json',},
            body: JSON.stringify({ query, variables }),
        });
        return fetchResult.json();
    };
    return {
        schema: await introspectSchema(executor),
        executor: executor,
        transforms: [
            new TransformObjectFields((typename, fieldName, fieldConfig) => {
                let newFieldConfig;
                // Map timestamptz to GraphQLDateTime
                if (fieldConfig.type.toString() === 'timestamptz') {
                    newFieldConfig = fieldConfig;
                    newFieldConfig.type = GraphQLDateTime;
                    return newFieldConfig;
                }

                // Map interval to GraphQLJSON
                if (fieldConfig.type.toString() === 'interval') {
                    newFieldConfig = fieldConfig;
                    newFieldConfig.type = GraphQLJSON;
                    return newFieldConfig;
                }
                return fieldConfig;
            }),
        ],
    };
};

此时createdAt和duration的警告消失,但quantity和customData的警告仍存在。尝试以下冲突解决配置无效:

// 配置1
transformSchema: async (schema: GraphQLSchema) => {
    return stitchSchemas({
        subschemas: [hasuraSchema, apolloSchema],
        typeMergingOptions: {
            typeCandidateMerger: (candidates) => candidates[0],
            typeDescriptionsMerger: (candidates) => candidates[0].type.description,
            fieldConfigMerger: (candidates) => candidates[0].fieldConfig,
            inputFieldConfigMerger: (candidates) => candidates[0].inputFieldConfig,
            enumValueConfigMerger: (candidates) => candidates[0].enumValueConfig,
        },
    });
},

// 配置2
transformSchema: async (schema: GraphQLSchema) => {
    return stitchSchemas({
        subschemas: [hasuraSchema, apolloSchema],
        onTypeConflict: (hasuraField, apolloField) => apolloField,
    });
},

问题与解答

问题1:我是否正确使用了TransformObjectFields()转换类型?

方向是对的,但有两处细节可优化:

  1. 不要直接修改原始fieldConfig对象,避免污染Schema,建议用扩展运算符创建新对象返回:
return { ...fieldConfig, type: GraphQLDateTime };
  1. 判断类型时,fieldConfig.type.toString()在嵌套类型(如非空、列表)场景下不准确,应使用getNamedType获取底层命名类型再判断:
import { getNamedType } from 'graphql';

// ...

const namedType = getNamedType(fieldConfig.type);
if (namedType.name === 'timestamptz') {
  return { ...fieldConfig, type: GraphQLDateTime };
}

问题2:因HASURA_GRAPHQL_STRINGFY_NUMERIC_TYPES=true,quantity字段被序列化为字符串,查询时抛出错误:Float cannot represent non numeric value: "182.24",该如何解决?

有两种可行方案:

  1. Hasura端调整配置:如果不需要全局将数值转字符串,可移除HASURA_GRAPHQL_STRINGFY_NUMERIC_TYPES=true,改用Hasura自定义类型映射,仅将特定数值类型配置为字符串,保留double precision为数值类型。
  2. Schema转换时处理:在Hasura的executor中把返回的字符串转为数值,或在TransformObjectFields中添加解析器转换值:
new TransformObjectFields((typename, fieldName, fieldConfig) => {
  if (typename === 'Todo' && fieldName === 'quantity') {
    const newFieldConfig = { ...fieldConfig, type: Float };
    newFieldConfig.resolve = async (source) => {
      return source.quantity ? parseFloat(source.quantity) : null;
    };
    return newFieldConfig;
  }
  // 其他转换逻辑...
})

问题3:customData可正常返回JSON格式,但警告仍存在,如何修复而非关闭警告?

警告根源是Hasura生成的jsonb类型标量,与你使用的GraphQLJSON(来自@nestjs/graphql或graphql-type-json)定义不一致。解决方法是在Hasura的Schema转换中,将jsonb对应的类型替换为你项目中的GraphQLJSON:

import { GraphQLJSON } from 'graphql-type-json'; // 按项目实际导入路径调整

// 在TransformObjectFields中添加:
const namedType = getNamedType(fieldConfig.type);
if (namedType.name === 'jsonb') {
  return { ...fieldConfig, type: GraphQLJSON };
}

问题4:尝试4种intervalstyles后,interval类型仍未转为JSON格式,如何实现该需求?

Postgres的intervalstyle仅改变字符串格式,不会转为JSON。可通过以下两种方式实现:

  1. Hasura端用自定义SQL函数转换:创建SQL函数将interval解析为包含days、hours、minutes等字段的JSON对象,在Hasura中把duration字段替换为该函数的返回值,或创建计算字段。
  2. Schema转换时处理:在Hasura的executor中修改返回值,将interval字符串解析为JSON对象:
const executor = async ({ document, variables }) => {
  const query = print(document);
  const fetchResult = await fetch(url, {
    method: 'POST',
    headers: {'Content-Type': 'application/json',},
    body: JSON.stringify({ query, variables }),
  });
  const data = await fetchResult.json();

  // 解析interval字符串为JSON对象
  const parseInterval = (intervalStr) => {
    const parts = intervalStr.split(':');
    return {
      hours: parseInt(parts[0]),
      minutes: parseInt(parts[1]),
      seconds: parseFloat(parts[2])
    };
  };

  // 递归处理返回数据
  const transformData = (obj) => {
    if (Array.isArray(obj)) return obj.map(transformData);
    if (obj && typeof obj === 'object') {
      const newObj = { ...obj };
      if (newObj.duration) newObj.duration = parseInterval(newObj.duration);
      Object.keys(newObj).forEach(key => newObj[key] = transformData(newObj[key]));
      return newObj;
    }
    return obj;
  };

  if (data.data) data.data = transformData(data.data);
  return data;
};

同时确保TransformObjectFields已将interval类型替换为GraphQLJSON,让Apollo正确识别为JSON类型。

内容的提问来源于stack exchange,提问作者Won Kim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 13:30:41