Skip to content

Commit ca11a66

Browse files
author
Kshitiz Mishra
committed
feat: add edge-fetch API endpoint for Tokowaka URL fetching
- Add fetchFromEdge method in suggestions controller - Implement site and opportunity validation - Add URL format validation (HTTP/HTTPS only) - Custom User-Agent: Tokowaka-AI Tokowaka/1.0 - Add unit tests (13 test cases) - Update OpenAPI documentation
1 parent 7810fe1 commit ca11a66

File tree

6 files changed

+482
-0
lines changed

6 files changed

+482
-0
lines changed

docs/openapi/api.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ paths:
220220
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-status'
221221
/sites/{siteId}/opportunities/{opportunityId}/suggestions/auto-fix:
222222
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-auto-fix'
223+
/sites/{siteId}/opportunities/{opportunityId}/suggestions/edge-fetch:
224+
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-edge-fetch'
223225
/sites/{siteId}/site-enrollments:
224226
$ref: './site-enrollments-api.yaml#/site-enrollments-by-site'
225227
/sites/{siteId}/user-activities:

docs/openapi/site-opportunities.yaml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,84 @@ site-opportunity-suggestion:
610610
security:
611611
- ims_key: [ ]
612612

613+
site-opportunity-suggestions-edge-fetch:
614+
parameters:
615+
- $ref: './parameters.yaml#/siteId'
616+
- $ref: './parameters.yaml#/opportunityId'
617+
post:
618+
operationId: fetchFromEdge
619+
summary: |
620+
Fetch content from a URL using Tokowaka-AI User-Agent
621+
description: |
622+
Fetches content from a given URL using the Tokowaka-AI User-Agent.
623+
This is useful for checking what content is currently deployed or accessible
624+
from a specific URL.
625+
tags:
626+
- opportunity-suggestions
627+
requestBody:
628+
required: true
629+
content:
630+
application/json:
631+
schema:
632+
type: object
633+
required:
634+
- url
635+
properties:
636+
url:
637+
type: string
638+
format: uri
639+
description: The URL to fetch content from
640+
example: https://www.lovesac.com/sactionals
641+
responses:
642+
'200':
643+
description: Successfully fetched content from the URL
644+
content:
645+
application/json:
646+
schema:
647+
type: object
648+
properties:
649+
status:
650+
type: string
651+
enum: [success, error]
652+
description: Fetch status
653+
statusCode:
654+
type: integer
655+
description: HTTP status code from the URL
656+
message:
657+
type: string
658+
description: Error message if fetch failed
659+
html:
660+
type: object
661+
description: HTML content object (similar to edge-preview response)
662+
properties:
663+
url:
664+
type: string
665+
description: The requested URL
666+
content:
667+
type: string
668+
nullable: true
669+
description: The fetched HTML content
670+
example:
671+
status: success
672+
statusCode: 200
673+
html:
674+
url: https://www.lovesac.com/sactionals
675+
content: "<html>...</html>"
676+
'400':
677+
$ref: './responses.yaml#/400'
678+
'401':
679+
$ref: './responses.yaml#/401'
680+
'404':
681+
$ref: './responses.yaml#/404'
682+
'429':
683+
$ref: './responses.yaml#/429'
684+
'500':
685+
$ref: './responses.yaml#/500'
686+
'503':
687+
$ref: './responses.yaml#/503'
688+
security:
689+
- ims_key: [ ]
690+
613691
site-opportunity-suggestion-fixes:
614692
parameters:
615693
- $ref: './parameters.yaml#/siteId'

src/controllers/suggestions.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,12 +1298,118 @@ function SuggestionsController(ctx, sqs, env) {
12981298
return createResponse(response, 207);
12991299
};
13001300

