GQLoom

中间件(Middleware)

中间件是一种函数,它介入了解析函数的处理流程。它提供了一种在请求和响应流程中插入逻辑的方式,以便在发送响应之前或在请求处理之前执行代码。 GQLoom 的中间件遵循了 Koa 的洋葱式中间件模式。

定义中间件

中间件是一个函数,它将在调用时被注入 options 对象作为参数,options 包含以下属性:

  • outputSilk: 输出丝线,包含了当前正在解析字段的输出类型;
  • parent: 当前字段的父节点,相当于 useResolverPayload().root
  • parseInput: 用于获取或修改当前字段的输入;
  • type: 当前字段的类型,其值为 query, mutation, subscriptionfield
  • next: 用于调用下一个中间件的函数;

options 还可以直接作为 next 函数使用。

另外,我们还可以通过 useContext()useResolverPayload() 获取到当前解析函数的上下文以及更多信息。

一个最基础的中间件函数如下:

import {  } from '@gqloom/core';

const :  = async () => {
  return await ();
}

接下来,我们将介绍一些常见的中间件形式。

错误捕获

在使用 ValibotZod 等库进行输入验证时,我们可以在中间件中捕获验证错误,并返回自定义的错误信息。

import { type  } from "@gqloom/core"
import {  } from "valibot"
import {  } from "graphql"

export const :  = async () => {
  try {
    return await ()
  } catch () {
    if ( instanceof ) {
      const { ,  } = 
      throw new (, { : {  } })
    }
    throw 
  }
}

验证输出

GQLoom中,默认不会对解析函数的输出执行验证。但我们可以通过中间件来验证解析函数的输出。

import { , type  } from "@gqloom/core"

export const :  = async ({ ,  }) => {
  const  = await ()
  return await .(, )
}

让我们尝试使用这个中间件:

import { , , ,  } from "@gqloom/valibot"
import * as  from "valibot"
import {  } from "node:http"
import {  } from "graphql-yoga"
import { ,  } from "./middlewares"

const  = ({
  : (.(.(), .(10)))
    .({ : .() })
    .() 
    .(({  }) => `Hello, ${}`),
})

export const  = (, , ) 

const  = ({  })
().(4000, () => {
  // eslint-disable-next-line no-console
  .("Server is running on http://localhost:4000/graphql")
})

在上面的代码中,我们对 hello 查询的输出添加了 v.minLength(10) 的要求,并在解析函数中添加了 outputValidator 中间件。 我们还在 weave 中添加了一个全局中间件 ValibotExceptionFilter

当我们进行以下查询时:

GraphQL Query
{
  hello(name: "W")
}

将会得到类似如下的结果:

{
  "errors": [
    {
      "message": "Invalid length: Expected >=10 but received 8",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "hello"
      ],
      "extensions": {
        "issues": [
          {
            "kind": "validation",
            "type": "min_length",
            "input": "Hello, W",
            "expected": ">=10",
            "received": "8",
            "message": "Invalid length: Expected >=10 but received 8",
            "requirement": 10
          }
        ]
      }
    }
  ],
  "data": null
}

如果我们调整输入,使返回的字符串长度符合要求:

GraphQL Query
{
  hello(name: "World")
}

将会得到没有异常的响应:

{
  "data": {
    "hello": "Hello, World"
  }
}

鉴权

对用户的权限进行校验是一个常见的需求,我们可以通过中间件来轻易实现。

考虑我们的用户有 "admin""editor" 两种角色,我们希望管理员和编辑员分别可以访问自己的操作。 首先,我们实现一个 authGuard 中间件,用于校验用户的角色:

import { type  } from "@gqloom/core"
import {  } from "./context"
import {  } from "graphql"

export function (: "admin" | "editor"):  {
  return async () => {
    const  = await ()
    if ( == null) throw new ("Not authenticated")
    if (!..()) throw new ("Not authorized")
    return ()
  }
}

在上面的代码中,我们声明了一个 authGuard 中间件,它接受一个角色参数,并返回一个中间件函数。 中间件函数会检查用户是否已经认证,并且是否具有指定的角色,如果不符合要求,则抛出一个 GraphQLError 异常。

我们可以为不同的解析器应用不同的中间件:

import { ,  } from "@gqloom/core"
import * as  from "valibot"
import {  } from "./middlewares"

