具有GraphQl Server Codegen预设的可伸缩API
#node #graphql #服务器 #codegen

本文于2023年1月24日星期二发表,Eddy Nguyen @ The Guild Blog

GraphQl Server通常是一个中心系统,团队需要能够开发功能,而
不阻止其他团队。每个团队都可以拥有各种标准和实践。因此,如果GraphQl
无法在结构中设置服务器,以允许并发贡献,开发减慢和
在管理任务上花费了更多时间,而不是提供新功能。

本博客文章探讨了一些常见问题GraphQL服务器按比例遇到的

推荐如何解决它们。

简单问题:代码所有权

问题:如何管理代码所有权?

在许多没有明确结构和准则的团队中共享一个代码库是
的食谱 灾难。代码库中的第一团队通常建立适合他们的结构。当一个
第二队加入,他们很可能遵循已经在那里的结构。同样的故事发生在
此后的每个团队。经过几回合,开发减慢了。可以回顾结构
并发现自己盯着一个由一个团队组成的代码库。

团队希望将其成员通知与他们的域相关的更改,但并非每个拉力

请求(PR)。如果您曾经通知您与团队无关的变化,则很可能是代码库
需要设置以支持许多从事它的团队。

在下面的示例中,A团队是第一个设置服务器的团队。他们管理UserAuth
域,因此它们为这些文件创建datasourcesresolvers文件夹:

├── src/
│   ├── schema/
│   │   ├── datasources/
│   │   │   ├── UserDatasource.ts
│   │   │   ├── AuthDatasource.ts
│   │   ├── resolvers/
│   │   │   ├── userResolvers.ts
│   │   │   ├── authResolvers.ts
│   │   ├── userSchema.graphql
│   │   ├── authSchema.graphql
│   ├── server.ts
│   ├── codegen.yml

然后,B团队进入代码库。他们管理Book域,因此他们按照
添加文件 相同的结构:

├── src/
│   ├── schema/
│   │   ├── datasources/
│   │   │   ├── UserDatasource.ts
│   │   │   ├── AuthDatasource.ts
│   │   │   ├── BookDatasource.ts
│   │   ├── resolvers/
│   │   │   ├── userResolvers.ts
│   │   │   ├── authResolvers.ts
│   │   │   ├── bookResolvers.ts
│   │   ├── userSchema.graphql
│   │   ├── authSchema.graphql
│   │   ├── bookSchema.graphql
│   ├── server.ts
│   ├── codegen.yml

虽然这很小,但通信开销较低,但无法扩展。当有
数十个或数百个数据源和解析器,很难知道谁拥有什么。一个简单的
解决方案是使用GitHub的Colepter或类似功能将文件分配给所有者。但是它必须
在每个文件上都可以完成,因为文件分为类别文件夹,例如resolvers
datasources

很容易将此结构分为文件夹,每个结构都以管理它的团队命名:

├── src/
│   ├── schema/
│   │   ├── TeamA/  # Team A notified (by CODEOWNERS) if changes happen in this folder
│   │   │   ├── datasources/
│   │   │   │   ├── UserDatasource.ts
│   │   │   │   ├── AuthDatasource.ts
│   │   │   ├── resolvers/
│   │   │   │   ├── userResolvers.ts
│   │   │   │   ├── authResolvers.ts
│   │   │   ├── userSchema.graphql
│   │   │   ├── authSchema.graphql
│   │   ├── TeamB/  # Team B notified (by CODEOWNERS) if changes happen in this folder
│   │   │   ├── datasources/
│   │   │   │   ├── BookDatasource.ts
│   │   │   ├── resolvers/
│   │   │   │   ├── bookResolvers.ts
│   │   │   ├── bookSchema.graphql
│   ├── server.ts
│   ├── codegen.yml

由于所有权的定义,这已经是一个重大改进,但是组织问题
保持。例如,当团队A更改自己拥有的东西,分为较小的团队或
时会发生什么情况 更改名称?所有这些方案都需要管理工作:在最佳情况下重命名文件夹,
在最坏的情况下移动文件。这样的管理工作对最终用户没有任何价值。

最重要的是,所有数据源和解析器都必须放在GraphQL Server中。是
此示例中的server.ts文件。谁拥有此文件和其他服务器维护,例如
软件包更新,安全补丁,codegen.yml等。?

