Skip to content

feat: Fixes controller #850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 30, 2025
10 changes: 10 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
@@ -155,6 +155,16 @@ paths:
$ref: './site-metrics-api.yaml#/page-metrics-by-source'
/sites/{siteId}/experiments:
$ref: './experiments-api.yaml#/experiments'
/sites/{siteId}/opportunities/{opportunityId}/fixes:
$ref: './site-opportunities.yaml#/site-opportunity-fixes'
/sites/{siteId}/opportunities/{opportunityId}/fixes/by-status/{status}:
$ref: './site-opportunities.yaml#/site-opportunity-fixes-by-status'
/sites/{siteId}/opportunities/{opportunityId}/fixes/{fixId}:
$ref: './site-opportunities.yaml#/site-opportunity-fix'
/sites/{siteId}/opportunities/{opportunityId}/fixes/{fixId}/suggestions:
$ref: './site-opportunities.yaml#/site-opportunity-fix-suggestions'
/sites/{siteId}/opportunities/{opportunityId}/status:
$ref: './site-opportunities.yaml#/site-opportunity-status'
/slack/channels/invite-by-user-id:
$ref: './slack-api.yaml#/invite-by-user-id'
/trigger:
7 changes: 7 additions & 0 deletions docs/openapi/parameters.yaml
Original file line number Diff line number Diff line change
@@ -180,3 +180,10 @@ screenshotDevice:
enum:
- 'desktop'
- 'iphone 6'
fixId:
name: fixId
description: The fix ID in uuid format
in: path
required: true
schema:
$ref: './schemas.yaml#/Id'
208 changes: 208 additions & 0 deletions docs/openapi/schemas.yaml
Original file line number Diff line number Diff line change
@@ -2715,3 +2715,211 @@ ScrapedContentResponse:
name: "screenshot-desktop-fullpage.png"
size: 123456
lastModified: "2025-04-11T00:44:31Z"


FixStatus:
description: |
Fix Status
- PENDING: the fix is pending to be deployed
- DEPLOYED: the fix was successfully applied
- PUBLISHED: the fix is live in production
- FAILED: failed to apply the fix
- ROLLED_BACK: the fix has been rolled_back
example: 'PENDING'
type: string
enum:
- 'PENDING'
- 'DEPLOYED'
- 'PUBLISHED'
- 'FAILED'
- 'ROLLED_BACK'


Fix:
type: object
properties:
id:
type: string
format: uuid
opportunityId:
type: string
format: uuid
type:
$ref: '#/SuggestionType'
createdAt:
type: string
format: date-time
executedBy:
type: string
executedAt:
type: string
format: date-time
publishedAt:
type: string
format: date-time
changeDetails:
type: object
additionalProperties: true
status:
$ref: '#/FixStatus'


FixList:
type: array
items:
$ref: '#/Fix'

FixCreate:
type: object
required:
- type
- changeDetails
properties:
type:
$ref: '#/SuggestionType'
changeDetails:
type: object
additionalProperties: true
status:
$ref: '#/FixStatus'

FixOperationSuccess:
type: object
description: Success response for a fix operation
properties:
index:
description: Index of this fix in the list from the request body, starting from 0
type: integer
minimum: 0
statusCode:
description: HTTP status code indicating the type of success
type: integer
fix:
$ref: '#/Fix'
required:
- index
- statusCode
- fix

FixUpdateFailure:
type: object
description: Fix update failure response
properties:
id:
description: UUID of this fix
$ref: '#/Id'
index:
description: Index of this fix in the list from the request body, starting from 0
type: integer
minimum: 0
statusCode:
description: HTTP status code indicating the type of failure
type: integer
message:
description: Error message describing the failure
type: string

FixCreateFailure:
type: object
description: Fix create failure response
properties:
index:
description: Index of this fix in the list from the request body, starting from 0
type: integer
minimum: 0
statusCode:
description: HTTP status code indicating the type of failure
type: integer
message:
description: Error message describing the failure
type: string

FixCreateList:
type: array
items:
$ref: '#/FixCreate'

FixCreateListResponse:
type: object
description: Fix create list response
properties:
fixes:
type: array
items:
oneOf:
- $ref: '#/FixOperationSuccess'
- $ref: '#/FixCreateFailure'
metadata:
type: object
properties:
total:
description: Total number of fixes in the response
type: integer
success:
description: Number of fixes successfully created
type: integer
failure:
description: Number of fixes that failed to create
type: integer
required:
- total
- success
- failure
required:
- fixes
- metadata

FixUpdate:
type: object
properties:
changeDetails:
type: object
additionalProperties: true
status:
$ref: '#/FixStatus'

FixStatusUpdate:
type: array
items:
type: object
properties:
id:
description: UUID of this fix
readOnly: false
$ref: '#/Id'
status:
description: Status of this fix; status reflects overall fix execution, flagged here for optimization
$ref: '#/SuggestionStatus'
required:
- id
- status

