Skip to content

Las actions son operaciones de escritura que modifican datos. Automaticamente invalidan las queries relacionadas cuando se completan.

Inscribe al usuario actual en un curso.

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

Actualiza el progreso de visualizacion de una leccion.

action updateLessonProgress {
fn: import { updateLessonProgress } from "@src/courses/operations",
entities: [LessonProgress, Enrollment]
}
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 segundos
const handleTimeUpdate = async (currentTime: number) => {
await updateLessonProgress({
lessonId: lesson.id,
watchedSeconds: Math.floor(currentTime),
isCompleted: false
});
};
// Al terminar el video
const handleVideoEnded = async () => {
await updateLessonProgress({
lessonId: lesson.id,
watchedSeconds: lesson.duration,
isCompleted: true
});
};

Marca un curso como completado y genera certificado.

action completeCourse {
fn: import { completeCourse } from "@src/courses/operations",
entities: [Enrollment, Certificate, LessonProgress]
}
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 };
};

Crea una sesion de checkout en Stripe para comprar un curso.

action createCourseCheckout {
fn: import { createCourseCheckout } from "@src/payment/operations",
entities: [Course, User]
}
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;
}

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
}
});
};
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
});
};
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 }
});
};
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
}
});
};