3 puntos por GN⁺ 2023-12-19 | 1 comentarios | Compartir por WhatsApp
  • Si una función de PostgreSQL devuelve directamente text/html, se puede crear una app de tareas que reciba el resultado de una solicitud AJAX y reemplace una parte del DOM usando solo PostgREST y htmx
  • El ejemplo, sin autenticación, otorga permisos sobre api.todos y api.todos_id_seq a web_anon, y agrega un tipo de medio personalizado para procesar solicitudes con Accept: text/html
  • El armado del HTML queda a cargo de funciones SQL; api.sanitize_html, api.html_todo y api.html_all_todos se encargan de escapar entradas y renderizar la lista
  • El comportamiento de la pantalla se configura con hx-post, hx-get, hx-target, hx-trigger, hx-vals y hx-headers; el HTML de respuesta reemplaza #todo-list-area o áreas de edición individuales
  • Esta configuración es más bien una prueba de concepto, y del lado de PostgREST se está trabajando en plmustache para mejorar el manejo de HTML

Roles de PostgREST y htmx

  • PostgREST devuelve contenido HTML, y htmx usa el HTML recibido como resultado de solicitudes AJAX para reemplazar elementos dentro del DOM
  • Como htmx espera respuestas HTML, encaja bien con un flujo en el que el servidor devuelve fragmentos HTML en lugar de JSON
  • El ejemplo es una prueba de concepto que muestra una configuración posible al usar ambas tecnologías juntas
  • PostgREST está trabajando en plmustache para mejorar aún más el manejo de HTML

Configuración inicial de la app de tareas

  • El ejemplo es una app de tareas basada en Tutorial 0 - Get it Running
  • Para simplificar, no usa autenticación y otorga los permisos necesarios al usuario web_anon
    • grant all sobre la tabla api.todos
    • grant usage, select sobre la secuencia api.todos_id_seq
  • Para que PostgREST reconozca como HTML una solicitud del navegador con Accept: text/html, es necesario agregar un manejador de tipo de medio
    • Se crea el dominio "text/html" como text
    • Con esta configuración, PostgREST puede devolver documentos HTML sin procesar

Devolución de la página HTML básica

  • La función api.index() devuelve "text/html" y genera el documento HTML básico
  • La página incluye el título PostgREST + HTMX To-Do List y los siguientes recursos
  • En el navegador, esta página se puede abrir en http://localhost:3000/rpc/index

Renderizado de la lista y agregado de tareas

  • Para convertir la lista de tareas existente en HTML se usan tres funciones SQL
    • api.sanitize_html(text): reemplaza los caracteres &, ", >, < y ' por entidades HTML
    • api.html_todo(api.todos): da formato a un único ítem de tarea como fragmento HTML
    • api.html_all_todos(): combina todos los ítems en una sola cadena HTML y, si no hay ítems, devuelve el mensaje There is nothing else to do.
  • api.html_todo y api.html_all_todos son funciones internas para crear la plantilla de la lista y no se usan directamente como endpoints de PostgREST
  • api.add_todo(_task text) inserta una nueva tarea en api.todos y luego devuelve el HTML de la lista completa actualizada
  • La versión actualizada de api.index() incluye htmx y un formulario de entrada
    • Se agrega hx-headers='{"Accept": "text/html"}' en un elemento superior para que las solicitudes htmx hijas pidan respuestas HTML
    • Sin este encabezado, PostgREST no reconoce la solicitud como HTML
  • El formulario para agregar tareas funciona con atributos de htmx
    • hx-post="/rpc/add_todo": envía por POST el valor de entrada _task a /rpc/add_todo
    • hx-target="#todo-list-area": inserta el HTML de respuesta en el área de la lista de tareas
    • hx-trigger="submit": envía la solicitud al enviar el formulario
    • hx-on="htmx:afterRequest: this.reset()": vacía el formulario después de completar la solicitud
  • Para reflejar la nueva función y los cambios, se debe refrescar la caché del esquema
  • Luego se puede verificar la visualización de la lista y el agregado de nuevas tareas en http://localhost:3000/rpc/index

