PostgREST: htmxでHTMLコンテンツを提供する
(postgrest.org)- PostgreSQL関数が
text/htmlを直接返すようにすれば、PostgRESTとhtmxだけでAJAXリクエストの結果を受け取り、DOMの一部を置き換えるToDoアプリを作れる - 例では認証を使わず、
web_anonにapi.todosとapi.todos_id_seqの権限を付与し、Accept: text/htmlリクエストを処理するためにカスタムメディアタイプを追加する - HTMLの組み立てはSQL関数が担い、
api.sanitize_html、api.html_todo、api.html_all_todosが入力値のエスケープとリストのレンダリングを処理する - 画面の動作は
hx-post、hx-get、hx-target、hx-trigger、hx-vals、hx-headersで構成され、レスポンスHTMLが#todo-list-areaや個別の編集領域を置き換える - この構成は概念実証に近く、PostgREST側ではHTML処理を改善するために
plmustacheの作業が進められている
PostgRESTとhtmxの役割
- PostgRESTはHTMLコンテンツを返し、htmxはAJAXリクエストの結果として受け取ったHTMLをDOM内部要素の置き換えに使用する
- htmxはHTMLレスポンスを想定しているため、サーバーがJSONではなくHTML片を返す流れと相性がよい
- 例は、2つの技術を一緒に使う際に可能な構成を示す概念実証である
- PostgRESTはHTML処理面をさらに改善するためにplmustacheに取り組んでいる
ToDoアプリの準備設定
- 例はTutorial 0 - Get it RunningをベースにしたToDoアプリである
- 簡略化のため認証は使用せず、
web_anonユーザーに必要な権限を付与するapi.todosテーブルにgrant allapi.todos_id_seqシーケンスにgrant usage, select
- ブラウザの
Accept: text/htmlリクエストをPostgRESTがHTMLとして認識するには、メディアタイプハンドラを追加する必要がある"text/html"ドメインをtextとして作成する- この設定により、PostgRESTが生のHTMLドキュメントを返せるようになる
基本HTMLページを返す
api.index()関数は"text/html"を返し、基本HTMLドキュメントを生成する- ページには
PostgREST + HTMX To-Do Listというタイトルとともに、次のリソースが含まれる - ブラウザでは
http://localhost:3000/rpc/indexでこのページを開ける
リストのレンダリングとToDoの追加
- 既存のToDoリストをHTMLにするため、3つのSQL関数が使われる
api.sanitize_html(text):&、"、>、<、'の各文字をHTMLエンティティに置換するapi.html_todo(api.todos): 単一のToDo項目をHTML片としてフォーマットするapi.html_all_todos(): 全項目を1つのHTML文字列に結合し、項目がなければThere is nothing else to do.というメッセージを返す
api.html_todoとapi.html_all_todosはリストテンプレートを作る内部用関数であり、PostgRESTエンドポイントとして直接は使用しないapi.add_todo(_task text)は新しいToDoをapi.todosに挿入したあと、更新された全リストのHTMLを返す- 更新後の
api.index()にはhtmxと入力フォームが含まれるhx-headers='{"Accept": "text/html"}'を上位要素に入れ、下位のhtmxリクエストがHTMLレスポンスを要求するようにする- このヘッダーがないと、PostgRESTはリクエストをHTMLとして認識できない
- ToDo追加フォームはhtmx属性で動作する
hx-post="/rpc/add_todo":_taskの入力値を/rpc/add_todoへPOSTリクエストするhx-target="#todo-list-area": レスポンスHTMLをToDoリスト領域に入れるhx-trigger="submit": フォーム送信時にリクエストを送るhx-on="htmx:afterRequest: this.reset()": リクエスト完了後にフォームを空にする
- 新しい関数と変更を反映するには、スキーマキャッシュをリフレッシュする必要がある
- その後、
http://localhost:3000/rpc/indexでリスト表示と新しいToDoの追加を確認できる
編集、削除、完了状態の変更
api.html_todoは、各項目に完了状態の変更、編集、削除UIを含むよう拡張される- 完了状態の変更は
<form>とhtmx属性で処理するhx-post="/rpc/change_todo_state": 該当エンドポイントへAJAX POSTリクエストを送るhx-vals='{"_id": ..., "_done": ...}': hidden inputの代わりにリクエストパラメータを追加するhx-trigger="click": 項目クリックでリクエストを実行する
- 編集ボタンは個別のタスクを入力フィールドに変える
hx-get="/rpc/html_editable_task": 編集可能なHTML入力を返すエンドポイントを呼び出すhx-target="#todo-edit-area-...": リスト全体ではなく個別のタスク領域だけを置き換えるhx-valsはGETリクエストパラメータを追加し、テーブルカラムを表すときはeq.演算子が必要になる
- 削除ボタンは
hx-post="/rpc/delete_todo"で該当ToDoの削除リクエストを送る api.html_editable_task(_id int)は、特定のToDoを編集できる入力フィールドHTMLを返すhx-post="/rpc/change_todo_task"で変更後のタスク名を送信するhx-trigger="submit,focusout"により、送信またはフォーカスアウト時に更新するhx-headers='{"Accept": "text/html"}'を含める
- データ変更エンドポイントはいずれも、変更後に全リストのHTMLを返す
api.change_todo_state(_id int, _done boolean):doneカラムを更新するapi.change_todo_task(_id int, _task text):taskカラムを更新するapi.delete_todo(_id int):_idに該当するToDoを削除する
- スキーマキャッシュをリフレッシュしたあと、
http://localhost:3000/rpc/indexでToDoの編集、削除、完了処理ができる
1件のコメント
Hacker News のコメント
PostgREST は私が最も好きなオープンソースプロジェクトのひとつ。Supabase が10億ドル規模の会社になったのは、PostgREST と Postgres の優れた設計によるところが大きいと思う。
Supabase がこのプロジェクトをどう支援しているのかは知らないが、かなり大規模であってほしい。少なくとも数百社の売上を生む企業が中核的な依存関係として使っているプロジェクトなのに、有料支援者が12人しかいないのを見ると、オープンソースの資金支援の現実が悲しくなる。
https://www.patreon.com/postgrest/about
https://github.com/steve-chavez
だから今は AGPLv3 かそれに近いライセンスだけを使っている。商用利用したいなら総売上の1%を払え、という形だ。
概念実証としては素晴らしいし実装も称賛に値するが、些細ではない Web アプリで保守するとなると 悪夢のように見える。
https://sqitch.org/
こうした機能やコーディングパターンが、新しい、あるいはモダンなアプリケーションでも使われているのか気になる。
CouchDB という JSON ドキュメントデータベースは HTTP ベースの API を提供していて、JavaScript インタプリタ内で作れる任意の形式で応答できる一覧/詳細メソッドが組み込まれていた。つまり、クライアントがデータベースを直接呼び出して HTML や JSON を受け取ることができ、サーバーは不要だった。
しかし v1 以降はその方向の作業をやめた。保守が悪夢になるからだ。昔の PHP や ASP の1ファイルに SQL 文と HTML が混在していた良き時代を覚えている人は多いだろうが、これはあまり違って見えない。
13年ほど前、個人プロジェクトでは CouchDB の Web 関連機能が本当に好きだったが、チームで扱うにはかなり不便だった。
私が触ったことのある唯一の PostgREST アプリはひどかった。というのも、この手の「シンプルな」フレームワークの大半がそうであるように、要件が複雑になる前までしかシンプルではないからだ。
元の開発者たちは望む結果を得るためにデータベースにストアドプロシージャを大量に書くようになり、それがスケーラビリティの問題につながった。いつものことだが、解決策は SQL に戻ることだ。
PostgREST は CRUD アプリのために作られており、私が目にするアプリケーションの多くはこれに当てはまる。たまに必要になるカスタムのバックエンドロジックは、別サーバーやエッジ関数として立てて処理すればよい。
こうしたツールはどれもそうだが、現実の要件に向き合うと、ツールに合わせて適応するコストが、置き換えようとしていた SQL + 何らかの言語とフレームワークより悪くなる。PostGREST もすでに複雑化しているので、こういう追加機能は自分にとって魅力を下げる。
Rails が登場して、遅いサーバーサイド言語にビジネスロジックを入れるのが良い考えだと皆に信じさせる前は、みんなこういうふうにアプリを作っていた。
本当にすっきりした Web 開発スタックだ。HTML とデータベースだけで、バックエンドもフロントエンドも要らない。
https://github.com/omnigres/omnigres
HTMX の輝きが薄れたら、その時の管理者は結局すべてを最初から書き直す必要があると認めることになる。
以前のプロジェクトで HTMX なしに PostgREST を使ったことがあるが、どこまで押し進められるのかは印象的だった。
HTMX とも相性はよさそうだが、SQL 関数の中で HTMX テンプレートをどれほど保守したいかはよく分からない。
遊びで作るにはよいが、安定したものを作るには不向きだ。
Haskell の観点から見ると PostgREST は面白い。あまりにも当然のプロジェクトに見えるからだ。
だが、まさにそれゆえに天才的だ。本当に Haskell らしいアイデアなので好きだ。
この概念を中〜大規模アプリケーションでも優れたユーザー体験を備えた保守可能なスタックにするには、どんな追加ツールが必要だと思うか?
私も SQL の上に似たものを作ろうとしていて、技術スタックは Jinja テンプレート SQL と YML で実装した OpenAPI レイヤーだったが、それでも社内ツールの範囲に限定することになりそう。ここにある: https://jinj.at
関連 Show HN: pg_render で SQL から HTML をレンダリングする
https://news.ycombinator.com/item?id=38677852
このアプリに忘れないようタスクを1つ書いておきたい。私のタスクはこれ:
この場合、task カラムがどこでもサニタイズされていないように見える
ところが今度はデータベースがビューなので、また XSS が発生しているということか? これは防げる問題だ
HTML は、そうした処理を行う任意のテンプレート言語でレンダリングできる