Saltar al contenido principal

📦 Estructura de Módulos

Visión General

PuntoHack organiza el código en módulos de dominio siguiendo un patrón consistente. Cada módulo encapsula toda la lógica relacionada con una entidad de negocio específica.

Estructura Estándar

Cada módulo sigue esta estructura:

modules/[domain]/
├── queries.ts # Operaciones de lectura (SELECT)
├── actions.ts # Server Actions (INSERT, UPDATE, DELETE)
├── types.ts # TypeScript interfaces y tipos
└── validations.ts # Zod schemas para validación

Patrón de Módulo

1. queries.ts - Lectura de Datos

Responsabilidad: Obtener datos de la base de datos sin modificarlos.

Características:

  • ✅ Funciones async
  • ✅ Nombres descriptivos: get, list, find, count
  • ✅ Incluir relaciones necesarias
  • ✅ Ordenar resultados cuando aplique

Ejemplo:

import { db } from '@/core/db';

/**
* Obtiene un hackathon por su slug
* Incluye criterios y contador de participaciones
*/
export async function getHackathon(slug: string) {
return db.hackathon.findUnique({
where: { slug },
include: {
criteria: {
orderBy: { createdAt: 'asc' },
},
_count: {
select: {
participations: true,
criteria: true,
teams: true,
},
},
},
});
}

/**
* Lista hackathons con filtros opcionales
*/
export async function listHackathons(filters?: {
status?: HackathonStatus;
organizerId?: string;
limit?: number;
}) {
const where: {
status?: HackathonStatus;
organizerId?: string;
} = {};

if (filters?.status) {
where.status = filters.status;
}

if (filters?.organizerId) {
where.organizerId = filters.organizerId;
}

return db.hackathon.findMany({
where,
take: filters?.limit || 50,
orderBy: { createdAt: 'desc' },
include: {
organizer: {
select: {
id: true,
name: true,
email: true,
},
},
_count: {
select: {
participations: true,
teams: true,
},
},
},
});
}

2. actions.ts - Mutaciones de Datos

Responsabilidad: Modificar datos mediante Server Actions.

Características:

  • 'use server' al inicio
  • ✅ Auth check obligatorio
  • ✅ RBAC check obligatorio
  • ✅ Validación con Zod
  • ✅ Revalidación de cache
  • ✅ Manejo de errores

Ejemplo:

'use server';

import { revalidatePath } from 'next/cache';
import { getCurrentUser } from '@/core/auth';
import { requireRole } from '@/core/rbac';
import { db } from '@/core/db';
import { captureError } from '@/core/errors';
import { createHackathonSchema } from './validations';

export async function createHackathon(formData: FormData) {
try {
// 1. Auth check
const user = await getCurrentUser();
if (!user) {
throw new Error('Unauthorized');
}

// 2. RBAC check
requireRole(user, ['ORGANIZER', 'ADMIN']);

// 3. Validate input
const rawData = {
name: formData.get('name'),
slug: formData.get('slug'),
description: formData.get('description'),
// ... otros campos
};

const validated = createHackathonSchema.parse(rawData);

// 4. Business logic
const hackathon = await db.hackathon.create({
data: validated,
});

// 5. Revalidate cache
revalidatePath('/hackathons');

// 6. Return result
return { success: true, data: hackathon };
} catch (error) {
captureError(error, { context: 'createHackathon' });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}

3. validations.ts - Validación de Datos

Responsabilidad: Definir schemas Zod para validar inputs.

Características:

  • ✅ Schemas descriptivos
  • ✅ Mensajes de error claros
  • ✅ Validaciones custom con .refine()
  • ✅ Coerción de tipos cuando sea necesario

Ejemplo:

import { z } from 'zod';

export const createHackathonSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
slug: z.string().regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),
description: z.string().min(10, 'Description must be at least 10 characters'),
registrationOpensAt: z.coerce.date(),
registrationClosesAt: z.coerce.date(),
startsAt: z.coerce.date(),
endsAt: z.coerce.date(),
submissionDeadline: z.coerce.date(),
judgingStartsAt: z.coerce.date(),
judgingEndsAt: z.coerce.date(),
maxTeamSize: z.coerce.number().int().min(1).max(10),
minTeamSize: z.coerce.number().int().min(1).max(5),
}).refine(
(data) => data.registrationOpensAt < data.registrationClosesAt,
{
message: 'registrationOpensAt must be before registrationClosesAt',
path: ['registrationOpensAt'],
}
).refine(
(data) => data.submissionDeadline < data.judgingStartsAt,
{
message: 'submissionDeadline must be before judgingStartsAt',
path: ['submissionDeadline'],
}
);

4. types.ts - Tipos TypeScript

Responsabilidad: Definir tipos e interfaces específicos del módulo.

Características:

  • ✅ Tipos extendidos con relaciones
  • ✅ Input types para crear/actualizar
  • ✅ Response types para queries