Edición, eliminación y cambio de estado de completado

  • api.html_todo se amplía para incluir en cada ítem una UI para cambiar el estado de completado, editar y eliminar
  • El cambio de estado de completado se maneja con un <form> y atributos de htmx
    • hx-post="/rpc/change_todo_state": envía una solicitud AJAX POST a ese endpoint
    • hx-vals='{"_id": ..., "_done": ...}': agrega parámetros a la solicitud en lugar de usar entradas ocultas
    • hx-trigger="click": ejecuta la solicitud al hacer clic en el ítem
  • El botón de edición convierte una tarea individual en un campo de entrada
    • hx-get="/rpc/html_editable_task": llama al endpoint que devuelve una entrada HTML editable
    • hx-target="#todo-edit-area-...": reemplaza solo el área de la tarea individual, en lugar de toda la lista
    • hx-vals agrega parámetros a la solicitud GET; al representar columnas de la tabla, se necesita el operador eq.
  • El botón de eliminación envía una solicitud para borrar esa tarea con hx-post="/rpc/delete_todo"
  • api.html_editable_task(_id int) devuelve el HTML de un campo de entrada para editar una tarea específica
    • Envía el nombre de tarea modificado con hx-post="/rpc/change_todo_task"
    • Actualiza al enviar o al perder el foco con hx-trigger="submit,focusout"
    • Incluye hx-headers='{"Accept": "text/html"}'
  • Todos los endpoints de modificación de datos devuelven el HTML de la lista completa después del cambio
    • api.change_todo_state(_id int, _done boolean): actualiza la columna done
    • api.change_todo_task(_id int, _task text): actualiza la columna task
    • api.delete_todo(_id int): elimina la tarea correspondiente a _id
  • Después de refrescar la caché del esquema, se puede editar, eliminar y marcar tareas como completadas en http://localhost:3000/rpc/index

