Skip to content
GQLoom

中间件(Middleware)

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

GQLoom 的中间件遵循了 Koa 的洋葱式中间件模式。

定义中间件

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

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

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

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

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

ts
import { 
Middleware
} from '@gqloom/core';
const
middleware
:
Middleware
= async (
next
) => {
return await
next
();
}

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

错误捕获

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

ts
import { type 
Middleware
} from "@gqloom/core"
import {
ValiError
} from "valibot"
import {
GraphQLError
} from "graphql"
export const
valibotExceptionFilter
:
Middleware
= async (
next
) => {
try { return await
next
()
} catch (
error
) {
if (
error
instanceof
ValiError
) {
const {
issues
,
message
} =
error
throw new
GraphQLError
(
message
, {
extensions
: {
issues
} })
} throw
error
} }
ts
import { type 
Middleware
} from "@gqloom/core"
import {
ZodError
} from "zod"
import {
GraphQLError
} from "graphql"
export const
zodExceptionFilter
:
Middleware
= async (
next
) => {
try { return await
next
()
} catch (
error
) {
if (
error
instanceof
ZodError
) {
throw new
GraphQLError
(
error
.
format
().
_errors
.
join
(", "), {
extensions
: {
issues
:
error
.
issues
},
}) } throw
error
} }

验证输出

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

ts
import { 
silk
, type
Middleware
} from "@gqloom/core"
import {
GraphQLError
} from "graphql"
export const
outputValidator
:
Middleware
= async (
opts
) => {
const
output
= await
opts
.
next
()
const
result
= await
silk
.
parse
(
opts
.
outputSilk
,
output
)
if (
result
.
issues
) {
throw new
GraphQLError
(
result
.
issues
[0].
message
, {
extensions
: {
issues
:
result
.
issues
},
}) } return
result
.
value
}

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

Valibot

ts
import { 
ValibotWeaver
,
weave
,
resolver
,
query
} from "@gqloom/valibot"
import * as
v
from "valibot"
import {
createServer
} from "node:http"
import {
createYoga
} from "graphql-yoga"
import {
outputValidator
,
valibotExceptionFilter
} from "./middlewares"
const
helloResolver
=
resolver
({
hello
:
query
(
v
.
pipe
(
v
.
string
(),
v
.
minLength
(10)))
.
input
({
name
:
v
.
string
() })
.
use
(
outputValidator
)
.
resolve
(({
name
}) => `Hello, ${
name
}`),
}) export const
schema
=
weave
(
ValibotWeaver
,
helloResolver
,
valibotExceptionFilter
)
const
yoga
=
createYoga
({
schema
})
createServer
(
yoga
).
listen
(4000, () => {
// eslint-disable-next-line no-console
console
.
info
("Server is running on http://localhost:4000/graphql")
})

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

Zod

ts
import { 
weave
,
resolver
,
query
} from "@gqloom/zod"
import * as
z
from "zod"
import {
createServer
} from "node:http"
import {
createYoga
} from "graphql-yoga"
import {
outputValidator
,
zodExceptionFilter
} from "./middlewares"
const
helloResolver
=
resolver
({
hello
:
query
(
z
.
string
().
min
(10))
.
input
({
name
:
z
.
string
() })
.
use
(
outputValidator
)
.
resolve
(({
name
}) => `Hello, ${
name
}`),
}) export const
schema
=
weave
(
helloResolver
,
zodExceptionFilter
)
const
yoga
=
createYoga
({
schema
})
createServer
(
yoga
).
listen
(4000, () => {
// eslint-disable-next-line no-console
console
.
info
("Server is running on http://localhost:4000/graphql")
})

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

结果

当我们进行以下查询时:

graphql
{
  hello(name: "W")
}

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

json
{
  "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
}
json
{
  "errors": [
    {
      "message": "String must contain at least 10 character(s)",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "hello"
      ],
      "extensions": {
        "issues": [
          {
            "code": "too_small",
            "minimum": 10,
            "type": "string",
            "inclusive": true,
            "exact": false,
            "message": "String must contain at least 10 character(s)",
            "path": []
          }
        ]
      }
    }
  ],
  "data": null
}

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

graphql
{
  hello(name: "World")
}

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

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

