Conceptos esenciales

El modelo mental detrás de todo

Archestack tiene un puñado de conceptos esenciales que aparecen en todas partes del producto. Cada uno existe para resolver un problema que de otro modo resolverías a mano, una y otra vez, de formas sutilmente distintas. Dedicar quince minutos a esta página te ahorrará horas de investigación. Cada sección de abajo introduce un concepto, te da un ejemplo práctico y señala los errores que cometen los usuarios nuevos.

El Schema es la fuente de la verdad

Todo empieza con el Schema, una descripción JSON de cada tabla, columna, relación e índice en tu aplicación. No escribes SQL a mano. Diseñas las tablas en el Schema Designer visual, y Archestack lo convierte en tablas PostgreSQL reales cuando despliegas.

Por qué existe

Un ERP tradicional es una maraña de tablas que ha crecido orgánicamente durante una década. Archestack invierte el modelo: el esquema es un artefacto versionado que editas explícitamente, como código fuente. Cualquier otra herramienta de la plataforma, Business Entities, Pages, Events, Scripts, lee del esquema. Si una columna no existe en el esquema, ninguna otra herramienta sabe de ella.

Cómo funciona en la práctica

  • Guardado automático frente a despliegue. Editar en Schema Designer guarda automáticamente tus cambios (aparece un indicador en la parte inferior izquierda). Guardar no cambia la base de datos, eso solo ocurre cuando creas un despliegue desde Database Deployments y haces clic en Deploy.
  • El SQL generado es editable. La pestaña Generated SQL del despliegue muestra el SQL de migración que ha producido la plataforma, CREATE TABLE, ALTER TABLE, etc. Puedes leerlo antes de desplegar, y puedes editarlo directamente si la versión autogenerada no es del todo correcta.
  • Hooks pre/post. Un despliegue lleva scripts de pre-despliegue y post-despliegue (PostgreSQL), útiles para rellenar una nueva columna NOT NULL o reconstruir un índice en un orden controlado.
  • El historial es permanente. Cada despliegue queda registrado con su estado (Draft / Executing / Succeeded / Failed) y el SQL que se ejecutó.

Ejemplo práctico

Añades una columna priority_score a deal. En Schema Designer eso consiste en seleccionar la tabla, abrir la pestaña Columns, escribir el nombre en el campo "column_name", elegir INTEGER en el desplegable Type y hacer clic en Add Column. El indicador de guardado automático muestra brevemente "Auto saving…" y luego "Saved". Después haz clic en Deploy en la barra de herramientas y aterrizas en una página de configuración de despliegue nueva. Abre la pestaña Generated SQL:

ALTER TABLE "deal" ADD COLUMN "priority_score" INTEGER;

Haz clic en Deploy arriba a la derecha, listo. La columna ya existe. Hasta que también la añadas a la Business Entity de deal (el siguiente concepto), ninguna página ni evento la verá. Esa separación es intencionada: los cambios de esquema son baratos, exponerlos es el siguiente paso deliberado.

Trampas

  • Renombrar una columna la elimina y la vuelve a crear. El generador del plan no puede leerte la mente. Si renombras, edita el SQL en la pestaña Generated SQL para usar RENAME COLUMN, o haz un despliegue en varios pasos: añade la nueva columna, escribe un post-script para copiar los datos y luego elimina la columna antigua en un despliegue posterior.
  • Añadir una columna NOT NULL a una tabla no vacía fallará. O proporcionas un valor por defecto, o la añades como nullable, rellenas los datos y luego la cambias a NOT NULL en un segundo despliegue.
  • Las claves foráneas restringen el comportamiento de borrado. Usa el desplegable On Delete en cada relación de forma deliberada, las opciones son CASCADE, SET NULL, SET DEFAULT, RESTRICT y NO ACTION.
  • Snake_case es la convención. El propio campo "Table name" de Schema Designer lo sugiere ("Lowercase with underscores recommended"). Las herramientas también funcionan con PascalCase, pero todos los ejemplos de código de este sitio asumen snake_case para los nombres de tablas y columnas.

