import { Router } from 'express'; import { getActiveTemplate, getAllActiveTemplates, getTemplateVersion, listTemplates, listTemplateVersions, publishTemplateVersion } from '../services/templateService.js'; import { logAuditEvent } from '../services/auditService.js'; import { asyncHandler } from '../utils/asyncHandler.js'; import { validateParam, validateNumericParam } from '../middleware/validateParams.js'; import { templateCache } from '../services/cacheService.js'; const router = Router(); /** * @openapi * /api/v1/templates: * get: * summary: List templates * tags: * - Templates * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: include * in: query * required: false * schema: * type: string * enum: [definitions] * responses: * 200: * description: List of templates * /api/v1/templates/{templateCode}: * get: * summary: Get active template * tags: * - Templates * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: templateCode * in: path * required: true * schema: * type: string * responses: * 200: * description: Active template details * 404: * description: Template not found * /api/v1/templates/{templateCode}/versions: * get: * summary: List template versions * tags: * - Templates * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: templateCode * in: path * required: true * schema: * type: string * responses: * 200: * description: List of template versions * /api/v1/templates/{templateCode}/versions/{versionNumber}: * get: * summary: Get specific template version * tags: * - Templates * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: templateCode * in: path * required: true * schema: * type: string * - name: versionNumber * in: path * required: true * schema: * type: integer * responses: * 200: * description: Template version details * 404: * description: Template version not found * /api/v1/templates/{templateCode}/versions/{versionNumber}/publish: * put: * summary: Publish template version * tags: * - Templates * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: templateCode * in: path * required: true * schema: * type: string * - name: versionNumber * in: path * required: true * schema: * type: integer * responses: * 200: * description: Template version published * 404: * description: Template version not found */ router.get( '/', asyncHandler(async (req, res) => { /* * When ?include=definitions is set the response embeds the full JSON * definition for every active template. This eliminates the N+1 round-trip * the old client performed (list → fetch each) and makes initial sync a * single request. Without the flag the response stays lightweight. */ const includeDefinitions = req.query.include === 'definitions'; const cacheKey = `templates-list-${includeDefinitions}`; const cached = templateCache.get(cacheKey); if (cached) { return res.json(cached); } if (includeDefinitions) { const templates = await getAllActiveTemplates(); const payload = { items: templates }; templateCache.set(cacheKey, payload); return res.json(payload); } const templates = await listTemplates(); const payload = { items: templates }; templateCache.set(cacheKey, payload); return res.json(payload); }) ); router.get( '/:templateCode', validateParam('templateCode'), asyncHandler(async (req, res) => { /* * New reports always use the latest active template, so the primary route is * optimized for that case. Older versions remain accessible through the * versioned route so existing drafts can stay bound to the original schema. */ const cacheKey = `template-active-${req.params.templateCode}`; const cached = templateCache.get(cacheKey); if (cached) { return res.json(cached); } const template = await getActiveTemplate(req.params.templateCode); if (!template) { return res.status(404).json({ message: 'Template not found.' }); } templateCache.set(cacheKey, template); return res.json(template); }) ); router.get( '/:templateCode/versions', validateParam('templateCode'), asyncHandler(async (req, res) => { /* * Version listing lets the admin workspace display a template's publication * history and choose which version to activate or review. */ const versions = await listTemplateVersions(req.params.templateCode); return res.json({ items: versions }); }) ); router.get( '/:templateCode/versions/:versionNumber', validateParam('templateCode'), validateNumericParam('versionNumber'), asyncHandler(async (req, res) => { /* * Version-specific access is what allows the frontend to reopen old drafts * safely even after templates evolve. Without this route, cached reports * would eventually drift away from the structure they were created against. */ const template = await getTemplateVersion( req.params.templateCode, req.params.versionNumber ); if (!template) { return res.status(404).json({ message: 'Template version not found.' }); } return res.json(template); }) ); router.put( '/:templateCode/versions/:versionNumber/publish', validateParam('templateCode'), validateNumericParam('versionNumber'), asyncHandler(async (req, res) => { /* * Publishing a version marks it active and retires the previously active * version for the same template. This lets the admin promote a draft version * to production. Existing reports keep their bound version unchanged. */ const result = await publishTemplateVersion( req.params.templateCode, Number(req.params.versionNumber) ); if (!result) { return res.status(404).json({ message: 'Template version not found.' }); } templateCache.clear(); await logAuditEvent({ entityType: 'template_version', entityCode: `${req.params.templateCode}::v${req.params.versionNumber}`, action: 'publish', newValue: { templateCode: req.params.templateCode, version: Number(req.params.versionNumber) } }); return res.json(result); }) ); export default router;