| 
10 | 10 |  * governing permissions and limitations under the License.  | 
11 | 11 |  */  | 
12 | 12 | 
 
  | 
13 |  | -import { createHash } from 'crypto';  | 
14 |  | -import {  | 
15 |  | -  ok, badRequest, notFound, internalServerError,  | 
16 |  | -} from '@adobe/spacecat-shared-http-utils';  | 
17 |  | -import { isNonEmptyArray } from '@adobe/spacecat-shared-utils';  | 
18 |  | -import { getObjectFromKey } from '../../utils/s3-utils.js';  | 
19 |  | - | 
20 |  | -/**  | 
21 |  | - * Generates a hash for the given URL and source combination.  | 
22 |  | - * @param {string} url - The URL to hash  | 
23 |  | - * @param {string} source - The source to hash  | 
24 |  | - * @returns {string} - The generated hash (first 16 characters of MD5)  | 
25 |  | - */  | 
26 |  | -function generateUrlSourceHash(url, source) {  | 
27 |  | -  const combined = `${url}_${source}`;  | 
28 |  | -  return createHash('md5').update(combined).digest('hex').substring(0, 16);  | 
29 |  | -}  | 
30 |  | - | 
31 |  | -/**  | 
32 |  | - * Reads code change report from S3 bucket  | 
33 |  | - * @param {Object} s3Client - The S3 client instance  | 
34 |  | - * @param {string} bucketName - The S3 bucket name  | 
35 |  | - * @param {string} siteId - The site ID  | 
36 |  | - * @param {string} url - The page URL  | 
37 |  | - * @param {string} source - The source (optional)  | 
38 |  | - * @param {string} type - The issue type (e.g., 'color-contrast')  | 
39 |  | - * @param {Object} log - Logger instance  | 
40 |  | - * @returns {Promise<Object|null>} - The report data or null if not found  | 
41 |  | - */  | 
42 |  | -async function readCodeChangeReport(s3Client, bucketName, siteId, url, source, type, log) {  | 
43 |  | -  try {  | 
44 |  | -    const urlSourceHash = generateUrlSourceHash(url, source || '');  | 
45 |  | -    const reportKey = `fixes/${siteId}/${urlSourceHash}/${type}/report.json`;  | 
46 |  | - | 
47 |  | -    log.info(`Reading code change report from S3: ${reportKey}`);  | 
48 |  | - | 
49 |  | -    const reportData = await getObjectFromKey(s3Client, bucketName, reportKey, log);  | 
50 |  | - | 
51 |  | -    if (!reportData) {  | 
52 |  | -      log.warn(`No code change report found for key: ${reportKey}`);  | 
53 |  | -      return null;  | 
54 |  | -    }  | 
55 |  | - | 
56 |  | -    log.info(`Successfully read code change report from S3: ${reportKey}`);  | 
57 |  | -    return reportData;  | 
58 |  | -  } catch (error) {  | 
59 |  | -    log.error(`Error reading code change report from S3: ${error.message}`, error);  | 
60 |  | -    return null;  | 
61 |  | -  }  | 
62 |  | -}  | 
63 |  | - | 
64 |  | -/**  | 
65 |  | - * Updates suggestions with code change data  | 
66 |  | - * @param {Array} suggestions - Array of suggestion objects  | 
67 |  | - * @param {string} url - The page URL to match  | 
68 |  | - * @param {string} source - The source to match (optional)  | 
69 |  | - * @param {string} ruleId - The WCAG rule ID to match  | 
70 |  | - * @param {Object} reportData - The code change report data  | 
71 |  | - * @param {Object} log - Logger instance  | 
72 |  | - * @returns {Promise<Array>} - Array of updated suggestions  | 
73 |  | - */  | 
74 |  | -async function updateSuggestionsWithCodeChange(suggestions, url, source, ruleId, reportData, log) {  | 
75 |  | -  const updatedSuggestions = [];  | 
76 |  | - | 
77 |  | -  try {  | 
78 |  | -    const promises = [];  | 
79 |  | -    for (const suggestion of suggestions) {  | 
80 |  | -      const suggestionData = suggestion.getData();  | 
81 |  | - | 
82 |  | -      // Check if this suggestion matches the criteria  | 
83 |  | -      const suggestionUrl = suggestionData.url;  | 
84 |  | -      const suggestionSource = suggestionData.source;  | 
85 |  | -      const suggestionRuleId = suggestionData.issues[0]?.type;  | 
86 |  | - | 
87 |  | -      if (suggestionUrl === url  | 
88 |  | -            && (!source || suggestionSource === source)  | 
89 |  | -            && suggestionRuleId === ruleId  | 
90 |  | -            && !!reportData.diff) {  | 
91 |  | -        log.info(`Updating suggestion ${suggestion.getId()} with code change data`);  | 
92 |  | - | 
93 |  | -        // Update suggestion data with diff content and availability flag  | 
94 |  | -        const updatedData = {  | 
95 |  | -          ...suggestionData,  | 
96 |  | -          patchContent: reportData.diff,  | 
97 |  | -          isCodeChangeAvailable: true,  | 
98 |  | -        };  | 
99 |  | - | 
100 |  | -        suggestion.setData(updatedData);  | 
101 |  | -        suggestion.setUpdatedBy('system');  | 
102 |  | - | 
103 |  | -        promises.push(suggestion.save());  | 
104 |  | -        updatedSuggestions.push(suggestion);  | 
105 |  | - | 
106 |  | -        log.info(`Successfully updated suggestion ${suggestion.getId()}`);  | 
107 |  | -      }  | 
108 |  | -    }  | 
109 |  | -    await Promise.all(promises);  | 
110 |  | -  } catch (error) {  | 
111 |  | -    log.error(`Error updating suggestions with code change data: ${error.message}`, error);  | 
112 |  | -    throw error;  | 
113 |  | -  }  | 
114 |  | - | 
115 |  | -  return updatedSuggestions;  | 
116 |  | -}  | 
117 |  | - | 
118 | 13 | /**  | 
119 |  | - * AccessibilityCodeFixHandler - Updates suggestions with code changes from S3  | 
 | 14 | + * Forms Accessibility Code Fix Handler  | 
120 | 15 |  *  | 
121 |  | - * Expected message format:  | 
122 |  | - * {  | 
123 |  | - *   "siteId": "<site-id>",  | 
124 |  | - *   "type": "codefix:accessibility",  | 
125 |  | - *   "data": {  | 
126 |  | - *     "opportunityId": "<uuid>",  | 
127 |  | - *     "updates": [  | 
128 |  | - *       {  | 
129 |  | - *         "url": "<page url>",  | 
130 |  | - *         "source": "<source>", // optional  | 
131 |  | - *         "type": ["color-contrast", "select-name"]  | 
132 |  | - *       }  | 
133 |  | - *     ]  | 
134 |  | - *   }  | 
135 |  | - * }  | 
 | 16 | + * This is a legacy entry point that now delegates to the common code fix response handler.  | 
 | 17 | + * Kept for backward compatibility with existing message routing.  | 
136 | 18 |  *  | 
137 |  | - * @param {Object} message - The SQS message  | 
138 |  | - * @param {Object} context - The context object containing dataAccess, log, s3Client, etc.  | 
139 |  | - * @returns {Promise<Response>} - HTTP response  | 
 | 19 | + * @deprecated Use the common codeFixResponseHandler directly  | 
140 | 20 |  */  | 
141 |  | -export default async function accessibilityCodeFixHandler(message, context) {  | 
142 |  | -  const {  | 
143 |  | -    log, dataAccess, s3Client, env,  | 
144 |  | -  } = context;  | 
145 |  | -  const { Opportunity } = dataAccess;  | 
146 |  | -  const { siteId, data } = message;  | 
147 |  | - | 
148 |  | -  if (!data) {  | 
149 |  | -    log.error('AccessibilityCodeFixHandler: No data provided in message');  | 
150 |  | -    return badRequest('No data provided in message');  | 
151 |  | -  }  | 
152 |  | - | 
153 |  | -  const { opportunityId, updates } = data;  | 
154 |  | - | 
155 |  | -  if (!opportunityId) {  | 
156 |  | -    log.error('[AccessibilityCodeFixHandler] No opportunityId provided');  | 
157 |  | -    return badRequest('No opportunityId provided');  | 
158 |  | -  }  | 
159 |  | - | 
160 |  | -  if (!isNonEmptyArray(updates)) {  | 
161 |  | -    log.error('[AccessibilityCodeFixHandler] No updates provided or updates is empty');  | 
162 |  | -    return badRequest('No updates provided or updates is empty');  | 
163 |  | -  }  | 
164 |  | - | 
165 |  | -  log.info(`[AccessibilityCodeFixHandler] Processing message for siteId: ${siteId}, opportunityId: ${opportunityId}`);  | 
166 |  | - | 
167 |  | -  try {  | 
168 |  | -    // Find the opportunity  | 
169 |  | -    const opportunity = await Opportunity.findById(opportunityId);  | 
170 |  | - | 
171 |  | -    if (!opportunity) {  | 
172 |  | -      log.error(`[AccessibilityCodeFixHandler] Opportunity not found for ID: ${opportunityId}`);  | 
173 |  | -      return notFound('Opportunity not found');  | 
174 |  | -    }  | 
175 |  | - | 
176 |  | -    // Verify the opportunity belongs to the correct site  | 
177 |  | -    if (opportunity.getSiteId() !== siteId) {  | 
178 |  | -      const errorMsg = `[AccessibilityCodeFixHandler] Site ID mismatch. Expected: ${siteId}, Found: ${opportunity.getSiteId()}`;  | 
179 |  | -      log.error(errorMsg);  | 
180 |  | -      return badRequest('Site ID mismatch');  | 
181 |  | -    }  | 
182 |  | - | 
183 |  | -    // Get all suggestions for the opportunity  | 
184 |  | -    const suggestions = await opportunity.getSuggestions();  | 
185 |  | - | 
186 |  | -    if (!isNonEmptyArray(suggestions)) {  | 
187 |  | -      log.warn(`[AccessibilityCodeFixHandler] No suggestions found for opportunity: ${opportunityId}`);  | 
188 |  | -      return ok('No suggestions found for opportunity');  | 
189 |  | -    }  | 
190 |  | - | 
191 |  | -    const bucketName = env.S3_MYSTIQUE_BUCKET_NAME;  | 
192 |  | - | 
193 |  | -    if (!bucketName) {  | 
194 |  | -      log.error('AccessibilityCodeFixHandler: S3_MYSTIQUE_BUCKET_NAME environment variable not set');  | 
195 |  | -      return internalServerError('S3 bucket name not configured');  | 
196 |  | -    }  | 
197 |  | - | 
198 |  | -    let totalUpdatedSuggestions = 0;  | 
199 |  | - | 
200 |  | -    // Process each update  | 
201 |  | -    await Promise.all(updates.map(async (update) => {  | 
202 |  | -      const { url, source, type: types } = update;  | 
203 |  | - | 
204 |  | -      if (!url) {  | 
205 |  | -        log.warn('[AccessibilityCodeFixHandler] Skipping update without URL');  | 
206 |  | -        return;  | 
207 |  | -      }  | 
208 |  | - | 
209 |  | -      if (!isNonEmptyArray(types)) {  | 
210 |  | -        log.warn(`[AccessibilityCodeFixHandler] Skipping update for URL ${url} without types`);  | 
211 |  | -        return;  | 
212 |  | -      }  | 
213 |  | - | 
214 |  | -      log.info(`[AccessibilityCodeFixHandler] Processing update for URL: ${url}, source: ${source || 'N/A'}, types: ${types.join(', ')}`);  | 
215 |  | - | 
216 |  | -      // For each type in the update, try to read the code change report  | 
217 |  | -      await Promise.all(types.map(async (ruleId) => {  | 
218 |  | -        let reportData = await readCodeChangeReport(  | 
219 |  | -          s3Client,  | 
220 |  | -          bucketName,  | 
221 |  | -          siteId,  | 
222 |  | -          url,  | 
223 |  | -          source,  | 
224 |  | -          ruleId,  | 
225 |  | -          log,  | 
226 |  | -        );  | 
227 |  | - | 
228 |  | -        if (!reportData) {  | 
229 |  | -          log.warn(`[AccessibilityCodeFixHandler] No code change report found for URL: ${url}, source: ${source}, type: ${ruleId}`);  | 
230 |  | -          return;  | 
231 |  | -        }  | 
232 |  | - | 
233 |  | -        reportData = JSON.parse(reportData);  | 
234 |  | - | 
235 |  | -        // Update matching suggestions with the code change data  | 
236 |  | -        const updatedSuggestions = await updateSuggestionsWithCodeChange(  | 
237 |  | -          suggestions,  | 
238 |  | -          url,  | 
239 |  | -          source,  | 
240 |  | -          ruleId,  | 
241 |  | -          reportData,  | 
242 |  | -          log,  | 
243 |  | -        );  | 
244 |  | -        totalUpdatedSuggestions += updatedSuggestions.length;  | 
245 |  | -      }));  | 
246 |  | -    }));  | 
 | 21 | +import codeFixResponseHandler from '../../common/codefix-response-handler.js';  | 
247 | 22 | 
 
  | 
248 |  | -    log.info(`[AccessibilityCodeFixHandler] Successfully processed all updates. Total suggestions updated: ${totalUpdatedSuggestions}`);  | 
249 |  | -    return ok();  | 
250 |  | -  } catch (error) {  | 
251 |  | -    log.error(`[AccessibilityCodeFixHandler] Error processing message: ${error.message}`, error);  | 
252 |  | -    return internalServerError(`Error processing message: ${error.message}`);  | 
253 |  | -  }  | 
254 |  | -}  | 
 | 23 | +export default codeFixResponseHandler;  | 
0 commit comments