解决方案:分为模块

通常看到团队更改他们的名字,分为团队的规模或结合结构和
优先级变化。但是,长期以来通常保持稳定的是业务
领域。如果我们根据业务领域分配模式并相应地分配团队,则将变成
更可扩展。如果一个团队需要将一个域名交给另一个团队,他们只需要更新
Covelowner的文件。

最好让一小部分专用维护器用于服务器维护。这可以组成
代码库中每个团队的一个成员的成员可以旋转。或者,有些
公司可以选择将此责任分配给专门的团队。有一个专门的小组 -
让我们称他们为维护者 - 有助于减少其余团队的噪音和认知负荷,
允许他们专注于提供功能。

以前使用相同的示例,我们可以识别3个主要域:UserAuthBook,每个
设置了计量人员,以通知适当的更改团队。维护者拥有server.ts
其他配置等codegen.yml(我们都使用
GraphQL Codegen,对吗? ð)。

├── src/
│   ├── schema/
│   │   ├── user/                      # Team A notified if changed
│   │   │   ├── datasources.ts
│   │   │   ├── resolvers.ts
│   │   │   ├── schema.graphql
│   │   ├── auth/                      # Team A notified if changed
│   │   │   ├── datasources.ts
│   │   │   ├── resolvers.ts
│   │   │   ├── schema.graphql
│   │   ├── book/                      # Team B notified if changed
│   │   │   ├── datasources.ts
│   │   │   ├── resolvers.ts
│   │   │   ├── schema.graphql
│   ├── server.ts                      # Maintainers notified if changed
│   ├── codegen.yml                    # Maintainers notified if changed

棘手的问题:大规模的最佳实践对齐

将模式分为模块通常很容易让团队达成共识。然后我们开始看到
严重问题:如何为团队执行最佳实践,同时减少维护者的需求
花在服务器维护上。

1.如何为所有团队执行最佳实践

在澳大利亚炎热的夏日,不良的做法和惯例像丛林大火一样传播;这是
最糟糕的!我曾经是维护者团队的一员。我们有有关各种主题的准则,一个是
解析器命名公约。有一次,开发人员错误地使用了帕斯卡案,而不是
骆驼香烟盒。第二天,我在帕斯卡(Pascal)案中醒来了一半以上的解析器。 ð±

好吧,我在这里夸大了,但是经历非常创伤。

只有在人们关注它们的情况下,准则才是好的。标准开始滑倒而没有明确的和
自动执法和不良实践开始传播。

我们需要自动化工具来有效执行指南。幸运的是,这些天我们有广泛的
帮助GraphQL Server最佳实践的工具范围:

但是,有一些需要改进的领域。

例如,我正在处理的GraphQL服务器之一的指南是使用生成的类型
来自GraphQl Codegen。一些团队成员,尤其是代码库的新团队成员,可能尚未是
意识到这一点。幸运的是,其他团队成员或维护者在公关审查时间遇到了这些问题。
但是,如果我们有自动执行此准则的工具,这将节省所有人的时间和精力。

2.如何最大程度地减少对维护者的噪音

维护者通常需要管理GraphQl Server的这些方面:

  • 核心服务器逻辑:解析器地图,架构,CI/CD等。
  • 配置文件:.graphqlrccodegen.yml等。

更改维护者域中的任何内容都应通知它们。不幸的是,这发生了
定期进行常规工作流程。维护者还拥有团队的工作和一般计划和
安全更新以担心。因此,被通知无关的PR会很快导致倦怠。

例如,如果添加了新的解析器,则必须手动将其添加到解析器地图中。所以,
维护者会收到每个新的解析器的通知。如果您正在使用
,可能看起来像这样 GraphQL Yoga

// server.ts (managed by the Maintainers)
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { Auth } from "./schema/auth/resolvers";
import { book, Book } from "./schema/auth/resolvers";
import { user, User } from "./schema/user/resolvers";

const schema = createSchema({
  typeDefs: `...`,
  resolvers: { // This is the resolver map
    Query: {
      user,
      book,
    },
    Auth,
    Book
    User,
  }
});

