PostgREST: servir contenido HTML con htmx
(postgrest.org)- 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.todosyapi.todos_id_seqaweb_anon, y agrega un tipo de medio personalizado para procesar solicitudes conAccept: text/html - El armado del HTML queda a cargo de funciones SQL;
api.sanitize_html,api.html_todoyapi.html_all_todosse 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-valsyhx-headers; el HTML de respuesta reemplaza#todo-list-areao á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
plmustachepara 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_anongrant allsobre la tablaapi.todosgrant usage, selectsobre la secuenciaapi.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"comotext - Con esta configuración, PostgREST puede devolver documentos HTML sin procesar
- Se crea el dominio
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 Listy 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 HTMLapi.html_todo(api.todos): da formato a un único ítem de tarea como fragmento HTMLapi.html_all_todos(): combina todos los ítems en una sola cadena HTML y, si no hay ítems, devuelve el mensajeThere is nothing else to do.
api.html_todoyapi.html_all_todosson funciones internas para crear la plantilla de la lista y no se usan directamente como endpoints de PostgRESTapi.add_todo(_task text)inserta una nueva tarea enapi.todosy 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
- Se agrega
- El formulario para agregar tareas funciona con atributos de htmx
hx-post="/rpc/add_todo": envía por POST el valor de entrada_taska/rpc/add_todohx-target="#todo-list-area": inserta el HTML de respuesta en el área de la lista de tareashx-trigger="submit": envía la solicitud al enviar el formulariohx-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_todose 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 htmxhx-post="/rpc/change_todo_state": envía una solicitud AJAX POST a ese endpointhx-vals='{"_id": ..., "_done": ...}': agrega parámetros a la solicitud en lugar de usar entradas ocultashx-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 editablehx-target="#todo-edit-area-...": reemplaza solo el área de la tarea individual, en lugar de toda la listahx-valsagrega parámetros a la solicitud GET; al representar columnas de la tabla, se necesita el operadoreq.
- 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"}'
- Envía el nombre de tarea modificado con
- 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 columnadoneapi.change_todo_task(_id int, _task text): actualiza la columnataskapi.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
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
https://github.com/steve-chavez
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.
https://sqitch.org/
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.
Hace unos 13 años me encantaban las funciones web de CouchDB en un proyecto personal, pero para un equipo eran bastante incómodas.
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.
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.
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í.
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.
https://github.com/omnigres/omnigres
Cuando el brillo de HTMX se desvanezca, quien lo mantenga terminará admitiendo que hay que reescribir todo desde cero.
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.
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?
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.
¿Pero ahora, como la base de datos es la vista, volvió el XSS? Esto es algo que se puede evitar.
El HTML se puede renderizar con cualquier lenguaje de plantillas que haga ese procesamiento.