import config from '../../config.js'; import { documentTemplateModel } from '../../database/schemas/management/documenttemplate.schema.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { deleteObject, listObjects, getObject, editObject, newObject, listObjectsByProperties, getModelStats, getModelHistory, searchObjects, getPropertyValues, getObjectNeighbors, } from '../../database/database.js'; import { randomUUID } from 'crypto'; import { redisServer } from '../../database/redis.js'; import { templateManager, DOCUMENT_TEMPLATE_RENDER_KEY_PREFIX, DOCUMENT_TEMPLATE_RENDER_TTL_SECONDS, } from '../../templates/templatemanager.js'; import { formatTemplateContent } from '../../templates/templateformatter.js'; const logger = log4js.getLogger('Document Templates'); logger.level = config.server.logLevel; function parseOptionalBoolean(value) { if (value === undefined || value === null || value === '') { return undefined; } if (value === true || value === false) { return value; } const lower = String(value).trim().toLowerCase(); if (lower === 'true' || lower === '1') { return true; } if (lower === 'false' || lower === '0') { return false; } return undefined; } function getPaddingOption(req, defaultValue = true) { return parseOptionalBoolean(req.body?.padding ?? req.query?.padding) ?? defaultValue; } export const listDocumentTemplatesRouteHandler = async ( req, res, page = 1, limit = 25, property = '', filter = {}, search = '', sort = '', order = 'ascend' ) => { const result = await listObjects({ model: documentTemplateModel, page, limit, property, filter, search, sort, order, populate: [ { path: 'documentSize' }, { path: 'parent' }, { path: 'documentPrinters', strictPopulate: false }, ], }); if (result?.error) { logger.error('Error listing document templates.'); res.status(result.code).send(result); return; } logger.debug( `List of document templates (Page ${page}, Limit ${limit}). Count: ${result.length}` ); res.send(result); }; export const listDocumentTemplatesByPropertiesRouteHandler = async ( req, res, properties = '', filter = {}, masterFilter = {} ) => { const result = await listObjectsByProperties({ model: documentTemplateModel, properties, filter, populate: ['documentSize'], masterFilter, }); if (result?.error) { logger.error('Error listing document templates.'); res.status(result.code).send(result); return; } logger.debug(`List of document templates. Count: ${result.length}`); res.send(result); }; export const getDocumentTemplatePropertyValuesRouteHandler = async (req, res, property) => { const result = await getPropertyValues({ model: documentTemplateModel, property, }); res.send(result); }; export const searchDocumentTemplatesRouteHandler = async (req, res, search) => { const result = await searchObjects({ model: documentTemplateModel, search, }); res.send(result); }; export const getDocumentTemplateRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: documentTemplateModel, id, populate: [ { path: 'documentSize' }, { path: 'parent', strictPopulate: false }, { path: 'documentPrinters', strictPopulate: false }, { path: 'testObject', strictPopulate: false }, ], }); if (result?.error) { logger.warn(`Document Template not found with supplied id.`); return res.status(result.code).send(result); } logger.debug(`Retreived document template with ID: ${id}`); res.send(result); }; export const editDocumentTemplateRouteHandler = async (req, res) => { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Document Template with ID: ${id}`); let formattedContent = req.body.content; if (req.body.content != null && typeof req.body.content === 'string') { const formatResult = formatTemplateContent(req.body.content); if (formatResult?.error) { logger.warn('Error formatting document template content:', formatResult.error); return res.status(formatResult.code || 400).send(formatResult); } formattedContent = formatResult.content; } const updateData = { updatedAt: new Date(), name: req.body.name, tags: req.body.tags, active: req.body.active, global: req.body.global, parent: req.body.parent, objectType: req.body.objectType, documentSize: req.body.documentSize, documentPrinters: req.body.documentPrinters, content: formattedContent, testObject: req.body.testObject?._id || req.body.testObject, }; // Create audit log before updating const result = await editObject({ model: documentTemplateModel, id, updateData, user: req.user, populate: [ { path: 'documentSize' }, { path: 'parent', strictPopulate: false }, { path: 'documentPrinters', strictPopulate: false }, { path: 'testObject', strictPopulate: false }, ], }); if (result.error) { logger.error('Error editing document template:', result.error); res.status(result).send(result); return; } logger.debug(`Edited document template with ID: ${id}`); res.send(result); }; export const newDocumentTemplateRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), name: req.body.name, tags: req.body.tags, active: req.body.active, global: req.body.global, parent: req.body.parent, objectType: req.body.objectType, documentSize: req.body.documentSize, documentPrinters: req.body.documentPrinters, content: req.body.content, }; const result = await newObject({ model: documentTemplateModel, newData, user: req.user, }); if (result.error) { logger.error('No document template created:', result.error); return res.status(result.code).send(result); } logger.debug(`New document template with ID: ${result._id}`); res.send(result); }; export const deleteDocumentTemplateRouteHandler = async (req, res) => { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Document Template with ID: ${id}`); const result = await deleteObject({ model: documentTemplateModel, id, user: req.user, }); if (result.error) { logger.error('No document template deleted:', result.error); return res.status(result.code).send(result); } logger.debug(`Deleted document template with ID: ${result._id}`); res.send(result); }; export const getDocumentTemplateStatsRouteHandler = async (req, res) => { const result = await getModelStats({ model: documentTemplateModel }); if (result?.error) { logger.error('Error fetching document template stats:', result.error); return res.status(result.code).send(result); } logger.trace('Document template stats:', result); res.send(result); }; export const getDocumentTemplateHistoryRouteHandler = async (req, res) => { const from = req.query.from; const to = req.query.to; const result = await getModelHistory({ model: documentTemplateModel, from, to }); if (result?.error) { logger.error('Error fetching document template history:', result.error); return res.status(result.code).send(result); } logger.trace('Document template history:', result); res.send(result); }; export const getDocumentTemplateNeighborsRouteHandler = async ( req, res, property = '', filter = {}, search = '', sort = '', order = 'ascend', id ) => { if (!id) { return res.status(400).send({ error: 'Missing id parameter', code: 400 }); } const result = await getObjectNeighbors({ model: documentTemplateModel, id, filter, search, sort, order, }); if (result?.error) { logger.error('Error fetching documentTemplate neighbors.'); return res.status(result.code).send(result); } logger.debug(`Retrieved documentTemplate neighbors for ID: ${id}`); res.send(result); }; export const formatDocumentTemplateRouteHandler = async (req, res) => { const content = req.body?.content; logger.debug('Formatting document template content'); const result = formatTemplateContent(content); if (result?.error) { logger.warn('Error formatting document template content:', result.error); return res.status(result.code || 400).send(result); } res.send({ content: result.content }); }; export const previewDocumentTemplateRouteHandler = async (req, res) => { const id = req.params.id; const content = req.body?.content; const testObject = req.body?.testObject || req.body?.object || {}; const scale = req.body?.scale ?? 1; const padding = getPaddingOption(req, true); logger.debug(`Previewing document template with ID: ${id}`); const result = await templateManager.renderTemplate( id, content, testObject, scale, { padding }, true ); if (result?.error) { logger.warn('Error previewing document template:', result.error); return res.status(result.code || 400).send(result); } res.send(result); }; function sendRenderedDownload(res, result, filename) { const buffers = result.buffers || []; if (buffers.length === 0) { return res.status(500).send({ error: 'Failed to render document template.' }); } if (buffers.length === 1) { const buffer = buffers[0]; res.set('Content-Type', result.mime); res.set( 'Content-Disposition', `attachment; filename="${filename}.${result.extension}"` ); res.set('Content-Length', String(buffer.length)); return res.send(buffer); } return res.send({ type: result.type, mime: result.mime, extension: result.extension, images: buffers.map((buffer) => Buffer.from(buffer).toString('base64')), }); } function decodeStoredRenderBuffers(stored) { return { ...stored, buffers: (stored.buffers || []).map((buffer) => Buffer.from(buffer, 'base64')), }; } export const downloadDocumentTemplateRouteHandler = async (req, res) => { const id = req.params.id; const type = req.query.type || req.body?.type || 'pdf'; const content = req.body?.content; const data = req.body?.object || req.body?.testObject || {}; const width = req.body?.width || req.query.width; const filename = req.body?.filename || req.query.filename || 'document'; const asyncRequested = parseOptionalBoolean(req.body?.async ?? req.query?.async) === true; const renderRequestId = req.body?.renderRequestId || req.query?.renderRequestId; const options = { padding: getPaddingOption(req, true), }; if (width) { options.width = Number(width); } if (renderRequestId) { logger.debug(`Fetching stored document template render ${renderRequestId}`); const stored = await redisServer.getKey( `${DOCUMENT_TEMPLATE_RENDER_KEY_PREFIX}${renderRequestId}` ); if (stored == null) { return res.status(404).send({ error: 'Render not found or expired.', code: 404 }); } if (stored.error) { return res.status(stored.code || 400).send(stored); } return sendRenderedDownload(res, decodeStoredRenderBuffers(stored), filename); } if (asyncRequested) { const newRenderRequestId = randomUUID(); logger.debug( `Starting async document template download ${id} as ${type} (${newRenderRequestId})` ); res.send({ renderRequestId: newRenderRequestId }); setImmediate(() => { templateManager .renderDownload(id, content, data, type, { ...options, renderRequestId: newRenderRequestId, }) .catch(async (error) => { logger.error('Error rendering document template asynchronously:', error); await redisServer.setKey( `${DOCUMENT_TEMPLATE_RENDER_KEY_PREFIX}${newRenderRequestId}`, { error: error?.message || 'Failed to render document template.', code: 500, }, DOCUMENT_TEMPLATE_RENDER_TTL_SECONDS ); }); }); return; } logger.debug(`Downloading document template ${id} as ${type}`); const result = await templateManager.renderDownload(id, content, data, type, options); if (result?.error) { logger.warn('Error downloading document template:', result.error); return res.status(result.code || 400).send(result); } return sendRenderedDownload(res, result, filename); };