3 pontos por GN⁺ 2023-12-19 | 1 comentários | Compartilhar no WhatsApp
  • Quando uma função do PostgreSQL retorna text/html diretamente, é possível criar um app de to-do que recebe o resultado de requisições AJAX e substitui partes do DOM usando apenas PostgREST e htmx
  • O exemplo concede permissões de api.todos e api.todos_id_seq a web_anon sem autenticação e adiciona um tipo de mídia personalizado para processar requisições com Accept: text/html
  • A montagem do HTML fica a cargo de funções SQL, e api.sanitize_html, api.html_todo e api.html_all_todos cuidam do escape das entradas e da renderização da lista
  • O comportamento da interface é composto com hx-post, hx-get, hx-target, hx-trigger, hx-vals e hx-headers, e o HTML de resposta substitui #todo-list-area ou áreas individuais de edição
  • Essa configuração é mais próxima de uma prova de conceito, e o lado do PostgREST está trabalhando em plmustache para melhorar o processamento de HTML

Papel do PostgREST e do htmx

  • O PostgREST retorna conteúdo HTML, e o htmx usa o HTML recebido como resultado de requisições AJAX para substituir elementos dentro do DOM
  • Como o htmx espera respostas em HTML, ele combina bem com um fluxo em que o servidor devolve fragmentos HTML em vez de JSON
  • O exemplo mostra uma configuração possível ao usar as duas tecnologias juntas como uma prova de conceito
  • O PostgREST está desenvolvendo plmustache para melhorar ainda mais o tratamento de HTML

Configuração inicial do app de to-do

  • O exemplo é um app de to-do baseado em Tutorial 0 - Get it Running
  • Para simplificar, ele não usa autenticação e concede ao usuário web_anon as permissões necessárias
    • grant all na tabela api.todos
    • grant usage, select na sequência api.todos_id_seq
  • Para que o PostgREST reconheça requisições do navegador com Accept: text/html como HTML, é preciso adicionar um handler de tipo de mídia
    • criar o domínio "text/html" sobre text
    • com essa configuração, o PostgREST pode retornar documentos HTML brutos

Retorno da página HTML básica

  • A função api.index() retorna "text/html" e gera o documento HTML básico
  • A página inclui o título PostgREST + HTMX To-Do List junto com os seguintes recursos
  • No navegador, essa página pode ser aberta em http://localhost:3000/rpc/index

Renderização da lista e adição de to-do

  • Três funções SQL são usadas para gerar a lista de to-dos em HTML
    • api.sanitize_html(text): substitui os caracteres &, ", >, < e ' por entidades HTML
    • api.html_todo(api.todos): formata um único item de to-do como fragmento HTML
    • api.html_all_todos(): junta todos os itens em uma única string HTML e, se não houver itens, retorna a mensagem There is nothing else to do.
  • api.html_todo e api.html_all_todos são funções internas para montar o template da lista e não são usadas diretamente como endpoints do PostgREST
  • api.add_todo(_task text) insere um novo to-do em api.todos e depois retorna o HTML da lista completa já atualizada
  • A versão atualizada de api.index() inclui htmx e o formulário de entrada
    • hx-headers='{"Accept": "text/html"}' é colocado no elemento pai para que as requisições htmx filhas solicitem respostas em HTML
    • sem esse cabeçalho, o PostgREST não reconhece a requisição como HTML
  • O formulário para adicionar to-dos funciona com atributos do htmx
    • hx-post="/rpc/add_todo": envia via POST o valor de entrada _task para /rpc/add_todo
    • hx-target="#todo-list-area": insere o HTML de resposta na área da lista de to-dos
    • hx-trigger="submit": envia a requisição ao submeter o formulário
    • hx-on="htmx:afterRequest: this.reset()": limpa o formulário após a conclusão da requisição
  • É preciso atualizar o cache de esquema para refletir as novas funções e alterações
  • Depois disso, é possível verificar a exibição da lista e a adição de novos to-dos em http://localhost:3000/rpc/index