1 comentarios

 
GN⁺ 2023-12-19
Opiniones en Hacker News
  • PostgREST es uno de mis proyectos open source favoritos. Creo que Supabase se convirtió en una empresa valuada en 1.000 millones de dólares en gran parte gracias al excelente diseño de PostgREST y Postgres.
    No sé cómo patrocina Supabase este proyecto, pero espero que sea con una suma muy grande. Es triste ver la realidad del financiamiento open source cuando un proyecto usado como dependencia central por al menos cientos de empresas que generan ingresos tiene solo 12 patrocinadores pagos.
    https://www.patreon.com/postgrest/about

    • Como es una parte central del stack de Supabase, tal como se mencionó, empleamos a Steve, el maintainer principal, para que trabaje principalmente en PostgREST.
      https://github.com/steve-chavez
    • Ahora son 13. Aun así, siguen siendo solo USD 1,529 al mes.
    • No sabía que PostgREST era parte de Supabase. Pensé que Supabase era un producto clon hecho desde cero.
    • Ya puedo escuchar los comentarios típicos que siempre aparecen en estas situaciones: “los usuarios no tienen obligación de apoyarte económicamente”, “si no te gusta, no deberías haberlo publicado con una licencia permisiva”, y cosas por el estilo.
      Por eso ahora uso solo AGPLv3 o licencias similares. Si quieres usarlo comercialmente, paga el 1% de tus ingresos brutos.
  • Como prueba de concepto está genial y la implementación merece elogios, pero para mantener esto en una webapp que no sea trivial parece una pesadilla.

    • Definitivamente se siente más orientado a sitios web o apps ligeras. Aun así, dentro de ese alcance se le puede sacar bastante provecho.
    • Ahora estoy experimentando con Sqitch para ver si puede facilitar el mantenimiento. Todavía se siente como un hack y sigo teniendo dudas sobre su viabilidad en uso real, pero es divertido y estoy aprendiendo varias funciones avanzadas de Postgres.
      https://sqitch.org/
    • Sinceramente, me da curiosidad qué parte de nuestra aplicación web principal no se volvería 100 veces más fácil si usáramos esto.
    • Por ahora creo que sí. Ahora solo hace falta una forma de compilar y desplegar las rutas HTMX en el servidor de base de datos como parte del proceso de CI.
  • Me pregunto si este tipo de funcionalidad o patrón de codificación es nuevo, o si también se usa en aplicaciones modernas.
    CouchDB, una base de datos de documentos JSON, ofrecía una API basada en HTTP y tenía métodos integrados de listado/detalle que podían responder en cualquier formato que se pudiera construir dentro de un intérprete de JavaScript. Es decir, el cliente podía llamar directamente a la base de datos y recibir HTML o JSON, sin necesidad de servidor.
    Pero después de la v1 dejaron de trabajar en esa dirección porque el mantenimiento se volvía una pesadilla. Mucha gente recordará los viejos buenos tiempos de archivos PHP o ASP con sentencias SQL y HTML mezclados en un solo archivo; esto no parece muy distinto.

    • Parece que una nueva generación está reaprendiendo cosas que antes se abandonaron por buenas razones.
      Hace unos 13 años me encantaban las funciones web de CouchDB en un proyecto personal, pero para un equipo eran bastante incómodas.
    • Difícil llamarlo moderno. Oracle ya tenía algo así hace más de 25 años.
  • La única app con PostgREST en la que trabajé fue horrible. Porque, como ocurre con la mayoría de estos frameworks “simples”, solo son simples hasta que los requisitos se vuelven complejos.
    Los autores originales terminaron usando un montón de procedimientos almacenados en la base de datos para obtener los resultados que querían, y eso derivó en problemas de escalabilidad. Como siempre, la solución fue volver a SQL.

    • Suena a que ese equipo eligió la herramienta equivocada para el trabajo. Si la mayoría de tus endpoints tienen lógica de backend compleja, no deberías usar PostgREST.
      PostgREST fue creado para apps CRUD, y muchas de las aplicaciones que veo encajan en eso. La lógica de backend personalizada que se necesite de vez en cuando puede manejarse levantando un servidor separado o funciones edge.
    • Uso bastante PostGREST, pero con guardrails fuertes. Creo que siempre debe existir una capa de API real, y esto conviene usarlo solo como ayuda para facilitar cargas básicas.
      Como pasa con todas estas herramientas, cuando llegan los requisitos reales, el costo de adaptarse a la herramienta termina siendo peor que aquello que pretendía reemplazar: SQL + algún lenguaje y framework. PostGREST ya se está volviendo complejo, y este tipo de funcionalidad adicional lo hace menos atractivo para mí.
    • ¿Los procedimientos almacenados no son, al final, SQL en forma de funciones?
      Antes de que apareciera Rails e hiciera que todos creyeran que poner la lógica de negocio en un lenguaje lento del lado del servidor era una buena idea, todo el mundo construía apps de esta manera.
  • Es un stack de desarrollo web realmente limpio: solo HTML y base de datos, sin necesidad de backend ni frontend.

    • A mediados de los 90 visité Compuserve en Ohio. La web recién estaba empezando, y un ingeniero que conocí allí me mostró una tienda de música que estaba construyendo devolviendo HTML desde procedimientos almacenados SQL :)
    • Si esto te parece genial, también vale la pena revisar Omnigres.
      https://github.com/omnigres/omnigres
    • Ya recorrimos este camino. Al principio es limpio, pero no es bueno para el mantenimiento a largo plazo, el soporte ni la capacitación.
      Cuando el brillo de HTMX se desvanezca, quien lo mantenga terminará admitiendo que hay que reescribir todo desde cero.
    • El backend y el frontend siguen existiendo. Solo que están todos enredados directamente dentro del código de la base de datos.
    • Al final volvimos al mainframe. Esta vez el navegador es solo la terminal.
  • En un proyecto anterior usé PostgREST sin HTMX, y me impresionó hasta dónde se podía llevar.
    HTMX también parece encajar bien, pero no estoy seguro de cuánto me gustaría mantener plantillas HTMX dentro de funciones SQL.

    • No me interesa en absoluto. Ya pasé por eso con PHP. A este patrón le faltan funciones importantes como análisis estático, pruebas locales, refactoring y cambios masivos entregados de forma atómica.
      Sirve para jugar y experimentar, pero no para construir algo confiable.
  • Desde la perspectiva de Haskell, PostgREST es interesante porque parece un proyecto demasiado obvio.
    Pero precisamente por eso es genial. Me gusta porque es una idea muy propia de Haskell.

  • ¿Qué herramientas adicionales creen que harían falta para convertir este concepto en un stack mantenible con buena experiencia de usuario incluso en aplicaciones medianas y grandes?

    • Para servir sitios estáticos uso Astro(https://astro.build), y para componentes simples centrados en datos uso PostgREST Htmx.
    • Para una escala mediana o grande, creo que haría falta una capa de abstracción separada, es decir, una API.
      Yo también intenté construir algo parecido sobre SQL, y el stack técnico era una capa OpenAPI implementada con SQL con plantillas Jinja y YML, pero aun así creo que lo limitaría al ámbito de herramientas internas. Está aquí: https://jinj.at
  • Show HN relacionado: renderizar HTML desde SQL con pg_render
    https://news.ycombinator.com/item?id=38677852

  • Quiero anotar una tarea en esta app para que no se me olvide. Mi tarea es esta:
    En este caso, parece que la columna task no se sanea en ningún lado.

    • En los viejos malos tiempos, como la vista hablaba directamente con la base de datos, el XSS era generalizado. Luego, al poner motores de plantillas y modelos en medio, pareció que el problema se había resuelto.
      ¿Pero ahora, como la base de datos es la vista, volvió el XSS? Esto es algo que se puede evitar.
    • Correcto. htmx ejecuta ese script con gusto. La demo tenía el mismo bug: https://github.com/bigskysoftware/htmx/pull/1995/files
    • Parece un ataque de hombre de paja contra la documentación de ejemplo. HTMX solo entrega HTML, y la entrada debe sanearse de alguna forma.
      El HTML se puede renderizar con cualquier lenguaje de plantillas que haga ese procesamiento.