@scayle/storefront-core 8.7.1 → 8.8.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,11 @@
1
1
  # @scayle/storefront-core
2
2
 
3
+ ## 8.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Deprecate `RpcContext.campaignKey`. The `getCampaignKey` RPC should be used instead.
8
+
3
9
  ## 8.7.1
4
10
 
5
11
  ### Patch Changes
@@ -9,6 +9,7 @@ import {
9
9
  import { mergeBaskets as mergeBasketFunction } from "../../../utils/user.mjs";
10
10
  import { unwrap } from "../../../utils/response.mjs";
11
11
  import { wasAddedWithReducedQuantity } from "../../../utils/index.mjs";
12
+ import { getCampaignKey } from "../campaign.mjs";
12
13
  const SAPI_ERROR_NAME = "SAPI ERROR";
13
14
  function getWithParams(params, context) {
14
15
  return params.with ?? context.withParams?.basket ?? MIN_WITH_PARAMS_BASKET;
@@ -31,7 +32,8 @@ export const addItemToBasket = async function addItemToBasket2({
31
32
  "No Session found"
32
33
  );
33
34
  }
34
- const { campaignKey, sapiClient, basketKey } = context;
35
+ const { sapiClient, basketKey } = context;
36
+ const campaignKey = await getCampaignKey(context);
35
37
  const resolvedWith = getWithParams(
36
38
  { with: _with },
37
39
  context
@@ -91,7 +93,8 @@ export const addItemsToBasket = async function addItemsToBasket2(params, context
91
93
  "No Session found"
92
94
  );
93
95
  }
94
- const { campaignKey, sapiClient, basketKey } = context;
96
+ const { sapiClient, basketKey } = context;
97
+ const campaignKey = await getCampaignKey(context);
95
98
  const resolvedWith = getWithParams(params, context);
96
99
  const itemsToBeAddedOrUpdated = params.items.map((item) => ({
97
100
  variantId: item.variantId,
@@ -151,7 +154,8 @@ export const getBasket = async function getBasket2(options, context) {
151
154
  "No Session found"
152
155
  );
153
156
  }
154
- const { sapiClient, campaignKey, basketKey } = context;
157
+ const { sapiClient, basketKey } = context;
158
+ const campaignKey = await getCampaignKey(context);
155
159
  const resolvedWith = getWithParams(
156
160
  { with: options },
157
161
  context
@@ -183,7 +187,8 @@ export const removeItemFromBasket = async function removeItemFromBasket2(options
183
187
  "No Session found"
184
188
  );
185
189
  }
186
- const { sapiClient, campaignKey, basketKey } = context;
190
+ const { sapiClient, basketKey } = context;
191
+ const campaignKey = await getCampaignKey(context);
187
192
  const resolvedWith = getWithParams(options, context);
188
193
  const basket = await sapiClient.basket.deleteItem(
189
194
  basketKey,
@@ -0,0 +1,13 @@
1
+ import type { RpcContext } from '../../types';
2
+ /**
3
+ * RPC method to retrieve the current campaign key.
4
+ * Used by other RPC methods which depend on a campaign key.
5
+ * Will return the key for the earliest active campaign.
6
+ *
7
+ * Uses SAPI's list-campaigns endpoint
8
+ * @see https://scayle.dev/en/api-guides/storefront-api/resources/campaigns/list-campaigns
9
+ *
10
+ * @param context The RPC Context
11
+ * @returns The current active campaign key.
12
+ */
13
+ export declare const getCampaignKey: (context: RpcContext) => Promise<string | undefined>;
@@ -0,0 +1,23 @@
1
+ import {
2
+ campaignHasNotEnded,
3
+ sortCampaignsByDateAscending,
4
+ isCampaignActive
5
+ } from "../../utils/campaign.mjs";
6
+ export const getCampaignKey = async (context) => {
7
+ if (context.campaignKey) {
8
+ return context.campaignKey;
9
+ }
10
+ const { cached, sapiClient } = context;
11
+ const { campaigns } = await cached(async () => {
12
+ const { entities } = await sapiClient.campaigns.get();
13
+ return {
14
+ campaigns: entities.filter((campaign) => {
15
+ return campaignHasNotEnded(campaign);
16
+ }).sort(sortCampaignsByDateAscending)
17
+ };
18
+ }, {
19
+ cacheKeyPrefix: "get-campaignKey",
20
+ ttl: 5 * 60
21
+ })();
22
+ return campaigns.find(isCampaignActive)?.key;
23
+ };
@@ -3,6 +3,7 @@ import { hasSession } from "../../../types/index.mjs";
3
3
  import { ErrorResponse } from "../../../errors/index.mjs";
4
4
  import { HttpStatusCode, HttpStatusMessage } from "../../../constants/index.mjs";
5
5
  import { getAccessToken } from "../user.mjs";
6
+ import { getCampaignKey } from "../campaign.mjs";
6
7
  export const getCheckoutToken = async function getCheckoutToken2(jwtPayload = {}, context) {
7
8
  if (!hasSession(context)) {
8
9
  return new ErrorResponse(
@@ -28,14 +29,15 @@ export const getCheckoutToken = async function getCheckoutToken2(jwtPayload = {}
28
29
  const secret = new TextEncoder().encode(context.checkout.secret);
29
30
  const now = /* @__PURE__ */ new Date();
30
31
  const { voucher, customData, preferredCollectionPoint, carrier } = jwtPayload;
32
+ const campaignKey = await getCampaignKey(context);
31
33
  const checkoutJwt = await new SignJWT({
32
34
  voucher,
33
35
  customData,
34
36
  preferredCollectionPoint,
35
37
  carrier,
36
38
  basketId: context.basketKey,
37
- campaignKey: context.campaignKey
38
- }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.7.1"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
39
+ campaignKey
40
+ }).setIssuedAt(now).setNotBefore(now).setExpirationTime("1h").setIssuer(`${"@scayle/storefront-core"}@${"8.8.0"}`).setProtectedHeader({ alg: "HS256", typ: "JWT" }).sign(secret);
39
41
  return {
40
42
  accessToken: refreshedAccessToken,
41
43
  checkoutJwt
@@ -1,6 +1,7 @@
1
1
  export * from './basket/basket';
2
2
  export * from './brands';
3
3
  export * from './categories';
4
+ export { getCampaignKey } from './campaign';
4
5
  export * from './cbd';
5
6
  export { getProductById, getProductsByIds, getProductsByReferenceKeys, getProductsCount, fetchAllFiltersForCategory, getFilters, getProductsByCategory, } from './products';
6
7
  export * from './search';
@@ -1,6 +1,7 @@
1
1
  export * from "./basket/basket.mjs";
2
2
  export * from "./brands.mjs";
3
3
  export * from "./categories.mjs";
4
+ export { getCampaignKey } from "./campaign.mjs";
4
5
  export * from "./cbd.mjs";
5
6
  export {
6
7
  getProductById,
@@ -20,9 +20,9 @@ export declare function resolveCategoryIdFromParams(context: RpcContext, params:
20
20
  category: string | undefined;
21
21
  categoryId: number | undefined;
22
22
  }>;
23
- export declare const getProductById: (options: FetchProductParams, { sapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Response | Product>;
24
- export declare const getProductsByIds: (options: FetchProductsByIdsParams, { sapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Response | Product[]>;
25
- export declare const getProductsByReferenceKeys: (options: FetchProductsByReferenceKeysParams, { sapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Response | Product[]>;
23
+ export declare const getProductById: (options: FetchProductParams, context: RpcContext) => Promise<Response | Product>;
24
+ export declare const getProductsByIds: (options: FetchProductsByIdsParams, context: RpcContext) => Promise<Response | Product[]>;
25
+ export declare const getProductsByReferenceKeys: (options: FetchProductsByReferenceKeysParams, context: RpcContext) => Promise<Response | Product[]>;
26
26
  export declare const getProductsCount: (params: FetchProductsCountParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
27
27
  count: number;
28
28
  }>;
@@ -31,7 +31,7 @@ export declare const fetchAllFiltersForCategory: ({ includedFilters, category }:
31
31
  category: {
32
32
  id?: number;
33
33
  };
34
- }, { cached, sapiClient, campaignKey }: RpcContext) => Promise<string[]>;
34
+ }, context: RpcContext) => Promise<string[]>;
35
35
  export declare const getFilters: (params: FetchFiltersParams, context: RpcContext) => Promise<(string[] & ErrorResponse) | {
36
36
  filters: FiltersEndpointResponseData;
37
37
  unfilteredCount: number;
@@ -4,6 +4,7 @@ import { MINUTE } from "../../cache/index.mjs";
4
4
  import { unwrap } from "../../utils/response.mjs";
5
5
  import { mapSAPIFetchErrorToResponse } from "../../utils/sapi.mjs";
6
6
  import { ErrorResponse } from "../../errors/index.mjs";
7
+ import { getCampaignKey } from "./campaign.mjs";
7
8
  const MAX_PER_PAGE = 100;
8
9
  const sanitizeAttributesForSAPI = ({
9
10
  attributes,
@@ -34,7 +35,9 @@ export async function resolveCategoryIdFromParams(context, params) {
34
35
  }
35
36
  return { category, categoryId };
36
37
  }
37
- export const getProductById = async function getProductById2(options, { sapiClient, cached, campaignKey, withParams }) {
38
+ export const getProductById = async function getProductById2(options, context) {
39
+ const { sapiClient, cached, withParams } = context;
40
+ const campaignKey = await getCampaignKey(context);
38
41
  return await cached(
39
42
  mapSAPIFetchErrorToResponse(sapiClient.products.getById),
40
43
  {
@@ -48,7 +51,9 @@ export const getProductById = async function getProductById2(options, { sapiClie
48
51
  includeSellableForFree: options.includeSellableForFree
49
52
  });
50
53
  };
51
- export const getProductsByIds = async function getProductsByIds2(options, { sapiClient, cached, campaignKey, withParams }) {
54
+ export const getProductsByIds = async function getProductsByIds2(options, context) {
55
+ const { sapiClient, cached, withParams } = context;
56
+ const campaignKey = await getCampaignKey(context);
52
57
  return await cached(
53
58
  mapSAPIFetchErrorToResponse(sapiClient.products.getByIds),
54
59
  {
@@ -61,7 +66,9 @@ export const getProductsByIds = async function getProductsByIds2(options, { sapi
61
66
  pricePromotionKey: options.pricePromotionKey
62
67
  });
63
68
  };
64
- export const getProductsByReferenceKeys = async function getProductsByReferenceKeys2(options, { sapiClient, cached, campaignKey, withParams }) {
69
+ export const getProductsByReferenceKeys = async function getProductsByReferenceKeys2(options, context) {
70
+ const { sapiClient, cached, withParams } = context;
71
+ const campaignKey = await getCampaignKey(context);
65
72
  return await cached(
66
73
  mapSAPIFetchErrorToResponse(sapiClient.products.getByReferenceKeys),
67
74
  {
@@ -82,7 +89,8 @@ export const getProductsCount = async function getProductsCount2(params, context
82
89
  includeSellableForFree = false,
83
90
  orFiltersOperator = void 0
84
91
  } = params;
85
- const { cached, sapiClient, campaignKey } = context;
92
+ const { cached, sapiClient } = context;
93
+ const campaignKey = await getCampaignKey(context);
86
94
  const { categoryId } = await resolveCategoryIdFromParams(context, params);
87
95
  const response = await getSanitizedAttributes(
88
96
  context,
@@ -121,7 +129,9 @@ export const getProductsCount = async function getProductsCount2(params, context
121
129
  count: productCount?.pagination.total
122
130
  };
123
131
  };
124
- export const fetchAllFiltersForCategory = async function fetchAllFiltersForCategory2({ includedFilters = [], category }, { cached, sapiClient, campaignKey }) {
132
+ export const fetchAllFiltersForCategory = async function fetchAllFiltersForCategory2({ includedFilters = [], category }, context) {
133
+ const { cached, sapiClient } = context;
134
+ const campaignKey = await getCampaignKey(context);
125
135
  const fetchAndMapFiltersToSlugs = async () => {
126
136
  const filtersFromSAPI = await cached(sapiClient.filters.get)({
127
137
  where: { categoryId: category.id },
@@ -162,7 +172,8 @@ export const getFilters = async function getFilters2(params, context) {
162
172
  includeSellableForFree = false,
163
173
  orFiltersOperator
164
174
  } = params;
165
- const { cached, sapiClient, campaignKey } = context;
175
+ const { cached, sapiClient } = context;
176
+ const campaignKey = await getCampaignKey(context);
166
177
  const { category, categoryId } = await resolveCategoryIdFromParams(
167
178
  context,
168
179
  params
@@ -241,7 +252,8 @@ export const getProductsByCategory = async function getProductsByCategory2(param
241
252
  orFiltersOperator = void 0,
242
253
  trackSearchAnalyticsEvent = void 0
243
254
  } = params;
244
- const { cached, sapiClient, campaignKey } = context;
255
+ const { cached, sapiClient } = context;
256
+ const campaignKey = await getCampaignKey(context);
245
257
  const { category, categoryId } = await resolveCategoryIdFromParams(
246
258
  context,
247
259
  params
@@ -5,4 +5,4 @@ export interface FetchVariantsParams {
5
5
  include?: ProductWith;
6
6
  pricePromotionKey?: string;
7
7
  }
8
- export declare const getVariantById: ({ ids, include, pricePromotionKey }: FetchVariantsParams, { sapiClient, cached, campaignKey, withParams }: RpcContext) => Promise<Response | VariantDetail[]>;
8
+ export declare const getVariantById: ({ ids, include, pricePromotionKey }: FetchVariantsParams, context: RpcContext) => Promise<Response | VariantDetail[]>;
@@ -1,8 +1,11 @@
1
1
  import { mapSAPIFetchErrorToResponse } from "../../utils/sapi.mjs";
2
+ import { getCampaignKey } from "./campaign.mjs";
2
3
  const defaultWith = {
3
4
  attributes: "all"
4
5
  };
5
- export const getVariantById = async function getVariantById2({ ids, include = defaultWith, pricePromotionKey = "default" }, { sapiClient, cached, campaignKey, withParams }) {
6
+ export const getVariantById = async function getVariantById2({ ids, include = defaultWith, pricePromotionKey = "default" }, context) {
7
+ const { sapiClient, cached, withParams } = context;
8
+ const campaignKey = await getCampaignKey(context);
6
9
  const resolvedWith = include ?? withParams?.variant ?? defaultWith;
7
10
  return await cached(
8
11
  mapSAPIFetchErrorToResponse(sapiClient.variants.getByIds),
@@ -7,6 +7,7 @@ import {
7
7
  import { unwrap } from "../../utils/response.mjs";
8
8
  import { ErrorResponse } from "../../errors/index.mjs";
9
9
  import { mapSAPIFetchErrorToResponse } from "../../utils/sapi.mjs";
10
+ import { getCampaignKey } from "./campaign.mjs";
10
11
  function getWithParams(params, context) {
11
12
  return params.with ?? context.withParams?.wishlist ?? MIN_WITH_PARAMS_WISHLIST;
12
13
  }
@@ -18,7 +19,8 @@ export const getWishlist = async function getWishlist2(options, context) {
18
19
  "No Session found"
19
20
  );
20
21
  }
21
- const { sapiClient, campaignKey, wishlistKey } = context;
22
+ const { sapiClient, wishlistKey } = context;
23
+ const campaignKey = await getCampaignKey(context);
22
24
  const resolvedWith = getWithParams({ with: options }, context);
23
25
  return await mapSAPIFetchErrorToResponse(sapiClient.wishlist.get)(
24
26
  wishlistKey,
@@ -37,7 +39,8 @@ export const addItemToWishlist = async function addItemToWishlist2(options, cont
37
39
  "No Session found"
38
40
  );
39
41
  }
40
- const { sapiClient, campaignKey, wishlistKey } = context;
42
+ const { sapiClient, wishlistKey } = context;
43
+ const campaignKey = await getCampaignKey(context);
41
44
  const { productId, variantId } = options;
42
45
  const resolvedWith = getWithParams(options, context);
43
46
  const pricePromotionKey = resolvedWith?.pricePromotionKey ?? "";
@@ -73,7 +76,8 @@ export const removeItemFromWishlist = async function removeItemFromWishlist2(opt
73
76
  "No Session found"
74
77
  );
75
78
  }
76
- const { sapiClient, campaignKey, wishlistKey } = context;
79
+ const { sapiClient, wishlistKey } = context;
80
+ const campaignKey = await getCampaignKey(context);
77
81
  const resolvedWith = getWithParams(options, context);
78
82
  return await mapSAPIFetchErrorToResponse(sapiClient.wishlist.deleteItem)(
79
83
  wishlistKey,
@@ -77,6 +77,9 @@ export type RpcContext = {
77
77
  shopId: number;
78
78
  domain?: string;
79
79
  withParams?: WithParams;
80
+ /**
81
+ * @deprecated Use the `getCampaignKey` RPC method instead.
82
+ */
80
83
  campaignKey?: string;
81
84
  destroySessionsForUserId: (userId: number, sessionsToKeep?: string[]) => Promise<void>;
82
85
  generateBasketKeyForUserId: (userId: string) => Promise<string>;
@@ -0,0 +1,23 @@
1
+ import type { Campaign } from '@scayle/storefront-api';
2
+ /**
3
+ * Determine if a campaign is active for the current date
4
+ *
5
+ * @param campaign the campaign to test
6
+ * @returns True when the current datetime is within the campaign's timeframe, false otherwise
7
+ */
8
+ export declare const isCampaignActive: (campaign?: Campaign) => boolean;
9
+ /**
10
+ * Determine if a campaign has ended based on the current datetime
11
+ *
12
+ * @param campaign
13
+ * @returns True if the campaign's end date is later than the current datetime, false otherwise
14
+ */
15
+ export declare const campaignHasNotEnded: (campaign: Campaign) => boolean;
16
+ /**
17
+ * Comparison function for campaigns based on their start date. To be used with Array.prototype.sort()
18
+ *
19
+ * @param a The first campaign for comparison
20
+ * @param b The second campaign for comparison
21
+ * @returns A positive value when a should come before b, negative value when b should come before a, or 0 when they are considered equal
22
+ */
23
+ export declare const sortCampaignsByDateAscending: (a: Campaign, b: Campaign) => number;
@@ -0,0 +1,17 @@
1
+ export const isCampaignActive = (campaign) => {
2
+ if (!campaign) {
3
+ return false;
4
+ }
5
+ const timeNow = Date.now();
6
+ const campaignStart = new Date(campaign.start_at).getTime();
7
+ const hasStarted = timeNow >= campaignStart;
8
+ const campaignEnd = new Date(campaign.end_at).getTime();
9
+ const hasEnded = timeNow >= campaignEnd;
10
+ return hasStarted && !hasEnded;
11
+ };
12
+ export const campaignHasNotEnded = (campaign) => {
13
+ return new Date(campaign.end_at).getTime() > Date.now();
14
+ };
15
+ export const sortCampaignsByDateAscending = (a, b) => {
16
+ return new Date(a.start_at).getTime() - new Date(b.start_at).getTime();
17
+ };
@@ -2,3 +2,4 @@ export * from './timeout';
2
2
  export * from './log';
3
3
  export * from './fetch';
4
4
  export * from './basket';
5
+ export * from './campaign';
@@ -2,3 +2,4 @@ export * from "./timeout.mjs";
2
2
  export * from "./log.mjs";
3
3
  export * from "./fetch.mjs";
4
4
  export * from "./basket.mjs";
5
+ export * from "./campaign.mjs";
@@ -1,6 +1,8 @@
1
1
  import { ExistingItemHandling } from "@scayle/storefront-api";
2
+ import { getCampaignKey } from "../rpc/methods/index.mjs";
2
3
  export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, context) => {
3
- const { sapiClient, campaignKey } = context;
4
+ const { sapiClient } = context;
5
+ const campaignKey = await getCampaignKey(context);
4
6
  const fromOriginBasket = await sapiClient.basket.get(fromBasketKey, {
5
7
  with: withOptions,
6
8
  campaignKey,
@@ -43,7 +45,8 @@ export const mergeBaskets = async (fromBasketKey, toBasketKey, withOptions, cont
43
45
  }
44
46
  };
45
47
  export const mergeWishlists = async (fromWishlistKey, toWishlistKey, withOptions, context) => {
46
- const { sapiClient, campaignKey } = context;
48
+ const { sapiClient } = context;
49
+ const campaignKey = await getCampaignKey(context);
47
50
  const oldWishlist = await sapiClient.wishlist.get(fromWishlistKey, {
48
51
  with: withOptions,
49
52
  campaignKey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-core",
3
- "version": "8.7.1",
3
+ "version": "8.8.0",
4
4
  "description": "Collection of essential utilities to work with the Storefront API",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",