【GraphQL】使用Apollo Server v4和Prisma,在context中共享数据的方法

首先

在学习GraphQL期间,我使用了Apollo Server的v4版本。然而,在实现context时遇到了很多困难,且缺少相关参考文献,只能通过试错的方式来最终实现。因此,我决定将这篇文章作为备忘录留下来,供他人参考。

实施内容如标题所述,使用Apollo Server的context在解析器和插件中共享数据。

模式

首先,我们来看一下表定义和模式。

Prisma模式

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Link {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  description String
  url         String
  user        User?    @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId      Int?
}

model User {
  id       Int    @id @default(autoincrement())
  name     String
  email    String @unique
  password String
  links    Link[]
}

在这里,重要的是关系部分,Link模型的user列和User模型的links列。

将此定义应用到GraphQL的模式定义中。

GraphQL模式

type Query {
  feed: [Link]!
}

type Mutation {
  post(url: String!, description: String!): Link!
  signUp(email: String!, password: String!, name: String!): AuthPayload
  login(email: String!, password: String!): AuthPayload
}

type Link {
  id: ID!
  description: String!
  url: String!
  user: User
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

type AuthPayload {
  token: String
  user: User
}

解释这一重要点时,首先要提到的是,在GraphQL的模式定义中,它与Prisma的模式定义保持一致性。

查询和变更

初学者专用说明书

对于GraphQL初学者的解说如下,Query和Mutation具体如下。

    • Query:SQLのSELECT文、CRUDのREADにあたり、データ取得に使用

 

    • Mutation:SQLのINSERT・UPDATE・DELETE文、CRUDのREAD以外にあたり、データの追加・変更に使用

 

    ※以下が対応表です
SQL/RESTGraphQLSELECTQueryINSERTMutationUPDATEMutationDELETEMutation

此外,我们在Query和Mutation中定义了feed和post等内容,这是为了定义一个名为”resolver”的函数类型。

解决者是什么意思。

解決器是将信息输入到查询类型中的过程。

简单来说,就是为模式定义的类型定义实际的值和操作。
例如,查询解析器的定义如下:

export const feed = async (_: unknown, __: unknown, context: Context) => {
  return context.prisma.link.findMany()
}

由于模式的类型信息如下,因此在解析器中,我们定义了feed函数,并将Link作为数组返回。

type Query {
  feed: [Link]!
}

换句话说,这意味着在模式中定义了函数名(解析器的名称)和返回类型。

突变也是一样的。

const APP_SECRET = process.env.APP_SECRET as string

// ユーザー新規登録
export const singUp = async (
  _: unknown,
  args: { email: string; password: string; name: string },
  context: Context
) => {
  // パスワードの設定
  const password = await bcrypt.hash(args.password, 10)

  if (!args.email || !password || !args.name) {
    throw new Error('Email、Password or Name is required')
  }

  // ユーザー新規作成
  const user = await context.prisma.user.create({
    data: {
      ...args,
      password,
    },
  })

  const token = jwt.sign({ userId: user.id }, APP_SECRET)

  if (!token) {
    throw new Error('Token is required')
  }

  return {
    token,
    user,
  }
}
type Mutation {
  signUp(email: String!, password: String!, name: String!): AuthPayload
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

type AuthPayload {
  token: String
  user: User
}

以注册为例,接受参数email、password和name,返回值为AuthPayload,其中包含token和user。然后,在解析器中详细描述处理过程。

有关自定义解析器的内容

在先前的架构定义中,似乎只是实现了User类型和Link类型,但是这些类型的user和links被定义为自定义解析器。
换句话说,在Prisma的架构中,虽然有关系关系,但实际上要执行诸如获取与User相关联的Link数组数据等操作,需要解析器。

如此一來,除了Query和Mutation之外,您也可以自行實現解析器。這稱為自定義解析器。
(以下的連結被稱為自定義解析器)

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

下面是解决器的处理内容。
其操作非常简单,只是通过一个数组获取与用户相关联的链接。

在参数部分最显著的特征是parent.id,这可以用来处理由父级解析器返回的对象的值。
简而言之,在Prisma中,User和Link建立了关系,但是由于父表是User,因此可获取的数据如下所示。
parent.id = user.id

export const links = (parent: { id: any }, __: unknown, context: Context) => {
  return context.prisma.user
    .findUnique({
      where: { id: parent.id },
    })
    .links()
}

关于解析器的参数

在这里,我想解释一下参数。以下图表表示了所有内容。请自行查找详细信息。

引数解説parent親のリゾルバーが返すオブジェクトの値argsクライアントから渡される値(フォームの入力値など)contextリクエスト全体で共有される値(認証情報やDB接続情報など)info実行されるクエリに関する詳細情報(ほとんど使用しない)

在 Context 中共享数据的方法

在前提的说明结束后,接下来是正题。
根据之前提到的上下文,在参数的说明中我将解释如何共享数据。

由於對於處理Apollo Server v4系列的文章只有官方文檔+一些其他資源,所以基本上是以官方文檔為基礎進行開發。
以下是實現程式碼。

import 'dotenv/config'

import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import { loadSchemaSync } from '@graphql-tools/load'
import { addResolversToSchema } from '@graphql-tools/schema'
import { PrismaClient } from '@prisma/client'
import { join } from 'path'

import { user } from './resolvers/Link'
import { login, post, singUp } from './resolvers/Mutation'
import { feed } from './resolvers/Query'
import { links } from './resolvers/User'
import { getUserId } from './utils'

const prisma = new PrismaClient()

const schema = loadSchemaSync(join(__dirname, './schema.graphql'), {
  loaders: [new GraphQLFileLoader()],
})

// リゾルバー関数
const resolvers = {
  Query: {
    feed: feed,
  },

  Mutation: {
    signUp: singUp,
    login: login,
    post: post,
  },

  Link: {
    user: user,
  },

  User: {
    links: links,
  },
}

const schemaWithResolvers = addResolversToSchema({ schema, resolvers })
const server = new ApolloServer({
  schema: schemaWithResolvers,
})

const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    context: async ({ req }) => ({
      ...req,
      prisma,
      userId: req && req.headers.authorization ? getUserId(req) : null,
    }),
    listen: { port: 4000 },
  })
  console.log(`?  Server ready at: ${url}`)
}

