Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 3 del modelo, que iba primero por ser lo único que podía costarle
dinero a alguien: siete tablas guardaban `cancion_slug` sin clave
foránea, porque cuando se escribieron el catálogo vivía en archivos y no
había a qué apuntar. Renombrar un slug dejaba huérfana una compra en
silencio, y quien la había pagado se enteraba al intentar descargar.
Ahora apuntan a `cancion.id`, que es opaco y no cambia. El borrado no se
propaga igual en todas: `compra`, `pedido_item` y `descarga` lo
RESTRINGEN —una canción pagada no se puede borrar, y el registro de
entrega tiene que sobrevivir a lo que prueba—; `carrito_item`,
`favorito`, `valoracion` y `reproduccion` caen con ella.
En dos migraciones y no en una: entre la 0007 y la 0008 conviven las dos
columnas, así que el despliegue no tiene que parar el sitio. La 0007 se
niega a seguir si alguna compra apunta a un slug que ya no existe:
borrarla en silencio sería repetir el fallo que esto viene a arreglar.
Por fuera nada cambia de forma. Los módulos siguen recibiendo slugs —es
lo que hay en las URL— y traducen en el borde, en `$lib/server/canciones`.
Al hacerlo, tres funciones pasan a devolver `false` cuando el slug no
existe, que antes se guardaba tal cual: se podía meter en el carrito un
tema inventado.
El script de guardas estaba roto desde que se volcó el catálogo —usaba
slugs de verdad y moría en la primera inserción por clave duplicada, sin
comprobar nada—. Ahora usa el prefijo `zz-` y el país ZZ, que ISO 3166
reserva para uso privado. Y la prueba de la valoración pasaba por el
motivo equivocado: rechazaba el seis porque la columna ya no existía, no
por el rango.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 tablas nuevas, una vista y las guardas que Drizzle no sabe expresar. El
sitio sigue leyendo de los archivos: esto solo crea el sitio donde caben.
El esquema pasa de archivo a carpeta —con el catálogo dentro pasaría de
las dos mil líneas—, y el corte se hizo por rangos de línea para no
reescribir los comentarios que explican decisiones. Se comprobó que era un
no-op: «No schema changes, nothing to migrate».
Lo que Drizzle no genera va escrito a mano al final de la migración, y es
justo lo que impide que el modelo mienta: la clave foránea de las
versiones contra su propia tabla, dos columnas generadas y una clave
compuesta que impiden a la vez la versión de una versión y los ciclos, la
vista `cancion_principal`, el disparador que exige que el reparto de
autoría sume cien, el que impide ciclos en el linaje de géneros, y los
índices parciales del estilo principal y de la arista principal.
Y las guardas se comprueban intentando lo que deben rechazar, en
`scripts/db/comprobar-guardas.mjs`. Que una restricción exista en
`pg_constraint` no significa que impida nada.
Tres cosas que salieron de hacerlo y no de suponerlo:
- `creado_en` era obligatorio SIN valor por defecto en la base: lo ponía
JavaScript al insertar con Drizzle, así que cualquier INSERT escrito a
mano fallaba y la invariante la sostenía la aplicación, no la tabla.
Ahora es `defaultNow()`.
- `drizzle-kit migrate` se atasca con los cuerpos `$$` de plpgsql. La
migración se aplicó con `scripts/db/probar-migracion.mjs`, que además
dice en qué sentencia falla, y se anotó en su registro.
- Dos pruebas pasaban por el motivo equivocado: una por clave duplicada en
vez de por la suma, y otra porque un disparador aplazado no salta dentro
de un savepoint que nunca se cierra. Las dos corregidas; una guarda dada
por buena sin ejecutarse es peor que no tenerla.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>