@scayle/storefront-core 7.64.4 → 7.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @scayle/storefront-core
2
2
 
3
+ ## 7.65.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix regresion in product RPC methods where the categoryId may not be resolved from the category path
8
+
9
+ ## 7.65.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Improves the handling of `getOrderDataByCbd` RPC to return descriptive errors in case the validation of the token fails.
14
+
15
+ In case the token can't be validated using the Checkout Secret, this is now returned and a warning on the server is logged.
16
+
17
+ If the token has expired, this is now also returned and logged on the server.
18
+
19
+ - Support passing `categoryId` as an alternative to the category path in `getProductsByCategory` and `getFilters`
20
+
21
+ ### Patch Changes
22
+
23
+ - Fixed cached fallback ttl to be used only if passed `option.ttl` is undefined
24
+
3
25
  ## 7.64.4
4
26
 
5
27
  ### Patch Changes
@@ -15,14 +15,10 @@ class CustomerAPIClient {
15
15
  this.context = context;
16
16
  }
17
17
  get headers() {
18
- const accessHeader = this.context.checkout.accessHeader;
19
18
  return {
20
19
  Authorization: `Bearer ${this.context.accessToken}`,
21
20
  Accept: "application/json",
22
- "Content-Type": "application/json",
23
- ...(accessHeader ? {
24
- "X-Access-Header": accessHeader
25
- } : {})
21
+ "Content-Type": "application/json"
26
22
  };
27
23
  }
