notebook_attachments — Tabla D1 y endpoints de imágenes embebidas en notas
notebook_attachments — Tabla D1 y endpoints de imágenes embebidas
Entidad introducida en la migración 0032_notebook_sharing.sql (commit ab1c02f, 2026-05-20). Gestiona las imágenes embebidas en las notas del Cuaderno, usando el R2 bucket TASK_ATTACHMENTS con prefijo de clave notebook/.
Esquema D1
CREATE TABLE IF NOT EXISTS notebook_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER NOT NULL,
r2_key TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
uploaded_by TEXT NOT NULL,
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (note_id) REFERENCES notebook_notes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_notebook_attachments_note ON notebook_attachments(note_id);
- La FK con
ON DELETE CASCADEborra las filas de adjuntos automáticamente al borrar la nota. - Importante: el objeto R2 correspondiente no se borra por CASCADE — el handler
DELETE /api/notebook/[id]lo hace manualmente iterandor2_keyantes de borrar la nota.
Tipos de imagen permitidos
| MIME type | Extensión |
|---|---|
image/png | .png |
image/jpeg | .jpg |
image/webp | .webp |
image/gif | .gif |
Límite de tamaño: 5 MB por imagen (MAX_SIZE_BYTES = 5 * 1024 * 1024).
Clave R2
Patrón: notebook/{note_id}/{uuid}.{ext}
Ejemplo: notebook/42/c3d9a1b2-…-f7e8.webp
El bucket es TASK_ATTACHMENTS (shared con el módulo de tareas, que usa prefijo tasks/).
Endpoint: POST /api/notebook/[id]/images
Archivo: functions/api/notebook/[id]/images.ts
Acceso
Owner OR collaborador (cualquier email en shared_with).
Request
multipart/form-data con campo file (File).
Flujo
- Verifica acceso a la nota (
hasAccess). - Valida tamaño y content-type.
- Genera
r2_key = notebook/{noteId}/{uuid}.{ext}. TASK_ATTACHMENTS.put(r2Key, buffer, { httpMetadata, customMetadata }).INSERT INTO notebook_attachments … RETURNING id.- Registra actividad (
entity_type: 'notebook_attachment',action: 'create'). - Devuelve
{ id, url: '/api/notebook-images/{id}', filename, content_type }—201 Created.
Errores
| Código | Motivo |
|---|---|
| 400 | note_id inválido, form-data malformada, campo file ausente, fichero vacío |
| 403 | Sin acceso a la nota |
| 404 | Nota no encontrada |
| 413 | Fichero > 5 MB |
| 415 | Content-type no soportado |
| 500 | Fallo al registrar en D1 |
Endpoint: GET /api/notebook-images/[id]
Archivo: functions/api/notebook-images/[id].ts
Acceso
Idéntico al de la nota padre: owner OR shared_with = 'ALL' OR email en shared_with.
Flujo
- Busca fila en
notebook_attachmentsporid. - Busca nota padre para verificar acceso.
TASK_ATTACHMENTS.get(r2_key).- Stream del
object.bodycon headers adecuados.
Headers de respuesta
Content-Type: <content_type del adjunto>
Content-Length: <size_bytes>
Content-Disposition: inline; filename="<encoded_filename>"
Cache-Control: private, max-age=3600
Errores
| Código | Motivo |
|---|---|
| 400 | id no es entero |
| 403 | Sin acceso a la nota padre |
| 404 | Adjunto o nota no encontrados |
| 410 | El adjunto existe en D1 pero el objeto R2 ya no existe (object gone) |
Uso desde el editor
El botón 🖼 del toolbar de NoteEditor hace POST al endpoint, recibe la URL y la inserta como Markdown:

El renderizador Markdown del editor muestra la imagen embebida en la preview.
Orphaned R2 objects
Al borrar una nota, el handler DELETE /api/notebook/[id] debe:
- Consultar todos los
r2_keydenotebook_attachments WHERE note_id = ?. - Llamar a
TASK_ATTACHMENTS.delete(r2_key)para cada uno. - Después ejecutar el
DELETE FROM notebook_notes(que limpia D1 por CASCADE).
Si el paso 2 falla a medias, quedan objetos huérfanos en R2. No existe job de reconciliación actualmente.
Véase también
- [[feature—cuaderno—multiusuario-sharing]]
- [[entity—cuaderno—endpoint—notebook-version]]