鉴权

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

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

ts
import { type 
Middleware
} from "@gqloom/core"
import {
useUser
} from "./context"
import {
GraphQLError
} from "graphql"
export function
authGuard
(
role
: "admin" | "editor"):
Middleware
{
return async (
next
) => {
const
user
= await
useUser
()
if (
user
== null) throw new
GraphQLError
("Not authenticated")
if (!
user
.
roles
.
includes
(
role
)) throw new
GraphQLError
("Not authorized")
return
next
()
} }

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

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

ts
import { 
resolver
,
mutation
} from "@gqloom/core"
import * as
v
from "valibot"
import {
authGuard
} from "./middlewares"
const
adminResolver
=
resolver
(
{
deleteArticle
:
mutation
(
v
.
boolean
(), () => true),
}, {
middlewares
: [
authGuard
("admin")],
} ) const
editorResolver
=
resolver
(
{
createArticle
:
mutation
(
v
.
boolean
(), () => true),
updateArticle
:
mutation
(
v
.
boolean
(), () => true),
}, {
middlewares
: [
authGuard
("editor")] }
)
ts
import { 
resolver
,
mutation
} from "@gqloom/zod"
import * as
z
from "zod"
import {
authGuard
} from "./middlewares"
const
adminResolver
=
resolver
(
{
deleteArticle
:
mutation
(
z
.
boolean
(), () => true),
}, {
middlewares
: [
authGuard
("admin")],
} ) const
editorResolver
=
resolver
(
{
createArticle
:
mutation
(
z
.
boolean
(), () => true),
updateArticle
:
mutation
(
z
.
boolean
(), () => true),
}, {
middlewares
: [
authGuard
("editor")] }
)

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

日志

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

ts
import { type 
Middleware
} from "@gqloom/core"
import {
useResolverPayload
} from "@gqloom/core/context"
export const
logger
:
Middleware
= async (
next
) => {
const
info
=
useResolverPayload
()!.
info
const
start
=
Date
.
now
()
const
result
= await
next
()
const
resolveTime
=
Date
.
now
() -
start
console
.
log
(`${
info
.
parentType
.
name
}.${
info
.
fieldName
} [${
resolveTime
} ms]`)
return
result
}

缓存

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

ts
import type { 
Middleware
} from "@gqloom/core"
/** Simple in-memory cache implementation */ const
cacheStore
= new
Map
<string, {
data
: unknown;
timestamp
: number }>()
export interface CacheOptions { /** * Time to live in milliseconds * @default 60000 */
ttl
?: number
} export const
cache
= (
options
: CacheOptions = {}):
Middleware
=> {
const {
ttl
= 60000 } =
options
const
middleware
:
Middleware
= async ({
next
,
payload
}) => {
if (!
payload
?.
info
) {
return
next
()
} const {
fieldName
,
parentType
} =
payload
.
info
const
args
=
payload
.
args
|| {}
const
cacheKey
= `${
parentType
.
name
}.${
fieldName
}:${
JSON
.
stringify
(
args
)}`
const
cached
=
cacheStore
.
get
(
cacheKey
)
if (
cached
&&
Date
.
now
() -
cached
.
timestamp
<
ttl
) {
return
cached
.
data
} const
result
= await
next
()
cacheStore
.
set
(
cacheKey
, {
data
:
result
,
timestamp
:
Date
.
now
() })
return
result
} // Only apply cache to queries by default
middleware
.
operations
= ["query"]
return
middleware
}

修改输入

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