Ejemplo:

import { Hackathon, Criterion, Profile } from '@prisma/client';

export type HackathonWithCriteria = Hackathon & {
criteria: Criterion[];
};

export type HackathonWithStats = Hackathon & {
_count: {
participations: number;
teams: number;
submissions: number;
criteria: number;
};
};

export type CreateHackathonInput = {
name: string;
slug: string;
description: string;
registrationOpensAt: Date;
registrationClosesAt: Date;
startsAt: Date;
endsAt: Date;
submissionDeadline: Date;
judgingStartsAt: Date;
judgingEndsAt: Date;
maxTeamSize: number;
minTeamSize: number;
};

export type UpdateHackathonInput = Partial<CreateHackathonInput>;

Interacción con RBAC

Uso de RBAC en Actions

Cada Server Action debe verificar permisos:

export async function deleteHackathon(hackathonId: string) {
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');

// RBAC check específico
requireRole(user, ['ORGANIZER', 'ADMIN']);

// Verificar propiedad (si aplica)
const hackathon = await db.hackathon.findUnique({
where: { id: hackathonId },
});

if (!hackathon) {
throw new Error('Hackathon not found');
}

// Solo el organizador o admin puede eliminar
if (hackathon.organizerId !== user.profile.id && user.profile.role !== 'ADMIN') {
throw new Error('Unauthorized: Only organizer or admin can delete');
}

// Continuar con eliminación...
}

RBAC en Queries

Las queries pueden filtrar por permisos:

export async function listHackathonsForUser(userId: string) {
const user = await getCurrentUser();
if (!user) return [];

const where: {
status?: HackathonStatus;
organizerId?: string;
} = {};

// Si es ORGANIZER, puede ver sus DRAFT hackathons
if (user.profile.role === 'ORGANIZER') {
where.OR = [
{ status: { not: 'DRAFT' } }, // Públicos
{ organizerId: user.profile.id }, // Sus propios
];
} else {
// Otros roles solo ven públicos
where.status = { not: 'DRAFT' };
}

return db.hackathon.findMany({ where });
}

Módulos Existentes

1. users/

Responsabilidad: Gestión de usuarios y perfiles.

Archivos:

  • queries.ts - Obtener perfiles, verificar roles
  • actions.ts - Crear/actualizar perfiles, cambiar roles (ADMIN)
  • validations.ts - Schemas para perfiles
  • types.ts - Tipos de usuario

2. hackathons/

Responsabilidad: CRUD de hackathons, gestión de estados.

Archivos:

  • queries.ts - Obtener hackathons, filtrar por estado
  • actions.ts - Crear, actualizar, publicar hackathons
  • validations.ts - Validación de fechas, estados
  • types.ts - Tipos de hackathon

3. teams/

Responsabilidad: Formación y gestión de equipos.

Archivos:

  • queries.ts - Obtener equipos, miembros, verificar membresía
  • actions.ts - Crear equipos, unirse, salir, invitar
  • validations.ts - Validación de códigos, tamaños
  • types.ts - Tipos de equipo

4. submissions/

Responsabilidad: Envío y gestión de proyectos.

Archivos:

  • queries.ts - Obtener submissions, filtrar por hackathon/equipo
  • actions.ts - Crear, actualizar submissions (hasta deadline)
  • validations.ts - Validación de URLs, descripciones
  • types.ts - Tipos de submission

5. evaluation/

Responsabilidad: Sistema de evaluación y scoring.

Archivos:

  • queries.ts - Obtener scores, calcular leaderboard
  • actions.ts - Asignar scores, evaluar challenges
  • validations.ts - Validación de scores (0-maxScore)
  • types.ts - Tipos de evaluación

6. sponsors/

Responsabilidad: Gestión de sponsors, challenges, shortlist.

Archivos:

  • queries.ts - Obtener organizaciones, sponsorships, challenges
  • actions.ts - Crear organizaciones, sponsorships, challenges, shortlist
  • validations.ts - Validación de tiers, challenges
  • types.ts - Tipos de sponsor

Diagrama de Interacción

Mejores Prácticas

1. Separación de Responsabilidades

  • ✅ Queries solo leen
  • ✅ Actions solo escriben
  • ✅ Validations solo validan
  • ✅ Types solo definen tipos

2. Reutilización

  • ✅ Usar queries dentro de actions cuando sea necesario
  • ✅ Compartir validations entre actions similares
  • ✅ Tipos compartidos en types.ts

3. Testing

  • ✅ Testear queries con datos reales
  • ✅ Testear actions con mocks
  • ✅ Testear validations con casos límite

4. Documentación

  • ✅ JSDoc en funciones públicas
  • ✅ Comentarios en lógica compleja
  • ✅ Ejemplos en tipos complejos

Próximos Pasos


Siguiente: Design Patterns