Edição, remoção e alteração do estado de conclusão

  • api.html_todo é expandida para incluir, em cada item, a interface de alteração de estado, edição e remoção
  • A alteração do estado de conclusão é tratada com <form> e atributos do htmx
    • hx-post="/rpc/change_todo_state": envia uma requisição AJAX POST para esse endpoint
    • hx-vals='{"_id": ..., "_done": ...}': adiciona parâmetros à requisição em vez de usar campos ocultos
    • hx-trigger="click": executa a requisição ao clicar no item
  • O botão de edição transforma a tarefa individual em um campo de entrada
    • hx-get="/rpc/html_editable_task": chama o endpoint que retorna um campo HTML editável
    • hx-target="#todo-edit-area-...": substitui apenas a área da tarefa individual, e não a lista inteira
    • hx-vals adiciona parâmetros à requisição GET e, ao representar uma coluna de tabela, é necessário o operador eq.
  • O botão de remoção envia uma solicitação para excluir o to-do correspondente com hx-post="/rpc/delete_todo"
  • api.html_editable_task(_id int) retorna o HTML de um campo de entrada para editar um to-do específico
    • envia o nome da tarefa alterado com hx-post="/rpc/change_todo_task"
    • atualiza em hx-trigger="submit,focusout" ao submeter ou ao perder o foco
    • inclui hx-headers='{"Accept": "text/html"}'
  • Todos os endpoints que alteram dados retornam o HTML da lista completa após a modificação
    • api.change_todo_state(_id int, _done boolean): atualiza a coluna done
    • api.change_todo_task(_id int, _task text): atualiza a coluna task
    • api.delete_todo(_id int): remove o to-do correspondente a _id
  • Após atualizar o cache de esquema, é possível editar, remover e marcar to-dos como concluídos em http://localhost:3000/rpc/index