ts
import { 
mutation
,
resolver
} from "@gqloom/core"
import * as
v
from "valibot"
const
Post
=
v
.
object
({
__typename
:
v
.
nullish
(
v
.
literal
("Post")),
id
:
v
.
number
(),
title
:
v
.
string
(),
content
:
v
.
string
(),
authorId
:
v
.
number
(),
}) interface IPost extends
v
.
InferOutput
<typeof
Post
> {}
const
posts
: IPost[] = []
export const
postsResolver
=
resolver
({
createPost
:
mutation
(
Post
)
.
input
(
v
.
object
({
title
:
v
.
string
(),
content
:
v
.
string
(),
authorId
:
v
.
number
(),
}) ) .
use
(async ({
next
,
parseInput
}) => {
const
result
= await
parseInput
.
getResult
()
result
.
authorId
= (await
useUser
()).
id
parseInput
.
setResult
(
result
)
return
next
()
}) .
resolve
(({
title
,
content
,
authorId
}) => {
const
post
= {
id
:
Math
.
random
(),
title
,
content
,
authorId
,
}
posts
.
push
(
post
)
return
post
}), })
ts
import { 
mutation
,
resolver
} from "@gqloom/core"
import {
z
} from "zod"
const
Post
=
z
.
object
({
__typename
:
z
.
literal
("Post").
nullish
(),
id
:
z
.
number
(),
title
:
z
.
string
(),
content
:
z
.
string
(),
authorId
:
z
.
number
(),
}) interface IPost extends
z
.
output
<typeof
Post
> {}
const
posts
: IPost[] = []
export const
postsResolver
=
resolver
({
createPost
:
mutation
(
Post
)
.
input
(
z
.
object
({
title
:
z
.
string
(),
content
:
z
.
string
(),
authorId
:
z
.
number
(),
}) ) .
use
(async ({
next
,
parseInput
}) => {
const
result
= await
parseInput
.
getResult
()
result
.
authorId
= (await
useUser
()).
id
parseInput
.
setResult
(
result
)
return
next
()
}) .
resolve
(({
title
,
content
,
authorId
}) => {
const
post
= {
id
:
Math
.
random
(),
title
,
content
,
authorId
,
}
posts
.
push
(
post
)
return
post
}), })

使用中间件

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

解析函数中间件

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

ts
import { 
resolver
,
query
} from "@gqloom/core"
import * as
v
from "valibot"
import {
outputValidator
} from "./middlewares"
const
helloResolver
=
resolver
({
hello
:
query
(
v
.
pipe
(
v
.
string
(),
v
.
minLength
(10)))
.
input
({
name
:
v
.
string
() })
.
use
(
outputValidator
)
.
resolve
(({
name
}) => `Hello, ${
name
}`),
})
ts
import { 
resolver
,
query
} from "@gqloom/zod"
import * as
z
from "zod"
import {
outputValidator
} from "./middlewares"
const
helloResolver
=
resolver
({
hello
:
query
(
z
.
string
().
min
(10))
.
input
({
name
:
z
.
string
() })
.
use
(
outputValidator
)
.
resolve
(({
name
}) => `Hello, ${
name
}`),
})

解析器局部中间件

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

ts
import { 
resolver
,
mutation
} from "@gqloom/core"
import * as
v
from "valibot"
import {
authGuard
} from "./middlewares"
const
adminResolver
=
resolver
({
deleteArticle
:
mutation
(
v
.
boolean
(), () => true),
}).
use
(
authGuard
("admin"))
const
editorResolver
=
resolver
({
createArticle
:
mutation
(
v
.
boolean
(), () => true),
updateArticle
:
mutation
(
v
.
boolean
(), () => true),
}).
use
(
authGuard
("editor"))
ts
import { 
resolver
,
mutation
} from "@gqloom/zod"
import * as
z
from "zod"
import {
authGuard
} from "./middlewares"
const
adminResolver
=
resolver
({
deleteArticle
:
mutation
(
z
.
boolean
(), () => true),
}).
use
(
authGuard
("admin"))
const
editorResolver
=
resolver
({
createArticle
:
mutation
(
z
.
boolean
(), () => true),
updateArticle
:
mutation
(
z
.
boolean
(), () => true),
}).
use
(
authGuard
("editor"))

全局中间件

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

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

export const schema = weave(helloResolver, exceptionFilter) 

根据操作类型应用中间件

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

ts
import type { 
Middleware
} from "@gqloom/core"
import {
GraphQLError
} from "graphql"
export const
transaction
:
Middleware
= async ({
next
}) => {
try { await
db
.
beginTransaction
()
const
result
= await
next
()
await
db
.
commit
()
return
result
} catch (
error
) {
await
db
.
rollback
()
throw new
GraphQLError
("Transaction failed", {
extensions
: {
originalError
:
error
},
}) } }
transaction
.
operations
= ["mutation"]

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

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

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