import DocumentXmlCache from '#models/DocumentXmlCache'; import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js'; import Dataset from '#models/dataset'; import Strategy from './Strategy.js'; import { builder } from 'xmlbuilder2'; import logger from '@adonisjs/core/services/logger'; /** * Configuration for XML serialization * * @interface XmlSerializationConfig */ export interface XmlSerializationConfig { /** The dataset model to serialize */ model: Dataset; /** DOM representation (if available) */ dom?: XMLBuilder; /** Fields to exclude from serialization */ excludeFields: Array; /** Whether to exclude empty fields */ excludeEmpty: boolean; /** Base URI for xlink:ref elements */ baseUri: string; } /** * Options for controlling serialization behavior */ export interface SerializationOptions { /** Enable XML caching */ enableCaching?: boolean; /** Exclude empty fields from output */ excludeEmptyFields?: boolean; /** Custom base URI */ baseUri?: string; /** Fields to exclude */ excludeFields?: string[]; } /** * DatasetXmlSerializer * * Handles XML serialization of Dataset models with intelligent caching. * Generates XML representations and manages cache lifecycle to optimize performance. * * @example * ```typescript * const serializer = new DatasetXmlSerializer(dataset); * serializer.enableCaching(); * serializer.excludeEmptyFields(); * * const xmlDocument = await serializer.toXmlDocument(); * ``` */ export default class DatasetXmlSerializer { private readonly config: XmlSerializationConfig; private readonly strategy: Strategy; private cache: DocumentXmlCache | null = null; private cachingEnabled = false; constructor(dataset: Dataset, options: SerializationOptions = {}) { this.config = { model: dataset, excludeEmpty: options.excludeEmptyFields ?? false, baseUri: options.baseUri ?? '', excludeFields: options.excludeFields ?? [], }; this.strategy = new Strategy({ excludeEmpty: options.excludeEmptyFields ?? false, baseUri: options.baseUri ?? '', excludeFields: options.excludeFields ?? [], model: dataset, }); if (options.enableCaching) { this.cachingEnabled = true; } } /** * Enable caching for XML generation * When enabled, generated XML is stored in database for faster retrieval */ public enableCaching(): this { this.cachingEnabled = true; return this; } /** * Disable caching for XML generation */ public disableCaching(): this { this.cachingEnabled = false; return this; } set model(model: Dataset) { this.config.model = model; } /** * Configure to exclude empty fields from XML output */ public excludeEmptyFields(): this { this.config.excludeEmpty = true; return this; } /** * Set the cache instance directly (useful when preloading) * @param cache - The DocumentXmlCache instance */ public setCache(cache: DocumentXmlCache): this { this.cache = cache; return this; } /** * Get the current cache instance */ public getCache(): DocumentXmlCache | null { return this.cache; } /** * Get DOM document with intelligent caching * Returns cached version if valid, otherwise generates new document */ public async toXmlDocument(): Promise { const dataset = this.config.model; // Try to get from cache first let cachedDocument: XMLBuilder | null = await this.retrieveFromCache(); if (cachedDocument) { logger.debug(`Using cached XML for dataset ${dataset.id}`); return cachedDocument; } // Generate fresh document logger.debug(`[DatasetXmlSerializer] Cache miss - generating fresh XML for dataset ${dataset.id}`); const freshDocument = await this.strategy.createDomDocument(); if (!freshDocument) { logger.error(`[DatasetXmlSerializer] Failed to generate XML for dataset ${dataset.id}`); return null; } // Cache if caching is enabled if (this.cachingEnabled) { await this.persistToCache(freshDocument, dataset); } // Extract the dataset-specific node return this.extractDatasetNode(freshDocument); } /** * Generate XML string representation * Convenience method that converts XMLBuilder to string */ public async toXmlString(): Promise { const document = await this.toXmlDocument(); return document ? document.end({ prettyPrint: false }) : null; } /** * Persist generated XML document to cache * Non-blocking - failures are logged but don't interrupt the flow */ private async persistToCache(domDocument: XMLBuilder, dataset: Dataset): Promise { try { this.cache = this.cache || new DocumentXmlCache(); this.cache.document_id = dataset.id; this.cache.xml_version = 1; this.cache.server_date_modified = dataset.server_date_modified.toFormat('yyyy-MM-dd HH:mm:ss'); this.cache.xml_data = domDocument.end(); await this.cache.save(); logger.debug(`Cached XML for dataset ${dataset.id}`); } catch (error) { logger.error(`Failed to cache XML for dataset ${dataset.id}: ${error.message}`); // Don't throw - caching failure shouldn't break the flow } } /** * Extract the Rdr_Dataset node from full document */ private extractDatasetNode(domDocument: XMLBuilder): XMLBuilder | null { const node = domDocument.find((n) => n.node.nodeName === 'Rdr_Dataset', false, true)?.node; if (node) { return builder({ version: '1.0', encoding: 'UTF-8', standalone: true }, node); } return domDocument; } /** * Attempt to retrieve valid cached XML document * Returns null if cache doesn't exist or is stale */ private async retrieveFromCache(): Promise { const dataset: Dataset = this.config.model; if (!this.cache) { return null; } // Check if cache is still valid const actuallyCached = await DocumentXmlCache.hasValidEntry(dataset.id, dataset.server_date_modified); if (!actuallyCached) { logger.debug(`Cache invalid for dataset ${dataset.id}`); return null; } //cache is actual return cached document try { if (this.cache) { return this.cache.getDomDocument(); } else { return null; } } catch (error) { logger.error(`Failed to retrieve cached document for dataset ${dataset.id}: ${error.message}`); return null; } } }