Files
dd0c/products/04-lightweight-idp/src/api/discovery.ts
Max Mayfield a17527dfa4 Flesh out dd0c/portal: service CRUD, discovery API, Meilisearch search, data layer
- Service API: list (filtered by type/owner/lifecycle/tier), detail, upsert, delete, ownership summary
- Discovery API: trigger AWS/GitHub scans, scan history, staged update review (apply/reject)
- Search: Meilisearch full-text with PG ILIKE fallback, reindex endpoint
- Data layer: withTenant() RLS wrapper, Zod config with MEILI_URL/MEILI_KEY
- Fastify server entry point
2026-03-01 03:05:55 +00:00

64 lines
2.2 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import pino from 'pino';
import { withTenant } from '../data/db.js';
const logger = pino({ name: 'api-discovery' });
export function registerDiscoveryRoutes(app: FastifyInstance) {
// Trigger AWS discovery scan
app.post('/api/v1/discovery/aws', async (req, reply) => {
const tenantId = (req as any).tenantId;
// TODO: Enqueue scan job (Redis queue or direct execution)
logger.info({ tenantId }, 'AWS discovery scan triggered');
return reply.status(202).send({ status: 'scan_queued', scanner: 'aws' });
});
// Trigger GitHub discovery scan
app.post('/api/v1/discovery/github', async (req, reply) => {
const tenantId = (req as any).tenantId;
logger.info({ tenantId }, 'GitHub discovery scan triggered');
return reply.status(202).send({ status: 'scan_queued', scanner: 'github' });
});
// Get scan history
app.get('/api/v1/discovery/history', async (req, reply) => {
const tenantId = (req as any).tenantId;
const result = await withTenant(tenantId, async (client) => {
return client.query('SELECT * FROM scan_history ORDER BY started_at DESC LIMIT 20');
});
return { scans: result.rows };
});
// List staged updates (from partial scans)
app.get('/api/v1/discovery/staged', async (req, reply) => {
const tenantId = (req as any).tenantId;
const result = await withTenant(tenantId, async (client) => {
return client.query("SELECT * FROM staged_updates WHERE status = 'pending' ORDER BY created_at DESC");
});
return { staged: result.rows };
});
// Apply or reject staged update
app.post('/api/v1/discovery/staged/:id/:action', async (req, reply) => {
const { id, action } = req.params as { id: string; action: string };
const tenantId = (req as any).tenantId;
if (action !== 'apply' && action !== 'reject') {
return reply.status(400).send({ error: 'Action must be apply or reject' });
}
const newStatus = action === 'apply' ? 'applied' : 'rejected';
await withTenant(tenantId, async (client) => {
await client.query('UPDATE staged_updates SET status = $1 WHERE id = $2', [newStatus, id]);
// TODO: If applied, merge changes into services table
});
return { status: newStatus };
});
}