28
24
  async sendRequest(request, retry = true) {
@@ -9,12 +9,10 @@ export class CustomerAPIClient {
9
9
  this.context = context;
10
10
  }
11
11
  get headers() {
12
- const accessHeader = this.context.checkout.accessHeader;
13
12
  return {
14
13
  Authorization: `Bearer ${this.context.accessToken}`,
15
14
  Accept: "application/json",
16
- "Content-Type": "application/json",
17
- ...accessHeader ? { "X-Access-Header": accessHeader } : {}
15
+ "Content-Type": "application/json"
18
16
  };
19
17
  }
20
18
  async sendRequest(request, retry = true) {
@@ -71,7 +71,7 @@ class Cached {
71
71
  if (!this.isCacheEnabled) {
72
72
  return;
73
73
  }
74
- const ttl = options?.ttl || 60 * MINUTE;
74
+ const ttl = options?.ttl ?? 60 * MINUTE;
75
75
  if (ttl) {
76
76
  await (0, _utils.timeout)(_constants.CACHE_TIMEOUT, this.cache.set(cacheKey, value, ttl));
77
77
  }
@@ -68,7 +68,7 @@ export class Cached {
68
68
  if (!this.isCacheEnabled) {
69
69
  return;
70
70
  }
71
- const ttl = options?.ttl || 60 * MINUTE;
71
+ const ttl = options?.ttl ?? 60 * MINUTE;
72
72
  if (ttl) {
73
73
  await timeout(CACHE_TIMEOUT, this.cache.set(cacheKey, value, ttl));
74
74
  }
@@ -4,37 +4,35 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getOrderDataByCbd = void 0;
7
+ var _constants = require("../../constants/index.cjs");
8
+ var _errors = require("../../errors/index.cjs");
7
9
  var _hash = require("../../utils/hash.cjs");
8
10
  const getOrderDataByCbd = exports.getOrderDataByCbd = async function getOrderDataByCbd2({
9
11
  cbdToken
10
12
  }, context) {
11
- if (!cbdToken) {
12
- return;
13
+ const payload = await (0, _hash.verifyOrderSuccessToken)(cbdToken, context.checkout.secret);
14
+ if (!payload) {
15
+ context.log.warn("Unable to verify CBD token. Please check that you configured the correct Checkout Secret");
16
+ return new _errors.ErrorResponse(_constants.HttpStatusCode.BAD_REQUEST, _constants.HttpStatusMessage.BAD_REQUEST, "Unable to verify CBD token.");
13
17
  }
14
- const shopId = context.shopId;
15
- const accessHeader = context.checkout.accessHeader;
16
- const checkoutSecret = context.checkout.secret;
17
- const decodedPayload = await (0, _hash.verifyToken)(cbdToken, checkoutSecret);
18
- if (decodedPayload) {
19
- const payload = JSON.parse(decodedPayload);
20
- const checkoutUsername = context.checkout.user;
21
- const checkoutToken = context.checkout.token;
22
- const checkoutUrl = context.checkout.url;
23
- if (context.checkout.cbdExpiration && Math.floor(Date.now() / 1e3) - payload.issued_at > context.checkout.cbdExpiration) {
24
- context.log.debug("Attempting to use expired CBD token");
25
- return;
18
+ if (context.checkout.cbdExpiration && Math.floor(Date.now() / 1e3) - payload.issued_at > context.checkout.cbdExpiration) {
19
+ context.log.warn("Attempting to use expired CBD token");
20
+ return new _errors.ErrorResponse(_constants.HttpStatusCode.BAD_REQUEST, _constants.HttpStatusMessage.BAD_REQUEST, "The CBD token has expired.");
21
+ }
22
+ const checkoutUrl = context.checkout.url;
23
+ const basicAuth = (0, _hash.encodeBase64)(`${context.checkout.user}:${context.checkout.token}`);
24
+ const response = await fetch(`${checkoutUrl}/api/v1/orders/${payload.order_id}`, {
25
+ headers: {
26
+ Accept: "application/json",
27
+ Authorization: `Basic ${basicAuth}`,
28
+ "X-Shop-Id": context.shopId.toString()
26
29
  }
27
- const accessToken = (0, _hash.encodeBase64)(`${checkoutUsername}:${unescape(checkoutToken)}`);
28
- const response = await fetch(`${checkoutUrl}/api/v1/orders/${payload.order_id}`, {
29
- headers: {
30
- Accept: "application/json",
31
- Authorization: `Basic ${accessToken}`,
32
- "x-shop-id": shopId.toString(),
33
- ...(accessHeader ? {
34
- "X-Access-Header": accessHeader
35
- } : {})
36
- }
37
- });
30
+ });
31
+ if (response.ok) {
38
32
  return await response.json();
39
33
  }
34
+ context.log.error(`Unexpected status code when fetching order from Checkout API: ${response.status}`);
35
+ return new _errors.ErrorResponse(_constants.HttpStatusCode.INTERNAL_SERVER_ERROR, _constants.HttpStatusMessage.INTERNAL_SERVER_ERROR, "Unknown error", {
36
+ detail: `Fetching order failed with status code ${response.status}`
37
+ });
40
38
  };
@@ -1,4 +1,5 @@
1
+ import { ErrorResponse } from '../../errors';
1
2
  import type { Order, RpcContext } from '../../types';
2
3
  export declare const getOrderDataByCbd: ({ cbdToken }: {
3
4
  cbdToken: string;
4
- }, context: RpcContext) => Promise<Order | undefined>;
5
+ }, context: RpcContext) => Promise<Order | ErrorResponse>;
@@ -1,35 +1,55 @@
1
- import { encodeBase64, verifyToken } from "../../utils/hash.mjs";
1
+ import { HttpStatusCode, HttpStatusMessage } from "../../constants/index.mjs";
2
+ import { ErrorResponse } from "../../errors/index.mjs";
3
+ import { encodeBase64, verifyOrderSuccessToken } from "../../utils/hash.mjs";
2
4
  export const getOrderDataByCbd = async function getOrderDataByCbd2({ cbdToken }, context) {
3
- if (!cbdToken) {
4
- return;
5
+ const payload = await verifyOrderSuccessToken(
6
+ cbdToken,
7
+ context.checkout.secret
8
+ );
9
+ if (!payload) {
10
+ context.log.warn(
11
+ "Unable to verify CBD token. Please check that you configured the correct Checkout Secret"
12
+ );
13
+ return new ErrorResponse(
14
+ HttpStatusCode.BAD_REQUEST,
15
+ HttpStatusMessage.BAD_REQUEST,
16
+ "Unable to verify CBD token."
17
+ );
5
18
  }
6
- const shopId = context.shopId;
7
- const accessHeader = context.checkout.accessHeader;
8
- const checkoutSecret = context.checkout.secret;
9
- const decodedPayload = await verifyToken(cbdToken, checkoutSecret);
10
- if (decodedPayload) {
11
- const payload = JSON.parse(decodedPayload);
12
- const checkoutUsername = context.checkout.user;
13
- const checkoutToken = context.checkout.token;
14
- const checkoutUrl = context.checkout.url;
15
- if (context.checkout.cbdExpiration && Math.floor(Date.now() / 1e3) - payload.issued_at > context.checkout.cbdExpiration) {
16
- context.log.debug("Attempting to use expired CBD token");
17
- return;
18
- }
19
- const accessToken = encodeBase64(
20
- `${checkoutUsername}:${unescape(checkoutToken)}`
19
+ if (context.checkout.cbdExpiration && Math.floor(Date.now() / 1e3) - payload.issued_at > context.checkout.cbdExpiration) {
20
+ context.log.warn("Attempting to use expired CBD token");
21
+ return new ErrorResponse(
22
+ HttpStatusCode.BAD_REQUEST,
23
+ HttpStatusMessage.BAD_REQUEST,
24
+ "The CBD token has expired."
21
25
  );
22
- const response = await fetch(
23
- `${checkoutUrl}/api/v1/orders/${payload.order_id}`,
24
- {
25
- headers: {
26
- Accept: "application/json",
27
- Authorization: `Basic ${accessToken}`,
28
- "x-shop-id": shopId.toString(),
29
- ...accessHeader ? { "X-Access-Header": accessHeader } : {}
30
- }
26
+ }
27
+ const checkoutUrl = context.checkout.url;
28
+ const basicAuth = encodeBase64(
29
+ `${context.checkout.user}:${context.checkout.token}`
30
+ );
31
+ const response = await fetch(
32
+ `${checkoutUrl}/api/v1/orders/${payload.order_id}`,
33
+ {
34
+ headers: {
35
+ Accept: "application/json",
36
+ Authorization: `Basic ${basicAuth}`,
37
+ "X-Shop-Id": context.shopId.toString()
31
38
  }
32
- );
39
+ }
40
+ );
41
+ if (response.ok) {
33
42
  return await response.json();
34
43
  }
44
+ context.log.error(
45
+ `Unexpected status code when fetching order from Checkout API: ${response.status}`
46
+ );
47
+ return new ErrorResponse(
48
+ HttpStatusCode.INTERNAL_SERVER_ERROR,
49
+ HttpStatusMessage.INTERNAL_SERVER_ERROR,
50
+ "Unknown error",
51
+ {
52
+ detail: `Fetching order failed with status code ${response.status}`
53
+ }
54
+ );
35
55
  };
@@ -5,10 +5,14 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.getCheckoutToken = void 0;
7
7
  var _jose = require("jose");
8
+ var _types = require("../../../types/index.cjs");
8
9
  var _errors = require("../../../errors/index.cjs");
9
10
  var _constants = require("../../../constants/index.cjs");
10
11
  var _user = require("../user.cjs");
11
12
  const getCheckoutToken = exports.getCheckoutToken = async function getCheckoutToken2(jwtPayload = {}, context) {
13
+ if (!(0, _types.hasSession)(context)) {
14
+ return new _errors.ErrorResponse(_constants.HttpStatusCode.BAD_REQUEST, _constants.HttpStatusMessage.BAD_REQUEST, "No Session found");
15
+ }
12
16
  if (!context.accessToken) {
13
17
  return new _errors.ErrorResponse(_constants.HttpStatusCode.UNAUTHORIZED, _constants.HttpStatusMessage.UNAUTHORIZED, "No access token present");
14
18
  }
@@ -33,7 +37,7 @@ const getCheckoutToken = exports.getCheckoutToken = async function getCheckoutTo
33
37
  carrier,
34
38
  basketId: context.basketKey,
35
39
  campaignKey: context.campaignKey
36
- }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"7.64.4"}`).setProtectedHeader({
40
+ }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"7.65.1"}`).setProtectedHeader({
37
41
  alg: "HS256",
38
42
  typ: "JWT"
39
43
  }).sign(secret);
@@ -1,8 +1,16 @@
1
1
  import { SignJWT } from "jose";
2
+ import { hasSession } from "../../../types/index.mjs";
2
3
  import { ErrorResponse } from "../../../errors/index.mjs";
3
4
  import { HttpStatusCode, HttpStatusMessage } from "../../../constants/index.mjs";
4
5
  import { getAccessToken } from "../user.mjs";
5
6
  export const getCheckoutToken = async function getCheckoutToken2(jwtPayload = {}, context) {
7
+ if (!hasSession(context)) {
8
+ return new ErrorResponse(
9
+ HttpStatusCode.BAD_REQUEST,
10
+ HttpStatusMessage.BAD_REQUEST,
11
+ "No Session found"
12
+ );
13
+ }
6
14
  if (!context.accessToken) {
7
15
  return new ErrorResponse(
8
16
  HttpStatusCode.UNAUTHORIZED,
@@ -27,7 +35,7 @@ export const getCheckoutToken = async function getCheckoutToken2(jwtPayload = {}
27
35
  carrier,
28
36
  basketId: context.basketKey,
29
37
  campaignKey: context.campaignKey
30
- }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"7.64.4"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
38
+ }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"7.65.1"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
31
39
  return {
32
40
  accessToken: refreshedAccessToken,
33
41
  checkoutJwt
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getProductsCount = exports.getProductsByReferenceKeys = exports.getProductsByIds = exports.getProductsByCategory = exports.getProductById = exports.getFilters = exports.fetchAllFiltersForCategory = void 0;
7
+ exports.resolveCategoryIdFromParams = resolveCategoryIdFromParams;
7
8
  var _helpers = require("../../helpers/index.cjs");
8
9
  var _constants = require("../../constants/index.cjs");
9
10
  var _cache = require("../../cache/index.cjs");
@@ -22,6 +23,25 @@ const sanitizeAttributesForBAPI = ({
22
23
  const mapFiltersToSlugs = (filters = []) => filters.map(({
23
24
  slug
24
25
  }) => slug.toLowerCase());
26
+ async function resolveCategoryIdFromParams(context, params) {
27
+ let category;
28
+ let categoryId;
29
+ if ("categoryId" in params) {
30
+ categoryId = params.categoryId;
31
+ } else if (params.category && params.category !== "/") {
32
+ category = params.category;
33
+ const result = await context.cached(context.bapiClient.categories.getByPath, {
34
+ cacheKeyPrefix: `getByPath-categories-${category}`
35
+ })((0, _helpers.splitAndRemoveEmpty)(category));
36
+ categoryId = result.id;
37
+ } else {
38
+ category = "/";
39
+ }
40
+ return {
41
+ category,
42
+ categoryId
43
+ };
44
+ }
25
45
  const getProductById = exports.getProductById = async function getProductById2(options, {
26
46
  bapiClient,
27
47
  cached,
@@ -68,31 +88,27 @@ const getProductsByReferenceKeys = exports.getProductsByReferenceKeys = async fu
68
88
  pricePromotionKey: options.pricePromotionKey
69
89
  });
70
90
  };
71
- const getProductsCount = exports.getProductsCount = async function getProductsCount2({
72
- category = "/",
73
- categoryId = void 0,
74
- where = void 0,
75
- includeSoldOut = false,
76
- includeSellableForFree = false,
77
- orFiltersOperator = void 0
78
- }, {
79
- cached,
80
- bapiClient,
81
- campaignKey
82
- }) {
83
- let result;
84
- if (category !== "/") {
85
- const sanitizedPath = (0, _helpers.splitAndRemoveEmpty)(category);
86
- result = await cached(bapiClient.categories.getByPath, {
87
- cacheKeyPrefix: `getByPath-categories-${sanitizedPath}`
88
- })(sanitizedPath);
89
- }
91
+ const getProductsCount = exports.getProductsCount = async function getProductsCount2(params, context) {
92
+ const {
93
+ where = void 0,
94
+ includeSoldOut = false,
95
+ includeSellableForFree = false,
96
+ orFiltersOperator = void 0
97
+ } = params;
98
+ const {
99
+ cached,
100
+ bapiClient,
101
+ campaignKey
102
+ } = context;
103
+ const {
104
+ categoryId
105
+ } = await resolveCategoryIdFromParams(context, params);
90
106
  const productCount = await cached(bapiClient.products.query, {
91
107
  ttl: 15 * _cache.MINUTE,
92
108
  cacheKeyPrefix: "products-query"
93
109
  })({
94
110
  where: {
95
- categoryId: categoryId ?? result?.id,
111
+ categoryId,
96
112
  minPrice: where?.minPrice,
97
113
  maxPrice: where?.maxPrice,
98
114
  attributes: where?.attributes,
@@ -131,32 +147,29 @@ const fetchAllFiltersForCategory = exports.fetchAllFiltersForCategory = async fu
131
147
  return await cached(fetchAndMapFiltersToSlugs, {
132
148
  ttl: 4 * 60 * 60,
133
149
  // cached for 4 hours
134
- cacheKey: `filters-for-category-${category.slug}`
150
+ cacheKey: `filters-for-category-${category.id}`
135
151
  })();
136
152
  };
137
- const getFilters = exports.getFilters = async function getFilters2({
138
- category = "/",
139
- includedFilters,
140
- where = void 0,
141
- includeSoldOut = false,
142
- includeSellableForFree = false,
143
- orFiltersOperator
144
- }, context) {
145
- let result;
153
+ const getFilters = exports.getFilters = async function getFilters2(params, context) {
154
+ const {
155
+ includedFilters,
156
+ where = void 0,
157
+ includeSoldOut = false,
158
+ includeSellableForFree = false,
159
+ orFiltersOperator
160
+ } = params;
146
161
  const {
147
162
  cached,
148
163
  bapiClient,
149
164
  campaignKey
150
165
  } = context;
151
- if (category !== "/") {
152
- result = await cached(bapiClient.categories.getByPath, {
153
- cacheKeyPrefix: `getByPath-categories-${category}`
154
- })((0, _helpers.splitAndRemoveEmpty)(category));
155
- }
166
+ const {
167
+ category,
168
+ categoryId
169
+ } = await resolveCategoryIdFromParams(context, params);
156
170
  const response = await fetchAllFiltersForCategory({
157
171
  category: {
158
- id: result?.id,
159
- slug: category
172
+ id: categoryId
160
173
  },
161
174
  includedFilters
162
175
  }, context);
@@ -170,10 +183,10 @@ const getFilters = exports.getFilters = async function getFilters2({
170
183
  includedFilters
171
184
  });
172
185
  const [filters, productCount] = await Promise.all([cached(bapiClient.filters.get, {
173
- cacheKeyPrefix: `get-filters-${category}`
186
+ cacheKeyPrefix: `get-filters-${categoryId}`
174
187
  })({
175
188
  where: {
176
- categoryId: result?.id,
189
+ categoryId,
177
190
  minPrice: where?.minPrice,
178
191
  maxPrice: where?.maxPrice,
179
192
  term: where?.term,
@@ -186,10 +199,10 @@ const getFilters = exports.getFilters = async function getFilters2({
186
199
  orFiltersOperator
187
200
  }), cached(bapiClient.products.query, {
188
201
  ttl: 15 * _cache.MINUTE,
189
- cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
202
+ cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : categoryId}`
190
203
  })({
191
204
  where: {
192
- categoryId: result?.id,
205
+ categoryId,
193
206
  minPrice: where?.minPrice,
194
207
  maxPrice: where?.maxPrice,
195
208
  term: where?.term,
@@ -207,34 +220,31 @@ const getFilters = exports.getFilters = async function getFilters2({
207
220
  unfilteredCount: productCount?.pagination.total
208
221
  };
209
222
  };
210
- const getProductsByCategory = exports.getProductsByCategory = async function getProductsByCategory2({
211
- category = "/",
212
- cache,
213
- with: _with,
214
- perPage = 20,
215
- page = 1,
216
- where = void 0,
217
- sort = void 0,
218
- pricePromotionKey = "",
219
- includeSellableForFree = void 0,
220
- includeSoldOut = void 0,
221
- orFiltersOperator = void 0
222
- }, context) {
223
- let result;
223
+ const getProductsByCategory = exports.getProductsByCategory = async function getProductsByCategory2(params, context) {
224
+ const {
225
+ cache,
226
+ with: _with,
227
+ perPage = 20,
228
+ page = 1,
229
+ where = void 0,
230
+ sort = void 0,
231
+ pricePromotionKey = "",
232
+ includeSellableForFree = void 0,
233
+ includeSoldOut = void 0,
234
+ orFiltersOperator = void 0
235
+ } = params;
224
236
  const {
225
237
  cached,
226
238
  bapiClient,
227
239
  campaignKey
228
240
  } = context;
229
- if (category !== "/") {
230
- result = await cached(bapiClient.categories.getByPath, {
231
- cacheKeyPrefix: `getByPath-categories-${category}`
232
- })((0, _helpers.splitAndRemoveEmpty)(category));
233
- }
241
+ const {
242
+ category,
243
+ categoryId
244
+ } = await resolveCategoryIdFromParams(context, params);
234
245
  const response = await fetchAllFiltersForCategory({
235
246
  category: {
236
- id: result?.id,
237
- slug: category
247
+ id: categoryId
238
248
  }
239
249
  }, context);
240
250
  if (response instanceof _errors.ErrorResponse) {
@@ -251,11 +261,11 @@ const getProductsByCategory = exports.getProductsByCategory = async function get
251
261
  } = await cached(bapiClient.products.query, {
252
262
  ...cache,
253
263
  ttl: 15 * _cache.MINUTE,
254
- cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
264
+ cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : categoryId}`
255
265
  })({
256
266
  where: {
257
267
  term: where?.term,
258
- categoryId: result?.id,
268
+ categoryId,
259
269
  minPrice: where?.minPrice,
260
270
  maxPrice: where?.maxPrice,
261
271
  attributes: [...sanitizedAttributes, ...(where?.whitelistAttributes || [])],
@@ -1,24 +1,42 @@
1
1
  import type { FiltersEndpointResponseData } from '@scayle/storefront-api';
2
2
  import type { FetchFiltersParams, FetchProductParams, FetchProductsByCategoryParams, FetchProductsByIdsParams, FetchProductsByReferenceKeysParams, FetchProductsCountParams, Product, RpcContext } from '../../types';
3
3
  import { ErrorResponse } from '../../errors';
4
+ /**
5
+ * For functions which accept either a category path or category ID,
6
+ * this function can resolve the ID which is necessary for the calling
7
+ * the Storefront APIs.
8
+ * @param context
9
+ * @param params
10
+ * @returns A object with `category` and `categoryId` properties.
11
+ * If no `category` was provided in the params, it will be undefined or '/'
12
+ */
13
+ export declare function resolveCategoryIdFromParams(context: RpcContext, params: {
14
+ category?: string;
15
+ categoryId?: undefined;
16
+ } | {
17
+ categoryId: number;
18
+ category?: undefined;
19
+ }): Promise<{
20
+ category: string | undefined;
21
+ categoryId: number | undefined;
22
+ }>;
4
23
  export declare const getProductById: (options: FetchProductParams, { bapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Product>;
5
24
  export declare const getProductsByIds: (options: FetchProductsByIdsParams, { bapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Product[]>;
6
25
  export declare const getProductsByReferenceKeys: (options: FetchProductsByReferenceKeysParams, { bapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Product[]>;
7
- export declare const getProductsCount: ({ category, categoryId, where, includeSoldOut, includeSellableForFree, orFiltersOperator, }: FetchProductsCountParams, { cached, bapiClient, campaignKey }: RpcContext) => Promise<{
26
+ export declare const getProductsCount: (params: FetchProductsCountParams, context: RpcContext) => Promise<{
8
27
  count: number;
9
28
  }>;
10
29
  export declare const fetchAllFiltersForCategory: ({ includedFilters, category }: {
11
30
  includedFilters?: string[];
12
31
  category: {
13
- slug: string;
14
32
  id?: number;
15
33
  };
16
34
  }, { cached, bapiClient, campaignKey }: RpcContext) => Promise<string[]>;
17
- export declare const getFilters: ({ category, includedFilters, where, includeSoldOut, includeSellableForFree, orFiltersOperator, }: FetchFiltersParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
35
+ export declare const getFilters: (params: FetchFiltersParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
18
36
  filters: FiltersEndpointResponseData;
19
37
  unfilteredCount: number;
20
38
  }>;
21
- export declare const getProductsByCategory: ({ category, cache, with: _with, perPage, page, where, sort, pricePromotionKey, includeSellableForFree, includeSoldOut, orFiltersOperator, }: FetchProductsByCategoryParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
39
+ export declare const getProductsByCategory: (params: FetchProductsByCategoryParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
22
40
  products: Product[];
23
41
  pagination: {
24
42
  current: number;
@@ -14,6 +14,25 @@ const sanitizeAttributesForBAPI = ({
14
14
  );
15
15
  };
16
16
  const mapFiltersToSlugs = (filters = []) => filters.map(({ slug }) => slug.toLowerCase());
17
+ export async function resolveCategoryIdFromParams(context, params) {
18
+ let category;
19
+ let categoryId;
20
+ if ("categoryId" in params) {
21
+ categoryId = params.categoryId;
22
+ } else if (params.category && params.category !== "/") {
23
+ category = params.category;
24
+ const result = await context.cached(
25
+ context.bapiClient.categories.getByPath,
26
+ {
27
+ cacheKeyPrefix: `getByPath-categories-${category}`
28
+ }
29
+ )(splitAndRemoveEmpty(category));
30
+ categoryId = result.id;
31
+ } else {
32
+ category = "/";
33
+ }
34
+ return { category, categoryId };
35
+ }
17
36
  export const getProductById = async function getProductById2(options, { bapiClient, cached, campaignKey, withParams }) {
18
37
  return await cached(bapiClient.products.getById, {
19
38
  cacheKeyPrefix: `getById-product-${options.id}`,
@@ -45,27 +64,21 @@ export const getProductsByReferenceKeys = async function getProductsByReferenceK
45
64
  pricePromotionKey: options.pricePromotionKey
46
65
  });
47
66
  };
48
- export const getProductsCount = async function getProductsCount2({
49
- category = "/",
50
- categoryId = void 0,
51
- where = void 0,
52
- includeSoldOut = false,
53
- includeSellableForFree = false,
54
- orFiltersOperator = void 0
55
- }, { cached, bapiClient, campaignKey }) {
56
- let result;
57
- if (category !== "/") {
58
- const sanitizedPath = splitAndRemoveEmpty(category);
59
- result = await cached(bapiClient.categories.getByPath, {
60
- cacheKeyPrefix: `getByPath-categories-${sanitizedPath}`
61
- })(sanitizedPath);
62
- }
67
+ export const getProductsCount = async function getProductsCount2(params, context) {
68
+ const {
69
+ where = void 0,
70
+ includeSoldOut = false,
71
+ includeSellableForFree = false,
72
+ orFiltersOperator = void 0
73
+ } = params;
74
+ const { cached, bapiClient, campaignKey } = context;
75
+ const { categoryId } = await resolveCategoryIdFromParams(context, params);
63
76
  const productCount = await cached(bapiClient.products.query, {
64
77
  ttl: 15 * MINUTE,
65
78
  cacheKeyPrefix: "products-query"
66
79
  })({
67
80
  where: {
68
- categoryId: categoryId ?? result?.id,
81
+ categoryId,
69
82
  minPrice: where?.minPrice,
70
83
  maxPrice: where?.maxPrice,
71
84
  attributes: where?.attributes,
@@ -95,26 +108,27 @@ export const fetchAllFiltersForCategory = async function fetchAllFiltersForCateg
95
108
  return await cached(fetchAndMapFiltersToSlugs, {
96
109
  ttl: 4 * 60 * 60,
97
110
  // cached for 4 hours
98
- cacheKey: `filters-for-category-${category.slug}`
111
+ cacheKey: `filters-for-category-${category.id}`
99
112
  })();
100
113
  };
101
- export const getFilters = async function getFilters2({
102
- category = "/",
103
- includedFilters,
104
- where = void 0,
105
- includeSoldOut = false,
106
- includeSellableForFree = false,
107
- orFiltersOperator
108
- }, context) {
109
- let result;
114
+ export const getFilters = async function getFilters2(params, context) {
115
+ const {
116
+ includedFilters,
117
+ where = void 0,
118
+ includeSoldOut = false,
119
+ includeSellableForFree = false,
120
+ orFiltersOperator
121
+ } = params;
110
122
  const { cached, bapiClient, campaignKey } = context;
111
- if (category !== "/") {
112
- result = await cached(bapiClient.categories.getByPath, {
113
- cacheKeyPrefix: `getByPath-categories-${category}`
114
- })(splitAndRemoveEmpty(category));
115
- }
123
+ const { category, categoryId } = await resolveCategoryIdFromParams(
124
+ context,
125
+ params
126
+ );
116
127
  const response = await fetchAllFiltersForCategory(
117
- { category: { id: result?.id, slug: category }, includedFilters },
128
+ {
129
+ category: { id: categoryId },
130
+ includedFilters
131
+ },
118
132
  context
119
133
  );
120
134
  if (response instanceof ErrorResponse) {
@@ -128,10 +142,10 @@ export const getFilters = async function getFilters2({
128
142
  });
129
143
  const [filters, productCount] = await Promise.all([
130
144
  cached(bapiClient.filters.get, {
131
- cacheKeyPrefix: `get-filters-${category}`
145
+ cacheKeyPrefix: `get-filters-${categoryId}`
132
146
  })({
133
147
  where: {
134
- categoryId: result?.id,
148
+ categoryId,
135
149
  minPrice: where?.minPrice,
136
150
  maxPrice: where?.maxPrice,
137
151
  term: where?.term,
@@ -148,10 +162,10 @@ export const getFilters = async function getFilters2({
148
162
  }),
149
163
  cached(bapiClient.products.query, {
150
164
  ttl: 15 * MINUTE,
151
- cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
165
+ cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : categoryId}`
152
166
  })({
153
167
  where: {
154
- categoryId: result?.id,
168
+ categoryId,
155
169
  minPrice: where?.minPrice,
156
170
  maxPrice: where?.maxPrice,
157
171
  term: where?.term,
@@ -173,28 +187,30 @@ export const getFilters = async function getFilters2({
173
187
  unfilteredCount: productCount?.pagination.total
174
188
  };
175
189
  };
176
- export const getProductsByCategory = async function getProductsByCategory2({
177
- category = "/",
178
- cache,
179
- with: _with,
180
- perPage = 20,
181
- page = 1,
182
- where = void 0,
183
- sort = void 0,
184
- pricePromotionKey = "",
185
- includeSellableForFree = void 0,
186
- includeSoldOut = void 0,
187
- orFiltersOperator = void 0
188
- }, context) {
189
- let result;
190
+ export const getProductsByCategory = async function getProductsByCategory2(params, context) {
191
+ const {
192
+ cache,
193
+ with: _with,
194
+ perPage = 20,
195
+ page = 1,
196
+ where = void 0,
197
+ sort = void 0,
198
+ pricePromotionKey = "",
199
+ includeSellableForFree = void 0,
200
+ includeSoldOut = void 0,
201
+ orFiltersOperator = void 0
202
+ } = params;
190
203
  const { cached, bapiClient, campaignKey } = context;
191
- if (category !== "/") {
192
- result = await cached(bapiClient.categories.getByPath, {
193
- cacheKeyPrefix: `getByPath-categories-${category}`
194
- })(splitAndRemoveEmpty(category));
195
- }
204
+ const { category, categoryId } = await resolveCategoryIdFromParams(
205
+ context,
206
+ params
207
+ );
196
208
  const response = await fetchAllFiltersForCategory(
197
- { category: { id: result?.id, slug: category } },
209
+ {
210
+ category: {
211
+ id: categoryId
212
+ }
213
+ },
198
214
  context
199
215
  );
200
216
  if (response instanceof ErrorResponse) {
@@ -210,12 +226,12 @@ export const getProductsByCategory = async function getProductsByCategory2({
210
226
  {
211
227
  ...cache,
212
228
  ttl: 15 * MINUTE,
213
- cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
229
+ cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : categoryId}`
214
230
  }
215
231
  )({
216
232
  where: {
217
233
  term: where?.term,
218
- categoryId: result?.id,
234
+ categoryId,
219
235
  minPrice: where?.minPrice,
220
236
  maxPrice: where?.maxPrice,
221
237
  attributes: [
@@ -15,8 +15,7 @@ export interface FilterParams {
15
15
  sort?: ProductSortConfig;
16
16
  orFiltersOperator?: ProductsSearchEndpointParameters['orFiltersOperator'];
17
17
  }
18
- export interface FetchFiltersParams {
19
- category: string;
18
+ export type FetchFiltersParams = {
20
19
  includedFilters?: Array<string>;
21
20
  where?: {
22
21
  term?: ProductSearchQuery['term'];
@@ -28,7 +27,13 @@ export interface FetchFiltersParams {
28
27
  includeSoldOut?: boolean;
29
28
  includeSellableForFree?: boolean;
30
29
  orFiltersOperator?: ProductsSearchEndpointParameters['orFiltersOperator'];
31
- }
30
+ } & ({
31
+ category?: string;
32
+ categoryId?: undefined;
33
+ } | {
34
+ categoryId: number;
35
+ category?: undefined;
36
+ });
32
37
  export interface FetchFiltersResponse {
33
38
  filters: FiltersEndpointResponseData;
34
39
  unfilteredCount: number;
@@ -43,8 +43,7 @@ export interface FetchProductsByReferenceKeysParams {
43
43
  with?: ProductWith;
44
44
  pricePromotionKey?: string;
45
45
  }
46
- export interface FetchProductsByCategoryParams {
47
- category: string;
46
+ export type FetchProductsByCategoryParams = {
48
47
  with?: ProductWith;
49
48
  cache?: CacheOptions;
50
49
  includedFilters?: Array<string>;
@@ -63,14 +62,18 @@ export interface FetchProductsByCategoryParams {
63
62
  includeSellableForFree?: boolean;
64
63
  includeSoldOut?: boolean;
65
64
  orFiltersOperator?: ProductsSearchEndpointParameters['orFiltersOperator'];
66
- }
65
+ } & ({
66
+ category?: string;
67
+ categoryId?: undefined;
68
+ } | {
69
+ categoryId: number;
70
+ category?: undefined;
71
+ });
67
72
  export interface FetchProductsByCategoryResponse {
68
73
  products: Product[];
69
74
  pagination: ProductsByIdsEndpointResponseData['pagination'];
70
75
  }
71
- export interface FetchProductsCountParams {
72
- category?: string;
73
- categoryId?: number;
76
+ export type FetchProductsCountParams = {
74
77
  where?: {
75
78
  term?: ProductSearchQuery['term'];
76
79
  minPrice?: ProductSearchQuery['minPrice'];
@@ -80,7 +83,13 @@ export interface FetchProductsCountParams {
80
83
  includeSoldOut?: boolean;
81
84
  includeSellableForFree?: boolean;
82
85
  orFiltersOperator?: ProductsSearchEndpointParameters['orFiltersOperator'];
83
- }
86
+ } & ({
87
+ category?: string;
88
+ categoryId?: undefined;
89
+ } | {
90
+ categoryId: number;
91
+ category?: undefined;
92
+ });
84
93
  export interface FetchProductsCountResponse {
85
94
  count: number;
86
95
  }
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.verifyToken = exports.sha256 = exports.md5 = exports.hmac = exports.encodeBase64 = exports.decodeBase64 = exports.buildSignature = exports.buildHashPayload = void 0;
6
+ exports.verifyOrderSuccessToken = exports.sha256 = exports.md5 = exports.hmac = exports.encodeBase64 = exports.decodeBase64 = exports.buildSignature = exports.buildHashPayload = void 0;
7
7
  var _uncrypto = require("uncrypto");
8
8
  const md5 = async value => {
9
9
  if (!process.env.SFC_OMIT_MD5) {
@@ -54,14 +54,15 @@ const decodeBase64 = string => {
54
54
  return new TextDecoder().decode(base64ToBytes(string));
55
55
  };
56
56
  exports.decodeBase64 = decodeBase64;
57
- const verifyToken = async (token, secret) => {
57
+ const verifyOrderSuccessToken = async (token, secret) => {
58
58
  const [encodedPayload, signature] = token.split(".", 2);
59
59
  const expectedSignature = encodeBase64(await hmac(encodedPayload, secret));
60
60
  if (expectedSignature === signature) {
61
- return decodeBase64(encodedPayload);
61
+ return JSON.parse(decodeBase64(encodedPayload));
62
62
  }
63
+ return void 0;
63
64
  };
64
- exports.verifyToken = verifyToken;
65
+ exports.verifyOrderSuccessToken = verifyOrderSuccessToken;
65
66
  const buildHashPayload = payload => encodeBase64(JSON.stringify(payload));
66
67
  exports.buildHashPayload = buildHashPayload;
67
68
  const buildSignature = async (payloadHash, secret) => encodeBase64(await hmac(payloadHash, secret));
@@ -3,6 +3,6 @@ export declare const sha256: (value: string) => Promise<string>;
3
3
  export declare const hmac: (value: string, secret: string, algorithm?: string) => Promise<string>;
4
4
  export declare const encodeBase64: (string: string) => string;
5
5
  export declare const decodeBase64: (string: string) => string;
6
- export declare const verifyToken: (token: string, secret: string) => Promise<string | undefined>;
6
+ export declare const verifyOrderSuccessToken: <T = unknown>(token: string, secret: string) => Promise<T | undefined>;
7
7
  export declare const buildHashPayload: (payload: unknown) => string;
8
8
  export declare const buildSignature: (payloadHash: string, secret: string) => Promise<string>;
@@ -50,12 +50,13 @@ export const decodeBase64 = (string) => {
50
50
  }
51
51
  return new TextDecoder().decode(base64ToBytes(string));
52
52
  };
53
- export const verifyToken = async (token, secret) => {
53
+ export const verifyOrderSuccessToken = async (token, secret) => {
54
54
  const [encodedPayload, signature] = token.split(".", 2);
55
55
  const expectedSignature = encodeBase64(await hmac(encodedPayload, secret));
56
56
  if (expectedSignature === signature) {
57
- return decodeBase64(encodedPayload);
57
+ return JSON.parse(decodeBase64(encodedPayload));
58
58
  }
59
+ return void 0;
59
60
  };
60
61
  export const buildHashPayload = (payload) => encodeBase64(JSON.stringify(payload));
61
62
  export const buildSignature = async (payloadHash, secret) => encodeBase64(await hmac(payloadHash, secret));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-core",
3
- "version": "7.64.4",
3
+ "version": "7.65.1",
4
4
  "description": "Collection of essential utilities to work with the Storefront API",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -73,7 +73,7 @@
73
73
  "@types/crypto-js": "4.2.2",
74
74
  "@types/node": "20.16.5",
75
75
  "@types/webpack-env": "1.18.5",
76
- "@vitest/coverage-v8": "2.0.5",
76
+ "@vitest/coverage-v8": "2.1.1",
77
77
  "dprint": "0.47.2",
78
78
  "eslint": "9.10.0",
79
79
  "eslint-formatter-gitlab": "5.1.0",
@@ -83,7 +83,7 @@
83
83
  "typescript": "5.6.2",
84
84
  "unbuild": "2.0.0",
85
85
  "unstorage": "1.12.0",
86
- "vitest": "2.0.5"
86
+ "vitest": "2.1.1"
87
87
  },
88
88
  "optionalDependencies": {
89
89
  "redis": "4"