Skip to content

Commit 9f42743

Browse files
authored
fix: add tokowaka auto-deploy api (#1357)
1 parent cf96412 commit 9f42743

File tree

7 files changed

+5463
-1750
lines changed

7 files changed

+5463
-1750
lines changed

package-lock.json

Lines changed: 4808 additions & 1748 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@
7272
"@adobe/helix-status": "10.1.5",
7373
"@adobe/helix-universal-logger": "3.0.28",
7474
"@adobe/spacecat-helix-content-sdk": "1.4.25",
75+
"@adobe/spacecat-shared-tokowaka-client": "1.0.0",
7576
"@adobe/spacecat-shared-athena-client": "1.3.7",
7677
"@adobe/spacecat-shared-brand-client": "1.1.26",
77-
"@adobe/spacecat-shared-data-access": "2.75.0",
78+
"@adobe/spacecat-shared-data-access": "2.76.0",
7879
"@adobe/spacecat-shared-gpt-client": "1.6.7",
7980
"@adobe/spacecat-shared-http-utils": "1.17.9",
8081
"@adobe/spacecat-shared-ims-client": "1.9.1",
@@ -156,4 +157,4 @@
156157
],
157158
"ext": ".js, .cjs, .ejs, .css"
158159
}
159-
}
160+
}

src/controllers/suggestions.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '@adobe/spacecat-shared-utils';
2828

2929
import { ValidationError, Suggestion as SuggestionModel, Site as SiteModel } from '@adobe/spacecat-shared-data-access';
30+
import TokowakaClient from '@adobe/spacecat-shared-tokowaka-client';
3031
import { SuggestionDto } from '../dto/suggestion.js';
3132
import { FixDto } from '../dto/fix.js';
3233
import { sendAutofixMessage, getCSPromiseToken, ErrorWithStatusCode } from '../support/utils.js';
@@ -778,9 +779,158 @@ function SuggestionsController(ctx, sqs, env) {
778779
}
779780
};
780781

