@faststore/api 1.8.22 → 1.8.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/__generated__/schema.d.ts +10 -10
- package/dist/api.cjs.development.js +115 -34
- package/dist/api.cjs.development.js.map +1 -1
- package/dist/api.cjs.production.min.js +1 -1
- package/dist/api.cjs.production.min.js.map +1 -1
- package/dist/api.esm.js +115 -34
- package/dist/api.esm.js.map +1 -1
- package/dist/platforms/vtex/clients/commerce/index.d.ts +6 -0
- package/dist/platforms/vtex/clients/commerce/types/OrderForm.d.ts +8 -1
- package/dist/platforms/vtex/clients/index.d.ts +6 -0
- package/dist/platforms/vtex/index.d.ts +6 -0
- package/dist/platforms/vtex/utils/md5.d.ts +1 -0
- package/package.json +2 -2
- package/src/__generated__/schema.ts +10 -10
- package/src/platforms/vtex/clients/commerce/index.ts +20 -0
- package/src/platforms/vtex/clients/commerce/types/OrderForm.ts +9 -1
- package/src/platforms/vtex/index.ts +7 -0
- package/src/platforms/vtex/resolvers/product.ts +6 -3
- package/src/platforms/vtex/resolvers/validateCart.ts +93 -20
- package/src/platforms/vtex/utils/md5.ts +4 -0
- package/src/typeDefs/cart.graphql +1 -1
- package/src/typeDefs/order.graphql +2 -2
- package/src/typeDefs/product.graphql +5 -5
- package/src/typeDefs/productGroup.graphql +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/api",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"graphql": "^15.6.0"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "da2e248b5446eea91fe96e96e6f7028d2ab79c45"
|
|
49
49
|
}
|
|
@@ -11,7 +11,7 @@ export type Scalars = {
|
|
|
11
11
|
Float: number;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/** Shopping cart
|
|
14
|
+
/** Shopping cart input. */
|
|
15
15
|
export type IStoreCart = {
|
|
16
16
|
/** Order information, including `orderNumber` and `acceptedOffer`. */
|
|
17
17
|
order: IStoreOrder;
|
|
@@ -43,7 +43,7 @@ export type IStoreOffer = {
|
|
|
43
43
|
export type IStoreOrder = {
|
|
44
44
|
/** Array with information on each accepted offer. */
|
|
45
45
|
acceptedOffer: Array<IStoreOffer>;
|
|
46
|
-
/**
|
|
46
|
+
/** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */
|
|
47
47
|
orderNumber: Scalars['String'];
|
|
48
48
|
};
|
|
49
49
|
|
|
@@ -53,13 +53,13 @@ export type IStoreOrganization = {
|
|
|
53
53
|
identifier: Scalars['String'];
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
/** Product input. */
|
|
56
|
+
/** Product input. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
|
|
57
57
|
export type IStoreProduct = {
|
|
58
58
|
/** Array of product images. */
|
|
59
59
|
image: Array<IStoreImage>;
|
|
60
60
|
/** Product name. */
|
|
61
61
|
name: Scalars['String'];
|
|
62
|
-
/** Stock Keeping Unit ID. */
|
|
62
|
+
/** Stock Keeping Unit. Merchant-specific ID for the product. */
|
|
63
63
|
sku: Scalars['String'];
|
|
64
64
|
};
|
|
65
65
|
|
|
@@ -350,7 +350,7 @@ export type StoreOrder = {
|
|
|
350
350
|
__typename?: 'StoreOrder';
|
|
351
351
|
/** Array with information on each accepted offer. */
|
|
352
352
|
acceptedOffer: Array<StoreOffer>;
|
|
353
|
-
/**
|
|
353
|
+
/** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */
|
|
354
354
|
orderNumber: Scalars['String'];
|
|
355
355
|
};
|
|
356
356
|
|
|
@@ -389,7 +389,7 @@ export type StorePerson = {
|
|
|
389
389
|
id: Scalars['String'];
|
|
390
390
|
};
|
|
391
391
|
|
|
392
|
-
/** Product information. */
|
|
392
|
+
/** Product information. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
|
|
393
393
|
export type StoreProduct = {
|
|
394
394
|
__typename?: 'StoreProduct';
|
|
395
395
|
/** Array of additional properties. */
|
|
@@ -412,13 +412,13 @@ export type StoreProduct = {
|
|
|
412
412
|
name: Scalars['String'];
|
|
413
413
|
/** Aggregate offer information. */
|
|
414
414
|
offers: StoreAggregateOffer;
|
|
415
|
-
/** Product ID. */
|
|
415
|
+
/** Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs. */
|
|
416
416
|
productID: Scalars['String'];
|
|
417
417
|
/** Array with review information. */
|
|
418
418
|
review: Array<StoreReview>;
|
|
419
419
|
/** Meta tag data. */
|
|
420
420
|
seo: StoreSeo;
|
|
421
|
-
/** Stock Keeping Unit ID. */
|
|
421
|
+
/** Stock Keeping Unit. Merchant-specific ID for the product. */
|
|
422
422
|
sku: Scalars['String'];
|
|
423
423
|
/** Corresponding collection URL slug, with which to retrieve this entity. */
|
|
424
424
|
slug: Scalars['String'];
|
|
@@ -442,12 +442,12 @@ export type StoreProductEdge = {
|
|
|
442
442
|
node: StoreProduct;
|
|
443
443
|
};
|
|
444
444
|
|
|
445
|
-
/** Product group information. */
|
|
445
|
+
/** Product group information. Product groups are catalog entities that may contain variants. They are equivalent to VTEX [Products](https://help.vtex.com/en/tutorial/what-is-a-product--2zrB2gFCHyQokCKKE8kuAw#), whereas each variant is equivalent to a VTEX [SKU](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
|
|
446
446
|
export type StoreProductGroup = {
|
|
447
447
|
__typename?: 'StoreProductGroup';
|
|
448
448
|
/** Array of additional properties. */
|
|
449
449
|
additionalProperty: Array<StorePropertyValue>;
|
|
450
|
-
/** Array of variants related to product group. */
|
|
450
|
+
/** Array of variants related to product group. Variants are equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). */
|
|
451
451
|
hasVariant: Array<StoreProduct>;
|
|
452
452
|
/** Product group name. */
|
|
453
453
|
name: Scalars['String'];
|
|
@@ -103,6 +103,26 @@ export const VtexCommerce = (
|
|
|
103
103
|
}
|
|
104
104
|
)
|
|
105
105
|
},
|
|
106
|
+
setCustomData: ({
|
|
107
|
+
id,
|
|
108
|
+
appId,
|
|
109
|
+
key,
|
|
110
|
+
value,
|
|
111
|
+
}: {
|
|
112
|
+
id: string
|
|
113
|
+
appId: string
|
|
114
|
+
key: string
|
|
115
|
+
value: string
|
|
116
|
+
}): Promise<OrderForm> => {
|
|
117
|
+
return fetchAPI(
|
|
118
|
+
`${base}/api/checkout/pub/orderForm/${id}/customData/${appId}/${key}`,
|
|
119
|
+
{
|
|
120
|
+
...BASE_INIT,
|
|
121
|
+
body: JSON.stringify({ value }),
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
},
|
|
106
126
|
region: async ({
|
|
107
127
|
postalCode,
|
|
108
128
|
country,
|
|
@@ -136,7 +136,7 @@ export interface OrderForm {
|
|
|
136
136
|
giftRegistryData: any | null
|
|
137
137
|
openTextField: any | null
|
|
138
138
|
invoiceData: any | null
|
|
139
|
-
customData:
|
|
139
|
+
customData: OrderFormCustomData | null
|
|
140
140
|
itemMetadata: {
|
|
141
141
|
items: MetadataItem[]
|
|
142
142
|
}
|
|
@@ -149,6 +149,14 @@ export interface OrderForm {
|
|
|
149
149
|
itemsOrdination: any | null
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export interface OrderFormCustomData {
|
|
153
|
+
customApps: Array<{
|
|
154
|
+
fields: Record<string, string>
|
|
155
|
+
id: string
|
|
156
|
+
major: number
|
|
157
|
+
}>
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
export interface OrderFormMarketingData {
|
|
153
161
|
utmCampaign?: string
|
|
154
162
|
utmMedium?: string
|
|
@@ -25,6 +25,11 @@ export interface Options {
|
|
|
25
25
|
// Default sales channel to use for fetching products
|
|
26
26
|
channel: string
|
|
27
27
|
hideUnavailableItems: boolean
|
|
28
|
+
flags?: FeatureFlags
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface FeatureFlags {
|
|
32
|
+
enableOrderFormSync?: boolean
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export interface Context {
|
|
@@ -38,6 +43,7 @@ export interface Context {
|
|
|
38
43
|
* */
|
|
39
44
|
storage: {
|
|
40
45
|
channel: Required<Channel>
|
|
46
|
+
flags: FeatureFlags
|
|
41
47
|
}
|
|
42
48
|
headers: Record<string, string>
|
|
43
49
|
}
|
|
@@ -68,6 +74,7 @@ const Resolvers = {
|
|
|
68
74
|
export const getContextFactory = (options: Options) => (ctx: any): Context => {
|
|
69
75
|
ctx.storage = {
|
|
70
76
|
channel: ChannelMarshal.parse(options.channel),
|
|
77
|
+
flags: options.flags ?? {},
|
|
71
78
|
}
|
|
72
79
|
ctx.clients = getClients(options, ctx)
|
|
73
80
|
ctx.loaders = getLoaders(options, ctx)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
|
|
2
2
|
import { bestOfferFirst } from '../utils/productStock'
|
|
3
|
+
import { slugify } from '../utils/slugify'
|
|
3
4
|
import type { EnhancedCommercialOffer } from '../utils/enhanceCommercialOffer'
|
|
4
5
|
import type { Resolver } from '..'
|
|
5
6
|
import type { PromiseType } from '../../../typings'
|
|
@@ -38,11 +39,13 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
|
|
|
38
39
|
return {
|
|
39
40
|
itemListElement: [
|
|
40
41
|
...categories.reverse().map((categoryPath, index) => {
|
|
41
|
-
const
|
|
42
|
+
const splitted = categoryPath.split('/')
|
|
43
|
+
const name = splitted[splitted.length - 2]
|
|
44
|
+
const item = splitted.map(slugify).join('/')
|
|
42
45
|
|
|
43
46
|
return {
|
|
44
|
-
name
|
|
45
|
-
item
|
|
47
|
+
name,
|
|
48
|
+
item,
|
|
46
49
|
position: index + 1,
|
|
47
50
|
}
|
|
48
51
|
}),
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import deepEquals from 'fast-deep-equal'
|
|
2
2
|
|
|
3
|
+
import { md5 } from '../utils/md5'
|
|
3
4
|
import type {
|
|
4
|
-
IStoreOrder,
|
|
5
5
|
IStoreCart,
|
|
6
6
|
IStoreOffer,
|
|
7
|
+
IStoreOrder,
|
|
7
8
|
} from '../../../__generated__/schema'
|
|
8
9
|
import type {
|
|
9
10
|
OrderForm,
|
|
10
|
-
OrderFormItem,
|
|
11
11
|
OrderFormInputItem,
|
|
12
|
+
OrderFormItem,
|
|
12
13
|
} from '../clients/commerce/types/OrderForm'
|
|
13
14
|
import type { Context } from '..'
|
|
14
15
|
|
|
@@ -69,6 +70,70 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
|
|
|
69
70
|
return isSameOrder && orderItemsAreSync
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
const orderFormToCart = (
|
|
74
|
+
form: OrderForm,
|
|
75
|
+
skuLoader: Context['loaders']['skuLoader']
|
|
76
|
+
) => {
|
|
77
|
+
return {
|
|
78
|
+
order: {
|
|
79
|
+
orderNumber: form.orderFormId,
|
|
80
|
+
acceptedOffer: form.items.map((item) => ({
|
|
81
|
+
...item,
|
|
82
|
+
product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
messages: form.messages.map(({ text, status }) => ({
|
|
86
|
+
text,
|
|
87
|
+
status: status.toUpperCase(),
|
|
88
|
+
})),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
|
|
93
|
+
|
|
94
|
+
const setOrderFormEtag = async (
|
|
95
|
+
form: OrderForm,
|
|
96
|
+
commerce: Context['clients']['commerce']
|
|
97
|
+
) => {
|
|
98
|
+
try {
|
|
99
|
+
const orderForm = await commerce.checkout.setCustomData({
|
|
100
|
+
id: form.orderFormId,
|
|
101
|
+
appId: 'faststore',
|
|
102
|
+
key: 'cartEtag',
|
|
103
|
+
value: getOrderFormEtag(form),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return orderForm
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(
|
|
109
|
+
'Error while setting custom data to orderForm.\n Make sure to add the following custom app to the orderForm: \n{"fields":["cartEtag"],"id":"faststore","major":1}.\n More info at: https://developers.vtex.com/vtex-rest-api/docs/customizable-fields-with-checkout-api'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if cartEtag stored on customData is up to date
|
|
118
|
+
* @description If cartEtag is not up to date, this means that
|
|
119
|
+
* another system changed the cart, like Checkout UI or Order Placed
|
|
120
|
+
*/
|
|
121
|
+
const isOrderFormStale = (form: OrderForm) => {
|
|
122
|
+
const faststoreData = form.customData?.customApps.find(
|
|
123
|
+
(app) => app.id === 'faststore'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const oldEtag = faststoreData?.fields?.cartEtag
|
|
127
|
+
|
|
128
|
+
if (oldEtag == null) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newEtag = getOrderFormEtag(form)
|
|
133
|
+
|
|
134
|
+
return newEtag !== oldEtag
|
|
135
|
+
}
|
|
136
|
+
|
|
72
137
|
/**
|
|
73
138
|
* This resolver implements the optimistic cart behavior. The main idea in here
|
|
74
139
|
* is that we receive a cart from the UI (as query params) and we validate it with
|
|
@@ -87,6 +152,7 @@ export const validateCart = async (
|
|
|
87
152
|
{ cart: { order } }: { cart: IStoreCart },
|
|
88
153
|
ctx: Context
|
|
89
154
|
) => {
|
|
155
|
+
const { enableOrderFormSync } = ctx.storage.flags
|
|
90
156
|
const { orderNumber, acceptedOffer } = order
|
|
91
157
|
const {
|
|
92
158
|
clients: { commerce },
|
|
@@ -98,6 +164,19 @@ export const validateCart = async (
|
|
|
98
164
|
id: orderNumber,
|
|
99
165
|
})
|
|
100
166
|
|
|
167
|
+
// Step1.5: Check if another system changed the orderForm with this orderNumber
|
|
168
|
+
// If so, this means the user interacted with this cart elsewhere and expects
|
|
169
|
+
// to see this new cart state instead of what's stored on the user's browser.
|
|
170
|
+
if (enableOrderFormSync === true) {
|
|
171
|
+
const isStale = isOrderFormStale(orderForm)
|
|
172
|
+
|
|
173
|
+
if (isStale === true && orderNumber) {
|
|
174
|
+
const newOrderForm = await setOrderFormEtag(orderForm, commerce)
|
|
175
|
+
|
|
176
|
+
return orderFormToCart(newOrderForm, skuLoader)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
101
180
|
// Step2: Process items from both browser and checkout so they have the same shape
|
|
102
181
|
const browserItemsById = groupById(acceptedOffer)
|
|
103
182
|
const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
|
|
@@ -139,28 +218,22 @@ export const validateCart = async (
|
|
|
139
218
|
}
|
|
140
219
|
|
|
141
220
|
// Step4: Apply delta changes to order form
|
|
142
|
-
const updatedOrderForm = await commerce.checkout
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
221
|
+
const updatedOrderForm = await commerce.checkout
|
|
222
|
+
// update orderForm items
|
|
223
|
+
.updateOrderFormItems({
|
|
224
|
+
id: orderForm.orderFormId,
|
|
225
|
+
orderItems: changes,
|
|
226
|
+
})
|
|
227
|
+
// update orderForm etag so we know last time we touched this orderForm
|
|
228
|
+
.then((form) =>
|
|
229
|
+
enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
|
|
230
|
+
)
|
|
146
231
|
|
|
147
232
|
// Step5: If no changes detected before/after updating orderForm, the order is validated
|
|
148
233
|
if (equals(order, updatedOrderForm)) {
|
|
149
234
|
return null
|
|
150
235
|
}
|
|
151
236
|
|
|
152
|
-
// Step6: There were changes, convert orderForm to
|
|
153
|
-
return
|
|
154
|
-
order: {
|
|
155
|
-
orderNumber: updatedOrderForm.orderFormId,
|
|
156
|
-
acceptedOffer: updatedOrderForm.items.map((item) => ({
|
|
157
|
-
...item,
|
|
158
|
-
product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
|
|
159
|
-
})),
|
|
160
|
-
},
|
|
161
|
-
messages: updatedOrderForm.messages.map(({ text, status }) => ({
|
|
162
|
-
text,
|
|
163
|
-
status: status.toUpperCase(),
|
|
164
|
-
})),
|
|
165
|
-
}
|
|
237
|
+
// Step6: There were changes, convert orderForm to StoreCart
|
|
238
|
+
return orderFormToCart(updatedOrderForm, skuLoader)
|
|
166
239
|
}
|
|
@@ -3,7 +3,7 @@ Information of a specific order.
|
|
|
3
3
|
"""
|
|
4
4
|
type StoreOrder {
|
|
5
5
|
"""
|
|
6
|
-
|
|
6
|
+
ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#).
|
|
7
7
|
"""
|
|
8
8
|
orderNumber: String!
|
|
9
9
|
"""
|
|
@@ -17,7 +17,7 @@ Offer input.
|
|
|
17
17
|
"""
|
|
18
18
|
input IStoreOrder {
|
|
19
19
|
"""
|
|
20
|
-
|
|
20
|
+
ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#).
|
|
21
21
|
"""
|
|
22
22
|
orderNumber: String!
|
|
23
23
|
"""
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Product information.
|
|
2
|
+
Product information. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
|
|
3
3
|
"""
|
|
4
4
|
type StoreProduct {
|
|
5
5
|
"""
|
|
@@ -19,7 +19,7 @@ type StoreProduct {
|
|
|
19
19
|
"""
|
|
20
20
|
name: String!
|
|
21
21
|
"""
|
|
22
|
-
Product ID.
|
|
22
|
+
Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs.
|
|
23
23
|
"""
|
|
24
24
|
productID: String!
|
|
25
25
|
"""
|
|
@@ -39,7 +39,7 @@ type StoreProduct {
|
|
|
39
39
|
"""
|
|
40
40
|
offers: StoreAggregateOffer!
|
|
41
41
|
"""
|
|
42
|
-
Stock Keeping Unit ID.
|
|
42
|
+
Stock Keeping Unit. Merchant-specific ID for the product.
|
|
43
43
|
"""
|
|
44
44
|
sku: String!
|
|
45
45
|
"""
|
|
@@ -65,11 +65,11 @@ type StoreProduct {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
"""
|
|
68
|
-
Product input.
|
|
68
|
+
Product input. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
|
|
69
69
|
"""
|
|
70
70
|
input IStoreProduct {
|
|
71
71
|
"""
|
|
72
|
-
Stock Keeping Unit ID.
|
|
72
|
+
Stock Keeping Unit. Merchant-specific ID for the product.
|
|
73
73
|
"""
|
|
74
74
|
sku: String!
|
|
75
75
|
"""
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Product group information.
|
|
2
|
+
Product group information. Product groups are catalog entities that may contain variants. They are equivalent to VTEX [Products](https://help.vtex.com/en/tutorial/what-is-a-product--2zrB2gFCHyQokCKKE8kuAw#), whereas each variant is equivalent to a VTEX [SKU](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
|
|
3
3
|
"""
|
|
4
4
|
type StoreProductGroup {
|
|
5
5
|
"""
|
|
6
|
-
Array of variants related to product group.
|
|
6
|
+
Array of variants related to product group. Variants are equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#).
|
|
7
7
|
"""
|
|
8
8
|
hasVariant: [StoreProduct!]!
|
|
9
9
|
"""
|