2026-01-08 04:00:56 +00:00
|
|
|
# DI-004: Esquema GraphQL
|
|
|
|
|
|
|
|
|
|
**Proyecto:** Sistema de Registro de Estudiantes
|
|
|
|
|
**Fecha:** 2026-01-07
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 1. Schema Completo
|
|
|
|
|
|
|
|
|
|
```graphql
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# TYPES
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
type Student {
|
|
|
|
|
id: Int!
|
|
|
|
|
name: String!
|
|
|
|
|
email: String!
|
|
|
|
|
totalCredits: Int!
|
2026-01-09 12:43:57 +00:00
|
|
|
isActivated: Boolean!
|
|
|
|
|
activationExpiresAt: DateTime
|
2026-01-08 04:00:56 +00:00
|
|
|
enrollments: [Enrollment!]!
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:43:57 +00:00
|
|
|
type User {
|
|
|
|
|
id: Int!
|
|
|
|
|
username: String!
|
|
|
|
|
role: String! # "Admin" | "Student"
|
|
|
|
|
studentId: Int
|
|
|
|
|
studentName: String
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 04:00:56 +00:00
|
|
|
type Subject {
|
|
|
|
|
id: Int!
|
|
|
|
|
name: String!
|
|
|
|
|
credits: Int!
|
|
|
|
|
professor: Professor!
|
|
|
|
|
enrolledStudents: [Student!]!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Professor {
|
|
|
|
|
id: Int!
|
|
|
|
|
name: String!
|
|
|
|
|
subjects: [Subject!]!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Enrollment {
|
|
|
|
|
id: Int!
|
|
|
|
|
student: Student!
|
|
|
|
|
subject: Subject!
|
|
|
|
|
enrolledAt: DateTime!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AvailableSubject {
|
|
|
|
|
subject: Subject!
|
|
|
|
|
isAvailable: Boolean!
|
|
|
|
|
unavailableReason: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Classmate {
|
|
|
|
|
subjectName: String!
|
|
|
|
|
students: [String!]!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# QUERIES
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
type Query {
|
2026-01-09 12:43:57 +00:00
|
|
|
# Autenticación
|
|
|
|
|
me: User # Usuario autenticado actual
|
|
|
|
|
validateActivationCode(code: String!): ActivationValidation!
|
|
|
|
|
|
2026-01-08 04:00:56 +00:00
|
|
|
# Estudiantes
|
|
|
|
|
students: [Student!]!
|
|
|
|
|
student(id: Int!): Student
|
|
|
|
|
|
|
|
|
|
# Materias
|
|
|
|
|
subjects: [Subject!]!
|
|
|
|
|
subject(id: Int!): Subject
|
|
|
|
|
availableSubjects(studentId: Int!): [AvailableSubject!]!
|
|
|
|
|
|
|
|
|
|
# Profesores
|
|
|
|
|
professors: [Professor!]!
|
|
|
|
|
professor(id: Int!): Professor
|
|
|
|
|
|
|
|
|
|
# Compañeros de clase
|
|
|
|
|
classmates(studentId: Int!): [Classmate!]!
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:43:57 +00:00
|
|
|
type ActivationValidation {
|
|
|
|
|
isValid: Boolean!
|
|
|
|
|
studentName: String
|
|
|
|
|
error: String
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 04:00:56 +00:00
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# MUTATIONS
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
type Mutation {
|
2026-01-09 12:43:57 +00:00
|
|
|
# Autenticación
|
|
|
|
|
login(input: LoginInput!): AuthPayload!
|
|
|
|
|
activateAccount(input: ActivateAccountInput!): AuthPayload!
|
|
|
|
|
resetPassword(input: ResetPasswordInput!): ResetPayload!
|
|
|
|
|
|
|
|
|
|
# Estudiantes (Admin crea con código de activación)
|
|
|
|
|
createStudent(input: CreateStudentInput!): CreateStudentPayload!
|
2026-01-08 04:00:56 +00:00
|
|
|
updateStudent(id: Int!, input: UpdateStudentInput!): StudentPayload!
|
|
|
|
|
deleteStudent(id: Int!): DeletePayload!
|
2026-01-09 12:43:57 +00:00
|
|
|
regenerateActivationCode(studentId: Int!): ActivationCodePayload!
|
2026-01-08 04:00:56 +00:00
|
|
|
|
|
|
|
|
# Inscripciones
|
|
|
|
|
enrollStudent(input: EnrollInput!): EnrollmentPayload!
|
|
|
|
|
unenrollStudent(enrollmentId: Int!): DeletePayload!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# INPUTS
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-09 12:43:57 +00:00
|
|
|
# Autenticación
|
|
|
|
|
input LoginInput {
|
|
|
|
|
username: String!
|
|
|
|
|
password: String!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input ActivateAccountInput {
|
|
|
|
|
activationCode: String!
|
|
|
|
|
username: String!
|
|
|
|
|
password: String!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input ResetPasswordInput {
|
|
|
|
|
username: String!
|
|
|
|
|
recoveryCode: String!
|
|
|
|
|
newPassword: String!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Estudiantes
|
2026-01-08 04:00:56 +00:00
|
|
|
input CreateStudentInput {
|
|
|
|
|
name: String!
|
|
|
|
|
email: String!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input UpdateStudentInput {
|
|
|
|
|
name: String
|
|
|
|
|
email: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input EnrollInput {
|
|
|
|
|
studentId: Int!
|
|
|
|
|
subjectId: Int!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# PAYLOADS (Union para errores)
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-09 12:43:57 +00:00
|
|
|
# Autenticación
|
|
|
|
|
type AuthPayload {
|
|
|
|
|
success: Boolean!
|
|
|
|
|
token: String
|
|
|
|
|
recoveryCode: String # Solo en activación (se muestra una vez)
|
|
|
|
|
user: User
|
|
|
|
|
error: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ResetPayload {
|
|
|
|
|
success: Boolean!
|
|
|
|
|
error: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Estudiantes
|
|
|
|
|
type CreateStudentPayload {
|
|
|
|
|
student: Student
|
|
|
|
|
activationCode: String # Código para activar cuenta
|
|
|
|
|
activationUrl: String # URL completa de activación
|
|
|
|
|
expiresAt: DateTime # Cuándo expira el código
|
|
|
|
|
errors: [String!]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ActivationCodePayload {
|
|
|
|
|
activationCode: String!
|
|
|
|
|
activationUrl: String!
|
|
|
|
|
expiresAt: DateTime!
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 04:00:56 +00:00
|
|
|
type StudentPayload {
|
|
|
|
|
student: Student
|
|
|
|
|
errors: [String!]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EnrollmentPayload {
|
|
|
|
|
enrollment: Enrollment
|
|
|
|
|
errors: [String!]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DeletePayload {
|
|
|
|
|
success: Boolean!
|
|
|
|
|
errors: [String!]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# SCALARS
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
scalar DateTime
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. Ejemplos de Queries
|
|
|
|
|
|
|
|
|
|
### Listar estudiantes con inscripciones
|
|
|
|
|
```graphql
|
|
|
|
|
query GetStudents {
|
|
|
|
|
students {
|
|
|
|
|
id
|
|
|
|
|
name
|
|
|
|
|
email
|
|
|
|
|
totalCredits
|
|
|
|
|
enrollments {
|
|
|
|
|
subject { name professor { name } }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Materias disponibles para inscripción
|
|
|
|
|
```graphql
|
|
|
|
|
query GetAvailableSubjects($studentId: Int!) {
|
|
|
|
|
availableSubjects(studentId: $studentId) {
|
|
|
|
|
subject { id name credits professor { name } }
|
|
|
|
|
isAvailable
|
|
|
|
|
unavailableReason
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Compañeros de clase
|
|
|
|
|
```graphql
|
|
|
|
|
query GetClassmates($studentId: Int!) {
|
|
|
|
|
classmates(studentId: $studentId) {
|
|
|
|
|
subjectName
|
|
|
|
|
students
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. Ejemplos de Mutations
|
|
|
|
|
|
|
|
|
|
### Crear estudiante
|
|
|
|
|
```graphql
|
|
|
|
|
mutation CreateStudent {
|
|
|
|
|
createStudent(input: { name: "Juan Pérez", email: "juan@email.com" }) {
|
|
|
|
|
student { id name email }
|
|
|
|
|
errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Inscribir en materia
|
|
|
|
|
```graphql
|
|
|
|
|
mutation Enroll {
|
|
|
|
|
enrollStudent(input: { studentId: 1, subjectId: 3 }) {
|
|
|
|
|
enrollment {
|
|
|
|
|
id
|
|
|
|
|
subject { name }
|
|
|
|
|
enrolledAt
|
|
|
|
|
}
|
|
|
|
|
errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 4. DataLoaders (Evitar N+1)
|
|
|
|
|
|
|
|
|
|
| DataLoader | Carga Batch |
|
|
|
|
|
|------------|-------------|
|
|
|
|
|
| `StudentByIdDataLoader` | Estudiantes por IDs |
|
|
|
|
|
| `SubjectByIdDataLoader` | Materias por IDs |
|
|
|
|
|
| `ProfessorByIdDataLoader` | Profesores por IDs |
|
|
|
|
|
| `EnrollmentsByStudentDataLoader` | Inscripciones por estudiante |
|
|
|
|
|
| `SubjectsByProfessorDataLoader` | Materias por profesor |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. Errores de Negocio
|
|
|
|
|
|
|
|
|
|
| Código | Mensaje | Contexto |
|
|
|
|
|
|--------|---------|----------|
|
|
|
|
|
| `MAX_ENROLLMENTS` | "Máximo 3 materias permitidas" | enrollStudent |
|
|
|
|
|
| `SAME_PROFESSOR` | "Ya tienes materia con este profesor" | enrollStudent |
|
|
|
|
|
| `DUPLICATE_EMAIL` | "Email ya registrado" | createStudent |
|
|
|
|
|
| `NOT_FOUND` | "Estudiante no encontrado" | updateStudent |
|
2026-01-09 12:43:57 +00:00
|
|
|
| `INVALID_CREDENTIALS` | "Usuario o contraseña incorrectos" | login |
|
|
|
|
|
| `USERNAME_EXISTS` | "El nombre de usuario ya está en uso" | activateAccount |
|
|
|
|
|
| `INVALID_ACTIVATION_CODE` | "Código de activación inválido o expirado" | activateAccount |
|
|
|
|
|
| `INVALID_RECOVERY_CODE` | "Código de recuperación inválido" | resetPassword |
|
|
|
|
|
| `UNAUTHORIZED` | "No tienes permiso para esta acción" | Operaciones protegidas |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 6. Ejemplos de Autenticación
|
|
|
|
|
|
|
|
|
|
### Login
|
|
|
|
|
```graphql
|
|
|
|
|
mutation Login {
|
|
|
|
|
login(input: { username: "admin", password: "Admin123!" }) {
|
|
|
|
|
success
|
|
|
|
|
token
|
|
|
|
|
user { id username role studentId studentName }
|
|
|
|
|
error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Activar Cuenta
|
|
|
|
|
```graphql
|
|
|
|
|
mutation ActivateAccount {
|
|
|
|
|
activateAccount(input: {
|
|
|
|
|
activationCode: "MSAGDM5DNLAF"
|
|
|
|
|
username: "juan.perez"
|
|
|
|
|
password: "MiPassword123"
|
|
|
|
|
}) {
|
|
|
|
|
success
|
|
|
|
|
token
|
|
|
|
|
recoveryCode # Solo se muestra UNA vez
|
|
|
|
|
user { id username role studentId studentName }
|
|
|
|
|
error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Crear Estudiante (Admin)
|
|
|
|
|
```graphql
|
|
|
|
|
mutation CreateStudent {
|
|
|
|
|
createStudent(input: { name: "Juan Pérez", email: "juan@email.com" }) {
|
|
|
|
|
student { id name email isActivated }
|
|
|
|
|
activationCode # "MSAGDM5DNLAF"
|
|
|
|
|
activationUrl # "https://app.com/activate?code=MSAGDM5DNLAF"
|
|
|
|
|
expiresAt # "2026-01-11T06:00:00Z"
|
|
|
|
|
errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|