import type { OperationPageQuery } from '@multi-simadmin/contracts'; import type { FastifyInstance, FastifyRequest } from 'fastify'; import { OperationCatalogService } from '../../application/operations/operation-catalog-service.js'; import type { OperationCatalogRegistry } from '../../application/operations/operation-catalog-service.js'; const capabilities = new Set(['query', 'command', 'job']); const keys = new Set([ 'page', 'pageSize', 'sort', 'direction', 'risk', 'capability', 'batchable', 'search', ]); class CatalogValidationError extends Error {} function scalarQuery(request: FastifyRequest): Record { const raw = request.query; if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new CatalogValidationError(); const result: Record = {}; for (const [key, value] of Object.entries(raw)) { if (!keys.has(key) || typeof value !== 'string') throw new CatalogValidationError(); result[key] = value; } return result; } function parseQuery(request: FastifyRequest): OperationPageQuery { const query = scalarQuery(request); const result: { page?: number; pageSize?: number; sort?: 'operationId' | 'risk' | 'capability'; direction?: 'asc' | 'desc'; risk?: 'R0' | 'R1' | 'R2' | 'R3'; capability?: string; batchable?: boolean; search?: string; } = {}; const integer = (key: 'page' | 'pageSize', maximum?: number): void => { const value = query[key]; if (value === undefined) return; if (!/^[1-9]\d*$/.test(value)) throw new CatalogValidationError(); const parsed = Number(value); if (!Number.isSafeInteger(parsed) || (maximum !== undefined && parsed > maximum)) throw new CatalogValidationError(); result[key] = parsed; }; integer('page'); integer('pageSize', 100); const sort = query.sort; if (sort !== undefined) { if (sort !== 'operationId' && sort !== 'risk' && sort !== 'capability') throw new CatalogValidationError(); result.sort = sort; } const direction = query.direction; if (direction !== undefined) { if (direction !== 'asc' && direction !== 'desc') throw new CatalogValidationError(); result.direction = direction; } const risk = query.risk; if (risk !== undefined) { if (risk !== 'R0' && risk !== 'R1' && risk !== 'R2' && risk !== 'R3') throw new CatalogValidationError(); result.risk = risk; } const capability = query.capability; if (capability !== undefined) { if (!capabilities.has(capability)) throw new CatalogValidationError(); result.capability = capability; } const batchable = query.batchable; if (batchable !== undefined) { if (batchable !== 'true' && batchable !== 'false') throw new CatalogValidationError(); result.batchable = batchable === 'true'; } const search = query.search; if (search !== undefined) { if (search.length > 200) throw new CatalogValidationError(); result.search = search; } return result; } export function registerOperationRoutes( app: FastifyInstance, registry: OperationCatalogRegistry, ): void { const service = new OperationCatalogService(registry); app.get('/api/v1/operations', async (request, reply) => { try { return service.list(parseQuery(request)); } catch (error) { if (!(error instanceof CatalogValidationError)) throw error; return reply.code(400).type('application/problem+json').send({ type: 'about:blank', title: 'Bad Request', status: 400, code: 'VALIDATION_FAILED', detail: 'The request did not satisfy the endpoint schema.', requestId: request.id, }); } }); }