1 comentários

 
GN⁺ 2023-12-19
Opiniões no Hacker News
  • PostgREST é um dos meus projetos open source favoritos. Acho que a Supabase ter se tornado uma empresa de US$ 1 bilhão se deve em grande parte ao excelente design do PostgREST e do Postgres.
    Não sei como a Supabase patrocina esse projeto, mas espero que seja em uma escala bem grande. É triste ver a realidade do financiamento de open source quando um projeto usado como dependência central por pelo menos centenas de empresas que geram receita tem só 12 patrocinadores pagos.
    https://www.patreon.com/postgrest/about

    • Como é uma parte central da stack da Supabase, como você mencionou, eles empregam o mantenedor líder, Steve, para trabalhar principalmente no PostgREST.
      https://github.com/steve-chavez
    • Agora são 13. Ainda assim, são apenas US$ 1.529/mês
    • Eu não sabia que o PostgREST era parte da Supabase. Achei que a Supabase fosse um produto copiado, feito do zero.
    • Já consigo ouvir as frases batidas que sempre aparecem nessas situações. Coisas como “os usuários não têm obrigação de te apoiar financeiramente” ou “se não gostava, não deveria ter publicado sob uma licença permissiva”.
      Por isso, agora só uso AGPLv3 ou licenças parecidas. Algo como: se quiser usar comercialmente, pague 1% da receita bruta.
  • Como prova de conceito é legal, e a implementação merece elogios, mas para manter em uma web app que não seja trivial parece um pesadelo.

    • Definitivamente parece mais voltado a sites ou apps leves. Ainda assim, dentro desse escopo, dá para aproveitar bastante.
    • No momento estou experimentando com Sqitch para ver se dá para facilitar a manutenção. Ainda parece um hack, e continuo com dúvidas sobre a viabilidade em uso real, mas é divertido e estou aprendendo vários recursos avançados do Postgres.
      https://sqitch.org/
    • Sinceramente, fico curioso para saber qual parte da nossa aplicação web principal não ficaria 100 vezes mais fácil usando isso.
    • Acho que, por enquanto, é isso. Agora só falta haver uma forma de compilar e implantar as rotas HTMX no servidor de banco de dados como parte do processo de CI.
  • Fico me perguntando se esse tipo de recurso ou padrão de código é novo, ou se também é usado em aplicações modernas.
    O CouchDB, um banco de dados de documentos JSON, oferecia uma API baseada em HTTP e tinha métodos embutidos de lista/detalhe que podiam responder em qualquer formato que fosse possível criar dentro de um interpretador JavaScript. Ou seja, o cliente podia chamar o banco de dados diretamente e receber HTML ou JSON, sem precisar de servidor.
    Mas, depois da v1, eles pararam de seguir nessa direção porque a manutenção virava um pesadelo. Muita gente deve se lembrar dos bons tempos em que um único arquivo PHP ou ASP misturava instruções SQL e HTML; isto não parece muito diferente.

    • Parece que uma nova geração está reaprendendo coisas que foram abandonadas no passado por bons motivos.
      Uns 13 anos atrás, eu adorava os recursos web do CouchDB em um projeto pessoal, mas eles eram bem inconvenientes para uma equipe lidar.
    • Difícil chamar isso de moderno. A Oracle já tinha esse tipo de coisa há mais de 25 anos.
  • O único app PostgREST em que trabalhei foi horrível. Porque, como a maioria desses frameworks “simples”, eles só são simples até os requisitos ficarem complexos.
    Os autores originais acabaram usando um monte de stored procedures no banco de dados para conseguir os resultados desejados, e isso levou a problemas de escalabilidade. Como sempre, a solução é voltar para SQL.

    • Parece que essa equipe escolheu uma ferramenta inadequada para o trabalho. Se a maioria dos endpoints tem lógica de backend complexa, não se deve usar PostgREST.
      O PostgREST foi criado para apps CRUD, e muitas das aplicações que encontro se encaixam nisso. A lógica de backend customizada que às vezes é necessária pode ser tratada em um servidor separado ou em edge functions.
    • Uso bastante PostGREST, mas com guardrails fortes. Acho que a camada de API real deve sempre existir, e isto deve ser usado apenas como um auxílio para facilitar o carregamento básico.
      Como acontece com todas essas ferramentas, quando se chega aos requisitos reais, o custo de se adaptar à ferramenta fica pior do que aquilo que ela tentava substituir: SQL + alguma linguagem e framework. O PostGREST já está ficando complexo, e esses recursos adicionais o tornam menos atraente para mim.
    • Stored procedures também não são, no fim das contas, SQL em forma de função?
      Antes de o Rails aparecer e fazer todo mundo acreditar que colocar lógica de negócio em uma linguagem lenta do lado do servidor era uma boa ideia, todos criavam apps desse jeito.
  • É uma stack de desenvolvimento web realmente limpa. Só HTML e banco de dados, sem precisar de backend nem frontend.

    • Visitei a Compuserve, em Ohio, em meados dos anos 90. A web estava apenas começando, e um engenheiro que conheci lá me mostrou uma loja de música que ele estava construindo retornando HTML a partir de stored procedures SQL :)
    • Se isso parece legal para você, vale conferir também o Omnigres.
      https://github.com/omnigres/omnigres
    • Já passamos por esse caminho. No começo é limpo, mas não é bom para manutenção de longo prazo, suporte e treinamento.
      Quando o brilho do HTMX passar, o mantenedor de então acabará admitindo que tudo precisa ser reescrito do zero.
    • Backend e frontend ainda existem. Só que estão todos diretamente emaranhados dentro do código do banco de dados.
    • No fim, voltamos ao mainframe. Desta vez, o navegador é apenas o terminal.
  • Usei PostgREST sem HTMX em um projeto antigo, e foi impressionante ver até onde dava para levar.
    HTMX também parece combinar bem, mas não sei o quanto eu gostaria de manter templates HTMX dentro de funções SQL.

    • Não tenho nenhum interesse. Já passei por isso com PHP. Esse padrão não tem recursos importantes como análise estática, testes locais, refatoração e grandes mudanças entregues de forma atômica.
      É bom para brincar, mas inadequado para construir algo confiável.
  • Do ponto de vista de Haskell, PostgREST é interessante. Porque parece um projeto óbvio demais.
    Mas é justamente por isso que é genial. Gosto porque é uma ideia muito Haskell.

  • Que ferramentas adicionais você acha necessárias para transformar esse conceito em uma stack sustentável com boa experiência de usuário também em aplicações de médio e grande porte?

    • Para servir sites estáticos, uso Astro(https://astro.build); para componentes simples centrados em dados, uso PostgREST Htmx
    • Em uma escala média ou grande, acho que é necessária uma camada de abstração separada, ou seja, uma API
      Eu também tentei criar algo parecido sobre SQL; a stack técnica era uma camada OpenAPI implementada com SQL em templates Jinja e YML, mas ainda assim acho que ficaria limitada ao escopo de ferramentas internas. Está aqui: https://jinj.at
  • Show HN relacionado: renderizar HTML a partir de SQL com pg_render
    https://news.ycombinator.com/item?id=38677852

  • Quero anotar uma tarefa para não esquecer neste app. Meu trabalho é este:
    Neste caso, a coluna task não parece ser sanitizada em lugar nenhum

    • Nos velhos tempos ruins, como a view conversava diretamente com o banco de dados, XSS era algo disseminado. Depois, ao colocar template engines e models no meio, pareceu que o problema tinha sido resolvido
      Mas agora, como o banco de dados é a view, o XSS voltou? Isso é um problema que dá para evitar
    • Exato. O htmx executa esse script de bom grado. A demo também tinha o mesmo bug: https://github.com/bigskysoftware/htmx/pull/1995/files
    • Parece um ataque ao espantalho contra a documentação de exemplo. O HTMX só serve HTML, e a entrada precisa ser sanitizada de alguma forma
      O HTML pode ser renderizado com qualquer linguagem de templates que faça esse tratamento