1301+
/**
1302+
* Fetches content from a URL using Tokowaka-AI User-Agent.
1303+
* This is a simple URL-based fetch, useful for checking deployed content.
1304+
* @param {Object} context of the request
1305+
* @returns {Promise<Response>} Fetch response with content
1306+
*/
1307+
const fetchFromEdge = async (context) => {
1308+
const siteId = context.params?.siteId;
1309+
const opportunityId = context.params?.opportunityId;
1310+
1311+
if (!isValidUUID(siteId)) {
1312+
return badRequest('Site ID required');
1313+
}
1314+
1315+
if (!isValidUUID(opportunityId)) {
1316+
return badRequest('Opportunity ID required');
1317+
}
1318+
1319+
// validate request body
1320+
if (!isNonEmptyObject(context.data)) {
1321+
return badRequest('No data provided');
1322+
}
1323+
1324+
const { url } = context.data;
1325+
1326+
// Validate URL
1327+
if (!hasText(url)) {
1328+
return badRequest('URL is required');
1329+
}
1330+
1331+
// Validate URL format
1332+
try {
1333+
const parsedUrl = new URL(url); // throws if invalid
1334+
if (!parsedUrl.protocol.startsWith('http')) {
1335+
return badRequest('Invalid URL format: only HTTP/HTTPS URLs are allowed');
1336+
}
1337+
} catch (error) {
1338+
return badRequest('Invalid URL format');
1339+
}
1340+
1341+
const site = await Site.findById(siteId);
1342+
if (!site) {
1343+
return notFound('Site not found');
1344+
}
1345+
1346+
if (!await accessControlUtil.hasAccess(site)) {
1347+
return forbidden('User does not belong to the organization');
1348+
}
1349+
1350+
const opportunity = await Opportunity.findById(opportunityId);
1351+
if (!opportunity || opportunity.getSiteId() !== siteId) {
1352+
return notFound('Opportunity not found');
1353+
}
1354+
1355+
try {
1356+
context.log.info(`Fetching content from URL: ${url}`);
1357+
1358+
// Make fetch request with Tokowaka-AI User-Agent
1359+
const response = await fetch(url, {
1360+
method: 'GET',
1361+
headers: {
1362+
'User-Agent': 'Tokowaka-AI Tokowaka/1.0',
1363+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1364+
},
1365+
});
1366+
1367+
if (!response.ok) {
1368+
context.log.warn(`Failed to fetch URL. Status: ${response.status}`);
1369+
return ok({
1370+
status: 'error',
1371+
statusCode: response.status,
1372+
message: `Failed to fetch content from URL: ${url}`,
1373+
html: {
1374+
url,
1375+
content: null,
1376+
},
1377+
});
1378+
}
1379+
1380+
const content = await response.text();
1381+
1382+
context.log.info(`Successfully fetched content from URL: ${url}`);
1383+
1384+
return ok({
1385+
status: 'success',
1386+
statusCode: response.status,
1387+
html: {
1388+
url,
1389+
content,
1390+
},
1391+
});
1392+
} catch (error) {
1393+
context.log.error(`Error fetching from URL ${url}: ${error.message}`, error);
1394+
return ok({
1395+
status: 'error',
1396+
statusCode: 500,
1397+
message: `Error fetching content: ${error.message}`,
1398+
html: {
1399+
url,
1400+
content: null,
1401+
},
1402+
});
1403+
}
1404+
};
1405+
13011406
return {
13021407
autofixSuggestions,
13031408
createSuggestions,
13041409
deploySuggestionToEdge,
13051410
rollbackSuggestionFromEdge,
13061411
previewSuggestions,
1412+
fetchFromEdge,
13071413
getAllForOpportunity,
13081414
getAllForOpportunityPaged,
13091415
getByID,

src/routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export default function getRouteHandlers(
195195
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-deploy': suggestionsController.deploySuggestionToEdge,
196196
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-rollback': suggestionsController.rollbackSuggestionFromEdge,
197197
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-preview': suggestionsController.previewSuggestions,
198+
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-fetch': suggestionsController.fetchFromEdge,
198199
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus,
199200
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit/:cursor': suggestionsController.getByStatusPaged,
200201
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit': suggestionsController.getByStatusPaged,

0 commit comments

Comments
 (0)