🐡

DBに向けるだけでREST APIと管理UIが生える単一バイナリのGoツール「adms」を作った

に公開

TL;DR

  • adms は、PostgreSQL / MySQL に接続するだけで PostgREST 風の HTTP API を自動で生やし、さらに ui.enabled: true管理 UI まで同じバイナリから配信する Go 製ツールです(読みは "adams")。
  • 単一バイナリ。UI(HTML/CSS/JS + tree-shake 済み Tailwind)は embed.FS で埋め込み済みなので、node_modules も別デプロイも不要。
  • 先日 v0.0.1 を出しました。MIT。

adms の管理 UI

なにこれ

管理ツールを作るたびに、毎回だいたい同じものを書いている気がしませんか。

  1. バックエンドは汎用的なのに、毎回書いている。 一覧エンドポイント、フィルタ、ソート、ページング、関連レコード、CRUD……形はスキーマにもう書いてあるのに、プロジェクトごとに手で書き直している。
  2. フロントもいずれ汎用化する。 API ができると、ダッシュボードは結局「フィルタとフォームのついたテーブル」になる。React アプリを立ち上げ、コンポーネントライブラリを選び、配線する——これがまた別プロジェクトになる。

adms はこの 2 つを 1 つのバイナリに畳み込みます。起動時にDBをイントロスペクトして、PostgREST 風の HTTP API を自動で公開します。設定で ui.enabled: true にすると、同じバイナリが管理フロントエンドまで配信します。追加のデプロイも別リポジトリも要りません。

一番近い隣人は PostgREST 自身ですが、PostgREST は PostgreSQL 専用 / API 専用です。adms は PostgreSQL + MySQLAPI +(任意の)UI を、バイナリ以外の依存なしで狙っています。

クイックスタート

adms は YAML / TOML の設定ファイルを読みます。引数なしならカレントの adms.yamladms.ymladms.toml を自動検出、明示するならパスを渡します。設定内の文字列は ${VAR} で環境変数展開されるので、シークレットをファイルに書かずに済みます。

# adms.yaml
driver: postgres
dsn: "${ADMS_DSN}"
ADMS_DSN="postgres://postgres@localhost:5432/myapp?sslmode=disable" adms

起動するとDBをイントロスペクトしてメモリ上にスキーマモデルを構築し、:7777 で待ち受けます。イントロスペクトした各テーブルが /<table_name> のリソースになります。

curl http://localhost:7777/                  # スキーマのダンプ (JSON)
curl http://localhost:7777/healthz           # → "ok"
curl http://localhost:7777/<some_table>      # 先頭 100 行を JSON で

MySQL なら driver: mysql にして DSN を差し替えるだけです。

HTTP API

GET /<table> が行を返します。フィルタ・射影・並び替え・ページング・埋め込みはすべて URL のクエリパラメータで表現します。書き込みは POST / PATCH / DELETE に JSON ボディ。形は PostgREST に寄せてあるので、既存のクライアントや頭の中のモデルがそのまま使えます。

フィルタ

?<column>=<op>.<value> の形。複数指定は AND。

curl 'http://localhost:7777/users?status=eq.active&age=gte.18&deleted_at=is.null'

eq / neq / gt / gte / lt / lte / like / ilike / in / is、否定の not.、JSON・配列の包含 cs / cd などに対応。like / ilike のワイルドカードは *% に変換)です。

射影・埋め込み

# 必要な列だけ
curl 'http://localhost:7777/users?select=id,name,email'

# FK をたどって関連行を埋め込む(posts.user_id → users.id)
curl 'http://localhost:7777/users?id=eq.1&select=id,name,posts(id,title,created_at)'

書き込み

POST / PATCH / DELETE に JSON ボディ。Prefer ヘッダや Content-Range にも対応。エラーは PostgREST 風の JSON エンベロープ(ADMS_ プレフィックスのコード)で返します。PATCH / DELETE はフィルタ必須(無条件書き込みを弾く)など、管理ダッシュボード寄りの安全側の既定になっています。

管理 UI

