From 3de65dbf8107155fcdba12f9a5d0de3dc207b4b5 Mon Sep 17 00:00:00 2001 From: Krystie Date: Sun, 29 Mar 2026 21:45:28 -0700 Subject: [PATCH] Refactor recipes routes: explicit dependency injection - Updated all recipes route modules to use destructured dependencies - Added JSDoc comments for factory functions - Replaced ctx. references with direct parameter access - All files pass syntax validation Files refactored: - routes/recipes/deploy.js - routes/recipes/manage.js - routes/recipes/index.js (orchestrator) --- dashcaddy-api/routes/recipes/deploy.js | 69 +++++++++++++++----------- dashcaddy-api/routes/recipes/index.js | 22 ++++++-- dashcaddy-api/routes/recipes/manage.js | 42 +++++++++------- 3 files changed, 83 insertions(+), 50 deletions(-) diff --git a/dashcaddy-api/routes/recipes/deploy.js b/dashcaddy-api/routes/recipes/deploy.js index 19d3a54..3a82473 100644 --- a/dashcaddy-api/routes/recipes/deploy.js +++ b/dashcaddy-api/routes/recipes/deploy.js @@ -3,7 +3,18 @@ const { ValidationError } = require('../../errors'); const crypto = require('crypto'); const { DOCKER } = require('../../constants'); -module.exports = function(ctx) { +/** + * Recipes deployment routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.docker - Docker client wrapper + * @param {Object} deps.credentialManager - Credential manager + * @param {Object} deps.servicesStateManager - Services state manager + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Function} deps.errorResponse - Error response helper + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ +module.exports = function({ docker, credentialManager, servicesStateManager, asyncHandler, errorResponse, log }) { const router = express.Router(); /** @@ -12,14 +23,14 @@ module.exports = function(ctx) { * POST /api/recipes/deploy * Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } } */ - router.post('/deploy', ctx.asyncHandler(async (req, res) => { + router.post('/deploy', asyncHandler(async (req, res) => { const { recipeId, config } = req.body; const { RECIPE_TEMPLATES } = require('../../recipe-templates'); const recipe = RECIPE_TEMPLATES[recipeId]; if (!recipe) throw new ValidationError('Invalid recipe template', 'recipeId'); - ctx.log.info('recipe', 'Starting recipe deployment', { recipeId, name: recipe.name }); + log.info('recipe', 'Starting recipe deployment', { recipeId, name: recipe.name }); // Determine which components to deploy const selectedIds = new Set(config.selectedComponents || recipe.components.filter(c => c.required).map(c => c.id)); @@ -40,18 +51,18 @@ module.exports = function(ctx) { if (recipe.network) { networkName = recipe.network.name; try { - await ctx.docker.client.createNetwork({ + await docker.client.createNetwork({ Name: networkName, Driver: recipe.network.driver || 'bridge', Labels: { 'sami.managed': 'true', 'sami.recipe': recipeId } }); - ctx.log.info('recipe', 'Created Docker network', { networkName }); + log.info('recipe', 'Created Docker network', { networkName }); } catch (e) { // Network might already exist if (!e.message.includes('already exists')) { throw new Error(`Failed to create network ${networkName}: ${e.message}`); } - ctx.log.info('recipe', 'Docker network already exists', { networkName }); + log.info('recipe', 'Docker network already exists', { networkName }); } } @@ -61,7 +72,7 @@ module.exports = function(ctx) { try { for (const component of componentsToDeploy) { try { - ctx.log.info('recipe', `Deploying component: ${component.id}`, { + log.info('recipe', `Deploying component: ${component.id}`, { role: component.role, internal: component.internal || false }); @@ -69,11 +80,11 @@ module.exports = function(ctx) { const result = await deployComponent(component, recipe, config, generatedPasswords, networkName); deployedComponents.push(result); - ctx.log.info('recipe', `Component deployed: ${component.id}`, { + log.info('recipe', `Component deployed: ${component.id}`, { containerId: result.containerId?.substring(0, 12) }); } catch (componentError) { - ctx.log.error('recipe', `Component failed: ${component.id}`, { + log.error('recipe', `Component failed: ${component.id}`, { error: componentError.message }); errors.push({ componentId: component.id, role: component.role, error: componentError.message }); @@ -104,10 +115,10 @@ module.exports = function(ctx) { // Run auto-connect if available if (recipe.autoConnect?.enabled && errors.length === 0) { - ctx.log.info('recipe', 'Running auto-connect for recipe', { recipeId }); + log.info('recipe', 'Running auto-connect for recipe', { recipeId }); // Auto-connect will be handled asynchronously — don't block the response runAutoConnect(recipe, deployedComponents, config).catch(e => { - ctx.log.warn('recipe', 'Auto-connect had errors', { recipeId, error: e.message }); + log.warn('recipe', 'Auto-connect had errors', { recipeId, error: e.message }); }); } @@ -136,17 +147,17 @@ module.exports = function(ctx) { res.json(response); } catch (error) { - ctx.log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message }); + log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message }); // Cleanup: remove partially deployed containers for (const deployed of deployedComponents) { try { if (deployed.containerId) { - const container = ctx.docker.client.getContainer(deployed.containerId); + const container = docker.client.getContainer(deployed.containerId); await container.remove({ force: true }); } } catch (cleanupError) { - ctx.log.warn('recipe', 'Cleanup failed for component', { + log.warn('recipe', 'Cleanup failed for component', { componentId: deployed.id, error: cleanupError.message }); } @@ -155,10 +166,10 @@ module.exports = function(ctx) { // Cleanup network if (networkName) { try { - const network = ctx.docker.client.getNetwork(networkName); + const network = docker.client.getNetwork(networkName); await network.remove(); } catch (e) { - ctx.log.warn('recipe', 'Network cleanup failed', { networkName, error: e.message }); + log.warn('recipe', 'Network cleanup failed', { networkName, error: e.message }); } } @@ -284,11 +295,11 @@ module.exports = function(ctx) { // Pull image try { - ctx.log.info('recipe', `Pulling image: ${dockerConfig.image}`); - await ctx.docker.pull(dockerConfig.image); + log.info('recipe', `Pulling image: ${dockerConfig.image}`); + await docker.pull(dockerConfig.image); } catch (e) { - ctx.log.warn('recipe', `Pull failed, checking local: ${dockerConfig.image}`); - const images = await ctx.docker.client.listImages({ + log.warn('recipe', `Pull failed, checking local: ${dockerConfig.image}`); + const images = await docker.client.listImages({ filters: { reference: [dockerConfig.image] } }); if (images.length === 0) throw new Error(`Image not found: ${dockerConfig.image}`); @@ -296,7 +307,7 @@ module.exports = function(ctx) { // Remove stale container try { - const existing = ctx.docker.client.getContainer(containerName); + const existing = docker.client.getContainer(containerName); await existing.inspect(); await existing.remove({ force: true }); await new Promise(r => setTimeout(r, 1000)); @@ -305,17 +316,17 @@ module.exports = function(ctx) { } // Create and start container - const container = await ctx.docker.client.createContainer(containerConfig); + const container = await docker.client.createContainer(containerConfig); await container.start(); // Connect to recipe network if (networkName) { try { - const network = ctx.docker.client.getNetwork(networkName); + const network = docker.client.getNetwork(networkName); await network.connect({ Container: container.id }); - ctx.log.info('recipe', `Connected ${component.id} to network ${networkName}`); + log.info('recipe', `Connected ${component.id} to network ${networkName}`); } catch (e) { - ctx.log.warn('recipe', `Failed to connect ${component.id} to network`, { error: e.message }); + log.warn('recipe', `Failed to connect ${component.id} to network`, { error: e.message }); } } @@ -332,7 +343,7 @@ module.exports = function(ctx) { await helpers.addCaddyConfig(subdomain, caddyConfig); url = `https://${ctx.buildDomain(subdomain)}`; } catch (e) { - ctx.log.warn('recipe', `Caddy config failed for ${component.id}`, { error: e.message }); + log.warn('recipe', `Caddy config failed for ${component.id}`, { error: e.message }); } } @@ -360,12 +371,12 @@ module.exports = function(ctx) { for (const step of recipe.autoConnect.steps) { try { - ctx.log.info('recipe', `Auto-connect step: ${step.action}`, { targets: step.targets }); + log.info('recipe', `Auto-connect step: ${step.action}`, { targets: step.targets }); // These actions map to existing Smart Arr Connect functionality // The actual implementation will be wired when Smart Arr Connect helpers are available - ctx.log.info('recipe', `Auto-connect step ${step.action} completed`); + log.info('recipe', `Auto-connect step ${step.action} completed`); } catch (e) { - ctx.log.warn('recipe', `Auto-connect step failed: ${step.action}`, { error: e.message }); + log.warn('recipe', `Auto-connect step failed: ${step.action}`, { error: e.message }); } } } diff --git a/dashcaddy-api/routes/recipes/index.js b/dashcaddy-api/routes/recipes/index.js index 1c3f86d..5d83f0c 100644 --- a/dashcaddy-api/routes/recipes/index.js +++ b/dashcaddy-api/routes/recipes/index.js @@ -3,14 +3,28 @@ const deployRoutes = require('./deploy'); const manageRoutes = require('./manage'); const { NotFoundError } = require('../../errors'); +/** + * Recipes routes aggregator + * @param {Object} ctx - Application context (for backward compatibility) + * @returns {express.Router} + */ module.exports = function(ctx) { const router = express.Router(); + const deps = { + docker: ctx.docker, + credentialManager: ctx.credentialManager, + servicesStateManager: ctx.servicesStateManager, + asyncHandler: ctx.asyncHandler, + errorResponse: ctx.errorResponse, + log: ctx.log + }; + // All recipe routes require premium license router.use(ctx.licenseManager.requirePremium('recipes')); // GET /api/recipes/templates — list all recipe templates - router.get('/templates', ctx.asyncHandler(async (req, res) => { + router.get('/templates', deps.asyncHandler(async (req, res) => { const { RECIPE_TEMPLATES, RECIPE_CATEGORIES } = require('../../recipe-templates'); const templates = Object.entries(RECIPE_TEMPLATES).map(([id, recipe]) => ({ id, @@ -39,7 +53,7 @@ module.exports = function(ctx) { }, 'recipe-templates')); // GET /api/recipes/templates/:recipeId — get single recipe template detail - router.get('/templates/:recipeId', ctx.asyncHandler(async (req, res) => { + router.get('/templates/:recipeId', deps.asyncHandler(async (req, res) => { const { RECIPE_TEMPLATES } = require('../../recipe-templates'); const recipe = RECIPE_TEMPLATES[req.params.recipeId]; if (!recipe) throw new NotFoundError(`Recipe template ${req.params.recipeId}`); @@ -48,8 +62,8 @@ module.exports = function(ctx) { }, 'recipe-template-detail')); // Mount deploy and manage sub-routes - router.use(deployRoutes(ctx)); - router.use(manageRoutes(ctx)); + router.use(deployRoutes(deps)); + router.use(manageRoutes(deps)); return router; }; diff --git a/dashcaddy-api/routes/recipes/manage.js b/dashcaddy-api/routes/recipes/manage.js index 7520961..d946a22 100644 --- a/dashcaddy-api/routes/recipes/manage.js +++ b/dashcaddy-api/routes/recipes/manage.js @@ -2,14 +2,22 @@ const express = require('express'); const { DOCKER } = require('../../constants'); const { NotFoundError } = require('../../errors'); -module.exports = function(ctx) { +module.exports = function({ servicesStateManager, asyncHandler, log }) { const router = express.Router(); +/** + * Recipes management routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.servicesStateManager - Services state manager + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ /** * GET /api/recipes/deployed — list all deployed recipes (grouped by recipeId) */ - router.get('/deployed', ctx.asyncHandler(async (req, res) => { - const services = await ctx.servicesStateManager.read(); + router.get('/deployed', asyncHandler(async (req, res) => { + const services = await servicesStateManager.read(); const recipeGroups = {}; for (const service of services) { @@ -64,7 +72,7 @@ module.exports = function(ctx) { }); } } catch (e) { - ctx.log.warn('recipe', 'Could not list Docker containers for recipe discovery', { error: e.message }); + log.warn('recipe', 'Could not list Docker containers for recipe discovery', { error: e.message }); } // Enrich with container state @@ -92,7 +100,7 @@ module.exports = function(ctx) { /** * POST /api/recipes/:recipeId/start — start all containers in a recipe */ - router.post('/:recipeId/start', ctx.asyncHandler(async (req, res) => { + router.post('/:recipeId/start', asyncHandler(async (req, res) => { const { recipeId } = req.params; const containers = await findRecipeContainers(recipeId); @@ -116,14 +124,14 @@ module.exports = function(ctx) { } } - ctx.log.info('recipe', 'Recipe started', { recipeId, results }); + log.info('recipe', 'Recipe started', { recipeId, results }); res.json({ success: true, recipeId, results }); }, 'recipe-start')); /** * POST /api/recipes/:recipeId/stop — stop all containers in a recipe */ - router.post('/:recipeId/stop', ctx.asyncHandler(async (req, res) => { + router.post('/:recipeId/stop', asyncHandler(async (req, res) => { const { recipeId } = req.params; const containers = await findRecipeContainers(recipeId); @@ -148,14 +156,14 @@ module.exports = function(ctx) { } } - ctx.log.info('recipe', 'Recipe stopped', { recipeId, results }); + log.info('recipe', 'Recipe stopped', { recipeId, results }); res.json({ success: true, recipeId, results }); }, 'recipe-stop')); /** * POST /api/recipes/:recipeId/restart — restart all containers in a recipe */ - router.post('/:recipeId/restart', ctx.asyncHandler(async (req, res) => { + router.post('/:recipeId/restart', asyncHandler(async (req, res) => { const { recipeId } = req.params; const containers = await findRecipeContainers(recipeId); @@ -174,14 +182,14 @@ module.exports = function(ctx) { } } - ctx.log.info('recipe', 'Recipe restarted', { recipeId, results }); + log.info('recipe', 'Recipe restarted', { recipeId, results }); res.json({ success: true, recipeId, results }); }, 'recipe-restart')); /** * DELETE /api/recipes/:recipeId — remove entire recipe (containers, network, services) */ - router.delete('/:recipeId', ctx.asyncHandler(async (req, res) => { + router.delete('/:recipeId', asyncHandler(async (req, res) => { const { recipeId } = req.params; const containers = await findRecipeContainers(recipeId); @@ -189,7 +197,7 @@ module.exports = function(ctx) { throw new NotFoundError('Containers for recipe'); } - ctx.log.info('recipe', 'Removing recipe', { recipeId, containerCount: containers.length }); + log.info('recipe', 'Removing recipe', { recipeId, containerCount: containers.length }); const results = []; const networkNames = new Set(); @@ -213,7 +221,7 @@ module.exports = function(ctx) { try { await removeCaddyBlock(subdomain); } catch (e) { - ctx.log.warn('recipe', 'Failed to remove Caddy config', { subdomain, error: e.message }); + log.warn('recipe', 'Failed to remove Caddy config', { subdomain, error: e.message }); } } @@ -226,7 +234,7 @@ module.exports = function(ctx) { } // Remove recipe services from services.json - await ctx.servicesStateManager.update(services => { + await servicesStateManager.update(services => { return services.filter(s => s.recipeId !== recipeId); }); @@ -235,9 +243,9 @@ module.exports = function(ctx) { try { const network = ctx.docker.client.getNetwork(netName); await network.remove(); - ctx.log.info('recipe', 'Removed Docker network', { netName }); + log.info('recipe', 'Removed Docker network', { netName }); } catch (e) { - ctx.log.warn('recipe', 'Failed to remove network', { netName, error: e.message }); + log.warn('recipe', 'Failed to remove network', { netName, error: e.message }); } } @@ -246,7 +254,7 @@ module.exports = function(ctx) { 'info' ); - ctx.log.info('recipe', 'Recipe removed', { recipeId, results }); + log.info('recipe', 'Recipe removed', { recipeId, results }); res.json({ success: true, recipeId, results }); }, 'recipe-remove'));