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}`);
}
}
Source