在发送之前修改 Graphql 的响应

Modifying Response of Graphql before sent out

提问人:Matt 提问时间:5/10/2019 更新时间:12/29/2021 访问量:5336

问:

我正在寻找一种方法来在发送之前修改 graphql 查询或突变的响应对象。

基本上除了数据对象之外,我还希望有额外的字段,如代码和消息。

目前,我正在通过将字段直接添加到我的 GQL 模式中来解决这个问题,例如,以以下类型定义为例:

type Query {
  myItems: myItemResponse
}

type myItemResponse {
  myItem: Item
  code: String!
  success: Boolean!
  message: String!
}

响应本身是这样的:

{
   data: {
      myItems: {
         myItem: [ ... fancy Items ... ],
         message: 'successfully retrieved fancy Items',
         code: <CODE_FOR_SUCCESSFUL_QUERY>
      }
   }
}

我发现这个解决方案不太好,因为它使我的前端过于复杂。

我更喜欢将消息代码和其他元数据与实际数据分开的解决方案,因此如下所示:

{
   data: {
      myItems: [ ... fancy Items ... ],
   },
   message: 'successfully retrieved fancy Items',
   code: <CODE_FOR_SUCCESSFUL_QUERY>
}

使用apollo-server,我已经尝试了构造函数中的formatResponse对象:

