@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 +14 -0
- package/package.json +3 -2
- package/src/calendar-week-helper.js +152 -43
- package/src/index.js +8 -1
- package/src/url-helpers.js +70 -0
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.
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
function isValidMonth(month, year) {
|
|
49
|
+
return Number.isInteger(year)
|
|
50
|
+
&& year >= 100 && Number.isInteger(month) && month >= 1 && month <= 12;
|
|
51
|
+
}
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
69
|
+
const start = new Date(firstMonday.getTime() + (week - 1) * MILLIS_IN_WEEK);
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
63
|
-
effectiveWeek =
|
|
64
|
-
effectiveYear =
|
|
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) *
|
|
69
|
-
const endDate = new Date(startDate.getTime() + 6 *
|
|
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
|
-
|
|
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 {
|
|
85
|
+
export {
|
|
86
|
+
getDateRanges,
|
|
87
|
+
getLastNumberOfWeeks,
|
|
88
|
+
getWeekInfo,
|
|
89
|
+
getMonthInfo,
|
|
90
|
+
getTemporalCondition,
|
|
91
|
+
} from './calendar-week-helper.js';
|
package/src/url-helpers.js
CHANGED
|
@@ -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
|
};
|