GraphQL 查询批处理

拦截和模拟批量 GraphQL 查询。

查询批处理是某些 GraphQL 客户端提供的一种性能机制,通过将它们分组到单个查询中来优化操作数量。虽然此功能具有实际好处,但查询批处理不是 GraphQL 规范(也不是 GraphQL-over-HTTP 规范)的一部分,缺乏对批处理查询的语法和行为的任何标准共识。因此,MSW 不提供处理此类查询的内置方法。

¥Query batching is a performance mechanism provided by some GraphQL clients to optimize the number of operations made by grouping them togeher in a single query. While this feature has its practical benefits, query batching is not a part of the GraphQL specification (neither the GraphQL-over-HTTP specification), lacking any standard consensus on the syntax and behavior of batched queries. Because of this, MSW does not provide a built-in way of handling such queries.

我们强烈建议将对批处理 GraphQL 查询的支持作为 MSW 设置的一部分来实现。下面,你可以找到几个如何实现这一点的示例。

¥We highly recommend implementing the support for batched GraphQL queries as a part of your MSW setup. Below, you can find a couple of examples of how to achieve that.

常识

¥General knowledge

从本质上讲,模拟批量 GraphQL 查询归结为以下步骤:

¥At its core, mocking a batched GraphQL query comes down to the following steps:

  1. 拦截批量 GraphQL 查询;

    ¥Intercept the batched GraphQL query;

  2. 将批处理查询解包为单独的 GraphQL 查询;

    ¥Unwrap the batched query into individual GraphQL queries;

  3. 针对现有请求处理程序解决单个查询;

    ¥Resolve the individual queries against the existing request handlers;

  4. 编写批处理响应。

    ¥Compose the batched response.

Apollo

Apollo 通过将多个查询分组到单个根级数组中来提供 查询批处理

¥Apollo provides Query batching by grouping multiple queries in a single root-level array.

[
  query GetUser {
    user {
      id
    }
  },
  query GetProduct {
    product {
      name
    }
  }
]

此分组稍后会反映在响应批处理查询时收到的有效负载结构中:

¥This grouping is later reflected in the payload structure received in response to a batched query:

[
  { "data": { "user": { "id": "abc-123" } } },
  { "data": { "product": { "name": "Hoover 2000" } } }
]

你可以通过引入自定义 batchedGraphQLQuery 高阶请求处理程序来模拟 Apollo 中的批处理 GraphQL 查询,该处理程序拦截此类批处理查询,解开它们,并使用 msw 中的 getResponse 函数根据任何给定的请求处理程序列表解析它们。

¥You can mock batched GraphQL queries in Apollo by introducing a custom batchedGraphQLQuery higher-order request handler that intercepts such batched queries, unwraps them, and resolves them against any given list of request handlers using the getResponse function from msw.

import { http, HttpResponse, getResponse, bypass } from 'msw'
 
function batchedGraphQLQuery(url, handlers) {
  return http.post(url, async ({ request }) => {
    const payload = await request.clone().json()
 
    // Ignore non-batched GraphQL queries.
    if (!Array.isArray(payload)) {
      return
    }
 
    const responses = await Promise.all(
      payload.map((query) => {
        // Construct an individual query request
        // to the same URL but with an unwrapped query body.
        const queryRequest = new Request(request, {
          body: JSON.stringify(operation),
        })
 
        // Resolve the individual query request
        // against the list of request handlers you provide.
        const response = await getResponse({
          request: queryRequest,
          handlers,
        })
 
        // Return the mocked response, if found.
        // Otherwise, perform the individual query as-is,
        // so it can be resolved against an original server.
        return response || fetch(bypass(queryRequest))
      })
    )
 
    // Read the mocked response JSON bodies to use
    // in the response to the entire batched query.
    const queryData = await Promise.all(
      responses.map((response) => response?.json()),
    )
 
    return HttpResponse.json(queryData)
  })
}

然后,在你的请求处理程序中使用 batchedGraphQLQuery 函数:

¥Then, use the batchedGraphQLQuery function in your request handlers:

import { graphql, HttpResponse } from 'msw'
 
const graphqlHandlers = [
  graphql.query('GetUser', () => {
    return HttpResponse.json({
      data: {
        user: { id: 'abc-123' },
      },
    })
  }),
]
 
export const handlers = [
  batchedGraphQLQuery('/graphql', graphqlHandlers),
  ...graphqlHandlers,
]

batched-execute

batched-execute 包通过在单个查询上提升多个操作并使用字段别名实现分组来提供 查询批处理

¥The batched-execute package provides Query batching by hoisting multiple operations on a single query and achieving grouping by using field aliases.

query {
  user_0: user {
    id
  }
  product_0: product {
    name
  }
}

然后,客户端将字段别名重新映射到原始操作,从而生成扁平响应对象。

¥The client then remap the field aliases to the original operations, producing a flat response object.

你可以通过引入自定义 batchedGraphQLQuery 高阶请求处理程序来模拟 batched-execute 中的批量 GraphQL 查询,该处理程序会拦截此类批量查询并根据模拟模式解析它们。在这种情况下,我们建议使用模式优先的 API 模拟来支持匿名查询。

¥You can mock batched GraphQL queries in batched-execute by introducing a custom batchedGraphQLQuery higher-order request handler that intercepts such batched queries and resolves them against a mocked schema. We recommend a schema-first API mocking in this case to support anonymous queries.

import {
  buildSchema,
  print,
  graphql as executeGraphQL,
  defaultFieldResolver,
} from 'graphql'
import { http, HttpResponse, bypass } from 'msw'
 
// Describe the GraphQL schema.
// You can also use an existing schema!
const schema = buildSchema(`
type User {
  id: ID!
}
 
type Query {
  user: User
}
`)
 
function batchedGraphQLQuery(url, handlers) {
  return http.post(url, async ({ request }) => {
    const payload = await request.json()
 
    // Resolve the intercepted GraphQL batched query
    // against the mocked GraphQL schema.
    const result = await executeGraphQL({
      source: payload.query,
      variableValues: data.variables,
      schema,
      rootValue: {
        // Mock individual queries, fields, and types.
        user: () => ({ id: 'abc-123' }),
      },
      async fieldResolver(source, args, context, info) {
        // Resolve the known fields from the "rootValue".
        if (source[info.fieldName]) {
          return defaultFieldResolver(source, args, context, info)
        }
 
        // Proxy the unknown fields to the actual GraphQL server.
        const compiledQuery = info.fieldNodes
          .map((node) => print(node))
          .join('\n')
 
        const query = `${info.operation.operation} { ${compiledQuery} }`
        const queryRequest = new Request(request, {
          body: JSON.stringify({ query }),
        })
        const response = await fetch(bypass(queryRequest))
        const { error, data } = await response.json()
 
        if (error) {
          throw error
        }
 
        return data[info.fieldName]
      },
    })
 
    return HttpResponse.json(result)
  })
}