const yoga = createYoga({ schema });
const server = createServer(yoga)
server.listen(4000, () => {
  console.info('Server is running on http://localhost:4000/graphql')
})

有一种减轻此问题的方法:每个模式模块导出一个解析器的对象,它们

作为数组传递到resolvers。在内部,解析器使用
合并 mergeResolvers from @graphql-tools/merge.
这意味着在每个新模块而不是每个新的解析器上都会通知维护者:

// server.ts
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import * as authResolvers from './schema/auth/resolvers'
import * as bookResolvers from './schema/book/resolvers'
import * as userResolvers from './schema/user/resolvers'

const schema = createSchema({
  typeDefs: `...`,
  resolvers: [
    // mergeResolvers are called internally
    authResolvers,
    bookResolvers,
    userResolvers
  ]
})

// rest of server config

但是,mergeResolvers有一个警告:它只是在运行时合并普通的JavaScript对象。
因此,有人会意外地覆盖他人的解析器的风险。这个问题很难
在具有数百个解析器和模块的大型代码库中找到。

mergeResolvers can accidentally override resolvers if there are conflicts in resolver names

另一个常用的功能是
mappers.
此功能允许解析器返回自定义映射器对象,而不是GraphQL输出类型。

映射器的问题是,我们需要每次需要创建一个:
时都需要更新codegen.yml

# codegen.yml
generates:
  src/schema/types.generated.ts:
    plugins:
      - typescript
      - typescript-resolvers
    mappers:
      User: './mappers#UserMapper'
      Profile: './mappers#ProfileMapper'
      # Add another line for each mapper

这对维护人员来说是一个问题,因为这些更改是基于团队要求。是否
团队是否使用映射者是团队的选择,不应关心维护者。但是,
通知维护者,因为他们拥有codegen.yml文件。

解决方案:使用GraphQL Server Codegen预设

要解决上述问题,我正在为GraphQl Server处理CodeGen Preset:
@eddeee888/gcg-typescript-resolver-files.
目的是从准则/配置转变为约定。所有变化发生在团队的模块中,
因此,功能工作不会通知维护者。

这适用于任何GraphQl Server实现,例如GraphQl Yoga,Apollo Server等

这是开始的方法:

yarn add -D @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files

然后,您可以添加以下配置:

# codegen.yml
schema: 'src/**/*.graphql'
generates:
  src/schema:
    preset: '@eddeee888/gcg-typescript-resolver-files'

请注意,此预设包括@graphql-codegen/typescript
引擎盖下的@graphql-codegen/typescript-resolvers,因此您不必手动设置它!

现在,我们要做的就是设置这样的模块模块:

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── schema.graphql

给定架构文件的以下内容:

# src/schema/base.graphql
type Query
type Mutation

# src/schema/user.graphql
extend type Query {
  user(id: ID!): User
}
type User {
  id: ID!
  fullName: String!
}

# src/schema/book.graphql
extend type Query {
  book(id: ID!): Book
}
extend type Mutation {
  markBookAsRead(id: ID!): Book!
}
type Book {
  id: ID!
  isbn: String!
}

我们运行CodeGen:

yarn codegen

我们将看到以下文件:

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── user.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── User.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── book.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── Mutation/
│   │   │   │   │   ├── markBookAsRead.ts  # Generated, changes not overwritten by codegen
│   │   │   │   ├── Book.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── types.generated.ts             # Entirely generated by codegen
│   │   ├── resolvers.generated.ts         # Entirely generated by codegen

生成的文件

  • 共享模式和解析器打字稿类型types.generated.ts。这是由
    生成的 @graphql-codegen/typescript@graphql-codegen/typescript-resolvers插件。这可以是
    在git中忽略或从清销商处删除,因为它是完全生成的。

  • 分辨率地图resolvers.generated.ts。这将所有其他解析器静态地放在一起,
    准备由GraphQL Server使用。这可以在git中忽略或从清算者中删除
    因为它是完全生成的。

// src/schema/resolvers.generated.ts