782+
/**
783+
* Deploys suggestions through Tokowaka edge delivery
784+
* @param {Object} context of the request
785+
* @returns {Promise<Response>} Deployment response
786+
*/
787+
const deploySuggestionToEdge = async (context) => {
788+
const siteId = context.params?.siteId;
789+
const opportunityId = context.params?.opportunityId;
790+
791+
if (!isValidUUID(siteId)) {
792+
return badRequest('Site ID required');
793+
}
794+
795+
if (!isValidUUID(opportunityId)) {
796+
return badRequest('Opportunity ID required');
797+
}
798+
799+
// validate request body
800+
if (!isNonEmptyObject(context.data)) {
801+
return badRequest('No data provided');
802+
}
803+
const { suggestionIds } = context.data;
804+
if (!isArray(suggestionIds) || suggestionIds.length === 0) {
805+
return badRequest('Request body must contain a non-empty array of suggestionIds');
806+
}
807+
808+
const site = await Site.findById(siteId);
809+
if (!site) {
810+
return notFound('Site not found');
811+
}
812+
813+
if (!await accessControlUtil.hasAccess(site)) {
814+
return forbidden('User does not belong to the organization');
815+
}
816+
817+
const opportunity = await Opportunity.findById(opportunityId);
818+
if (!opportunity || opportunity.getSiteId() !== siteId) {
819+
return notFound('Opportunity not found');
820+
}
821+
822+
// Fetch all suggestions for this opportunity
823+
const allSuggestions = await Suggestion.allByOpportunityId(opportunityId);
824+
825+
// Track valid, failed, and missing suggestions
826+
const validSuggestions = [];
827+
const failedSuggestions = [];
828+
829+
// Check each requested suggestion (basic validation only)
830+
suggestionIds.forEach((suggestionId, index) => {
831+
const suggestion = allSuggestions.find((s) => s.getId() === suggestionId);
832+
833+
if (!suggestion) {
834+
failedSuggestions.push({
835+
uuid: suggestionId,
836+
index,
837+
message: 'Suggestion not found',
838+
statusCode: 404,
839+
});
840+
} else if (suggestion.getStatus() !== SuggestionModel.STATUSES.NEW) {
841+
failedSuggestions.push({
842+
uuid: suggestionId,
843+
index,
844+
message: 'Suggestion is not in NEW status',
845+
statusCode: 400,
846+
});
847+
} else {
848+
validSuggestions.push(suggestion);
849+
}
850+
});
851+
852+
let succeededSuggestions = [];
853+
854+
// Only attempt deployment if we have valid suggestions
855+
if (isNonEmptyArray(validSuggestions)) {
856+
try {
857+
const tokowakaClient = TokowakaClient.createFrom(context);
858+
const deploymentResult = await tokowakaClient.deploySuggestions(
859+
site,
860+
opportunity,
861+
validSuggestions,
862+
);
863+
864+
// Process deployment results
865+
const {
866+
succeededSuggestions: deployedSuggestions,
867+
failedSuggestions: ineligibleSuggestions,
868+
} = deploymentResult;
869+
870+
// Update successfully deployed suggestions with deployment timestamp
871+
const deploymentTimestamp = Date.now();
872+
succeededSuggestions = await Promise.all(
873+
deployedSuggestions.map(async (suggestion) => {
874+
const currentData = suggestion.getData();
875+
suggestion.setData({
876+
...currentData,
877+
tokowakaDeployed: deploymentTimestamp,
878+
});
879+
suggestion.setUpdatedBy('tokowaka-deployment');
880+
return suggestion.save();
881+
}),
882+
);
883+
884+
// Add ineligible suggestions to failed list
885+
ineligibleSuggestions.forEach((item) => {
886+
failedSuggestions.push({
887+
uuid: item.suggestion.getId(),
888+
index: suggestionIds.indexOf(item.suggestion.getId()),
889+
message: item.reason,
890+
statusCode: 400,
891+
});
892+
});
893+
894+
context.log.info(`Successfully deployed ${succeededSuggestions.length} suggestions to Edge`);
895+
} catch (error) {
896+
context.log.error(`Error deploying to Tokowaka: ${error.message}`, error);
897+
// If deployment fails, mark all valid suggestions as failed
898+
validSuggestions.forEach((suggestion) => {
899+
failedSuggestions.push({
900+
uuid: suggestion.getId(),
901+
index: suggestionIds.indexOf(suggestion.getId()),
902+
message: 'Deployment failed: Internal server error',
903+
statusCode: 500,
904+
});
905+
});
906+
}
907+
}
908+
909+
const response = {
910+
suggestions: [
911+
...succeededSuggestions.map((suggestion) => ({
912+
uuid: suggestion.getId(),
913+
index: suggestionIds.indexOf(suggestion.getId()),
914+
statusCode: 200,
915+
suggestion: SuggestionDto.toJSON(suggestion),
916+
})),
917+
...failedSuggestions,
918+
],
919+
metadata: {
920+
total: suggestionIds.length,
921+
success: succeededSuggestions.length,
922+
failed: failedSuggestions.length,
923+
},
924+
};
925+
response.suggestions.sort((a, b) => a.index - b.index);
926+
927+
return createResponse(response, 207);
928+
};
929+
781930
return {
782931
autofixSuggestions,
783932
createSuggestions,
933+
deploySuggestionToEdge,
784934
getAllForOpportunity,
785935
getPagedForOpportunity,
786936
getByID,

src/routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default function getRouteHandlers(
184184
'GET /sites/:siteId/opportunities/:opportunityId/suggestions': suggestionsController.getAllForOpportunity,
185185
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/paged/:pageNum/:pageSize': suggestionsController.getPagedForOpportunity,
186186
'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/auto-fix': suggestionsController.autofixSuggestions,
187+
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-deploy': suggestionsController.deploySuggestionToEdge,
187188
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus,
188189
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId': suggestionsController.getByID,
189190
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId/fixes': suggestionsController.getSuggestionFixes,

test/controllers/audits.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ describe('Audits Controller', () => {
631631
getBrandConfig: () => ({ brandId: 'test-brand' }),
632632
getCdnLogsConfig: () => ({}),
633633
getLlmoConfig: () => ({}),
634+
getTokowakaConfig: () => ({}),
634635
});
635636

636637
const result = await auditsController.patchAuditForSite(context);
@@ -668,6 +669,7 @@ describe('Audits Controller', () => {
668669
getBrandConfig: () => ({ brandId: 'test-brand' }),
669670
getCdnLogsConfig: () => ({}),
670671
getLlmoConfig: () => ({}),
672+
getTokowakaConfig: () => ({}),
671673
});
672674

673675
const result = await auditsController.patchAuditForSite(context);
@@ -706,6 +708,7 @@ describe('Audits Controller', () => {
706708
getBrandConfig: () => ({ brandId: 'test-brand' }),
707709
getCdnLogsConfig: () => ({}),
708710
getLlmoConfig: () => ({}),
711+
getTokowakaConfig: () => ({}),
709712
});
710713

711714
const result = await auditsController.patchAuditForSite(context);
@@ -747,6 +750,7 @@ describe('Audits Controller', () => {
747750
getBrandConfig: () => ({ brandId: 'test-brand' }),
748751
getCdnLogsConfig: () => ({}),
749752
getLlmoConfig: () => ({}),
753+
getTokowakaConfig: () => ({}),
750754
});
751755

752756
const result = await auditsController.patchAuditForSite(context);
@@ -828,6 +832,7 @@ describe('Audits Controller', () => {
828832
getBrandConfig: () => ({ brandId: 'test-brand' }),
829833
getCdnLogsConfig: () => ({}),
830834
getLlmoConfig: () => ({}),
835+
getTokowakaConfig: () => ({}),
831836
});
832837

833838
const result = await auditsController.patchAuditForSite(context);
@@ -870,6 +875,7 @@ describe('Audits Controller', () => {
870875
getBrandConfig: () => ({ brandId: 'test-brand' }),
871876
getCdnLogsConfig: () => ({}),
872877
getLlmoConfig: () => ({}),
878+
getTokowakaConfig: () => ({}),
873879
});
874880

875881
const result = await auditsController.patchAuditForSite(context);
@@ -910,6 +916,7 @@ describe('Audits Controller', () => {
910916
getImports: () => [],
911917
getCdnLogsConfig: () => ({}),
912918
getLlmoConfig: () => ({}),
919+
getTokowakaConfig: () => ({}),
913920
});
914921

915922
const result = await auditsController.patchAuditForSite(context);
@@ -949,6 +956,7 @@ describe('Audits Controller', () => {
949956
getImports: () => [],
950957
getCdnLogsConfig: () => ({}),
951958
getLlmoConfig: () => ({}),
959+
getTokowakaConfig: () => ({}),
952960
});
953961

954962
const result = await auditsController.patchAuditForSite(context);
@@ -987,6 +995,7 @@ describe('Audits Controller', () => {
987995
getHandlers: () => (({ [auditType]: {} })),
988996
getCdnLogsConfig: () => ({}),
989997
getLlmoConfig: () => ({}),
998+
getTokowakaConfig: () => ({}),
990999
});
9911000

9921001
const result = await auditsController.patchAuditForSite(context);
@@ -1051,6 +1060,7 @@ describe('Audits Controller', () => {
10511060
getBrandConfig: () => ({ brandId: 'test-brand' }),
10521061
getCdnLogsConfig: () => ({}),
10531062
getLlmoConfig: () => ({}),
1063+
getTokowakaConfig: () => ({}),
10541064
};
10551065

10561066
site.getConfig = () => siteConfig;

0 commit comments

Comments
 (0)