chore: import zh skill graphql-operations
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- Skill 名称:`graphql-operations`
|
||||
- 中文类目:GraphQL Schema 设计
|
||||
- 上游仓库:`apollographql__skills`
|
||||
- 上游路径:`skills/graphql-operations/SKILL.md`
|
||||
- 上游链接:https://github.com/apollographql/skills/blob/HEAD/skills/graphql-operations/SKILL.md
|
||||
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
name: graphql-operations
|
||||
description: >
|
||||
编写 GraphQL 操作(查询、变更、片段)的最佳实践指南。在以下情况下使用本技能:
|
||||
(1) 编写 GraphQL 查询或变更,
|
||||
(2) 使用片段组织操作,
|
||||
(3) 优化数据获取模式,
|
||||
(4) 设置类型生成或代码检查,
|
||||
(5) 审查操作效率。
|
||||
license: MIT
|
||||
compatibility: 任意 GraphQL 客户端(Apollo Client、urql、Relay 等)
|
||||
metadata:
|
||||
author: apollographql
|
||||
version: "1.0.0"
|
||||
allowed-tools: Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep
|
||||
---
|
||||
|
||||
# GraphQL 操作指南
|
||||
|
||||
本指南涵盖了作为客户端开发者编写 GraphQL 操作(查询、变更、订阅)的最佳实践。编写良好的操作应具备高效、类型安全且易于维护的特点。
|
||||
|
||||
## 操作基础
|
||||
|
||||
### 查询结构
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 变更结构
|
||||
|
||||
```graphql
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 订阅结构
|
||||
|
||||
```graphql
|
||||
subscription OnMessageReceived($channelId: ID!) {
|
||||
messageReceived(channelId: $channelId) {
|
||||
id
|
||||
content
|
||||
sender {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
### 操作命名
|
||||
|
||||
| 模式 | 示例 |
|
||||
| ------------ | --------------------------------------------- |
|
||||
| 查询 | `GetUser`、`ListPosts`、`SearchProducts` |
|
||||
| 变更 | `CreateUser`、`UpdatePost`、`DeleteComment` |
|
||||
| 订阅 | `OnMessageReceived`、`OnUserStatusChanged` |
|
||||
|
||||
### 变量语法
|
||||
|
||||
```graphql
|
||||
# 必填变量
|
||||
query GetUser($id: ID!) { ... }
|
||||
|
||||
# 带默认值的可选变量
|
||||
query ListPosts($first: Int = 20) { ... }
|
||||
|
||||
# 多个变量
|
||||
query SearchPosts($query: String!, $status: PostStatus, $first: Int = 10) { ... }
|
||||
```
|
||||
|
||||
### 片段语法
|
||||
|
||||
```graphql
|
||||
# 定义片段
|
||||
fragment UserBasicInfo on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
|
||||
# 使用片段
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
...UserBasicInfo
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 指令
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!, $includeEmail: Boolean!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email @include(if: $includeEmail)
|
||||
}
|
||||
}
|
||||
|
||||
query GetPosts($skipDrafts: Boolean!) {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
draft @skip(if: $skipDrafts)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关键原则
|
||||
|
||||
### 1. 仅请求所需字段
|
||||
|
||||
```graphql
|
||||
# 良好:指定字段
|
||||
query GetUserName($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# 避免:过度获取
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email
|
||||
bio
|
||||
posts {
|
||||
id
|
||||
title
|
||||
content
|
||||
comments {
|
||||
id
|
||||
}
|
||||
}
|
||||
followers {
|
||||
id
|
||||
name
|
||||
}
|
||||
# ... 大量未使用的字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 为所有操作命名
|
||||
|
||||
```graphql
|
||||
# 良好:命名操作
|
||||
query GetUserPosts($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 避免:匿名操作
|
||||
query {
|
||||
user(id: "123") {
|
||||
posts {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用变量,而非内联值
|
||||
|
||||
```graphql
|
||||
# 良好:变量
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# 避免:硬编码值
|
||||
query {
|
||||
user(id: "123") {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 将片段与组件共存
|
||||
|
||||
```tsx
|
||||
// UserAvatar.tsx
|
||||
export const USER_AVATAR_FRAGMENT = gql`
|
||||
fragment UserAvatar on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
`;
|
||||
|
||||
function UserAvatar({ user }) {
|
||||
return <img src={user.avatarUrl} alt={user.name} />;
|
||||
}
|
||||
```
|
||||
|
||||
## 参考文件
|
||||
|
||||
各主题的详细文档:
|
||||
|
||||
- [查询](references/queries.md) —— 查询模式与优化
|
||||
- [变更](references/mutations.md) —— 变更模式与错误处理
|
||||
- [片段](references/fragments.md) —— 片段组织与复用
|
||||
- [变量](references/variables.md) —— 变量用法与类型
|
||||
- [工具链](references/tooling.md) —— 代码生成与代码检查
|
||||
|
||||
## 基本规则
|
||||
|
||||
- 始终为你的操作命名(禁止匿名查询/变更)
|
||||
- 始终对动态值使用变量
|
||||
- 始终只请求你需要的字段
|
||||
- 始终为可缓存类型包含 `id` 字段
|
||||
- 切勿在操作中硬编码值
|
||||
- 切勿在文件间重复选择相同的字段
|
||||
- 优先使用片段来复用字段选择
|
||||
- 优先将片段与组件共存
|
||||
- 使用描述性的操作名称来体现用途
|
||||
- 对条件字段使用 `@include`/`@skip`
|
||||
@@ -0,0 +1,536 @@
|
||||
# Fragment Patterns(片段模式)
|
||||
|
||||
本参考文档涵盖了有效组织与使用 GraphQL 片段(Fragment)的模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [Fragment 基础](#fragment-basics)
|
||||
- [Fragment 同地存放(Colocation)](#fragment-colocation)
|
||||
- [Fragment 复用](#fragment-reuse)
|
||||
- [内联 Fragment(Inline Fragments)](#inline-fragments)
|
||||
- [类型条件(Type Conditions)](#type-conditions)
|
||||
- [Fragment 组合(Composition)](#fragment-composition)
|
||||
- [反模式(Anti-Patterns)](#anti-patterns)
|
||||
|
||||
## Fragment 基础
|
||||
|
||||
### 定义 Fragment
|
||||
|
||||
```graphql
|
||||
fragment UserBasicInfo on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 Fragment
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
...UserBasicInfo
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserBasicInfo on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
```
|
||||
|
||||
### Fragment 展开(Spread)
|
||||
|
||||
`...` 运算符用于展开 Fragment 的字段:
|
||||
|
||||
```graphql
|
||||
query GetPost($id: ID!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
author {
|
||||
...UserBasicInfo # 展开 id、name、avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fragment 同地存放(Colocation)
|
||||
|
||||
### 与组件同地存放
|
||||
|
||||
将 Fragment 放在使用它们的组件旁边:
|
||||
|
||||
```
|
||||
src/
|
||||
components/
|
||||
UserAvatar/
|
||||
UserAvatar.tsx
|
||||
UserAvatar.fragment.graphql
|
||||
UserCard/
|
||||
UserCard.tsx
|
||||
UserCard.fragment.graphql
|
||||
PostList/
|
||||
PostList.tsx
|
||||
PostList.query.graphql
|
||||
```
|
||||
|
||||
### 组件拥有自己的数据
|
||||
|
||||
```tsx
|
||||
// UserAvatar.tsx
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const USER_AVATAR_FRAGMENT = gql`
|
||||
fragment UserAvatar on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
`;
|
||||
|
||||
interface UserAvatarProps {
|
||||
user: UserAvatarFragment;
|
||||
}
|
||||
|
||||
export function UserAvatar({ user }: UserAvatarProps) {
|
||||
return <img src={user.avatarUrl} alt={user.name} className="avatar" />;
|
||||
}
|
||||
```
|
||||
|
||||
### 父组件组合 Fragment
|
||||
|
||||
```tsx
|
||||
// UserCard.tsx
|
||||
import { gql } from "@apollo/client";
|
||||
import { USER_AVATAR_FRAGMENT, UserAvatar } from "./UserAvatar";
|
||||
|
||||
export const USER_CARD_FRAGMENT = gql`
|
||||
fragment UserCard on User {
|
||||
id
|
||||
name
|
||||
bio
|
||||
...UserAvatar
|
||||
}
|
||||
${USER_AVATAR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export function UserCard({ user }: { user: UserCardFragment }) {
|
||||
return (
|
||||
<div className="user-card">
|
||||
<UserAvatar user={user} />
|
||||
<h3>{user.name}</h3>
|
||||
<p>{user.bio}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 查询使用组件 Fragment
|
||||
|
||||
```tsx
|
||||
// UserProfile.tsx
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import { USER_CARD_FRAGMENT, UserCard } from "./UserCard";
|
||||
|
||||
const GET_USER = gql`
|
||||
query GetUserProfile($id: ID!) {
|
||||
user(id: $id) {
|
||||
...UserCard
|
||||
email
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
${USER_CARD_FRAGMENT}
|
||||
`;
|
||||
|
||||
export function UserProfile({ userId }: { userId: string }) {
|
||||
const { data } = useQuery(GET_USER, { variables: { id: userId } });
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UserCard user={data.user} />
|
||||
<p>邮箱:{data.user.email}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Fragment 复用
|
||||
|
||||
### 共享 Fragment
|
||||
|
||||
用于跨多个组件使用的通用模式:
|
||||
|
||||
```graphql
|
||||
# fragments/common.graphql
|
||||
|
||||
fragment Timestamps on Node {
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
fragment PageInfoFields on PageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
```
|
||||
|
||||
### 领域特定 Fragment
|
||||
|
||||
```graphql
|
||||
# fragments/user.graphql
|
||||
|
||||
fragment UserSummary on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
|
||||
fragment UserProfile on User {
|
||||
...UserSummary
|
||||
bio
|
||||
location
|
||||
website
|
||||
socialLinks {
|
||||
platform
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserWithStats on User {
|
||||
...UserSummary
|
||||
followerCount
|
||||
followingCount
|
||||
postCount
|
||||
}
|
||||
```
|
||||
|
||||
### 使用共享 Fragment
|
||||
|
||||
```graphql
|
||||
query GetPost($id: ID!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
...Timestamps
|
||||
author {
|
||||
...UserSummary
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内联 Fragment(Inline Fragments)
|
||||
|
||||
### 匿名内联 Fragment
|
||||
|
||||
用于与指令(Directives)一起对字段进行分组:
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!, $includeDetails: Boolean!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
... @include(if: $includeDetails) {
|
||||
email
|
||||
bio
|
||||
website
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 接口上的内联 Fragment
|
||||
|
||||
```graphql
|
||||
query GetNodes($ids: [ID!]!) {
|
||||
nodes(ids: $ids) {
|
||||
id
|
||||
... on User {
|
||||
name
|
||||
email
|
||||
}
|
||||
... on Post {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 类型条件(Type Conditions)
|
||||
|
||||
### 联合类型上的 Fragment
|
||||
|
||||
```graphql
|
||||
query Search($query: String!) {
|
||||
search(query: $query) {
|
||||
... on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
... on Post {
|
||||
id
|
||||
title
|
||||
excerpt
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
body
|
||||
post {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 针对联合类型的命名 Fragment
|
||||
|
||||
```graphql
|
||||
query Search($query: String!) {
|
||||
search(query: $query) {
|
||||
...SearchResultUser
|
||||
...SearchResultPost
|
||||
...SearchResultComment
|
||||
}
|
||||
}
|
||||
|
||||
fragment SearchResultUser on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
|
||||
fragment SearchResultPost on Post {
|
||||
id
|
||||
title
|
||||
excerpt
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fragment SearchResultComment on Comment {
|
||||
id
|
||||
body
|
||||
post {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 处理 \_\_typename
|
||||
|
||||
```typescript
|
||||
function SearchResult({ result }) {
|
||||
switch (result.__typename) {
|
||||
case 'User':
|
||||
return <UserResult user={result} />;
|
||||
case 'Post':
|
||||
return <PostResult post={result} />;
|
||||
case 'Comment':
|
||||
return <CommentResult comment={result} />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fragment 组合(Composition)
|
||||
|
||||
### 构建 Fragment
|
||||
|
||||
```graphql
|
||||
# 基础 Fragment
|
||||
fragment PostCore on Post {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
|
||||
# 扩展 Fragment
|
||||
fragment PostPreview on Post {
|
||||
...PostCore
|
||||
excerpt
|
||||
featuredImage {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
# 完整 Fragment
|
||||
fragment PostFull on Post {
|
||||
...PostPreview
|
||||
content
|
||||
publishedAt
|
||||
author {
|
||||
...UserSummary
|
||||
}
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fragment 中嵌套 Fragment
|
||||
|
||||
```graphql
|
||||
fragment CommentWithAuthor on Comment {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
author {
|
||||
...UserSummary
|
||||
}
|
||||
}
|
||||
|
||||
fragment PostWithComments on Post {
|
||||
id
|
||||
title
|
||||
comments(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
...CommentWithAuthor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fragment 展开顺序
|
||||
|
||||
顺序无关紧要——字段会被合并:
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
...UserProfile
|
||||
...UserStats
|
||||
# 两个 Fragment 的字段都会被包含
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 反模式(Anti-Patterns)
|
||||
|
||||
### 避免巨型 Fragment
|
||||
|
||||
```graphql
|
||||
# 错误:字段过多,并非所有地方都需要
|
||||
fragment UserEverything on User {
|
||||
id
|
||||
name
|
||||
email
|
||||
bio
|
||||
avatarUrl
|
||||
coverImage
|
||||
website
|
||||
location
|
||||
socialLinks { ... }
|
||||
posts { ... }
|
||||
followers { ... }
|
||||
following { ... }
|
||||
# ……还有 50 多个字段
|
||||
}
|
||||
|
||||
# 正确:针对特定用途的精简 Fragment
|
||||
fragment UserAvatar on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
|
||||
fragment UserProfile on User {
|
||||
id
|
||||
name
|
||||
bio
|
||||
avatarUrl
|
||||
website
|
||||
location
|
||||
}
|
||||
```
|
||||
|
||||
### 避免未使用的 Fragment 字段
|
||||
|
||||
```graphql
|
||||
# 错误:组件只使用了 name 和 avatarUrl
|
||||
fragment UserInfo on User {
|
||||
id
|
||||
name
|
||||
email # 未使用
|
||||
avatarUrl
|
||||
bio # 未使用
|
||||
website # 未使用
|
||||
}
|
||||
|
||||
# 正确:只请求所需字段
|
||||
fragment UserInfo on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
```
|
||||
|
||||
### 避免深度嵌套的 Fragment
|
||||
|
||||
```graphql
|
||||
# 错误:难以理解正在获取什么内容
|
||||
fragment Level1 on User {
|
||||
...Level2
|
||||
}
|
||||
fragment Level2 on User {
|
||||
...Level3
|
||||
}
|
||||
fragment Level3 on User {
|
||||
...Level4
|
||||
}
|
||||
# ……继续嵌套
|
||||
|
||||
# 正确:保持嵌套层级较浅
|
||||
fragment UserWithPosts on User {
|
||||
id
|
||||
name
|
||||
posts {
|
||||
...PostPreview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免循环 Fragment 依赖
|
||||
|
||||
```graphql
|
||||
# 错误:循环引用(无法工作)
|
||||
fragment UserWithPosts on User {
|
||||
posts {
|
||||
...PostWithAuthor
|
||||
}
|
||||
}
|
||||
|
||||
fragment PostWithAuthor on Post {
|
||||
author {
|
||||
...UserWithPosts # 循环引用!
|
||||
}
|
||||
}
|
||||
|
||||
# 正确:打破循环
|
||||
fragment UserWithPosts on User {
|
||||
posts {
|
||||
...PostPreview
|
||||
}
|
||||
}
|
||||
|
||||
fragment PostWithAuthor on Post {
|
||||
author {
|
||||
...UserSummary # 不同的 Fragment,没有循环
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,435 @@
|
||||
# 变异模式
|
||||
|
||||
本参考文档涵盖了编写高效 GraphQL 变异的模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [变异结构](#mutation-structure)
|
||||
- [输入模式](#input-patterns)
|
||||
- [响应选择](#response-selection)
|
||||
- [错误处理](#error-handling)
|
||||
- [乐观更新](#optimistic-updates)
|
||||
- [变异命名](#mutation-naming)
|
||||
|
||||
## 变异结构
|
||||
|
||||
### 基础变异
|
||||
|
||||
```graphql
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
变量:
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"title": "My Post",
|
||||
"content": "Post content..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 带多个参数的变异
|
||||
|
||||
```graphql
|
||||
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
|
||||
updatePost(id: $id, input: $input) {
|
||||
id
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多个变异
|
||||
|
||||
在单个请求中执行多个变异(顺序执行):
|
||||
|
||||
```graphql
|
||||
mutation SetupUserProfile($userId: ID!, $profileInput: ProfileInput!, $settingsInput: SettingsInput!) {
|
||||
updateProfile(userId: $userId, input: $profileInput) {
|
||||
id
|
||||
bio
|
||||
}
|
||||
updateSettings(userId: $userId, input: $settingsInput) {
|
||||
id
|
||||
theme
|
||||
notifications
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 输入模式
|
||||
|
||||
### 单一输入对象
|
||||
|
||||
推荐模式——单一输入参数:
|
||||
|
||||
```graphql
|
||||
mutation CreateUser($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"password": "secret123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套输入对象
|
||||
|
||||
```graphql
|
||||
mutation CreateOrder($input: CreateOrderInput!) {
|
||||
createOrder(input: $input) {
|
||||
id
|
||||
total
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"items": [
|
||||
{ "productId": "prod_1", "quantity": 2 },
|
||||
{ "productId": "prod_2", "quantity": 1 }
|
||||
],
|
||||
"shippingAddress": {
|
||||
"street": "123 Main St",
|
||||
"city": "New York",
|
||||
"country": "US"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可选字段
|
||||
|
||||
```graphql
|
||||
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
|
||||
updateUser(id: $id, input: $input) {
|
||||
id
|
||||
name
|
||||
bio
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "user_123",
|
||||
"input": {
|
||||
"name": "New Name"
|
||||
// bio 未包含——不会被更改
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 响应选择
|
||||
|
||||
### 返回修改后的对象
|
||||
|
||||
始终返回带有更新字段的变异对象:
|
||||
|
||||
```graphql
|
||||
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
|
||||
updatePost(id: $id, input: $input) {
|
||||
id
|
||||
title
|
||||
content
|
||||
updatedAt # 服务端设置的字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 返回相关对象
|
||||
|
||||
如果变异影响了关联数据,请包含它:
|
||||
|
||||
```graphql
|
||||
mutation AddComment($input: AddCommentInput!) {
|
||||
addComment(input: $input) {
|
||||
id
|
||||
body
|
||||
post {
|
||||
id
|
||||
commentCount # 更新后的计数
|
||||
}
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 为缓存更新返回
|
||||
|
||||
选择更新缓存所需的字段:
|
||||
|
||||
```graphql
|
||||
mutation DeletePost($id: ID!) {
|
||||
deletePost(id: $id) {
|
||||
id # 需要从缓存中移除
|
||||
author {
|
||||
id
|
||||
postCount # 可能需要递减
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 为列表更新返回连接
|
||||
|
||||
```graphql
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
createdAt
|
||||
author {
|
||||
id
|
||||
posts(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 查询结果联合类型
|
||||
|
||||
当 schema 使用联合类型来表示错误时:
|
||||
|
||||
```graphql
|
||||
mutation CreateUser($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
... on CreateUserSuccess {
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
... on ValidationError {
|
||||
message
|
||||
field
|
||||
}
|
||||
... on EmailAlreadyExists {
|
||||
message
|
||||
existingUserId
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 处理所有情况
|
||||
|
||||
```typescript
|
||||
const result = await client.mutate({
|
||||
mutation: CREATE_USER,
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const { createUser } = result.data;
|
||||
|
||||
switch (createUser.__typename) {
|
||||
case "CreateUserSuccess":
|
||||
// 处理成功
|
||||
return createUser.user;
|
||||
case "ValidationError":
|
||||
// 处理验证错误
|
||||
throw new ValidationError(createUser.field, createUser.message);
|
||||
case "EmailAlreadyExists":
|
||||
// 处理特定的业务错误
|
||||
throw new EmailExistsError(createUser.existingUserId);
|
||||
}
|
||||
```
|
||||
|
||||
### GraphQL 错误
|
||||
|
||||
处理网络错误和 GraphQL 错误:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await client.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: { input },
|
||||
});
|
||||
return result.data.createPost;
|
||||
} catch (error) {
|
||||
if (error.graphQLErrors?.length) {
|
||||
// 处理 GraphQL 错误
|
||||
const gqlError = error.graphQLErrors[0];
|
||||
if (gqlError.extensions?.code === "UNAUTHENTICATED") {
|
||||
// 重定向到登录页
|
||||
}
|
||||
}
|
||||
if (error.networkError) {
|
||||
// 处理网络错误
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## 乐观更新
|
||||
|
||||
### 为乐观响应选择字段
|
||||
|
||||
包含所有将立即显示的字段:
|
||||
|
||||
```graphql
|
||||
mutation LikePost($postId: ID!) {
|
||||
likePost(postId: $postId) {
|
||||
id
|
||||
likeCount
|
||||
isLikedByViewer
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
client.mutate({
|
||||
mutation: LIKE_POST,
|
||||
variables: { postId: "post_123" },
|
||||
optimisticResponse: {
|
||||
likePost: {
|
||||
__typename: "Post",
|
||||
id: "post_123",
|
||||
likeCount: currentCount + 1,
|
||||
isLikedByViewer: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 包含创建的 ID
|
||||
|
||||
对于创建型变异,使用临时 ID:
|
||||
|
||||
```graphql
|
||||
mutation AddComment($input: AddCommentInput!) {
|
||||
addComment(input: $input) {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
author {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
client.mutate({
|
||||
mutation: ADD_COMMENT,
|
||||
variables: { input: { postId, body } },
|
||||
optimisticResponse: {
|
||||
addComment: {
|
||||
__typename: "Comment",
|
||||
id: `temp-${Date.now()}`, // 临时 ID
|
||||
body,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: {
|
||||
__typename: "User",
|
||||
id: currentUser.id,
|
||||
name: currentUser.name,
|
||||
avatarUrl: currentUser.avatarUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 变异命名
|
||||
|
||||
### 命名约定
|
||||
|
||||
| 操作 | 模式 | 示例 |
|
||||
| ------------ | ------------------- | ----------------------------------- |
|
||||
| 创建(Create) | `Create{Type}` | `CreateUser`, `CreatePost` |
|
||||
| 更新(Update) | `Update{Type}` | `UpdateUser`, `UpdatePost` |
|
||||
| 删除(Delete) | `Delete{Type}` | `DeleteUser`, `DeletePost` |
|
||||
| 操作(Action) | `{Verb}{Type}` | `PublishPost`, `ArchiveProject` |
|
||||
| 关系(Relationship) | `{Add/Remove}{Type}` | `AddTeamMember`, `RemoveTag` |
|
||||
|
||||
### 好的命名示例
|
||||
|
||||
```graphql
|
||||
mutation CreateUser($input: CreateUserInput!) { ... }
|
||||
mutation UpdateUserProfile($userId: ID!, $input: ProfileInput!) { ... }
|
||||
mutation DeletePost($id: ID!) { ... }
|
||||
mutation PublishArticle($id: ID!) { ... }
|
||||
mutation ArchiveProject($id: ID!) { ... }
|
||||
mutation AddItemToCart($input: AddItemInput!) { ... }
|
||||
mutation RemoveTeamMember($teamId: ID!, $userId: ID!) { ... }
|
||||
mutation FollowUser($userId: ID!) { ... }
|
||||
mutation MarkNotificationAsRead($id: ID!) { ... }
|
||||
```
|
||||
|
||||
### 操作名称与服务端一致
|
||||
|
||||
使客户端操作名称与服务端变异名称一致:
|
||||
|
||||
```graphql
|
||||
# 服务端 schema
|
||||
type Mutation {
|
||||
createPost(input: CreatePostInput!): Post!
|
||||
}
|
||||
|
||||
# 客户端操作——名称反映其行为
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 上下文特定的名称
|
||||
|
||||
当同一个变异用于不同场景时,添加上下文:
|
||||
|
||||
```graphql
|
||||
# 用于创建草稿
|
||||
mutation CreateDraftPost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
# 用于创建并立即发布
|
||||
mutation CreateAndPublishPost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
title
|
||||
status
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,504 @@
|
||||
# 查询模式
|
||||
|
||||
本参考文档涵盖了编写高效 GraphQL 查询的模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [查询结构](#查询结构)
|
||||
- [字段选择](#字段选择)
|
||||
- [别名](#别名)
|
||||
- [指令](#指令)
|
||||
- [查询命名](#查询命名)
|
||||
- [查询组织](#查询组织)
|
||||
- [性能优化](#性能优化)
|
||||
|
||||
## 查询结构
|
||||
|
||||
### 基本查询
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
组成部分:
|
||||
|
||||
- `query` — 操作类型
|
||||
- `GetUser` — 操作名称
|
||||
- `($id: ID!)` — 变量定义
|
||||
- `user(id: $id)` — 带参数的字段
|
||||
- `{ id name email }` — 选择集
|
||||
|
||||
### 多根字段查询
|
||||
|
||||
```graphql
|
||||
query GetDashboardData($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
notifications(first: 5) {
|
||||
id
|
||||
message
|
||||
}
|
||||
stats {
|
||||
totalPosts
|
||||
totalComments
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套查询
|
||||
|
||||
```graphql
|
||||
query GetUserWithPosts($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
name
|
||||
posts(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
comments(first: 3) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 字段选择
|
||||
|
||||
### 只请求需要的字段
|
||||
|
||||
```graphql
|
||||
# 用户卡片组件
|
||||
query GetUserCard($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
# 如果不展示,不要请求 email、bio 等字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 始终包含 ID 字段
|
||||
|
||||
为可能缓存或重新获取的类型包含 `id`:
|
||||
|
||||
```graphql
|
||||
query GetPost($id: ID!) {
|
||||
post(id: $id) {
|
||||
id # 始终包含,用于缓存
|
||||
title
|
||||
author {
|
||||
id # 包含,用于作者缓存条目
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 选择连接(Connection)
|
||||
|
||||
对于分页数据,按需请求:
|
||||
|
||||
```graphql
|
||||
query GetUserPosts($userId: ID!, $first: Int!, $after: String) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
posts(first: $first, after: $after) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
excerpt
|
||||
}
|
||||
cursor # 仅在实现无限滚动时需要
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
totalCount # 仅在展示总数时需要
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 别名
|
||||
|
||||
### 基本别名
|
||||
|
||||
在响应中重命名字段:
|
||||
|
||||
```graphql
|
||||
query GetUserNames($id: ID!) {
|
||||
user(id: $id) {
|
||||
userId: id
|
||||
displayName: name
|
||||
}
|
||||
}
|
||||
|
||||
# 响应:{ user: { userId: "123", displayName: "John" } }
|
||||
```
|
||||
|
||||
### 多次查询同一字段
|
||||
|
||||
```graphql
|
||||
query GetMultipleUsers {
|
||||
admin: user(id: "1") {
|
||||
id
|
||||
name
|
||||
}
|
||||
moderator: user(id: "2") {
|
||||
id
|
||||
name
|
||||
}
|
||||
currentUser: user(id: "3") {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用不同参数的别名
|
||||
|
||||
```graphql
|
||||
query GetPostsByStatus($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
publishedPosts: posts(status: PUBLISHED, first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
draftPosts: posts(status: DRAFT, first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 指令
|
||||
|
||||
### @include 指令
|
||||
|
||||
仅当条件为 true 时包含字段:
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!, $includeEmail: Boolean!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email @include(if: $includeEmail)
|
||||
}
|
||||
}
|
||||
|
||||
# 变量:{ id: "123", includeEmail: true }
|
||||
# 返回 email 字段
|
||||
|
||||
# 变量:{ id: "123", includeEmail: false }
|
||||
# 不返回 email 字段
|
||||
```
|
||||
|
||||
### @skip 指令
|
||||
|
||||
当条件为 true 时跳过字段:
|
||||
|
||||
```graphql
|
||||
query GetPost($id: ID!, $isPreview: Boolean!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
content @skip(if: $isPreview)
|
||||
excerpt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在片段上使用指令
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!, $expanded: Boolean!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
...UserDetails @include(if: $expanded)
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserDetails on User {
|
||||
bio
|
||||
website
|
||||
socialLinks {
|
||||
platform
|
||||
url
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组合使用指令
|
||||
|
||||
```graphql
|
||||
query GetPost($id: ID!, $showComments: Boolean!, $hideAuthor: Boolean!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
author @skip(if: $hideAuthor) {
|
||||
id
|
||||
name
|
||||
}
|
||||
comments(first: 10) @include(if: $showComments) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 查询命名
|
||||
|
||||
### 命名约定
|
||||
|
||||
| 用途 | 模式 | 示例 |
|
||||
| --------------- | --------------- | ----------------------------------- |
|
||||
| 获取单个条目 | `Get{Type}` | `GetUser`,`GetPost` |
|
||||
| 获取列表 | `List{Types}` | `ListUsers`,`ListPosts` |
|
||||
| 搜索 | `Search{Types}` | `SearchUsers`,`SearchProducts` |
|
||||
| 为特定 UI 获取 | `Get{Feature}Data` | `GetDashboardData`,`GetProfilePage` |
|
||||
|
||||
### 好的命名
|
||||
|
||||
```graphql
|
||||
query GetUserProfile($id: ID!) { ... }
|
||||
query ListRecentPosts($first: Int!) { ... }
|
||||
query SearchProducts($query: String!) { ... }
|
||||
query GetOrderDetails($orderId: ID!) { ... }
|
||||
query GetHomeFeed($userId: ID!) { ... }
|
||||
```
|
||||
|
||||
### 避免泛化命名
|
||||
|
||||
```graphql
|
||||
# 避免
|
||||
query Data { ... }
|
||||
query Query1 { ... }
|
||||
query FetchStuff { ... }
|
||||
|
||||
# 推荐
|
||||
query GetCurrentUser { ... }
|
||||
query ListActiveProjects { ... }
|
||||
query SearchCustomers($query: String!) { ... }
|
||||
```
|
||||
|
||||
## 查询组织
|
||||
|
||||
### 每个文件一个查询
|
||||
|
||||
```
|
||||
src/
|
||||
graphql/
|
||||
queries/
|
||||
GetUser.graphql
|
||||
ListPosts.graphql
|
||||
SearchProducts.graphql
|
||||
```
|
||||
|
||||
```graphql
|
||||
# GetUser.graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 与组件同目录放置
|
||||
|
||||
```
|
||||
src/
|
||||
components/
|
||||
UserProfile/
|
||||
UserProfile.tsx
|
||||
UserProfile.graphql
|
||||
UserProfile.test.tsx
|
||||
```
|
||||
|
||||
### 导入并使用
|
||||
|
||||
```typescript
|
||||
// 使用 graphql-tag
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const GET_USER = gql`
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 使用 .graphql 文件(需要加载器)
|
||||
import { GetUserDocument } from "./UserProfile.generated";
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 避免过度获取
|
||||
|
||||
只请求组件会用到的字段:
|
||||
|
||||
```graphql
|
||||
# 列表视图——最少字段
|
||||
query ListPostsForIndex {
|
||||
posts(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
excerpt
|
||||
author { name }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 详情视图——更多字段
|
||||
query GetPostDetail($id: ID!) {
|
||||
post(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
publishedAt
|
||||
author {
|
||||
id
|
||||
name
|
||||
bio
|
||||
avatarUrl
|
||||
}
|
||||
comments(first: 10) { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用分页
|
||||
|
||||
切勿获取无界列表:
|
||||
|
||||
```graphql
|
||||
# 避免
|
||||
query GetAllPosts {
|
||||
posts {
|
||||
# 可能返回数千条
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
# 推荐
|
||||
query GetPosts($first: Int = 20, $after: String) {
|
||||
posts(first: $first, after: $after) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 批量合并相关查询
|
||||
|
||||
在一次请求中获取相关数据:
|
||||
|
||||
```graphql
|
||||
# 替代多次查询
|
||||
query GetDashboard($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
recentPosts: posts(first: 5, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
notifications(first: 10, unreadOnly: true) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 对重复选择集使用片段
|
||||
|
||||
```graphql
|
||||
query GetPostsWithAuthors {
|
||||
posts(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
author {
|
||||
...AuthorInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
featuredPost {
|
||||
id
|
||||
title
|
||||
author {
|
||||
...AuthorInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment AuthorInfo on User {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,410 @@
|
||||
---
|
||||
name: tooling
|
||||
description: 用于处理 GraphQL 操作的工具参考,包括代码生成与代码检查
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# 工具
|
||||
|
||||
本参考文档涵盖了用于处理 GraphQL 操作的工具,包括代码生成和代码检查。
|
||||
|
||||
## 目录
|
||||
|
||||
- [GraphQL 代码生成器](#graphql-code-generator)
|
||||
- [ESLint GraphQL](#eslint-graphql)
|
||||
- [IDE 扩展](#ide-extensions)
|
||||
- [操作验证](#operation-validation)
|
||||
|
||||
## GraphQL 代码生成器
|
||||
|
||||
### 概述
|
||||
|
||||
GraphQL 代码生成器可从你的 schema 和操作中生成 TypeScript 类型,确保整个应用程序的类型安全。
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
|
||||
```
|
||||
|
||||
### 基本配置
|
||||
|
||||
创建 `codegen.ts`:
|
||||
|
||||
```typescript
|
||||
// codegen.ts
|
||||
import { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: "<URL_OF_YOUR_GRAPHQL_API>",
|
||||
// 假设所有源文件都在顶层的 `src/` 目录下——你可能需要根据你的文件结构调整此项
|
||||
documents: ["src/**/*.{ts,tsx}"],
|
||||
// 当没有文档时不以非零状态退出
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
// 使用最适合你应用程序结构的路径
|
||||
"./src/types/__generated__/graphql.ts": {
|
||||
plugins: ["typescript", "typescript-operations", "typed-document-node"],
|
||||
config: {
|
||||
avoidOptionals: {
|
||||
// 对可空字段使用 `null` 而非可选字段
|
||||
field: true,
|
||||
// 允许可空输入字段保持未指定状态
|
||||
inputValue: false,
|
||||
},
|
||||
// 对未配置的标量使用 `unknown` 而非 `any`
|
||||
defaultScalarType: "unknown",
|
||||
// Apollo Client 始终包含 `__typename` 字段
|
||||
nonOptionalTypename: true,
|
||||
// Apollo Client 不会为根类型添加 `__typename` 字段,因此
|
||||
// 不要为根操作类型生成 `__typename` 的类型。
|
||||
skipTypeNameForRoot: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### 运行生成
|
||||
|
||||
```bash
|
||||
# 一次性生成
|
||||
npx graphql-codegen
|
||||
|
||||
# 开发时的监听模式
|
||||
npx graphql-codegen --watch
|
||||
```
|
||||
|
||||
### Package 脚本
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
"codegen:watch": "graphql-codegen --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生成类型的使用
|
||||
|
||||
```tsx
|
||||
// 之前:手动类型定义
|
||||
const GET_USER = gql`
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 手动类型定义
|
||||
interface GetUserData {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const { data } = useQuery<GetUserData>(GET_USER, { variables: { id } });
|
||||
|
||||
// 之后:使用生成类型
|
||||
import { useGetUserQuery } from "./generated/graphql";
|
||||
|
||||
const { data } = useGetUserQuery({ variables: { id } });
|
||||
// data.user 现在是完整类型化的!
|
||||
```
|
||||
|
||||
### 近操作文件生成
|
||||
|
||||
在操作文件旁边生成类型:
|
||||
|
||||
```typescript
|
||||
const config: CodegenConfig = {
|
||||
schema: "http://localhost:4000/graphql",
|
||||
documents: ["src/**/*.graphql"],
|
||||
generates: {
|
||||
"src/": {
|
||||
preset: "near-operation-file",
|
||||
presetConfig: {
|
||||
extension: ".generated.ts",
|
||||
baseTypesPath: "generated/graphql.ts",
|
||||
},
|
||||
plugins: ["typescript-operations", "typescript-react-apollo"],
|
||||
},
|
||||
"src/generated/graphql.ts": {
|
||||
plugins: ["typescript"],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
结果如下:
|
||||
|
||||
```
|
||||
src/
|
||||
components/
|
||||
UserCard/
|
||||
UserCard.graphql
|
||||
UserCard.generated.ts # 该文件的生成类型
|
||||
```
|
||||
|
||||
### 片段类型
|
||||
|
||||
```tsx
|
||||
// UserAvatar.graphql
|
||||
// fragment UserAvatar on User {
|
||||
// id
|
||||
// name
|
||||
// avatarUrl
|
||||
// }
|
||||
|
||||
import { UserAvatarFragment } from "./UserAvatar.generated";
|
||||
|
||||
interface UserAvatarProps {
|
||||
user: UserAvatarFragment;
|
||||
}
|
||||
|
||||
export function UserAvatar({ user }: UserAvatarProps) {
|
||||
return <img src={user.avatarUrl} alt={user.name} />;
|
||||
}
|
||||
```
|
||||
|
||||
## ESLint GraphQL
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
npm install -D @graphql-eslint/eslint-plugin
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
```javascript
|
||||
// eslint.config.js(扁平配置)
|
||||
import graphqlPlugin from "@graphql-eslint/eslint-plugin";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["**/*.graphql"],
|
||||
languageOptions: {
|
||||
parser: graphqlPlugin.parser,
|
||||
},
|
||||
plugins: {
|
||||
"@graphql-eslint": graphqlPlugin,
|
||||
},
|
||||
rules: {
|
||||
"@graphql-eslint/known-type-names": "error",
|
||||
"@graphql-eslint/no-anonymous-operations": "error",
|
||||
"@graphql-eslint/no-duplicate-fields": "error",
|
||||
"@graphql-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
OperationDefinition: {
|
||||
style: "PascalCase",
|
||||
forbiddenPrefixes: ["Query", "Mutation", "Subscription"],
|
||||
},
|
||||
FragmentDefinition: {
|
||||
style: "PascalCase",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 推荐规则
|
||||
|
||||
```javascript
|
||||
rules: {
|
||||
// 语法与合法性
|
||||
'@graphql-eslint/known-type-names': 'error',
|
||||
'@graphql-eslint/known-fragment-names': 'error',
|
||||
'@graphql-eslint/no-undefined-variables': 'error',
|
||||
'@graphql-eslint/no-unused-variables': 'error',
|
||||
'@graphql-eslint/no-unused-fragments': 'error',
|
||||
'@graphql-eslint/unique-operation-name': 'error',
|
||||
'@graphql-eslint/unique-fragment-name': 'error',
|
||||
|
||||
// 最佳实践
|
||||
'@graphql-eslint/no-anonymous-operations': 'error',
|
||||
'@graphql-eslint/no-duplicate-fields': 'error',
|
||||
'@graphql-eslint/require-id-when-available': 'warn',
|
||||
|
||||
// 命名规范
|
||||
'@graphql-eslint/naming-convention': ['error', { ... }],
|
||||
}
|
||||
```
|
||||
|
||||
### Schema 感知规则
|
||||
|
||||
提供 schema 以实现高级验证:
|
||||
|
||||
```javascript
|
||||
{
|
||||
files: ['**/*.graphql'],
|
||||
languageOptions: {
|
||||
parser: graphqlPlugin.parser,
|
||||
parserOptions: {
|
||||
schema: './schema.graphql',
|
||||
// 或者
|
||||
schema: 'http://localhost:4000/graphql',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## IDE 扩展
|
||||
|
||||
### VS Code
|
||||
|
||||
**GraphQL:语言功能支持**(GraphQL 基金会)
|
||||
|
||||
- 语法高亮
|
||||
- Schema 类型的自动补全
|
||||
- 跳转到定义
|
||||
- 悬停文档
|
||||
- 针对 schema 的验证
|
||||
|
||||
配置(`.graphqlrc.yml`):
|
||||
|
||||
```yaml
|
||||
schema: "http://localhost:4000/graphql"
|
||||
documents: "src/**/*.{graphql,ts,tsx}"
|
||||
```
|
||||
|
||||
**Apollo GraphQL**(Apollo)
|
||||
|
||||
- Apollo 特有功能
|
||||
- Schema 注册表集成
|
||||
- 性能洞察
|
||||
|
||||
### JetBrains IDE
|
||||
|
||||
**GraphQL** 插件:
|
||||
|
||||
- 语法高亮
|
||||
- Schema 感知的补全
|
||||
- 验证
|
||||
- 导航到定义
|
||||
|
||||
配置(`.graphqlconfig`):
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaPath": "./schema.graphql",
|
||||
"includes": ["src/**/*.graphql"]
|
||||
}
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
常见的配置文件名:
|
||||
|
||||
- `.graphqlrc`(JSON)
|
||||
- `.graphqlrc.yml`(YAML)
|
||||
- `.graphqlrc.json`(JSON)
|
||||
- `graphql.config.js`(JavaScript)
|
||||
|
||||
```yaml
|
||||
# .graphqlrc.yml
|
||||
schema: "http://localhost:4000/graphql"
|
||||
documents: "src/**/*.graphql"
|
||||
extensions:
|
||||
codegen:
|
||||
generates:
|
||||
./src/generated/graphql.ts:
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
```
|
||||
|
||||
## 操作验证
|
||||
|
||||
### 针对 Schema 进行验证
|
||||
|
||||
```bash
|
||||
# 使用 graphql-inspector
|
||||
npx graphql-inspector validate ./src/**/*.graphql ./schema.graphql
|
||||
```
|
||||
|
||||
### CI 集成
|
||||
|
||||
```yaml
|
||||
# .github/workflows/graphql.yml
|
||||
name: GraphQL 验证
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 安装依赖
|
||||
run: npm ci
|
||||
|
||||
- name: 下载 schema
|
||||
run: npx graphql-inspector introspect http://localhost:4000/graphql --write schema.graphql
|
||||
|
||||
- name: 验证操作
|
||||
run: npx graphql-inspector validate './src/**/*.graphql' schema.graphql
|
||||
|
||||
- name: 检查破坏性变更
|
||||
run: npx graphql-inspector diff schema.graphql http://localhost:4000/graphql
|
||||
```
|
||||
|
||||
### Pre-commit 钩子
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"lint-staged": {
|
||||
"*.graphql": ["eslint --fix", "graphql-inspector validate ./schema.graphql"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 操作复杂度检查
|
||||
|
||||
```bash
|
||||
# 检查查询复杂度
|
||||
npx graphql-query-complexity-checker \
|
||||
--schema ./schema.graphql \
|
||||
--query ./src/queries/GetUser.graphql \
|
||||
--max-complexity 100
|
||||
```
|
||||
|
||||
### 持久化查询提取
|
||||
|
||||
生成用于生产环境的持久化查询:
|
||||
|
||||
```typescript
|
||||
// codegen.ts
|
||||
const config: CodegenConfig = {
|
||||
generates: {
|
||||
"./persisted-queries.json": {
|
||||
plugins: ["graphql-codegen-persisted-query-ids"],
|
||||
config: {
|
||||
output: "client",
|
||||
algorithm: "sha256",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"abc123...": "query GetUser($id: ID!) { user(id: $id) { id name } }",
|
||||
"def456...": "mutation CreatePost($input: CreatePostInput!) { ... }"
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
# GraphQL 变量模式
|
||||
|
||||
本参考文档涵盖在 GraphQL 操作中使用变量的模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [变量基础](#variable-basics)
|
||||
- [变量类型](#variable-types)
|
||||
- [默认值](#default-values)
|
||||
- [复杂输入](#complex-inputs)
|
||||
- [最佳实践](#best-practices)
|
||||
|
||||
## 变量基础
|
||||
|
||||
### 声明变量
|
||||
|
||||
变量在操作定义中声明:
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用变量
|
||||
|
||||
变量以 `$` 前缀引用:
|
||||
|
||||
```graphql
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
# 此处使用 $id
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 传递变量
|
||||
|
||||
变量以独立的 JSON 对象形式传递:
|
||||
|
||||
```typescript
|
||||
const { data } = await client.query({
|
||||
query: GET_USER,
|
||||
variables: {
|
||||
id: "user_123",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 多个变量
|
||||
|
||||
```graphql
|
||||
query SearchPosts($query: String!, $status: PostStatus, $first: Int!, $after: String) {
|
||||
searchPosts(query: $query, status: $status, first: $first, after: $after) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "graphql",
|
||||
"status": "PUBLISHED",
|
||||
"first": 10,
|
||||
"after": "cursor_abc"
|
||||
}
|
||||
```
|
||||
|
||||
## 变量类型
|
||||
|
||||
### 标量类型
|
||||
|
||||
```graphql
|
||||
query Example(
|
||||
$id: ID!
|
||||
$name: String!
|
||||
$count: Int!
|
||||
$price: Float!
|
||||
$active: Boolean!
|
||||
) {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义标量类型
|
||||
|
||||
```graphql
|
||||
query Example(
|
||||
$date: DateTime!
|
||||
$email: Email!
|
||||
$url: URL!
|
||||
) {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 枚举类型
|
||||
|
||||
```graphql
|
||||
query GetPosts($status: PostStatus!) {
|
||||
posts(status: $status) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "PUBLISHED"
|
||||
}
|
||||
```
|
||||
|
||||
### 列表类型
|
||||
|
||||
```graphql
|
||||
query GetUsers($ids: [ID!]!) {
|
||||
users(ids: $ids) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": ["user_1", "user_2", "user_3"]
|
||||
}
|
||||
```
|
||||
|
||||
### 输入对象类型
|
||||
|
||||
```graphql
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"title": "My Post",
|
||||
"content": "Post content...",
|
||||
"tags": ["graphql", "api"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 必需 vs 可选
|
||||
|
||||
```graphql
|
||||
query Example(
|
||||
$required: String! # 必须提供,不能为 null
|
||||
$optional: String # 可以省略或为 null
|
||||
$requiredList: [String!]! # 列表必填,元素必填
|
||||
$optionalList: [String] # 列表可选,元素可选
|
||||
) {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
## 默认值
|
||||
|
||||
### 简单默认值
|
||||
|
||||
```graphql
|
||||
query GetPosts($first: Int = 10, $status: PostStatus = PUBLISHED) {
|
||||
posts(first: $first, status: $status) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果未提供,则使用默认值:
|
||||
|
||||
```json
|
||||
{}
|
||||
// 等效于:{ "first": 10, "status": "PUBLISHED" }
|
||||
```
|
||||
|
||||
覆盖默认值:
|
||||
|
||||
```json
|
||||
{
|
||||
"first": 20
|
||||
}
|
||||
// 使用 first: 20,status: PUBLISHED(默认值)
|
||||
```
|
||||
|
||||
### 可选变量的默认值
|
||||
|
||||
```graphql
|
||||
# 变量为可选(无 !),但有默认值
|
||||
query GetPosts($first: Int = 10) {
|
||||
posts(first: $first) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂类型的默认值
|
||||
|
||||
```graphql
|
||||
query GetPosts($orderBy: PostOrderInput = { field: CREATED_AT, direction: DESC }) {
|
||||
posts(orderBy: $orderBy) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 何时使用默认值
|
||||
|
||||
在以下场景使用默认值:
|
||||
|
||||
- 分页限制(`first: Int = 20`)
|
||||
- 排序顺序(`direction: SortDirection = DESC`)
|
||||
- 常见筛选值(`status: Status = ACTIVE`)
|
||||
- 功能开关(`includeArchived: Boolean = false`)
|
||||
|
||||
## 复杂输入
|
||||
|
||||
### 嵌套输入对象
|
||||
|
||||
```graphql
|
||||
mutation CreateOrder($input: CreateOrderInput!) {
|
||||
createOrder(input: $input) {
|
||||
id
|
||||
total
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"customer": {
|
||||
"email": "customer@example.com",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"items": [
|
||||
{ "productId": "prod_1", "quantity": 2 },
|
||||
{ "productId": "prod_2", "quantity": 1 }
|
||||
],
|
||||
"shippingAddress": {
|
||||
"street": "123 Main St",
|
||||
"city": "New York",
|
||||
"state": "NY",
|
||||
"zipCode": "10001",
|
||||
"country": "US"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 输入对象列表
|
||||
|
||||
```graphql
|
||||
mutation BulkCreateUsers($inputs: [CreateUserInput!]!) {
|
||||
bulkCreateUsers(inputs: $inputs) {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"inputs": [
|
||||
{ "email": "user1@example.com", "name": "User 1" },
|
||||
{ "email": "user2@example.com", "name": "User 2" },
|
||||
{ "email": "user3@example.com", "name": "User 3" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 筛选输入
|
||||
|
||||
```graphql
|
||||
query SearchProducts($filter: ProductFilter!) {
|
||||
products(filter: $filter) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"filter": {
|
||||
"category": "ELECTRONICS",
|
||||
"priceRange": {
|
||||
"min": 100,
|
||||
"max": 500
|
||||
},
|
||||
"inStock": true,
|
||||
"tags": ["featured", "sale"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 始终对动态值使用变量
|
||||
|
||||
```graphql
|
||||
# 好:使用变量
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
# 差:硬编码值
|
||||
query GetUser {
|
||||
user(id: "123") {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 变量名与参数名保持一致
|
||||
|
||||
```graphql
|
||||
# 好:关系清晰
|
||||
query GetUser($userId: ID!) {
|
||||
user(id: $userId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
# 也不错:同名
|
||||
query GetUser($id: ID!) {
|
||||
user(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
# 差:命名混乱
|
||||
query GetUser($x: ID!) {
|
||||
user(id: $x) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用描述性变量名
|
||||
|
||||
```graphql
|
||||
# 好
|
||||
query SearchPosts(
|
||||
$searchQuery: String!
|
||||
$authorId: ID
|
||||
$publishedAfter: DateTime
|
||||
$maxResults: Int = 20
|
||||
) {
|
||||
searchPosts(
|
||||
query: $searchQuery
|
||||
author: $authorId
|
||||
after: $publishedAfter
|
||||
first: $maxResults
|
||||
) {
|
||||
# ...
|
||||
}
|
||||
}
|
||||
|
||||
# 差
|
||||
query SearchPosts($q: String!, $a: ID, $d: DateTime, $n: Int) {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 对相关变量进行分组
|
||||
|
||||
```typescript
|
||||
// 好:variables 对象反映输入结构
|
||||
const variables = {
|
||||
input: {
|
||||
title: formData.title,
|
||||
content: formData.content,
|
||||
tags: formData.tags,
|
||||
},
|
||||
};
|
||||
|
||||
// 不够清晰:扁平变量
|
||||
const variables = {
|
||||
title: formData.title,
|
||||
content: formData.content,
|
||||
tags: formData.tags,
|
||||
};
|
||||
```
|
||||
|
||||
### 在客户端校验变量
|
||||
|
||||
```typescript
|
||||
function createPost(input: CreatePostInput) {
|
||||
// 发送前校验
|
||||
if (!input.title?.trim()) {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
if (input.title.length > 200) {
|
||||
throw new Error("Title too long");
|
||||
}
|
||||
|
||||
return client.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: { input },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 TypeScript 为变量提供类型
|
||||
|
||||
```typescript
|
||||
// 从 schema 生成的类型
|
||||
interface GetUserQueryVariables {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 配合 Apollo Client 使用
|
||||
const { data } = useQuery<GetUserQuery, GetUserQueryVariables>(GET_USER, {
|
||||
variables: { id: userId }, // 类型已检查
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user