Source

schemas/investigation.validation.js

import mongoose from "mongoose";
import { z } from "zod";
/**
 * Zod validation schemas for investigation endpoints
 */
// Vector3 validation schema
/**
 * Vector3 validation schema
 * @category Schemas
 * @returns {z.ZodObject<{ x: z.ZodNumber, y: z.ZodNumber, z: z.ZodNumber }>} The Vector3 schema
 */
const Vector3Schema = z.object({
    x: z.number(),
    y: z.number(),
    z: z.number(),
});
// Interaction validation schema
const InteractionSchema = z.object({
    name: z.string().min(1).max(100),
});
// ViewComponent validation schema
const ViewComponentSchema = z.object({
    mesh: z.string().min(1),
    material: z.string().min(1),
    texture: z.string().min(1),
    interactions: z.array(InteractionSchema),
});
// RigidBodyComponent validation schema
const RigidBodyComponentSchema = z.object({
    mass: z.number().min(0),
    velocity: Vector3Schema,
    angularVelocity: Vector3Schema,
    drag: z.number().min(0),
    angularDrag: z.number().min(0),
    useGravity: z.boolean(),
    isKinematic: z.boolean(),
    interactions: z.array(InteractionSchema),
});
// FluidRigidBodyComponent validation schema
const FluidRigidBodyComponentSchema = z.object({
    density: z.number().min(0),
    viscosity: z.number().min(0),
    surfaceTension: z.number().min(0),
    particleSize: z.number().min(0),
    simulationMethod: z.enum(["SPH", "Grid"]),
    maxParticles: z.number().min(1),
    interactions: z.array(InteractionSchema),
});
// CollisionMeshComponent validation schema
const CollisionMeshComponentSchema = z.object({
    type: z.enum(["Box", "Sphere", "Capsule", "Mesh", "Convex"]),
    size: Vector3Schema,
    isTrigger: z.boolean(),
    material: z.string().min(1),
    interactions: z.array(InteractionSchema),
});
// TemperatureComponent validation schema
const TemperatureComponentSchema = z.object({
    value: z.number(),
    unit: z.enum(["C", "F", "K"]),
    conductivity: z.number().min(0),
    specificHeat: z.number().min(0),
    interactions: z.array(InteractionSchema),
});
// StateOfMatterComponent validation schema
const StateOfMatterComponentSchema = z.object({
    currentState: z.enum(["Solid", "Liquid", "Gas", "Plasma"]),
    meltingPoint: z.number(),
    boilingPoint: z.number(),
    sublimationPoint: z.number().optional(),
    stateChangeObjects: z.object({
        solid: z.string().optional(),
        liquid: z.string().optional(),
        gas: z.string().optional(),
        plasma: z.string().optional(),
    }),
    interactions: z.array(InteractionSchema),
});
// ShatterComponent validation schema
const ShatterComponentSchema = z.object({
    health: z.number().min(0),
    maxHealth: z.number().min(0),
    shatterThreshold: z.number().min(0),
    shatterEffect: z.enum(["Visual", "Vaporize", "Fragment"]),
    fragmentCount: z.number().min(0).optional(),
    fragmentObject: z.string().optional(),
    interactions: z.array(InteractionSchema),
});
// HardAttachComponent validation schema
const HardAttachComponentSchema = z.object({
    attachedObjects: z.array(z.string()),
    attachmentStrength: z.number().min(0),
    canDetach: z.boolean(),
    detachForce: z.number().min(0),
    interactions: z.array(InteractionSchema),
});
// FillComponent validation schema
const FillComponentSchema = z.object({
    maxVolume: z.number().min(0),
    currentVolume: z.number().min(0),
    currentLiquid: z.string().optional(),
    fillRate: z.number().min(0),
    drainRate: z.number().min(0),
    hasOverflow: z.boolean(),
    interactions: z.array(InteractionSchema),
});
// Object validation schema
export const ObjectSchema = z.object({
    id: z.string().min(1),
    name: z.string().min(1).max(200),
    position: Vector3Schema,
    rotation: Vector3Schema,
    size: z.number().min(0),
    view: ViewComponentSchema,
    rigidBody: RigidBodyComponentSchema.optional(),
    fluidRigidBody: FluidRigidBodyComponentSchema.optional(),
    collisionMesh: CollisionMeshComponentSchema.optional(),
    temperature: TemperatureComponentSchema.optional(),
    stateOfMatter: StateOfMatterComponentSchema.optional(),
    shatter: ShatterComponentSchema.optional(),
    hardAttach: HardAttachComponentSchema.optional(),
    fill: FillComponentSchema.optional(),
});
// Editable field payload schema - supports both simple value and {value, aiGeneratedValue} format
const EditableFieldStringSchema = z.union([
    z.string(),
    z.object({
        value: z.string(),
        aiGeneratedValue: z.string().nullable().optional(),
    }),
]);
const EditableFieldBooleanSchema = z.union([
    z.boolean(),
    z.object({
        value: z.boolean(),
        aiGeneratedValue: z.boolean().nullable().optional(),
    }),
]);
// Simple Step validation schema (for API input)
// Supports both simple values and {value, aiGeneratedValue} format
export const SimpleStepSchema = z.object({
    title: EditableFieldStringSchema.nullable().optional(),
    desiredOutcome: EditableFieldStringSchema.nullable().optional(),
    alternativeOutcome: EditableFieldStringSchema.nullable().optional(),
    name: z.string().max(200).optional(), // Legacy field, kept for backward compatibility
    descriptionEn: EditableFieldStringSchema.nullable().optional(),
    skippable: EditableFieldBooleanSchema.nullable().optional(),
    skippable_after_marking: EditableFieldBooleanSchema.nullable().optional(),
});
// Step validation schema (for database storage with EditableField)
// export const StepSchema = z.object({
//   title: z.string().min(1).optional(),
//   desiredOutcome: z.string().min(1).optional(),
//   alternativeOutcome: z.string().min(1).optional(),
//   name: z.string().min(1).max(200).optional(),
//   descriptionEn: z.string().min(1).max(2000).optional(),
//   skippable: EditableFieldSchema.optional(),
//   skippable_after_marking: EditableFieldSchema.optional(),
// });
const createVectorSchema = z.object({
    x: z.number().optional(),
    y: z.number().optional(),
    z: z.number().optional(),
});
const CreateObjectSchema = z.object({
    name: z.string().min(0),
    objectId: z.string(),
    position: createVectorSchema.optional(),
    rotation: createVectorSchema.optional(),
    size: z.number().optional(),
});
// Update Object schema - supports {value, aiGeneratedValue} format
const UpdateObjectFieldSchema = z
    .union([
    z.string(),
    z.object({
        value: z.string(),
        aiGeneratedValue: z.string().nullable().optional(),
    }),
])
    .nullable()
    .optional();
