From b0943f3fa0c855f6c539a84f85d1d45f4eebfbd8 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 22 Aug 2026 16:39:10 +0100 Subject: [PATCH] Enhance document template schema and service with referenced templates functionality This commit introduces a new field, `referencedTemplates`, to the document template schema, allowing for the association of other document templates. It also adds utility functions for extracting references from template content and converting filter values to ObjectIds. The document template service is updated to recalculate and populate referenced templates when editing a document template. Additionally, route handlers are modified to support the new referenced templates in their population logic, improving the management of document templates. --- .../management/documenttemplate.schema.js | 100 ++++++++++++++++++ src/routes/management/documenttemplates.js | 2 + src/services/management/documenttemplates.js | 3 + 3 files changed, 105 insertions(+) diff --git a/src/database/schemas/management/documenttemplate.schema.js b/src/database/schemas/management/documenttemplate.schema.js index 4b0c6b9..48e75ea 100644 --- a/src/database/schemas/management/documenttemplate.schema.js +++ b/src/database/schemas/management/documenttemplate.schema.js @@ -2,6 +2,53 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; +const RENDER_DOCUMENT_TEMPLATE_CALL = + /fc\.renderDocumentTemplate\s*\(\s*(['"])([^'"]+)\1/g; + +function extractRenderDocumentTemplateReferences(content) { + if (content == null || typeof content !== 'string' || content === '') { + return []; + } + + const references = []; + const seen = new Set(); + const regex = new RegExp(RENDER_DOCUMENT_TEMPLATE_CALL.source, 'g'); + let match; + while ((match = regex.exec(content)) !== null) { + const reference = match[2]?.trim(); + if (!reference || seen.has(reference)) { + continue; + } + seen.add(reference); + references.push(reference); + } + return references; +} + +function objectIdsFromFilterValue(value) { + if (value == null) { + return []; + } + if (value instanceof mongoose.Types.ObjectId) { + return [value]; + } + if (typeof value === 'string' && /^[a-f\d]{24}$/i.test(value)) { + return [new mongoose.Types.ObjectId(value)]; + } + if (Array.isArray(value)) { + return value.flatMap(objectIdsFromFilterValue); + } + if (typeof value === 'object') { + if (Array.isArray(value.$in)) { + return objectIdsFromFilterValue(value.$in); + } + if (value._id != null) { + return objectIdsFromFilterValue(value._id); + } + } + return []; +} + const documentTemplateSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, @@ -39,6 +86,13 @@ const documentTemplateSchema = new Schema( required: false, }, ], + referencedTemplates: [ + { + type: Schema.Types.ObjectId, + ref: 'documentTemplate', + required: false, + }, + ], content: { type: String, required: false, @@ -63,4 +117,50 @@ documentTemplateSchema.virtual('id').get(function () { // Configure JSON serialization to include virtuals documentTemplateSchema.set('toJSON', { virtuals: true }); +documentTemplateSchema.statics.recalculate = async function (documentTemplate, user) { + const documentTemplateId = documentTemplate?._id || documentTemplate; + if (!documentTemplateId) { + return; + } + + const stillExists = await this.exists({ _id: documentTemplateId }); + if (!stillExists) { + return; + } + + const { getFilter } = await import('../../../utils.js'); + const references = extractRenderDocumentTemplateReferences(documentTemplate?.content); + const referencedTemplateIds = []; + const seenIds = new Set(); + + for (const reference of references) { + const filter = await getFilter({ parent: reference }, ['parent'], true, this); + const ids = objectIdsFromFilterValue(filter.parent); + for (const id of ids) { + const idString = String(id); + if (!idString || seenIds.has(idString)) { + continue; + } + seenIds.add(idString); + referencedTemplateIds.push(id); + } + } + + if (documentTemplate && typeof documentTemplate === 'object' && !documentTemplate._bsontype) { + documentTemplate.referencedTemplates = referencedTemplateIds.map((id) => ({ + _id: String(id), + })); + } + + const { editObject } = await import('../../database.js'); + await editObject({ + model: this, + id: documentTemplateId, + updateData: { referencedTemplates: referencedTemplateIds }, + user, + populate: [{ path: 'referencedTemplates', strictPopulate: false }], + recalculate: false, + }); +}; + export const documentTemplateModel = mongoose.model('documentTemplate', documentTemplateSchema); diff --git a/src/routes/management/documenttemplates.js b/src/routes/management/documenttemplates.js index 5ed5626..ec128d7 100644 --- a/src/routes/management/documenttemplates.js +++ b/src/routes/management/documenttemplates.js @@ -14,6 +14,8 @@ const listAllowedFilters = [ 'parent._id', 'documentSize', 'documentSize._id', + 'referencedTemplates', + 'referencedTemplates._id', 'objectType', 'createdAt', 'updatedAt', diff --git a/src/services/management/documenttemplates.js b/src/services/management/documenttemplates.js index 2e37971..a680920 100644 --- a/src/services/management/documenttemplates.js +++ b/src/services/management/documenttemplates.js @@ -71,6 +71,7 @@ export const listDocumentTemplatesRouteHandler = async ( { path: 'documentSize' }, { path: 'parent' }, { path: 'documentPrinters', strictPopulate: false }, + { path: 'referencedTemplates', strictPopulate: false }, ], }); @@ -136,6 +137,7 @@ export const getDocumentTemplateRouteHandler = async (req, res) => { { path: 'documentSize' }, { path: 'parent', strictPopulate: false }, { path: 'documentPrinters', strictPopulate: false }, + { path: 'referencedTemplates', strictPopulate: false }, { path: 'testObject', strictPopulate: false }, ], }); @@ -186,6 +188,7 @@ export const editDocumentTemplateRouteHandler = async (req, res) => { { path: 'documentSize' }, { path: 'parent', strictPopulate: false }, { path: 'documentPrinters', strictPopulate: false }, + { path: 'referencedTemplates', strictPopulate: false }, { path: 'testObject', strictPopulate: false }, ], });