@adobe/spacecat-shared-utils 1.74.0 → 1.75.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,10 @@
1
+ # [@adobe/spacecat-shared-utils-v1.75.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.74.0...@adobe/spacecat-shared-utils-v1.75.0) (2025-11-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * add aggregation key to get Oppty SC API ([#1148](https://github.com/adobe/spacecat-shared/issues/1148)) ([07bf485](https://github.com/adobe/spacecat-shared/commit/07bf485a5534a066899abf9488ef71301d8cb3e1))
7
+
1
8
  # [@adobe/spacecat-shared-utils-v1.74.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.73.1...@adobe/spacecat-shared-utils-v1.74.0) (2025-11-17)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.74.0",
3
+ "version": "1.75.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,248 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Accessibility suggestion aggregation strategies
15
+ *
16
+ * Defines how HTML elements with accessibility issues are grouped into database suggestions.
17
+ * Each granularity level has a function that builds an aggregation key from suggestion data.
18
+ */
19
+
20
+ /**
21
+ * Granularity levels for suggestion aggregation
22
+ * @enum {string}
23
+ */
24
+ export const Granularity = {
25
+ /** One suggestion per HTML element - url|type|selector (e.g., page1|color-contrast|div.header) */
26
+ INDIVIDUAL: 'INDIVIDUAL',
27
+
28
+ /**
29
+ * One suggestion per issue type per page - url|type (e.g., page1|color-contrast)
30
+ */
31
+ PER_PAGE_PER_COMPONENT: 'PER_PAGE_PER_COMPONENT',
32
+
33
+ /** One suggestion per page - url (e.g., page1) */
34
+ PER_PAGE: 'PER_PAGE',
35
+
36
+ /**
37
+ * One suggestion per component type across all pages - type|selector
38
+ */
39
+ PER_COMPONENT: 'PER_COMPONENT',
40
+
41
+ /** One suggestion per issue type globally - type (e.g., color-contrast) */
42
+ PER_TYPE: 'PER_TYPE',
43
+ };
44
+
45
+ /**
46
+ * Generic key builder that concatenates non-empty values with pipe separator
47
+ * @param {...string} parts - Variable number of key parts to concatenate
48
+ * @returns {string} Concatenated key
49
+ * @private - exported for testing purposes
50
+ */
51
+ export function buildKey(...parts) {
52
+ return parts.filter((part) => part != null && part !== '').join('|');
53
+ }
54
+
55
+ /**
56
+ * Builds aggregation key for INDIVIDUAL granularity
57
+ * Key format: url|type|selector|source
58
+ * @private - exported for testing purposes
59
+ */
60
+ export function buildIndividualKey({
61
+ url, issueType, targetSelector, source,
62
+ }) {
63
+ return buildKey(url, issueType, targetSelector, source);
64
+ }
65
+
66
+ /**
67
+ * Builds aggregation key for PER_PAGE_PER_COMPONENT granularity
68
+ * Key format: url|type|source
69
+ */
70
+ function buildPerPagePerComponentKey({ url, issueType, source }) {
71
+ return buildKey(url, issueType, source);
72
+ }
73
+
74
+ /**
75
+ * Builds aggregation key for PER_PAGE granularity
76
+ * Key format: url|source
77
+ */
78
+ function buildPerPageKey({ url, source }) {
79
+ return buildKey(url, source);
80
+ }
81
+
82
+ /**
83
+ * Builds aggregation key for COMPONENT granularity
84
+ * Key format: type|selector
85
+ */
86
+ function buildComponentKey({ issueType, targetSelector }) {
87
+ return buildKey(issueType, targetSelector);
88
+ }
89
+
90
+ /**
91
+ * Builds aggregation key for GLOBAL granularity
92
+ * Key format: type
93
+ */
94
+ function buildGlobalKey({ issueType }) {
95
+ return buildKey(issueType);
96
+ }
97
+
98
+ /**
99
+ * Registry of key-building functions by granularity level
100
+ */
101
+ export const GRANULARITY_KEY_BUILDERS = {
102
+ [Granularity.INDIVIDUAL]: buildIndividualKey,
103
+ [Granularity.PER_PAGE_PER_COMPONENT]: buildPerPagePerComponentKey,
104
+ [Granularity.PER_PAGE]: buildPerPageKey,
105
+ [Granularity.PER_COMPONENT]: buildComponentKey,
106
+ [Granularity.PER_TYPE]: buildGlobalKey,
107
+ };
108
+
109
+ /**
110
+ * Maps issue types to their aggregation granularity
111
+ * Based on the nature of each issue and how they should be grouped
112
+ */
113
+ export const ISSUE_GRANULARITY_MAP = {
114
+ 'color-contrast': Granularity.INDIVIDUAL,
115
+ list: Granularity.PER_COMPONENT,
116
+ 'aria-roles': Granularity.PER_PAGE_PER_COMPONENT,
117
+ 'image-alt': Granularity.PER_PAGE_PER_COMPONENT,
118
+ 'link-in-text-block': Granularity.PER_PAGE_PER_COMPONENT,
119
+ 'link-name': Granularity.PER_PAGE_PER_COMPONENT,
120
+ 'target-size': Granularity.PER_PAGE_PER_COMPONENT,
121
+ listitem: Granularity.PER_COMPONENT,
122
+ label: Granularity.PER_PAGE_PER_COMPONENT,
123
+ 'aria-prohibited-attr': Granularity.PER_TYPE,
124
+ 'button-name': Granularity.PER_PAGE_PER_COMPONENT,
125
+ 'frame-title': Granularity.PER_PAGE_PER_COMPONENT,
126
+ 'aria-valid-attr-value': Granularity.PER_PAGE_PER_COMPONENT,
127
+ 'aria-allowed-attr': Granularity.PER_TYPE,
128
+ 'aria-hidden-focus': Granularity.PER_PAGE_PER_COMPONENT,
129
+ 'nested-interactive': Granularity.PER_PAGE_PER_COMPONENT,
130
+ 'html-has-lang': Granularity.PER_PAGE,
131
+ 'meta-viewport': Granularity.PER_PAGE,
132
+ 'aria-required-children': Granularity.PER_PAGE_PER_COMPONENT,
133
+ 'aria-required-parent': Granularity.PER_PAGE_PER_COMPONENT,
134
+ 'meta-refresh': Granularity.PER_PAGE,
135
+ 'role-img-alt': Granularity.PER_PAGE_PER_COMPONENT,
136
+ 'aria-input-field-name': Granularity.PER_PAGE_PER_COMPONENT,
137
+ 'scrollable-region-focusable': Granularity.PER_PAGE_PER_COMPONENT,
138
+ 'select-name': Granularity.PER_PAGE_PER_COMPONENT,
139
+ };
140
+
141
+ /**
142
+ * Gets the granularity level for a specific issue type
143
+ *
144
+ * @param {string} issueType - The issue type (e.g., "color-contrast")
145
+ * @returns {string} The granularity level (defaults to PER_PAGE_PER_COMPONENT)
146
+ */
147
+ export function getGranularityForIssueType(issueType) {
148
+ return ISSUE_GRANULARITY_MAP[issueType] || Granularity.PER_PAGE_PER_COMPONENT;
149
+ }
150
+
151
+ /**
152
+ * Builds an aggregation key for grouping HTML elements during processing
153
+ *
154
+ * @param {string} issueType - The issue type
155
+ * @param {string} url - Page URL
156
+ * @param {string} targetSelector - CSS selector for the element
157
+ * @param {string} source - Optional source identifier
158
+ * @returns {string} The aggregation key based on the issue type's granularity
159
+ */
160
+ export function buildAggregationKey(issueType, url, targetSelector, source) {
161
+ const granularity = getGranularityForIssueType(issueType);
162
+ const keyBuilder = GRANULARITY_KEY_BUILDERS[granularity];
163
+
164
+ /* c8 ignore start - defensive code */
165
+ if (!keyBuilder) {
166
+ return buildIndividualKey({
167
+ url, issueType, targetSelector, source,
168
+ });
169
+ }
170
+ /* c8 ignore stop */
171
+
172
+ return keyBuilder({
173
+ url, issueType, targetSelector, source,
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Builds an aggregation key from suggestion data.
179
+ * Extracts the necessary fields from a suggestion object and calls buildAggregationKey.
180
+ *
181
+ * @param {Object} suggestionData - The suggestion data object
182
+ * @param {string} suggestionData.url - Page URL
183
+ * @param {Array} suggestionData.issues - Array of issues
184
+ * @param {string} suggestionData.source - Optional source
185
+ * @returns {string|null} The aggregation key based on the issue type's granularity,
186
+ * or null if no issues
187
+ */
188
+ export function buildAggregationKeyFromSuggestion(suggestionData) {
189
+ // Handle null, undefined, or non-object inputs
190
+ if (!suggestionData || typeof suggestionData !== 'object') {
191
+ return null;
192
+ }
193
+
194
+ const { url, issues, source } = suggestionData;
195
+
196
+ if (!issues || issues.length === 0) {
197
+ return null;
198
+ }
199
+
200
+ const firstIssue = issues[0];
201
+ if (!firstIssue || !firstIssue.type) {
202
+ return null;
203
+ }
204
+
205
+ const issueType = firstIssue.type;
206
+ const htmlWithIssue = firstIssue.htmlWithIssues?.[0];
207
+ // Support both snake_case and camelCase for backwards compatibility
208
+ const targetSelector = htmlWithIssue?.target_selector || htmlWithIssue?.targetSelector || '';
209
+
210
+ return buildAggregationKey(issueType, url, targetSelector, source);
211
+ }
212
+
213
+ /**
214
+ * Builds a database-level key for matching suggestions across audit runs.
215
+ * Used by syncSuggestions to identify existing suggestions.
216
+ *
217
+ * This ALWAYS uses INDIVIDUAL granularity (url|type|selector|source) to ensure
218
+ * each HTML element gets its own suggestion in the database. This prevents
219
+ * incorrect merging of different HTML elements.
220
+ *
221
+ * IMPORTANT: This maintains backwards compatibility with the original buildKey logic
222
+ * by including a trailing pipe when selector is empty (url|type|).
223
+ *
224
+ * @param {Object} suggestionData - The suggestion data object
225
+ * @param {string} suggestionData.url - Page URL
226
+ * @param {Array} suggestionData.issues - Array of issues
227
+ * @param {string} suggestionData.source - Optional source
228
+ * @returns {string} The key for suggestion matching
229
+ */
230
+ export function buildSuggestionKey(suggestionData) {
231
+ const { url, issues, source } = suggestionData;
232
+
233
+ if (!issues || issues.length === 0) {
234
+ return url;
235
+ }
236
+
237
+ const firstIssue = issues[0];
238
+ const issueType = firstIssue.type;
239
+ const targetSelector = firstIssue.htmlWithIssues?.[0]?.target_selector || '';
240
+
241
+ // Always build INDIVIDUAL-level key for database uniqueness
242
+ // Backwards compatible: url|type|selector|source or url|type| when selector is empty
243
+ let key = `${url}|${issueType}|${targetSelector}`;
244
+ if (source) {
245
+ key += `|${source}`;
246
+ }
247
+ return key;
248
+ }
package/src/index.js CHANGED
@@ -108,3 +108,15 @@ export * as schemas from './schemas.js';
108
108
 
109
109
  export { detectLocale } from './locale-detect/locale-detect.js';
110
110
  export { prettifyLogForwardingConfig } from './cdn-helpers.js';
111
+
112
+ export {
113
+ buildAggregationKey,
114
+ buildAggregationKeyFromSuggestion,
115
+ buildSuggestionKey,
116
+ buildIndividualKey,
117
+ buildKey,
118
+ getGranularityForIssueType,
119
+ Granularity,
120
+ GRANULARITY_KEY_BUILDERS,
121
+ ISSUE_GRANULARITY_MAP,
122
+ } from './aggregation/aggregation-strategies.js';