Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33,395 changes: 17,135 additions & 16,260 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@
"@adobe/helix-status": "10.1.5",
"@adobe/helix-universal-logger": "3.0.28",
"@adobe/spacecat-helix-content-sdk": "1.4.25",
"@adobe/spacecat-shared-data-access": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-data-access?tokowaka-json",
"@adobe/spacecat-shared-tokowaka-client": "https://gitpkg.now.sh/adobe/spacecat-shared/packages/spacecat-shared-tokowaka-client?tokowaka-json",
"@adobe/spacecat-shared-athena-client": "1.3.7",
"@adobe/spacecat-shared-brand-client": "1.1.25",
"@adobe/spacecat-shared-data-access": "2.74.1",
"@adobe/spacecat-shared-gpt-client": "1.6.6",
"@adobe/spacecat-shared-http-utils": "1.17.8",
"@adobe/spacecat-shared-ims-client": "1.9.0",
Expand Down Expand Up @@ -156,4 +157,4 @@
],
"ext": ".js, .cjs, .ejs, .css"
}
}
}
150 changes: 150 additions & 0 deletions src/controllers/suggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '@adobe/spacecat-shared-utils';

import { ValidationError, Suggestion as SuggestionModel, Site as SiteModel } from '@adobe/spacecat-shared-data-access';
import TokowakaClient from '@adobe/spacecat-shared-tokowaka-client';
import { SuggestionDto } from '../dto/suggestion.js';
import { FixDto } from '../dto/fix.js';
import { sendAutofixMessage, getCSPromiseToken, ErrorWithStatusCode } from '../support/utils.js';
Expand Down Expand Up @@ -712,9 +713,158 @@ function SuggestionsController(ctx, sqs, env) {
}
};

/**
* Deploys suggestions through Tokowaka edge delivery
* @param {Object} context of the request
* @returns {Promise<Response>} Deployment response
*/
const deploySuggestionToEdge = async (context) => {
const siteId = context.params?.siteId;
const opportunityId = context.params?.opportunityId;

if (!isValidUUID(siteId)) {
return badRequest('Site ID required');
}

if (!isValidUUID(opportunityId)) {
return badRequest('Opportunity ID required');
}

// validate request body
if (!isNonEmptyObject(context.data)) {
return badRequest('No data provided');
}
const { suggestionIds } = context.data;
if (!isArray(suggestionIds) || suggestionIds.length === 0) {
return badRequest('Request body must contain a non-empty array of suggestionIds');
}

const site = await Site.findById(siteId);
if (!site) {
return notFound('Site not found');
}

if (!await accessControlUtil.hasAccess(site)) {
return forbidden('User does not belong to the organization');
}

const opportunity = await Opportunity.findById(opportunityId);
if (!opportunity || opportunity.getSiteId() !== siteId) {
return notFound('Opportunity not found');
}

// Fetch all suggestions for this opportunity
const allSuggestions = await Suggestion.allByOpportunityId(opportunityId);

// Track valid, failed, and missing suggestions
const validSuggestions = [];
const failedSuggestions = [];

// Check each requested suggestion (basic validation only)
suggestionIds.forEach((suggestionId, index) => {
const suggestion = allSuggestions.find((s) => s.getId() === suggestionId);

if (!suggestion) {
failedSuggestions.push({
uuid: suggestionId,
index,
message: 'Suggestion not found',
statusCode: 404,
});
} else if (suggestion.getStatus() !== SuggestionModel.STATUSES.NEW) {
failedSuggestions.push({
uuid: suggestionId,
index,
message: 'Suggestion is not in NEW status',
statusCode: 400,
});
} else {
validSuggestions.push(suggestion);
}
});

let succeededSuggestions = [];

// Only attempt deployment if we have valid suggestions
if (isNonEmptyArray(validSuggestions)) {
try {
const tokowakaClient = TokowakaClient.createFrom(context);
const deploymentResult = await tokowakaClient.deploySuggestions(
site,
opportunity,
validSuggestions,
);

// Process deployment results
const {
succeededSuggestions: deployedSuggestions,
failedSuggestions: ineligibleSuggestions,
} = deploymentResult;

// Update successfully deployed suggestions with deployment timestamp
const deploymentTimestamp = Date.now();
succeededSuggestions = await Promise.all(
deployedSuggestions.map(async (suggestion) => {
const currentData = suggestion.getData();
suggestion.setData({
...currentData,
tokowakaDeployed: deploymentTimestamp,
});
suggestion.setUpdatedBy('tokowaka-deployment');
return suggestion.save();
}),
);

// Add ineligible suggestions to failed list
ineligibleSuggestions.forEach((item) => {
failedSuggestions.push({
uuid: item.suggestion.getId(),
index: suggestionIds.indexOf(item.suggestion.getId()),
message: item.reason,
statusCode: 400,
});
});

context.log.info(`Successfully deployed ${succeededSuggestions.length} suggestions to Edge`);
} catch (error) {
context.log.error(`Error deploying to Tokowaka: ${error.message}`, error);
// If deployment fails, mark all valid suggestions as failed
validSuggestions.forEach((suggestion) => {
failedSuggestions.push({
uuid: suggestion.getId(),
index: suggestionIds.indexOf(suggestion.getId()),
message: 'Deployment failed: Internal server error',
statusCode: 500,
});
});
}
}

const response = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make sure these suggestion props are not overwritten by audit worker while updating the suggestions.

suggestions: [
...succeededSuggestions.map((suggestion) => ({
uuid: suggestion.getId(),
index: suggestionIds.indexOf(suggestion.getId()),
statusCode: 200,
suggestion: SuggestionDto.toJSON(suggestion),
})),
...failedSuggestions,
],
metadata: {
total: suggestionIds.length,
success: succeededSuggestions.length,
failed: failedSuggestions.length,
},
};
response.suggestions.sort((a, b) => a.index - b.index);

return createResponse(response, 207);
};

return {
autofixSuggestions,
createSuggestions,
deploySuggestionToEdge,
getAllForOpportunity,
getByID,
getByStatus,
Expand Down
1 change: 1 addition & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export default function getRouteHandlers(
'DELETE /sites/:siteId/opportunities/:opportunityId': opportunitiesController.removeOpportunity,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions': suggestionsController.getAllForOpportunity,
'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/auto-fix': suggestionsController.autofixSuggestions,
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-deploy': suggestionsController.deploySuggestionToEdge,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId': suggestionsController.getByID,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId/fixes': suggestionsController.getSuggestionFixes,
Expand Down
10 changes: 10 additions & 0 deletions test/controllers/audits.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -668,6 +669,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -706,6 +708,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -747,6 +750,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -828,6 +832,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -870,6 +875,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -910,6 +916,7 @@ describe('Audits Controller', () => {
getImports: () => [],
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -949,6 +956,7 @@ describe('Audits Controller', () => {
getImports: () => [],
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -987,6 +995,7 @@ describe('Audits Controller', () => {
getHandlers: () => (({ [auditType]: {} })),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
});

const result = await auditsController.patchAuditForSite(context);
Expand Down Expand Up @@ -1051,6 +1060,7 @@ describe('Audits Controller', () => {
getBrandConfig: () => ({ brandId: 'test-brand' }),
getCdnLogsConfig: () => ({}),
getLlmoConfig: () => ({}),
getTokowakaConfig: () => ({}),
};

site.getConfig = () => siteConfig;
Expand Down
Loading