mcpField
V
vtex-io-graphql-api

vtex-io-graphql-api

github.com/vtex开发471.2k 次安装2 周前更新
代码仓库

关于 vtex-io-graphql-api

用于构建 VTEX IO GraphQL API 的开发技能,涵盖模式定义和 TypeScript 解析器。还介绍身份验证、缓存、解析器注册及通过客户端访问 VTEX 数据。

主要功能

  1. 01定义 GraphQL 模式
  2. 02编写 TypeScript 解析器
  3. 03配置身份验证和缓存指令
  4. 04将解析器绑定到模式字段
  5. 05使用 VTEX 客户端访问数据

适用场景

  1. 01向店铺前端组件提供数据
  2. 02为其他 VTEX IO 应用提供 API
  3. 03聚合 VTEX Commerce API 数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vtex/skills --skill vtex-io-graphql-api

安装后在 Claude Code 中输入/vtex-io-graphql-api确认已加载。

详细介绍

GraphQL 架构与解析器

适用场景

当你的 VTEX IO 应用需要公开 GraphQL API 时,请使用此技能——无论是供前端 React 组件查询、供其他 VTEX IO 应用使用,还是用于在 VTEX Commerce API 之上实现自定义数据聚合层。

  • 在 /graphql 目录中的 .graphql 文件里定义架构
  • 在 /node/resolvers/ 中使用 TypeScript 编写解析器函数
  • 配置 @cacheControl 和 @auth 指令
  • 将解析器接入 Service 类

以下情况请勿使用此技能:

  • 后端服务结构和客户端系统(请改用 vtex-io-service-apps)
  • Manifest 和 builder 配置(请改用 vtex-io-app-structure)
  • MasterData 集成详情(请改用 vtex-io-masterdata)

