Campos
Un campo declara tres cosas a la vez: la columna de D1, la validación que se aplica al escribir, y el componente que lo edita en el administrador. Los declaras una vez y las tres salen solas.
Los nueve tipos
Sección titulada «Los nueve tipos»type |
Columna en D1 | Tipo en TypeScript |
|---|---|---|
text |
text('name') |
string |
textarea |
text('name') |
string |
number |
real('name'), o integer con integer: true |
number |
checkbox |
integer('name', { mode: 'boolean' }) |
boolean |
date |
integer('name', { mode: 'timestamp_ms' }) |
Date |
select |
text('name') + CHECK |
unión de sus value |
json |
text('name', { mode: 'json' }) |
unknown |
relationship |
text('name') + clave foránea + índice |
string | Doc |
upload |
text('name') + clave foránea + índice |
string | Doc |
textarea genera exactamente la misma columna que text; lo único que cambia es el componente del
administrador.
Opciones comunes
Sección titulada «Opciones comunes»Todos los tipos aceptan esto:
{ name: 'publishedAt', // nombre de la columna y clave en la API label: 'Fecha', // por defecto se deriva del name: 'publishedAt' → 'Published At' required: true, // NOT NULL, y validación al escribir unique: true, // índice único index: true, // índice defaultValue: 'draft', // se aplica en la aplicación, no como DEFAULT de SQL admin: { description: 'Texto de ayuda bajo el campo', readOnly: false, hidden: false, // fuera del administrador, presente en la API position: 'sidebar', // 'main' | 'sidebar' width: 'half', // 'full' | 'half' },}Un campo con unique: true e index: true genera solo el índice único. Un índice único ya sirve para
toda lectura que serviría el normal, y tener los dos duplica el coste de escritura por fila a cambio de nada.
Las cinco claves de admin las lee el editor del panel: description se pinta
como pista bajo el campo, readOnly lo deshabilita, hidden lo saca del formulario sin sacarlo de la
API, position decide en qué columna cae y width si ocupa media anchura.
Campos de sistema
Sección titulada «Campos de sistema»Toda colección lleva estos tres, sin declararlos:
id: text('id').primaryKey(),createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),El id es un UUID v7: ordenado por tiempo, así que el índice de la clave primaria sirve además como índice
de orden de creación. Redefinir cualquiera de los tres en tu config es un error de configuración.
Tipo por tipo
Sección titulada «Tipo por tipo»text y textarea
Sección titulada «text y textarea»{ name: 'slug', type: 'text', required: true, unique: true, minLength: 1, maxLength: 200, pattern: '^[a-z0-9-]+$' }pattern es una expresión regular en forma de cadena. Se compila al arrancar, así que una mal escrita falla
al inicio y no a mitad de una escritura.
Mientras no haya rich text, textarea con markdown cubre el texto largo.
{ name: 'readingTime', type: 'number', integer: true, min: 0, max: 120 }integer: true cambia las dos mitades a la vez: la columna pasa de REAL a INTEGER y la validación deja
de aceptar decimales.
checkbox
Sección titulada «checkbox»{ name: 'featured', type: 'checkbox' }SQLite no tiene booleanos: se guarda como 0 o 1 y vuelve como boolean.
{ name: 'publishedAt', type: 'date', mode: 'datetime' }Se guarda como epoch en milisegundos y vuelve como Date. mode ('date' o 'datetime') solo elige el
componente del administrador — los dos guardan lo mismo.
Al escribir acepta un Date o una cadena ISO, porque el mismo campo llega de las dos formas según vengas de
la API local o de HTTP.
En el administrador, 'datetime' te da calendario y hora y 'date' solo calendario. Eliges en el huso
de tu navegador, que el campo rotula, pero lo que se guarda es UTC en los dos casos. Un 'date' es un
día del calendario, no un instante: se guarda tal cual, sin convertir, o cambiaría de día para
cualquiera al oeste de Greenwich cada vez que abriera y guardara.
{ name: 'status', type: 'select', required: true, options: ['draft', 'published'] }Las opciones pueden llevar etiqueta:
options: [ { label: 'Borrador', value: 'draft' }, { label: 'Publicado', value: 'published' },]Genera un CHECK en la tabla, así que la base de datos rechaza un valor fuera de la lista aunque algo se
salte la validación:
CONSTRAINT "chk_posts_status" CHECK(status IN ('draft', 'published'))Y en TypeScript sale la unión de los valores, no string:
status: 'draft' | 'published'Que es lo que hace que el editor te corrija al escribir una plantilla.
{ name: 'meta', type: 'json' }Para estructuras a medida mientras no existan array ni blocks. Entra cualquier cosa serializable y vuelve
tal cual, anidamiento incluido. En TypeScript es unknown, así que te toca estrecharlo.
relationship
Sección titulada «relationship»{ name: 'author', type: 'relationship', to: 'users', onDelete: 'cascade' }to es el slug de otra colección. Se comprueba al arrancar: si apuntas a una que no existe, el error te
sugiere la más parecida.
onDelete acepta 'setNull' (por defecto), 'cascade' y 'restrict'. El defecto es deliberado — borrar un
autor no debería borrar sus posts en silencio; si es lo que quieres, lo pides.
Siempre lleva índice, lo declares o no.
{ name: 'cover', type: 'upload', to: 'media', accept: ['image/*'] }Como relationship, pero apuntando a una colección que declare upload. Si apunta a una que no lo declara,
falla al arrancar: sería una clave foránea a una tabla sin ficheros.
accept restringe qué fichero puede referenciar este campo: escribir en cover el id de un PDF es un
400 con código INVALID_FILE_TYPE, aunque el PDF esté bien guardado. Se comprueba al escribir el
documento, no al subir el fichero — la subida va contra la colección de media y no sabe qué campo acabará
usándolo. No cuesta ninguna consulta si el campo no declara accept.
Es una regla distinta del mimeTypes de la colección, y esa va primero. Ver
Ficheros.
Nulabilidad
Sección titulada «Nulabilidad»La regla es una: required: true da un tipo llano, cualquier otra cosa añade | null.
export interface Post { id: string createdAt: Date updatedAt: Date title: string // required: true slug: string // required: true excerpt: string | null // sin required readingTime: number | null featured: boolean | null publishedAt: Date | null status: 'draft' | 'published' // select con required meta: unknown | null cover: string | Media | null // upload → el id o el documento author: string | User | null // relationship → el id o el documento}Una relationship y un upload se tipan como la unión, no solo como el id: depth se resuelve en
tiempo de ejecución y por defecto vale 1, así que TypeScript no puede saber cuál de las dos ramas toca y
un tipo que dijera solo string mentiría justo en el caso por defecto. En la plantilla lo estrechas antes
de pintarlo — ver Relaciones: depth.
En el administrador
Sección titulada «En el administrador»Cada tipo trae su propio control en el panel, y cinco hacen bastante más que un input: relationship
busca en la colección destino, date abre un calendario, select se vuelve un buscador a partir de
diez opciones, json valida al salir del campo y upload abre tu biblioteca de medios. Los mismos
controles pintan el valor de un filtro en el listado.
Puedes sustituir el de cualquier tipo por uno tuyo con admin.fieldComponents. Ver
El administrador.
Fuera de la versión 1
Sección titulada «Fuera de la versión 1»richText, array y blocks no están todavía. Mientras tanto, textarea con markdown cubre el texto largo
y json cubre las estructuras a medida.
Ver también Schema y migraciones para lo que ocurre cuando cambias un campo que ya está en producción.