Sesión 2 · chuleta
En el orden de las slides. Copia con el botón de cada bloque; si la slide dice «teclea», esas líneas van a mano.
📋 S2·01 · receta «Cambiar de rama» a s2-start (terminal)
Sección titulada «📋 S2·01 · receta «Cambiar de rama» a s2-start (terminal)»git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s2 origin/s2-start # checkpoint: mi-s2-cp origin/s2-checkpointnpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed📋 S2·02 · Category → Course (prisma/schema.prisma)
Sección titulada «📋 S2·02 · Category → Course (prisma/schema.prisma)»model Course { // ...campos de la sesión 1 (title, description, price, published, level, createdAt) instructorId Int instructor User @relation(fields: [instructorId], references: [id])
categoryId Int? category Category? @relation(fields: [categoryId], references: [id])}
model Category { id Int @id @default(autoincrement()) name String @unique courses Course[]}📋 S2·03 · enum EnrollmentStatus (prisma/schema.prisma)
Sección titulada «📋 S2·03 · enum EnrollmentStatus (prisma/schema.prisma)»enum EnrollmentStatus { ACTIVE COMPLETED CANCELLED}📋 S2·04 · migrar y generar el cliente (terminal)
Sección titulada «📋 S2·04 · migrar y generar el cliente (terminal)»npx prisma migrate dev --name add_enrollmentsnpx prisma generate📋 S2·05 · src/seed-manual.ts completo (sustituye el de la S1)
Sección titulada «📋 S2·05 · src/seed-manual.ts completo (sustituye el de la S1)»// src/seed-manual.ts — sustituye entero el de la S1import { prisma } from './db.js';
const PLACEHOLDER = 'hashed-password-placeholder'; // sesión 4: Argon2id
async function upsertUser(email: string, name: string, role: 'STUDENT' | 'INSTRUCTOR' | 'ADMIN') { return prisma.user.upsert({ where: { email }, update: {}, create: { email, name, password: PLACEHOLDER, role }, });}
async function main() { const marta = await upsertUser('marta@academia.dev', 'Marta', 'INSTRUCTOR'); const students = await Promise.all([ upsertUser('luis@academia.dev', 'Luis', 'STUDENT'), upsertUser('sara@academia.dev', 'Sara', 'STUDENT'), upsertUser('omar@academia.dev', 'Omar', 'STUDENT'), ]);
const [backend, databases] = await Promise.all( ['Backend', 'Bases de datos'].map((name) => prisma.category.upsert({ where: { name }, update: {}, create: { name } }), ), );
// Course no tiene campo único aparte del id: // reseteamos los cursos de Marta (primero sus inscripciones, por el RESTRICT) await prisma.enrollment.deleteMany({ where: { course: { instructorId: marta.id } } }); await prisma.course.deleteMany({ where: { instructorId: marta.id } });
// create, no createMany: necesitamos el id de cada curso const ts = await prisma.course.create({ data: { title: 'TypeScript desde cero', price: 49.99, published: true, level: 'BEGINNER', instructorId: marta.id, categoryId: backend.id } }); const prismaAdv = await prisma.course.create({ data: { title: 'Prisma avanzado', price: 79.99, published: true, level: 'ADVANCED', instructorId: marta.id, categoryId: databases.id } }); await prisma.course.create({ data: { title: 'SQL para backend', price: 39.99, published: false, level: 'INTERMEDIATE', instructorId: marta.id, categoryId: databases.id } });
// Inscripciones cubriendo los 3 estados (cada par usuario-curso es único) await prisma.enrollment.createMany({ data: [ { userId: students[0].id, courseId: ts.id, status: 'ACTIVE' }, { userId: students[1].id, courseId: ts.id, status: 'COMPLETED', grade: 9 }, { userId: students[2].id, courseId: prismaAdv.id, status: 'CANCELLED' }, ], });
console.log('Seed OK:', await prisma.course.count(), 'cursos,', await prisma.enrollment.count(), 'inscripciones');} // fin de main()
main().finally(() => prisma.$disconnect());📋 S2·06 · el seed dos veces (terminal)
Sección titulada «📋 S2·06 · el seed dos veces (terminal)»npm run seed # Seed OK: 3 cursos, 3 inscripcionesnpm run seed # Seed OK: 3 cursos, 3 inscripciones (otra vez, sin errores)📋 S2·07 · esqueleto de src/playground/queries.ts
Sección titulada «📋 S2·07 · esqueleto de src/playground/queries.ts»import { prisma } from '../db.js';
async function main() { // ✍️ a mano: las queries de esta sección, aquí dentro}
main().finally(() => prisma.$disconnect());📋 S2·08 · paginación offset (dentro de main en queries.ts)
Sección titulada «📋 S2·08 · paginación offset (dentro de main en queries.ts)»const page = 1, pageSize = 2;const offsetPage = await prisma.course.findMany({ skip: (page - 1) * pageSize, take: pageSize, orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],});📋 S2·09 · andamiaje del todo o nada (al final de main en queries.ts)
Sección titulada «📋 S2·09 · andamiaje del todo o nada (al final de main en queries.ts)»// añade al final de main() en src/playground/queries.tsconst fail = true; // paso 2: cámbialo a falsetry { await prisma.$transaction(async (tx) => { // ✍️ a mano: dos tx.category.create y el throw });} catch (err) { console.log('tx →', (err as Error).message);}const names = ['Frontend', 'DevOps'];console.log('guardadas →', await prisma.category.count({ where: { name: { in: names } } }));await prisma.category.deleteMany({ where: { name: { in: names } } }); // deja la base como estaba📋 S2·10 · src/playground/s2-listado-y-aforo.ia.ts (código de IA)
Sección titulada «📋 S2·10 · src/playground/s2-listado-y-aforo.ia.ts (código de IA)»// ─────────────────────────────────────────────────────────────────────────// Generado por un asistente de IA · Sesión 2// Prompt 1: "Lista los estudiantes con cuántas inscripciones tiene cada uno"// Prompt 2: "Función para inscribir a un alumno respetando el aforo maxSeats"// Tu tarea: (1) activa log de queries y cuenta cuántas lanza listStudents.// (2) lee enrollStudent: ¿qué pasa si dos alumnos se inscriben a la vez?// (lo comprobaréis con la demo src/playground/race.ts).// ─────────────────────────────────────────────────────────────────────────import { prisma } from '../db.js';
export async function listStudents() { const students = await prisma.user.findMany({ where: { role: 'STUDENT' } }); const result = []; for (const s of students) { const enrollments = await prisma.enrollment.findMany({ where: { userId: s.id } }); result.push({ name: s.name, enrollments: enrollments.length }); } return result;}
export async function enrollStudent(userId: number, courseId: number) { const course = await prisma.course.findUnique({ where: { id: courseId } }); const count = await prisma.enrollment.count({ where: { courseId } }); if (course && count < course.maxSeats) { return prisma.enrollment.create({ data: { userId, courseId } }); } throw new Error('Course full');}📋 S2·11 · cliente que cuenta queries (inicio de src/playground/list-students.ts)
Sección titulada «📋 S2·11 · cliente que cuenta queries (inicio de src/playground/list-students.ts)»import 'dotenv/config';import { PrismaPg } from '@prisma/adapter-pg';import { PrismaClient } from '../../generated/prisma/client.js'; // dos ../
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), log: [{ emit: 'event', level: 'query' }],});
let queries = 0;prisma.$on('query', () => { queries++; });📋 S2·12 · traer del checkpoint solo los ficheros que no has escrito (terminal)
Sección titulada «📋 S2·12 · traer del checkpoint solo los ficheros que no has escrito (terminal)»git fetch origingit checkout origin/s2-checkpoint -- src/services src/errors.ts src/playground/race.ts src/playground/n-plus-one.ts📋 S2·13 · receta «Cambiar de rama» a s2-checkpoint (terminal)
Sección titulada «📋 S2·13 · receta «Cambiar de rama» a s2-checkpoint (terminal)»git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s2-cp origin/s2-checkpoint # mi-s2 ya existe: otro nombrenpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed📋 S2·14 · measure y main (sigue en src/playground/list-students.ts)
Sección titulada «📋 S2·14 · measure y main (sigue en src/playground/list-students.ts)»// sigue en src/playground/list-students.tsasync function measure(label: string, fn: () => Promise<unknown>) { queries = 0; const result = await fn(); console.log(`${label}: ${queries} queries`, JSON.stringify(result));}// al final del fichero, como en queries.tsasync function main() { await measure('IA (bucle)', listStudents); // + una por versión tuya}main().finally(() => prisma.$disconnect());