251 lines
6.8 KiB
JavaScript
251 lines
6.8 KiB
JavaScript
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;
|