Racoonity

El monstruo ya puede crecer

Racoonity terminó una de sus iteraciones más ambiciosas: ahora puede crear modelos, campos y registros dinámicos sin perder los límites de su arquitectura.

Por El equipo de Racoonity •
7 min de lectura
  • #racoonity
  • #desarrollo
  • #arquitectura
  • #openspec
  • #mvp

El monstruo ya puede crecer

Hasta ahora, cada iteración de Racoonity había hecho que la tienda supiera hacer algo nuevo.

Primero pudo existir. Luego tuvo catálogo, inventario, clientes, caja, ventas y una forma de saber qué había pasado.

Esta vez el cambio fue distinto.

No le enseñamos una nueva operación de negocio.

Le enseñamos a cambiar su propia forma.

Construir el monstruo

Internamente, esta iteración tuvo un nombre poco discreto: Construir el monstruo.

La idea era sencilla de explicar y bastante menos sencilla de implementar:

Racoonity debía poder recibir nuevas estructuras de información sin que cada una requiriera una nueva tabla, un nuevo módulo o una recompilación del backend.

No queríamos convertir el sistema en una masa indefinida donde todo pudiera hacer de todo. Tampoco queríamos un motor de scripting arbitrario escondido detrás de un botón de “personalizar”.

Queríamos conservar algo que se ha vuelto una regla importante para el proyecto: flexibilidad sin abandonar los límites.

El resultado es la primera versión real de la capa dinámica de Racoonity.

Un producto puede aprender campos nuevos

El primer paso fue trabajar sobre algo que Racoonity ya conocía: los productos.

Ahora una instalación puede definir campos adicionales para un producto sin alterar la tabla principal ni crear columnas dinámicamente.

Por ejemplo, una tienda podría añadir información como:

  • capacidad;
  • material;
  • código interno del proveedor;
  • fecha de caducidad;
  • una selección propia de clasificación.

Esos valores viven separados del modelo base, pero siguen participando en las reglas normales del sistema: validación, permisos, transacciones, consultas y presentación.

También pueden deshabilitarse sin borrar la información que ya existía. Si vuelven a habilitarse, los datos reaparecen.

La estructura puede cambiar sin destruir la historia.

Después dejamos de depender de modelos conocidos

Extender Product era solo la prueba inicial.

El siguiente paso fue permitir que una empresa creara un modelo completamente nuevo en tiempo de ejecución.

Supongamos que una tienda necesita llevar un pequeño registro de reparaciones.

Racoonity puede recibir una definición como:

Reparación

- título
- estado
- notas
- producto relacionado
- reparación relacionada

Y a partir de ella generar su metadata necesaria:

Modelo
├── campos
├── relaciones
├── vista de lista
├── formulario
├── vista de detalle
├── menú
└── acciones disponibles

Todo esto ocurre sin crear una estructura Repair en Go, sin generar un módulo repairs y sin crear una tabla SQL nueva para ese modelo.

El backend no conocía “Reparación” cuando fue compilado.

Ahora puede conocerla después.

Guardar cosas que no existían cuando compilamos el programa

Crear la definición no era suficiente.

Un modelo dinámico que no puede guardar registros es básicamente documentación muy elegante.

Así que Racoonity también aprendió a almacenar registros de esas estructuras nuevas.

Una reparación puede terminar existiendo en tiempo de ejecución así:

Reparación #019...

Título: Cambio de pantalla
Estado: Pendiente
Notas: Cliente espera llamada
Producto: Producto #...
Reparación padre: Reparación #...

Los datos se guardan en almacenamiento genérico, pero continúan teniendo tipos, campos obligatorios, selecciones válidas y relaciones verificadas.

También tienen ciclo de vida: pueden crearse, actualizarse, archivarse y volver a habilitarse.

Nada de esto requiere que la aplicación conozca previamente qué significa una reparación.

Relaciones sin convertir todo en una sopa genérica

Una de las partes delicadas fue permitir relaciones sin abrir la puerta a cualquier combinación imaginable.

En esta primera versión, los modelos dinámicos pueden relacionarse con productos y con otros modelos dinámicos de la misma empresa.

Eso permite cosas como:

Reparación
  └── producto → Product

Reparación
  └── reparación padre → Reparación

Pero seguimos manteniendo límites explícitos.

No existe una relación genérica hacia cualquier cosa del sistema, no generamos capacidades por cada modelo y tampoco recorremos árboles infinitos de relaciones para intentar adivinar cómo mostrar algo.

La flexibilidad es deliberada, no accidental.

Atlas ahora entiende metadata por empresa

Tener estructuras dinámicas introduce otro problema: dos empresas pueden definir cosas distintas, incluso usando el mismo nombre técnico.