ui.enabled: true:7778ui.listen で変更可)に管理 UI が立ちます。API の :7777 はそのまま。UI は API を叩く「最初のクライアント」で、2 つのリスナ間の CORS は自動設定されます。

できること:

  • サイドバー — スキーマごとにグルーピングしたテーブル一覧 + インクリメンタル検索
  • テーブルビュー — PostgREST 風フィルタ入力、列ヘッダソート、ページング、FK 矢印で参照先へジャンプ、現在の絞り込み結果を CSV / JSON エクスポート
  • キーボードCmd/Ctrl+K でテーブルパレット、 で行選択、Enter で開く
  • 行編集 — セルをダブルクリックでインライン編集、または行の "edit" でモーダル編集
  • 挿入 — 型に応じた入力フォーム(boolean は <select>、数値・日付は専用 input、JSON は <textarea>
  • スキーマビュー/t/{table}/schema で列・PK・FK・被参照・インデックスを表示
  • スキーマ図/schema に全テーブルと FK の ER 図。ドラッグでパン、スクロールでズーム、ノードクリックでそのテーブルへ
  • ライト / ダークテーマ — OS 設定に追従、トグルで上書き(ブラウザごとに記憶)
  • read-only ゲーティングread_only: true で書き込み UI を丸ごと隠す

技術的に面白かったところ(Go 目線)

UI ごと 1 バイナリに埋め込む

UI の HTML / CSS / JS は embed.FS でバイナリに焼き込んでいます。CSS は Tailwind の standalone CLI でビルド時に tree-shake + minify したものを同梱するので、閉じたネットワークでも CDN 不要。node_modules もフロントの別デプロイもありません。レンダリングは Go の html/template による SSR で、インタラクションは素の fetch。いわゆる SPA フレームワークは使っていません。

ルーティングは標準の net/http の拡張パターン(mux.HandleFunc("GET /{table}", ...))だけで組んでいて、外部ルータも入れていません。

PostgreSQL と MySQL の両対応

ドライバは PostgreSQL に pgx、MySQL に go-sql-driver/mysql。どちらも pure Go なので CGO なし、当然 Haskell ランタイム(PostgREST)のような別物を配る必要もありません。方言差(識別子のクオート、ILIKE 相当、真偽値の扱いなど)は dialect 層に閉じ込めています。

ER 図を「外部 JS ライブラリなし」で描く

管理 UI は CSP・バンドルの都合で外部 JS ライブラリを持ち込まない方針にしています。なので ER 図のレイアウトは Go 側で決定論的に計算して、インライン SVG として吐いています。

  • 力学モデル(Fruchterman-Reingold)でノード(テーブル)を配置。初期配置は黄金角スパイラル、反復回数は固定、乱数は使わない → 同じスキーマなら常に同じ絵になり、テストもできる。
  • エッジ(FK)は箱の境界でクリップして矢印を付与。自己参照 FK は円弧の <path> で描画。
  • 配色は Tailwind の fill-* / stroke-*dark: バリアントを付けてテーマ対応。
  • パン / ズーム / ノードクリック遷移だけは軽量な vanilla JS で、レイアウト計算には手を出さない。

サーバ側計算にしたのは、SSR + 文字列アサーションという既存のテスト方針に乗るのと、決定論性を担保しやすいからでした。

PostgREST との使い分け

PostgREST が向いているのは、PostgreSQL 一本で、実績豊富で大きなコミュニティのあるプロジェクトが欲しい場合です(実際とても優秀で、adms も URL 規約を大きく借りています)。

adms を選ぶのは:

  • MySQL を使っている、または PostgreSQL と MySQL 混在のフリートを 1 つのサーバで面倒みたい
  • API と同じバイナリに UI も同梱したい(別フロントプロジェクトを持ちたくない)
  • Haskell ランタイム不要の、自己完結した Go バイナリ 1 つでデプロイしたい
  • 管理ダッシュボード向けに絞ったスコープ(意見の強い既定、識別子の allowlist、書き込み時のフィルタ必須)が欲しい

おわりに

v0.0.1 を出したばかりです。フィードバック・Issue・PR お待ちしています 🙏

Discussion