# Tablas y Campos Este documento explica cómo gestionar tablas y campos en Acai usando las tools del MCP. Cubre: cómo se almacena el schema (`cms/data/schema/{tabla}.ini.php`), los `menuType` (`multi`, `single`, `category`, `separador`), el flag `enlace` para tablas públicas, todos los tipos de campo (`textfield`, `textbox`, `wysiwyg`, `codigo`, `date`, `list`, `checkbox`, `upload`, `multitext`, `separator`), los props comunes (`isRequired`, `defaultValue`, `optionsType`, etc.), la diferencia entre operaciones reversibles e irreversibles (`dropData`, `dropColumn`, rename), y el flujo correcto para crear una funcionalidad nueva. Léelo antes de usar cualquier tool del grupo `tables/`. ## Schemas Cada tabla tiene un schema en `cms/data/schema/{tabla}.ini.php`. Define: - Nombres y tipos de campo - Reglas de validación - Relaciones (foreign keys) - Configuración de display (orden, ancho, etc.) - Bloque `[meta]` con `menuName`, `menuType`, `menuOrder`, `controller`, etc. Antes de operar sobre una tabla, **siempre** consulta el schema: - `list_tables` — inventario rápido del proyecto - `get_table_schema` con `tableName` (sin `cms_`) — schema completo - `get_table_schema` con `minimal=true` — solo nombres + tipos + labels (ahorra tokens) - `get_table_schema` con `filterFields="galeria|foto|image"` — filtra por palabras clave **NUNCA inventes nombres de campos o tablas.** Siempre confirma con el schema. ## Convenciones inmutables | Regla | Valor correcto | |-------|----------------| | Nombres de tabla en tools/Twig/CmsApi | sin prefijo `cms_` | | Nombres en `queryDB` | con prefijo `cms_` | | Primary key | `num` (siempre) | | Foreign key | `_num` (e.g. `categoria_num`) | | Upload field | array `[{urlPath, info1, info2, info3, info4}]` | ## Crear una tabla — `create_table` ``` create_table({ tableName: "vacantes", // sin cms_, lowercase + underscores menuName: "Vacantes", // display en sidebar admin menuType: "multi", // multi | single | category | separador enlace: true, // ¿es tabla pública con URLs? seoMetas: true, // añade campos SEO meta (default false) menuOrder: 5 // opcional, orden en sidebar }) ``` ### Decisiones obligatorias antes de llamar - **`enlace: true|false`** es una decisión de arquitectura. **PREGUNTA AL USUARIO** antes de llamar: - `true` → la tabla genera URLs públicas, automáticamente añade campo `enlace` + slug. Cada registro será una página y puede tener detalle vía `custom-{tableName}`. - `false` → tabla puramente administrativa (categorías internas, configuraciones, logs). - **`menuType`**: - `multi` → lista plana (productos, noticias, vacantes) - `single` → un único registro (home, configuración, about us) - `category` → contenedor jerárquico que agrupa otras tablas en el menú - `separador` → solo un separador visual ### Después de crear la tabla 1. Añade los campos necesarios con `create_field` (uno por uno). 2. Si la tabla tiene `enlace: true`, considera crear la sección general `custom-{tableName}` para el detalle (ver `03-modules-and-sections.md`). 3. Si la tabla quiere ordenar/filtrar por fechas, añade campos `fecha_publicacion`, `fecha_expiracion`, `visible` (ver `04-pages-and-records.md`). ## Crear un campo — `create_field` ``` create_field({ tableName: "vacantes", fieldName: "salario_minimo", // identificador SQL-safe label: "Salario Mínimo", // display en formulario admin type: "textfield", initialProps: { // opcional — overrides de defaults isRequired: 1, maxLength: 100 } }) ``` ### Tipos de campo | Tipo | Uso | |------|-----| | `textfield` | Texto de una línea | | `textbox` | Texto multilínea plano | | `wysiwyg` | Editor de texto enriquecido | | `codigo` | Editor de código (HTML/JS/CSS snippet) | | `date` | Selector de fecha o datetime | | `list` | Select / radio / checkboxes (necesita `listType` + `optionsType` en `initialProps`) | | `checkbox` | Booleano (1/0) | | `upload` | Subida de archivos (imágenes, docs) | | `multitext` | Repetidor de entradas de texto | | `separator` | Separador visual en el formulario (sin columna en BD) | ### Props comunes (`initialProps`) Pasa solo los que quieres sobrescribir; el resto usa defaults. | Prop | Aplica a | Descripción | |------|----------|-------------| | `isRequired` | todos | `1` o `0` | | `isUnique` | textfield, textbox | `1` o `0` | | `defaultValue` | todos | Valor por defecto | | `description` | todos | Texto de ayuda en el formulario | | `minLength` / `maxLength` | textfield, textbox, wysiwyg | Longitud min/max | | `listType` | list | `select`, `radio`, `checkboxes` | | `optionsType` | list | `text` (opciones fijas) o `tablename` (opciones desde otra tabla) | | `optionsText` | list (text) | `"opcion1,opcion2,|valor3,etiqueta3"` | | `optionsTablename` | list (tablename) | Tabla origen (sin `cms_`) | | `optionsValueField` | list (tablename) | Campo del valor (típico: `num`) | | `optionsLabelField` | list (tablename) | Campo de la etiqueta | | `optionsQuery` | list (tablename) | Filtro WHERE adicional | | `filterField` | list (tablename) | Filtro dinámico por valor de otro campo | | `allowedExtensions` | upload | `"jpg,png,webp,pdf"` | | `maxUploads` | upload | Número máximo de archivos | | `createThumbnails` | upload | `1` o `0` | | `maxThumbnailWidth` / `maxThumbnailHeight` | upload | px | | `fieldWidth` / `fieldHeight` | upload | px sugeridos al builder | | `adminOnly` | todos | `1` oculta el campo en formularios públicos | | `charsetRule` | textfield | Restricciones de caracteres | | `tipoTags` | wysiwyg | Tags HTML permitidos | ### Ejemplo: lista desde tabla ``` create_field({ tableName: "vacantes", fieldName: "categoria_num", label: "Categoría", type: "list", initialProps: { listType: "select", optionsType: "tablename", optionsTablename: "categorias", optionsValueField: "num", optionsLabelField: "nombre" } }) ``` ## Actualizar un campo — `update_field` ``` update_field({ tableName: "vacantes", fieldName: "descripcion", newFieldName: "descripcion_corta", // OPCIONAL — renombra columna MySQL props: { label: "Descripción Corta", maxLength: 200 } }) ``` ### Casos destructivos - **`newFieldName`** renombra la columna MySQL. Los datos se preservan, pero **rompe cualquier referencia hardcodeada** (Twig, hooks, JS, queryDB). Audita el código antes de renombrar. - **Cambiar `type`** puede coercer/truncar datos (ej. `wysiwyg` → `textfield` elimina HTML). El backend devuelve `warnings` en la respuesta — **muéstralos al usuario**. ## Borrar un campo — `delete_field` ``` delete_field({ tableName: "vacantes", fieldName: "campo_obsoleto", dropColumn: false // default }) ``` - `dropColumn: false` → solo elimina del schema. Si la columna MySQL tiene datos, el backend rechaza y devuelve `dataCount` para que avises al usuario. - `dropColumn: true` → `ALTER TABLE DROP COLUMN`. **Los datos de esa columna se pierden permanentemente.** ## Borrar una tabla — `delete_table` ``` delete_table({ tableName: "tabla_obsoleta", dropData: false, // default dryRun: true // pre-flight check }) ``` - `dryRun: true` → no borra nada, solo reporta `recordCount`. **Úsalo siempre antes de pedir confirmación al usuario.** - `dropData: false` → solo borra el schema (`.ini.php`). Si la tabla MySQL tiene registros, el backend rechaza. - `dropData: true` → `DROP TABLE` + delete schema. **Datos perdidos permanentemente.** ## Reordenar — `reorder_tables`, `reorder_fields` Pasa la lista completa ordenada de nombres. Solo cambia el orden visual, los datos no se tocan. ``` reorder_tables({ order: ["apartados", "blog", "productos", "vacantes"] }) reorder_fields({ tableName: "vacantes", order: ["titulo", "descripcion", "salario_minimo", "fecha_publicacion", "visible"] }) ``` Los campos del sistema (`num`, `creationDate`, etc.) se ignoran automáticamente. ## Actualizar metadata — `update_table_metadata` Modifica el bloque `[meta]` del schema. ``` update_table_metadata({ tableName: "vacantes", newTableName: "ofertas_empleo", // OPCIONAL — renombra tabla MySQL meta: { menuName: "Ofertas de Empleo", menuOrder: 3, listPageFields: "titulo,fecha_publicacion,visible", breadcrumbField: "titulo" } }) ``` Keys aceptadas en `meta`: `menuName`, `menuDesc`, `menuType`, `menuOrder`, `menuDisplay`, `menuHidden`, `controller`, `breadcrumbField`, `breadcrumbByLink`, `breadcrumbParentNum`, `listPageFields` (csv), `listPageOrder`, `listPageSearchFields`. **`newTableName` renombra la tabla MySQL** y rompe cualquier referencia hardcodeada (controllers custom, módulos con SQL embebido, queryDB en plantillas). Audita el código antes y avisa al usuario. ## Regenerar enlaces — `regenerate_enlaces` Regenera el campo `enlace` (slug) de todos los registros de una tabla. **Cambia URLs públicas** — todo lo que apunte a las antiguas dará 404 a menos que actives los aliases. ``` regenerate_enlaces({ tableName: "vacantes", generateAlias: true // recomendado si la tabla ya es pública }) ``` - `generateAlias: false` (default) — solo actualiza `enlace`. URLs antiguas → 404. - `generateAlias: true` — escribe entradas en `alias_urls` para redirigir las URLs antiguas a las nuevas. Más seguro. ## Listar tablas — `list_tables` Devuelve todas las tablas con su `menuName`, `menuType`, `menuOrder` y `tableName`. Sin prefijo `cms_`. Úsalo cuando necesites un inventario rápido. ## Tipos de campo y formato al insertar/actualizar registros Al usar `create_or_update_record`, cada tipo espera un formato específico: | Tipo | Formato | Ejemplo | |------|---------|---------| | `textfield` | String | `"Texto"` | | `textbox` | String multilínea | `"Línea 1\nLínea 2"` | | `date`/datetime | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` | | `wysiwyg` | String HTML | `"