const server = new ApolloServer({
   ...
   formatResponse({ data }) {
     return {
        data,
        test: 'Property to test if shown in the FrontEnd',
     }
   }
   ...
}

不幸的是,这并没有达到预期的效果。在我使用快速中间件之前,我想问一下是否有可能通过开箱即用的 apollo-server 执行此操作,或者我是否可能只是在 formatResponse 函数中缺少一些东西。

javascript graphql apollo-server

评论

0赞 Daniel Rearden 5/10/2019
formatResponse是添加这些字段的正确方法。说“没有达到预期的效果”并不能描述您遇到的问题。请具体说明您预期的行为以及您遇到的意外行为。
1赞 Matt 5/10/2019
我的意思是,即使我返回 { data, test },我只在响应中返回数据,预期行为将是响应包含 { data, test }
2赞 David Maze 5/10/2019
GraphQL 明确支持错误,甚至在每个字段级别上,这些错误可以携带扩展值,如人类可读的消息和机器可读的代码。Apollo 也有一些少量记录的错误处理,可能会满足您的需求。
0赞 Daniel Rearden 5/10/2019
@Matt 看看这个 CodeSandbox。 按预期工作。如果前端的客户端未公开此信息,则这是一个单独的问题。您可以通过打开“网络”选项卡并查看从服务器收到的实际响应来验证是否是这种情况。formatResponse
0赞 Daniel Rearden 5/10/2019
如果确实发生了这种情况,那么这个问题可能应该重写为“为什么 [CLIENT] 不读取我在响应中设置的自定义属性?

答:

0赞 Matt 5/11/2019 #1

在做了大量研究之后,我发现 graphql 响应中唯一允许的顶级属性是数据、错误、扩展。在这里,您可以在 GitHub 中找到相关问题

GitHub 问题

出于我的目的,我可能会使用扩展字段。

2赞 eric.g 5/30/2020 #2

从 graphql.org:对 GraphQL 操作的响应必须是映射。

如果操作遇到任何错误,则响应映射必须包含包含键错误的条目。此项的值在“错误”一节中进行了描述。如果操作完成时未遇到任何错误,则此条目不得存在。

如果操作包括执行,则响应映射必须包含包含关键数据的条目。此项的值在“数据”一节中进行了描述。如果操作在执行之前由于语法错误、缺少信息或验证错误而失败,则此条目不得存在。

响应映射还可能包含具有键扩展的条目。如果设置了此条目,则必须将映射作为其值。此条目保留给实现者以他们认为合适的方式扩展协议,因此对其内容没有额外的限制。

为确保将来对协议的更改不会破坏现有服务器和客户端,顶级响应映射不得包含除上述三个条目以外的任何条目。

0赞 Alexander Gavazov 12/29/2021 #3

示例数据修饰符

此函数将在输出对象中的每个字符串上连接“:OK”后缀

// Data/output modifier - concat ":OK" after each string
function outputModifier(input: any): any {
    const inputType = typeof input;

    if (inputType === 'string') {
        return input + ':OK';
    } else if (Array.isArray(input)) {
        const inputLength = input.length;
        for (let i = 0; i < inputLength; i += 1) {
            input[i] = outputModifier(input[i]);
        }
    } else if (inputType === 'object') {
        for (const key in input) {
            if (input.hasOwnProperty(key)) {
                input[key] = outputModifier(input[key]);
            }
        }
    }

    return input;
}

解决方案 1 - 覆盖 GraphQL 解析器

长话短说:您有 3 种主要类型(查询、变更和订阅)。 每个主类型都有带有解析程序的字段。 解析器返回输出数据。

因此,如果覆盖解析器,您将能够修改输出。

示例转换器

import { GraphQLSchema } from 'graphql';

export const exampleTransformer = (schema: GraphQLSchema): GraphQLSchema => {
    // Collect all main types & override the resolvers
    [
        schema?.getQueryType()?.getFields(),
        schema?.getMutationType()?.getFields(),
        schema?.getSubscriptionType()?.getFields()
    ].forEach(fields => {
        // Resolvers override
        Object.values(fields ?? {}).forEach(field => {
            // Check is there any resolver at all
            if (typeof field.resolve !== 'function') {
                return;
            }

            // Save the original resolver
            const originalResolve = field.resolve;

            // Override the current resolver
            field.resolve = async (source, inputData, context, info) => {
                // Get the original output
                const outputData: any = await originalResolve.apply(originalResolve.prototype, [source, inputData, context, info]);

                // Modify and return the output
                return outputModifier(outputData);
            };
        });
    });

    return schema;
};

如何使用它:

// Attach it to the GraphQLSchema > https://graphql.org/graphql-js/type/
let schema = makeExecutableSchema({...});
schema = exampleTransformer(schema);
const server = new ApolloServer({schema});
server.listen(serverConfig.port);

该解决方案适用于任何 GraphQL-JS 服务(apollo、express-graphql、graphql-tools 等)。

保持最小值使用此解决方案,您也可以操作。inputData

解决方案 2 - 修改响应

这种解决方案更优雅,但是在指令和标量类型实现之后实现的,不能操作输入数据。

输出对象的具体情况是数据是对象(没有像 .hasOwnProperty()、.toString() 等实例方法),错误是锁定对象(只读)。 在示例中,我正在解锁错误对象...请注意这一点,不要更改对象的结构。null-prototype

示例转换器

import { Translator } from '@helpers/translations';
import type { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
import type { GraphQLFormattedError } from 'graphql';

export const exampleResponseFormatter = () => (response: GraphQLResponse, requestContext: GraphQLRequestContext) => {
    // Parse locked error fields
    response?.errors?.forEach(error => {
        (error['message'] as GraphQLFormattedError['message']) = exampleTransformer(error['message']);
        (error['extensions'] as GraphQLFormattedError['extensions']) = exampleTransformer(error['extensions']);
    });

    // Parse response data
    response.data = exampleTransformer(response.data);

    // Response
    return response;
};

如何使用它:

// Provide the schema to the ApolloServer constructor
const server = new ApolloServer({
    schema,
    formatResponse: exampleResponseFormatter()
});

结论

我在我的项目中同时使用了这两种解决方案。使用第一个,您可以根据代码中的特定访问指令控制输入和输出,或者验证整个数据流(在任何 graphql 类型上)。 其次,根据用户提供的上下文标头翻译所有字符串,而不会弄乱解析器和带有语言变量的代码。

这些示例在 TS 4+ 以及 GraphQL 15 和 16 上进行了测试