Las Business Entities son la vista curada

Una tabla cruda (customer) rara vez es lo que un usuario final quiere ver. Quiere "el cliente con el total de su último pedido" o "el vehículo con el nombre y teléfono de su mecánico asignado". Una Business Entity (BE) es una vista curada de una tabla fuente más columnas unidas de tablas relacionadas. Las páginas se construyen sobre BEs, no sobre tablas crudas.

Por qué existe

Dos razones. Primero, unir tablas en cada página sería repetitivo e inconsistente: páginas distintas unirían las mismas tablas de formas ligeramente diferentes y el comportamiento acabaría divergiendo. La BE centraliza la lógica de unión. Segundo, a menudo quieres varias "vistas" de la misma tabla fuente para distintas audiencias: la vista de ventas de customer muestra ingresos y último contacto; la vista de soporte muestra tickets y SLAs incumplidos. Páginas distintas referencian BEs distintas, todas respaldadas por la misma fila en customer.

Anatomía

  • Master Table - la tabla sobre la que se ancla la BE. Cada BE tiene exactamente una. La clave primaria de la BE es la clave primaria de la tabla maestra.
  • Label Column - una columna cuyo valor identifica un registro en listas y selectores (típicamente name, title o similar). Se elige al crear la BE.
  • Joins - se añaden mediante el botón Add Join. Cada join tiene un tipo (INNER / LEFT / RIGHT), una tabla destino, columnas origen/destino y una lista de qué columnas del destino exponer.
  • Columnas agregadas - activa Aggregate Mode en un join y luego elige una función de agregado por columna: COUNT, SUM, AVG, MIN, MAX, COUNT DISTINCT. "Número de contactos abiertos de esta empresa" es una columna agregada típica.

Ejemplo práctico

Master Table: vehicle. Columnas nativas expuestas: vin, make, model, year. Join a customer mediante vehicle.owner_id → customer.id, exponiendo customer.full_name como owner_name. Segundo join en modo agregado: recuento de filas work_order donde vehicle_id coincide y status != 'Closed', expuesto como open_work_order_count.

Una página vinculada a esta BE renderiza una sola fila de rejilla que parece un dato plano pese a estar tirando de tres tablas, y lo hace de forma consistente en toda la plataforma.

Trampas

  • Las BEs no son fronteras de escritura por defecto. Guardar desde un formulario vinculado a una BE escribe en la tabla maestra. Las columnas unidas suelen ser de solo lectura; para editar owner_name en el ejemplo anterior, navega al Customer.
  • Usa Run Preview sin reparos. El botón Run Preview en el editor de BE ejecuta la configuración de joins y muestra filas reales. Si una columna unida vuelve vacía, tu relación o el mapeo de columnas está mal configurado.
  • No te extiendas en demasiados niveles. Una BE que une tres tablas en profundidad empieza a notarse lenta con conjuntos de datos grandes. Si te encuentras uniendo cuatro o cinco tablas, eso es una pista de que querías un Script Module personalizado o una vista de base de datos.

Las páginas se configuran, no se programan

Una Page es una interfaz de usuario en tiempo de ejecución construida a partir de una o más Business Entities. Las configuras en el Page Editor indicando un nombre, una ruta (por ejemplo, /companies), una vinculación a Business Entity y un conmutador Published. La plataforma autogenera secciones a partir de las columnas de la BE; a partir de ahí, ajustas el diseño.

