@faststore/api 4.5.0-dev.2 → 4.5.0-dev.4
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 +95 -44
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.mjs +1088 -837
- package/dist/es/index.mjs.map +1 -1
- package/dist/src/__generated__/schema.d.ts +44 -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/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/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/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/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 +46 -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/index.ts +3 -0
- package/src/platforms/vtex/clients/search/index.ts +16 -4
- 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/product.ts +160 -11
- package/src/platforms/vtex/resolvers/query.ts +148 -56
- package/src/platforms/vtex/typeDefs/collection.graphql +33 -1
- package/src/platforms/vtex/typeDefs/product.graphql +19 -0
- package/src/platforms/vtex/utils/localization.ts +45 -0
|
@@ -1347,6 +1347,11 @@ export type StoreCollection = {
|
|
|
1347
1347
|
id: Scalars['ID']['output'];
|
|
1348
1348
|
/** Collection meta information. Used for search. */
|
|
1349
1349
|
meta: StoreCollectionMeta;
|
|
1350
|
+
/**
|
|
1351
|
+
* Localized versions of this collection for all available locales.
|
|
1352
|
+
* Only populated when localization is enabled.
|
|
1353
|
+
*/
|
|
1354
|
+
otherLocales?: Maybe<Array<StoreCollectionLocale>>;
|
|
1350
1355
|
/** Meta tag data. */
|
|
1351
1356
|
seo: StoreSeo;
|
|
1352
1357
|
/** Corresponding collection URL slug, with which to retrieve this entity. */
|
|
@@ -1382,6 +1387,15 @@ export type StoreCollectionFacet = {
|
|
|
1382
1387
|
value: Scalars['String']['output'];
|
|
1383
1388
|
};
|
|
1384
1389
|
|
|
1390
|
+
/** Localized collection data for a specific locale. */
|
|
1391
|
+
export type StoreCollectionLocale = {
|
|
1392
|
+
__typename?: 'StoreCollectionLocale';
|
|
1393
|
+
/** Locale code (e.g. "pt-BR", "it-IT"). */
|
|
1394
|
+
locale: Scalars['String']['output'];
|
|
1395
|
+
/** Localized collection slug (e.g. "vestuario/camisetas"). */
|
|
1396
|
+
slug: Scalars['String']['output'];
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1385
1399
|
/** Collection meta information. Used for search. */
|
|
1386
1400
|
export type StoreCollectionMeta = {
|
|
1387
1401
|
__typename?: 'StoreCollectionMeta';
|
|
@@ -1389,19 +1403,34 @@ export type StoreCollectionMeta = {
|
|
|
1389
1403
|
selectedFacets: Array<StoreCollectionFacet>;
|
|
1390
1404
|
};
|
|
1391
1405
|
|
|
1392
|
-
/**
|
|
1406
|
+
/**
|
|
1407
|
+
* Product collection type. Possible values are `Department`, `Category`, `Brand` or `Collection`.
|
|
1408
|
+
*
|
|
1409
|
+
* `SubCategory` and `Cluster` are still declared for backward compatibility but are
|
|
1410
|
+
* deprecated and never returned.
|
|
1411
|
+
*/
|
|
1393
1412
|
export const enum StoreCollectionType {
|
|
1394
1413
|
/** Product brand. */
|
|
1395
1414
|
Brand = 'Brand',
|
|
1396
1415
|
/** Second level of product categorization. */
|
|
1397
1416
|
Category = 'Category',
|
|
1398
|
-
/**
|
|
1417
|
+
/**
|
|
1418
|
+
* Product cluster.
|
|
1419
|
+
*
|
|
1420
|
+
* Deprecated: never returned — clusters resolve as `Collection`.
|
|
1421
|
+
* @deprecated 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.
|
|
1422
|
+
*/
|
|
1399
1423
|
Cluster = 'Cluster',
|
|
1400
1424
|
/** Product collection. */
|
|
1401
1425
|
Collection = 'Collection',
|
|
1402
1426
|
/** First level of product categorization. */
|
|
1403
1427
|
Department = 'Department',
|
|
1404
|
-
/**
|
|
1428
|
+
/**
|
|
1429
|
+
* Third level of product categorization.
|
|
1430
|
+
*
|
|
1431
|
+
* Deprecated: never returned — third-level categories resolve as `Category`.
|
|
1432
|
+
* @deprecated 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.
|
|
1433
|
+
*/
|
|
1405
1434
|
SubCategory = 'SubCategory'
|
|
1406
1435
|
};
|
|
1407
1436
|
|
|
@@ -1688,6 +1717,11 @@ export type StoreProduct = {
|
|
|
1688
1717
|
name: Scalars['String']['output'];
|
|
1689
1718
|
/** Aggregate offer information. */
|
|
1690
1719
|
offers: StoreAggregateOffer;
|
|
1720
|
+
/**
|
|
1721
|
+
* Localized versions of this product for all available locales.
|
|
1722
|
+
* Only populated when localization is enabled.
|
|
1723
|
+
*/
|
|
1724
|
+
otherLocales?: Maybe<Array<StoreProductLocale>>;
|
|
1691
1725
|
/** Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs. */
|
|
1692
1726
|
productID: Scalars['String']['output'];
|
|
1693
1727
|
/** The product's release date. Formatted using https://en.wikipedia.org/wiki/ISO_8601 */
|
|
@@ -1752,6 +1786,15 @@ export type StoreProductGroup = {
|
|
|
1752
1786
|
skuVariants?: Maybe<SkuVariants>;
|
|
1753
1787
|
};
|
|
1754
1788
|
|
|
1789
|
+
/** Localized product data for a specific locale. */
|
|
1790
|
+
export type StoreProductLocale = {
|
|
1791
|
+
__typename?: 'StoreProductLocale';
|
|
1792
|
+
/** Locale code (e.g. "pt-BR", "it-IT"). */
|
|
1793
|
+
locale: Scalars['String']['output'];
|
|
1794
|
+
/** Localized product slug including the SKU ID suffix (e.g. "adidas-polo-uomo-65"). */
|
|
1795
|
+
slug: Scalars['String']['output'];
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1755
1798
|
/** Properties that can be associated with products and products groups. */
|
|
1756
1799
|
export type StorePropertyValue = {
|
|
1757
1800
|
__typename?: 'StorePropertyValue';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { fetchAPI } from '../fetch'
|
|
2
|
+
|
|
3
|
+
export interface LocalizedCategoryEntry {
|
|
4
|
+
id: number
|
|
5
|
+
name: string
|
|
6
|
+
/** Slash-separated IDs from root to this node, e.g. "9281/9285". Used to determine depth. */
|
|
7
|
+
fullPath: string
|
|
8
|
+
/** Slash-separated localized slugs from root to this node, e.g. "apparel/t-shirts". */
|
|
9
|
+
fullPathUriName: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LocalizedProductEntry {
|
|
13
|
+
linkId: string
|
|
14
|
+
/** All categories the product belongs to across all trees, as returned by Catalog Dataplane. */
|
|
15
|
+
categories: LocalizedCategoryEntry[]
|
|
16
|
+
/** Localized linkIds keyed by locale, covering all available locales in a single response. */
|
|
17
|
+
availableLinkIds: Record<string, string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LocalizedProductResponse {
|
|
21
|
+
id: number
|
|
22
|
+
linkId: string
|
|
23
|
+
name: string
|
|
24
|
+
/** Leaf category (deepest level) the product is registered under. */
|
|
25
|
+
category: LocalizedCategoryEntry | null
|
|
26
|
+
/** Full ancestry chain for every category tree the product belongs to. */
|
|
27
|
+
categories: LocalizedCategoryEntry[]
|
|
28
|
+
/** Localized linkIds keyed by locale, covering all available locales in a single response. */
|
|
29
|
+
availableLinkIds: Record<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Client for the VTEX Catalog Dataplane API.
|
|
34
|
+
* Uses Accept-Language header to return locale-specific product data.
|
|
35
|
+
*/
|
|
36
|
+
export const CatalogDataplane = ({ account, environment }: Options) => {
|
|
37
|
+
const base = `https://${account}.${environment}.com.br`
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
getLocalizedProduct: (
|
|
41
|
+
productId: string,
|
|
42
|
+
locale: string
|
|
43
|
+
): Promise<LocalizedProductResponse> =>
|
|
44
|
+
fetchAPI(`${base}/api/catalog-dataplane/product/${productId}`, {
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: {
|
|
47
|
+
'Accept-Language': locale,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type UserOrderCancel,
|
|
15
15
|
type UserOrderListResult,
|
|
16
16
|
} from '../../../..'
|
|
17
|
+
import { isNotFoundError } from '../../../errors'
|
|
17
18
|
import type { GraphqlContext } from '../../index'
|
|
18
19
|
import { getWithAppKeyAndToken } from '../../utils/auth'
|
|
19
20
|
import type { Channel } from '../../utils/channel'
|
|
@@ -26,6 +27,11 @@ import {
|
|
|
26
27
|
import type { ContractResponse } from './Contract'
|
|
27
28
|
import type { Address, AddressInput } from './types/Address'
|
|
28
29
|
import type { Brand } from './types/Brand'
|
|
30
|
+
import type {
|
|
31
|
+
ByLinkIdBrandResponse,
|
|
32
|
+
ByLinkIdCategoryResponse,
|
|
33
|
+
ByLinkIdCollectionResponse,
|
|
34
|
+
} from './types/ByLinkId'
|
|
29
35
|
import type { CategoryTree } from './types/CategoryTree'
|
|
30
36
|
import type { MasterDataResponse } from './types/Newsletter'
|
|
31
37
|
import type {
|
|
@@ -74,6 +80,24 @@ const BASE_INIT = {
|
|
|
74
80
|
},
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Encode a by-linkid path for the category endpoint. Category link paths can be
|
|
85
|
+
* multi-segment (e.g. "computer---software/eletronicos"); the Catalog endpoint
|
|
86
|
+
* expects the "/" separators to stay literal so it can validate each level,
|
|
87
|
+
* while each segment is individually URL-encoded. Running encodeURIComponent on
|
|
88
|
+
* the whole string would turn "/" into "%2F" and break multi-segment resolution.
|
|
89
|
+
*/
|
|
90
|
+
const encodeLinkIdPath = (linkId: string): string =>
|
|
91
|
+
linkId.split('/').map(encodeURIComponent).join('/')
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the fetch init that forwards a locale to a by-linkid endpoint. When no
|
|
95
|
+
* locale is provided the endpoint falls back to the store's default registered
|
|
96
|
+
* language (non-localized stores behavior).
|
|
97
|
+
*/
|
|
98
|
+
const byLinkIdInit = (locale?: string): RequestInit | undefined =>
|
|
99
|
+
locale ? { headers: { 'Accept-Language': locale } } : undefined
|
|
100
|
+
|
|
77
101
|
const QUOTE_ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
78
102
|
const QUOTE_VALID_STATUSES = new Set([
|
|
79
103
|
'Draft',
|
|
@@ -222,6 +246,53 @@ export const VtexCommerce = (
|
|
|
222
246
|
{ storeCookies }
|
|
223
247
|
),
|
|
224
248
|
},
|
|
249
|
+
byLinkId: {
|
|
250
|
+
// The by-linkid endpoints resolve a slug to a single catalog entity
|
|
251
|
+
// (or 404 when there is no match). We surface a 404 as `null` so the
|
|
252
|
+
// loader can cascade category → brand → collection.
|
|
253
|
+
category: async (
|
|
254
|
+
linkId: string,
|
|
255
|
+
locale?: string
|
|
256
|
+
): Promise<ByLinkIdCategoryResponse | null> => {
|
|
257
|
+
try {
|
|
258
|
+
return await fetchAPI(
|
|
259
|
+
`${base}/api/catalog_system/pub/category/by-linkid/${encodeLinkIdPath(linkId)}`,
|
|
260
|
+
byLinkIdInit(locale)
|
|
261
|
+
)
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (isNotFoundError(error)) return null
|
|
264
|
+
throw error
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
brand: async (
|
|
268
|
+
linkId: string,
|
|
269
|
+
locale?: string
|
|
270
|
+
): Promise<ByLinkIdBrandResponse | null> => {
|
|
271
|
+
try {
|
|
272
|
+
return await fetchAPI(
|
|
273
|
+
`${base}/api/catalog_system/pub/brand/by-linkid/${encodeURIComponent(linkId)}`,
|
|
274
|
+
byLinkIdInit(locale)
|
|
275
|
+
)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (isNotFoundError(error)) return null
|
|
278
|
+
throw error
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
collection: async (
|
|
282
|
+
linkId: string,
|
|
283
|
+
locale?: string
|
|
284
|
+
): Promise<ByLinkIdCollectionResponse | null> => {
|
|
285
|
+
try {
|
|
286
|
+
return await fetchAPI(
|
|
287
|
+
`${base}/api/catalog_system/pub/collection/by-linkid/${encodeURIComponent(linkId)}`,
|
|
288
|
+
byLinkIdInit(locale)
|
|
289
|
+
)
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (isNotFoundError(error)) return null
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
},
|
|
225
296
|
products: {
|
|
226
297
|
crossselling: ({
|
|
227
298
|
type,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ByLinkIdCategoryResponse {
|
|
2
|
+
id: number
|
|
3
|
+
fatherCategoryId: number | null
|
|
4
|
+
name: string
|
|
5
|
+
linkId: string
|
|
6
|
+
title: string | null
|
|
7
|
+
description: string | null
|
|
8
|
+
/**
|
|
9
|
+
* Optional SEO meta description. pagetype.metaTagDescription
|
|
10
|
+
* was backed by the same source as `description`; prefer `description` for
|
|
11
|
+
* parity when this field is absent.
|
|
12
|
+
*/
|
|
13
|
+
metaTagDescription: string | null
|
|
14
|
+
/** Localized linkIds keyed by locale. Null when no multilanguage entries are registered. */
|
|
15
|
+
availableLinkIds: Record<string, string> | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ByLinkIdBrandResponse {
|
|
19
|
+
id: number
|
|
20
|
+
name: string
|
|
21
|
+
linkId: string
|
|
22
|
+
title: string | null
|
|
23
|
+
description: string | null
|
|
24
|
+
/**
|
|
25
|
+
* Optional SEO meta description. Catalog confirmed pagetype.metaTagDescription
|
|
26
|
+
* was backed by the same source as `description`; prefer `description` for
|
|
27
|
+
* parity when this field is absent.
|
|
28
|
+
*/
|
|
29
|
+
metaTagDescription: string | null
|
|
30
|
+
availableLinkIds: Record<string, string> | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ByLinkIdCollectionResponse {
|
|
34
|
+
id: number
|
|
35
|
+
name: string
|
|
36
|
+
linkId: string | null
|
|
37
|
+
title: string | null
|
|
38
|
+
description: string | null
|
|
39
|
+
/**
|
|
40
|
+
* Optional SEO meta description. pagetype.metaTagDescription
|
|
41
|
+
* was backed by the same source as `description`; prefer `description` for
|
|
42
|
+
* parity when this field is absent.
|
|
43
|
+
*/
|
|
44
|
+
metaTagDescription: string | null
|
|
45
|
+
availableLinkIds: Record<string, string> | null
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GraphqlContext } from '..'
|
|
2
|
+
import { CatalogDataplane } from './catalog'
|
|
2
3
|
import { VtexCommerce } from './commerce'
|
|
3
4
|
import { IntelligentSearch } from './search'
|
|
4
5
|
|
|
@@ -7,9 +8,11 @@ export type Clients = ReturnType<typeof getClients>
|
|
|
7
8
|
export const getClients = (options: Options, ctx: GraphqlContext) => {
|
|
8
9
|
const search = IntelligentSearch(options, ctx)
|
|
9
10
|
const commerce = VtexCommerce(options, ctx)
|
|
11
|
+
const catalog = CatalogDataplane(options)
|
|
10
12
|
|
|
11
13
|
return {
|
|
12
14
|
search,
|
|
13
15
|
commerce,
|
|
16
|
+
catalog,
|
|
14
17
|
}
|
|
15
18
|
}
|
|
@@ -3,6 +3,7 @@ import pLimit from 'p-limit'
|
|
|
3
3
|
import type { GraphqlContext } from '../../'
|
|
4
4
|
import { getWithCookie } from '../../utils/cookies'
|
|
5
5
|
import type { SelectedFacet } from '../../utils/facets'
|
|
6
|
+
import { isLocalizationEnabled } from '../../utils/localization'
|
|
6
7
|
import {
|
|
7
8
|
buildIntelligentSearchRequest,
|
|
8
9
|
parseSegmentCookie,
|
|
@@ -80,10 +81,21 @@ function getRegionIdFromContext(ctx: GraphqlContext): string | undefined {
|
|
|
80
81
|
|
|
81
82
|
function getSegmentLocale(ctx: GraphqlContext): string {
|
|
82
83
|
const segment = parseSegmentCookie(ctx.headers?.cookie)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
84
|
+
const cultureInfo = (segment.cultureInfo as string | undefined) || ''
|
|
85
|
+
|
|
86
|
+
// Locale flows from the URL into IS in one chain:
|
|
87
|
+
// router.locale (Next.js)
|
|
88
|
+
// → selectedFacets locale facet (useLocalizedVariables in @faststore/core)
|
|
89
|
+
// → ctx.storage.locale (mutateLocaleContext in query.ts)
|
|
90
|
+
// → IS locale query param (here)
|
|
91
|
+
//
|
|
92
|
+
// For localized stores: prefer storage.locale over the vtex_segment cookie.
|
|
93
|
+
// The cookie can lag after locale navigation, while storage.locale is derived
|
|
94
|
+
// from the trusted selectedFacets locale facet in the same request.
|
|
95
|
+
|
|
96
|
+
return isLocalizationEnabled(ctx)
|
|
97
|
+
? ctx.storage.locale || cultureInfo
|
|
98
|
+
: cultureInfo || ctx.storage.locale || ''
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
export const IntelligentSearch = (
|
|
@@ -32,10 +32,22 @@ export interface GraphqlContext {
|
|
|
32
32
|
flags: FeatureFlags
|
|
33
33
|
searchArgs?: Omit<SearchArgs, 'type'>
|
|
34
34
|
cookies: Map<string, Record<string, string>>
|
|
35
|
+
/**
|
|
36
|
+
* Cached in-flight localized product lookups keyed by "productId:locale".
|
|
37
|
+
* Stores the promise (not just the resolved value) so concurrent sibling
|
|
38
|
+
* resolvers (slug validation, otherLocales, breadcrumb) dedupe to a single
|
|
39
|
+
* Catalog Dataplane request within the same request.
|
|
40
|
+
*/
|
|
41
|
+
productTranslationsCache?: Map<
|
|
42
|
+
string,
|
|
43
|
+
Promise<import('./clients/catalog').LocalizedProductEntry | null>
|
|
44
|
+
>
|
|
35
45
|
}
|
|
36
46
|
headers: Record<string, string>
|
|
37
47
|
account: string
|
|
38
48
|
OTEL: Record<string, unknown>
|
|
49
|
+
/** Discovery config passed from @faststore/core, including localization settings. */
|
|
50
|
+
discoveryConfig?: Record<string, unknown>
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
export const GraphqlVtexContextFactory = async (options: Options) => {
|
|
@@ -49,11 +61,14 @@ export const GraphqlVtexContextFactory = async (options: Options) => {
|
|
|
49
61
|
locale: options.locale,
|
|
50
62
|
cookies: new Map<string, Record<string, string>>(),
|
|
51
63
|
}
|
|
52
|
-
ctx.clients = getClients(options, ctx)
|
|
53
|
-
ctx.loaders = getLoaders(options, ctx)
|
|
54
64
|
ctx.account = options.account
|
|
55
65
|
ctx.OTEL = options.OTEL
|
|
56
66
|
ctx.discoveryConfig = options.discoveryConfig
|
|
67
|
+
// Build clients/loaders last: they capture `ctx` and read storage,
|
|
68
|
+
// discoveryConfig, etc. at request time, so everything they may depend on
|
|
69
|
+
// must already be assigned.
|
|
70
|
+
ctx.clients = getClients(options, ctx)
|
|
71
|
+
ctx.loaders = getLoaders(options, ctx)
|
|
57
72
|
|
|
58
73
|
return ctx
|
|
59
74
|
}
|
|
@@ -3,49 +3,123 @@ import pLimit from 'p-limit'
|
|
|
3
3
|
|
|
4
4
|
import { NotFoundError } from '../../errors'
|
|
5
5
|
import type { Clients } from '../clients'
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ByLinkIdBrandResponse,
|
|
8
|
+
ByLinkIdCategoryResponse,
|
|
9
|
+
ByLinkIdCollectionResponse,
|
|
10
|
+
} from '../clients/commerce/types/ByLinkId'
|
|
7
11
|
|
|
8
|
-
// Limits concurrent requests to 20 so that they don't timeout
|
|
9
12
|
const CONCURRENT_REQUESTS_MAX = 20
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Load key for the collection DataLoader. `locale` is captured at the call
|
|
16
|
+
* site (not re-read from mutable `ctx.storage`) so concurrent aliased
|
|
17
|
+
* `collection` fields with different locales cannot share a cache entry or
|
|
18
|
+
* race on Accept-Language.
|
|
19
|
+
*/
|
|
20
|
+
export type CollectionLoadKey = {
|
|
21
|
+
slug: string
|
|
22
|
+
/** Catalog Accept-Language; `undefined` when localization is disabled. */
|
|
23
|
+
locale?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ByLinkIdCategoryRoot = ByLinkIdCategoryResponse & {
|
|
27
|
+
entityType: 'category'
|
|
28
|
+
/**
|
|
29
|
+
* Full accumulated input slug injected by the loader (e.g. "vestuario/camisetas").
|
|
30
|
+
* Used by slugifyRoot so that meta.selectedFacets builds the correct facet keys
|
|
31
|
+
* without relying on root.url (which is absent in the by-linkid response).
|
|
32
|
+
*/
|
|
33
|
+
slug: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ByLinkIdBrandRoot = ByLinkIdBrandResponse & {
|
|
37
|
+
entityType: 'brand'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ByLinkIdCollectionRoot = ByLinkIdCollectionResponse & {
|
|
41
|
+
entityType: 'collection'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Root =
|
|
45
|
+
| ByLinkIdCategoryRoot
|
|
46
|
+
| ByLinkIdBrandRoot
|
|
47
|
+
| ByLinkIdCollectionRoot
|
|
48
|
+
|
|
49
|
+
export const isCategory = (root: Root): root is ByLinkIdCategoryRoot =>
|
|
50
|
+
root.entityType === 'category'
|
|
19
51
|
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
export const isBrand = (root: Root): root is ByLinkIdBrandRoot =>
|
|
53
|
+
root.entityType === 'brand'
|
|
54
|
+
|
|
55
|
+
export const isCollection = (root: Root): root is ByLinkIdCollectionRoot =>
|
|
56
|
+
root.entityType === 'collection'
|
|
23
57
|
|
|
24
58
|
export const getCollectionLoader = (_: Options, clients: Clients) => {
|
|
25
59
|
const limit = pLimit(CONCURRENT_REQUESTS_MAX)
|
|
26
60
|
|
|
27
|
-
const loader = async (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return Promise.all(
|
|
31
|
-
slugs.map((slug: string) =>
|
|
61
|
+
const loader = async (keys: readonly CollectionLoadKey[]): Promise<Root[]> =>
|
|
62
|
+
Promise.all(
|
|
63
|
+
keys.map((key) =>
|
|
32
64
|
limit(async () => {
|
|
33
|
-
|
|
65
|
+
// Normalize to lowercase for DataLoader cache-key consistency
|
|
66
|
+
// (e.g. "Sporting" and "sporting" are the same). The by-linkid API is
|
|
67
|
+
// case-insensitive by design (it preserves the legacy pagetype behavior).
|
|
68
|
+
// Accents are significant and are preserved by toLowerCase() (e.g. "vestuário").
|
|
69
|
+
const normalizedSlug = key.slug.toLowerCase()
|
|
70
|
+
// Use the locale captured on the load key — do not re-read
|
|
71
|
+
// ctx.storage.locale here (it can change mid-request).
|
|
72
|
+
const locale = key.locale
|
|
73
|
+
|
|
74
|
+
// Step 1: category
|
|
75
|
+
// Pass the full path so the API validates each segment and returns only
|
|
76
|
+
// the unambiguous leaf category. E.g. "vestuario/camisetas" resolves to
|
|
77
|
+
// the "camisetas" that is a child of "vestuario", not any other category
|
|
78
|
+
// that happens to share the linkId "camisetas".
|
|
79
|
+
// The full slug is also injected into the result for meta.selectedFacets
|
|
80
|
+
// and breadcrumb URL construction.
|
|
81
|
+
const category = await clients.commerce.catalog.byLinkId.category(
|
|
82
|
+
normalizedSlug,
|
|
83
|
+
locale
|
|
84
|
+
)
|
|
85
|
+
if (category) {
|
|
86
|
+
return {
|
|
87
|
+
...category,
|
|
88
|
+
entityType: 'category' as const,
|
|
89
|
+
slug: normalizedSlug,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
34
92
|
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
// Step 2: brand (always single-segment)
|
|
94
|
+
const brand = await clients.commerce.catalog.byLinkId.brand(
|
|
95
|
+
normalizedSlug,
|
|
96
|
+
locale
|
|
97
|
+
)
|
|
98
|
+
if (brand) {
|
|
99
|
+
return { ...brand, entityType: 'brand' as const }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 3: collection cluster (always single-segment)
|
|
103
|
+
const collection = await clients.commerce.catalog.byLinkId.collection(
|
|
104
|
+
normalizedSlug,
|
|
105
|
+
locale
|
|
106
|
+
)
|
|
107
|
+
if (collection) {
|
|
108
|
+
return { ...collection, entityType: 'collection' as const }
|
|
37
109
|
}
|
|
38
110
|
|
|
39
111
|
throw new NotFoundError(
|
|
40
|
-
`
|
|
112
|
+
`No catalog entity found for slug: ${key.slug}. Cascade exhausted (category → brand → collection).`
|
|
41
113
|
)
|
|
42
114
|
})
|
|
43
115
|
)
|
|
44
116
|
)
|
|
45
|
-
}
|
|
46
117
|
|
|
47
|
-
return new DataLoader<
|
|
48
|
-
// DataLoader is
|
|
118
|
+
return new DataLoader<CollectionLoadKey, Root, string>(loader, {
|
|
119
|
+
// DataLoader is used for caching, not batching
|
|
49
120
|
batch: false,
|
|
121
|
+
// Dedup by lowercased slug + captured locale so casing variants share an
|
|
122
|
+
// entry while same-slug / different-locale loads stay isolated.
|
|
123
|
+
cacheKeyFn: ({ slug, locale }) => `${slug.toLowerCase()}::${locale ?? ''}`,
|
|
50
124
|
})
|
|
51
125
|
}
|
|
@@ -6,7 +6,8 @@ import { getSkuLoader } from './sku'
|
|
|
6
6
|
|
|
7
7
|
export type Loaders = ReturnType<typeof getLoaders>
|
|
8
8
|
|
|
9
|
-
export const getLoaders = (options: Options,
|
|
9
|
+
export const getLoaders = (options: Options, ctx: GraphqlContext) => {
|
|
10
|
+
const { clients } = ctx
|
|
10
11
|
const skuLoader = getSkuLoader(options, clients)
|
|
11
12
|
const simulationLoader = getSimulationLoader(options, clients)
|
|
12
13
|
const collectionLoader = getCollectionLoader(options, clients)
|