Schema y migraciones
Tu configuración no crea tablas por sí sola. Kevin CMS la convierte en un schema de Drizzle y en ficheros SQL que revisas antes de aplicar. Nada toca la base de datos sin que tú lo pidas.
La cadena completa
Sección titulada «La cadena completa»cms.config.ts │ cms db:generate ├──▶ .cms/schema.ts schema de Drizzle (generado, no se commitea) └──▶ .cms/types.d.ts tipos + App.Locals (generado, no se commitea) │ │ cms db:migrate ▼ migrations/<fecha>_<nombre>/migration.sql (se commitea) ▲ migrations/<fecha>_<nombre>/snapshot.json (se commitea) │ db:pop │ │ lo deshace │ cms db:apply ▼ D1Commitea migrations/. Añade .cms/ al .gitignore: se regenera siempre, y commitear artefactos
generados solo produce conflictos de merge.
Por qué en build y no en caliente
Sección titulada «Por qué en build y no en caliente»Un CMS que altera su propio schema durante un request es imposible de revisar y de revertir. Generando antes de desplegar consigues tres cosas: las migraciones son ficheros SQL que se leen en una pull request, se pueden revertir, y el runtime nunca toca el schema, así que ningún request puede romper la base de datos.
El ciclo
Sección titulada «El ciclo»-
Cambia tu
cms.config.ts— añade un campo, marca uno comounique, lo que sea. -
Genera la migración:
Ventana de terminal pnpm cms db:migrateEsto regenera
.cms/y después llama adrizzle-kit generate, que escribe el SQL. -
Lee el SQL que ha salido. Es el paso que no conviene saltarse:
Ventana de terminal cat migrations/*/migration.sqlSi no te convence, tíralo y vuelve al paso 1:
Ventana de terminal pnpm cms db:pop -
Aplícalo en local y compruébalo:
Ventana de terminal pnpm cms db:apply -
Cuando estés conforme, en producción:
Ventana de terminal pnpm cms db:apply --remote
Ninguno de esos comandos hace el trabajo del siguiente. db:migrate no aplica, db:apply no genera. Es a
propósito: el paso 3, leer el SQL antes de que toque una base de datos, sólo existe si nada lo salta por ti.
Configuración
Sección titulada «Configuración»Hacen falta dos ficheros, una vez.
import { defineConfig } from 'drizzle-kit'
export default defineConfig({ dialect: 'sqlite', schema: './.cms/schema.ts', out: './migrations',})Y en wrangler.jsonc, junto al binding de D1:
"d1_databases": [{ "binding": "DB", "database_name": "mi-sitio", "database_id": "<id>", "migrations_dir": "migrations", "migrations_pattern": "migrations/*/migration.sql"}]drizzle-kit lo instalas tú, en tu proyecto:
pnpm add -D drizzle-kitQué genera cada campo
Sección titulada «Qué genera cada campo»Un ejemplo real. Este config:
const posts = defineCollection({ slug: 'posts', fields: [ { name: 'title', type: 'text', required: true }, { name: 'slug', type: 'text', required: true, unique: true, index: true }, { name: 'status', type: 'select', options: ['draft', 'published'], required: true }, { name: 'author', type: 'relationship', to: 'users', onDelete: 'cascade' }, ],})produce este schema:
export const posts = sqliteTable('posts', { id: text('id').primaryKey(), createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(), title: text('title').notNull(), slug: text('slug').notNull(), status: text('status').notNull(), author: text('author').references(() => users.id, { onDelete: 'cascade' }),}, (t) => [ uniqueIndex('uniq_posts_slug').on(t.slug), check('chk_posts_status', sql`status IN ('draft', 'published')`), index('idx_posts_author').on(t.author),])Tres cosas que aparecen sin que las pidas:
id,createdAtyupdatedAten toda colección.- Un índice en cada relación, la pidas o no. Filtrar por una relación es el patrón de acceso más común de un CMS, y sin índice cada una de esas consultas recorre la tabla entera.
- Un
CHECKpor cadaselect, con sus opciones. La base de datos rechaza un valor fuera de la lista aunque algo se salte la validación.
La tabla completa de tipo de campo a columna está en Campos.
Nombres
Sección titulada «Nombres»| Elemento | Regla | Ejemplo |
|---|---|---|
| Tabla | El slug de la colección, tal cual | posts |
| Columna | El nombre del campo, tal cual | publishedAt |
| Índice | idx_<tabla>_<columna> |
idx_posts_author |
| Único | uniq_<tabla>_<columna> |
uniq_posts_slug |
| CHECK | chk_<tabla>_<columna> |
chk_posts_status |
Sin conversión a snake_case: el nombre que escribes en el config es el que aparece en la base de datos.
Una traducción implícita solo obliga a recordar dos nombres para lo mismo.
Aviso de deriva
Sección titulada «Aviso de deriva»Cuando cambias el config sin generar la migración, db:generate te lo dice:
El schema ha cambiado desde la última migración: ~ posts.readingTime (integer → real) + posts.views (integer) - posts.bodyEjecuta `cms db:migrate` para generar la migración.+ es una columna nueva, - una que has quitado, ~ un cambio de tipo.
Es un aviso, no un error: en desarrollo cambias el config todo el rato y bloquear el dev sería
insoportable. En integración continua usa --check, que convierte ese aviso en un fallo:
pnpm cms db:generate --checkLa comparación se hace contra los snapshot.json que hay en migrations/, que están commiteados, así que
funciona igual en tu máquina que en un clon recién hecho por CI.
Qué genera cada cambio
Sección titulada «Qué genera cada cambio»No todos los cambios cuestan lo mismo. Estos son los cuatro casos, con el SQL que sale de verdad:
| Cambio en el config | SQL generado | Tus datos |
|---|---|---|
| Añadir un campo | ALTER TABLE posts ADD subtitle text |
Intactos |
| Quitar un campo | ALTER TABLE posts DROP COLUMN subtitle |
Se pierde esa columna |
| Cambiar el tipo de un campo | Recrea la tabla con INSERT … SELECT |
Se conservan las filas |
| Renombrar un campo | Ninguno: falla pidiendo --hints |
Intactos |
db:migrate te pide confirmación en los dos casos destructivos —quitar y cambiar tipo— antes de generar
nada.
Renombrar un campo no está soportado
Sección titulada «Renombrar un campo no está soportado»Kevin CMS no intenta adivinar renombrados, y drizzle-kit tampoco: si quitas title y añades heading,
no puede saber si querías renombrar o si querías borrar uno y crear otro. En vez de elegir por ti, se planta:
missing_hints: 1 unresolved decisions1. Rename or create — column public.posts.headingdb:migrate sale con código 2 y no genera ninguna migración. cms todavía no sabe pasarle esas
pistas, así que hoy el camino es escribir la migración a mano en migrations/:
ALTER TABLE posts RENAME COLUMN title TO heading;Adivinar la intención sería peor que plantarse: una herramienta que confunde un renombrado con un borrado te vacía una columna sin avisar.
Durante el desarrollo
Sección titulada «Durante el desarrollo»La integración de Astro llama a generate al arrancar y vigila tu cms.config.ts, así que editarlo
regenera los tipos sin salir del dev.
Lo que no hace nunca es generar ni aplicar migraciones. Eso siempre lo pides tú.