@faststore/api 4.5.0-dev.3 → 4.5.0-dev.5
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/dist/cjs/index.js +105 -44
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.mjs +1164 -901
- package/dist/es/index.mjs.map +1 -1
- package/dist/src/__generated__/schema.d.ts +52 -3
- package/dist/src/__generated__/schema.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/catalog/index.d.ts +34 -0
- package/dist/src/platforms/vtex/clients/catalog/index.d.ts.map +1 -0
- package/dist/src/platforms/vtex/clients/commerce/index.d.ts +6 -0
- package/dist/src/platforms/vtex/clients/commerce/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/commerce/types/ByLinkId.d.ts +45 -0
- package/dist/src/platforms/vtex/clients/commerce/types/ByLinkId.d.ts.map +1 -0
- package/dist/src/platforms/vtex/clients/commerce/types/OrderForm.d.ts +5 -0
- package/dist/src/platforms/vtex/clients/commerce/types/OrderForm.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/index.d.ts +8 -0
- package/dist/src/platforms/vtex/clients/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/search/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/clients/search/types/ProductSearchResult.d.ts +7 -0
- package/dist/src/platforms/vtex/clients/search/types/ProductSearchResult.d.ts.map +1 -1
- package/dist/src/platforms/vtex/index.d.ts +12 -13
- package/dist/src/platforms/vtex/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/loaders/collection.d.ts +32 -3
- package/dist/src/platforms/vtex/loaders/collection.d.ts.map +1 -1
- package/dist/src/platforms/vtex/loaders/index.d.ts +2 -2
- package/dist/src/platforms/vtex/loaders/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/collection.d.ts +20 -2
- package/dist/src/platforms/vtex/resolvers/collection.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/getOrderEntryOperation.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/index.d.ts +3 -13
- package/dist/src/platforms/vtex/resolvers/index.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/offer.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/product.d.ts +13 -1
- package/dist/src/platforms/vtex/resolvers/product.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/query.d.ts +8 -17
- package/dist/src/platforms/vtex/resolvers/query.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/validateCart.d.ts.map +1 -1
- package/dist/src/platforms/vtex/utils/localization.d.ts +28 -0
- package/dist/src/platforms/vtex/utils/localization.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/__generated__/schema.ts +54 -3
- package/src/platforms/vtex/clients/catalog/index.ts +52 -0
- package/src/platforms/vtex/clients/commerce/index.ts +71 -0
- package/src/platforms/vtex/clients/commerce/types/ByLinkId.ts +46 -0
- package/src/platforms/vtex/clients/commerce/types/OrderForm.ts +5 -0
- package/src/platforms/vtex/clients/index.ts +3 -0
- package/src/platforms/vtex/clients/search/index.ts +16 -4
- package/src/platforms/vtex/clients/search/types/ProductSearchResult.ts +7 -0
- package/src/platforms/vtex/index.ts +17 -2
- package/src/platforms/vtex/loaders/collection.ts +99 -25
- package/src/platforms/vtex/loaders/index.ts +2 -1
- package/src/platforms/vtex/resolvers/collection.ts +173 -66
- package/src/platforms/vtex/resolvers/offer.ts +7 -0
- package/src/platforms/vtex/resolvers/product.ts +160 -11
- package/src/platforms/vtex/resolvers/query.ts +148 -56
- package/src/platforms/vtex/resolvers/validateCart.ts +6 -1
- package/src/platforms/vtex/typeDefs/collection.graphql +33 -1
- package/src/platforms/vtex/typeDefs/offer.graphql +10 -0
- package/src/platforms/vtex/typeDefs/product.graphql +19 -0
- package/src/platforms/vtex/utils/localization.ts +45 -0
|
@@ -18,8 +18,6 @@ import type {
|
|
|
18
18
|
StoreContract,
|
|
19
19
|
UserOrderFromList,
|
|
20
20
|
} from '../../../__generated__/schema'
|
|
21
|
-
import { getOrderEntryOperation } from './getOrderEntryOperation'
|
|
22
|
-
import { getOrderFormItems } from './getOrderFormItems'
|
|
23
21
|
import { recommendations } from './recommendations'
|
|
24
22
|
import {
|
|
25
23
|
BadRequestError,
|
|
@@ -33,6 +31,10 @@ import type { ProfileAddress } from '../clients/commerce/types/Profile'
|
|
|
33
31
|
import type { SearchArgs } from '../clients/search'
|
|
34
32
|
import type { ProductSearchResult } from '../clients/search/types/ProductSearchResult'
|
|
35
33
|
import type { GraphqlContext } from '../index'
|
|
34
|
+
import type {
|
|
35
|
+
ByLinkIdBrandRoot,
|
|
36
|
+
ByLinkIdCategoryRoot,
|
|
37
|
+
} from '../loaders/collection'
|
|
36
38
|
import { extractRuleForAuthorization } from '../utils/commercialAuth'
|
|
37
39
|
import {
|
|
38
40
|
mapSessionContractsToStoreContracts,
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
} from '../utils/contract'
|
|
43
45
|
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
|
|
44
46
|
import { getAuthCookie, parseJwt } from '../utils/cookies'
|
|
45
|
-
import { enhanceSku } from '../utils/enhanceSku'
|
|
47
|
+
import { enhanceSku, type EnhancedSku } from '../utils/enhanceSku'
|
|
46
48
|
import {
|
|
47
49
|
findChannel,
|
|
48
50
|
findCrossSelling,
|
|
@@ -51,10 +53,36 @@ import {
|
|
|
51
53
|
findSlug,
|
|
52
54
|
transformSelectedFacet,
|
|
53
55
|
} from '../utils/facets'
|
|
56
|
+
import { getCatalogLocale, isLocalizationEnabled } from '../utils/localization'
|
|
54
57
|
import { isValidSkuId, pickBestSku } from '../utils/sku'
|
|
58
|
+
import { slugify } from '../utils/slugify'
|
|
55
59
|
import { SORT_MAP } from '../utils/sort'
|
|
56
60
|
import { FACET_CROSS_SELLING_MAP } from './../utils/facets'
|
|
57
61
|
import { StoreCollection } from './collection'
|
|
62
|
+
import { getOrderEntryOperation } from './getOrderEntryOperation'
|
|
63
|
+
import { getOrderFormItems } from './getOrderFormItems'
|
|
64
|
+
import { getLocalizedProductEntry } from './product'
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validates that a slug mismatch between IS linkText and the requested slug is
|
|
68
|
+
* actually a localized slug match. Fetches the localized product entry from
|
|
69
|
+
* Catalog Dataplane (with request-scoped caching) and checks whether the slug
|
|
70
|
+
* prefix matches the localized linkId for the current locale.
|
|
71
|
+
*
|
|
72
|
+
* Returns true if the slug is a valid localized match, false otherwise
|
|
73
|
+
* (including when the Dataplane API is unavailable).
|
|
74
|
+
*/
|
|
75
|
+
async function isLocalizedSlugMatch(
|
|
76
|
+
ctx: GraphqlContext,
|
|
77
|
+
slug: string,
|
|
78
|
+
productGroupID: string,
|
|
79
|
+
locale: string
|
|
80
|
+
): Promise<boolean> {
|
|
81
|
+
const slugPrefix = slug.slice(0, slug.lastIndexOf('-'))
|
|
82
|
+
const entry = await getLocalizedProductEntry(ctx, productGroupID, locale)
|
|
83
|
+
|
|
84
|
+
return entry?.linkId === slugPrefix
|
|
85
|
+
}
|
|
58
86
|
|
|
59
87
|
const INVALID_SKU_ID_ERROR = 'Invalid SkuId'
|
|
60
88
|
const SLUG_MISMATCH_ERROR =
|
|
@@ -66,6 +94,80 @@ const shouldFallbackToProductRoute = (error: unknown) =>
|
|
|
66
94
|
(error.message === INVALID_SKU_ID_ERROR ||
|
|
67
95
|
error.message.startsWith(SLUG_MISMATCH_ERROR)))
|
|
68
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Here be dragons 🦄🦄🦄
|
|
99
|
+
*
|
|
100
|
+
* In some cases, the slug has a valid skuId for a different product. This
|
|
101
|
+
* guards that the fetched sku is the one we actually asked for, throwing
|
|
102
|
+
* SLUG_MISMATCH_ERROR (caught by the caller) when it isn't.
|
|
103
|
+
*
|
|
104
|
+
* When localization is enabled, the slug prefix may be a localized LinkId
|
|
105
|
+
* that differs from the IS linkText (always in the default locale). In that
|
|
106
|
+
* case we validate against the Catalog Dataplane API before rejecting the
|
|
107
|
+
* slug.
|
|
108
|
+
*/
|
|
109
|
+
async function assertSkuMatchesSlug(
|
|
110
|
+
ctx: GraphqlContext,
|
|
111
|
+
sku: EnhancedSku,
|
|
112
|
+
slug: string | null | undefined,
|
|
113
|
+
locale: string | null | undefined
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const { linkText, productId } = sku.isVariantOf
|
|
116
|
+
|
|
117
|
+
if (!slug || !linkText || slug.startsWith(linkText)) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isValidLocalizedMatch =
|
|
122
|
+
isLocalizationEnabled(ctx) &&
|
|
123
|
+
locale &&
|
|
124
|
+
isValidSkuId(slug.split('-').pop() ?? '') &&
|
|
125
|
+
(await isLocalizedSlugMatch(ctx, slug, productId, locale))
|
|
126
|
+
|
|
127
|
+
if (isValidLocalizedMatch) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`${SLUG_MISMATCH_ERROR} slug: ${slug}, linkText: ${linkText}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Fallback used when the sku/slug lookup above fails (invalid skuId, not
|
|
136
|
+
* found, or slug mismatch): resolves the slug through the legacy pagetype
|
|
137
|
+
* route and fetches the product from Intelligent Search by id instead.
|
|
138
|
+
*/
|
|
139
|
+
async function fetchProductBySlugFallback(
|
|
140
|
+
ctx: GraphqlContext,
|
|
141
|
+
slug: string | null | undefined
|
|
142
|
+
): Promise<EnhancedSku> {
|
|
143
|
+
if (slug == null) {
|
|
144
|
+
throw new BadRequestError('Missing slug or id')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const {
|
|
148
|
+
clients: { commerce, search },
|
|
149
|
+
} = ctx
|
|
150
|
+
|
|
151
|
+
const route = await commerce.catalog.portal.pagetype(`${slug}/p`)
|
|
152
|
+
|
|
153
|
+
if (route.pageType !== 'Product' || !route.id) {
|
|
154
|
+
throw new NotFoundError(`No product found for slug ${slug}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const product = await search
|
|
158
|
+
.fetchProduct({
|
|
159
|
+
field: 'id',
|
|
160
|
+
value: String(route.id),
|
|
161
|
+
})
|
|
162
|
+
.catch(() => null)
|
|
163
|
+
|
|
164
|
+
if (!product) {
|
|
165
|
+
throw new NotFoundError(`No product found for id ${route.id}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return enhanceSku(pickBestSku(product.items), product)
|
|
169
|
+
}
|
|
170
|
+
|
|
69
171
|
export const Query = {
|
|
70
172
|
product: async (
|
|
71
173
|
_: unknown,
|
|
@@ -88,7 +190,6 @@ export const Query = {
|
|
|
88
190
|
|
|
89
191
|
const {
|
|
90
192
|
loaders: { skuLoader },
|
|
91
|
-
clients: { commerce, search },
|
|
92
193
|
} = ctx
|
|
93
194
|
|
|
94
195
|
try {
|
|
@@ -100,22 +201,7 @@ export const Query = {
|
|
|
100
201
|
|
|
101
202
|
const sku = await skuLoader.load(skuId)
|
|
102
203
|
|
|
103
|
-
|
|
104
|
-
* Here be dragons 🦄🦄🦄
|
|
105
|
-
*
|
|
106
|
-
* In some cases, the slug has a valid skuId for a different
|
|
107
|
-
* product. This condition makes sure that the fetched sku
|
|
108
|
-
* is the one we actually asked for
|
|
109
|
-
* */
|
|
110
|
-
if (
|
|
111
|
-
slug &&
|
|
112
|
-
sku.isVariantOf.linkText &&
|
|
113
|
-
!slug.startsWith(sku.isVariantOf.linkText)
|
|
114
|
-
) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`Slug was set but the fetched sku does not satisfy the slug condition. slug: ${slug}, linkText: ${sku.isVariantOf.linkText}`
|
|
117
|
-
)
|
|
118
|
-
}
|
|
204
|
+
await assertSkuMatchesSlug(ctx, sku, slug, locale)
|
|
119
205
|
|
|
120
206
|
return sku
|
|
121
207
|
} catch (err) {
|
|
@@ -123,30 +209,7 @@ export const Query = {
|
|
|
123
209
|
throw err
|
|
124
210
|
}
|
|
125
211
|
|
|
126
|
-
|
|
127
|
-
throw new BadRequestError('Missing slug or id')
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const route = await commerce.catalog.portal.pagetype(`${slug}/p`)
|
|
131
|
-
|
|
132
|
-
if (route.pageType !== 'Product' || !route.id) {
|
|
133
|
-
throw new NotFoundError(`No product found for slug ${slug}`)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const product = await search
|
|
137
|
-
.fetchProduct({
|
|
138
|
-
field: 'id',
|
|
139
|
-
value: String(route.id),
|
|
140
|
-
})
|
|
141
|
-
.catch(() => null)
|
|
142
|
-
|
|
143
|
-
if (!product) {
|
|
144
|
-
throw new NotFoundError(`No product found for id ${route.id}`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const sku = pickBestSku(product.items)
|
|
148
|
-
|
|
149
|
-
return enhanceSku(sku, product)
|
|
212
|
+
return fetchProductBySlugFallback(ctx, slug)
|
|
150
213
|
}
|
|
151
214
|
},
|
|
152
215
|
collection: (
|
|
@@ -158,7 +221,14 @@ export const Query = {
|
|
|
158
221
|
loaders: { collectionLoader },
|
|
159
222
|
} = ctx
|
|
160
223
|
|
|
161
|
-
|
|
224
|
+
// The request locale is set on ctx.storage.locale by the core `execute`
|
|
225
|
+
// wrapper (from Next.js i18n) rather than a GraphQL argument, so overridable
|
|
226
|
+
// fragments (API extensions) that also select `collection` keep merging
|
|
227
|
+
// without argument conflicts.
|
|
228
|
+
return collectionLoader.load({
|
|
229
|
+
slug,
|
|
230
|
+
locale: getCatalogLocale(ctx),
|
|
231
|
+
})
|
|
162
232
|
},
|
|
163
233
|
search: async (
|
|
164
234
|
_: unknown,
|
|
@@ -323,25 +393,47 @@ export const Query = {
|
|
|
323
393
|
commerce.catalog.category.tree(),
|
|
324
394
|
])
|
|
325
395
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
396
|
+
// Flatten the category tree. parentId is tracked per node so
|
|
397
|
+
// the type resolver can correctly classify Departments (fatherCategoryId: null)
|
|
398
|
+
// vs Categories (fatherCategoryId: number).
|
|
399
|
+
const categoryRoots: ByLinkIdCategoryRoot[] = []
|
|
400
|
+
const dfs = (node: CategoryTree, parentId: number | null) => {
|
|
401
|
+
categoryRoots.push({
|
|
402
|
+
id: node.id,
|
|
403
|
+
name: node.name,
|
|
404
|
+
fatherCategoryId: parentId,
|
|
405
|
+
linkId: slugify(node.name),
|
|
406
|
+
title: node.Title,
|
|
407
|
+
description: null,
|
|
408
|
+
metaTagDescription: node.MetaTagDescription,
|
|
409
|
+
availableLinkIds: null,
|
|
410
|
+
entityType: 'category' as const,
|
|
411
|
+
slug: new URL(node.url).pathname.slice(1).toLowerCase(),
|
|
412
|
+
})
|
|
329
413
|
|
|
330
414
|
for (const child of node.children) {
|
|
331
|
-
dfs(child,
|
|
415
|
+
dfs(child, node.id)
|
|
332
416
|
}
|
|
333
417
|
}
|
|
334
418
|
|
|
335
419
|
for (const node of tree) {
|
|
336
|
-
dfs(node,
|
|
420
|
+
dfs(node, null)
|
|
337
421
|
}
|
|
338
422
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
423
|
+
const brandRoots: ByLinkIdBrandRoot[] = brands
|
|
424
|
+
.filter((brand) => brand.isActive)
|
|
425
|
+
.map((brand) => ({
|
|
426
|
+
id: brand.id,
|
|
427
|
+
name: brand.name,
|
|
428
|
+
linkId: slugify(brand.name),
|
|
429
|
+
title: brand.title,
|
|
430
|
+
description: null,
|
|
431
|
+
metaTagDescription: brand.metaTagDescription,
|
|
432
|
+
availableLinkIds: null,
|
|
433
|
+
entityType: 'brand' as const,
|
|
434
|
+
}))
|
|
435
|
+
|
|
436
|
+
const collections = [...brandRoots, ...categoryRoots]
|
|
345
437
|
|
|
346
438
|
const validCollections = collections
|
|
347
439
|
// Nullable slugs may cause one route to override the other
|
|
@@ -78,6 +78,7 @@ const offerToOrderItemInput = (
|
|
|
78
78
|
name: attachment.name,
|
|
79
79
|
content: attachment.value,
|
|
80
80
|
})),
|
|
81
|
+
...(offer.priceToken ? { priceToken: offer.priceToken } : {}),
|
|
81
82
|
})
|
|
82
83
|
|
|
83
84
|
const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer[]> =>
|
|
@@ -94,7 +95,11 @@ const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer[]> =>
|
|
|
94
95
|
}, new Map<string, IStoreOffer[]>())
|
|
95
96
|
|
|
96
97
|
const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
|
|
97
|
-
|
|
98
|
+
// Omit priceToken: it exists on the browser payload but not on orderForm items.
|
|
99
|
+
const pick = (
|
|
100
|
+
{ priceToken: _, ...item }: Indexed<IStoreOffer>,
|
|
101
|
+
index: number
|
|
102
|
+
) => ({
|
|
98
103
|
...item,
|
|
99
104
|
itemOffered: {
|
|
100
105
|
sku: item.itemOffered.sku,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Product collection type. Possible values are `Department`, `Category`, `Brand
|
|
2
|
+
Product collection type. Possible values are `Department`, `Category`, `Brand` or `Collection`.
|
|
3
|
+
|
|
4
|
+
`SubCategory` and `Cluster` are still declared for backward compatibility but are
|
|
5
|
+
deprecated and never returned.
|
|
3
6
|
"""
|
|
4
7
|
enum StoreCollectionType {
|
|
5
8
|
"""
|
|
@@ -12,16 +15,26 @@ enum StoreCollectionType {
|
|
|
12
15
|
Category
|
|
13
16
|
"""
|
|
14
17
|
Third level of product categorization.
|
|
18
|
+
|
|
19
|
+
Deprecated: never returned — third-level categories resolve as `Category`.
|
|
15
20
|
"""
|
|
16
21
|
SubCategory
|
|
22
|
+
@deprecated(
|
|
23
|
+
reason: "Never returned since the by-linkid migration: the category response only exposes `fatherCategoryId`, which distinguishes root from non-root but not tree depth, so third-level categories resolve as `Category`. Scheduled for removal in the next major."
|
|
24
|
+
)
|
|
17
25
|
"""
|
|
18
26
|
Product brand.
|
|
19
27
|
"""
|
|
20
28
|
Brand
|
|
21
29
|
"""
|
|
22
30
|
Product cluster.
|
|
31
|
+
|
|
32
|
+
Deprecated: never returned — clusters resolve as `Collection`.
|
|
23
33
|
"""
|
|
24
34
|
Cluster
|
|
35
|
+
@deprecated(
|
|
36
|
+
reason: "Never returned since the by-linkid migration: clusters and curated collections are both served by `collection/by-linkid`, whose response carries no discriminator between them, so both resolve as `Collection`. Scheduled for removal in the next major."
|
|
37
|
+
)
|
|
25
38
|
"""
|
|
26
39
|
Product collection.
|
|
27
40
|
"""
|
|
@@ -80,4 +93,23 @@ type StoreCollection {
|
|
|
80
93
|
Collection type.
|
|
81
94
|
"""
|
|
82
95
|
type: StoreCollectionType!
|
|
96
|
+
"""
|
|
97
|
+
Localized versions of this collection for all available locales.
|
|
98
|
+
Only populated when localization is enabled.
|
|
99
|
+
"""
|
|
100
|
+
otherLocales: [StoreCollectionLocale!]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
Localized collection data for a specific locale.
|
|
105
|
+
"""
|
|
106
|
+
type StoreCollectionLocale {
|
|
107
|
+
"""
|
|
108
|
+
Locale code (e.g. "pt-BR", "it-IT").
|
|
109
|
+
"""
|
|
110
|
+
locale: String!
|
|
111
|
+
"""
|
|
112
|
+
Localized collection slug (e.g. "vestuario/camisetas").
|
|
113
|
+
"""
|
|
114
|
+
slug: String!
|
|
83
115
|
}
|
|
@@ -54,6 +54,12 @@ type StoreOffer {
|
|
|
54
54
|
Whether this offer is a gift (e.g. free promotional item).
|
|
55
55
|
"""
|
|
56
56
|
isGift: Boolean
|
|
57
|
+
"""
|
|
58
|
+
Signed price token (JWT) used by the Pricing Fallback feature. When present, it should be
|
|
59
|
+
forwarded to Checkout on add-to-cart so the platform can trust the offer's price even if
|
|
60
|
+
the Pricing System is unavailable. May be null when the signing step fails or the feature is disabled.
|
|
61
|
+
"""
|
|
62
|
+
priceToken: String
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
"""
|
|
@@ -80,4 +86,8 @@ input IStoreOffer {
|
|
|
80
86
|
Number of items offered.
|
|
81
87
|
"""
|
|
82
88
|
quantity: Int!
|
|
89
|
+
"""
|
|
90
|
+
Signed price token (JWT) from the Pricing Fallback feature, forwarded to Checkout on add-to-cart.
|
|
91
|
+
"""
|
|
92
|
+
priceToken: String
|
|
83
93
|
}
|
|
@@ -94,6 +94,25 @@ type StoreProduct {
|
|
|
94
94
|
Delivery Promise product's badge.
|
|
95
95
|
"""
|
|
96
96
|
deliveryPromiseBadges: [DeliveryPromiseBadge]
|
|
97
|
+
"""
|
|
98
|
+
Localized versions of this product for all available locales.
|
|
99
|
+
Only populated when localization is enabled.
|
|
100
|
+
"""
|
|
101
|
+
otherLocales: [StoreProductLocale!]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
Localized product data for a specific locale.
|
|
106
|
+
"""
|
|
107
|
+
type StoreProductLocale {
|
|
108
|
+
"""
|
|
109
|
+
Locale code (e.g. "pt-BR", "it-IT").
|
|
110
|
+
"""
|
|
111
|
+
locale: String!
|
|
112
|
+
"""
|
|
113
|
+
Localized product slug including the SKU ID suffix (e.g. "adidas-polo-uomo-65").
|
|
114
|
+
"""
|
|
115
|
+
slug: String!
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
type SkuSpecification {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GraphqlContext } from '..'
|
|
2
|
+
|
|
3
|
+
export interface LocalizationConfig {
|
|
4
|
+
enabled?: boolean
|
|
5
|
+
defaultLocale?: string
|
|
6
|
+
locales?: Record<string, unknown>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getLocalizationConfig = (
|
|
10
|
+
ctx: GraphqlContext
|
|
11
|
+
): LocalizationConfig =>
|
|
12
|
+
(ctx.discoveryConfig as { localization?: LocalizationConfig } | undefined)
|
|
13
|
+
?.localization ?? {}
|
|
14
|
+
|
|
15
|
+
export const isLocalizationEnabled = (ctx: GraphqlContext): boolean =>
|
|
16
|
+
getLocalizationConfig(ctx).enabled === true
|
|
17
|
+
|
|
18
|
+
export const getConfiguredLocales = (ctx: GraphqlContext): string[] =>
|
|
19
|
+
Object.keys(getLocalizationConfig(ctx).locales ?? {})
|
|
20
|
+
|
|
21
|
+
export const getDefaultLocale = (ctx: GraphqlContext): string | undefined =>
|
|
22
|
+
getLocalizationConfig(ctx).defaultLocale
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether `locale` is one of the store's configured locales. Used to validate
|
|
26
|
+
* client-supplied locale input before propagating it to downstream platform
|
|
27
|
+
* APIs. Returns false when localization is disabled (no configured locales).
|
|
28
|
+
*/
|
|
29
|
+
export const isConfiguredLocale = (
|
|
30
|
+
ctx: GraphqlContext,
|
|
31
|
+
locale: string
|
|
32
|
+
): boolean => getConfiguredLocales(ctx).includes(locale)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Locale to forward to Catalog by-linkid endpoints via the Accept-Language
|
|
36
|
+
* header so they return localized name/title/description. Returns the active
|
|
37
|
+
* locale only when localization is enabled; otherwise `undefined`, so
|
|
38
|
+
* non-localized stores send no header and falls back to the store's default
|
|
39
|
+
* registered language (legacy behavior).
|
|
40
|
+
*
|
|
41
|
+
* Read at request time (not client construction) because `ctx.storage.locale`
|
|
42
|
+
* is mutated later by the collection resolver.
|
|
43
|
+
*/
|
|
44
|
+
export const getCatalogLocale = (ctx: GraphqlContext): string | undefined =>
|
|
45
|
+
isLocalizationEnabled(ctx) ? ctx.storage.locale || undefined : undefined
|