Una empresa puede tener un modelo repairs con ciertos campos y otra puede tener su propio repairs completamente diferente.

Atlas ahora construye una visión efectiva de la metadata para cada empresa.

El sistema utiliza una generación durable para detectar cambios y reconstruir su cache cuando la definición cambia.

Lo importante es que la cache nunca se convierte en la autoridad.

PostgreSQL sigue siendo la fuente de verdad.

Si Racoonity se reinicia, Atlas puede reconstruir todo desde la base de datos sin necesitar que alguien vuelva a ejecutar los comandos con los que se creó la configuración.

Painter ya puede presentar lo desconocido

Después de almacenar y resolver metadata dinámica quedaba una pregunta inevitable:

¿Cómo presentas algo que no conocías cuando escribiste la interfaz?

Ahí entra Painter.

Painter puede tomar la definición efectiva de un modelo y convertir sus campos en una descripción de presentación: texto, número, fecha, selección, booleano o relación.

También puede utilizar un campo de representación para darle un nombre humano a un registro relacionado.

Si una reparación llamada Cambio de batería está relacionada con otra, la interfaz puede mostrar ese nombre en lugar de limitarse a enseñar un UUID.

Y si el campo utilizado como representación deja de estar disponible, existe un fallback determinista al identificador del registro.

No intentamos que Painter sea un diseñador visual completo todavía.

Pero ya tiene la pieza fundamental: puede presentar estructuras que no estaban compiladas dentro de Racoonity.

Los permisos siguen siendo los mismos permisos

Una tentación obvia habría sido crear capacidades nuevas por cada modelo dinámico:

repairs.create
repairs.update
repairs.archive

No lo hicimos.

Todos los modelos dinámicos reutilizan capacidades globales:

custom_record.create
custom_record.update
custom_record.archive
custom_record.enable
records.list
records.get

La metadata decide qué acciones pertenecen a un modelo y el sistema de permisos decide cuáles puede ejecutar cada usuario.

Eso mantiene una separación importante: crear un nuevo modelo no modifica el universo de capacidades ejecutables del backend.

El sistema puede crecer sin que su seguridad tenga que reinventarse con cada definición.

Y luego intentamos romperlo

La última parte de la iteración no añadió funcionalidades nuevas.

Se dedicó a intentar romper las existentes.

Probamos, entre otras cosas:

  • reinicios completos reconstruyendo el estado únicamente desde PostgreSQL;
  • operaciones concurrentes;
  • un deadlock real de PostgreSQL y el retry completo de la transacción;
  • rollback cuando una operación falla a mitad de camino;
  • reutilización y conflicto de claves de idempotencia;
  • aislamiento entre múltiples empresas;
  • invalidación de cache por generación;
  • permisos de administrador, cajero y consulta;
  • eventos y ausencia de eventos que deliberadamente no existen;
  • persistencia histórica de relaciones hacia registros archivados;
  • ausencia de tablas o estructuras generadas dinámicamente.

Después recorrimos dos journeys completos: uno extendiendo productos y otro creando desde cero un modelo dinámico, sus registros, relaciones, vistas y ciclos de vida.

Finalmente destruimos la composición en memoria, levantamos una nueva contra la misma base de datos y comprobamos que el monstruo seguía ahí.

Simple por defecto, monstruoso por elección

Esta iteración probablemente sea la que mejor representa hasta ahora una frase que hemos repetido durante el diseño de Racoonity:

Simple por defecto, monstruoso por elección.

Nada de esto significa que una tienda tenga que configurar modelos dinámicos.

Un usuario puede ignorar completamente esta capacidad y seguir utilizando los módulos normales de Racoonity tal como fueron diseñados.

Pero cuando una operación necesita algo que el producto base no contempla, ahora existe un camino para crecer sin modificar media aplicación.

Ese era el objetivo.

No construir un ERP donde absolutamente todo fuera genérico.

Construir una base suficientemente estable para que algunas partes pudieran volverse dinámicas cuando realmente hiciera falta.

La arquitectura ya no depende solo de una promesa

Durante meses hemos hablado de Racoonity como un conjunto de motores capaces de crecer sin rediseñar todo el sistema.

Hasta esta iteración, una parte importante de esa idea seguía siendo una promesa arquitectónica.

Ahora tenemos una primera prueba tangible.

Artifier puede construir.

Scribe puede operar los registros.

Metadata puede describirlos.

Warehouse puede persistirlos.

Atlas puede resolverlos.

Painter puede presentarlos.

Foreman puede mantener las operaciones atómicas.

Y el resto de Racoonity puede seguir funcionando sin saber de antemano qué modelo acaba de aparecer.

El monstruo ya existe.

Lo importante es que, por ahora, sigue obedeciendo. 🦝