Actions
Section titled “Actions”Las actions son operaciones de escritura que modifican datos. Automaticamente invalidan las queries relacionadas cuando se completan.
enrollInCourse
Section titled “enrollInCourse”Inscribe al usuario actual en un curso.
Declaracion
Section titled “Declaracion”action enrollInCourse { fn: import { enrollInCourse } from "@src/courses/operations", entities: [Enrollment, User, Course]}Implementacion
Section titled “Implementacion”import type { EnrollInCourse } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface EnrollInCourseInput { courseId: number;}
export const enrollInCourse: EnrollInCourse<EnrollInCourseInput, void> = async ( { courseId }, context) => { if (!context.user) { throw new HttpError(401, 'Debes iniciar sesion'); }
// Verificar que el curso existe y esta publicado const course = await context.entities.Course.findUnique({ where: { id: courseId } });
if (!course || !course.isPublished) { throw new HttpError(404, 'Curso no encontrado'); }
// Verificar que no esta ya inscrito const existingEnrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId } } });
if (existingEnrollment) { throw new HttpError(400, 'Ya estas inscrito en este curso'); }
// Crear enrollment await context.entities.Enrollment.create({ data: { userId: context.user.id, courseId } });};import { enrollInCourse } from 'wasp/client/operations';
async function handleEnroll(courseId: number) { try { await enrollInCourse({ courseId }); // Redirigir al curso navigate(`/curso/${courseSlug}/aprender`); } catch (error) { toast.error(error.message); }}updateLessonProgress
Section titled “updateLessonProgress”Actualiza el progreso de visualizacion de una leccion.
Declaracion
Section titled “Declaracion”action updateLessonProgress { fn: import { updateLessonProgress } from "@src/courses/operations", entities: [LessonProgress, Enrollment]}Implementacion
Section titled “Implementacion”import type { UpdateLessonProgress } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface UpdateLessonProgressInput { lessonId: number; watchedSeconds: number; isCompleted: boolean;}
export const updateLessonProgress: UpdateLessonProgress< UpdateLessonProgressInput, void> = async ({ lessonId, watchedSeconds, isCompleted }, context) => { if (!context.user) { throw new HttpError(401); }
// Obtener la leccion y su curso const lesson = await context.entities.Lesson.findUnique({ where: { id: lessonId }, include: { course: true } });
if (!lesson) { throw new HttpError(404, 'Leccion no encontrada'); }
// Verificar enrollment const enrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId: lesson.courseId } } });
if (!enrollment) { throw new HttpError(403, 'No estas inscrito en este curso'); }
// Upsert del progreso await context.entities.LessonProgress.upsert({ where: { userId_lessonId: { userId: context.user.id, lessonId } }, create: { userId: context.user.id, lessonId, enrollmentId: enrollment.id, watchedSeconds, isCompleted }, update: { watchedSeconds, // Solo marcar como completado, nunca desmarcar isCompleted: isCompleted ? true : undefined } });};import { updateLessonProgress } from 'wasp/client/operations';
// En el VideoPlayer, llamar cada 10 segundosconst handleTimeUpdate = async (currentTime: number) => { await updateLessonProgress({ lessonId: lesson.id, watchedSeconds: Math.floor(currentTime), isCompleted: false });};
// Al terminar el videoconst handleVideoEnded = async () => { await updateLessonProgress({ lessonId: lesson.id, watchedSeconds: lesson.duration, isCompleted: true });};completeCourse
Section titled “completeCourse”Marca un curso como completado y genera certificado.
Declaracion
Section titled “Declaracion”action completeCourse { fn: import { completeCourse } from "@src/courses/operations", entities: [Enrollment, Certificate, LessonProgress]}Implementacion
Section titled “Implementacion”import type { CompleteCourse } from 'wasp/server/operations';import { HttpError } from 'wasp/server';import { generateCertificatePDF } from '../server/certificates';
interface CompleteCourseInput { courseId: number;}
export const completeCourse: CompleteCourse< CompleteCourseInput, { certificateUrl: string }> = async ({ courseId }, context) => { if (!context.user) { throw new HttpError(401); }
// Obtener enrollment con progreso const enrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId } }, include: { course: { include: { lessons: true } }, progress: { where: { isCompleted: true } } } });
if (!enrollment) { throw new HttpError(404, 'No estas inscrito en este curso'); }
if (enrollment.completedAt) { throw new HttpError(400, 'El curso ya esta completado'); }
// Verificar que todas las lecciones estan completadas const totalLessons = enrollment.course.lessons.length; const completedLessons = enrollment.progress.length;
if (completedLessons < totalLessons) { throw new HttpError(400, `Faltan ${totalLessons - completedLessons} lecciones por completar`); }
// Generar certificado const certificateUrl = await generateCertificatePDF({ userName: context.user.email!, // O nombre si lo tienes courseName: enrollment.course.title, completionDate: new Date() });
// Actualizar enrollment y crear certificado await context.entities.Enrollment.update({ where: { id: enrollment.id }, data: { completedAt: new Date() } });
await context.entities.Certificate.create({ data: { userId: context.user.id, courseId, certificateUrl } });
return { certificateUrl };};createCourseCheckout
Section titled “createCourseCheckout”Crea una sesion de checkout en Stripe para comprar un curso.
Declaracion
Section titled “Declaracion”action createCourseCheckout { fn: import { createCourseCheckout } from "@src/payment/operations", entities: [Course, User]}Implementacion
Section titled “Implementacion”import type { CreateCourseCheckout } from 'wasp/server/operations';import { HttpError } from 'wasp/server';import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
interface CreateCourseCheckoutInput { courseId: number;}
export const createCourseCheckout: CreateCourseCheckout< CreateCourseCheckoutInput, { url: string }> = async ({ courseId }, context) => { if (!context.user) { throw new HttpError(401, 'Debes iniciar sesion'); }
const course = await context.entities.Course.findUnique({ where: { id: courseId } });
if (!course || !course.isPublished) { throw new HttpError(404, 'Curso no encontrado'); }
// Verificar que no esta ya inscrito const existingEnrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId } } });
if (existingEnrollment) { throw new HttpError(400, 'Ya estas inscrito en este curso'); }
// Crear sesion de Stripe const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [{ price_data: { currency: course.currency.toLowerCase(), product_data: { name: course.title, description: course.description.substring(0, 500), images: course.thumbnail ? [course.thumbnail] : [] }, unit_amount: course.price }, quantity: 1 }], mode: 'payment', success_url: `${process.env.CLIENT_URL}/curso/${course.slug}/aprender?success=true`, cancel_url: `${process.env.CLIENT_URL}/curso/${course.slug}?canceled=true`, customer_email: context.user.email!, metadata: { userId: context.user.id.toString(), courseId: course.id.toString(), type: 'course_purchase' } });
return { url: session.url! };};import { createCourseCheckout } from 'wasp/client/operations';
async function handleBuy(courseId: number) { const { url } = await createCourseCheckout({ courseId }); window.location.href = url;}Admin Actions
Section titled “Admin Actions”createCourse
Section titled “createCourse”import type { CreateCourse } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface CreateCourseInput { title: string; slug: string; description: string; category: string; difficulty: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED'; price: number; thumbnail?: string;}
export const createCourse: CreateCourse<CreateCourseInput, Course> = async ( data, context) => { if (!context.user?.isAdmin) { throw new HttpError(403, 'Solo administradores'); }
// Verificar slug unico const existing = await context.entities.Course.findUnique({ where: { slug: data.slug } });
if (existing) { throw new HttpError(400, 'El slug ya existe'); }
return context.entities.Course.create({ data: { ...data, isPublished: false } });};updateCourse
Section titled “updateCourse”import type { UpdateCourse } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface UpdateCourseInput { id: number; title?: string; description?: string; category?: string; difficulty?: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED'; price?: number; thumbnail?: string; isPublished?: boolean;}
export const updateCourse: UpdateCourse<UpdateCourseInput, Course> = async ( { id, ...data }, context) => { if (!context.user?.isAdmin) { throw new HttpError(403); }
return context.entities.Course.update({ where: { id }, data });};deleteCourse
Section titled “deleteCourse”import type { DeleteCourse } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface DeleteCourseInput { id: number;}
export const deleteCourse: DeleteCourse<DeleteCourseInput, void> = async ( { id }, context) => { if (!context.user?.isAdmin) { throw new HttpError(403); }
// Verificar que no hay inscripciones const enrollments = await context.entities.Enrollment.count({ where: { courseId: id } });
if (enrollments > 0) { throw new HttpError(400, `No se puede eliminar: hay ${enrollments} estudiantes inscritos`); }
// Eliminar lecciones primero await context.entities.Lesson.deleteMany({ where: { courseId: id } });
// Eliminar curso await context.entities.Course.delete({ where: { id } });};createLesson
Section titled “createLesson”import type { CreateLesson } from 'wasp/server/operations';import { HttpError } from 'wasp/server';
interface CreateLessonInput { courseId: number; title: string; description?: string; order: number; duration: number; videoUrl: string; content?: string; isPreview?: boolean;}
export const createLesson: CreateLesson<CreateLessonInput, Lesson> = async ( data, context) => { if (!context.user?.isAdmin) { throw new HttpError(403); }
// Verificar que el curso existe const course = await context.entities.Course.findUnique({ where: { id: data.courseId } });
if (!course) { throw new HttpError(404, 'Curso no encontrado'); }
return context.entities.Lesson.create({ data: { ...data, isPreview: data.isPreview ?? false } });};