Texto

"` | | `list` | String o número | `"activo"` o `"1"` (num si es FK) | | `checkbox` | Número 1/0 | `1` o `0` | | `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` | | `upload` | **NO enviar** | Usa `upload_record_image` después de crear el registro | Ver `06-hooks-and-cmsapi.md` para los detalles de `CmsApi::insert` / `update`. ## Flujo canónico — Funcionalidad nueva tipo "vacantes" 1. `create_table({ tableName: "vacantes", menuType: "multi", enlace: true, seoMetas: true })` — pregunta al usuario si quiere `enlace` y `seoMetas`. 2. `create_field` para cada campo: `titulo`, `descripcion` (wysiwyg), `salario_minimo` (textfield), `categoria_num` (list desde tabla), `fecha_publicacion` (date), `fecha_expiracion` (date), `visible` (checkbox), `imagen_destacada` (upload). 3. Crear sección general `template/estandar/modulos/custom-vacantes/index-base.tpl` con `acai-write` (compila automáticamente). 4. (Opcional) Módulo de listado `vacantes_listado_xxxxxx` que liste registros con `'vacantes' | get('visible=1', 'fecha_publicacion DESC', 20)`. 5. (Opcional) Página índice `/vacantes/` en `apartados` con el módulo de listado. ## Reglas críticas 1. **Tabla sin prefijo `cms_`** en todas las tools. PK siempre `num`. 2. **Antes de cualquier operación**: `get_table_schema` para confirmar nombres y tipos de campo. 3. **Pregunta al usuario antes de `create_table`** sobre `enlace` y `seoMetas` (decisiones de arquitectura). 4. **`dropData`, `dropColumn`, `newFieldName`, `newTableName`** son destructivos o irreversibles — pide confirmación explícita. 5. **`regenerate_enlaces`**: usa `generateAlias: true` si la tabla ya tiene tráfico público. 6. **Surfacea los `warnings`** que el backend devuelve (cambios de tipo, renames, conteos de datos en riesgo). 7. **Upload fields no se setean en insert/update** — usa `upload_record_image` después.