startServer()

以下是上下文的实现部分。

const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    context: async ({ req }) => ({
      ...req,
      prisma,
      userId: req && req.headers.authorization ? getUserId(req) : null,
    }),
    listen: { port: 4000 },
  })
  console.log(`?  Server ready at: ${url}`)

根据公式,Context的作用如下。

通过在GraphQL操作中创建一个名为contextValue的对象,可以在服务器的解析器和插件之间共享数据。
通过contextValue,可以传递给解析器所需的便利项,如身份验证范围、获取数据的源、数据库连接、自定义获取函数等。如果使用数据加载器在解析器之间进行请求批处理,还可以将数据加载器附加到共享的contextValue上。
上下文函数是异步的,必须返回一个对象。通过使用名为contextValue的名称,可以访问服务器的解析器和插件中的这个对象。
可以将上下文函数传递给选择的集成函数(如expressMiddleware或startStandaloneServer)。
服务器每个请求调用一次上下文函数,并使用每个请求的详细信息(如HTTP头)来定制contextValue。

通常情况下,通过将Context传递给startStandaloneServer等集成函数,可以在解析器和插件之间共享解析器所需的认证信息和其他必要信息。

另外,还需要满足以下两个要求。

    • 非同期で実装する必要

 

    オブジェクトを返す必要がある

换句话说,我的实现意味着以下内容可以在整个解析器中共享。

    • req:リクエスト情報

 

    • prisma: prismaClientのインスタンス

 

    userId: 認証情報(ユーザーIDからトークンを生成するためuserIdが認証情報となる)

上下文的使用例

让我们来看一个投稿API实现的真实例子作为参考。

export const post = async (
  _: unknown,
  args: { description: string; url: string },
  context: Context
) => {
  const { userId } = context

  const newLink = await context.prisma.link.create({
    data: {
      url: args.url,
      description: args.description,
      user: { connect: { id: userId } },
    },
  })

  return newLink
}

以下是Context的类型安排。

import type { Prisma, PrismaClient } from '@prisma/client'
import type { DefaultArgs } from '@prisma/client/runtime'

export type Context = {
  prisma: PrismaClient<
    Prisma.PrismaClientOptions,
    never,
    Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined,
    DefaultArgs
  >
  userId: number
}

使用上下文context的地方分为两部分:用户Id的解构赋值和context.prisma部分。通过这样做,可以在其他解析器函数中同样使用context.prisma.方法进行实现。好处包括减少了认证实现和PrismaClient实例化的工作,从而减少了代码量和内存使用量等。

顺便说一句,关于 “user: { connect: { id: userId } }” 这部分,Prisma中的Link和User建立了关联。这个描述表示了它们之间的关系。

通过使用connect选项,您可以将现有的User记录(现有用户)与Link记录(在此处是新建的Link)关联起来。键值对应于userId,因此请使用{ id: userId }。
这是一个常见的误解:“为什么是`{ id: userId }`而不是Link的ID?”
由于最初指定了user: {connect: ・・・},所以{ id: userId }是正确的。

最后

最后衷心建议,请务必注意公式方面的信息更加准确。关于“参考文献”章节中的“背景和背景值”,请参阅官方文档,有兴趣的话也可以一起查阅。

请提供相关文献。

上下文和上下文值
使用Apollo Server v4、Next.js、Prisma来设置GraphQL

广告
将在 10 秒后关闭
bannerAds