const UpdateObjectSchema = z.object({
    name: UpdateObjectFieldSchema,
    objectId: UpdateObjectFieldSchema,
    position: createVectorSchema.optional(),
    rotation: createVectorSchema.optional(),
    size: z.number().optional(),
});
// API Input validation schema (simple values for creating investigation)
export const CreateInvestigationSchema = z.object({
    // Basic fields (all optional as requested)
    curriculum: z.string().optional(),
    grade: z.string().optional(),
    title: z.string().optional(),
    unitNumberAndTitle: z.string().optional(),
    lessonNumberAndTitle: z.string().optional(),
    ngss: z.string().optional(),
    day: z.string().optional(),
    objectives: z.string().optional(),
    // Additional content fields
    analyticalFacts: z.string().optional(),
    goals: z.string().optional(),
    // Complex nested fields
    steps: z.array(SimpleStepSchema).optional(),
    objects: z.array(CreateObjectSchema).optional(),
});
// Query parameters validation schema for GET investigations
export const GetInvestigationsQuerySchema = z.object({
    limit: z.coerce.number().int().min(1).max(100).optional(),
    offset: z.coerce.number().int().min(0).optional(),
    sortBy: z.enum(["createdAt", "updatedAt", "title", "views"]).optional(),
    sortOrder: z.enum(["asc", "desc"]).optional(),
    search: z.string().min(1).optional(),
});
const ObjectIdParamSchema = z.string().refine((val) => mongoose.Types.ObjectId.isValid(val), {
    message: "Invalid MongoDB ObjectId format",
});
// Query parameters validation schema for GET single investigation
export const GetInvestigationParamsSchema = z.object({
    id: ObjectIdParamSchema,
});
// Params validation schema for DELETE single investigation
export const DeleteInvestigationParamsSchema = z.object({
    id: ObjectIdParamSchema,
});
// Update investigation params validation schema with MongoDB ObjectId validation
export const UpdateInvestigationParamsSchema = z.object({
    id: z
        .string()
        .min(1)
        .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId format"),
});
// Step update validation schema for individual step field updates
// All fields are required when steps array is provided
export const StepUpdateSchema = z.object({
    index: z.number().int().min(0),
    key: z.string().min(1),
    value: z.union([z.string(), z.number(), z.boolean(), z.null()]), // Properly typed union
});
// Object update validation schema
export const ObjectUpdateSchema = {
    isContradicting: z.boolean(),
    targetFieldName: z.string().nullable(),
    contradictionReason: z.string().nullable(),
    value: z.array(z.object({
        name: z.string(),
        position: Vector3Schema,
        rotation: Vector3Schema,
        size: z.number(),
        view: z.object({
            mesh: z.string(),
            material: z.string(),
            texture: z.string(),
        }),
    })),
};
// New update investigation validation schema for the new API format
export const UpdateInvestigationSchema = z.object({
    // String fields (curriculum, gradeAndUnit, title, correspondence, objectives, discussionTopics, analyticalFacts, introAndGoals, references)
    curriculum: z.string().optional(),
    title: z.string().optional(),
    objectives: z.string().optional(),
    analyticalFacts: z.string().optional(),
    goals: z.string().optional(),
    grade: z.string().optional(),
    unitNumberAndTitle: z.string().optional(),
    lessonNumberAndTitle: z.string().optional(),
    ngss: z.string().optional(),
    day: z.string().optional(),
    // Step updates (array of step objects where each field can be simple value or {value, aiGeneratedValue})
    steps: z.array(SimpleStepSchema).optional(),
    objects: z.array(UpdateObjectSchema).optional(),
});
// Get field history params validation schema
export const GetFieldHistoryParamsSchema = z.object({
    id: ObjectIdParamSchema,
    fieldPath: z.string().min(1),
});
// Get field history query validation schema
export const GetFieldHistoryQuerySchema = z.object({
    limit: z.coerce.number().int().min(1).max(50).optional(),
});
export const GetInvestigationVersionsParamsSchema = z.object({
    id: ObjectIdParamSchema,
});
export const GetInvestigationVersionsQuerySchema = z.object({
    limit: z.coerce.number().int().min(1).max(50).optional(),
});
// Regenerate other fields params validation schema
export const RegenerateOtherFieldsParamsSchema = z.object({
    id: ObjectIdParamSchema,
});
// Regenerate other fields body validation schema
export const RegenerateOtherFieldsBodySchema = z.object({
    fieldName: z.union([
        // Block fields
        z.enum([
            "title",
            "curriculum",
            "unitNumberAndTitle",
            "grade",
            "lessonNumberAndTitle",
            "objectives",
            "ngss",
            "analyticalFacts",
            "goals",
            "day",
        ]),
        // Step fields (format: steps.{index}.{field})
        z.string().regex(/^steps\.\d+\.(title|descriptionEn|desiredOutcome|alternativeOutcome)$/),
        // Object fields (format: objects.{index}.name)
        z.union([z.literal("name"), z.string().regex(/^objects\.\d+\.name$/)]),
    ]),
});
// Resolve contradiction params validation schema
export const ResolveContradictionParamsSchema = z.object({
    id: ObjectIdParamSchema,
});
// Resolve contradiction body validation schema
export const ResolveContradictionBodySchema = z.object({
    fieldName: z.union([
        // Block fields
        z.enum([
            "title",
            "curriculum",
            "unitNumberAndTitle",
            "grade",
            "lessonNumberAndTitle",
            "objectives",
            "ngss",
            "analyticalFacts",
            "goals",
            "day",
        ]),
        // Step fields (format: steps.{index}.{field})
        z.string().regex(/^steps\.\d+\.(title|descriptionEn|desiredOutcome|alternativeOutcome)$/),
        // Object fields (format: objects.{index}.name)
        z.union([z.literal("name"), z.string().regex(/^objects\.\d+\.name$/)]),
    ]),
});