决策规则

  • graphql builder 会处理 /graphql 中的 .graphql 文件,并将其合并为单个架构。
  • 为便于维护,请将定义拆分到多个文件中:用 schema.graphql 定义根类型,用 directives.graphql 声明指令,用 types/*.graphql 定义自定义类型。
  • 在所有公开的 Query 字段上使用 @cacheControl(scope: PUBLIC, maxAge: SHORT|MEDIUM|LONG)。PUBLIC = 共享 CDN 缓存,PRIVATE = 按用户隔离的缓存。
  • 在所有 Mutation 以及返回敏感数据或特定用户数据的 Query 上使用 @auth。
  • 切勿在 Mutation 上使用 @cacheControl。
  • Service 入口中的解析器函数键必须与 schema.graphql 中的字段名称完全一致。
  • 始终在解析器中使用 ctx.clients 访问数据,切勿直接发起 HTTP 调用。

建议的目录结构:

graphql/
├── schema.graphql        # Query and Mutation root type definitions
├── directives.graphql    # Custom directive declarations (@cacheControl, @auth)
└── types/
    ├── Review.graphql    # Custom type definitions
    └── Product.graphql   # One file per type for organization

内置指令:

  • @cacheControl:scope(PUBLIC/PRIVATE)、maxAge(SHORT 30s、MEDIUM 5min、LONG 1h)
  • @auth:强制使用有效的 VTEX 身份验证令牌。没有该指令时,未经身份验证的用户也能调用该端点。
  • @smartcache:在 VTEX 基础设施中自动缓存查询结果。

硬性约束

约束:声明 graphql Builder

任何使用 .graphql 架构文件的应用都必须在 manifest.json 中声明 graphql builder。graphql builder 会解析架构,并将其注册到 VTEX IO 运行时中。

重要原因

如果未声明 graphql builder,/graphql 目录将被完全忽略。架构文件不会得到处理,解析器也不会注册,GraphQL 查询会返回“schema not found”错误。应用虽然可以成功 link,但 GraphQL 会在没有报错的情况下无法运行。

检测

如果你在 /graphql 目录中看到 .graphql 文件,但 manifest 的 builders 中没有 "graphql": "1.x",请立即停止并添加 builder 声明。

正确

{
  "builders": {
    "node": "7.x",
    "graphql": "1.x"
  }
}

错误

{
  "builders": {
    "node": "7.x"
  }
}

缺少 "graphql": "1.x"——/graphql 目录及其中的架构文件会被忽略。由于没有注册架构,GraphQL 查询会返回错误。应用可以成功 link,从而掩盖问题。


约束:在公开查询上使用 @cacheControl

所有面向公众的 Query 字段(即获取非特定用户数据的字段)都必须包含 @cacheControl 指令,并设置合适的 scope 和 maxAge。Mutation 不得使用 @cacheControl。

重要原因

没有 @cacheControl 时,每次请求都会调用解析器——没有 CDN 缓存、边缘缓存或共享缓存。这会增加 VTEX 基础设施的不必要负载、降低响应速度,并可能触发速率限制。对于公开的商品数据,缓存对性能至关重要。

检测

如果 Query 字段返回公开数据(非特定用户数据),但没有 @cacheControl,请提醒开发者添加该指令。如果 Mutation 上有 @cacheControl,请立即停止并将其移除。

正确

type Query {
  reviews(productId: String!, limit: Int): [Review]
    @cacheControl(scope: PUBLIC, maxAge: SHORT)

  productMetadata(slug: String!): ProductMetadata
    @cacheControl(scope: PUBLIC, maxAge: MEDIUM)

  myReviews: [Review]
    @cacheControl(scope: PRIVATE, maxAge: SHORT)
    @auth
}

type Mutation {
  createReview(review: ReviewInput!): Review @auth
}

错误

type Query {
  reviews(productId: String!, limit: Int): [Review]

  myReviews: [Review]
}

type Mutation {
  createReview(review: ReviewInput!): Review
    @cacheControl(scope: PUBLIC, maxAge: LONG)
}

查询没有缓存控制(每次请求都会调用解析器)、特定用户数据缺少 @auth,而 Mutation 上使用了 @cacheControl(毫无意义)。


约束:解析器名称必须与架构字段匹配

Service 入口点中的解析器函数键必须与 schema.graphql 中定义的字段名称完全一致。解析器对象结构必须与 GraphQL 类型层级对应。

为什么这很重要

GraphQL 运行时会按名称将传入的查询映射到解析器函数。如果解析器键与架构字段名称不匹配,该字段将解析为 null,且不会报错——这是一种极难调试的静默失败。

检测

如果架构字段没有匹配的解析器键(反之亦然),请停止。逐一核对每个 Query 和 Mutation 字段与 node/index.ts 中的解析器注册情况。

正确

type Query {
  reviews(productId: String!): [Review]
  reviewById(id: ID!): Review
}
// node/index.ts — resolver keys match schema field names exactly
export default new Service({
  graphql: {
    resolvers: {
      Query: {
        reviews: reviewsResolver,
        reviewById: reviewByIdResolver,
      },
    },
  },
})

错误

// node/index.ts — resolver key "getReviews" does not match schema field "reviews"
export default new Service({
  graphql: {
    resolvers: {
      Query: {
        getReviews: reviewsResolver,    // Wrong! Schema says "reviews", not "getReviews"
        getReviewById: reviewByIdResolver, // Wrong! Schema says "reviewById"
      },
    },
  },
})

这两个字段都会静默解析为 null。日志中不会出现错误。

推荐模式

将 GraphQL builder 添加到清单中:

{
  "builders": {
    "node": "7.x",
    "graphql": "1.x"
  }
}

定义架构:

type Query {
  reviews(productId: String!, limit: Int, offset: Int): ReviewsResponse
    @cacheControl(scope: PUBLIC, maxAge: SHORT)

  review(id: ID!): Review
    @cacheControl(scope: PUBLIC, maxAge: SHORT)
}

type Mutation {
  createReview(input: ReviewInput!): Review @auth
  updateReview(id: ID!, input: ReviewInput!): Review @auth
  deleteReview(id: ID!): Boolean @auth
}

定义自定义类型:

type Review {
  id: ID!
  productId: String!
  author: String!
  rating: Int!
  title: String!
  text: String!
  createdAt: String!
  approved: Boolean!
}

type ReviewsResponse {
  data: [Review!]!
  total: Int!
  hasMore: Boolean!
}

input ReviewInput {
  productId: String!
  rating: Int!
  title: String!
  text: String!
}

声明指令:

directive @cacheControl(
  scope: CacheControlScope
  maxAge: CacheControlMaxAge
) on FIELD_DEFINITION

enum CacheControlScope {
  PUBLIC
  PRIVATE
}

enum CacheControlMaxAge {
  SHORT
  MEDIUM
  LONG
}

directive @auth on FIELD_DEFINITION
directive @smartcache on FIELD_DEFINITION

实现解析器:

// node/resolvers/reviews.ts
import type { ServiceContext } from '@vtex/api'
import type { Clients } from '../clients'

type Context = ServiceContext<Clients>

export const queries = {
  reviews: async (
    _root: unknown,
    args: { productId: string; limit?: number; offset?: number },
    ctx: Context
  ) => {
    const { productId, limit = 10, offset = 0 } = args
    const reviews = await ctx.clients.masterdata.searchDocuments<Review>({
      dataEntity: 'reviews',
      fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
      where: `productId=${productId} AND approved=true`,
      pagination: { page: Math.floor(offset / limit) + 1, pageSize: limit },
      schema: 'review-schema-v1',
    })

    return {
      data: reviews,
      total: reviews.length,
      hasMore: reviews.length === limit,
    }
  },

  review: async (
    _root: unknown,
    args: { id: string },
    ctx: Context
  ) => {
    return ctx.clients.masterdata.getDocument<Review>({
      dataEntity: 'reviews',
      id: args.id,
      fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
    })
  },
}

export const mutations = {
  createReview: async (
    _root: unknown,
    args: { input: ReviewInput },
    ctx: Context
  ) => {
    const { input } = args

    const documentResponse = await ctx.clients.masterdata.createDocument({
      dataEntity: 'reviews',
      fields: {
        ...input,
        author: ctx.vtex.storeUserEmail ?? 'anonymous',
        approved: false,
        createdAt: new Date().toISOString(),
      },
      schema: 'review-schema-v1',
    })

    return ctx.clients.masterdata.getDocument<Review>({
      dataEntity: 'reviews',
      id: documentResponse.DocumentId,
      fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
    })
  },

  deleteReview: async (
    _root: unknown,
    args: { id: string },
    ctx: Context
  ) => {
    await ctx.clients.masterdata.deleteDocument({
      dataEntity: 'reviews',
      id: args.id,
    })

    return true
  },
}

将解析器接入 Service:

// node/index.ts
import type { ParamsContext, RecorderState } from '@vtex/api'
import { Service } from '@vtex/api'

import { Clients } from './clients'
import { queries, mutations } from './resolvers/reviews'

export default new Service<Clients, RecorderState, ParamsContext>({
  clients: {
    implementation: Clients,
    options: {
      default: {
        retries: 2,
        timeout: 5000,
      },
    },
  },
  graphql: {
    resolvers: {
      Query: queries,
      Mutation: mutations,
    },
  },
})

链接后测试 GraphQL API:

query GetReviews {
  reviews(productId: "12345", limit: 5) {
    data {
      id
      author
      rating
      title
      text
      createdAt
    }
    total
    hasMore
  }
}

mutation CreateReview {
  createReview(input: {
    productId: "12345"
    rating: 5
    title: "Excellent product"
    text: "Really happy with this purchase."
  }) {
    id
    author
    createdAt
  }
}

常见失败模式

  • 定义的解析器没有对应的架构字段:GraphQL 运行时只会公开架构中定义的字段。没有对应字段的解析器会被静默忽略。反过来,没有解析器的架构字段会返回 null。务必先定义架构,再实现名称完全一致的对应解析器。
  • 在解析器中直接查询外部 API:使用 fetch() 或 axios 会绕过 @vtex/api 客户端系统,导致无法使用缓存、重试、指标和身份验证。解析器中务必使用 ctx.clients。
  • Mutation 端点缺少 @auth:如果没有 @auth,任何匿名用户都可以调用该 Mutation——这会造成严重的安全漏洞。务必为 Mutation 以及返回敏感数据的 Query 添加 @auth。
  • 公开 Query 缺少 @cacheControl:每个请求都会在不使用缓存的情况下调用解析器,造成不必要的负载并降低响应速度。为所有公开的 Query 字段添加适当的缓存指令。

审查清单

  • 是否在 manifest.json 中声明了 graphql builder?
  • 所有公开的 Query 字段是否都带有 @cacheControl,且 scope 和 maxAge 设置是否合适?
  • 所有 Mutation 和敏感 Query 是否都带有 @auth?
  • 解析器函数键是否与架构字段名称完全一致?
  • 解析器是否使用 ctx.clients 访问数据(没有直接调用 HTTP)?
  • directives.graphql 中是否存在指令声明?
  • 是否已在 Service 入口点的 graphql.resolvers 下接入解析器?

相关技能

参考资料

评论

支持 Markdown