/** * Dependencies Route — REST API for service dependency tracking * * Endpoints: * GET /dependencies/graph Full dependency graph * GET /dependencies/validate Validate a proposed dep chain * GET /dependencies/:serviceId Direct deps for one service * GET /dependencies/:serviceId/chain Ordered restart chain * GET /dependencies/:serviceId/status Dependency health status * POST /dependencies/:serviceId Set dependencies * DELETE /dependencies/:serviceId Remove all dependencies * POST /dependencies/:serviceId/restart Restart with dependency chain * * @module routes/dependencies */ const express = require('express'); const { success, error: errorResponse } = require('../src/utils/responses'); const { NotFoundError, ValidationError } = require('../errors'); /** * Dependencies route factory * * @param {Object} deps - Explicit dependencies * @param {Object} deps.dependencyManager - DependencyManager instance * @param {Object} deps.servicesStateManager - State manager for services.json * @param {Object} deps.docker - Docker client wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.logError - Error logging function * @param {Function} deps.resyncHealthChecker - Health checker resync function * @param {Object} deps.log - Logger instance * @returns {express.Router} */ module.exports = function({ dependencyManager, servicesStateManager, docker, asyncHandler, logError, resyncHealthChecker, log, }) { const router = express.Router(); // ------------------------------------------------------------------------- // GET /dependencies/graph — Full dependency graph // ------------------------------------------------------------------------- router.get('/graph', asyncHandler(async (req, res) => { const graph = await dependencyManager.getDependencyGraph(); success(res, { graph }); }, 'dep-graph')); // ------------------------------------------------------------------------- // GET /dependencies/validate — Validate a proposed dep chain (query params) // ------------------------------------------------------------------------- router.get('/validate', asyncHandler(async (req, res) => { const { serviceId, dependsOn } = req.query; if (!serviceId) { throw new ValidationError('serviceId query parameter is required'); } // dependsOn may be a comma-separated string or already an array let parsed; if (Array.isArray(dependsOn)) { parsed = dependsOn; } else if (typeof dependsOn === 'string' && dependsOn.length > 0) { parsed = dependsOn.split(',').map(s => s.trim()).filter(Boolean); } else { parsed = []; } const result = await dependencyManager.validateDependencies(serviceId, parsed); success(res, result); }, 'dep-validate')); // ------------------------------------------------------------------------- // GET /dependencies/:serviceId — Direct deps for one service // ------------------------------------------------------------------------- router.get('/:serviceId', asyncHandler(async (req, res) => { const { serviceId } = req.params; const dependencies = await dependencyManager.getDependencies(serviceId); const dependents = await dependencyManager.getDependents(serviceId); // Read the service's current dependsOn array const services = await servicesStateManager.read(); const allServices = Array.isArray(services) ? services : (services.services || []); const service = allServices.find(s => s.id === serviceId); if (!service) { throw new NotFoundError(`Service "${serviceId}"`); } success(res, { serviceId, dependsOn: service.dependsOn || [], dependencies, dependents: dependents.map(d => ({ id: d.id, name: d.name })), }); }, 'dep-get')); // ------------------------------------------------------------------------- // GET /dependencies/:serviceId/chain — Ordered restart chain // ------------------------------------------------------------------------- router.get('/:serviceId/chain', asyncHandler(async (req, res) => { const { serviceId } = req.params; const chain = await dependencyManager.getOrderedRestartChain(serviceId); success(res, { serviceId, chain }); }, 'dep-chain')); // ------------------------------------------------------------------------- // GET /dependencies/:serviceId/status — Dependency health status // ------------------------------------------------------------------------- router.get('/:serviceId/status', asyncHandler(async (req, res) => { const { serviceId } = req.params; const statuses = await dependencyManager.getDependencyStatus(serviceId); success(res, { serviceId, statuses }); }, 'dep-status')); // ------------------------------------------------------------------------- // POST /dependencies/:serviceId — Set dependencies // ------------------------------------------------------------------------- router.post('/:serviceId', asyncHandler(async (req, res) => { const { serviceId } = req.params; const { dependsOn } = req.body; if (!Array.isArray(dependsOn)) { throw new ValidationError('Request body must include dependsOn as an array of service IDs'); } // Validate first const validation = await dependencyManager.validateDependencies(serviceId, dependsOn); if (!validation.valid) { return errorResponse(res, validation.errors.join('; '), 400); } // Update the service let found = false; await servicesStateManager.update(services => { const arr = Array.isArray(services) ? services : []; return arr.map(s => { if (s.id === serviceId) { found = true; return { ...s, dependsOn: dependsOn.slice() }; } return s; }); }); if (!found) { throw new NotFoundError(`Service "${serviceId}"`); } log.info('dependency', 'Dependencies updated', { serviceId, dependsOn }); success(res, { message: `Dependencies updated for "${serviceId}"`, serviceId, dependsOn, }); }, 'dep-set')); // ------------------------------------------------------------------------- // DELETE /dependencies/:serviceId — Remove all dependencies for a service // ------------------------------------------------------------------------- router.delete('/:serviceId', asyncHandler(async (req, res) => { const { serviceId } = req.params; let found = false; await servicesStateManager.update(services => { const arr = Array.isArray(services) ? services : []; return arr.map(s => { if (s.id === serviceId) { found = true; const updated = { ...s }; delete updated.dependsOn; return updated; } return s; }); }); if (!found) { throw new NotFoundError(`Service "${serviceId}"`); } log.info('dependency', 'Dependencies removed', { serviceId }); success(res, { message: `All dependencies removed for "${serviceId}"`, serviceId, }); }, 'dep-delete')); // ------------------------------------------------------------------------- // POST /dependencies/:serviceId/restart — Restart with dependency chain // ------------------------------------------------------------------------- router.post('/:serviceId/restart', asyncHandler(async (req, res) => { const { serviceId } = req.params; // Verify the service exists const services = await servicesStateManager.read(); const allServices = Array.isArray(services) ? services : (services.services || []); if (!allServices.find(s => s.id === serviceId)) { throw new NotFoundError(`Service "${serviceId}"`); } // Get the chain first for the response (before async restart begins) let chain; try { chain = await dependencyManager.getOrderedRestartChain(serviceId); } catch (err) { return errorResponse(res, err.message, 400); } // Respond immediately with the chain order success(res, { message: `Dependency restart initiated for "${serviceId}"`, serviceId, chain, }); // Run the restart chain asynchronously so the client doesn't block dependencyManager.restartWithDependencies(serviceId).catch(err => { if (log) { log.error('dependency', 'Async dependency restart failed', { serviceId, error: err.message, }); } }); }, 'dep-restart')); return router; };