Files
insta-recipe/src/routes/api/queue/+server.ts
Giancarmine Salucci 6b022d8348 feat(validation): relax Instagram URL validation to support all content types
- Create validateInstagramUrl utility using URL constructor
- Replace regex-based validation with hostname and protocol checks
- Support posts, reels, IGTV, and URLs with query parameters
- Add comprehensive unit tests (22 tests, all passing)
- Add integration tests for new URL formats
- Update API documentation with supported URL formats

Closes: #RelaxInstagramUrlValidation
2025-12-22 03:10:29 +01:00

149 lines
4.5 KiB
TypeScript

/**
* Queue API Endpoints
*
* Provides HTTP interface for queue operations:
* - POST /api/queue - Enqueue Instagram URL for processing
* - GET /api/queue - List all queue items with optional status filtering
*/
import { json, error } from '@sveltejs/kit';
import { queueManager } from '$lib/server/queue/QueueManager';
import { validateInstagramUrl } from '$lib/server/validation/instagram-url';
import type { RequestHandler } from './$types';
/**
* POST /api/queue - Enqueue Instagram URL
*
* Body: { url: string }
* Returns: { id: string, url: string, status: string, enqueuedAt: string }
*
* Validates Instagram URL format and enqueues for processing.
* Returns 400 for invalid URLs, 500 for server errors.
*/
export const POST: RequestHandler = async ({ request }) => {
try {
// Parse JSON body with proper error handling
let body;
try {
body = await request.json();
} catch (jsonError) {
return error(400, { message: 'Invalid JSON in request body' });
}
// Validate request body
if (!body || typeof body !== 'object') {
return error(400, { message: 'Request body must be JSON object' });
}
const { url } = body;
// Validate URL presence
if (!url || typeof url !== 'string') {
return error(400, { message: 'URL is required and must be a string' });
}
// Validate Instagram URL format using utility
const validation = validateInstagramUrl(url);
if (!validation.valid) {
return error(400, { message: validation.error || 'Invalid Instagram URL' });
}
// Enqueue the URL
const queueItem = queueManager.enqueue(url);
// Return minimal response (full details available at GET /api/queue/{id})
return json({
id: queueItem.id,
url: queueItem.url,
status: queueItem.status,
enqueuedAt: queueItem.enqueuedAt
});
} catch (err) {
console.error('Failed to enqueue URL:', err);
return error(500, { message: 'Internal server error' });
}
};
/**
* GET /api/queue - List queue items
*
* Query params:
* - status?: string - Filter by status (pending, in_progress, success, unhealthy, error)
* - limit?: number - Maximum items to return (default: 50, max: 200)
* - offset?: number - Pagination offset (default: 0)
*
* Returns: { items: QueueItem[], total: number, hasMore: boolean }
*/
export const GET: RequestHandler = async ({ url }) => {
try {
const searchParams = url.searchParams;
// Parse query parameters
const statusFilter = searchParams.get('status');
const limitParam = searchParams.get('limit');
const offsetParam = searchParams.get('offset');
// Validate and parse limit
let limit = 50; // default
if (limitParam) {
const parsedLimit = parseInt(limitParam, 10);
if (isNaN(parsedLimit) || parsedLimit < 1) {
return error(400, { message: 'Limit must be a positive integer' });
}
if (parsedLimit > 200) {
return error(400, { message: 'Limit cannot exceed 200' });
}
limit = parsedLimit;
}
// Validate and parse offset
let offset = 0; // default
if (offsetParam) {
const parsedOffset = parseInt(offsetParam, 10);
if (isNaN(parsedOffset) || parsedOffset < 0) {
return error(400, { message: 'Offset must be a non-negative integer' });
}
offset = parsedOffset;
}
// Validate status filter
const validStatuses = ['pending', 'in_progress', 'success', 'unhealthy', 'error'];
if (statusFilter && !validStatuses.includes(statusFilter)) {
return error(400, {
message: `Invalid status filter. Must be one of: ${validStatuses.join(', ')}`
});
}
// Get all items
let items = queueManager.getAll();
const totalCount = items.length;
// Apply status filter
if (statusFilter) {
items = items.filter(item => item.status === statusFilter);
}
// Sort by enqueued time (newest first)
items.sort((a, b) => new Date(b.enqueuedAt).getTime() - new Date(a.enqueuedAt).getTime());
// Apply pagination
const paginatedItems = items.slice(offset, offset + limit);
const hasMore = (offset + limit) < items.length;
return json({
items: paginatedItems,
total: statusFilter ? items.length : totalCount,
hasMore,
pagination: {
offset,
limit,
count: paginatedItems.length
}
});
} catch (err) {
console.error('Failed to list queue items:', err);
return error(500, { message: 'Internal server error' });
}
};