发送前修改 Graphql 的 Response

Modifying Response of Graphql before sent out

我正在寻找一种在发送之前修改 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',
     }
   }
   ...
}

不幸的是,没有达到预期的效果。在我使用 express 中间件之前,我想问一下是否有可能通过开箱即用的 apollo-server 来做到这一点,或者我是否可能只是在 formatResponse 函数中遗漏了一些东西。

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

中的相关问题

GitHub Issue

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

from graphql.org: 对 GraphQL 操作的响应必须是地图。

如果操作遇到任何错误,响应映射必须包含一个带有关键错误的条目。此条目的值在“错误”部分中描述。如果操作完成且未遇到任何错误,则不得存在此条目。

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

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

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

示例数据修饰符

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

// 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 - 修改响应

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

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

的结构

示例变压器

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 类型上)。 其次,根据用户提供的上下文 headers 翻译所有字符串,而不会弄乱解析器和带有语言变量的代码。

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