Source

services/investigation/clone.js

import { Investigation } from "@models/investigation/investigation.model";
import { InvestigationStatus } from "@typez/investigation";
import { getLogger } from "@utils/asyncLocalStorage";
import { Types } from "mongoose";
import { getInvestigationById } from "./crud";
import { removeAiGeneratedValues } from "./versioning-helpers";
/**
 * Clones the current investigation by adding a "copy_N" postfix to its identifier.
 * @category Services
 * @param {string} investigationId - The ID of the current investigation to clone.
 * @param {string} [userId] - The ID of the user cloning the investigation (will be set as the new author).
 * @returns {Promise<IInvestigation>} - The cloned investigation object.
 * @throws {Error} - If an error occurs during the cloning process.
 */
export async function cloneInvestigation(investigationId, userId) {
    const logger = getLogger();
    try {
        logger.info({ investigationId }, "Cloning investigation");
        // Get the original investigation first
        const originalInvestigation = await getInvestigationById(investigationId);
        if (originalInvestigation.status === InvestigationStatus.IN_DEVELOPMENT) {
            throw new Error("Unable to clone investigation, when its status is InDevelopment.");
        }
        logger.debug({ originalInvestigationId: originalInvestigation._id }, "Original investigation found");
        // Create a deep copy of the investigation data with proper typing
        const investigationData = originalInvestigation.toObject();
        // Remove the _id and __v fields to create a new document
        delete investigationData._id;
        delete investigationData.__v;
        delete investigationData.createdAt;
        delete investigationData.updatedAt;
        // Remove all aiGeneratedValue fields recursively
        removeAiGeneratedValues(investigationData);
        // Handle title postfix logic
        const originalTitle = investigationData.title?.value || "";
        const copyRegex = / copy_(\d+)$/;
        const copyMatch = originalTitle.match(copyRegex);
        let newTitle;
        if (copyMatch && copyMatch[1]) {
            // If title already has copy postfix, increment the number
            const currentCopyNumber = parseInt(copyMatch[1], 10);
            const baseTitle = originalTitle.replace(copyRegex, "");
            newTitle = `${baseTitle} copy_${currentCopyNumber + 1}`;
        }
        else {
            // If no copy postfix, count existing clones and add copy_1 (or next number)
            const existingClonesCount = await Investigation.countDocuments({
                "metadata.sourceInvestigation": originalInvestigation._id,
            });
            newTitle = `${originalTitle} copy_${existingClonesCount + 1}`;
        }
        // Update the title
        investigationData.title.value = newTitle;
        // Update metadata for the clone - merge original metadata with specific updates
        investigationData.metadata.sourceInvestigation =
            originalInvestigation._id;
        investigationData.metadata.dateOfCreation = new Date();
        // Set the author to the user who cloned the investigation
        if (userId && Types.ObjectId.isValid(userId)) {
            investigationData.metadata.author = new Types.ObjectId(userId);
        }
        // Reset status to draft
        investigationData.status = "draft_incomplete";
        investigationData.isLocked = false;
        investigationData.lockedBy = null;
        investigationData.lockedAt = null;
        // Create new investigation document
        const clonedInvestigation = new Investigation(investigationData);
        const savedInvestigation = await clonedInvestigation.save();
        logger.info({
            originalInvestigationId: originalInvestigation._id,
            clonedInvestigationId: savedInvestigation._id,
        }, "Investigation cloned successfully");
        return savedInvestigation;
    }
    catch (error) {
        // Enhanced error logging for better debugging
        const errorMessage = error instanceof Error ? error.message : String(error);
        const errorStack = error instanceof Error ? error.stack : undefined;
        const errorName = error instanceof Error ? error.name : "UnknownError";
        logger.error({
            error: {
                message: errorMessage,
                name: errorName,
                stack: errorStack,
                originalError: error,
            },
            investigationId,
        }, "Failed to clone investigation");
        throw new Error(`Failed to clone investigation: ${errorMessage}`);
    }
}