import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { withTenant } from '../data/db.js'; const listQuerySchema = z.object({ page: z.coerce.number().min(1).default(1), limit: z.coerce.number().min(1).max(100).default(20), type: z.string().optional(), owner: z.string().optional(), lifecycle: z.enum(['active', 'deprecated', 'decommissioned']).optional(), tier: z.enum(['critical', 'high', 'medium', 'low']).optional(), }); const upsertServiceSchema = z.object({ name: z.string().min(1).max(200), type: z.string().default('unknown'), owner: z.string().default('unknown'), description: z.string().max(2000).optional(), tier: z.enum(['critical', 'high', 'medium', 'low']).default('medium'), lifecycle: z.enum(['active', 'deprecated', 'decommissioned']).default('active'), links: z.record(z.string()).default({}), tags: z.record(z.string()).default({}), }); export function registerServiceRoutes(app: FastifyInstance) { // List services app.get('/api/v1/services', async (req, reply) => { const query = listQuerySchema.parse(req.query); const tenantId = (req as any).tenantId; const offset = (query.page - 1) * query.limit; const result = await withTenant(tenantId, async (client) => { let sql = 'SELECT * FROM services WHERE 1=1'; const params: any[] = []; let idx = 1; if (query.type) { sql += ` AND type = $${idx++}`; params.push(query.type); } if (query.owner) { sql += ` AND owner = $${idx++}`; params.push(query.owner); } if (query.lifecycle) { sql += ` AND lifecycle = $${idx++}`; params.push(query.lifecycle); } if (query.tier) { sql += ` AND tier = $${idx++}`; params.push(query.tier); } sql += ` ORDER BY name LIMIT $${idx++} OFFSET $${idx++}`; params.push(query.limit, offset); return client.query(sql, params); }); return { services: result.rows, page: query.page, limit: query.limit }; }); // Get service detail app.get('/api/v1/services/:id', async (req, reply) => { const { id } = req.params as { id: string }; const tenantId = (req as any).tenantId; const result = await withTenant(tenantId, async (client) => { return client.query('SELECT * FROM services WHERE id = $1', [id]); }); if (!result.rows[0]) return reply.status(404).send({ error: 'Not found' }); return { service: result.rows[0] }; }); // Create/update service (manual entry) app.put('/api/v1/services', async (req, reply) => { const body = upsertServiceSchema.parse(req.body); const tenantId = (req as any).tenantId; const result = await withTenant(tenantId, async (client) => { return client.query( `INSERT INTO services (tenant_id, name, type, owner, owner_source, description, tier, lifecycle, links, tags) VALUES ($1, $2, $3, $4, 'config', $5, $6, $7, $8, $9) ON CONFLICT (tenant_id, name) DO UPDATE SET type = EXCLUDED.type, owner = EXCLUDED.owner, owner_source = 'config', description = EXCLUDED.description, tier = EXCLUDED.tier, lifecycle = EXCLUDED.lifecycle, links = EXCLUDED.links, tags = EXCLUDED.tags, updated_at = now() RETURNING *`, [tenantId, body.name, body.type, body.owner, body.description, body.tier, body.lifecycle, JSON.stringify(body.links), JSON.stringify(body.tags)], ); }); return reply.status(201).send({ service: result.rows[0] }); }); // Delete service app.delete('/api/v1/services/:id', async (req, reply) => { const { id } = req.params as { id: string }; const tenantId = (req as any).tenantId; await withTenant(tenantId, async (client) => { await client.query('DELETE FROM services WHERE id = $1', [id]); }); return { status: 'deleted' }; }); // Ownership summary (who owns what) app.get('/api/v1/ownership', async (req, reply) => { const tenantId = (req as any).tenantId; const result = await withTenant(tenantId, async (client) => { return client.query(` SELECT owner, owner_source, COUNT(*)::int as service_count, array_agg(name ORDER BY name) as services FROM services WHERE lifecycle = 'active' GROUP BY owner, owner_source ORDER BY service_count DESC `); }); return { owners: result.rows }; }); }