Anatomía

  • Pestaña Visual - el editor de diseño WYSIWYG. Secciones, campos, pestañas, botones de acción.
  • Pestaña Overview - configuración de lista/rejilla: qué columnas aparecen, ordenación por defecto, filtros.
  • Pestaña Create - el formulario de nuevo registro. A menudo difiere ligeramente del formulario de detalle (menos campos, valores por defecto distintos).
  • Pestaña Entities - vinculaciones adicionales a BE (la página puede referenciar más de una BE para pestañas y rejillas relacionadas).
  • Pestaña Events - event triggers con ámbito de página.
  • Pestaña JSON - JSON en bruto para usuarios avanzados.

Tipos de widget de campo

Para cada campo del formulario de detalle eliges un Type en un control Select. Las opciones reales:

  • Text - entrada de una sola línea
  • Textarea - entrada de varias líneas
  • Number - entrada numérica
  • Date - selector de fecha
  • Select - desplegable (úsalo para campos de clave foránea y columnas con estilo enum; para campos FK, establece el autocompletado Entity a la BE referenciada para que los usuarios elijan por etiqueta)
  • Checkbox - booleano
  • Email - entrada de texto con validación de email

Para cada campo también configuras Label, Placeholder, Required, Read Only y Span (ancho de columna en la rejilla).

Pestañas y rejillas relacionadas

El formulario de detalle de una página soporta pestañas. Usa el botón Add Tab para añadir una. Dentro de una pestaña puedes añadir una sección RelatedGrid que muestre filas de otra BE filtradas por un join (por ejemplo, todas las filas contact donde company_id = id de la empresa actual). Así es como construyes páginas maestro-detalle "Empresa → Contactos / Oportunidades" sin nada de código.

Publicación

Cada página tiene un conmutador Published en la cabecera superior. OFF = borrador (solo tú en el editor ves tus cambios); ON = la página publicada aparece bajo Published Pages en la barra lateral y es lo que ven los usuarios finales cuando navegan a su ruta.

Frontend Templates: cuando "configurar, no programar" se queda corto

A veces las plantillas configuradas no bastan, quieres un widget personalizado, un diseño inusual o una visualización específica. Las Frontend Templates te permiten escribir un fragmento de TSX que queda disponible en el editor de páginas como un componente reutilizable. Son empaquetables y viven junto al resto de tu configuración. Consulta Referencia → Frontend Templates.

Trampas

  • ¿Olvidaste cambiar el conmutador Published? La página no aparecerá en la barra lateral y los usuarios finales que naveguen a su ruta verán un 404. Es fácil pasarlo por alto porque el editor no lo recuerda de forma visible.
  • El ancho importa. Si tu BE tiene 25 columnas, la rejilla será inusable en un portátil. Oculta las columnas de poco valor; siguen siendo consultables, simplemente no se muestran.
  • Las pestañas disparan consultas separadas. Una página con cinco pestañas hace consultas adicionales cuando abres una fila de detalle. Normalmente está bien, a veces es un problema de rendimiento con conjuntos de datos grandes.

Los Business Events hacen reaccionar a los datos

Un Business Event observa una Business Entity en busca de cambios y ejecuta acciones cuando se cumplen sus condiciones. Son las reglas "cuando ocurra X, haz Y" que convierten una base de datos pasiva en una aplicación funcional.

Momentos de disparo

Las opciones reales al crear un evento (puedes marcar más de una):

  • BeforeCreate - se dispara antes de guardar un nuevo registro. El script puede mutar Entity y los cambios se persisten.
  • BeforeUpdate - se dispara antes de guardar un registro existente. Mismo comportamiento de mutación.
  • AfterCreate - se dispara después de confirmar un nuevo registro.
  • AfterUpdate - se dispara después de confirmar un registro existente.
  • BeforeDelete - se dispara antes de borrar un registro.
  • InitialValue - se dispara cuando se abre un nuevo formulario; establece valores por defecto de los campos.
  • OnSchedule - lo dispara Quartz según un horario cron (consulta Scheduled Events).
  • Manual - lo dispara explícitamente un botón de acción de usuario en una página.

Tipos de acción