FixStatusUpdateListResponse:
type: object
description: Fixes status update response
properties:
fixes:
type: array
items:
oneOf:
- $ref: '#/FixOperationSuccess'
- $ref: '#/FixUpdateFailure'
metadata:
type: object
properties:
total:
description: Total number of fixes in the response
type: integer
success:
description: Number of fixes successfully updated
type: integer
failure:
description: Number of fixes that failed to update
type: integer
required:
- total
- success
- failure
required:
- fixes
- metadata
236 changes: 236 additions & 0 deletions docs/openapi/site-opportunities.yaml
Original file line number Diff line number Diff line change
@@ -456,3 +456,239 @@ site-opportunity-suggestion:
security:
- ims_key: [ ]

site-opportunity-fixes:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
get:
operationId: getSiteOpportunityFixes
summary: |
Retrieve a list of all fixes for a specific opportunity
tags:
- opportunity-fixes
responses:
'200':
description: A list of fixes
content:
application/json:
schema:
$ref: './schemas.yaml#/FixList'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]
post:
operationId: createSiteOpportunityFixes
summary: |
Create and add a list of one or more fixes to an opportunity in one transaction
tags:
- opportunity-fixes
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/FixCreateList'
responses:
'207':
description: |
A list of fixes created and added to the opportunity,
or the status code and error message for the ones failed.
content:
application/json:
schema:
$ref: './schemas.yaml#/FixCreateListResponse'
security:
- ims_key: [ ]

site-opportunity-fixes-by-status:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
- name: status
in: path
required: true
schema:
type: string
get:
operationId: getSiteOpportunityFixesByStatus
summary: |
Retrieve fixes for a specific opportunity filtered by status
tags:
- opportunity-fixes
responses:
'200':
description: A list of fixes filtered by status
content:
application/json:
schema:
$ref: './schemas.yaml#/FixList'
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]

site-opportunity-fix:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
- $ref: './parameters.yaml#/fixId'
get:
operationId: getSiteOpportunityFix
summary: |
Retrieve details of a specific fix
tags:
- opportunity-fixes
responses:
'200':
description: Details of the fix
content:
application/json:
schema:
$ref: './schemas.yaml#/Fix'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]
patch:
operationId: updateSiteOpportunityFix
summary: |
Update specific attributes of an existing fix
tags:
- opportunity-fixes
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/FixUpdate'
responses:
'200':
description: Fix updated
content:
application/json:
schema:
$ref: './schemas.yaml#/Fix'
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]
delete:
operationId: deleteSiteOpportunityFix
summary: |
Delete a fix
tags:
- opportunity-fixes
responses:
'204':
description: Fix deleted
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]


site-opportunity-fix-suggestions:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
- $ref: './parameters.yaml#/fixId'
get:
operationId: getSiteOpportunityFixSuggestions
summary: |
Retrieve a list of all suggestions for a specific fix
tags:
- opportunity-fixes
responses:
'200':
description: A list of suggestions
content:
application/json:
schema:
$ref: './schemas.yaml#/SuggestionList'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]



site-opportunity-status:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
patch:
operationId: updateSiteOpportunityStatus
summary: |
Update the status of one or multiple fixes in one transaction
tags:
- opportunity-fixes
requestBody:
required: true
content:
application/json:
schema:
$ref: './schemas.yaml#/FixStatusUpdate'
responses:
'207':
description: |
A list of fixes updated,
or the status code and error message for the ones failed.
content:
application/json:
schema:
$ref: './schemas.yaml#/FixStatusUpdateListResponse'
security:
- ims_key: [ ]
462 changes: 462 additions & 0 deletions src/controllers/fixes.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions src/dto/fix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

/**
* @import { FixEntity } from "@adobe/spacecat-shared-data-access"
*/