const  = (
  {
    : (.(), () => true),
  },
  {
    : [("admin")], 
  }
)

const  = (
  {
    : (.(), () => true),

    : (.(), () => true),
  },
  { : [("editor")] } 
)

在上面的代码中,我们为 adminResolvereditorResolver 分别应用了 authGuard 中间件,并指定了不同的角色。这样,只有具有相应角色的用户才能访问对应解析器内的操作。

日志

我们也可以通过中间件来实现日志记录功能。例如,我们可以创建一个 logger 中间件,用于记录每个字段解析函数的执行时间:

import { type  } from "@gqloom/core"
import {  } from "@gqloom/core/context"

export const :  = async () => {
  const  = ()!.

  const  = .()
  const  = await ()
  const  = .() - 

  .(`${..}.${.} [${} ms]`)
  return 
}

缓存

我们可以通过中间件来实现缓存功能。例如,我们可以创建一个 cache 中间件,用于缓存每个查询的解析结果:

import type {  } from "@gqloom/core"

/** Simple in-memory cache implementation */
const  = new <string, { : unknown; : number }>()

export interface CacheOptions {
  /**
   * Time to live in milliseconds
   * @default 60000
   */
  ?: number
}

export const  = (: CacheOptions = {}):  => {
  const {  = 60000 } = 

  const :  = async ({ ,  }) => {
    if (!?.) {
      return ()
    }

    const { ,  } = .
    const  = . || {}
    const  = `${.}.${}:${.()}`

    const  = .()
    if ( && .() - . < ) {
      return .
    }

    const  = await ()
    .(, { : , : .() })
    return 
  }

  // Only apply cache to queries by default
  . = ["query"]

  return 
}

修改输入

我们可以通过中间件来修改请求输入:

import { ,  } from "@gqloom/core"
import * as  from "valibot"

const  = .({
  : .(.("Post")),
  : .(),
  : .(),
  : .(),
  : .(),
})

interface IPost extends .<typeof > {}

const : IPost[] = []

export const  = ({
  : ()
    .(
      .({
        : .(),
        : .(),
        : .(),
      })
    )
    .(async ({ ,  }) => { 
      const  = await .() 
      . = (await ()). 
      .() 
      return () 
    })
    .(({ , ,  }) => {
      const  = {
        : .(),
        ,
        ,
        ,
      }
      .()
      return 
    }),
})

使用中间件

GQLoom 能够在各种范围内应用中间件,包括解析函数、解析器局部中间件和全局中间件。

解析函数中间件

我们可以在解析函数中直接使用中间件,只需要在构造时使用 use 方法,例如:

import { ,  } from "@gqloom/core"
import * as  from "valibot"
import {  } from "./middlewares"

const  = ({
  : (.(.(), .(10)))
    .({ : .() })
    .() 
    .(({  }) => `Hello, ${}`),
})

解析器局部中间件

我们也可以在解析器级别应用中间件,这样中间件将对解析器内的所有操作生效。 只需要使用 use 方法为解析器构添加 middlewares

import { ,  } from "@gqloom/core"
import * as  from "valibot"
import {  } from "./middlewares"

const  = ({
  : (.(), () => true),
}).(("admin")) 

const  = ({
  : (.(), () => true),

  : (.(), () => true),
}).(("editor")) 

全局中间件

为了应用全局中间件,我们需要在 weave 函数中传入中间件字段,例如:

import { weave } from "@gqloom/core"
import { exceptionFilter } from "./middlewares"

export const schema = weave(helloResolver, exceptionFilter) 

根据操作类型应用中间件

我们可以为中间件指定在哪些操作类型上生效。

import type {  } from "@gqloom/core"
import {  } from "graphql"

export const :  = async ({  }) => {
  try {
    await .()

    const  = await ()

    await .()

    return 
  } catch () {
    await .()
    throw new ("Transaction failed", {
      : { :  },
    })
  }
}

. = ["mutation"]

Middleware.operations 是一个字符串数组,用于指定中间件在哪些操作类型上生效,可选值为:

  • "query"
  • "mutation"
  • "subscription"
  • "field"
  • "subscription.resolve"
  • "subscription.subscribe"

Middleware.operations 的默认值为 ["field", "query", "mutation", "subscription.subscribe"]

目录