Queries
Section titled “Queries”Las queries son operaciones de lectura que se cachean automaticamente y se invalidan cuando las actions modifican los mismos entities.
getCourses
Section titled “getCourses”Obtiene todos los cursos publicados.
Declaracion
Section titled “Declaracion”query getCourses { fn: import { getCourses } from "@src/courses/operations", entities: [Course, Lesson]}Implementacion
Section titled “Implementacion”import type { GetCourses } from 'wasp/server/operations';import type { Course } from 'wasp/entities';
type CourseWithLessons = Course & { lessons: { id: number; duration: number }[]; _count: { enrollments: number };};
export const getCourses: GetCourses<void, CourseWithLessons[]> = async ( _args, context) => { return context.entities.Course.findMany({ where: { isPublished: true }, include: { lessons: { select: { id: true, duration: true }, orderBy: { order: 'asc' } }, _count: { select: { enrollments: true } } }, orderBy: { createdAt: 'desc' } });};import { useQuery, getCourses } from 'wasp/client/operations';
function CoursesPage() { const { data: courses, isLoading } = useQuery(getCourses);
return ( <div> {courses?.map(course => ( <CourseCard key={course.id} course={course} /> ))} </div> );}getCourseBySlug
Section titled “getCourseBySlug”Obtiene un curso por su slug con todas sus lecciones.
Declaracion
Section titled “Declaracion”query getCourseBySlug { fn: import { getCourseBySlug } from "@src/courses/operations", entities: [Course, Lesson, Enrollment]}Implementacion
Section titled “Implementacion”import type { GetCourseBySlug } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface GetCourseBySlugInput { slug: string;}
export const getCourseBySlug: GetCourseBySlug<GetCourseBySlugInput, CourseDetail> = async ( { slug }, context) => { const course = await context.entities.Course.findUnique({ where: { slug }, include: { lessons: { orderBy: { order: 'asc' }, select: { id: true, title: true, description: true, order: true, duration: true, isPreview: true // No incluir videoUrl aqui por seguridad } }, _count: { select: { enrollments: true } } } });
if (!course) { throw new HttpError(404, 'Curso no encontrado'); }
// Verificar si el usuario esta inscrito let enrollment = null; if (context.user) { enrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId: course.id } } }); }
return { ...course, isEnrolled: !!enrollment, totalDuration: course.lessons.reduce((sum, l) => sum + l.duration, 0) };};import { useQuery, getCourseBySlug } from 'wasp/client/operations';import { useParams } from 'wasp/client/router';
function CourseDetailPage() { const { slug } = useParams<{ slug: string }>(); const { data: course, isLoading } = useQuery(getCourseBySlug, { slug });
if (isLoading) return <Loading />; if (!course) return <NotFound />;
return <CourseDetail course={course} />;}getMyEnrollments
Section titled “getMyEnrollments”Obtiene las inscripciones del usuario actual con progreso.
Declaracion
Section titled “Declaracion”query getMyEnrollments { fn: import { getMyEnrollments } from "@src/courses/operations", entities: [Enrollment, Course, LessonProgress]}Implementacion
Section titled “Implementacion”import type { GetMyEnrollments } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
export const getMyEnrollments: GetMyEnrollments<void, EnrollmentWithProgress[]> = async ( _args, context) => { if (!context.user) { throw new HttpError(401); }
const enrollments = await context.entities.Enrollment.findMany({ where: { userId: context.user.id }, include: { course: { include: { lessons: { select: { id: true, duration: true } } } }, progress: { where: { isCompleted: true } } }, orderBy: { createdAt: 'desc' } });
// Calcular porcentaje de progreso return enrollments.map(enrollment => { const totalLessons = enrollment.course.lessons.length; const completedLessons = enrollment.progress.length; const progressPercent = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
return { ...enrollment, progressPercent, completedLessons, totalLessons }; });};import { useQuery, getMyEnrollments } from 'wasp/client/operations';
function MyCoursesPage() { const { data: enrollments, isLoading } = useQuery(getMyEnrollments);
return ( <div className="grid grid-cols-3 gap-6"> {enrollments?.map(enrollment => ( <EnrolledCourseCard key={enrollment.id} enrollment={enrollment} /> ))} </div> );}getLessonProgress
Section titled “getLessonProgress”Obtiene el progreso de una leccion especifica.
Declaracion
Section titled “Declaracion”query getLessonProgress { fn: import { getLessonProgress } from "@src/courses/operations", entities: [LessonProgress, Lesson]}Implementacion
Section titled “Implementacion”import type { GetLessonProgress } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface GetLessonProgressInput { lessonId: number;}
export const getLessonProgress: GetLessonProgress< GetLessonProgressInput, LessonProgress | null> = async ({ lessonId }, context) => { if (!context.user) { throw new HttpError(401); }
return context.entities.LessonProgress.findUnique({ where: { userId_lessonId: { userId: context.user.id, lessonId } } });};getSignedVideoUrl
Section titled “getSignedVideoUrl”Obtiene una URL firmada para acceder al video de una leccion.
Declaracion
Section titled “Declaracion”query getSignedVideoUrl { fn: import { getSignedVideoUrl } from "@src/courses/operations", entities: [Lesson, Enrollment]}Implementacion
Section titled “Implementacion”import type { GetSignedVideoUrl } from 'wasp/server/operations';import { HttpError } from 'wasp/server';import { getSignedUrl } from '@aws-sdk/cloudfront-signer';
interface GetSignedVideoUrlInput { lessonId: number;}
export const getSignedVideoUrl: GetSignedVideoUrl< GetSignedVideoUrlInput, { url: string; expiresAt: Date }> = async ({ lessonId }, context) => { // Obtener leccion const lesson = await context.entities.Lesson.findUnique({ where: { id: lessonId }, include: { course: true } });
if (!lesson) { throw new HttpError(404, 'Leccion no encontrada'); }
// Verificar acceso // Preview lessons son publicas if (!lesson.isPreview) { if (!context.user) { throw new HttpError(401, 'Debes iniciar sesion'); }
// Verificar suscripcion o enrollment const hasAccess = await checkCourseAccess( context.user.id, lesson.courseId, context );
if (!hasAccess) { throw new HttpError(403, 'No tienes acceso a esta leccion'); } }
// Generar URL firmada (expira en 1 hora) const expiresAt = new Date(Date.now() + 3600 * 1000);
const signedUrl = getSignedUrl({ url: `https://${process.env.CLOUDFRONT_DOMAIN}/${lesson.videoUrl}`, keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!, privateKey: process.env.CLOUDFRONT_PRIVATE_KEY!, dateLessThan: expiresAt.toISOString() });
return { url: signedUrl, expiresAt };};import { useQuery, getSignedVideoUrl } from 'wasp/client/operations';
function VideoPlayer({ lessonId }: { lessonId: number }) { const { data, isLoading } = useQuery(getSignedVideoUrl, { lessonId });
if (isLoading) return <Loading />;
return ( <video src={data?.url} controls /> );}getAdminStats
Section titled “getAdminStats”Estadisticas para el dashboard de admin.
Declaracion
Section titled “Declaracion”query getAdminStats { fn: import { getAdminStats } from "@src/admin/operations", entities: [User, Course, Enrollment]}Implementacion
Section titled “Implementacion”import type { GetAdminStats } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
export const getAdminStats: GetAdminStats<void, AdminStats> = async ( _args, context) => { if (!context.user?.isAdmin) { throw new HttpError(403); }
const [ totalUsers, totalCourses, totalEnrollments, recentEnrollments ] = await Promise.all([ context.entities.User.count(), context.entities.Course.count({ where: { isPublished: true } }), context.entities.Enrollment.count(), context.entities.Enrollment.count({ where: { createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 dias } } }) ]);
return { totalUsers, totalCourses, totalEnrollments, recentEnrollments };};