@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/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
- export declare const fetchAPI: (info: RequestInfo, init?: RequestInit | undefined) => Promise<any>;
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: {
@@ -12,6 +12,8 @@ export interface SearchArgs {
12
12
  selectedFacets?: SelectedFacet[];
13
13
  fuzzy?: '0' | '1' | 'auto';
14
14
  hideUnavailableItems?: boolean;
15
+ showInvisibleItems?: boolean;
16
+ regionId?: string;
15
17
  }
16
18
  export interface ProductLocator {
17
19
  field: 'id' | 'slug';
@@ -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.48",
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": "9a273837d3afc05e1ef2c03d8aaee7eb90df1299"
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
- BASE_INIT
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
- export const fetchAPI = async (info: RequestInfo, init?: RequestInit) => {
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 (!regionId) {
74
+ if (!facet) {
72
75
  return null
73
76
  }
74
77
 
75
78
  return {
76
79
  key: REGION_KEY,
77
- value: regionId,
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 (skuIds: readonly string[]) => {
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