Loading...
Loading...
Compare original and translation side by side
src/graphql/vtex/src/graphql/thirdParty/src/fragments/faststore-state-managementfaststore-themingfaststore-overridessrc/graphql/vtex/src/graphql/thirdParty/src/fragments/faststore-state-managementfaststore-themingfaststore-overridessrc/graphql/vtex/src/graphql/thirdParty/src/fragments/ServerProduct.tssrc/fragments/ClientProduct.tsNEXT_PUBLIC_pages/api/src/graphql/vtex/src/graphql/thirdParty/src/fragments/ServerProduct.tssrc/fragments/ClientProduct.tsNEXT_PUBLIC_pages/api//api/catalog//api/catalog_system/fetch('https://{account}.vtexcommercestable.com.br/api/catalog')fetch('https://{account}.myvtex.com/api/catalog')src/graphql/axiosfetchsrc/components/src/pages/// src/graphql/vtex/resolvers/product.ts
// Server-side resolver — REST calls to VTEX APIs are correct here
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
customAttribute: async (root, _args, context) => {
// Server-side: safe to call VTEX REST APIs in resolvers
const response = await context.clients.commerce.catalog.getProduct(
root.productID
)
return response.customAttribute
},
},
}
export default productResolver// src/components/ProductCustomData.tsx
// WRONG: Direct REST call to VTEX Catalog API from a client component
import React, { useEffect, useState } from 'react'
interface ProductCustomDataProps {
productId: string
}
export default function ProductCustomData({ productId }: ProductCustomDataProps) {
const [data, setData] = useState(null)
useEffect(() => {
// WRONG: Direct REST call from the browser
// This exposes the VTEX domain, bypasses caching, and creates CORS issues.
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`)
.then((res) => res.json())
.then(setData)
}, [productId])
return <div>{data?.Name}</div>
}/api/catalog//api/catalog_system/fetch('https://{account}.vtexcommercestable.com.br/api/catalog')fetch('https://{account}.myvtex.com/api/catalog')src/graphql/src/components/src/pages/axiosfetch// src/graphql/vtex/resolvers/product.ts
// 服务端解析器——在此处调用VTEX REST API是合规的
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
customAttribute: async (root, _args, context) => {
// 服务端:在解析器中调用VTEX REST API是安全的
const response = await context.clients.commerce.catalog.getProduct(
root.productID
)
return response.customAttribute
},
},
}
export default productResolver// src/components/ProductCustomData.tsx
// 错误:从客户端组件直接调用VTEX目录API
import React, { useEffect, useState } from 'react'
interface ProductCustomDataProps {
productId: string
}
export default function ProductCustomData({ productId }: ProductCustomDataProps) {
const [data, setData] = useState(null)
useEffect(() => {
// 错误:从浏览器直接调用REST接口
// 这会暴露VTEX域名、绕过缓存并引发CORS问题。
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`)
.then((res) => res.json())
.then(setData)
}, [productId])
return <div>{data?.Name}</div>
}VTEX_APP_KEYVTEX_APP_TOKENNEXT_PUBLIC_VTEX_APP_KEYVTEX_APP_TOKENX-VTEX-API-AppKeyX-VTEX-API-AppTokensrc/components/src/pages/NEXT_PUBLIC_VTEX_APP_KEYNEXT_PUBLIC_VTEX_APP_TOKEN.envNEXT_PUBLIC_// src/graphql/vtex/resolvers/installments.ts
// API keys are used ONLY in server-side resolvers, accessed via context
import type { Resolver } from '@faststore/api'
const installmentResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
// context.clients handles authentication automatically
// No API keys are hardcoded or exposed
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments = product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default installmentResolver// src/components/ProductInstallments.tsx
// CRITICAL SECURITY ISSUE: API keys exposed in client-side code
import React, { useEffect, useState } from 'react'
export default function ProductInstallments({ productId }: { productId: string }) {
const [installments, setInstallments] = useState([])
useEffect(() => {
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`, {
headers: {
// CRITICAL: These keys are now visible to EVERY visitor of your site.
// Anyone can extract them from the browser's network tab.
'X-VTEX-API-AppKey': 'vtexappkey-mystore-ABCDEF',
'X-VTEX-API-AppToken': 'very-secret-token-12345',
},
})
.then((res) => res.json())
.then((data) => setInstallments(data.Installments))
}, [productId])
return <div>{installments.length} installments available</div>
}NEXT_PUBLIC_VTEX_APP_KEYVTEX_APP_TOKENsrc/components/src/pages/VTEX_APP_KEYVTEX_APP_TOKENX-VTEX-API-AppKeyX-VTEX-API-AppToken.envNEXT_PUBLIC_VTEX_APP_KEYNEXT_PUBLIC_VTEX_APP_TOKENNEXT_PUBLIC_// src/graphql/vtex/resolvers/installments.ts
// API密钥仅在服务端解析器中使用,通过context访问
import type { Resolver } from '@faststore/api'
const installmentResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
// context.clients会自动处理身份验证
// 无需硬编码或暴露API密钥
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments = product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default installmentResolver// src/components/ProductInstallments.tsx
// 严重安全问题:API密钥暴露在客户端代码中
import React, { useEffect, useState } from 'react'
export default function ProductInstallments({ productId }: { productId: string }) {
const [installments, setInstallments] = useState([])
useEffect(() => {
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`, {
headers: {
// 严重问题:这些密钥现在对所有网站访客可见。
// 任何人都可以从浏览器的网络面板中提取它们。
'X-VTEX-API-AppKey': 'vtexappkey-mystore-ABCDEF',
'X-VTEX-API-AppToken': 'very-secret-token-12345',
},
})
.then((res) => res.json())
.then((data) => setInstallments(data.Installments))
}, [productId])
return <div>{installments.length} installments available</div>
}src/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/.graphqlsrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/undefinedsrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/.graphqlsrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/undefined
```typescript
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from './product'
const resolvers = {
...StoreProductResolver,
}
export default resolvers// WRONG: Resolver placed in src/api/ instead of src/graphql/vtex/resolvers/
// src/api/resolvers/product.ts
// This file will NOT be discovered by FastStore's build system.
// The GraphQL schema will NOT include the extended fields.
// Components querying these fields will get runtime errors.
const productResolver = {
StoreProduct: {
availableInstallments: async (root: any) => {
return []
},
},
}
export default productResolver
```typescript
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from './product'
const resolvers = {
...StoreProductResolver,
}
export default resolvers// 错误:解析器放在src/api/而非src/graphql/vtex/resolvers/目录下
// src/api/resolvers/product.ts
// 该文件不会被FastStore的构建系统发现。
// GraphQL模式将不会包含扩展的字段。
// 查询这些字段的组件会出现运行时错误。
const productResolver = {
StoreProduct: {
availableInstallments: async (root: any) => {
return []
},
},
}
export default productResolversrc/
├── graphql/
│ ├── vtex/
│ │ ├── typeDefs/
│ │ │ └── product.graphql
│ │ └── resolvers/
│ │ ├── product.ts
│ │ └── index.ts
│ └── thirdParty/
│ ├── typeDefs/
│ │ └── extra.graphql
│ └── resolvers/
│ ├── reviews.ts
│ └── index.ts
└── fragments/
├── ServerProduct.ts ← server-side fragment (SSR)
└── ClientProduct.ts ← client-side fragment (post-render)StoreProductundefinedsrc/
├── graphql/
│ ├── vtex/
│ │ ├── typeDefs/
│ │ │ └── product.graphql
│ │ └── resolvers/
│ │ ├── product.ts
│ │ └── index.ts
│ └── thirdParty/
│ ├── typeDefs/
│ │ └── extra.graphql
│ └── resolvers/
│ ├── reviews.ts
│ └── index.ts
└── fragments/
├── ServerProduct.ts ← 服务端片段(SSR)
└── ClientProduct.ts ← 客户端片段(渲染后加载)StoreProductundefined
```typescript
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/fragments/ServerProduct.ts
import { gql } from '@faststore/core/api'
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
count
value
totalValue
interestRate
}
}
}
`)// src/graphql/thirdParty/resolvers/reviews.ts
import type { Resolver } from '@faststore/api'
const REVIEWS_API_KEY = process.env.REVIEWS_API_KEY // Server-only env var (no NEXT_PUBLIC_ prefix)
const reviewsResolver: Record<string, Resolver> = {
StoreProduct: {
reviews: async (root) => {
const response = await fetch(
`https://api.reviews-service.com/products/${root.productID}/reviews`,
{
headers: {
Authorization: `Bearer ${REVIEWS_API_KEY}`,
},
}
)
const data = await response.json()
return {
averageRating: data.average_rating,
totalReviews: data.total_count,
reviews: data.reviews.slice(0, 5).map((r: any) => ({
author: r.author_name,
rating: r.rating,
text: r.review_text,
date: r.created_at,
})),
}
},
},
}
export default reviewsResolver
```typescript
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/fragments/ServerProduct.ts
import { gql } from '@faststore/core/api'
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
count
value
totalValue
interestRate
}
}
}
`)// src/graphql/thirdParty/resolvers/reviews.ts
import type { Resolver } from '@faststore/api'
const REVIEWS_API_KEY = process.env.REVIEWS_API_KEY // 仅服务端环境变量(无NEXT_PUBLIC_前缀)
const reviewsResolver: Record<string, Resolver> = {
StoreProduct: {
reviews: async (root) => {
const response = await fetch(
`https://api.reviews-service.com/products/${root.productID}/reviews`,
{
headers: {
Authorization: `Bearer ${REVIEWS_API_KEY}`,
},
}
)
const data = await response.json()
return {
averageRating: data.average_rating,
totalReviews: data.total_count,
reviews: data.reviews.slice(0, 5).map((r: any) => ({
author: r.author_name,
rating: r.rating,
text: r.review_text,
date: r.created_at,
})),
}
},
},
}
export default reviewsResolverVTEX_APP_KEYVTEX_APP_TOKENNEXT_PUBLIC_src/graphql/vtex/src/graphql/thirdParty/pages/api/src/graphql/vtex/resolvers/index.tsNEXT_PUBLIC_VTEX_APP_KEYVTEX_APP_TOKENsrc/graphql/vtex/src/graphql/thirdParty/pages/api/src/graphql/vtex/resolvers/index.tssrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/NEXT_PUBLIC_src/fragments/src/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/NEXT_PUBLIC_src/fragments/faststore-state-managementfaststore-state-management