Files
multi-simadmin/apps/api/src/interface/http/operation-routes.ts
T

110 lines
3.6 KiB
TypeScript

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<string, string> {
const raw = request.query;
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
throw new CatalogValidationError();
const result: Record<string, string> = {};
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,
});
}
});
}