/* This file was automatically generated. DO NOT UPDATE MANUALLY. */
import type { Resolvers } from './types.generated'
import { book as Query_book } from './book/resolvers/Query/book'
import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'
import { Book } from './book/resolvers/Book'
import { user as Query_user } from './user/resolvers/Query/user'
import { User } from './user/resolvers/User'
export const resolvers: Resolvers = {
  Query: {
    book: Query_book,
    user: Query_user
  },
  Mutation: {
    markBookAsRead: Mutation_markBookAsRead
  },

  Book: Book,
  User: User
}
  • 操作解析器
    • src/schema/user/resolvers/Query/user.ts
    • src/schema/book/resolvers/Query/book.ts
    • src/schema/book/resolvers/Mutation/book.ts
// Example: src/schema/user/resolvers/Query/user.ts
import type { QueryResolvers } from './../../../types.generated'
export const user: NonNullable<QueryResolvers['user']> = async (_parent, _arg, _ctx) => {
  /* Implement Query.user resolver logic here */
}
  • 对象类型解析器
    • src/schema/user/resolvers/User.ts
    • src/schema/book/resolvers/Book.ts
// Example: src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const User: UserResolvers = {
  /* Implement User resolver logic here */
}

解析器是考虑到开发人员经验的生成:

  • 自动键入:您可以直接进行解析器逻辑。
  • 架构中的位置与文件系统上的生成位置匹配:您可以轻松跳到一个 解析器文件。例如,可以在Query/user.ts中找到user查询逻辑。

Search for resolver files easily because the filesystem location matches schema location

当CodeGen再次运行时,解析器文件不会被覆盖。但是,有一些聪明的人
内置以确保正确导出解析器。例如,如果我们将User解析器重命名为
WrongUsersrc/schema/user/resolvers/User.ts中,然后运行Codegen,将更新文件
警告:

// Example: src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const WrongUser: UserResolvers = {
  /* Implement User resolver logic here */
}
/* WARNING: The following resolver was missing from this file. Make sure it is properly implemented or there could be runtime errors. */
export const User: UserResolvers = {
  /* Implement User resolver logic here */
}

其中一些功能受gqlgen的启发,因此请检查一下是否需要
Golang GraphQL Server实现。

其他GraphQL类型

这些其他类型也得到了预设的支持:

  • 联合:为每种联合类型生成一个文件。
  • 标量:

    • 如果标量名与graphql-scalars中的名称匹配,则 自动从graphql-scalars导入到解析器图中。确保安装它:
    yarn add graphql-scalars
    
    • 如果标量名在graphql-scalars中不存在,则为每个标量生成一个文件 类型。

对于其他当前非支撑类型的类型,我们可以使用externalResolvers预设
声明它们 配置。

映射者大会

可以通过导出使用Mapper后缀的类型或接口来添加映射器
每个模块中的文件。例如,UserMapper将用作User的映射器类型。

// src/schema/user/schema.mappers.ts

// This works! This will be used as mapper for `User` object type
export { User as UserMapper } from 'external-module'

// This 1 works! For `User1` object type
export interface User1Mapper {
  id: string
}

// This works 2! For `User2` object type
export type User2Mapper = { id: string }

// This works 3! For `User3` object type
interface User3Mapper {
  id: string
}
export { User3Mapper }

支持逐渐迁移

如果您有模块化结构中的现有代码库,但不能一次迁移,则 预设具有whitelistedModulesblacklistedModules支持逐渐迁移的选项。

可定制的约定

所有提到的约定都是可定制的!查看
的文档 more options.

服务器预设和graphql-modules之间的差异

到目前为止,预设听起来可能与graphql-modules一样 他们都将模式分为模块。但是,他们解决了不同的问题。

graphql-modules是一个模块化实用程序库,允许每个模块维护架构
分别定义和解析器,同时在运行时提供统一的模式。

预设专注于约定(例如文件结构,类型,与其他集成
图书馆等)。但是,它不会迫使架构模块化对用户。实际上,
建议使用默认的modules模式来实现其可扩展性和简单性。预设也有
merged模式以单片的方式生成文件可能适合某些团队和组织。

这也意味着预设中可能会有一种支持graphql-modules的模式。

概括

在此博客文章中,我们探讨了如果您的GraphQl Server不是
准备比例:

  • 所有权不清楚
  • 忽略最佳实践
  • 嘈杂的维护

我们可以通过遵循概述的建议来解决这些问题:

SEEK上,我们正在尝试此预设并获得阳性
反馈。 Let me know on Twitter如果也适合您!