/**
* DTO Converter Functions
*
* These functions convert database models to DTOs following best practices:
* - Separation of concerns: Data transformation logic is separate from business logic
* - Reusability: Can be used across different services and controllers
* - Maintainability: Single place to update conversion logic
* - Type safety: Ensures proper type conversion
*/
/**
* Convert Investigation database model to Response DTO
* @category Types
* @param {IEditableField<unknown>} field - The editable field to convert
* @param {unknown} defaultValue - The default value to use if the field is null or undefined
* @returns {IInvestigationResponseDto} - The formatted response DTO
*/
function fromEditableField(field, defaultValue) {
const history = field?.history
? field.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined;
return {
value: field?.value ?? defaultValue,
originalValue: field?.aiGeneratedValue ?? null,
isContradicting: field?.isContradicting ?? false,
targetFieldName: field?.targetFieldName ?? null,
contradictionReason: field?.contradictionReason ?? null,
history,
};
}
/**
* Convert Investigation database model to Response DTO
* @category Types
* @param {IInvestigation} investigation - The investigation document from the database
* @returns {IInvestigationResponseDto} - The formatted response DTO
*/
export function toInvestigationResponseDto(investigation) {
const authorId = investigation?.metadata?.author?.toString() || null;
const originalRequest = null;
const currentVersionIndex = investigation.currentVersionIndex ?? 0;
// If currentVersionIndex is -1, return empty fields (don't update DB, just send empty response)
const shouldReturnEmptyFields = currentVersionIndex === -1;
const steps = shouldReturnEmptyFields
? []
: investigation?.steps
? investigation.steps.map((step) => {
return {
title: fromEditableField(step.title, null),
descriptionEn: fromEditableField(step.descriptionEn, null),
desiredOutcome: fromEditableField(step.desiredOutcome, null),
alternativeOutcome: fromEditableField(step.alternativeOutcome, null),
skippable: fromEditableField(step.skippable, false),
skippable_after_marking: fromEditableField(step.skippable_after_marking, false),
};
})
: null;
const objects = shouldReturnEmptyFields
? []
: (investigation?.objects ?? []).map((object) => {
const posHistory = object.position
? {
x: object.position.x?.history
? object.position.x.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
y: object.position.y?.history
? object.position.y.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
z: object.position.z?.history
? object.position.z.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
}
: undefined;
const rotHistory = object.rotation
? {
x: object.rotation.x?.history
? object.rotation.x.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
y: object.rotation.y?.history
? object.rotation.y.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
z: object.rotation.z?.history
? object.rotation.z.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined,
}
: undefined;
const sizeHistory = object.size?.history
? object.size.history.slice(0, 2).map((entry) => ({
value: entry.value,
updatedBy: entry.updatedBy?.toString() || null,
updatedAt: entry.updatedAt,
}))
: undefined;
return {
name: fromEditableField(object.name, ""),
objectId: fromEditableField(object.objectId, ""),
position: object.position
? {
x: object.position.x?.value ?? 0,
y: object.position.y?.value ?? 0,
z: object.position.z?.value ?? 0,
}
: undefined,
rotation: object.rotation
? {
x: object.rotation.x?.value ?? 0,
y: object.rotation.y?.value ?? 0,
z: object.rotation.z?.value ?? 0,
}
: undefined,
size: object.size?.value ?? undefined,
isContradicting: object.name?.isContradicting ?? false,
targetFieldName: object.name?.targetFieldName ?? null,
contradictionReason: object.name?.contradictionReason ?? null,
positionHistory: posHistory,
rotationHistory: rotHistory,
sizeHistory,
};
});
// Helper function to create empty editable field
const createEmptyEditableField = (defaultValue) => ({
value: defaultValue,
originalValue: null,
isContradicting: false,
targetFieldName: null,
contradictionReason: null,
history: undefined,
});
return {
id: investigation._id.toString(),
status: investigation.status,
isLocked: investigation.isLocked,
lockedBy: investigation.lockedBy?.toString() || null,
lockedAt: investigation.lockedAt,
originalRequest,
title: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.title, null),
curriculum: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.curriculum, null),
unitNumberAndTitle: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.unitNumberAndTitle, null),
grade: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.grade, null),
lessonNumberAndTitle: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.lessonNumberAndTitle, null),
objectives: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.objectives, null),
ngss: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.ngss, null),
analyticalFacts: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.analyticalFacts, null),
goals: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.goals, null),
day: shouldReturnEmptyFields
? createEmptyEditableField(null)
: fromEditableField(investigation.day, null),
steps,
objects,
metadata: {
dateOfDevelopment: investigation?.metadata?.dateOfDevelopment ?? null,
dateOfDevelopmentDelivery: investigation?.metadata?.dateOfDevelopmentDelivery ?? null,
dateOfPublishing: investigation?.metadata?.dateOfPublishing ?? null,
author: {
id: authorId,
name: null,
email: null,
avatar: null,
},
editors: investigation?.metadata?.editors?.map((id) => id.toString()) || [],
views: investigation?.metadata?.views ?? null,
sourceInvestigation: investigation?.metadata?.sourceInvestigation?.toString() || null,
dateOfCreation: investigation?.metadata?.dateOfCreation ?? null,
dateModified: investigation?.metadata?.dateModified ?? null,
editorsDetailed: [],
},
createdAt: investigation.createdAt,
updatedAt: investigation.updatedAt,
currentVersionIndex: investigation.currentVersionIndex ?? 0,
lastChangeWithAI: investigation.lastChangeWithAI ?? false,
};
}
Source