@scayle/storefront-core 8.61.2 → 8.62.0

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,42 @@
1
1
  # @scayle/storefront-core
2
2
 
3
+ ## 8.62.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **\[Promotions\]** Added customer-segmented promotion fetches when a valid session token is present.
8
+
9
+ `getPromotions`, `getCurrentPromotions`, and `getPromotionsByIds` accept a `segmentation` flag. When it is `true` and the request has a non-expired access token, the handlers call the Storefront API with `customerToken` and skip the shared cache so results match the logged-in customer. When it is `false`, handlers use the cached anonymous promotion list so customer-specific promotions are not stored in shared cache responses.
10
+
11
+ **\[BREAKING\]** `getPromotionsByIds` now expects `{ ids: string[], segmentation: boolean }` instead of a bare `string[]` array.
12
+ - Before (custom RPC handler via `context.callRpc`):
13
+
14
+ ```ts
15
+ const promotions = await context.callRpc?.('getPromotionsByIds', [
16
+ 'promo-1',
17
+ 'promo-2',
18
+ ])
19
+ ```
20
+
21
+ - After:
22
+
23
+ ```ts
24
+ const promotions = await context.callRpc?.('getPromotionsByIds', {
25
+ ids: ['promo-1', 'promo-2'],
26
+ segmentation: true,
27
+ })
28
+ ```
29
+
30
+ Promotion list handlers use `POST` instead of `GET` so `segmentation` and `customerToken` are not sent in the URL.
31
+
32
+ **\[User\]** Exported `getValidatedAccessToken` to return a JWT only when it is present and not expired. Basket merge and promotion handlers use it when passing `customerToken` to the Storefront API.
33
+
34
+ ## 8.61.3
35
+
36
+ ### Patch Changes
37
+
38
+ - Fix `updateTokens` and `updateUser` return type from `void` to `Promise<void>` in `ContextWithSession`. The underlying implementations were already async (`await session.save()`), but the incorrect `void` type prevented call sites from knowing they needed to await the result. All call sites in the RPC methods and customer API now correctly await these calls, ensuring session state is persisted before execution continues.
39
+
3
40
  ## 8.61.2
4
41
 
5
42
  No changes in this release.
@@ -58,7 +58,7 @@ export class CustomerAPIClient {
58
58
  grant_type: "refresh_token",
59
59
  refresh_token: this.context.refreshToken
60
60
  });
