diff --git a/prisma/content/courses/bases-de-datos/index.ts b/prisma/content/courses/bases-de-datos/index.ts index 8dff42e..1b8b278 100644 --- a/prisma/content/courses/bases-de-datos/index.ts +++ b/prisma/content/courses/bases-de-datos/index.ts @@ -11,8 +11,20 @@ import { bd1Dml } from "./units/08-dml"; import { bd1ConsultasReportes } from "./units/09-consultas-reportes"; import { bd1RespaldoIntegrador } from "./units/10-respaldo-integrador"; +import { bd2Procedimientos } from "./units/11-procedimientos"; +import { bd2TriggersJobs } from "./units/12-triggers-jobs"; +import { bd2Transacciones } from "./units/13-transacciones"; +import { bd2UsuariosPermisos } from "./units/14-usuarios-permisos"; +import { bd2Mantenimiento } from "./units/15-mantenimiento"; +import { bd2Conexiones } from "./units/16-conexiones"; +import { bd2CrudInterfaz } from "./units/17-crud-interfaz"; +import { bd2NosqlModelo } from "./units/18-nosql-modelo"; +import { bd2MongodbCrud } from "./units/19-mongodb-crud"; +import { bd2Integrador } from "./units/20-integrador"; + // ===================================================================== -// Paquete de curso: Bases de datos (Base de Datos I, S4). +// Paquete de curso: Bases de datos (Base de Datos I, S4 + Base de Datos +// II, S5). // // Curso nuevo y lenguaje nuevo de plataforma: `sql`, perfil // `sql-sqlite3-wandbox` (ver TECHNICAL_CONTRACT del paquete de contenido @@ -20,8 +32,18 @@ import { bd1RespaldoIntegrador } from "./units/10-respaldo-integrador"; // portable compatible con SQLite; backup/restore real y la GUI de // escritorio que pide el programa oficial quedan como laboratorio local. // -// 10 unidades, una sola CurriculumSection semestral, las 10 nacen -// `published: true` — sin release gate para esta entrega. +// S5 (Base de Datos II, ver TECHNICAL_CONTRACT de `db2-ceti-content-pack`) +// extiende el MISMO Course con procedimientos, triggers/jobs, +// transacciones/ACID, usuarios/permisos, mantenimiento, conexión de +// aplicaciones, CRUD/vistas/informes, modelo documental, CRUD MongoDB y un +// integrador — sin agregar lenguaje, provider ni schema nuevos. Las +// capacidades que SQLite no soporta honestamente (CREATE PROCEDURE, +// Event Scheduler, GRANT/REVOKE, mantenimiento específico de MySQL, +// conexión C#↔MySQL y todo MongoDB) quedan como laboratorio local +// (`runnable:false` + `localOnlyNote`), nunca adaptadas artificialmente. +// +// 20 unidades en total (10 + 10), dos CurriculumSection semestrales; las +// 20 nacen `published: true` — sin release gate para esta entrega. // ===================================================================== export const basesDeDatos = defineCourse({ @@ -51,5 +73,22 @@ export const basesDeDatos = defineCourse({ bd1RespaldoIntegrador, ], }, + { + key: "s5-base-de-datos-2", + semester: 5, + subjectName: "Base de Datos II", + units: [ + bd2Procedimientos, + bd2TriggersJobs, + bd2Transacciones, + bd2UsuariosPermisos, + bd2Mantenimiento, + bd2Conexiones, + bd2CrudInterfaz, + bd2NosqlModelo, + bd2MongodbCrud, + bd2Integrador, + ], + }, ], }); diff --git a/prisma/content/courses/bases-de-datos/units/11-procedimientos/index.ts b/prisma/content/courses/bases-de-datos/units/11-procedimientos/index.ts new file mode 100644 index 0000000..b5a4a72 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/11-procedimientos/index.ts @@ -0,0 +1,16 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-por-que-procedimientos"; +import { leccion02 } from "./lessons/02-parametros-contrato"; +import { leccion03 } from "./lessons/03-create-procedure-mysql"; +import { leccion04 } from "./lessons/04-limites-procedimientos"; + +export const bd2Procedimientos = defineUnit({ + slug: "bd2-11-procedimientos", + title: "Procedimientos almacenados y modularización", + description: + "Diseña el contrato de un procedimiento almacenado y reconoce cuándo modularizar dentro del SGBD.", + icon: "🧱", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04], +}); diff --git a/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/01-por-que-procedimientos.ts b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/01-por-que-procedimientos.ts new file mode 100644 index 0000000..aaf3efc --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/01-por-que-procedimientos.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "por-que-procedimientos", + title: "Por qué modularizar dentro del SGBD", + description: "Entender el contrato de un procedimiento almacenado.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un procedimiento almacenado encapsula una operación nombrada dentro del SGBD. Puede recibir parámetros, ejecutar varias sentencias y concentrar reglas cerca de los datos. No es automáticamente mejor que lógica en la aplicación: su valor aparece cuando centralización, permisos o reutilización lo justifican.`, + }, + { + type: "matching", + pairs: [ + { left: "Operación repetida", right: "Candidata a encapsulación" }, + { left: "Parámetro", right: "Entrada" }, + { left: "Efecto", right: "Cambio observable" }, + { left: "CALL", right: "Invocación MySQL" }, + ], + explanation: "Primero entiende el contrato.", + }, + { + type: "quiz", + question: "¿Qué beneficio puede aportar un procedimiento?", + options: [ + "Evitar diseñar", + "Centralizar una operación reutilizable", + "Eliminar transacciones", + "Reemplazar permisos", + ], + correctIndex: 1, + explanation: "Modulariza una operación bien definida.", + }, + { + type: "fill_blank", + template: "procedimiento = nombre + {{0}} + operación + efecto", + blanks: [{ answer: "parámetros" }], + explanation: "El contrato debe ser explícito.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/02-parametros-contrato.ts b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/02-parametros-contrato.ts new file mode 100644 index 0000000..475550f --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/02-parametros-contrato.ts @@ -0,0 +1,39 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "parametros-contrato", + title: "Parámetros y pre/postcondiciones", + description: "Diseñar entradas y efectos antes de escribir sintaxis.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Antes de crear \`cerrar_ticket(id)\` define precondición, efecto y comportamiento si el ticket no existe. La definición del contrato importa más que memorizar delimitadores.`, + }, + { + type: "matching", + pairs: [ + { left: "IN", right: "Entrada" }, + { left: "OUT", right: "Salida" }, + { left: "INOUT", right: "Entrada/salida" }, + { left: "Precondición", right: "Debe cumplirse antes" }, + { left: "Postcondición", right: "Debe cumplirse después" }, + ], + explanation: "Parámetros son parte de la interfaz.", + }, + { + type: "quiz", + question: "¿Qué debe quedar claro primero?", + options: ["Editor", "Qué recibe y qué cambia", "Número de líneas", "Color"], + correctIndex: 1, + explanation: "Un contrato verificable evita operaciones opacas.", + }, + { + type: "fill_blank", + template: "precondición → CALL → {{0}}", + blanks: [{ answer: "postcondición" }], + explanation: "La postcondición describe el resultado.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/03-create-procedure-mysql.ts b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/03-create-procedure-mysql.ts new file mode 100644 index 0000000..8286d05 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/03-create-procedure-mysql.ts @@ -0,0 +1,50 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "create-procedure-mysql", + title: "CREATE PROCEDURE en MySQL", + description: "Leer un procedimiento real como laboratorio local.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `MySQL implementa procedimientos con sintaxis propia. SQLite no los soporta; por eso el ejemplo es local-only y no se adapta artificialmente.`, + }, + { + type: "code_example", + code: `DELIMITER // +CREATE PROCEDURE cerrar_ticket(IN p_id INT) +BEGIN + UPDATE ticket SET estado='CERRADO' WHERE id=p_id; +END// +DELIMITER ; +CALL cerrar_ticket(10);`, + explanation: "Define e invoca una operación.", + runnable: false, + localOnlyNote: "Requiere MySQL 8.x local.", + }, + { + type: "quiz", + question: "¿Por qué runnable:false?", + options: [ + "Seguridad", + "SQLite no soporta CREATE PROCEDURE", + "UPDATE no existe", + "MySQL no usa SQL", + ], + correctIndex: 1, + explanation: "No ejecutar sintaxis específica contra otro SGBD.", + }, + { + type: "matching", + pairs: [ + { left: "CREATE PROCEDURE", right: "Definir" }, + { left: "CALL", right: "Invocar" }, + { left: "IN p_id", right: "Entrada" }, + { left: "UPDATE", right: "Efecto" }, + ], + explanation: "Reconoce estructura.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/04-limites-procedimientos.ts b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/04-limites-procedimientos.ts new file mode 100644 index 0000000..0698940 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/11-procedimientos/lessons/04-limites-procedimientos.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "limites-procedimientos", + title: "Cuándo no usarlos", + description: "Reconocer acoplamiento y complejidad.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `La lógica propietaria dentro del SGBD reduce portabilidad y también necesita versionado, pruebas y migraciones. No conviertas cada query simple en un procedimiento.`, + }, + { + type: "matching", + pairs: [ + { left: "Regla compartida por varias apps", right: "Puede justificarlo" }, + { left: "SELECT simple", right: "Normalmente no" }, + { left: "Sintaxis propietaria", right: "Reduce portabilidad" }, + { left: "Migración", right: "Debe versionarlo" }, + ], + explanation: "Hay beneficios y costos.", + }, + { + type: "quiz", + question: "¿Cuál criterio es correcto?", + options: [ + "Todo procedimiento", + "Nunca procedimientos", + "Usarlos cuando el beneficio supera el acoplamiento", + "Sólo por requisito académico", + ], + correctIndex: 2, + explanation: "Es una decisión de diseño.", + }, + { + type: "fill_blank", + template: "beneficio > costo de {{0}}", + blanks: [{ answer: "acoplamiento" }], + explanation: "Evalúa el trade-off.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/index.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/index.ts new file mode 100644 index 0000000..099d4b9 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-evento-accion"; +import { leccion02 } from "./lessons/02-trigger-auditoria"; +import { leccion03 } from "./lessons/03-trigger-vs-restriccion"; +import { leccion04 } from "./lessons/04-jobs"; +import { leccion05 } from "./lessons/05-automatizacion-segura"; + +import { practice } from "./practice"; + +export const bd2TriggersJobs = defineUnit({ + slug: "bd2-12-triggers-jobs", + title: "Triggers y automatización programada", + description: + "Reacciona a eventos de datos con triggers, distingue jobs por tiempo y diseña automatización observable.", + icon: "⚡", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/01-evento-accion.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/01-evento-accion.ts new file mode 100644 index 0000000..11e7d14 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/01-evento-accion.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "evento-accion", + title: "Evento y acción automática", + description: "Entender trigger como reacción del SGBD.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un trigger ejecuta lógica cuando ocurre un evento sobre datos. Es útil para auditoría e invariantes que no encajan mejor en restricciones, pero puede ocultar efectos si no se documenta.`, + }, + { + type: "matching", + pairs: [ + { left: "INSERT", right: "Evento" }, + { left: "AFTER INSERT", right: "Momento" }, + { left: "NEW", right: "Fila nueva" }, + { left: "auditoria", right: "Efecto" }, + ], + explanation: "Un trigger tiene causa y efecto.", + }, + { + type: "quiz", + question: "¿Riesgo de abusar de triggers?", + options: [ + "No modifican nada", + "Efectos implícitos difíciles de rastrear", + "No existen", + "Siempre son lentos", + ], + correctIndex: 1, + explanation: "La automatización invisible complica diagnóstico.", + }, + { + type: "fill_blank", + template: "evento + momento + {{0}} = trigger", + blanks: [{ answer: "acción" }], + explanation: "La causalidad debe ser clara.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/02-trigger-auditoria.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/02-trigger-auditoria.ts new file mode 100644 index 0000000..da61e66 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/02-trigger-auditoria.ts @@ -0,0 +1,40 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "trigger-auditoria", + title: "Auditoría con trigger", + description: "Crear un trigger SQLite verificable.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `SQLite sí soporta triggers. Cada ticket insertado puede generar una entrada de auditoría.`, + }, + { + type: "code_example", + code: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); +CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); +CREATE TRIGGER audit_ticket AFTER INSERT ON ticket +BEGIN INSERT INTO auditoria VALUES(NEW.id,'INSERT'); END; +INSERT INTO ticket VALUES(7,'ABIERTO'); +SELECT ticket_id,accion FROM auditoria;`, + explanation: "El efecto ocurre automáticamente.", + runnable: true, + expectedOutput: `7|INSERT`, + }, + { + type: "quiz", + question: "¿Qué representa NEW.id?", + options: ["PK de fila nueva", "Id de auditoría", "Variable global", "Id previo"], + correctIndex: 0, + explanation: "NEW expone la fila insertada.", + }, + { + type: "fill_blank", + template: "AFTER INSERT ... NEW.{{0}}", + blanks: [{ answer: "id" }], + explanation: "Referencia valores nuevos.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/03-trigger-vs-restriccion.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/03-trigger-vs-restriccion.ts new file mode 100644 index 0000000..3531610 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/03-trigger-vs-restriccion.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "trigger-vs-restriccion", + title: "Trigger o restricción", + description: "Preferir reglas declarativas simples.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Antes de usar trigger, pregunta si \`CHECK\`, \`UNIQUE\`, \`NOT NULL\` o FK expresan mejor la regla. Usa trigger cuando la reacción realmente requiere lógica adicional.`, + }, + { + type: "matching", + pairs: [ + { left: "costo>=0", right: "CHECK" }, + { left: "folio único", right: "UNIQUE" }, + { left: "cliente existente", right: "FK" }, + { left: "auditar cambios", right: "Trigger" }, + ], + explanation: "Prefiere la herramienta más declarativa.", + }, + { + type: "quiz", + question: "¿Qué usar primero para costo no negativo?", + options: ["Trigger", "CHECK", "Job", "Procedure"], + correctIndex: 1, + explanation: "CHECK es directo.", + }, + { + type: "fill_blank", + template: "regla simple → {{0}} antes que trigger", + blanks: [{ answer: "restricción" }], + explanation: "Reduce lógica oculta.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/04-jobs.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/04-jobs.ts new file mode 100644 index 0000000..40f6ad9 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/04-jobs.ts @@ -0,0 +1,46 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "jobs", + title: "Jobs y Event Scheduler", + description: "Separar automatización por tiempo de automatización por evento.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un trigger responde a un cambio de datos; un job responde a calendario/tiempo. MySQL concreta jobs con Event Scheduler. SQLite no tiene un scheduler interno equivalente.`, + }, + { + type: "code_example", + code: `CREATE EVENT limpiar_sesiones +ON SCHEDULE EVERY 1 DAY +DO DELETE FROM sesion WHERE expira_en < NOW();`, + explanation: "Ejemplo MySQL de trabajo periódico.", + runnable: false, + localOnlyNote: "Requiere MySQL Event Scheduler local.", + }, + { + type: "quiz", + question: "¿Diferencia principal?", + options: [ + "Trigger usa tiempo", + "Trigger responde a datos; job a calendario", + "Son iguales", + "Job siempre DELETE", + ], + correctIndex: 1, + explanation: "El disparador causal es distinto.", + }, + { + type: "matching", + pairs: [ + { left: "Trigger", right: "Evento de datos" }, + { left: "Event/Job", right: "Tiempo" }, + { left: "Procedure", right: "Operación invocable" }, + { left: "Scheduler", right: "Planifica" }, + ], + explanation: "Mecanismos relacionados, no equivalentes.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/05-automatizacion-segura.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/05-automatizacion-segura.ts new file mode 100644 index 0000000..fcfda25 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/lessons/05-automatizacion-segura.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "automatizacion-segura", + title: "Automatización observable y repetible", + description: "Limitar alcance y registrar efectos.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una automatización debe limitar su alcance, registrar qué hizo y tolerar re-ejecuciones cuando sea razonable. Una tarea periódica amplifica cualquier error.`, + }, + { + type: "matching", + pairs: [ + { left: "WHERE preciso", right: "Alcance" }, + { left: "log", right: "Observabilidad" }, + { left: "re-ejecución segura", right: "Idempotencia" }, + { left: "backup", right: "Mitigación" }, + ], + explanation: "Automatizar no elimina responsabilidad.", + }, + { + type: "quiz", + question: "¿Qué es peor?", + options: [ + "Registrar", + "Job destructivo sin WHERE ni auditoría", + "Probar primero", + "Hacerlo repetible", + ], + correctIndex: 1, + explanation: "El error se repetiría automáticamente.", + }, + { + type: "fill_blank", + template: "automatización segura = alcance + observabilidad + {{0}}", + blanks: [{ answer: "repetibilidad" }], + explanation: "Debe poder auditarse.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/practice.ts b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/practice.ts new file mode 100644 index 0000000..a920290 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/12-triggers-jobs/practice.ts @@ -0,0 +1,110 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "bd2-trigger-insert", + title: "Auditar INSERT", + description: "Crea trigger de INSERT, inserta ticket 5 y muestra auditoría.", + prompt: "Crea trigger de INSERT, inserta ticket 5 y muestra auditoría.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE TRIGGER audit_ticket AFTER INSERT ON ticket BEGIN INSERT INTO auditoria VALUES(NEW.id,'INSERT'); END; INSERT INTO ticket VALUES(5,'ABIERTO'); SELECT ticket_id,accion FROM auditoria;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT);`, + expectedStdout: `5|INSERT`, + visible: true, + }, + { + // Fixture DISTINTO del visible (auditoria ya trae una fila previa): + // el pack original repetía el fixture visible sin cambios, lo que + // dejaba el test oculto sin poder discriminar una solución + // hardcodeada — ver tests/content/course-contract.test.ts, "un + // output hardcodeado no puede pasar un ejercicio que lee stdin". + description: "auditoría con entrada previa", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO auditoria VALUES(3,'INSERT');`, + expectedStdout: `3|INSERT +5|INSERT`, + visible: false, + }, + ], + }, + { + slug: "bd2-trigger-update", + title: "Auditar UPDATE", + description: "Registra OLD.estado>NEW.estado al cambiar id 1 a CERRADO.", + prompt: "Registra OLD.estado>NEW.estado al cambiar id 1 a CERRADO.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE TRIGGER audit_update AFTER UPDATE OF estado ON ticket BEGIN INSERT INTO auditoria VALUES(NEW.id,OLD.estado||'>'||NEW.estado); END; UPDATE ticket SET estado='CERRADO' WHERE id=1; SELECT ticket_id,accion FROM auditoria;`, + difficulty: "hard", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'ABIERTO');`, + expectedStdout: `1|ABIERTO>CERRADO`, + visible: true, + }, + { + description: "otro estado", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'EN_PROCESO');`, + expectedStdout: `1|EN_PROCESO>CERRADO`, + visible: false, + }, + ], + }, + { + slug: "bd2-trigger-delete", + title: "Auditar DELETE", + description: "Registra OLD.id al borrar ticket 2.", + prompt: "Registra OLD.id al borrar ticket 2.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE TRIGGER audit_delete AFTER DELETE ON ticket BEGIN INSERT INTO auditoria VALUES(OLD.id,'DELETE'); END; DELETE FROM ticket WHERE id=2; SELECT ticket_id,accion FROM auditoria;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(2,'CERRADO');`, + expectedStdout: `2|DELETE`, + visible: true, + }, + { + description: "otro estado", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(2,'ABIERTO');`, + expectedStdout: `2|DELETE`, + visible: false, + }, + ], + }, + { + slug: "bd2-trigger-contador", + title: "Contador automático", + description: "Incrementa metricas.total tras INSERT y devuelve total.", + prompt: "Incrementa metricas.total tras INSERT y devuelve total.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE TRIGGER inc_total AFTER INSERT ON ticket BEGIN UPDATE metricas SET total=total+1; END; INSERT INTO ticket VALUES(9,'ABIERTO'); SELECT total FROM metricas;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "parte de 2", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE metricas(total INTEGER); INSERT INTO metricas VALUES(2);`, + expectedStdout: `3`, + visible: true, + }, + { + description: "parte de 0", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE metricas(total INTEGER); INSERT INTO metricas VALUES(0);`, + expectedStdout: `1`, + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/index.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/index.ts new file mode 100644 index 0000000..7b3d733 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-unidad-trabajo"; +import { leccion02 } from "./lessons/02-acid"; +import { leccion03 } from "./lessons/03-commit"; +import { leccion04 } from "./lessons/04-rollback"; +import { leccion05 } from "./lessons/05-frontera-transaccional"; + +import { practice } from "./practice"; + +export const bd2Transacciones = defineUnit({ + slug: "bd2-13-transacciones", + title: "Transacciones y modelo ACID", + description: + "Agrupa cambios que deben ocurrir juntos, confirma o revierte con COMMIT/ROLLBACK y razona con ACID.", + icon: "🔒", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/01-unidad-trabajo.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/01-unidad-trabajo.ts new file mode 100644 index 0000000..1490053 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/01-unidad-trabajo.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "unidad-trabajo", + title: "Transacción como unidad de trabajo", + description: "Agrupar cambios que deben ocurrir juntos.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una transacción delimita una unidad lógica. Si transferir saldo requiere restar y sumar, ambas operaciones deben confirmarse juntas o revertirse juntas.`, + }, + { + type: "matching", + pairs: [ + { left: "BEGIN", right: "Iniciar" }, + { left: "COMMIT", right: "Confirmar" }, + { left: "ROLLBACK", right: "Revertir" }, + { left: "unidad lógica", right: "Cambios inseparables" }, + ], + explanation: "Ciclo básico.", + }, + { + type: "quiz", + question: "¿Cuándo conviene?", + options: [ + "Varias operaciones preservan una invariante juntas", + "Sólo SELECT", + "Sólo triggers", + "Nunca", + ], + correctIndex: 0, + explanation: "La atomicidad protege operaciones compuestas.", + }, + { + type: "fill_blank", + template: "BEGIN → cambios → {{0}} o {{1}}", + blanks: [{ answer: "COMMIT" }, { answer: "ROLLBACK" }], + explanation: "Hay dos finales principales.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/02-acid.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/02-acid.ts new file mode 100644 index 0000000..5162b70 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/02-acid.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "acid", + title: "ACID con escenarios", + description: "Relacionar cada propiedad con un riesgo.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Atomicidad = todo/nada; Consistencia = invariantes válidas; Aislamiento = interacción concurrente controlada; Durabilidad = cambios confirmados persisten según garantías del SGBD.`, + }, + { + type: "matching", + pairs: [ + { left: "Atomicidad", right: "No media transferencia" }, + { left: "Consistencia", right: "Reglas válidas" }, + { left: "Aislamiento", right: "Concurrencia controlada" }, + { left: "Durabilidad", right: "Commit persistente" }, + ], + explanation: "No memorices letras sin escenario.", + }, + { + type: "quiz", + question: "Si se resta y falla antes de sumar, ¿qué propiedad importa?", + options: ["Durabilidad", "Atomicidad", "Proyección", "Normalización"], + correctIndex: 1, + explanation: "Todo o nada.", + }, + { + type: "fill_blank", + template: "A C I D = Atomicidad, Consistencia, {{0}}, Durabilidad", + blanks: [{ answer: "Aislamiento" }], + explanation: "Propiedades coordinadas.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/03-commit.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/03-commit.ts new file mode 100644 index 0000000..654ee8b --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/03-commit.ts @@ -0,0 +1,42 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "commit", + title: "COMMIT", + description: "Confirmar una unidad correcta.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `COMMIT hace definitivos los cambios de la transacción para las operaciones posteriores.`, + }, + { + type: "code_example", + code: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); +INSERT INTO cuenta VALUES(1,100),(2,50); +BEGIN; +UPDATE cuenta SET saldo=saldo-30 WHERE id=1; +UPDATE cuenta SET saldo=saldo+30 WHERE id=2; +COMMIT; +SELECT id,saldo FROM cuenta ORDER BY id;`, + explanation: "La transferencia se confirma.", + runnable: true, + expectedOutput: `1|70 +2|80`, + }, + { + type: "quiz", + question: "Total antes/después?", + options: ["150/150", "150/120", "100/80", "depende"], + correctIndex: 0, + explanation: "Conserva el total.", + }, + { + type: "fill_blank", + template: "cambios correctos → {{0}}", + blanks: [{ answer: "COMMIT" }], + explanation: "Confirmar al final.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/04-rollback.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/04-rollback.ts new file mode 100644 index 0000000..3f62741 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/04-rollback.ts @@ -0,0 +1,40 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "rollback", + title: "ROLLBACK", + description: "Volver al estado previo ante fallo.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `ROLLBACK revierte cambios no confirmados de la transacción.`, + }, + { + type: "code_example", + code: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); +INSERT INTO cuenta VALUES(1,100); +BEGIN; +UPDATE cuenta SET saldo=saldo-30 WHERE id=1; +ROLLBACK; +SELECT saldo FROM cuenta WHERE id=1;`, + explanation: "La modificación desaparece.", + runnable: true, + expectedOutput: `100`, + }, + { + type: "quiz", + question: "¿Qué descarta cambios pendientes?", + options: ["COMMIT", "ROLLBACK", "GRANT", "TRIGGER"], + correctIndex: 1, + explanation: "ROLLBACK aborta.", + }, + { + type: "fill_blank", + template: "error detectado → {{0}}", + blanks: [{ answer: "ROLLBACK" }], + explanation: "Protege estado.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/05-frontera-transaccional.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/05-frontera-transaccional.ts new file mode 100644 index 0000000..0914cf3 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/lessons/05-frontera-transaccional.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "frontera-transaccional", + title: "Elegir la frontera", + description: "Diseñar qué cambios pertenecen a una transacción.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Caso: cerrar ticket y registrar pago deben ocurrir juntos si el negocio exige que todo cierre facturable tenga pago. La regla de negocio define la frontera.`, + }, + { + type: "matching", + pairs: [ + { left: "Cerrar ticket", right: "Cambio 1" }, + { left: "Registrar pago", right: "Cambio 2" }, + { left: "Invariante conjunta", right: "Misma transacción" }, + { left: "Reporte", right: "Lectura posterior" }, + ], + explanation: "Agrupa lo inseparable.", + }, + { + type: "quiz", + question: "¿Un ROLLBACK revierte un COMMIT anterior?", + options: ["Sí", "No", "Sólo SQLite", "Sólo Mongo"], + correctIndex: 1, + explanation: "Un commit previo ya terminó esa transacción.", + }, + { + type: "fill_blank", + template: "frontera = cambios que preservan una {{0}} conjunta", + blanks: [{ answer: "invariante" }], + explanation: "Diseña desde la regla.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/13-transacciones/practice.ts b/prisma/content/courses/bases-de-datos/units/13-transacciones/practice.ts new file mode 100644 index 0000000..03a6945 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/13-transacciones/practice.ts @@ -0,0 +1,131 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "bd2-tx-commit", + title: "Transferencia", + description: "Mueve 40 de cuenta 1 a 2 y COMMIT.", + prompt: "Mueve 40 de cuenta 1 a 2 y COMMIT.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; UPDATE cuenta SET saldo=saldo-40 WHERE id=1; UPDATE cuenta SET saldo=saldo+40 WHERE id=2; COMMIT; SELECT id,saldo FROM cuenta ORDER BY id;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,100),(2,20);`, + expectedStdout: `1|60 +2|60`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,200),(2,0);`, + expectedStdout: `1|160 +2|40`, + visible: false, + }, + ], + }, + { + slug: "bd2-tx-rollback", + title: "Revertir", + description: "Resta 25 y ROLLBACK.", + prompt: "Resta 25 y ROLLBACK.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; UPDATE cuenta SET saldo=saldo-25 WHERE id=1; ROLLBACK; SELECT saldo FROM cuenta WHERE id=1;`, + difficulty: "easy", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,100);`, + expectedStdout: `100`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,77);`, + expectedStdout: `77`, + visible: false, + }, + ], + }, + { + slug: "bd2-tx-dos-tablas", + title: "Ticket y pago", + description: "Cierra ticket e inserta pago en la misma transacción.", + prompt: "Cierra ticket e inserta pago en la misma transacción.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; UPDATE ticket SET estado='CERRADO' WHERE id=1; INSERT INTO pago(ticket_id,monto) VALUES(1,300); COMMIT; SELECT ticket.estado,pago.monto FROM ticket JOIN pago ON pago.ticket_id=ticket.id WHERE ticket.id=1;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER); INSERT INTO ticket VALUES(1,'ABIERTO');`, + expectedStdout: `CERRADO|300`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER); INSERT INTO ticket VALUES(1,'EN_PROCESO');`, + expectedStdout: `CERRADO|300`, + visible: false, + }, + ], + }, + { + slug: "bd2-tx-revertir-dos", + title: "Revertir dos tablas", + description: "Haz cambios en ticket/pago y ROLLBACK.", + prompt: "Haz cambios en ticket/pago y ROLLBACK.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; UPDATE ticket SET estado='CERRADO' WHERE id=1; INSERT INTO pago VALUES(1,300); ROLLBACK; SELECT estado,(SELECT COUNT(*) FROM pago) FROM ticket WHERE id=1;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER); INSERT INTO ticket VALUES(1,'ABIERTO');`, + expectedStdout: `ABIERTO|0`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER); INSERT INTO ticket VALUES(1,'EN_PROCESO');`, + expectedStdout: `EN_PROCESO|0`, + visible: false, + }, + ], + }, + { + slug: "bd2-tx-invariante", + title: "Conservar total", + description: "Transfiere 10 y devuelve SUM(saldo).", + prompt: "Transfiere 10 y devuelve SUM(saldo).", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; UPDATE cuenta SET saldo=saldo-10 WHERE id=1; UPDATE cuenta SET saldo=saldo+10 WHERE id=2; COMMIT; SELECT SUM(saldo) FROM cuenta;`, + difficulty: "easy", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,50),(2,50);`, + expectedStdout: `100`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE cuenta(id INTEGER PRIMARY KEY,saldo INTEGER); INSERT INTO cuenta VALUES(1,80),(2,25);`, + expectedStdout: `105`, + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/index.ts b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/index.ts new file mode 100644 index 0000000..6cbe505 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/index.ts @@ -0,0 +1,16 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-identidad-autorizacion"; +import { leccion02 } from "./lessons/02-minimo-privilegio"; +import { leccion03 } from "./lessons/03-grant-revoke"; +import { leccion04 } from "./lessons/04-matriz-permisos"; + +export const bd2UsuariosPermisos = defineUnit({ + slug: "bd2-14-usuarios-permisos", + title: "Usuarios, roles, privilegios y permisos", + description: + "Separa identidad de autorización, aplica mínimo privilegio y traduce responsabilidades a una matriz de permisos.", + icon: "🛡️", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04], +}); diff --git a/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/01-identidad-autorizacion.ts b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/01-identidad-autorizacion.ts new file mode 100644 index 0000000..3b22535 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/01-identidad-autorizacion.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "identidad-autorizacion", + title: "Identidad y autorización", + description: "Separar quién es una cuenta de qué puede hacer.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un usuario de base de datos es una identidad del SGBD. Los privilegios determinan operaciones permitidas. El login de la aplicación y el usuario del SGBD pueden ser capas distintas.`, + }, + { + type: "matching", + pairs: [ + { left: "Usuario", right: "Identidad SGBD" }, + { left: "Privilegio", right: "Operación permitida" }, + { left: "Rol", right: "Conjunto de privilegios" }, + { left: "Autorización", right: "Permitir/denegar" }, + ], + explanation: "Separa conceptos.", + }, + { + type: "quiz", + question: "¿Qué expresa un rol?", + options: ["Contraseña", "Conjunto reutilizable de permisos", "Tabla", "Transacción"], + correctIndex: 1, + explanation: "Agrupa privilegios.", + }, + { + type: "fill_blank", + template: "usuario → rol → {{0}}", + blanks: [{ answer: "privilegios" }], + explanation: "Modelo común.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/02-minimo-privilegio.ts b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/02-minimo-privilegio.ts new file mode 100644 index 0000000..813880f --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/02-minimo-privilegio.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "minimo-privilegio", + title: "Mínimo privilegio", + description: "Otorgar sólo lo necesario.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una cuenta de reportes no necesita DROP TABLE. Una app CRUD no necesita privilegios administrativos. Menos permisos reducen daño potencial.`, + }, + { + type: "matching", + pairs: [ + { left: "report_reader", right: "SELECT" }, + { left: "app_writer", right: "CRUD necesario" }, + { left: "migrador", right: "DDL controlado" }, + { left: "admin", right: "Administración" }, + ], + explanation: "Permiso sigue responsabilidad.", + }, + { + type: "quiz", + question: "¿Qué cuenta debe ser especialmente limitada?", + options: ["App pública", "DBA", "Migrador", "Todas root"], + correctIndex: 0, + explanation: "Es una superficie expuesta.", + }, + { + type: "fill_blank", + template: "responsabilidad mínima → permisos {{0}}", + blanks: [{ answer: "mínimos" }], + explanation: "No empezar con ALL.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/03-grant-revoke.ts b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/03-grant-revoke.ts new file mode 100644 index 0000000..2db49c0 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/03-grant-revoke.ts @@ -0,0 +1,47 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "grant-revoke", + title: "GRANT y REVOKE en MySQL", + description: "Leer sintaxis real como laboratorio local.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `MySQL ofrece usuarios, roles, GRANT y REVOKE. SQLite no implementa este modelo de seguridad, por lo que el ejemplo no es ejecutable en plataforma.`, + }, + { + type: "code_example", + code: `CREATE ROLE 'report_reader'; +GRANT SELECT ON soporte.* TO 'report_reader'; +GRANT 'report_reader' TO 'analista'@'localhost'; +REVOKE INSERT ON soporte.* FROM 'analista'@'localhost';`, + explanation: "Concede/revoca privilegios.", + runnable: false, + localOnlyNote: "Requiere MySQL 8.x local y privilegios administrativos.", + }, + { + type: "quiz", + question: "¿Por qué local-only?", + options: [ + "SELECT no existe", + "SQLite no implementa usuarios/GRANT como MySQL", + "Es NoSQL", + "Sólo UI", + ], + correctIndex: 1, + explanation: "Capacidad específica.", + }, + { + type: "matching", + pairs: [ + { left: "GRANT", right: "Conceder" }, + { left: "REVOKE", right: "Retirar" }, + { left: "ROLE", right: "Agrupar" }, + { left: "ALL PRIVILEGES", right: "Evitar por defecto" }, + ], + explanation: "Administra explícitamente.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/04-matriz-permisos.ts b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/04-matriz-permisos.ts new file mode 100644 index 0000000..e9a34da --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/14-usuarios-permisos/lessons/04-matriz-permisos.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "matriz-permisos", + title: "Matriz de permisos", + description: "Traducir responsabilidades a privilegios revisables.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Antes de crear usuarios, documenta una matriz: operador CRUD limitado, analista lectura, migrador DDL, administrador mantenimiento.`, + }, + { + type: "matching", + pairs: [ + { left: "Operador", right: "CRUD limitado" }, + { left: "Analista", right: "SELECT" }, + { left: "Migrador", right: "DDL" }, + { left: "Admin", right: "Administración" }, + ], + explanation: "Hace auditable el diseño.", + }, + { + type: "quiz", + question: "¿Qué es más auditable?", + options: [ + "Todos root", + "Roles nombrados con permisos explícitos", + "Contraseña compartida", + "Permisos improvisados", + ], + correctIndex: 1, + explanation: "Permite revisión.", + }, + { + type: "fill_blank", + template: "actor → responsabilidad → {{0}} → privilegios", + blanks: [{ answer: "rol" }], + explanation: "La autorización nace del trabajo real.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/15-mantenimiento/index.ts b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/index.ts new file mode 100644 index 0000000..d96745d --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/index.ts @@ -0,0 +1,16 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-por-que-mantener"; +import { leccion02 } from "./lessons/02-fragmentacion"; +import { leccion03 } from "./lessons/03-analizar-mysql"; +import { leccion04 } from "./lessons/04-plan-mantenimiento"; + +export const bd2Mantenimiento = defineUnit({ + slug: "bd2-15-mantenimiento", + title: "Mantenimiento y fragmentación", + description: + "Relaciona crecimiento y cambios con mantenimiento del SGBD y diseña un plan basado en evidencia, no en superstición.", + icon: "🧰", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04], +}); diff --git a/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/01-por-que-mantener.ts b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/01-por-que-mantener.ts new file mode 100644 index 0000000..6d7ec82 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/01-por-que-mantener.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "por-que-mantener", + title: "Por qué mantener", + description: "Relacionar crecimiento/cambios con mantenimiento del SGBD.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Inserciones, updates y deletes cambian estructuras físicas. Según el motor pueden requerirse estadísticas, limpieza o reorganización. Mantenimiento debe responder a medición, no superstición.`, + }, + { + type: "matching", + pairs: [ + { left: "crecimiento", right: "Más espacio" }, + { left: "borrados", right: "Espacio reutilizable/fragmentación según motor" }, + { left: "estadísticas", right: "Ayudan al optimizador" }, + { left: "mantenimiento", right: "Responde a evidencia" }, + ], + explanation: "No hay comando universal.", + }, + { + type: "quiz", + question: "¿Antes de una operación destructiva?", + options: ["Nada", "Backup/plan y medición", "DROP", "Cambiar lenguaje"], + correctIndex: 1, + explanation: "Necesitas reversibilidad.", + }, + { + type: "fill_blank", + template: "medir → decidir → respaldar → {{0}} → verificar", + blanks: [{ answer: "mantener" }], + explanation: "Proceso controlado.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/02-fragmentacion.ts b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/02-fragmentacion.ts new file mode 100644 index 0000000..b9cac70 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/02-fragmentacion.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "fragmentacion", + title: "Concepto de fragmentación", + description: "Entender que la definición física depende del SGBD.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Fragmentación puede referirse a espacio no aprovechado u organización interna. Los datos pueden seguir lógicamente correctos. El diagnóstico y la corrección dependen del motor.`, + }, + { + type: "matching", + pairs: [ + { left: "Integridad", right: "Corrección lógica" }, + { left: "Fragmentación", right: "Organización física" }, + { left: "Síntoma", right: "Espacio/rendimiento" }, + { left: "Acción", right: "Específica del motor" }, + ], + explanation: "No confundir conceptos.", + }, + { + type: "quiz", + question: "¿Fragmentación implica corrupción?", + options: ["Sí", "No", "Sólo Mongo", "Sólo triggers"], + correctIndex: 1, + explanation: "Puede ser un problema físico sin pérdida lógica.", + }, + { + type: "fill_blank", + template: "fragmentación = problema de organización {{0}}", + blanks: [{ answer: "física" }], + explanation: "No necesariamente integridad.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/03-analizar-mysql.ts b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/03-analizar-mysql.ts new file mode 100644 index 0000000..9d9c19a --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/03-analizar-mysql.ts @@ -0,0 +1,40 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "analizar-mysql", + title: "Analizar mantenimiento en MySQL", + description: "Conocer herramientas locales sin fingir portabilidad.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `MySQL expone información y comandos como \`ANALYZE TABLE\` y \`OPTIMIZE TABLE\`. Su impacto/semántica dependen del engine y versión, por lo que se practican localmente con medición antes/después.`, + }, + { + type: "code_example", + code: `ANALYZE TABLE ticket; +OPTIMIZE TABLE ticket;`, + explanation: "Ejemplos administrativos MySQL.", + runnable: false, + localOnlyNote: "Requiere MySQL local; medir antes/después y respaldar según riesgo.", + }, + { + type: "quiz", + question: "¿Qué error evitar?", + options: ["Medir", "Ejecutar OPTIMIZE como ritual universal", "Documentar", "Verificar"], + correctIndex: 1, + explanation: "La acción debe responder a un síntoma.", + }, + { + type: "matching", + pairs: [ + { left: "ANALYZE TABLE", right: "Actualizar/analizar estadísticas según motor" }, + { left: "OPTIMIZE TABLE", right: "Reorganización específica" }, + { left: "backup", right: "Mitigación" }, + { left: "medición", right: "Justificación" }, + ], + explanation: "Administra con evidencia.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/04-plan-mantenimiento.ts b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/04-plan-mantenimiento.ts new file mode 100644 index 0000000..468cebe --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/15-mantenimiento/lessons/04-plan-mantenimiento.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "plan-mantenimiento", + title: "Plan de mantenimiento", + description: "Diseñar tareas, frecuencia y evidencia.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un plan mínimo nombra qué se observa, umbral/condición, tarea, frecuencia, riesgo, respaldo y verificación. No conviertas mantenimiento en cronograma ciego.`, + }, + { + type: "matching", + pairs: [ + { left: "Métrica", right: "Qué observas" }, + { left: "Umbral", right: "Cuándo actuar" }, + { left: "Acción", right: "Qué haces" }, + { left: "Verificación", right: "Cómo sabes que ayudó" }, + ], + explanation: "El plan es falsable.", + }, + { + type: "quiz", + question: "¿Cuál plan es mejor?", + options: [ + "OPTIMIZE diario siempre", + "Medir, actuar por criterio y verificar", + "Nunca tocar", + "Recrear DB", + ], + correctIndex: 1, + explanation: "Evita mantenimiento sin evidencia.", + }, + { + type: "fill_blank", + template: "métrica + criterio + acción + {{0}}", + blanks: [{ answer: "verificación" }], + explanation: "Cierra el ciclo.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/index.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/index.ts new file mode 100644 index 0000000..55f2f0f --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/index.ts @@ -0,0 +1,17 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-capas-conexion"; +import { leccion02 } from "./lessons/02-especifica-generica"; +import { leccion03 } from "./lessons/03-ciclo-conexion"; +import { leccion04 } from "./lessons/04-parametros"; +import { leccion05 } from "./lessons/05-conexion-local"; + +export const bd2Conexiones = defineUnit({ + slug: "bd2-16-conexiones", + title: "Conectar aplicaciones con bases de datos", + description: + "Entiende las capas app-driver-SGBD, gestiona el ciclo de vida de una conexión y usa consultas parametrizadas.", + icon: "🔌", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/01-capas-conexion.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/01-capas-conexion.ts new file mode 100644 index 0000000..ff9df32 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/01-capas-conexion.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "capas-conexion", + title: "Aplicación, driver y SGBD", + description: "Entender las capas de una conexión.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `La aplicación no "habla SQL por magia". Usa un driver/proveedor que implementa un protocolo para conectarse al SGBD. La cadena de conexión describe destino y credenciales/configuración.`, + }, + { + type: "matching", + pairs: [ + { left: "Aplicación", right: "Lógica/UI" }, + { left: "Driver", right: "Adaptador/protocolo" }, + { left: "Connection string", right: "Destino/configuración" }, + { left: "SGBD", right: "Ejecuta operaciones" }, + ], + explanation: "Separa capas.", + }, + { + type: "quiz", + question: "¿Qué componente traduce la API de la app al protocolo del SGBD?", + options: ["Driver", "Tabla", "Trigger", "View"], + correctIndex: 0, + explanation: "El driver implementa la conexión.", + }, + { + type: "fill_blank", + template: "app → {{0}} → SGBD", + blanks: [{ answer: "driver" }], + explanation: "Flujo conceptual.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/02-especifica-generica.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/02-especifica-generica.ts new file mode 100644 index 0000000..da97a53 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/02-especifica-generica.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "especifica-generica", + title: "Conexiones específicas y genéricas", + description: "Distinguir provider nativo de abstracciones estándar.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una conexión específica aprovecha un proveedor concreto; una genérica usa una interfaz/estándar común (por ejemplo ODBC/ADO.NET abstraído), con trade-offs de portabilidad y capacidades.`, + }, + { + type: "matching", + pairs: [ + { left: "Provider específico", right: "Capacidades concretas" }, + { left: "Interfaz genérica", right: "Portabilidad conceptual" }, + { left: "ODBC", right: "Ejemplo de capa genérica" }, + { left: "trade-off", right: "Portabilidad vs features" }, + ], + explanation: "No hay una opción universal.", + }, + { + type: "quiz", + question: "¿Qué suele ganar una conexión específica?", + options: [ + "Cero dependencias", + "Acceso directo a capacidades del motor", + "Portabilidad total", + "No requiere credenciales", + ], + correctIndex: 1, + explanation: "Puede exponer features propias.", + }, + { + type: "fill_blank", + template: "genérica favorece {{0}}; específica favorece capacidades concretas", + blanks: [{ answer: "portabilidad" }], + explanation: "Trade-off.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/03-ciclo-conexion.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/03-ciclo-conexion.ts new file mode 100644 index 0000000..662838c --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/03-ciclo-conexion.ts @@ -0,0 +1,44 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "ciclo-conexion", + title: "Abrir, usar, cerrar", + description: "Gestionar el ciclo de vida de la conexión.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una conexión es un recurso. Debe abrirse cuando se necesita, usarse de forma controlada y liberarse aun ante error. Pools y detalles de producción quedan fuera del alcance, pero el principio de ciclo de vida sí importa.`, + }, + { + type: "code_completion", + lines: [ + "Construir configuración segura", + "Abrir conexión", + "Ejecutar comando parametrizado", + "Procesar resultado", + "Cerrar/liberar", + ], + explanation: "El recurso debe cerrarse incluso ante fallo.", + }, + { + type: "quiz", + question: "¿Qué debe ocurrir si falla la query?", + options: [ + "Dejar conexión abierta", + "Liberar recursos en ruta de error", + "Reintentar infinito", + "Mostrar password", + ], + correctIndex: 1, + explanation: "Evita fugas y exposición.", + }, + { + type: "fill_blank", + template: "abrir → usar → {{0}}", + blanks: [{ answer: "cerrar" }], + explanation: "Ciclo básico.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/04-parametros.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/04-parametros.ts new file mode 100644 index 0000000..7855f79 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/04-parametros.ts @@ -0,0 +1,50 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "parametros", + title: "Consultas parametrizadas", + description: "Evitar concatenar entrada del usuario dentro de SQL.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `La entrada del usuario no debe convertirse en sintaxis SQL mediante concatenación. Los parámetros separan datos de código y son la defensa básica contra inyección SQL.`, + }, + { + type: "code_example", + code: `// C# / MySQL — laboratorio local +using var cmd = connection.CreateCommand(); +cmd.CommandText = "SELECT id,estado FROM ticket WHERE id=@id"; +var p = cmd.CreateParameter(); +p.ParameterName = "@id"; +p.Value = id; +cmd.Parameters.Add(p);`, + explanation: "El valor viaja como parámetro, no como fragmento SQL.", + runnable: false, + localOnlyNote: "Ejemplo C# local: requiere provider real configurado.", + }, + { + type: "quiz", + question: "¿Qué patrón es inseguro?", + options: [ + "CommandText = '...'+entradaUsuario", + "Parámetro @id", + "Validar tipos", + "Mínimo privilegio", + ], + correctIndex: 0, + explanation: "Concatenar permite que datos alteren sintaxis.", + }, + { + type: "matching", + pairs: [ + { left: "SQL", right: "Código" }, + { left: "parámetro", right: "Dato" }, + { left: "concatenación", right: "Mezcla peligrosa" }, + { left: "mínimo privilegio", right: "Reduce impacto" }, + ], + explanation: "Separa código y datos.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/05-conexion-local.ts b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/05-conexion-local.ts new file mode 100644 index 0000000..7562f10 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/16-conexiones/lessons/05-conexion-local.ts @@ -0,0 +1,45 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "conexion-local", + title: "Laboratorio de conexión real", + description: "Conectar una app de consola a MySQL y comprobar CRUD mínimo.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Laboratorio local: crear una DB de prueba, un usuario limitado, configurar un provider C# compatible y ejecutar SELECT parametrizado. Las credenciales nunca se commitean.`, + }, + { + type: "code_completion", + lines: [ + "Crear DB/usuario de laboratorio", + "Guardar credenciales fuera del repo", + "Abrir conexión", + "Ejecutar SELECT parametrizado", + "Cerrar conexión", + "Guardar evidencia", + ], + explanation: "Secuencia reproducible.", + }, + { + type: "quiz", + question: "¿Dónde NO guardar password?", + options: [ + "Variable de entorno/local secret", + "Archivo .env ignorado", + "Código fuente versionado", + "Secret manager", + ], + correctIndex: 2, + explanation: "No entra al historial.", + }, + { + type: "fill_blank", + template: "credencial → configuración {{0}}, no código", + blanks: [{ answer: "externa" }], + explanation: "Separa secreto de fuente.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/index.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/index.ts new file mode 100644 index 0000000..6132315 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-crud-capas"; +import { leccion02 } from "./lessons/02-create-read"; +import { leccion03 } from "./lessons/03-update-delete"; +import { leccion04 } from "./lessons/04-views"; +import { leccion05 } from "./lessons/05-informes"; + +import { practice } from "./practice"; + +export const bd2CrudInterfaz = defineUnit({ + slug: "bd2-17-crud-interfaz", + title: "CRUD, vistas e informes desde una aplicación", + description: + "Separa UI, lógica y acceso a datos; implementa Create/Read/Update/Delete y usa vistas para informes.", + icon: "🖥️", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/01-crud-capas.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/01-crud-capas.ts new file mode 100644 index 0000000..036391b --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/01-crud-capas.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "crud-capas", + title: "CRUD no es pegar SQL en botones", + description: "Separar UI, lógica y acceso a datos.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una interfaz CRUD debe separar captura/presentación de datos, reglas de aplicación y acceso al SGBD. Un botón no debería construir SQL concatenando strings.`, + }, + { + type: "matching", + pairs: [ + { left: "Formulario", right: "Entrada/salida UI" }, + { left: "Servicio", right: "Reglas" }, + { left: "Repositorio/DAO", right: "Acceso a datos" }, + { left: "SGBD", right: "Persistencia" }, + ], + explanation: "Separar responsabilidades facilita pruebas.", + }, + { + type: "quiz", + question: "¿Dónde debe vivir SQL de persistencia?", + options: [ + "Dentro del texto del botón", + "Capa de acceso a datos", + "En etiquetas UI", + "En CSS", + ], + correctIndex: 1, + explanation: "Evita acoplar UI a persistencia.", + }, + { + type: "fill_blank", + template: "UI → lógica → {{0}} → DB", + blanks: [{ answer: "acceso a datos" }], + explanation: "Capas mínimas.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/02-create-read.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/02-create-read.ts new file mode 100644 index 0000000..0e433af --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/02-create-read.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "create-read", + title: "Create y Read", + description: "Diseñar alta/consulta con parámetros y validación.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `CREATE valida datos, usa INSERT parametrizado y confirma resultado. READ recupera sólo columnas necesarias y maneja "no encontrado" explícitamente.`, + }, + { + type: "matching", + pairs: [ + { left: "Create", right: "INSERT" }, + { left: "Read", right: "SELECT" }, + { left: "input usuario", right: "Parámetro" }, + { left: "no encontrado", right: "Estado esperado, no crash" }, + ], + explanation: "CRUD mapea a operaciones concretas.", + }, + { + type: "quiz", + question: "¿Qué evita hardcodear salida?", + options: ["Query con parámetros/fixtures", "SELECT fijo sin datos", "Captura", "Comentario"], + correctIndex: 0, + explanation: "Los tests deben variar datos.", + }, + { + type: "fill_blank", + template: "Create={{0}}, Read={{1}}", + blanks: [{ answer: "INSERT" }, { answer: "SELECT" }], + explanation: "Operaciones básicas.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/03-update-delete.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/03-update-delete.ts new file mode 100644 index 0000000..19b40a4 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/03-update-delete.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "update-delete", + title: "Update y Delete con confirmación", + description: "Evitar cambios accidentales por falta de identidad/alcance.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `UPDATE y DELETE deben operar por identidad estable y verificar cuántas filas cambiaron. La UI debería confirmar acciones destructivas apropiadas y reportar si el registro ya no existe.`, + }, + { + type: "matching", + pairs: [ + { left: "UPDATE ... WHERE id=@id", right: "Cambio acotado" }, + { left: "DELETE ... WHERE id=@id", right: "Borrado acotado" }, + { left: "sin WHERE", right: "Riesgo masivo" }, + { left: "rows affected", right: "Evidencia de efecto" }, + ], + explanation: "El alcance es parte del contrato.", + }, + { + type: "quiz", + question: "¿Qué señal indica posible conflicto/no encontrado?", + options: ["0 filas afectadas", "1 fila", "Commit", "SELECT"], + correctIndex: 0, + explanation: "No se cambió el objetivo esperado.", + }, + { + type: "fill_blank", + template: "UPDATE/DELETE + {{0}} estable", + blanks: [{ answer: "id" }], + explanation: "Identifica la fila.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/04-views.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/04-views.ts new file mode 100644 index 0000000..55d5c2e --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/04-views.ts @@ -0,0 +1,42 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "views", + title: "Vistas como interfaz de lectura", + description: "Usar CREATE VIEW para encapsular una consulta de reporte.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Una vista presenta una consulta con nombre. Puede simplificar reportes y ocultar detalles de joins. No debe usarse para esconder un mal modelo.`, + }, + { + type: "code_example", + code: `CREATE TABLE cliente(id INTEGER,nombre TEXT); +CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT); +CREATE VIEW tickets_cliente AS + SELECT ticket.id,cliente.nombre,ticket.estado + FROM ticket JOIN cliente ON cliente.id=ticket.cliente_id; +INSERT INTO cliente VALUES(1,'Ana'); +INSERT INTO ticket VALUES(10,1,'ABIERTO'); +SELECT * FROM tickets_cliente;`, + explanation: "SQLite permite practicar vistas.", + runnable: true, + expectedOutput: `10|Ana|ABIERTO`, + }, + { + type: "quiz", + question: "¿Qué encapsula una vista?", + options: ["Una consulta nombrada", "Un usuario", "Un job", "Un backup"], + correctIndex: 0, + explanation: "Es una interfaz de lectura SQL.", + }, + { + type: "fill_blank", + template: "CREATE {{0}} nombre AS SELECT ...", + blanks: [{ answer: "VIEW" }], + explanation: "Sintaxis base.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/05-informes.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/05-informes.ts new file mode 100644 index 0000000..ec19321 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/lessons/05-informes.ts @@ -0,0 +1,44 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "informes", + title: "Informes que responden decisiones", + description: "Convertir consultas en salidas útiles de la aplicación.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un informe no es "mostrar toda la tabla". Define una pregunta, columnas, agregados, filtros y orden. La UI puede presentar el resultado en tabla/gráfica, pero el SQL debe producir información correcta.`, + }, + { + type: "code_completion", + lines: [ + "Definir pregunta", + "Diseñar query", + "Probar con datos variados", + "Presentar resultado", + "Verificar interpretación", + ], + explanation: "Primero la pregunta, luego la pantalla.", + }, + { + type: "quiz", + question: "¿Cuál es mejor reporte?", + options: [ + "SELECT *", + "Tickets abiertos por técnico con conteo y orden", + "Todos los campos siempre", + "Captura manual", + ], + correctIndex: 1, + explanation: "Responde una necesidad.", + }, + { + type: "fill_blank", + template: "pregunta → query → {{0}} → decisión", + blanks: [{ answer: "informe" }], + explanation: "La presentación tiene propósito.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/practice.ts b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/practice.ts new file mode 100644 index 0000000..27bf6da --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/17-crud-interfaz/practice.ts @@ -0,0 +1,83 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "bd2-view-reporte", + title: "Vista de tickets", + description: "Crea vista ticket_cliente y devuelve id|cliente|estado.", + prompt: "Crea vista `ticket_cliente` y devuelve `id|cliente|estado`.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE VIEW ticket_cliente AS SELECT ticket.id,cliente.nombre,ticket.estado FROM ticket JOIN cliente ON cliente.id=ticket.cliente_id; SELECT id,nombre,estado FROM ticket_cliente ORDER BY id;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cliente(id INTEGER,nombre TEXT); CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO cliente VALUES(1,'Ana'),(2,'Luis'); INSERT INTO ticket VALUES(10,1,'ABIERTO',100),(11,1,'CERRADO',300),(12,2,'ABIERTO',200);`, + expectedStdout: `10|Ana|ABIERTO +11|Ana|CERRADO +12|Luis|ABIERTO`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE cliente(id INTEGER,nombre TEXT); CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO cliente VALUES(5,'Eva'); INSERT INTO ticket VALUES(7,5,'CERRADO',9);`, + expectedStdout: `7|Eva|CERRADO`, + visible: false, + }, + ], + }, + { + slug: "bd2-informe-estado", + title: "Informe por estado", + description: "Devuelve estado|cantidad|total costo.", + prompt: "Devuelve `estado|cantidad|total costo`.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `SELECT estado,COUNT(*),SUM(costo) FROM ticket GROUP BY estado ORDER BY estado;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cliente(id INTEGER,nombre TEXT); CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO cliente VALUES(1,'Ana'),(2,'Luis'); INSERT INTO ticket VALUES(10,1,'ABIERTO',100),(11,1,'CERRADO',300),(12,2,'ABIERTO',200);`, + expectedStdout: `ABIERTO|2|300 +CERRADO|1|300`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO ticket VALUES(1,1,'CERRADO',50),(2,1,'CERRADO',70);`, + expectedStdout: `CERRADO|2|120`, + visible: false, + }, + ], + }, + { + slug: "bd2-informe-cliente", + title: "Informe por cliente", + description: "Devuelve cliente|tickets|costo.", + prompt: "Devuelve `cliente|tickets|costo`.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `SELECT cliente.nombre,COUNT(*),SUM(ticket.costo) FROM cliente JOIN ticket ON ticket.cliente_id=cliente.id GROUP BY cliente.id,cliente.nombre ORDER BY cliente.nombre;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE cliente(id INTEGER,nombre TEXT); CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO cliente VALUES(1,'Ana'),(2,'Luis'); INSERT INTO ticket VALUES(10,1,'ABIERTO',100),(11,1,'CERRADO',300),(12,2,'ABIERTO',200);`, + expectedStdout: `Ana|2|400 +Luis|1|200`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE cliente(id INTEGER,nombre TEXT); CREATE TABLE ticket(id INTEGER,cliente_id INTEGER,estado TEXT,costo INTEGER); INSERT INTO cliente VALUES(4,'Eva'); INSERT INTO ticket VALUES(1,4,'A',20),(2,4,'B',30);`, + expectedStdout: `Eva|2|50`, + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/index.ts b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/index.ts new file mode 100644 index 0000000..0bf578c --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/index.ts @@ -0,0 +1,16 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-por-que-nosql"; +import { leccion02 } from "./lessons/02-documentos-colecciones"; +import { leccion03 } from "./lessons/03-embeber-referenciar"; +import { leccion04 } from "./lessons/04-relacional-vs-documental"; + +export const bd2NosqlModelo = defineUnit({ + slug: "bd2-18-nosql-modelo", + title: "Pensar en documentos y colecciones", + description: + "Entiende el modelo documental, representa datos como documentos y elige entre embeber o referenciar.", + icon: "📄", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04], +}); diff --git a/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/01-por-que-nosql.ts b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/01-por-que-nosql.ts new file mode 100644 index 0000000..287237e --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/01-por-que-nosql.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "por-que-nosql", + title: "Por qué existe otro modelo", + description: "Entender qué problema intenta resolver un modelo documental.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `NoSQL no significa "sin estructura". Un modelo documental agrupa datos en documentos flexibles dentro de colecciones. Puede encajar cuando los datos se consumen juntos y su forma varía razonablemente.`, + }, + { + type: "matching", + pairs: [ + { left: "Relacional", right: "Tablas/relaciones" }, + { left: "Documental", right: "Documentos/colecciones" }, + { left: "flexible", right: "No significa sin reglas" }, + { left: "elección", right: "Depende de acceso y consistencia" }, + ], + explanation: "NoSQL no reemplaza universalmente SQL.", + }, + { + type: "quiz", + question: "¿Qué afirmación es falsa?", + options: [ + "Documentos tienen estructura", + "NoSQL siempre es mejor", + "Colecciones agrupan documentos", + "El modelo depende del problema", + ], + correctIndex: 1, + explanation: "No existe ganador universal.", + }, + { + type: "fill_blank", + template: "NoSQL documental = {{0}} dentro de colecciones", + blanks: [{ answer: "documentos" }], + explanation: "Unidad principal.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/02-documentos-colecciones.ts b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/02-documentos-colecciones.ts new file mode 100644 index 0000000..70fdc1e --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/02-documentos-colecciones.ts @@ -0,0 +1,44 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "documentos-colecciones", + title: "Documento, colección y campos", + description: "Representar un ticket como documento.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Un documento contiene campos y valores; una colección agrupa documentos del mismo propósito general. Un ticket puede incluir subdocumentos o arreglos si esos datos se consumen como parte de la misma unidad.`, + }, + { + type: "code_example", + code: `{ + "_id": 10, + "estado": "ABIERTO", + "cliente": {"id": 1, "nombre": "Ana"}, + "etiquetas": ["red", "urgente"] +}`, + explanation: "Ejemplo conceptual JSON de un documento.", + runnable: false, + localOnlyNote: "Representación documental; se practica con MongoDB local.", + }, + { + type: "quiz", + question: "¿Qué agrupa documentos?", + options: ["Tabla", "Colección", "Trigger", "Rol"], + correctIndex: 1, + explanation: "Colección es el contenedor lógico.", + }, + { + type: "matching", + pairs: [ + { left: "_id", right: "Identidad" }, + { left: "cliente", right: "Subdocumento" }, + { left: "etiquetas", right: "Arreglo" }, + { left: "colección tickets", right: "Grupo de documentos" }, + ], + explanation: "Lee la estructura.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/03-embeber-referenciar.ts b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/03-embeber-referenciar.ts new file mode 100644 index 0000000..3547fe1 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/03-embeber-referenciar.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "embeber-referenciar", + title: "Embeber o referenciar", + description: "Elegir según patrón de acceso y ciclo de vida.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `En un modelo documental puedes embeber datos relacionados o guardar referencias. Embeber favorece lectura conjunta y atomicidad del documento; referenciar evita duplicar entidades compartidas que cambian independientemente.`, + }, + { + type: "matching", + pairs: [ + { left: "Dirección única del cliente", right: "Puede embeberse" }, + { left: "Técnico compartido por miles de tickets", right: "Referencia puede ser mejor" }, + { left: "Datos que siempre se leen juntos", right: "Favorece embedding" }, + { left: "Entidad con vida propia", right: "Favorece referencia" }, + ], + explanation: "El patrón de acceso guía la decisión.", + }, + { + type: "quiz", + question: "¿Qué pregunta es clave?", + options: [ + "Qué color tiene Mongo", + "Cómo se leen y cambian juntos los datos", + "Cuántas tablas había", + "Qué IDE", + ], + correctIndex: 1, + explanation: "El agregado/documento debe reflejar acceso.", + }, + { + type: "fill_blank", + template: "leer/cambiar juntos → considerar {{0}}", + blanks: [{ answer: "embedding" }], + explanation: "Heurística, no regla absoluta.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/04-relacional-vs-documental.ts b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/04-relacional-vs-documental.ts new file mode 100644 index 0000000..796f7b1 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/18-nosql-modelo/lessons/04-relacional-vs-documental.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "relacional-vs-documental", + title: "Comparar sin caricaturas", + description: "Elegir modelo por invariantes, acceso y evolución.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Relacional destaca en relaciones explícitas, constraints y consultas cruzadas. Documental facilita agregados autocontenidos y estructura flexible. Ambos pueden ser robustos o malos según modelado.`, + }, + { + type: "matching", + pairs: [ + { left: "Muchas relaciones cruzadas/ad hoc", right: "Relacional suele encajar" }, + { left: "Agregado autocontenido leído junto", right: "Documental puede encajar" }, + { left: "Consistencia fuerte transversal", right: "Favorece modelo con constraints claros" }, + { left: "Esquema cambiante", right: "Documental puede reducir fricción" }, + ], + explanation: "Son trade-offs.", + }, + { + type: "quiz", + question: "¿Migrarías todo a Mongo sólo por 'flexibilidad'?", + options: [ + "Sí", + "No; primero patrón de datos/consultas/invariantes", + "Siempre", + "Sólo si hay JSON", + ], + correctIndex: 1, + explanation: "Flexibilidad tiene costes.", + }, + { + type: "fill_blank", + template: "modelo correcto = requisitos + invariantes + {{0}} de acceso", + blanks: [{ answer: "patrones" }], + explanation: "Decidir desde uso.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/index.ts b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/index.ts new file mode 100644 index 0000000..7a9f931 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/index.ts @@ -0,0 +1,16 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-insert-mongodb"; +import { leccion02 } from "./lessons/02-find-mongodb"; +import { leccion03 } from "./lessons/03-update-delete-mongodb"; +import { leccion04 } from "./lessons/04-crud-documental-completo"; + +export const bd2MongodbCrud = defineUnit({ + slug: "bd2-19-mongodb-crud", + title: "CRUD en una base documental", + description: + "Practica insertOne, find, updateOne y deleteOne en un laboratorio local de MongoDB con evidencia reproducible.", + icon: "🍃", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04], +}); diff --git a/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/01-insert-mongodb.ts b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/01-insert-mongodb.ts new file mode 100644 index 0000000..20d9b4e --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/01-insert-mongodb.ts @@ -0,0 +1,39 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "insert-mongodb", + title: "Insertar documentos", + description: "Leer insertOne y el documento creado.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `MongoDB usa colecciones y documentos BSON. Este bloque es laboratorio local con \`mongosh\`; no se envía al executor SQL.`, + }, + { + type: "code_example", + code: `db.tickets.insertOne({ + folio: 'T-10', + estado: 'ABIERTO', + costo: 100 +})`, + explanation: "Inserta un documento.", + runnable: false, + localOnlyNote: "Requiere MongoDB + mongosh local.", + }, + { + type: "quiz", + question: "¿Qué método inserta un documento?", + options: ["find", "insertOne", "updateOne", "deleteOne"], + correctIndex: 1, + explanation: "CRUD Create.", + }, + { + type: "fill_blank", + template: "Create → {{0}}", + blanks: [{ answer: "insertOne" }], + explanation: "Operación básica.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/02-find-mongodb.ts b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/02-find-mongodb.ts new file mode 100644 index 0000000..029606f --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/02-find-mongodb.ts @@ -0,0 +1,42 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "find-mongodb", + title: "Consultar documentos", + description: "Usar filtros de igualdad/comparación simples.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `\`find\` recibe un filtro documental. Empieza por condiciones explícitas y proyecta sólo campos necesarios cuando el reporte lo requiera.`, + }, + { + type: "code_example", + code: `db.tickets.find( + { estado: 'ABIERTO', costo: { $gte: 100 } }, + { folio: 1, costo: 1, _id: 0 } +)`, + explanation: "Filtra y proyecta.", + runnable: false, + localOnlyNote: "MongoDB local.", + }, + { + type: "quiz", + question: "¿Qué representa $gte?", + options: ["igual", "mayor o igual", "menor", "borrar"], + correctIndex: 1, + explanation: "Operador de comparación.", + }, + { + type: "matching", + pairs: [ + { left: "filtro", right: "Qué documentos" }, + { left: "proyección", right: "Qué campos" }, + { left: "$gte", right: "Comparación" }, + { left: "find", right: "Read" }, + ], + explanation: "Consulta documental.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/03-update-delete-mongodb.ts b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/03-update-delete-mongodb.ts new file mode 100644 index 0000000..9767bc9 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/03-update-delete-mongodb.ts @@ -0,0 +1,40 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "update-delete-mongodb", + title: "Modificar y eliminar", + description: "Aplicar filtros precisos en updateOne/deleteOne.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `\`updateOne\` modifica un documento que cumpla el filtro; \`$set\` cambia campos. \`deleteOne\` elimina un documento coincidente. Igual que en SQL, un filtro pobre puede cambiar el registro equivocado.`, + }, + { + type: "code_example", + code: `db.tickets.updateOne( + { folio: 'T-10' }, + { $set: { estado: 'CERRADO' } } +); + +db.tickets.deleteOne({ folio: 'T-99' });`, + explanation: "Update/Delete con filtro.", + runnable: false, + localOnlyNote: "MongoDB local.", + }, + { + type: "quiz", + question: "¿Qué operador cambia campos sin reemplazar todo el documento?", + options: ["$set", "$gte", "$find", "$drop"], + correctIndex: 0, + explanation: "$set actualiza campos.", + }, + { + type: "fill_blank", + template: "Update → updateOne + {{0}}", + blanks: [{ answer: "$set" }], + explanation: "Patrón básico.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/04-crud-documental-completo.ts b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/04-crud-documental-completo.ts new file mode 100644 index 0000000..54f9313 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/19-mongodb-crud/lessons/04-crud-documental-completo.ts @@ -0,0 +1,41 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "crud-documental-completo", + title: "CRUD y evidencia", + description: "Ejecutar una secuencia reproducible y verificar resultados.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `El laboratorio debe partir de una colección conocida, insertar documentos, consultar, modificar, eliminar y demostrar el estado final. Capturas sin script reproducible son evidencia débil.`, + }, + { + type: "code_completion", + lines: [ + "Preparar colección", + "insertOne", + "find", + "updateOne", + "deleteOne", + "find final", + "Guardar script/evidencia", + ], + explanation: "Secuencia verificable.", + }, + { + type: "quiz", + question: "¿Qué evidencia es mejor?", + options: ["Sólo captura", "Script + resultados reproducibles", "Memoria", "Nombre de DB"], + correctIndex: 1, + explanation: "Permite repetir la práctica.", + }, + { + type: "fill_blank", + template: "script + resultados = {{0}} reproducible", + blanks: [{ answer: "evidencia" }], + explanation: "Cierra la práctica.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/index.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/index.ts new file mode 100644 index 0000000..48f0b32 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/index.ts @@ -0,0 +1,20 @@ +import { defineUnit } from "../../../../authoring"; + +import { leccion01 } from "./lessons/01-alcance-integrador"; +import { leccion02 } from "./lessons/02-automatizacion-integrador"; +import { leccion03 } from "./lessons/03-seguridad-mantenimiento-integrador"; +import { leccion04 } from "./lessons/04-gui-conexion-integrador"; +import { leccion05 } from "./lessons/05-entrega-integrador"; + +import { practice } from "./practice"; + +export const bd2Integrador = defineUnit({ + slug: "bd2-20-integrador", + title: "Proyecto integrador de Base de Datos II", + description: + "Cierra el sistema de soporte con automatización justificada, transacciones, permisos, respaldo y una interfaz CRUD conectada.", + icon: "🧪", + published: true, + lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], + practice, +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/01-alcance-integrador.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/01-alcance-integrador.ts new file mode 100644 index 0000000..707e51b --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/01-alcance-integrador.ts @@ -0,0 +1,39 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion01 = defineLesson({ + slug: "alcance-integrador", + title: "Elegir problema y alcance", + description: "Definir un CRUD demostrable conectado a una base.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `El integrador continúa el sistema de soporte. Debe tener problema, necesidades, modelo y un CRUD desde interfaz. El alumno elige una ruta principal: relacional avanzada o documental; puede comparar la otra como extensión.`, + }, + { + type: "matching", + pairs: [ + { left: "Problema", right: "Qué se resuelve" }, + { left: "Necesidades", right: "Información requerida" }, + { left: "Modelo", right: "Estructura" }, + { left: "CRUD", right: "Interacción" }, + { left: "Reporte", right: "Evidencia/reflexión" }, + ], + explanation: "Los entregables se conectan.", + }, + { + type: "quiz", + question: "¿Qué debe existir antes de la GUI?", + options: ["Modelo y necesidades", "Colores", "Logo", "Animación"], + correctIndex: 0, + explanation: "La interfaz gestiona un modelo, no lo reemplaza.", + }, + { + type: "fill_blank", + template: "problema → necesidades → {{0}} → conexión → CRUD", + blanks: [{ answer: "modelado" }], + explanation: "Trazabilidad.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/02-automatizacion-integrador.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/02-automatizacion-integrador.ts new file mode 100644 index 0000000..4b8706c --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/02-automatizacion-integrador.ts @@ -0,0 +1,43 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion02 = defineLesson({ + slug: "automatizacion-integrador", + title: "Automatización y transacciones", + description: "Incorporar al menos una capacidad avanzada útil.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `En ruta relacional, el proyecto debe usar al menos una automatización justificada (trigger/procedure/job) y una transacción donde exista una invariante real. No se aceptan features sólo decorativas.`, + }, + { + type: "matching", + pairs: [ + { left: "Auditoría de cambios", right: "Trigger razonable" }, + { left: "Operación reutilizable MySQL", right: "Procedure posible" }, + { left: "Tarea temporal", right: "Job/Event" }, + { left: "Cambio inseparable", right: "Transacción" }, + ], + explanation: "Cada herramienta responde a un problema.", + }, + { + type: "quiz", + question: "¿Qué vale más?", + options: [ + "Tres triggers sin motivo", + "Una automatización justificada y probada", + "Mucho SQL", + "Nombres largos", + ], + correctIndex: 1, + explanation: "La evidencia debe conectar con requisito.", + }, + { + type: "fill_blank", + template: "feature avanzada + requisito + {{0}}", + blanks: [{ answer: "prueba" }], + explanation: "Debe justificarse.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/03-seguridad-mantenimiento-integrador.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/03-seguridad-mantenimiento-integrador.ts new file mode 100644 index 0000000..1213ec7 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/03-seguridad-mantenimiento-integrador.ts @@ -0,0 +1,44 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion03 = defineLesson({ + slug: "seguridad-mantenimiento-integrador", + title: "Permisos, respaldo y mantenimiento", + description: "Cerrar el producto con operación segura básica.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `La ruta relacional debe documentar roles/permisos mínimos, respaldo restaurable y una decisión de mantenimiento basada en evidencia. En ruta documental debe existir respaldo y control de acceso equivalente según herramienta.`, + }, + { + type: "code_completion", + lines: [ + "Definir roles", + "Aplicar mínimo privilegio", + "Crear respaldo", + "Restaurar/prueba", + "Documentar mantenimiento", + ], + explanation: "Operación forma parte del producto.", + }, + { + type: "quiz", + question: "¿Qué demuestra el backup?", + options: [ + "Que existe un archivo", + "Que puede restaurarse y verificarse", + "Que la app compila", + "Que hay un usuario", + ], + correctIndex: 1, + explanation: "Restore probado es evidencia.", + }, + { + type: "fill_blank", + template: "backup + {{0}} probado", + blanks: [{ answer: "restore" }], + explanation: "Recuperación real.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/04-gui-conexion-integrador.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/04-gui-conexion-integrador.ts new file mode 100644 index 0000000..d0cefc4 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/04-gui-conexion-integrador.ts @@ -0,0 +1,38 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion04 = defineLesson({ + slug: "gui-conexion-integrador", + title: "Conexión e interfaz CRUD", + description: "Construir la aplicación local sin mezclar responsabilidades.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `La app local debe abrir conexión mediante configuración externa, usar consultas parametrizadas, separar UI de acceso a datos y demostrar Create/Read/Update/Delete.`, + }, + { + type: "matching", + pairs: [ + { left: "UI", right: "Entrada/presentación" }, + { left: "Servicio", right: "Reglas" }, + { left: "Acceso datos", right: "Queries/driver" }, + { left: "config externa", right: "Credenciales" }, + ], + explanation: "Arquitectura mínima.", + }, + { + type: "quiz", + question: "¿Qué NO debe estar hardcodeado?", + options: ["Título", "Credenciales de producción", "Nombre de pantalla", "Texto botón"], + correctIndex: 1, + explanation: "Secreto fuera del repo.", + }, + { + type: "fill_blank", + template: "UI → lógica → acceso a datos → {{0}}", + blanks: [{ answer: "SGBD" }], + explanation: "Flujo.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/05-entrega-integrador.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/05-entrega-integrador.ts new file mode 100644 index 0000000..0b89052 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/lessons/05-entrega-integrador.ts @@ -0,0 +1,44 @@ +import { defineLesson } from "../../../../../authoring"; + +export const leccion05 = defineLesson({ + slug: "entrega-integrador", + title: "Entrega y reflexión", + description: "Reunir respaldo, aplicación, scripts y reporte reproducible.", + estimatedMinutes: 15, + xpReward: 45, + steps: [ + { + type: "theory", + markdown: `Entrega: aplicación, respaldo, scripts relevantes y reporte digital con datos del estudiante, problema, necesidades, modelado, conexión, evidencias de queries y reflexión. La reflexión debe explicar decisiones y límites, no sólo "aprendí mucho".`, + }, + { + type: "matching", + pairs: [ + { left: "Aplicación", right: "Producto ejecutable" }, + { left: "Respaldo", right: "Recuperación" }, + { left: "Scripts", right: "Reproducibilidad" }, + { left: "Reporte", right: "Trazabilidad/evidencia" }, + { left: "Reflexión", right: "Decisiones y límites" }, + ], + explanation: "El producto oficial es compuesto.", + }, + { + type: "quiz", + question: "¿Qué falta si sólo entregas capturas?", + options: [ + "Nada", + "Artefactos reproducibles: app/scripts/respaldo", + "Más colores", + "Más tablas", + ], + correctIndex: 1, + explanation: "Las capturas no reconstruyen el sistema.", + }, + { + type: "fill_blank", + template: "aplicación + respaldo + scripts + {{0}}", + blanks: [{ answer: "reporte" }], + explanation: "Entrega completa.", + }, + ], +}); diff --git a/prisma/content/courses/bases-de-datos/units/20-integrador/practice.ts b/prisma/content/courses/bases-de-datos/units/20-integrador/practice.ts new file mode 100644 index 0000000..bded2e9 --- /dev/null +++ b/prisma/content/courses/bases-de-datos/units/20-integrador/practice.ts @@ -0,0 +1,83 @@ +import type { PracticeExerciseDefinition } from "../../../../exercises/types"; + +export const practice = [ + { + slug: "bd2-final-trigger", + title: "Integrador: auditoría", + description: "Crea trigger que audite cambios de estado y cambia ticket 1 a CERRADO.", + prompt: "Crea trigger que audite cambios de estado y cambia ticket 1 a CERRADO.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `CREATE TRIGGER audit_estado AFTER UPDATE OF estado ON ticket BEGIN INSERT INTO auditoria VALUES(NEW.id,OLD.estado||'>'||NEW.estado); END; UPDATE ticket SET estado='CERRADO' WHERE id=1; SELECT ticket_id,accion FROM auditoria;`, + difficulty: "hard", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'ABIERTO',100),(2,'CERRADO',200);`, + expectedStdout: `1|ABIERTO>CERRADO`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'EN_PROCESO',5);`, + expectedStdout: `1|EN_PROCESO>CERRADO`, + visible: false, + }, + ], + }, + { + slug: "bd2-final-transaccion", + title: "Integrador: operación atómica", + description: + "Cierra ticket 1 e inserta pago por su costo dentro de transacción; devuelve estado|monto.", + prompt: + "Cierra ticket 1 e inserta pago por su costo dentro de transacción; devuelve `estado|monto`.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `BEGIN; INSERT INTO pago(ticket_id,monto) SELECT id,costo FROM ticket WHERE id=1; UPDATE ticket SET estado='CERRADO' WHERE id=1; COMMIT; SELECT ticket.estado,pago.monto FROM ticket JOIN pago ON pago.ticket_id=ticket.id WHERE ticket.id=1;`, + difficulty: "hard", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'ABIERTO',100),(2,'CERRADO',200); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER);`, + expectedStdout: `CERRADO|100`, + visible: true, + }, + { + description: "costo distinto", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); CREATE TABLE pago(ticket_id INTEGER,monto INTEGER); INSERT INTO ticket VALUES(1,'ABIERTO',450);`, + expectedStdout: `CERRADO|450`, + visible: false, + }, + ], + }, + { + slug: "bd2-final-reporte", + title: "Integrador: reporte", + description: "Devuelve estado|cantidad|total costo ordenado.", + prompt: "Devuelve `estado|cantidad|total costo` ordenado.", + starterCode: `-- escribe tu SQL aquí +`, + solutionCode: `SELECT estado,COUNT(*),SUM(costo) FROM ticket GROUP BY estado ORDER BY estado;`, + difficulty: "medium", + xpReward: 30, + testCases: [ + { + description: "visible", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); CREATE TABLE auditoria(ticket_id INTEGER,accion TEXT); INSERT INTO ticket VALUES(1,'ABIERTO',100),(2,'CERRADO',200);`, + expectedStdout: `ABIERTO|1|100 +CERRADO|1|200`, + visible: true, + }, + { + description: "oculto", + stdin: `CREATE TABLE ticket(id INTEGER PRIMARY KEY,estado TEXT,costo INTEGER); INSERT INTO ticket VALUES(1,'ABIERTO',10),(2,'ABIERTO',20),(3,'CERRADO',50);`, + expectedStdout: `ABIERTO|2|30 +CERRADO|1|50`, + visible: false, + }, + ], + }, +] satisfies PracticeExerciseDefinition[]; diff --git a/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts b/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts index 6e9a283..4c0f6e9 100644 --- a/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/09-colecciones/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Colecciones = defineUnit({ description: "Sustituye arreglos de tamaño fijo por colecciones dinámicas y elige entre lista, pila y cola según la operación del problema.", icon: "📚", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts index 3922b5e..5922f53 100644 --- a/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/10-diccionarios/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Diccionarios = defineUnit({ description: "Organiza objetos por una clave estable y realiza altas, consultas, actualizaciones y bajas sin recorrer toda la colección.", icon: "🗂️", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/index.ts b/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/index.ts index 7628239..423f957 100644 --- a/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/11-ordenamiento/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Ordenamiento = defineUnit({ description: "Define criterios de comparación, implementa ordenamientos básicos y aprovecha el orden para buscar objetos de forma predecible.", icon: "↕️", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts b/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts index c60ada1..e74d86e 100644 --- a/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/12-xml/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Xml = defineUnit({ description: "Representa objetos como XML, valida su estructura, transforma texto en datos tipados y separa serialización de persistencia física.", icon: "🧾", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts b/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts index e9a1661..51a59f3 100644 --- a/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/13-genericos/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Genericos = defineUnit({ description: "Reutiliza estructuras y operaciones sin perder type safety, y usa restricciones para expresar qué capacidades necesita un tipo genérico.", icon: "🧩", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/14-concurrencia/index.ts b/prisma/content/courses/csharp-poo-1/units/14-concurrencia/index.ts index bacf46d..71a11aa 100644 --- a/prisma/content/courses/csharp-poo-1/units/14-concurrencia/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/14-concurrencia/index.ts @@ -14,7 +14,7 @@ export const csharpPoo2Concurrencia = defineUnit({ description: "Ejecuta trabajo concurrente, identifica estado compartido y protege recursos críticos con sincronización explícita.", icon: "🧵", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/15-redes/index.ts b/prisma/content/courses/csharp-poo-1/units/15-redes/index.ts index 924deaa..0596834 100644 --- a/prisma/content/courses/csharp-poo-1/units/15-redes/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/15-redes/index.ts @@ -15,7 +15,7 @@ export const csharpPoo2Redes = defineUnit({ description: "Diseña mensajes, identifica protocolos y direccionamiento, y construye la lógica de una aplicación cliente-servidor sin confundir el protocolo con el transporte.", icon: "🌐", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04, leccion05, leccion06], practice, }); diff --git a/prisma/content/courses/csharp-poo-1/units/16-integrador/index.ts b/prisma/content/courses/csharp-poo-1/units/16-integrador/index.ts index ef0c804..b49a109 100644 --- a/prisma/content/courses/csharp-poo-1/units/16-integrador/index.ts +++ b/prisma/content/courses/csharp-poo-1/units/16-integrador/index.ts @@ -13,7 +13,7 @@ export const csharpPoo2Integrador = defineUnit({ description: "Integra estructuras dinámicas, genéricos, XML, concurrencia y un protocolo cliente-servidor en una solución orientada a objetos documentada.", icon: "🏗️", - published: false, + published: true, lessons: [leccion01, leccion02, leccion03, leccion04], practice, }); diff --git a/scripts/verify-content.ts b/scripts/verify-content.ts index 96a7334..4aa4369 100644 --- a/scripts/verify-content.ts +++ b/scripts/verify-content.ts @@ -230,13 +230,36 @@ function formatSqlValue(value: unknown): string { * Separa un script en sentencias individuales por `;`. Naïve a propósito * (sin parser SQL, ver TECHNICAL_CONTRACT §7): el contenido del curso no * usa `;` dentro de literales ni comentarios con `;`, así que partir por el - * delimitador es suficiente y exactamente lo que hace `sqlite3 < script.sql`. + * delimitador es suficiente y exactamente lo que hace `sqlite3 < script.sql` + * — con UNA excepción: `CREATE TRIGGER ... BEGIN ... END` (DB2, unidad + * triggers/integrador) SÍ contiene `;` internos entre `BEGIN` y `END`. El + * `sqlite3` CLI real reconoce ese bloque como una sola sentencia; este + * splitter naïve debe hacer lo mismo o corta el CREATE TRIGGER a la mitad + * ("incomplete input"). No es una re-implementación de un parser SQL: sólo + * sigue acumulando fragmentos mientras el trozo actual empieza con + * `CREATE TRIGGER` y todavía no termina en `END`. */ function splitSqlStatements(script: string): string[] { - return script - .split(";") - .map((s) => s.trim()) - .filter((s) => s.length > 0); + const statements: string[] = []; + let buffer: string | null = null; + + for (const rawPart of script.split(";")) { + buffer = buffer === null ? rawPart : `${buffer};${rawPart}`; + const trimmed = buffer.trim(); + if (trimmed.length === 0) { + buffer = null; + continue; + } + + const opensTriggerBody = /^create\s+trigger\b/i.test(trimmed); + const closesTriggerBody = /\bend\s*$/i.test(trimmed); + if (opensTriggerBody && !closesTriggerBody) continue; + + statements.push(trimmed); + buffer = null; + } + + return statements; } async function runCase( diff --git a/tests/content/course-contract.test.ts b/tests/content/course-contract.test.ts index 1ec5ddd..4d45368 100644 --- a/tests/content/course-contract.test.ts +++ b/tests/content/course-contract.test.ts @@ -354,7 +354,7 @@ describe("curriculum de C#: 2 secciones (S3 POO I + S4 POO II)", () => { ]); }); - it("S4: semester 4, subjectName exacto, 8 unidades EXACTAS en el orden contractual, todas unpublished", () => { + it("S4: semester 4, subjectName exacto, 8 unidades EXACTAS en el orden contractual, todas publicadas", () => { const s4 = csharp.curriculum!.find( (s) => s.key === "s4-programacion-orientada-objetos-2", )!; @@ -373,8 +373,9 @@ describe("curriculum de C#: 2 secciones (S3 POO I + S4 POO II)", () => { ]); expect(lessonsOf(s4.unitSlugs)).toBe(40); expect(practicesOf(s4.unitSlugs)).toBe(32); + // Release gate levantado: el fix del sprint DB2 publica las 8 unidades. for (const slug of s4.unitSlugs) { - expect(unitBySlug.get(slug)!.published, slug).toBe(false); + expect(unitBySlug.get(slug)!.published, slug).toBe(true); } }); @@ -488,9 +489,9 @@ describe("política de ejecución de S4 (red, sockets, concurrencia no determini } }); - it("las 8 unidades de S4 están unpublished (release gate pendiente)", () => { + it("las 8 unidades de S4 están publicadas (release gate levantado)", () => { for (const unit of s4Units) { - expect(unit.published, unit.slug).toBe(false); + expect(unit.published, unit.slug).toBe(true); } }); }); @@ -633,38 +634,217 @@ describe("curso S5: Modelos y métodos de desarrollo de software", () => { }); }); -describe("curso S4: Bases de datos (Base de Datos I)", () => { +describe("curso: Bases de datos (S4 Base de Datos I + S5 Base de Datos II)", () => { it("el curso está registrado EXACTAMENTE una vez", () => { const matches = allCourses.filter((c) => c.slug === "bases-de-datos"); expect(matches).toHaveLength(1); }); - const bd1 = courseBySlug.get("bases-de-datos")!; + const bd = courseBySlug.get("bases-de-datos")!; + const bdUnitBySlug = new Map(bd.units.map((u) => [u.slug, u])); + const bdLessonsOf = (slugs: string[]) => + slugs.reduce((n, slug) => n + bdUnitBySlug.get(slug)!.lessons.length, 0); + const bdStepsOf = (slugs: string[]) => + slugs.reduce( + (n, slug) => + n + bdUnitBySlug.get(slug)!.lessons.reduce((m, l) => m + l.steps.length, 0), + 0, + ); + const bdPracticesOf = (slugs: string[]) => + allPracticeSets + .filter((s) => s.courseSlug === "bases-de-datos" && slugs.includes(s.unitSlug)) + .reduce((n, s) => n + s.exercises.length, 0); it("declara la metadata exacta del contrato", () => { - expect(bd1).toBeDefined(); - expect(bd1.title).toBe("Bases de datos"); - expect(bd1.description).toBe( + expect(bd).toBeDefined(); + expect(bd.title).toBe("Bases de datos"); + expect(bd.description).toBe( "Diseña bases relacionales desde necesidades reales, normalízalas y consulta información con SQL.", ); - expect(bd1.subjectName).toBe("Bases de datos"); - expect(bd1.academicContext).toBe("CETI · Tecnólogo en Desarrollo de Software"); - expect(bd1.language).toBe("sql"); - expect(bd1.executionProfile).toBe("sql-sqlite3-wandbox"); + expect(bd.subjectName).toBe("Bases de datos"); + expect(bd.academicContext).toBe("CETI · Tecnólogo en Desarrollo de Software"); + expect(bd.language).toBe("sql"); + expect(bd.executionProfile).toBe("sql-sqlite3-wandbox"); + }); + + it("declara EXACTAMENTE dos CurriculumSection, S4 y S5, en orden", () => { + expect(bd.curriculum).toBeDefined(); + expect(bd.curriculum).toHaveLength(2); + expect(bd.curriculum!.map((s) => s.key)).toEqual([ + "s4-base-de-datos-1", + "s5-base-de-datos-2", + ]); }); - it("declara EXACTAMENTE una CurriculumSection de semestre 4", () => { - expect(bd1.curriculum).toBeDefined(); - expect(bd1.curriculum).toHaveLength(1); - const s4 = bd1.curriculum![0]; - expect(s4.key).toBe("s4-base-de-datos-1"); - expect(s4.semester).toBe(4); - expect(s4.order).toBe(1); - expect(s4.subjectName).toBe("Base de Datos I"); + it("todo el contenido usa el fence de SQL", () => { + expect(LANGUAGE_PROFILES[bd.language].markdownFences).toContain("sql"); + }); + + describe("S4 — Base de Datos I (intacta)", () => { + const s4 = bd.curriculum!.find((s) => s.key === "s4-base-de-datos-1")!; + + it("semester 4, order 1, subjectName exacto", () => { + expect(s4.semester).toBe(4); + expect(s4.order).toBe(1); + expect(s4.subjectName).toBe("Base de Datos I"); + }); + + it("declara las 10 Units EXACTAS, en el orden contractual", () => { + expect(s4.unitSlugs).toEqual([ + "bd1-01-fundamentos-sgbd", + "bd1-02-requerimientos-informacion", + "bd1-03-modelo-er", + "bd1-04-modelo-relacional", + "bd1-05-normalizacion", + "bd1-06-algebra-relacional", + "bd1-07-ddl", + "bd1-08-dml", + "bd1-09-consultas-reportes", + "bd1-10-respaldo-integrador", + ]); + }); + + it("las 10 Units están published=true (sin release gate para BD I)", () => { + for (const slug of s4.unitSlugs) { + expect(bdUnitBySlug.get(slug)!.published, slug).toBe(true); + } + }); + + it("totales EXACTOS: 47 lessons, 188 steps, 36 practices", () => { + expect(s4.unitSlugs.length).toBe(10); + expect(bdLessonsOf(s4.unitSlugs)).toBe(47); + expect(bdStepsOf(s4.unitSlugs)).toBe(188); + expect(bdPracticesOf(s4.unitSlugs)).toBe(36); + }); + + it("cada lección tiene EXACTAMENTE 4 steps", () => { + for (const slug of s4.unitSlugs) { + for (const lesson of bdUnitBySlug.get(slug)!.lessons) { + expect(lesson.steps.length, `${slug}/${lesson.slug}`).toBe(4); + } + } + }); + + it("todas las 10 unidades traen práctica independiente", () => { + const practiceUnitSlugs = new Set( + allPracticeSets + .filter((s) => s.courseSlug === "bases-de-datos") + .map((s) => s.unitSlug), + ); + for (const slug of s4.unitSlugs) { + expect(practiceUnitSlugs.has(slug), slug).toBe(true); + } + }); + }); + + describe("S5 — Base de Datos II", () => { + const s5 = bd.curriculum!.find((s) => s.key === "s5-base-de-datos-2")!; + + it("semester 5, order 2, subjectName exacto", () => { + expect(s5.semester).toBe(5); + expect(s5.order).toBe(2); + expect(s5.subjectName).toBe("Base de Datos II"); + }); + + it("declara las 10 Units EXACTAS, en el orden contractual", () => { + expect(s5.unitSlugs).toEqual([ + "bd2-11-procedimientos", + "bd2-12-triggers-jobs", + "bd2-13-transacciones", + "bd2-14-usuarios-permisos", + "bd2-15-mantenimiento", + "bd2-16-conexiones", + "bd2-17-crud-interfaz", + "bd2-18-nosql-modelo", + "bd2-19-mongodb-crud", + "bd2-20-integrador", + ]); + }); + + it("las 10 Units DB2 están published=true (10/10)", () => { + for (const slug of s5.unitSlugs) { + expect(bdUnitBySlug.get(slug)!.published, slug).toBe(true); + } + }); + + it("totales EXACTOS: 45 lessons, 180 steps, 15 practices", () => { + expect(s5.unitSlugs.length).toBe(10); + expect(bdLessonsOf(s5.unitSlugs)).toBe(45); + expect(bdStepsOf(s5.unitSlugs)).toBe(180); + expect(bdPracticesOf(s5.unitSlugs)).toBe(15); + }); + + it("cada lección tiene EXACTAMENTE 4 steps", () => { + for (const slug of s5.unitSlugs) { + for (const lesson of bdUnitBySlug.get(slug)!.lessons) { + expect(lesson.steps.length, `${slug}/${lesson.slug}`).toBe(4); + } + } + }); + + it("sólo las 4 unidades con práctica independiente la declaran (triggers, transacciones, crud-interfaz, integrador)", () => { + const practiceUnitSlugs = new Set( + allPracticeSets + .filter((s) => s.courseSlug === "bases-de-datos" && s5.unitSlugs.includes(s.unitSlug)) + .map((s) => s.unitSlug), + ); + expect(practiceUnitSlugs).toEqual( + new Set([ + "bd2-12-triggers-jobs", + "bd2-13-transacciones", + "bd2-17-crud-interfaz", + "bd2-20-integrador", + ]), + ); + }); + + it("las capacidades MySQL-only y MongoDB quedan runnable:false con localOnlyNote, nunca se envían al profile SQLite", () => { + const s5Units = bd.units.filter((u) => s5.unitSlugs.includes(u.slug)); + const codeExampleSteps = s5Units.flatMap((u) => + u.lessons.flatMap((l) => + l.steps + .filter((s): s is CodeExampleStep => s.type === "code_example") + .map((step) => ({ where: `${u.slug}/${l.slug}`, step })), + ), + ); + // Marcadores de sintaxis que SQLite no soporta: si aparecen en un + // ejemplo, ese ejemplo NUNCA puede ser runnable:true (no debe + // enviarse al profile sql-sqlite3-wandbox). + const NON_SQLITE_MARKERS = + /\bCREATE PROCEDURE\b|\bCALL \w|\bCREATE EVENT\b|\bCREATE ROLE\b|\bGRANT\b|\bREVOKE\b|\bANALYZE TABLE\b|\bOPTIMIZE TABLE\b|\bdb\.\w+\.(insertOne|find|updateOne|deleteOne)\b/; + let sawNonSqliteExample = false; + for (const { where, step } of codeExampleSteps) { + if (NON_SQLITE_MARKERS.test(step.code)) { + sawNonSqliteExample = true; + expect(step.runnable === true, `${where}: sintaxis no-SQLite marcada runnable`).toBe( + false, + ); + } + if (step.runnable !== true) { + expect(step.localOnlyNote?.trim(), `${where}: falta localOnlyNote`).toBeTruthy(); + } + } + expect(sawNonSqliteExample).toBe(true); + + // Ningún ejercicio de práctica automática (siempre ejecutado contra + // SQLite) referencia sintaxis MySQL/MongoDB: los 15 practices de DB2 + // son SQL compatible con SQLite, tal como exige el contrato. + const s5PracticeSets = allPracticeSets.filter( + (s) => s.courseSlug === "bases-de-datos" && s5.unitSlugs.includes(s.unitSlug), + ); + for (const set of s5PracticeSets) { + for (const ex of set.exercises) { + expect( + NON_SQLITE_MARKERS.test(ex.solutionCode), + `${ex.slug}: la práctica automática usa sintaxis no-SQLite`, + ).toBe(false); + } + } + }); }); - it("declara las 10 Units EXACTAS, en el orden contractual", () => { - expect(bd1.units.map((u) => u.slug)).toEqual([ + it("el orden GLOBAL aplanado (Unit.order) pone S4 completo antes de S5 completo", () => { + expect(bd.units.map((u) => u.slug)).toEqual([ "bd1-01-fundamentos-sgbd", "bd1-02-requerimientos-informacion", "bd1-03-modelo-er", @@ -675,50 +855,30 @@ describe("curso S4: Bases de datos (Base de Datos I)", () => { "bd1-08-dml", "bd1-09-consultas-reportes", "bd1-10-respaldo-integrador", + "bd2-11-procedimientos", + "bd2-12-triggers-jobs", + "bd2-13-transacciones", + "bd2-14-usuarios-permisos", + "bd2-15-mantenimiento", + "bd2-16-conexiones", + "bd2-17-crud-interfaz", + "bd2-18-nosql-modelo", + "bd2-19-mongodb-crud", + "bd2-20-integrador", ]); - expect(bd1.curriculum![0].unitSlugs).toEqual(bd1.units.map((u) => u.slug)); }); - it("las 10 Units están published=true (sin release gate para BD I)", () => { - for (const unit of bd1.units) { - expect(unit.published, unit.slug).toBe(true); - } - }); - - it("totales EXACTOS: 47 lessons, 188 steps, 36 practices", () => { - const lessons = bd1.units.flatMap((u) => u.lessons); + it("totales del curso completo: 20 units, 92 lessons, 368 steps, 51 practices", () => { + const lessons = bd.units.flatMap((u) => u.lessons); const steps = lessons.flatMap((l) => l.steps); const totalPractice = allPracticeSets .filter((s) => s.courseSlug === "bases-de-datos") .reduce((n, s) => n + s.exercises.length, 0); - expect(bd1.units.length).toBe(10); - expect(lessons.length).toBe(47); - expect(steps.length).toBe(188); - expect(totalPractice).toBe(36); - }); - - it("cada lección tiene EXACTAMENTE 4 steps", () => { - for (const unit of bd1.units) { - for (const lesson of unit.lessons) { - expect(lesson.steps.length, `${unit.slug}/${lesson.slug}`).toBe(4); - } - } - }); - - it("todas las 10 unidades traen práctica independiente", () => { - const practiceUnitSlugs = new Set( - allPracticeSets - .filter((s) => s.courseSlug === "bases-de-datos") - .map((s) => s.unitSlug), - ); - for (const unit of bd1.units) { - expect(practiceUnitSlugs.has(unit.slug), unit.slug).toBe(true); - } - }); - - it("todo el contenido usa el fence de SQL", () => { - expect(LANGUAGE_PROFILES[bd1.language].markdownFences).toContain("sql"); + expect(bd.units.length).toBe(20); + expect(lessons.length).toBe(92); + expect(steps.length).toBe(368); + expect(totalPractice).toBe(51); }); /** @@ -727,11 +887,12 @@ describe("curso S4: Bases de datos (Base de Datos I)", () => { * genérica de "cada práctica que usa stdin necesita un oculto * discriminante" (ver describe "contrato de prácticas" arriba) YA cubre * a SQL. Esta prueba confirma explícitamente que las prácticas con fixture - * no vacío del curso declaran al menos un test oculto con fixture distinto. + * no vacío del curso (DB1 + DB2) declaran al menos un test oculto con + * fixture distinto. */ it("las prácticas SQL con fixture no vacío tienen un test oculto con fixture distinto", () => { - const bd1Sets = allPracticeSets.filter((s) => s.courseSlug === "bases-de-datos"); - for (const set of bd1Sets) { + const bdSets = allPracticeSets.filter((s) => s.courseSlug === "bases-de-datos"); + for (const set of bdSets) { for (const ex of set.exercises) { const hasFixture = ex.testCases.some( (tc) => (tc.stdin ?? "").trim().length > 0, @@ -748,7 +909,7 @@ describe("curso S4: Bases de datos (Base de Datos I)", () => { } }); - it("no toca csharp-poo-1, cpp-desde-cero ni S5 al agregarse", () => { + it("no toca csharp-poo-1, cpp-desde-cero ni el curso de modelos y métodos al agregarse", () => { const cpp = courseBySlug.get("cpp-desde-cero")!; const csharp = courseBySlug.get("csharp-poo-1")!; const mm = courseBySlug.get("modelos-metodos-desarrollo-software")!; diff --git a/tests/content/structure-contracts.test.ts b/tests/content/structure-contracts.test.ts index c219263..75d2210 100644 --- a/tests/content/structure-contracts.test.ts +++ b/tests/content/structure-contracts.test.ts @@ -162,6 +162,85 @@ describe("contratos estructurales publicados", () => { "modelos-metodos-desarrollo-software/mm-10-integrador/mm-final-autorizacion", "modelos-metodos-desarrollo-software/mm-10-integrador/mm-final-prioridad", "modelos-metodos-desarrollo-software/mm-10-integrador/mm-final-pausado", + + // S4 POO II (csharp-poo-1): el fix del sprint DB2 levanta el release + // gate (published:false→true) de las 8 unidades 09..16, sin tocar su + // contenido. Esas unidades ya declaraban `structure` SÓLO donde el + // objetivo es diseño/modelado de datos (colecciones/09: + // inventario-en-memoria, list-de-objetos; integrador/16: + // requisitos-y-uml) — el resto evalúa comportamiento: operaciones de + // colecciones/diccionarios, criterios de ordenamiento, serialización + // XML, restricciones genéricas, exclusión mutua/concurrencia, + // protocolo de mensajes/redes y el integrador de esas capacidades. + // Ninguno de esos retos pide al alumno diseñar una estructura de + // clases, así que "sin contrato" es su diseño original, no una + // omisión de este sprint. + "csharp-poo-1/csharp-poo2-01-colecciones/de-arreglos-a-colecciones", + "csharp-poo-1/csharp-poo2-01-colecciones/stack-y-queue", + "csharp-poo-1/csharp-poo2-01-colecciones/agregar-buscar-eliminar", + "csharp-poo-1/csharp-poo2-02-diccionarios/clave-y-valor", + "csharp-poo-1/csharp-poo2-02-diccionarios/dictionary-basico", + "csharp-poo-1/csharp-poo2-02-diccionarios/buscar-seguro", + "csharp-poo-1/csharp-poo2-02-diccionarios/actualizar-y-eliminar", + "csharp-poo-1/csharp-poo2-02-diccionarios/elegir-coleccion", + "csharp-poo-1/csharp-poo2-03-ordenamiento/criterio-de-orden", + "csharp-poo-1/csharp-poo2-03-ordenamiento/burbuja-con-objetos", + "csharp-poo-1/csharp-poo2-03-ordenamiento/insercion-con-objetos", + "csharp-poo-1/csharp-poo2-03-ordenamiento/icomparer-e-icomparer", + "csharp-poo-1/csharp-poo2-03-ordenamiento/buscar-objetos", + "csharp-poo-1/csharp-poo2-04-xml/que-es-xml", + "csharp-poo-1/csharp-poo2-04-xml/bien-formado-y-valido", + "csharp-poo-1/csharp-poo2-04-xml/objetos-a-xml", + "csharp-poo-1/csharp-poo2-04-xml/xml-a-objetos", + "csharp-poo-1/csharp-poo2-04-xml/persistencia-xml", + "csharp-poo-1/csharp-poo2-05-genericos/por-que-genericos", + "csharp-poo-1/csharp-poo2-05-genericos/clase-generica", + "csharp-poo-1/csharp-poo2-05-genericos/metodo-generico", + "csharp-poo-1/csharp-poo2-05-genericos/restricciones-genericas", + "csharp-poo-1/csharp-poo2-05-genericos/repositorio-generico", + "csharp-poo-1/csharp-poo2-06-concurrencia/proceso-hilo-concurrencia", + "csharp-poo-1/csharp-poo2-06-concurrencia/thread-y-join", + "csharp-poo-1/csharp-poo2-06-concurrencia/lock-y-exclusion-mutua", + "csharp-poo-1/csharp-poo2-06-concurrencia/errores-en-hilos", + "csharp-poo-1/csharp-poo2-07-redes/protocolo-mensaje-socket", + "csharp-poo-1/csharp-poo2-07-redes/ip-puerto-endpoint", + "csharp-poo-1/csharp-poo2-07-redes/cliente-servidor-tcp", + "csharp-poo-1/csharp-poo2-07-redes/udp-y-mensajes", + "csharp-poo-1/csharp-poo2-07-redes/seguridad-del-mensaje", + "csharp-poo-1/csharp-poo2-08-integrador/arquitectura-del-proyecto", + "csharp-poo-1/csharp-poo2-08-integrador/integracion-concurrente", + "csharp-poo-1/csharp-poo2-01-colecciones/csharp-poo2-lista-productos", + "csharp-poo-1/csharp-poo2-01-colecciones/csharp-poo2-eliminar-agotados", + "csharp-poo-1/csharp-poo2-01-colecciones/csharp-poo2-pila-historial", + "csharp-poo-1/csharp-poo2-01-colecciones/csharp-poo2-cola-turnos", + "csharp-poo-1/csharp-poo2-02-diccionarios/csharp-poo2-indice-productos", + "csharp-poo-1/csharp-poo2-02-diccionarios/csharp-poo2-buscar-con-trygetvalue", + "csharp-poo-1/csharp-poo2-02-diccionarios/csharp-poo2-actualizar-stock", + "csharp-poo-1/csharp-poo2-02-diccionarios/csharp-poo2-directorio-clientes", + "csharp-poo-1/csharp-poo2-03-ordenamiento/csharp-poo2-ordenar-productos-precio", + "csharp-poo-1/csharp-poo2-03-ordenamiento/csharp-poo2-ordenar-alumnos-nombre", + "csharp-poo-1/csharp-poo2-03-ordenamiento/csharp-poo2-comparador-existencias", + "csharp-poo-1/csharp-poo2-03-ordenamiento/csharp-poo2-buscar-producto", + "csharp-poo-1/csharp-poo2-04-xml/csharp-poo2-leer-productos-xml", + "csharp-poo-1/csharp-poo2-04-xml/csharp-poo2-crear-xml-producto", + "csharp-poo-1/csharp-poo2-04-xml/csharp-poo2-parsear-datos-xml", + "csharp-poo-1/csharp-poo2-04-xml/csharp-poo2-inventario-xml", + "csharp-poo-1/csharp-poo2-05-genericos/csharp-poo2-caja-generica", + "csharp-poo-1/csharp-poo2-05-genericos/csharp-poo2-repositorio-productos", + "csharp-poo-1/csharp-poo2-05-genericos/csharp-poo2-metodo-mostrar", + "csharp-poo-1/csharp-poo2-05-genericos/csharp-poo2-repositorio-con-restriccion", + "csharp-poo-1/csharp-poo2-06-concurrencia/csharp-poo2-hilo-calculo", + "csharp-poo-1/csharp-poo2-06-concurrencia/csharp-poo2-dos-trabajadores", + "csharp-poo-1/csharp-poo2-06-concurrencia/csharp-poo2-contador-protegido", + "csharp-poo-1/csharp-poo2-06-concurrencia/csharp-poo2-error-en-worker", + "csharp-poo-1/csharp-poo2-07-redes/csharp-poo2-crear-mensaje-protocolo", + "csharp-poo-1/csharp-poo2-07-redes/csharp-poo2-parsear-mensaje", + "csharp-poo-1/csharp-poo2-07-redes/csharp-poo2-validar-endpoint", + "csharp-poo-1/csharp-poo2-07-redes/csharp-poo2-procesar-comando-remoto", + "csharp-poo-1/csharp-poo2-08-integrador/csharp-poo2-modelo-inventario", + "csharp-poo-1/csharp-poo2-08-integrador/csharp-poo2-repositorio-dominio", + "csharp-poo-1/csharp-poo2-08-integrador/csharp-poo2-import-export-xml", + "csharp-poo-1/csharp-poo2-08-integrador/csharp-poo2-comandos-thread-safe", ]); const sinContrato = challenges .filter((c) => c.language === "csharp" && c.structure === undefined)