@adobe/spacecat-shared-utils 1.46.0 → 1.48.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,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-utils-v1.48.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.47.0...@adobe/spacecat-shared-utils-v1.48.0) (2025-08-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * added `urlMatchesFilter` in `spacecat-shared-utils` ([#921](https://github.com/adobe/spacecat-shared/issues/921)) ([74e11e4](https://github.com/adobe/spacecat-shared/commit/74e11e4124137b13942b0c58ead620905c438538))
7
+
8
+ # [@adobe/spacecat-shared-utils-v1.47.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.46.0...@adobe/spacecat-shared-utils-v1.47.0) (2025-08-15)
9
+
10
+
11
+ ### Features
12
+
13
+ * add support for month and last full week and last full month in shared ([#915](https://github.com/adobe/spacecat-shared/issues/915)) ([bdf3f3e](https://github.com/adobe/spacecat-shared/commit/bdf3f3e5bd6b9e749368cd72cc375bdb6fa83e2c))
14
+
1
15
  # [@adobe/spacecat-shared-utils-v1.46.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.45.0...@adobe/spacecat-shared-utils-v1.46.0) (2025-08-14)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "engines": {
@@ -52,6 +52,7 @@
52
52
  "@aws-sdk/client-sqs": "3.864.0",
53
53
  "@json2csv/plainjs": "7.0.6",
54
54
  "aws-xray-sdk": "3.10.3",
55
- "uuid": "11.1.0"
55
+ "uuid": "11.1.0",
56
+ "date-fns": "2.30.0"
56
57
  }
57
58
  }
@@ -9,69 +9,104 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
13
- const MILLISECONDS_IN_A_WEEK = 7 * MILLISECONDS_IN_A_DAY;
14
-
15
- const getFirstMondayOfYear = (year) => {
16
- const jan4 = new Date(Date.UTC(year, 0, 4));
17
- return new Date(Date.UTC(year, 0, 4 - (jan4.getUTCDay() || 7) + 1));
18
- };
19
-
20
- const getThursdayOfWeek = (date) => {
21
- const thursday = new Date(date.getTime());
22
- const dayOfWeek = date.getUTCDay() || 7;
23
- thursday.setUTCDate(date.getUTCDate() + 4 - dayOfWeek);
24
- return thursday;
25
- };
26
-
27
- const has53CalendarWeeks = (year) => {
28
- const jan1 = new Date(Date.UTC(year, 0, 1));
29
- const dec31 = new Date(Date.UTC(year, 11, 31));
12
+ import {
13
+ startOfWeek as dfStartOfWeek,
14
+ subWeeks,
15
+ getISOWeek,
16
+ getISOWeekYear,
17
+ } from 'date-fns';
18
+
19
+ const MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
20
+ const MILLIS_IN_WEEK = 7 * MILLIS_IN_DAY;
21
+
22
+ function createUTCDate(year, month, day) {
23
+ // If year is < 100, normalize to the current UTC year as requested
24
+ if (!Number.isInteger(year) || year < 100) {
25
+ const currentYear = new Date().getUTCFullYear();
26
+ return new Date(Date.UTC(currentYear, month, day));
27
+ }
28
+ return new Date(Date.UTC(year, month, day));
29
+ }
30
+
31
+ function getFirstMondayOfYear(year) {
32
+ const jan4 = createUTCDate(year, 0, 4);
33
+ return createUTCDate(year, 0, 4 - (jan4.getUTCDay() || 7) + 1);
34
+ }
35
+
36
+ function has53CalendarWeeks(year) {
37
+ const jan1 = createUTCDate(year, 0, 1);
38
+ const dec31 = createUTCDate(year, 11, 31);
30
39
  return jan1.getUTCDay() === 4 || dec31.getUTCDay() === 4;
31
- };
40
+ }
32
41
 
33
- const isValidWeek = (week, year) => {
34
- if (year < 100 || week < 1) return false;
42
+ function isValidWeek(week, year) {
43
+ if (!Number.isInteger(year) || year < 100 || !Number.isInteger(week) || week < 1) return false;
35
44
  if (week === 53) return has53CalendarWeeks(year);
36
45
  return week <= 52;
37
- };
38
-
39
- const getLastFullCalendarWeek = () => {
40
- const currentDate = new Date();
41
- currentDate.setUTCHours(0, 0, 0, 0);
46
+ }
42
47
 
43
- const previousWeekDate = new Date(currentDate.getTime() - MILLISECONDS_IN_A_WEEK);
48
+ function isValidMonth(month, year) {
49
+ return Number.isInteger(year)
50
+ && year >= 100 && Number.isInteger(month) && month >= 1 && month <= 12;
51
+ }
44
52
 
45
- const thursdayOfPreviousWeek = getThursdayOfWeek(previousWeekDate);
46
- const year = thursdayOfPreviousWeek.getUTCFullYear();
53
+ // Get last full ISO week { week, year }
54
+ function getLastFullCalendarWeek() {
55
+ const anchor = subWeeks(
56
+ dfStartOfWeek(new Date(), { weekStartsOn: 1 }), // Monday start
57
+ 1,
58
+ );
59
+ return {
60
+ week: getISOWeek(anchor),
61
+ year: getISOWeekYear(anchor),
62
+ };
63
+ }
47
64
 
65
+ // --- Week triples builder (UTC-safe) ---
66
+ function getWeekTriples(week, year) {
67
+ const triplesSet = new Set();
48
68
  const firstMonday = getFirstMondayOfYear(year);
49
- const thursdayOfFirstWeek = new Date(firstMonday.getTime() + 3 * MILLISECONDS_IN_A_DAY);
69
+ const start = new Date(firstMonday.getTime() + (week - 1) * MILLIS_IN_WEEK);
50
70
 
51
- const week = Math.ceil((thursdayOfPreviousWeek.getTime() - thursdayOfFirstWeek.getTime())
52
- / MILLISECONDS_IN_A_WEEK) + 1;
71
+ for (let i = 0; i < 7; i += 1) {
72
+ const d = new Date(start.getTime() + i * MILLIS_IN_DAY);
73
+ const month = d.getUTCMonth() + 1;
74
+ const calYear = d.getUTCFullYear();
75
+ triplesSet.add(`${calYear}-${month}-${week}`);
76
+ }
53
77
 
54
- return { week, year };
55
- };
78
+ return Array.from(triplesSet).map((t) => {
79
+ const [y, m, w] = t.split('-').map(Number);
80
+ return { year: y, month: m, week: w };
81
+ });
82
+ }
83
+
84
+ function buildWeeklyCondition(triples) {
85
+ const parts = triples.map(({ year, month, week }) => `(year=${year} AND month=${month} AND week=${week})`);
86
+ return parts.length === 1 ? parts[0] : parts.join(' OR ');
87
+ }
56
88
 
57
89
  export function getDateRanges(week, year) {
58
90
  let effectiveWeek = week;
59
91
  let effectiveYear = year;
60
92
 
61
93
  if (!isValidWeek(effectiveWeek, effectiveYear)) {
62
- const lastFullWeek = getLastFullCalendarWeek();
63
- effectiveWeek = lastFullWeek.week;
64
- effectiveYear = lastFullWeek.year;
94
+ const lastFull = getLastFullCalendarWeek();
95
+ effectiveWeek = lastFull.week;
96
+ effectiveYear = lastFull.year;
65
97
  }
66
98
 
67
99
  const firstMonday = getFirstMondayOfYear(effectiveYear);
68
- const startDate = new Date(firstMonday.getTime() + (effectiveWeek - 1) * MILLISECONDS_IN_A_WEEK);
69
- const endDate = new Date(startDate.getTime() + 6 * MILLISECONDS_IN_A_DAY);
100
+ const startDate = new Date(firstMonday.getTime() + (effectiveWeek - 1) * MILLIS_IN_WEEK);
101
+ const endDate = new Date(startDate.getTime() + 6 * MILLIS_IN_DAY);
70
102
  endDate.setUTCHours(23, 59, 59, 999);
103
+
71
104
  const startMonth = startDate.getUTCMonth() + 1;
72
105
  const endMonth = endDate.getUTCMonth() + 1;
73
106
  const startYear = startDate.getUTCFullYear();
107
+ const endYear = endDate.getUTCFullYear();
74
108
 
109
+ // Week in one month
75
110
  if (startMonth === endMonth) {
76
111
  return [{
77
112
  year: startYear,
@@ -81,12 +116,11 @@ export function getDateRanges(week, year) {
81
116
  }];
82
117
  }
83
118
 
84
- const endYear = endDate.getUTCFullYear();
85
-
119
+ // Week spans two months
86
120
  const endOfFirstMonth = new Date(Date.UTC(
87
121
  startYear,
88
- startDate.getUTCMonth() + 1,
89
- 0,
122
+ startDate.getUTCMonth() + 1, // next month
123
+ 0, // last day prev month
90
124
  23,
91
125
  59,
92
126
  59,
@@ -115,6 +149,81 @@ export function getDateRanges(week, year) {
115
149
  ];
116
150
  }
117
151
 
152
+ // --- Public: Get week info ---
153
+ export function getWeekInfo(inputWeek = null, inputYear = null) {
154
+ let effectiveWeek = inputWeek;
155
+ let effectiveYear = inputYear;
156
+
157
+ if (!isValidWeek(effectiveWeek, effectiveYear)) {
158
+ const lastFull = getLastFullCalendarWeek();
159
+ effectiveWeek = lastFull.week;
160
+ effectiveYear = lastFull.year;
161
+ }
162
+
163
+ const triples = getWeekTriples(effectiveWeek, effectiveYear);
164
+ const thursday = new Date(
165
+ getFirstMondayOfYear(effectiveYear).getTime()
166
+ + (effectiveWeek - 1) * MILLIS_IN_WEEK + 3 * MILLIS_IN_DAY,
167
+ );
168
+ const month = thursday.getUTCMonth() + 1;
169
+
170
+ return {
171
+ week: effectiveWeek,
172
+ year: effectiveYear,
173
+ month,
174
+ temporalCondition: buildWeeklyCondition(triples),
175
+ };
176
+ }
177
+
178
+ // --- Public: Get month info ---
179
+ export function getMonthInfo(inputMonth = null, inputYear = null) {
180
+ const now = new Date();
181
+ const bothProvided = Number.isInteger(inputMonth) && Number.isInteger(inputYear);
182
+ const validProvided = bothProvided && isValidMonth(inputMonth, inputYear);
183
+
184
+ if (validProvided) {
185
+ return { month: inputMonth, year: inputYear, temporalCondition: `(year=${inputYear} AND month=${inputMonth})` };
186
+ }
187
+
188
+ if (!bothProvided) {
189
+ // No or partial inputs → last full month
190
+ const lastMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
191
+ return {
192
+ month: lastMonth.getUTCMonth() + 1,
193
+ year: lastMonth.getUTCFullYear(),
194
+ temporalCondition: `(year=${lastMonth.getUTCFullYear()} AND month=${lastMonth.getUTCMonth() + 1})`,
195
+ };
196
+ }
197
+
198
+ // Both provided but invalid → current month
199
+ const currMonth = now.getUTCMonth() + 1;
200
+ const currYear = now.getUTCFullYear();
201
+ return { month: currMonth, year: currYear, temporalCondition: `(year=${currYear} AND month=${currMonth})` };
202
+ }
203
+
204
+ // --- Public: Main decision function ---
205
+ export function getTemporalCondition({ week, month, year } = {}) {
206
+ const hasWeek = Number.isInteger(week) && Number.isInteger(year);
207
+ const hasMonth = Number.isInteger(month) && Number.isInteger(year);
208
+
209
+ if (hasWeek && isValidWeek(week, year)) {
210
+ return getWeekInfo(week, year).temporalCondition;
211
+ }
212
+
213
+ if (hasMonth && isValidMonth(month, year)) {
214
+ return getMonthInfo(month, year).temporalCondition;
215
+ }
216
+
217
+ // Fallbacks
218
+ if (Number.isInteger(week) || (!hasWeek && !hasMonth)) {
219
+ // default last full week
220
+ return getWeekInfo().temporalCondition;
221
+ }
222
+
223
+ // Otherwise fall back to last full month
224
+ return getMonthInfo().temporalCondition;
225
+ }
226
+
118
227
  // Note: This function binds week exclusively to one year
119
228
  export function getLastNumberOfWeeks(number) {
120
229
  const result = [];
package/src/index.js CHANGED
@@ -64,6 +64,7 @@ export {
64
64
  resolveCanonicalUrl,
65
65
  getSpacecatRequestHeaders,
66
66
  ensureHttps,
67
+ urlMatchesFilter,
67
68
  } from './url-helpers.js';
68
69
 
69
70
  export { getStoredMetrics, storeMetrics } from './metrics-store.js';
@@ -81,4 +82,10 @@ export {
81
82
 
82
83
  export { retrievePageAuthentication, getAccessToken } from './auth.js';
83
84
 
84
- export { getDateRanges, getLastNumberOfWeeks } from './calendar-week-helper.js';
85
+ export {
86
+ getDateRanges,
87
+ getLastNumberOfWeeks,
88
+ getWeekInfo,
89
+ getMonthInfo,
90
+ getTemporalCondition,
91
+ } from './calendar-week-helper.js';
@@ -171,6 +171,75 @@ async function resolveCanonicalUrl(urlString, method = 'HEAD') {
171
171
  }
172
172
  }
173
173
 
174
+ /**
175
+ * Normalize a URL by trimming whitespace and handling trailing slashes
176
+ * @param {string} url - The URL to normalize
177
+ * @returns {string} The normalized URL
178
+ */
179
+ function normalizeUrl(url) {
180
+ if (!url || typeof url !== 'string') return url;
181
+ // Trim whitespace from beginning and end
182
+ let normalized = url.trim();
183
+ // Handle trailing slashes - normalize multiple trailing slashes to single slash
184
+ // or no slash depending on whether it's a root path
185
+ if (normalized.endsWith('/')) {
186
+ // Remove all trailing slashes
187
+ normalized = normalized.replace(/\/+$/, '');
188
+ // Add back a single slash if it's a root path (domain only)
189
+ const parts = normalized.split('/');
190
+ if (parts.length === 1 || (parts.length === 2 && parts[1] === '')) {
191
+ normalized += '/';
192
+ }
193
+ }
194
+ return normalized;
195
+ }
196
+
197
+ /**
198
+ * Normalize a pathname by removing trailing slashes
199
+ * @param {string} pathname - The pathname to normalize
200
+ * @returns {string} The normalized pathname
201
+ */
202
+ function normalizePathname(pathname) {
203
+ if (!pathname || typeof pathname !== 'string') return pathname;
204
+ if (pathname === '/') return '/';
205
+ return pathname.replace(/\/+$/, '');
206
+ }
207
+
208
+ /**
209
+ * Check if a URL matches any of the filter URLs by comparing pathnames
210
+ * @param {string} url - URL to check (format: https://domain.com/path)
211
+ * @param {string[]} filterUrls - Array of filter URLs (format: domain.com/path)
212
+ * @returns {boolean} True if URL matches any filter URL, false if any URL is invalid
213
+ */
214
+ function urlMatchesFilter(url, filterUrls) {
215
+ if (!filterUrls || filterUrls.length === 0) return true;
216
+ try {
217
+ // Normalize the input URL
218
+ const normalizedInputUrl = normalizeUrl(url);
219
+ const normalizedUrl = prependSchema(normalizedInputUrl);
220
+ const urlPath = normalizePathname(new URL(normalizedUrl).pathname);
221
+ return filterUrls.some((filterUrl) => {
222
+ try {
223
+ // Normalize each filter URL
224
+ const normalizedInputFilterUrl = normalizeUrl(filterUrl);
225
+ const normalizedFilterUrl = prependSchema(normalizedInputFilterUrl);
226
+ const filterPath = normalizePathname(new URL(normalizedFilterUrl).pathname);
227
+ return urlPath === filterPath;
228
+ } catch (error) {
229
+ // If any filter URL is invalid, skip it and continue checking others
230
+ /* eslint-disable-next-line no-console */
231
+ console.warn(`Invalid filter URL: ${filterUrl}`, error.message);
232
+ return false;
233
+ }
234
+ });
235
+ } catch (error) {
236
+ // If the main URL is invalid, return false
237
+ /* eslint-disable-next-line no-console */
238
+ console.warn(`Invalid URL: ${url}`, error.message);
239
+ return false;
240
+ }
241
+ }
242
+
174
243
  export {
175
244
  ensureHttps,
176
245
  getSpacecatRequestHeaders,
@@ -182,4 +251,5 @@ export {
182
251
  stripTrailingDot,
183
252
  stripTrailingSlash,
184
253
  stripWWW,
254
+ urlMatchesFilter,
185
255
  };