61
- this.context.updateTokens({
61
+ await this.context.updateTokens({
62
62
  accessToken: tokens?.access_token,
63
63
  refreshToken: tokens?.refresh_token
64
64
  });
@@ -41,7 +41,7 @@ export const getCheckoutToken = defineRpcHandler(
41
41
  ...customData,
42
42
  ...orderCustomData
43
43
  }
44
- }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.61.2"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
44
+ }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.62.0"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
45
45
  return {
46
46
  accessToken: refreshedAccessToken,
47
47
  checkoutJwt
@@ -2,45 +2,67 @@ import type { PromotionsParams, PromotionsResponseData, RpcHandler } from '../..
2
2
  /**
3
3
  * Retrieves promotions based on provided parameters.
4
4
  *
5
- * This function uses the Storefront cache (`cached()`) with a `getPromotions` key prefix to improve performance.
5
+ * Uses the Storefront cache (`cached()`) with a `getPromotions` key prefix to improve performance.
6
6
  * Cached entries are returned if found; otherwise, data is fetched and cached.
7
7
  *
8
+ * When `segmentation` is `true` and the request includes a valid customer access token, promotions are
9
+ * fetched directly from the Storefront API and are not cached.
10
+ *
8
11
  * @param params The parameters for retrieving promotions.
9
12
  * @param params.pagination Pagination settings.
10
13
  * @param params.pagination.page Page number.
11
14
  * @param params.pagination.perPage Number of items per page.
15
+ * @param params.segmentation When `true`, fetches customer-segmented promotions without caching.
16
+ * Defaults to `false`.
12
17
  * @param context The RPC context.
13
18
  *
14
19
  * @returns A Promise that resolves with the promotion data.
15
20
  * It will return an `ErrorResponse` if the The Storefront API request fails.
16
21
  */
17
- export declare const getPromotions: RpcHandler<Omit<PromotionsParams, 'ids'>, PromotionsResponseData>;
22
+ export declare const getPromotions: RpcHandler<Omit<PromotionsParams, 'ids' | 'customerToken'> & {
23
+ segmentation: boolean;
24
+ }, PromotionsResponseData>;
18
25
  /**
19
26
  * Retrieves currently active promotions.
20
27
  *
21
- * This function uses the Storefront cache (`cached()`) with a `getPromotions` key prefix to improve performance.
28
+ * Uses the Storefront cache (`cached()`) with a `getPromotions` key prefix to improve performance.
22
29
  * Cached entries are returned if found; otherwise, data is fetched and cached.
23
30
  *
31
+ * When `segmentation` is `true` and the request includes a valid customer access token, promotions are
32
+ * fetched directly from the Storefront API and are not cached.
33
+ *
24
34
  * @param params The parameters for retrieving promotions.
25
35
  * @param params.pagination Pagination settings.
26
36
  * @param params.pagination.page Page number.
27
37
  * @param params.pagination.perPage Number of items per page.
38
+ * @param params.segmentation When `true`, fetches customer-segmented promotions without caching.
39
+ * Defaults to `false`.
28
40
  * @param context The RPC context.
29
41
  *
30
42
  * @returns A Promise that resolves with the current promotion data.
31
43
  * It will return an `ErrorResponse` if the The Storefront API request fails.
32
44
  */
33
- export declare const getCurrentPromotions: RpcHandler<Omit<PromotionsParams, 'ids' | 'activeAt'>, PromotionsResponseData>;
45
+ export declare const getCurrentPromotions: RpcHandler<Omit<PromotionsParams, 'ids' | 'activeAt' | 'customerToken'> & {
46
+ segmentation: boolean;
47
+ }, PromotionsResponseData>;
34
48
  /**
35
49
  * Retrieves promotions by their IDs.
36
50
  *
37
- * This function uses the Storefront cache (`cached()`) with a `getByIds-promotions` key prefix to improve performance.
51
+ * Uses the Storefront cache (`cached()`) with a `getByIds-promotions` key prefix to improve performance.
38
52
  * Cached entries are returned if found; otherwise, data is fetched and cached.
39
53
  *
40
- * @param ids An array of promotion IDs.
54
+ * When `segmentation` is `true` and the request includes a valid customer access token, promotions are
55
+ * fetched directly from the Storefront API and are not cached.
56
+ *
57
+ * @param params Request parameters.
58
+ * @param params.ids Promotion IDs to retrieve.
59
+ * @param params.segmentation When `true`, fetches customer-segmented promotions without caching.
41
60
  * @param context The RPC context.
42
61
  *
43
62
  * @returns A Promise that resolves with the promotion data.
44
63
  * It will return an `ErrorResponse` if the The Storefront API request fails.
45
64
  */
46
- export declare const getPromotionsByIds: RpcHandler<string[], PromotionsResponseData>;
65
+ export declare const getPromotionsByIds: RpcHandler<{
66
+ ids: string[];
67
+ segmentation: boolean;
68
+ }, PromotionsResponseData>;
@@ -4,6 +4,7 @@ import {
4
4
  } from "../../constants/index.mjs";
5
5
  import { mapSAPIFetchErrorToResponse } from "../../utils/sapi.mjs";
6
6
  import { defineRpcHandler } from "../../utils/index.mjs";
7
+ import { getValidatedAccessToken } from "../../utils/user.mjs";
7
8
  const setPaginationDefault = (pagination) => {
8
9
  return {
9
10
  page: pagination?.page || PROMOTION_PAGE_DEFAULT,
@@ -11,42 +12,69 @@ const setPaginationDefault = (pagination) => {
11
12
  };
12
13
  };
13
14
  export const getPromotions = defineRpcHandler(
14
- async (params = {}, context) => {
15
+ async (params = { segmentation: false }, context) => {
15
16
  const { sapiClient, cached } = context;
17
+ const { segmentation, ...promotionsParams } = params;
18
+ const customerToken = getValidatedAccessToken(context.accessToken);
19
+ if (segmentation && customerToken) {
20
+ return mapSAPIFetchErrorToResponse(sapiClient.promotions.get)({
21
+ ...promotionsParams,
22
+ pagination: setPaginationDefault(params.pagination),
23
+ customerToken
24
+ });
25
+ }
16
26
  return await cached(
17
27
  mapSAPIFetchErrorToResponse(sapiClient.promotions.get),
18
28
  {
19
29
  cacheKeyPrefix: "getPromotions"
20
30
  }
21
31
  )({
22
- ...params,
32
+ ...promotionsParams,
23
33
  pagination: setPaginationDefault(params.pagination)
24
34
  });
25
35
  },
26
- { method: "GET" }
36
+ { method: "POST" }
27
37
  );
28
38
  export const getCurrentPromotions = defineRpcHandler(
29
- async (params = {}, context) => {
39
+ async (params = { segmentation: false }, context) => {
30
40
  const { sapiClient, cached } = context;
31
41
  const now = /* @__PURE__ */ new Date();
32
42
  now.setSeconds(0, 0);
33
43
  const activeAt = now.toISOString();
44
+ const { segmentation, ...promotionsParams } = params;
45
+ const customerToken = getValidatedAccessToken(context.accessToken);
46
+ if (segmentation && customerToken) {
47
+ return mapSAPIFetchErrorToResponse(sapiClient.promotions.get)({
48
+ ...promotionsParams,
49
+ activeAt,
50
+ pagination: setPaginationDefault(params.pagination),
51
+ customerToken
52
+ });
53
+ }
34
54
  return await cached(
35
55
  mapSAPIFetchErrorToResponse(sapiClient.promotions.get),
36
56
  {
37
57
  cacheKeyPrefix: "getPromotions"
38
58
  }
39
59
  )({
40
- ...params,
60
+ ...promotionsParams,
41
61
  activeAt,
42
62
  pagination: setPaginationDefault(params.pagination)
43
63
  });
44
64
  },
45
- { method: "GET" }
65
+ { method: "POST" }
46
66
  );
47
67
  export const getPromotionsByIds = defineRpcHandler(
48
- async (ids, context) => {
68
+ async (params, context) => {
49
69
  const { sapiClient, cached } = context;
70
+ const { ids, segmentation } = params;
71
+ const customerToken = getValidatedAccessToken(context.accessToken);
72
+ if (segmentation && customerToken) {
73
+ return mapSAPIFetchErrorToResponse(sapiClient.promotions.getByIds)(
74
+ ids,
75
+ customerToken
76
+ );
77
+ }
50
78
  return await cached(
51
79
  mapSAPIFetchErrorToResponse(sapiClient.promotions.getByIds),
52
80
  {
@@ -54,5 +82,5 @@ export const getPromotionsByIds = defineRpcHandler(
54
82
  }
55
83
  )(ids);
56
84
  },
57
- { method: "GET" }
85
+ { method: "POST" }
58
86
  );
@@ -36,11 +36,11 @@ export async function postLogin(context, tokens) {
36
36
  return fetchUserResponse;
37
37
  }
38
38
  const user = await unwrap(fetchUserResponse);
39
- context.updateTokens({
39
+ await context.updateTokens({
40
40
  accessToken: tokens.access_token,
41
41
  refreshToken: tokens.refresh_token
42
42
  });
43
- context.updateUser(user);
43
+ await context.updateUser(user);
44
44
  const { customerId } = decodeJwt(tokens.access_token);
45
45
  await Promise.all([
46
46
  mergeBaskets(
@@ -201,7 +201,7 @@ export const refreshAccessToken = defineRpcHandler(
201
201
  grant_type: "refresh_token",
202
202
  refresh_token: refreshToken
203
203
  });
204
- context.updateTokens({
204
+ await context.updateTokens({
205
205
  accessToken: tokens?.access_token,
206
206
  refreshToken: tokens?.refresh_token
207
207
  });
@@ -337,7 +337,7 @@ export const updatePasswordByHash = defineRpcHandler(
337
337
  ...passwordHash,
338
338
  shop_id: shopId
339
339
  });
340
- context.updateTokens({
340
+ await context.updateTokens({
341
341
  accessToken: tokens.access_token,
342
342
  refreshToken: tokens.refresh_token
343
343
  });
@@ -98,7 +98,7 @@ const getAccessToken = defineRpcHandler(
98
98
  grant_type: "refresh_token",
99
99
  refresh_token: context.refreshToken
100
100
  });
101
- context.updateTokens({
101
+ await context.updateTokens({
102
102
  accessToken: tokens.access_token,
103
103
  refreshToken: tokens.refresh_token
104
104
  });
@@ -72,8 +72,8 @@ export interface ContextWithSession {
72
72
  destroySession: () => Promise<void>;
73
73
  createUserBoundSession: () => Promise<void>;
74
74
  updateSessionCustomData: (updatedCustomData: SessionCustomData) => Promise<void>;
75
- updateUser: (user: ShopUser) => void;
76
- updateTokens: (tokens: OAuthTokens) => void;
75
+ updateUser: (user: ShopUser) => Promise<void>;
76
+ updateTokens: (tokens: OAuthTokens) => Promise<void>;
77
77
  }
78
78
  /**
79
79
  * Context interface without session information.
@@ -6,7 +6,7 @@ export const getValidatedAccessToken = (accessToken) => {
6
6
  }
7
7
  const payload = decodeJwt(accessToken);
8
8
  if (!payload.exp || payload.exp < Date.now() / 1e3) {
9
- return;
9
+ return void 0;
10
10
  }
11
11
  return accessToken;
12
12
  };
@@ -51,7 +51,9 @@ export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, cont
51
51
  }
52
52
  );
53
53
  fromBasket.items.map(async (item) => {
54
- return await sapiClient.basket.deleteItem(fromBasketKey, item.key);
54
+ return await sapiClient.basket.deleteItem(fromBasketKey, item.key, {
55
+ customerToken: getValidatedAccessToken(context.accessToken) ?? void 0
56
+ });
55
57
  });
56
58
  return mergedBasket;
57
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-core",
3
- "version": "8.61.2",
3
+ "version": "8.62.0",
4
4
  "description": "Collection of essential utilities to work with the Storefront API",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "fishery": "^2.2.3"
42
42
  },
43
43
  "dependencies": {
44
- "@scayle/storefront-api": "^19.0.0",
44
+ "@scayle/storefront-api": "^19.1.0",
45
45
  "@scayle/unstorage-scayle-kv-driver": "^2.1.0",
46
46
  "crypto-js": "^4.2.0",
47
47
  "hookable": "^5.5.3",
@@ -56,20 +56,19 @@
56
56
  "@types/crypto-js": "4.2.2",
57
57
  "@types/node": "24.12.2",
58
58
  "@types/webpack-env": "1.18.8",
59
- "@vitest/coverage-v8": "4.1.5",
59
+ "@vitest/coverage-v8": "4.1.7",
60
60
  "eslint-formatter-gitlab": "7.1.0",
61
- "eslint": "10.3.0",
61
+ "eslint": "10.4.0",
62
62
  "fishery": "2.4.0",
63
- "publint": "0.3.18",
64
- "rimraf": "6.1.3",
63
+ "publint": "0.3.21",
65
64
  "typescript": "6.0.3",
66
65
  "unbuild": "3.6.1",
67
66
  "unstorage": "1.17.5",
68
- "vitest": "4.1.5",
67
+ "vitest": "4.1.7",
69
68
  "@scayle/vitest-config-storefront": "1.0.0"
70
69
  },
71
70
  "scripts": {
72
- "clean": "rimraf ./dist",
71
+ "clean": "node -e \"require('fs').rmSync('./dist',{recursive:true,force:true})\"",
73
72
  "build": "unbuild",
74
73
  "typecheck": "tsc --noEmit -p tsconfig.json",
75
74
  "lint": "eslint .",