Source

types/investigation/dto.js

/**
 * 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,
    };
}