Los tipos de acción reales (RuleActionKind):

  • ExecuteScript - ejecuta un script C#. La acción más flexible; es a la que recurres cuando quieres establecer un campo, hacer un cálculo, llamar a una API, cualquier cosa a medida. El script recibe Entity (modifícalo para cambiar el registro), OldEntity, Log, Db, Modules.
  • Validate - ejecuta un script C# que devuelve bool. true significa que la validación falla y el guardado se bloquea con el mensaje de error configurado. Opcionalmente resalta columnas específicas.
  • BlockOperation - detiene la operación en seco con un mensaje. Sin script.
  • CreateEntity - inserta un registro en otra BE. Los valores de los campos admiten expresiones de plantilla.
  • UpdateEntity - actualiza registros en otra BE que coincidan con un filtro de condiciones.
  • DeleteEntity - elimina registros que coincidan con una condición (se niega a dispararse si no hay una, por seguridad).
  • SendEmail, SendWebhook, PublishEvent - definidos en el esquema como mejoras futuras; actualmente se registran y se omiten.

Expresiones de plantilla

La configuración de las acciones admite expresiones de plantilla entre llaves {{ … }}. Los tokens reales:

  • {{ Entity.column_name }} - campo del registro actual ({{ Entity.id }} también funciona)
  • {{ OldEntity.column_name }} - valor anterior (solo en triggers de update)
  • {{ now() }} o {{ getdate() }} - datetime ISO 8601 UTC
  • {{ today() }} - cadena de fecha
  • {{ guid() }} o {{ newid() }} - GUID nuevo
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}
  • Agregados dentro de un contexto de join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Aritmética: {{ Entity.quantity * Entity.price }} - evaluada tras la sustitución

Nota: no existe el token {{ user.email }}, los scripts y las expresiones de plantilla no tienen acceso al usuario actual. Si necesitas la identidad del usuario en una regla, guárdala en el registro al insertarlo desde el front-end y léela desde ahí.

Ejemplos prácticos

  • Normalizar un campo al guardar. Trigger: Before Update sobre Deal. Sin condiciones. Acción: Execute Script con Entity.title = ((string)Entity.title)?.Trim(); para quitar los espacios sobrantes. (No necesitas una regla para created_at / updated_at / created_by / updated_by, la plataforma los sella automáticamente.)
  • Bloquear que oportunidades de bajo importe se marquen como Won. Trigger: Before Update sobre Deal. Condición: stage = 'Won' AND amount < 100. Acción: Block Operation con el mensaje "Las oportunidades por debajo de 100 € no se pueden marcar como Won, regístralas como Lost o elimínalas."
  • Crear una Note cuando se marca un Customer. Trigger: After Update sobre Customer. Condición: flagged = true. Acción: Create Entity sobre note con title = "Customer flagged at {{ now() }}", body = "Auto-generated for {{ Entity.full_name }}".

Simula antes de guardar

El editor de trigger tiene una pestaña Simulate con un botón Run Simulation. Elige un registro real y la plataforma ejecuta las condiciones y acciones contra él sin persistir cambios, las operaciones de escritura se ejecutan en una transacción que se revierte. El panel de salida muestra qué condiciones coincidieron y qué habría hecho cada acción. Úsalo antes de activar un trigger sobre datos de producción.

Trampas

  • Triggers recursivos. Un trigger After Update que usa Update Entity para actualizar el mismo registro se volverá a disparar a sí mismo. Usa en su lugar Before Update + Execute Script + Entity.field = …, eso modifica la escritura en curso en lugar de iniciar una nueva.
  • El orden importa entre triggers. Varios triggers sobre el mismo evento se disparan en orden de prioridad. Si el comportamiento depende del orden, establece las prioridades explícitamente.
  • Los eventos desactivados siguen apareciendo en la lista. El conmutador Enabled es independiente de la configuración del trigger, verifica que esté activado antes de probar.

Los Script Modules te dan vías de escape

