This commit is contained in:
Jordan Diaz
2026-04-25 10:27:51 +00:00
parent e84a36c83d
commit 6881d64a08
42 changed files with 3207 additions and 3413 deletions

View File

@@ -0,0 +1,272 @@
# 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 | `<entidad>_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 | `"<p>Texto</p>"` |
| `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.