Skip to content

Las queries son operaciones de lectura que se cachean automaticamente y se invalidan cuando las actions modifican los mismos entities.

Obtiene todos los cursos publicados.

query getCourses {
fn: import { getCourses } from "@src/courses/operations",
entities: [Course, Lesson]
}
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>
);
}

Obtiene un curso por su slug con todas sus lecciones.

query getCourseBySlug {
fn: import { getCourseBySlug } from "@src/courses/operations",
entities: [Course, Lesson, Enrollment]
}
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} />;
}

Obtiene las inscripciones del usuario actual con progreso.

query getMyEnrollments {
fn: import { getMyEnrollments } from "@src/courses/operations",
entities: [Enrollment, Course, LessonProgress]
}
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>
);
}

Obtiene el progreso de una leccion especifica.

query getLessonProgress {
fn: import { getLessonProgress } from "@src/courses/operations",
entities: [LessonProgress, Lesson]
}
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
}
}
});
};

Obtiene una URL firmada para acceder al video de una leccion.

query getSignedVideoUrl {
fn: import { getSignedVideoUrl } from "@src/courses/operations",
entities: [Lesson, Enrollment]
}
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 />
);
}

Estadisticas para el dashboard de admin.

query getAdminStats {
fn: import { getAdminStats } from "@src/admin/operations",
entities: [User, Course, Enrollment]
}
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
};
};