Para cualquier cosa que no se pueda expresar como una simple condición + acción (calcular un descuento complejo, llamar a una API externa, generar un slug, hacer una operación de base de datos en varios pasos), escribes un Script Module: un pequeño fragmento de C# (compilado en tiempo de ejecución por Roslyn) que puedes invocar desde un Business Event, desde un Scheduled Event o desde el front-end.

A qué tienen acceso los scripts

Cada script obtiene estos globales (no existen otros nombres en el ámbito del script):

  • Entity - el registro actual (en contextos de trigger) como dynamic. Accede a los campos con Entity.column_name. Modificar Entity en un trigger Before* es la forma de "establecer un campo".
  • OldEntity - el registro anterior (en triggers de update y delete). De solo lectura.
  • Log - un ILogger para diagnóstico. Log.LogInformation("…") escribe en los logs del servidor y en la entrada del Event Log.
  • Db - el helper de base de datos. Ver más abajo.
  • Modules - llama a otros Script Modules: await Modules.CallAsync("OtherModule", new Dictionary<string, object?> { ["param"] = value }).
  • Pdf - renderiza una PDF Template almacenada por nombre: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) devuelve los byte[] renderizados. Pdf.GenerateBase64Async(...) devuelve el mismo payload codificado en base64.

No existe un global User, Http ni Email.

Aspecto de un script

// Parameters declared in the UI: customer_id (int)
var customer = await Db.GetAsync("customer", customer_id);
if (customer == null) return new { ok = false, error = "Customer not found" };

var orders = await Db.From("sales_order")
    .Where("customer_id", "=", customer_id)
    .Where("status", "=", "Closed")
    .ToListAsync();

decimal totalRevenue = 0;
foreach (var o in orders) totalRevenue += (decimal)(o.total ?? 0);

return new { ok = true, customer = customer.name, revenue = totalRevenue };

La mayoría de los scripts tienen entre 5 y 30 líneas. No necesitas experiencia en .NET, te basta con control de flujo básico de C# y el helper Db. IntelliSense está activado, y tras asignar desde Db.GetAsync("customer", …) el editor conoce las columnas de esa variable y ofrece autocompletado.

Cuándo recurrir a un script

  • Cálculos que abarcan varias tablas (el valor de vida de un cliente, el total del historial de reparaciones de un vehículo).
  • Llamadas a APIs externas (geocodificación, proveedores de pago, pasarelas de SMS).
  • Operaciones masivas disparadas desde un Scheduled Event (recálculo nocturno, resumen semanal).
  • Devolver datos al front-end demasiado a medida para una Business Entity.
  • Cualquier cosa donde de otro modo encadenarías seis Business Events, suele ser una señal de que querías un script.

Trampas

  • El helper Db es async. Siempre await. Si lo olvidas, compila pero devuelve un Task, no los datos.
  • No hay FirstOrDefaultAsync ni SumAsync. Los terminales son ToListAsync(), FirstAsync() y CountAsync(). Para una suma, recupera las filas y súmalas en C#.
  • Where toma tres argumentos - columna, operador y valor. Los operadores incluyen =, !=, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL.
  • Update es una llamada de nivel superior: await Db.UpdateAsync("table", id, new { field = value }), no encadenada tras un Where.
  • Los scripts en una prueba se ejecutan en el mismo sandbox que producción. No asumas que "es solo una prueba" significa límites más laxos, tu script puede igualmente machacar Postgres.

Los Scheduled Events se ejecutan según un reloj

Un Scheduled Event es el mismo sistema de Event Trigger con el momento OnSchedule, una expresión cron de Quartz dispara la regla de forma recurrente en lugar de en respuesta a un cambio de datos. Úsalo para resúmenes nocturnos, refrescos diarios de datos, limpiezas semanales o informes mensuales.

Formato cron