/**
* Data transfer object for Site.
*/
export const FixDto = {

/**
* Converts a Suggestion object into a JSON object.
* @param {Readonly<FixEntity>} fix - FixEntity object.
* @returns {{
* id: string
* suggestionId: string
* type: string
* executedBy: string
* executedAt: string
* publishedAt: string
* changeDetails: object
* status: string
* }} JSON object.
*/
toJSON(fix) {
return {
id: fix.getId(),
opportunityId: fix.getOpportunityId(),
type: fix.getType(),
createdAt: fix.getCreatedAt(),
executedBy: fix.getExecutedAt(),
executedAt: fix.getExecutedAt(),
publishedAt: fix.getPublishedAt(),
changeDetails: fix.getChangeDetails(),
status: fix.getStatus(),
};
},
};
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ import trigger from './controllers/trigger.js';
import { App as SlackApp } from './utils/slack/bolt.cjs';
import ConfigurationController from './controllers/configuration.js';
import FulfillmentController from './controllers/event/fulfillment.js';
import { FixesController } from './controllers/fixes.js';
import ImportController from './controllers/import.js';
import { s3ClientWrapper } from './support/s3.js';
import { multipartFormData } from './support/multipart-form-data.js';
@@ -113,6 +114,7 @@ async function run(request, context) {
const preflightController = PreflightController(context, log, context.env);
const demoController = DemoController(context);
const scrapeController = ScrapeController(context);
const fixesController = new FixesController(context);

/* ---------- build MCP registry & controller ---------- */
const mcpRegistry = buildRegistry({
@@ -143,6 +145,7 @@ async function run(request, context) {
demoController,
scrapeController,
mcpController,
fixesController,
);

const routeMatch = matchPath(method, suffix, routeHandlers);
16 changes: 16 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@
* governing permissions and limitations under the License.
*/

/**
* @import type { FixesController } from "../controllers/fixes.js"
*/

/**
* Extracts parameter names from a route pattern. For example, for the route pattern
* /sites/:siteId/audits/:auditType, the parameter names are siteId and auditType.
@@ -61,6 +65,7 @@ function isStaticRoute(routePattern) {
* @param {Object} demoController - The demo controller.
* @param {Object} scrapeController - The scrape controller.
* @param {Object} mcpController - The MCP controller.
* @param {FixesController} fixesController - The fixes controller.
* @return {{staticRoutes: {}, dynamicRoutes: {}}} - An object with static and dynamic routes.
*/
export default function getRouteHandlers(
@@ -83,6 +88,7 @@ export default function getRouteHandlers(
demoController,
scrapeController,
mcpController,
fixesController,
) {
const staticRoutes = {};
const dynamicRoutes = {};
@@ -165,6 +171,16 @@ export default function getRouteHandlers(
'GET /sites/:siteId/scraped-content/:type': scrapeController.listScrapedContentFiles,
'GET /sites/:siteId/files': scrapeController.getFileByKey,
'POST /mcp': mcpController.handleRpc,

// Fixes
'GET /sites/:siteId/opportunities/:opportunityId/fixes': (c) => fixesController.getAllForOpportunity(c),
'GET /sites/:siteId/opportunities/:opportunityId/fixes/by-status/:status': (c) => fixesController.getByStatus(c),
'GET /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.getByID(c),
'GET /sites/:siteId/opportunities/:opportunityId/fixes/:fixId/suggestions': (c) => fixesController.getAllSuggestionsForFix(c),
'POST /sites/:siteId/opportunities/:opportunityId/fixes': (c) => fixesController.createFixes(c),
'PATCH /sites/:siteId/opportunities/:opportunityId/status': (c) => fixesController.patchFixesStatus(c),
'PATCH /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.patchFix(c),
'DELETE /sites/:siteId/opportunities/:opportunityId/fixes/:fixId': (c) => fixesController.removeFix(c),
};

// Initialization of static and dynamic routes
1,035 changes: 1,035 additions & 0 deletions test/controllers/fixes.test.js

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions test/routes/index.test.js
Original file line number Diff line number Diff line change
@@ -126,6 +126,17 @@ describe('getRouteHandlers', () => {
listScrapedContentFiles: sinon.stub(),
};

const mockFixesController = {
getAllForOpportunity: () => null,
getByStatus: () => null,
getByID: () => null,
getAllSuggestionsForFix: () => null,
createFixes: () => null,
patchFixesStatus: () => null,
patchFix: () => null,
removeFix: () => null,
};

it('segregates static and dynamic routes', () => {
const { staticRoutes, dynamicRoutes } = getRouteHandlers(
mockAuditsController,
@@ -147,6 +158,7 @@ describe('getRouteHandlers', () => {
mockDemoController,
mockScrapeController,
mockMcpController,
mockFixesController,
);

expect(staticRoutes).to.have.all.keys(
@@ -245,6 +257,14 @@ describe('getRouteHandlers', () => {
'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/status',
'GET /sites/:siteId/scraped-content/:type',
'GET /sites/:siteId/files',
'GET /sites/:siteId/opportunities/:opportunityId/fixes',
'GET /sites/:siteId/opportunities/:opportunityId/fixes/by-status/:status',
'GET /sites/:siteId/opportunities/:opportunityId/fixes/:fixId',
'GET /sites/:siteId/opportunities/:opportunityId/fixes/:fixId/suggestions',
'POST /sites/:siteId/opportunities/:opportunityId/fixes',
'PATCH /sites/:siteId/opportunities/:opportunityId/status',
'PATCH /sites/:siteId/opportunities/:opportunityId/fixes/:fixId',
'DELETE /sites/:siteId/opportunities/:opportunityId/fixes/:fixId',
);

expect(dynamicRoutes['GET /audits/latest/:auditType'].handler).to.equal(mockAuditsController.getAllLatest);