发送前修改 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
中的相关问题
出于我的目的,我可能会使用扩展字段。
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 上进行了测试
我正在寻找一种在发送之前修改 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
中的相关问题出于我的目的,我可能会使用扩展字段。
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 上进行了测试