Cron de 6 o 7 campos al estilo Quartz (second minute hour day-of-month month day-of-week [year]). Algunos patrones habituales:

  • 0 0 3 * * ? - todos los días a las 03:00.
  • 0 0 9 ? * MON-FRI - días laborables a las 09:00.
  • 0 */15 * * * ? - cada 15 minutos.
  • 0 0 0 1 * ? - el primer día de cada mes a medianoche.

Las horas son hora del servidor (UTC en la pila de producción). La página Scheduled Events muestra la próxima hora de disparo para que puedas hacer una comprobación rápida antes de guardar.

Trampas

  • Los trabajos largos ocupan su turno. Si un trabajo tarda 25 minutos y el cron es cada 15 minutos, la siguiente ejecución se omite, Quartz no disparará dos instancias del mismo trabajo a la vez.
  • Las ejecuciones fallidas no se reintentan. Se registran en Event Logs con la excepción. Añade lógica de reintento explícita a tu script si la necesitas.

Los Packages empaquetan la configuración para promoción

Una vez que has construido algo, un conjunto de tablas, BEs, páginas, eventos y scripts, querrás moverlo de tu prueba a un entorno real, o compartirlo con otro socio. Un Package es un ZIP de objetos de configuración seleccionados (con cascada: incluir una página automáticamente incluye su BE, que incluye sus tablas) que puedes exportar e importar.

La cascada explicada

Añades los objetos de nivel superior que te interesan (normalmente páginas). El constructor de paquetes recorre el grafo de dependencias y añade todo lo que esos objetos necesitan: las BEs a las que se vinculan las páginas, las tablas que referencian las BEs, los Frontend Templates que las páginas embeben, los Script Modules que llaman los eventos. Ves la lista completa en cascada antes de exportar y puedes desmarcar cualquier cosa que quieras omitir.

Enviar datos semilla

Los paquetes pueden incluir opcionalmente datos de filas, útil para enviar tablas de búsqueda por defecto (países, monedas, enums de estado) o datos de demostración. Marca Include data al exportar. Al importar, las filas se hacen upsert por clave primaria.

Trampas

  • Los deltas de esquema no se despliegan automáticamente al importar. Si al entorno importador le faltan tablas, la importación falla. Despliega el esquema primero, luego importa el paquete.
  • Los identificadores son por nombre, no por ID. Una BE llamada "customer" en el entorno origen se vincula a una BE llamada "customer" en el destino, no al mismo ID numérico. Los renombres en cualquier lado rompen el enlace.

Los Business Units controlan quién ve qué

Para configuraciones multi-tenant o multi-equipo, los Business Units son grupos de Keycloak que se pueden asignar a recursos específicos. Un usuario en el Business Unit "Garage A" solo puede ver Pages, BEs y Triggers asignados a Garage A.

Cómo funciona

Cada usuario pertenece a uno o más BUs (gestionados en User Management y Business Units). El front-end rastrea el BU activo del usuario y establece la cabecera HTTP X-Business-Unit en cada petición. El backend usa esa cabecera para filtrar los endpoints de lista, solo vuelven los recursos asignados al BU activo.

Cuándo usarlos (y cuándo no)

  • Úsalos cuando equipos o clientes distintos necesiten su propia porción de la misma plataforma, los mecánicos de Garage A no deberían ver las órdenes de trabajo de Garage B.
  • No los uses como sistema de permisos. Los BUs filtran la visibilidad, no la autorización. Los roles (admin / owner / editor / user) gestionan quién puede hacer qué.
  • No los uses para seguridad a nivel de fila dentro de un mismo tenant. Eso es trabajo de filtros en la Business Entity, no de un BU.

Hacia dónde va esto a continuación

Con esos conceptos en mano, el resto de la documentación fluye con naturalidad. El tutorial de la primera aplicación usa todos los conceptos de esta página. La Referencia baja un nivel más en cada herramienta, qué pulsar, en qué tener cuidado, cuándo recurrir a ella.