Sesión: Implementación de propiedad renews en getPaginatedStudents¶
Fecha: 2025-10-02
Objetivo: Agregar una propiedad renews que indique si un estudiante tiene matrículas en el curso escolar actual.
Problema Inicial¶
El endpoint getPaginatedStudents devolvía estudiantes pero no indicaba si tenían matrículas (registers) en el curso escolar actual. Se necesitaba:
- Filtrar
registerspor el curso escolar actual (último curso) - Agregar una propiedad
renews: booleanque indique: truesi el estudiante tiene registros en el curso actualfalsesi el arrayregistersestá vacío
Patrón Establecido en el Proyecto¶
Después de analizar múltiples casos de uso, se identificó el patrón del proyecto:
✅ Obtener el último curso escolar en el CASO DE USO, NO en el repositorio
Ejemplos del patrón existente:¶
GetScheduleEmployeeByIdUseCaseGetRegistersByStudetUseCaseGetAttendanceRecordsByStudentIdUseCaseGetDashboardStudentUseCase
// Patrón encontrado en múltiples casos de uso
if(!schoolCourseId){
const schoolCourse = await this.schoolCourseRepository.getLastCourse();
schoolCourseId = schoolCourse?.getId();
}
Cambios Implementados¶
1. GetPaginatedStudentsUseCase.ts¶
Ubicación: src/application/use-cases/student/GetPaginatedStudentsUseCase.ts
Cambios:
- Agregado DomainSchoolCourseRepository en el constructor
- Obtener el último curso escolar si no se proporciona
- Actualizada interfaz PaginatedStudentsResult para incluir studentsRaw?: any[]
interface PaginatedStudentsResult {
students: Student[];
studentsRaw?: any[]; // ← NUEVO
totalItems: number;
currentPage: number;
totalPages: number;
pageSize: number;
}
export class GetPaginatedStudentsUseCase {
constructor(
private readonly repository: DomainStudentRepository,
private readonly schoolCourseRepository: DomainSchoolCourseRepository // ← NUEVO
) {}
async execute({ page = 1, limit = 20, search, statuses, subjectId, schoolCourseId }: GetPaginatedStudentsDTO): Promise<PaginatedStudentsResult> {
const sanitizedPage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
const sanitizedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 20;
const lastCourse = await this.schoolCourseRepository.getLastCourse(); // ← NUEVO
const { students, studentsRaw, totalItems } = await this.repository.getPaginatedStudents(
sanitizedPage,
sanitizedLimit,
search,
statuses,
subjectId,
lastCourse.getId() // ← NUEVO: Pasamos el ID del último curso
);
const totalPages = Math.ceil(totalItems / sanitizedLimit) || 1;
return {
students,
studentsRaw, // ← NUEVO
totalItems,
currentPage: sanitizedPage,
totalPages,
pageSize: sanitizedLimit,
};
}
}
2. app.ts - Inyección de dependencia¶
Ubicación: src/app.ts
Cambio:
// ANTES
const getPaginatedStudentsUseCase = new GetPaginatedStudentsUseCase(studentRepository);
// DESPUÉS
const getPaginatedStudentsUseCase = new GetPaginatedStudentsUseCase(studentRepository, schoolCourseRepository);
3. StudentRepositoryImpl.ts¶
Ubicación: src/infrastructure/adapters/persistence/StudentRepositoryImpl.ts
Cambios:
- Agregado filtro where en el include de StudentRegistration para filtrar por schoolCourseId
- Agregado required: false para hacer LEFT JOIN (incluye estudiantes sin registros)
- Agregado studentsRaw al return para que el controlador pueda calcular renews
- Actualizada firma del método para incluir studentsRaw en el tipo de retorno
async getPaginatedStudents(
page: number,
limit: number,
search?: string,
statuses?: string[],
subjectId?: number,
schoolCourseId?: number
): Promise<{ students: Student[]; studentsRaw?: any[]; totalItems: number }> { // ← ACTUALIZADO
// ... código de filtros ...
const { rows: students, count: totalItems } = await StudentModel.findAndCountAll({
where: whereClause,
include: [
{ model: UserModel },
{
model: GuardianModel,
include: [{ model: UserModel }]
},
{
where: {
schoolCoursesId: schoolCourseId // ← NUEVO: Filtro por curso
},
model: StudentRegistration,
as: 'registers',
required: false // ← NUEVO: LEFT JOIN para incluir estudiantes sin registros
}
],
limit: sanitizedLimit,
offset: offset,
order: [["createdAt", "DESC"]],
distinct: true
});
return {
students: students.map((s) => StudentMapperInfra.toDomain(s)),
studentsRaw: students, // ← NUEVO: Datos raw con registers
totalItems
};
}
4. DomainStudentRepository.ts¶
Ubicación: src/domain/repositories/DomainStudentRepository.ts
Cambio: Actualizada la interfaz para incluir studentsRaw opcional
getPaginatedStudents(
page: number,
limit: number,
search?: string,
statuses?: string[],
subjectId?: number,
schoolCourseId?: number
): Promise<{ students: Student[]; studentsRaw?: any[]; totalItems: number }>; // ← studentsRaw agregado
5. student.controller.ts¶
Ubicación: src/infrastructure/controllers/student.controller.ts
Cambio: Calcular la propiedad renews en el controlador basándose en studentsRaw
getAdministrativeStudents = catchAsync(async (req: Request, res: Response) => {
// ... código de parseo de parámetros ...
const response = await this.getPaginatedStudentsUseCase.execute({
page,
limit,
search,
statuses,
subjectId,
schoolCourseId,
});
// ← NUEVO: Calcular renews basado en si tiene registros en el curso actual
const dataWithRenews = response.students.map((student, index) => {
const studentRaw = response.studentsRaw?.[index];
const renews = studentRaw?.registers && studentRaw.registers.length > 0;
return {
...student.toJSON(),
renews // ← NUEVO: Propiedad agregada
};
});
res.status(200).json({
data: dataWithRenews, // ← ACTUALIZADO
meta: {
totalItems: response.totalItems,
totalPages: response.totalPages,
currentPage: response.currentPage,
pageSize: response.pageSize,
},
});
});
Resultado Final¶
Respuesta del endpoint antes:¶
{
"data": [
{
"id": 6,
"name": "Teresa",
"lastname": "Merino Bobillo",
"status": "active"
// ... otros campos
}
]
}
Respuesta del endpoint después:¶
{
"data": [
{
"id": 795,
"name": "Teresa",
"lastname": "Barazal Díaz",
"status": "restricted",
"renews": false // ← NUEVO: No tiene registros en el curso actual
},
{
"id": 6,
"name": "Teresa",
"lastname": "Merino Bobillo",
"status": "active",
"renews": true // ← NUEVO: Tiene registros en el curso actual
}
]
}
Decisiones de Diseño¶
✅ Por qué agregamos studentsRaw en lugar de modificar el mapper¶
Problema: El mapper StudentMapperInfra.toDomain() no incluye los registers porque mapea a la entidad de dominio Student, que no tiene esa propiedad.
Opciones consideradas:
- ❌ Modificar la entidad de dominio
Student- No apropiado, ya queregisterses contexto específico de una consulta - ❌ Modificar el mapper para incluir
registers- Afectaría otros casos de uso que no necesitan esta info - ✅ Agregar
studentsRawopcional al return - Solución óptima porque: - No rompe compatibilidad (propiedad opcional)
- No afecta otros casos de uso
- Permite al controlador calcular propiedades contextuales
✅ Por qué calculamos renews en el controlador¶
La propiedad renews es específica del contexto del endpoint administrativo. Otros casos de uso que llamen a getPaginatedStudents pueden no necesitar esta propiedad, por lo que es mejor calcularla en el controlador específico.
Archivos Modificados¶
- ✅
src/application/use-cases/student/GetPaginatedStudentsUseCase.ts - ✅
src/app.ts - ✅
src/infrastructure/adapters/persistence/StudentRepositoryImpl.ts - ✅
src/domain/repositories/DomainStudentRepository.ts - ✅
src/infrastructure/controllers/student.controller.ts
Testing Recomendado¶
# 1. Verificar compilación
npm run rebuild
# 2. Probar endpoint con diferentes escenarios:
# Caso 1: Estudiantes que renuevan (tienen registros)
GET /api/students/administrative?page=1&limit=20
# Caso 2: Filtrar por estado
GET /api/students/administrative?status=active&page=1&limit=20
# Caso 3: Búsqueda por nombre
GET /api/students/administrative?search=Teresa&page=1&limit=20
Notas Adicionales¶
- ⚠️ Importante: Esta implementación respeta el patrón del proyecto de obtener el último curso en el caso de uso
- ✅ Compatibilidad: Los cambios son retrocompatibles -
studentsRawes opcional - 📝 Mantenibilidad: El código está documentado y sigue las convenciones del proyecto
- 🔄 Escalabilidad: Fácil agregar más propiedades calculadas en el futuro siguiendo el mismo patrón
Referencias¶
- Repositorio:
danielcasta0398/backend-musica-maestro - Curso Escolar Repository:
SchoolCourseRepositoryImplimplementagetLastCourse() - Util existente:
src/utils/getLastCourse.ts(consulta directa a BD, no usado en el patrón del dominio)