@faststore/api 1.12.48 → 1.12.51
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/api.cjs.development.js +203 -16
- 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 +203 -16
- package/dist/api.esm.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/platforms/vtex/clients/fetch.d.ts +5 -1
- package/dist/platforms/vtex/clients/index.d.ts +3 -3
- package/dist/platforms/vtex/clients/search/index.d.ts +2 -0
- package/dist/platforms/vtex/index.d.ts +7 -2
- package/dist/platforms/vtex/resolvers/query.d.ts +1 -1
- package/dist/platforms/vtex/utils/cookies.d.ts +52 -0
- package/dist/platforms/vtex/utils/facets.d.ts +1 -0
- package/package.json +2 -2
- package/src/platforms/vtex/clients/commerce/index.ts +47 -4
- package/src/platforms/vtex/clients/fetch.ts +13 -1
- package/src/platforms/vtex/clients/search/index.ts +15 -6
- package/src/platforms/vtex/index.ts +6 -0
- package/src/platforms/vtex/loaders/sku.ts +5 -1
- package/src/platforms/vtex/resolvers/query.ts +3 -0
- package/src/platforms/vtex/resolvers/validateCart.ts +1 -1
- package/src/platforms/vtex/utils/cookies.ts +157 -0
- package/src/platforms/vtex/utils/facets.ts +3 -0
package/dist/index.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export declare const getResolvers: (options: Options) => {
|
|
|
73
73
|
attachmentsValues?: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[] | undefined;
|
|
74
74
|
}, unknown, any>>;
|
|
75
75
|
StoreSearchResult: Record<string, import("./platforms/vtex").Resolver<{
|
|
76
|
-
searchArgs: Pick<import("./platforms/vtex/clients/search").SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy">;
|
|
76
|
+
searchArgs: Pick<import("./platforms/vtex/clients/search").SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy" | "showInvisibleItems" | "regionId">;
|
|
77
77
|
productSearchPromise: Promise<import("./platforms/vtex/clients/search/types/ProductSearchResult").ProductSearchResult>;
|
|
78
78
|
}, unknown, any>>;
|
|
79
79
|
StorePropertyValue: Record<string, import("./platforms/vtex").Resolver<import("./__generated__/schema").IStorePropertyValue, unknown, any>>;
|
|
@@ -93,7 +93,7 @@ export declare const getResolvers: (options: Options) => {
|
|
|
93
93
|
product: (_: unknown, { locator }: import("./__generated__/schema").QueryProductArgs, ctx: import("./platforms/vtex").Context) => Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
|
|
94
94
|
collection: (_: unknown, { slug }: import("./__generated__/schema").QueryCollectionArgs, ctx: import("./platforms/vtex").Context) => Promise<import("./platforms/vtex/clients/commerce/types/Portal").CollectionPageType>;
|
|
95
95
|
search: (_: unknown, { first, after: maybeAfter, sort, term, selectedFacets }: import("./__generated__/schema").QuerySearchArgs, ctx: import("./platforms/vtex").Context) => Promise<{
|
|
96
|
-
searchArgs: Pick<import("./platforms/vtex/clients/search").SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy">;
|
|
96
|
+
searchArgs: Pick<import("./platforms/vtex/clients/search").SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy" | "showInvisibleItems" | "regionId">;
|
|
97
97
|
productSearchPromise: Promise<import("./platforms/vtex/clients/search/types/ProductSearchResult").ProductSearchResult>;
|
|
98
98
|
}>;
|
|
99
99
|
allProducts: (_: unknown, { first, after: maybeAfter }: import("./__generated__/schema").QueryAllProductsArgs, ctx: import("./platforms/vtex").Context) => Promise<{
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
interface FetchAPIOptions {
|
|
2
|
+
storeCookies?: (headers: Headers) => void;
|
|
3
|
+
}
|
|
4
|
+
export declare const fetchAPI: (info: RequestInfo, init?: RequestInit | undefined, options?: FetchAPIOptions | undefined) => Promise<any>;
|
|
5
|
+
export {};
|
|
@@ -2,9 +2,9 @@ import type { Context, Options } from '..';
|
|
|
2
2
|
export declare type Clients = ReturnType<typeof getClients>;
|
|
3
3
|
export declare const getClients: (options: Options, ctx: Context) => {
|
|
4
4
|
search: {
|
|
5
|
-
facets: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">) => Promise<import("./search/types/FacetSearchResult").FacetSearchResult>;
|
|
6
|
-
products: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">) => Promise<import("./search/types/ProductSearchResult").ProductSearchResult>;
|
|
7
|
-
suggestedTerms: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">) => Promise<import("./search/types/ProductSearchResult").Suggestion>;
|
|
5
|
+
facets: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy" | "showInvisibleItems" | "regionId">) => Promise<import("./search/types/FacetSearchResult").FacetSearchResult>;
|
|
6
|
+
products: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy" | "showInvisibleItems" | "regionId">) => Promise<import("./search/types/ProductSearchResult").ProductSearchResult>;
|
|
7
|
+
suggestedTerms: (args: Pick<import("./search").SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy" | "showInvisibleItems" | "regionId">) => Promise<import("./search/types/ProductSearchResult").Suggestion>;
|
|
8
8
|
topSearches: () => Promise<import("./search/types/ProductSearchResult").Suggestion>;
|
|
9
9
|
};
|
|
10
10
|
commerce: {
|
|
@@ -28,6 +28,11 @@ export interface Context {
|
|
|
28
28
|
locale: string;
|
|
29
29
|
flags: FeatureFlags;
|
|
30
30
|
searchArgs?: Omit<SearchArgs, 'type'>;
|
|
31
|
+
/**
|
|
32
|
+
* Checkout cookies received while resolving this request, keyed by cookie
|
|
33
|
+
* name. The GraphQL handler forwards `setCookie` values to the browser.
|
|
34
|
+
*/
|
|
35
|
+
cookies: Map<string, Record<string, string>>;
|
|
31
36
|
};
|
|
32
37
|
headers: Record<string, string>;
|
|
33
38
|
}
|
|
@@ -100,7 +105,7 @@ export declare const getResolvers: (_: Options) => {
|
|
|
100
105
|
attachmentsValues?: import("./clients/commerce/types/OrderForm").Attachment[] | undefined;
|
|
101
106
|
}, unknown, any>>;
|
|
102
107
|
StoreSearchResult: Record<string, Resolver<{
|
|
103
|
-
searchArgs: Pick<SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">;
|
|
108
|
+
searchArgs: Pick<SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy" | "showInvisibleItems" | "regionId">;
|
|
104
109
|
productSearchPromise: Promise<import("./clients/search/types/ProductSearchResult").ProductSearchResult>;
|
|
105
110
|
}, unknown, any>>;
|
|
106
111
|
StorePropertyValue: Record<string, Resolver<import("../..").IStorePropertyValue, unknown, any>>;
|
|
@@ -120,7 +125,7 @@ export declare const getResolvers: (_: Options) => {
|
|
|
120
125
|
product: (_: unknown, { locator }: import("../..").QueryProductArgs, ctx: Context) => Promise<import("./utils/enhanceSku").EnhancedSku>;
|
|
121
126
|
collection: (_: unknown, { slug }: import("../..").QueryCollectionArgs, ctx: Context) => Promise<import("./clients/commerce/types/Portal").CollectionPageType>;
|
|
122
127
|
search: (_: unknown, { first, after: maybeAfter, sort, term, selectedFacets }: import("../..").QuerySearchArgs, ctx: Context) => Promise<{
|
|
123
|
-
searchArgs: Pick<SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy">;
|
|
128
|
+
searchArgs: Pick<SearchArgs, "hideUnavailableItems" | "query" | "page" | "count" | "sort" | "selectedFacets" | "fuzzy" | "showInvisibleItems" | "regionId">;
|
|
124
129
|
productSearchPromise: Promise<import("./clients/search/types/ProductSearchResult").ProductSearchResult>;
|
|
125
130
|
}>;
|
|
126
131
|
allProducts: (_: unknown, { first, after: maybeAfter }: import("../..").QueryAllProductsArgs, ctx: Context) => Promise<{
|
|
@@ -6,7 +6,7 @@ export declare const Query: {
|
|
|
6
6
|
product: (_: unknown, { locator }: QueryProductArgs, ctx: Context) => Promise<import("../utils/enhanceSku").EnhancedSku>;
|
|
7
7
|
collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => Promise<import("../clients/commerce/types/Portal").CollectionPageType>;
|
|
8
8
|
search: (_: unknown, { first, after: maybeAfter, sort, term, selectedFacets }: QuerySearchArgs, ctx: Context) => Promise<{
|
|
9
|
-
searchArgs: Pick<SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy">;
|
|
9
|
+
searchArgs: Pick<SearchArgs, "sort" | "selectedFacets" | "hideUnavailableItems" | "query" | "page" | "count" | "fuzzy" | "showInvisibleItems" | "regionId">;
|
|
10
10
|
productSearchPromise: Promise<import("../clients/search/types/ProductSearchResult").ProductSearchResult>;
|
|
11
11
|
}>;
|
|
12
12
|
allProducts: (_: unknown, { first, after: maybeAfter }: QueryAllProductsArgs, ctx: Context) => Promise<{
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Context } from '../index';
|
|
2
|
+
export interface ContextForCookies {
|
|
3
|
+
headers: Context['headers'];
|
|
4
|
+
storage: Pick<Context['storage'], 'cookies'>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns every `set-cookie` value of a fetch response.
|
|
8
|
+
*
|
|
9
|
+
* `Headers.getSetCookie()` only exists on Node >= 18.14 (undici). FastStore v1
|
|
10
|
+
* stores may still run on Node 16, where isomorphic-unfetch falls back to
|
|
11
|
+
* node-fetch v2, which exposes repeated headers through `raw()` instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare const getSetCookies: (headers: Headers) => string[];
|
|
14
|
+
/**
|
|
15
|
+
* This function updates the ctx.storage.cookies, that is used in each request.
|
|
16
|
+
*
|
|
17
|
+
* ctx.storage.cookies is a Map<string[1], Record<string, string>[2]> where,
|
|
18
|
+
* [1] cookie key
|
|
19
|
+
* [2] {
|
|
20
|
+
* value: cookie value,
|
|
21
|
+
* setCookie: setCookie used in browser response
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export declare const updatesContextStorageCookies: (ctx: Pick<Context, 'storage'>, setCookieValue: string) => void;
|
|
25
|
+
export declare const setCookie: (ctx: Pick<Context, 'storage'>, headers: Headers) => void;
|
|
26
|
+
export declare const getStoreCookie: (ctx: Pick<Context, 'storage'>) => (headers: Headers) => void;
|
|
27
|
+
/**
|
|
28
|
+
* This function returns a modified copy of the original cookie header (ctx.headers.cookie from the first request)
|
|
29
|
+
* with the cookie values that comes in each request (ctx.storage.cookies).
|
|
30
|
+
* If there is no cookies in storage, the ctx.headers?.cookie is used
|
|
31
|
+
*
|
|
32
|
+
* ctx.storage.cookies is a Map<string[1], Record<string, string>[2]> where,
|
|
33
|
+
* [1] cookie key
|
|
34
|
+
* [2] {
|
|
35
|
+
* value: cookie value,
|
|
36
|
+
* setCookie: setCookie used in browser response
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
export declare const getUpdatedCookie: (ctx: ContextForCookies) => string | null;
|
|
40
|
+
export declare const getWithCookie: (ctx: ContextForCookies) => <T extends Record<string, any>>(headers: T) => T | (T & {
|
|
41
|
+
cookie: string;
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* This function updates the cookie value based on its key
|
|
45
|
+
*
|
|
46
|
+
* const existingCookies = 'key=value1; key2=; key3=value3';
|
|
47
|
+
* const storageCookieKey = 'key2';
|
|
48
|
+
* const storageCookieValue = 'value2'
|
|
49
|
+
*
|
|
50
|
+
* updatesCookieValueByKey(existingCookies, storageCookieKey, storageCookieValue) returns 'key=value1; key2=value2; key3=value3';
|
|
51
|
+
*/
|
|
52
|
+
export declare const updatesCookieValueByKey: (existingCookies: string, storageCookieKey: string, storageCookieValue: string) => string;
|
|
@@ -32,3 +32,4 @@ export declare const findSlug: (facets?: SelectedFacet[] | null | undefined) =>
|
|
|
32
32
|
export declare const findSkuId: (facets?: SelectedFacet[] | null | undefined) => string | null;
|
|
33
33
|
export declare const findLocale: (facets?: SelectedFacet[] | null | undefined) => string | null;
|
|
34
34
|
export declare const findChannel: (facets?: SelectedFacet[] | null | undefined) => string | null;
|
|
35
|
+
export declare const findRegionId: (facets?: SelectedFacet[] | null | undefined) => string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/api",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.51",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"@envelop/core": "^1 || ^2",
|
|
54
54
|
"graphql": "^15.6.0"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "6f9ba2786d82b074125318339deac2ea456fb3d9"
|
|
57
57
|
}
|
|
@@ -19,6 +19,7 @@ import type { SalesChannel } from './types/SalesChannel'
|
|
|
19
19
|
import { MasterDataResponse } from './types/Newsletter'
|
|
20
20
|
import type { Address, AddressInput } from './types/Address'
|
|
21
21
|
import { ShippingDataBody } from './types/ShippingData'
|
|
22
|
+
import { getStoreCookie, getWithCookie } from '../../utils/cookies'
|
|
22
23
|
|
|
23
24
|
type ValueOf<T> = T extends Record<string, infer K> ? K : never
|
|
24
25
|
|
|
@@ -29,11 +30,43 @@ const BASE_INIT = {
|
|
|
29
30
|
},
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const getHeader = (headers: Context['headers'] | undefined, name: string) => {
|
|
34
|
+
const value: string | string[] | undefined = (headers as any)?.[name]
|
|
35
|
+
|
|
36
|
+
return Array.isArray(value) ? value[0] : value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Host the Checkout API uses as the domain of the cookies it sets. Checkout
|
|
41
|
+
* only honors it for hosts registered in the account, so a forged value is
|
|
42
|
+
* ignored there.
|
|
43
|
+
*/
|
|
44
|
+
const getForwardedHost = (headers: Context['headers'] | undefined) => {
|
|
45
|
+
// Proxies may append values: "client, proxy". Only the first one is the store host.
|
|
46
|
+
const host =
|
|
47
|
+
getHeader(headers, 'x-forwarded-host')?.split(',')[0].trim() ||
|
|
48
|
+
getHeader(headers, 'host') ||
|
|
49
|
+
''
|
|
50
|
+
|
|
51
|
+
return host.trim().replace(/^www\./, '')
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
export const VtexCommerce = (
|
|
33
55
|
{ account, environment }: Options,
|
|
34
56
|
ctx: Context
|
|
35
57
|
) => {
|
|
36
58
|
const base = `https://${account}.${environment}.com.br`
|
|
59
|
+
const storeCookies = getStoreCookie(ctx)
|
|
60
|
+
const withCookie = getWithCookie(ctx)
|
|
61
|
+
const forwardedHost = getForwardedHost(ctx.headers)
|
|
62
|
+
|
|
63
|
+
// Checkout calls forward the shopper cookies and keep the cookies Checkout
|
|
64
|
+
// sets (e.g. CheckoutOrderFormOwnership) so they can reach the browser.
|
|
65
|
+
const checkoutHeaders = () =>
|
|
66
|
+
withCookie({
|
|
67
|
+
'content-type': 'application/json',
|
|
68
|
+
'X-FORWARDED-HOST': forwardedHost,
|
|
69
|
+
})
|
|
37
70
|
|
|
38
71
|
return {
|
|
39
72
|
catalog: {
|
|
@@ -107,8 +140,10 @@ export const VtexCommerce = (
|
|
|
107
140
|
`${base}/api/checkout/pub/orderForm/${id}/attachments/shippingData`,
|
|
108
141
|
{
|
|
109
142
|
...BASE_INIT,
|
|
143
|
+
headers: checkoutHeaders(),
|
|
110
144
|
body: JSON.stringify(body),
|
|
111
|
-
}
|
|
145
|
+
},
|
|
146
|
+
{ storeCookies }
|
|
112
147
|
)
|
|
113
148
|
},
|
|
114
149
|
orderForm: ({
|
|
@@ -128,7 +163,11 @@ export const VtexCommerce = (
|
|
|
128
163
|
|
|
129
164
|
return fetchAPI(
|
|
130
165
|
`${base}/api/checkout/pub/orderForm/${id}?${params.toString()}`,
|
|
131
|
-
|
|
166
|
+
{
|
|
167
|
+
...BASE_INIT,
|
|
168
|
+
headers: checkoutHeaders(),
|
|
169
|
+
},
|
|
170
|
+
{ storeCookies }
|
|
132
171
|
)
|
|
133
172
|
},
|
|
134
173
|
updateOrderFormItems: ({
|
|
@@ -153,12 +192,14 @@ export const VtexCommerce = (
|
|
|
153
192
|
`${base}/api/checkout/pub/orderForm/${id}/items?${params}`,
|
|
154
193
|
{
|
|
155
194
|
...BASE_INIT,
|
|
195
|
+
headers: checkoutHeaders(),
|
|
156
196
|
body: JSON.stringify({
|
|
157
197
|
orderItems,
|
|
158
198
|
noSplitItem: !shouldSplitItem,
|
|
159
199
|
}),
|
|
160
200
|
method: 'PATCH',
|
|
161
|
-
}
|
|
201
|
+
},
|
|
202
|
+
{ storeCookies }
|
|
162
203
|
)
|
|
163
204
|
},
|
|
164
205
|
setCustomData: ({
|
|
@@ -176,9 +217,11 @@ export const VtexCommerce = (
|
|
|
176
217
|
`${base}/api/checkout/pub/orderForm/${id}/customData/${appId}/${key}`,
|
|
177
218
|
{
|
|
178
219
|
...BASE_INIT,
|
|
220
|
+
headers: checkoutHeaders(),
|
|
179
221
|
body: JSON.stringify({ value }),
|
|
180
222
|
method: 'PUT',
|
|
181
|
-
}
|
|
223
|
+
},
|
|
224
|
+
{ storeCookies }
|
|
182
225
|
)
|
|
183
226
|
},
|
|
184
227
|
region: async ({
|
|
@@ -3,7 +3,15 @@ import packageJson from '../../../../package.json'
|
|
|
3
3
|
|
|
4
4
|
const USER_AGENT = `${packageJson.name}@${packageJson.version}`
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface FetchAPIOptions {
|
|
7
|
+
storeCookies?: (headers: Headers) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const fetchAPI = async (
|
|
11
|
+
info: RequestInfo,
|
|
12
|
+
init?: RequestInit,
|
|
13
|
+
options?: FetchAPIOptions
|
|
14
|
+
) => {
|
|
7
15
|
const response = await fetch(info, {
|
|
8
16
|
...init,
|
|
9
17
|
headers: {
|
|
@@ -13,6 +21,10 @@ export const fetchAPI = async (info: RequestInfo, init?: RequestInit) => {
|
|
|
13
21
|
})
|
|
14
22
|
|
|
15
23
|
if (response.ok) {
|
|
24
|
+
if (options?.storeCookies) {
|
|
25
|
+
options.storeCookies(response.headers)
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
return response.status !== 204 ? response.json() : undefined
|
|
17
29
|
}
|
|
18
30
|
|
|
@@ -31,6 +31,8 @@ export interface SearchArgs {
|
|
|
31
31
|
selectedFacets?: SelectedFacet[]
|
|
32
32
|
fuzzy?: '0' | '1' | 'auto'
|
|
33
33
|
hideUnavailableItems?: boolean
|
|
34
|
+
showInvisibleItems?: boolean
|
|
35
|
+
regionId?: string
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface ProductLocator {
|
|
@@ -65,27 +67,28 @@ export const IntelligentSearch = (
|
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
const getRegionFacet = (): IStoreSelectedFacet | null => {
|
|
70
|
+
const getRegionFacet = (regionIdFromQuery?: string): IStoreSelectedFacet | null => {
|
|
69
71
|
const { regionId } = ctx.storage.channel
|
|
72
|
+
const facet = regionIdFromQuery ?? regionId
|
|
70
73
|
|
|
71
|
-
if (!
|
|
74
|
+
if (!facet) {
|
|
72
75
|
return null
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
return {
|
|
76
79
|
key: REGION_KEY,
|
|
77
|
-
value:
|
|
80
|
+
value: facet,
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
const addDefaultFacets = (facets: SelectedFacet[]) => {
|
|
84
|
+
const addDefaultFacets = (facets: SelectedFacet[], regionIdFromQuery?: string) => {
|
|
82
85
|
const withDefaltFacets = facets.filter(({ key }) => !CHANNEL_KEYS.has(key))
|
|
83
86
|
|
|
84
87
|
const policyFacet =
|
|
85
88
|
facets.find(({ key }) => key === POLICY_KEY) ?? getPolicyFacet()
|
|
86
89
|
|
|
87
90
|
const regionFacet =
|
|
88
|
-
facets.find(({ key }) => key === REGION_KEY) ?? getRegionFacet()
|
|
91
|
+
facets.find(({ key }) => key === REGION_KEY) ?? getRegionFacet(regionIdFromQuery)
|
|
89
92
|
|
|
90
93
|
if (policyFacet !== null) {
|
|
91
94
|
withDefaltFacets.push(policyFacet)
|
|
@@ -106,6 +109,8 @@ export const IntelligentSearch = (
|
|
|
106
109
|
selectedFacets = [],
|
|
107
110
|
type,
|
|
108
111
|
fuzzy = 'auto',
|
|
112
|
+
showInvisibleItems,
|
|
113
|
+
regionId,
|
|
109
114
|
}: SearchArgs): Promise<T> => {
|
|
110
115
|
const params = new URLSearchParams({
|
|
111
116
|
page: (page + 1).toString(),
|
|
@@ -116,11 +121,15 @@ export const IntelligentSearch = (
|
|
|
116
121
|
locale: ctx.storage.locale,
|
|
117
122
|
})
|
|
118
123
|
|
|
124
|
+
if (showInvisibleItems) {
|
|
125
|
+
params.append('show-invisible-items', 'true')
|
|
126
|
+
}
|
|
127
|
+
|
|
119
128
|
if (hideUnavailableItems !== undefined) {
|
|
120
129
|
params.append('hideUnavailableItems', hideUnavailableItems.toString())
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
const pathname = addDefaultFacets(selectedFacets)
|
|
132
|
+
const pathname = addDefaultFacets(selectedFacets, regionId)
|
|
124
133
|
.map(({ key, value }) => `${key}/${value}`)
|
|
125
134
|
.join('/')
|
|
126
135
|
|
|
@@ -56,6 +56,11 @@ export interface Context {
|
|
|
56
56
|
locale: string
|
|
57
57
|
flags: FeatureFlags
|
|
58
58
|
searchArgs?: Omit<SearchArgs, 'type'>
|
|
59
|
+
/**
|
|
60
|
+
* Checkout cookies received while resolving this request, keyed by cookie
|
|
61
|
+
* name. The GraphQL handler forwards `setCookie` values to the browser.
|
|
62
|
+
*/
|
|
63
|
+
cookies: Map<string, Record<string, string>>
|
|
59
64
|
}
|
|
60
65
|
headers: Record<string, string>
|
|
61
66
|
}
|
|
@@ -96,6 +101,7 @@ export const getContextFactory =
|
|
|
96
101
|
channel: ChannelMarshal.parse(options.channel),
|
|
97
102
|
flags: options.flags ?? {},
|
|
98
103
|
locale: options.locale,
|
|
104
|
+
cookies: new Map<string, Record<string, string>>(),
|
|
99
105
|
}
|
|
100
106
|
ctx.clients = getClients(options, ctx)
|
|
101
107
|
ctx.loaders = getLoaders(options, ctx)
|
|
@@ -7,11 +7,15 @@ import type { Options } from '..'
|
|
|
7
7
|
import type { Clients } from '../clients'
|
|
8
8
|
|
|
9
9
|
export const getSkuLoader = (_: Options, clients: Clients) => {
|
|
10
|
-
const loader = async (
|
|
10
|
+
const loader = async (keys: readonly string[]) => {
|
|
11
|
+
const skuIds = keys.map((key) => key.split('-')[0]);
|
|
12
|
+
const showInvisibleItems = keys.some((key) => key.split('-')[1] === 'invisibleItems')
|
|
13
|
+
|
|
11
14
|
const { products } = await clients.search.products({
|
|
12
15
|
query: `sku:${skuIds.join(';')}`,
|
|
13
16
|
page: 0,
|
|
14
17
|
count: skuIds.length,
|
|
18
|
+
showInvisibleItems
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
const skuBySkuId = products.reduce((acc, product) => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
findSkuId,
|
|
10
10
|
findSlug,
|
|
11
11
|
transformSelectedFacet,
|
|
12
|
+
findRegionId,
|
|
12
13
|
} from "../utils/facets"
|
|
13
14
|
import { SORT_MAP } from "../utils/sort"
|
|
14
15
|
import { StoreCollection } from "./collection"
|
|
@@ -31,6 +32,7 @@ export const Query = {
|
|
|
31
32
|
// Insert channel in context for later usage
|
|
32
33
|
const channel = findChannel(locator)
|
|
33
34
|
const locale = findLocale(locator)
|
|
35
|
+
const regionId = findRegionId(locator)
|
|
34
36
|
const id = findSkuId(locator)
|
|
35
37
|
const slug = findSlug(locator)
|
|
36
38
|
|
|
@@ -90,6 +92,7 @@ export const Query = {
|
|
|
90
92
|
page: 0,
|
|
91
93
|
count: 1,
|
|
92
94
|
query: `product:${route.id}`,
|
|
95
|
+
regionId,
|
|
93
96
|
})
|
|
94
97
|
|
|
95
98
|
if (!product) {
|
|
@@ -145,7 +145,7 @@ const orderFormToCart = async (
|
|
|
145
145
|
orderNumber: form.orderFormId,
|
|
146
146
|
acceptedOffer: form.items.map(async (item) => ({
|
|
147
147
|
...item,
|
|
148
|
-
product: await skuLoader.load(item.id),
|
|
148
|
+
product: await skuLoader.load(`${item.id}-invisibleItems`),
|
|
149
149
|
})),
|
|
150
150
|
},
|
|
151
151
|
messages: form.messages.map(({ text, status }) => ({
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Context } from '../index'
|
|
2
|
+
|
|
3
|
+
export interface ContextForCookies {
|
|
4
|
+
headers: Context['headers']
|
|
5
|
+
storage: Pick<Context['storage'], 'cookies'>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const MATCH_FIRST_SET_COOKIE_KEY_VALUE = /^([^=]+)=([^;]*)/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Splits a combined `set-cookie` header value (cookies joined by `, `) into
|
|
12
|
+
* individual cookies. Commas inside `expires=...` dates are kept.
|
|
13
|
+
*/
|
|
14
|
+
const splitSetCookieHeader = (value: string) =>
|
|
15
|
+
value.split(/,(?=\s*[^;,=\s]+=)/).map((setCookie) => setCookie.trim())
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns every `set-cookie` value of a fetch response.
|
|
19
|
+
*
|
|
20
|
+
* `Headers.getSetCookie()` only exists on Node >= 18.14 (undici). FastStore v1
|
|
21
|
+
* stores may still run on Node 16, where isomorphic-unfetch falls back to
|
|
22
|
+
* node-fetch v2, which exposes repeated headers through `raw()` instead.
|
|
23
|
+
*/
|
|
24
|
+
export const getSetCookies = (headers: Headers): string[] => {
|
|
25
|
+
const maybeHeaders = headers as Headers & {
|
|
26
|
+
getSetCookie?: () => string[]
|
|
27
|
+
raw?: () => Record<string, string[]>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof maybeHeaders.getSetCookie === 'function') {
|
|
31
|
+
return maybeHeaders.getSetCookie()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof maybeHeaders.raw === 'function') {
|
|
35
|
+
return maybeHeaders.raw()['set-cookie'] ?? []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const setCookie = headers.get('set-cookie')
|
|
39
|
+
|
|
40
|
+
return setCookie ? splitSetCookieHeader(setCookie) : []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function updates the ctx.storage.cookies, that is used in each request.
|
|
45
|
+
*
|
|
46
|
+
* ctx.storage.cookies is a Map<string[1], Record<string, string>[2]> where,
|
|
47
|
+
* [1] cookie key
|
|
48
|
+
* [2] {
|
|
49
|
+
* value: cookie value,
|
|
50
|
+
* setCookie: setCookie used in browser response
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
export const updatesContextStorageCookies = (
|
|
54
|
+
ctx: Pick<Context, 'storage'>,
|
|
55
|
+
setCookieValue: string
|
|
56
|
+
) => {
|
|
57
|
+
const matchCookie = setCookieValue.match(MATCH_FIRST_SET_COOKIE_KEY_VALUE)
|
|
58
|
+
|
|
59
|
+
if (matchCookie) {
|
|
60
|
+
const cookieKey = matchCookie[1]
|
|
61
|
+
const cookieValue = matchCookie[2]
|
|
62
|
+
|
|
63
|
+
ctx.storage.cookies.set(cookieKey, {
|
|
64
|
+
value: cookieValue,
|
|
65
|
+
setCookie: setCookieValue,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const setCookie = (ctx: Pick<Context, 'storage'>, headers: Headers) => {
|
|
71
|
+
getSetCookies(headers).forEach((setCookieValue) =>
|
|
72
|
+
updatesContextStorageCookies(ctx, setCookieValue)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const getStoreCookie =
|
|
77
|
+
(ctx: Pick<Context, 'storage'>) => (headers: Headers) =>
|
|
78
|
+
setCookie(ctx, headers)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* This function returns a modified copy of the original cookie header (ctx.headers.cookie from the first request)
|
|
82
|
+
* with the cookie values that comes in each request (ctx.storage.cookies).
|
|
83
|
+
* If there is no cookies in storage, the ctx.headers?.cookie is used
|
|
84
|
+
*
|
|
85
|
+
* ctx.storage.cookies is a Map<string[1], Record<string, string>[2]> where,
|
|
86
|
+
* [1] cookie key
|
|
87
|
+
* [2] {
|
|
88
|
+
* value: cookie value,
|
|
89
|
+
* setCookie: setCookie used in browser response
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
export const getUpdatedCookie = (ctx: ContextForCookies) => {
|
|
93
|
+
if (!ctx.headers?.cookie) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const contextStorageCookies = Array.from(ctx.storage.cookies.entries())
|
|
98
|
+
|
|
99
|
+
if (contextStorageCookies.length === 0) {
|
|
100
|
+
return ctx.headers.cookie
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return contextStorageCookies.reduce(
|
|
104
|
+
(existingCookies, [storageCookieKey, { value: storageCookieValue }]) =>
|
|
105
|
+
updatesCookieValueByKey(
|
|
106
|
+
existingCookies,
|
|
107
|
+
storageCookieKey,
|
|
108
|
+
storageCookieValue
|
|
109
|
+
),
|
|
110
|
+
ctx.headers.cookie
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const getWithCookie = (ctx: ContextForCookies) =>
|
|
115
|
+
function withCookie<T extends Record<string, any>>(
|
|
116
|
+
headers: T
|
|
117
|
+
): T | (T & { cookie: string }) {
|
|
118
|
+
const updatedCookie = getUpdatedCookie(ctx)
|
|
119
|
+
|
|
120
|
+
if (!updatedCookie) {
|
|
121
|
+
return headers
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...headers,
|
|
126
|
+
cookie: updatedCookie,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* This function updates the cookie value based on its key
|
|
132
|
+
*
|
|
133
|
+
* const existingCookies = 'key=value1; key2=; key3=value3';
|
|
134
|
+
* const storageCookieKey = 'key2';
|
|
135
|
+
* const storageCookieValue = 'value2'
|
|
136
|
+
*
|
|
137
|
+
* updatesCookieValueByKey(existingCookies, storageCookieKey, storageCookieValue) returns 'key=value1; key2=value2; key3=value3';
|
|
138
|
+
*/
|
|
139
|
+
export const updatesCookieValueByKey = (
|
|
140
|
+
existingCookies: string,
|
|
141
|
+
storageCookieKey: string,
|
|
142
|
+
storageCookieValue: string
|
|
143
|
+
) => {
|
|
144
|
+
const MATCH_COOKIE_KEY_VALUE = new RegExp(`(${storageCookieKey})=([^;]*)`)
|
|
145
|
+
const cookieParts = existingCookies.match(MATCH_COOKIE_KEY_VALUE)
|
|
146
|
+
|
|
147
|
+
// replaces original cookie with the one coming from storage
|
|
148
|
+
if (cookieParts) {
|
|
149
|
+
return existingCookies.replace(
|
|
150
|
+
cookieParts[0],
|
|
151
|
+
`${cookieParts[1]}=${storageCookieValue}`
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// add new storage cookie to the original list of cookies
|
|
156
|
+
return `${existingCookies};${storageCookieKey}=${storageCookieValue}`
|
|
157
|
+
}
|
|
@@ -104,3 +104,6 @@ export const findLocale = (facets?: Maybe<SelectedFacet[]>) =>
|
|
|
104
104
|
|
|
105
105
|
export const findChannel = (facets?: Maybe<SelectedFacet[]>) =>
|
|
106
106
|
facets?.find((facet) => facet.key === 'channel')?.value ?? null
|
|
107
|
+
|
|
108
|
+
export const findRegionId = (facets?: Maybe<SelectedFacet[]>) =>
|
|
109
|
+
facets?.find((facet) => facet.key === 'regionId')?.value ?? undefined
|