しんのきです。
前回の記事で Hasura Allow List についてのの運用方法を考察しました。
次に世代管理ですが、
v1
v2
のようなバージョン管理だと崩壊しそうなので日付ベースの名前をつけたほうがよさそうです。そこで使わなくなったオペレーションは
<クライアント名>/<ロール名>/<オペレーション名>~<使わなくなった日時>
のように名前を変更するとよさそうです。
命名規則については考察できたのですが、 Allow List を管理するために追加で手作業が発生してしまうため、どうしても開発スピードが落ちたりヒューマンエラーが発生しそうで導入を足踏みしておりました。
しかし先日の GraphQL Tokyo Meetup #10 にて Codegen の自作に関する LT に影響を受け、 yaml ファイルで管理される Allow List の metadata を自動生成するという着想を得たのでやってみました。
#GraphQLTokyo 今日のLT資料です。 https://t.co/wLzGcjFGAchttps://t.co/TcuYa88Yp5 も興味がある人は使ってみてください
— gql`quramy { name }` (@Quramy) 2020年8月28日
Meetup の様子は全て YouTube にアーカイブとして残っているのでぜひ御覧ください!
(しんのきもスピーカーとして参加しました)
Hasura が生成する metadata ファイル
Hasura は v1.2.0 以降で導入された config v2 から、データベースのマイグレーションは migrations
ディレクトリに .sql
ファイルとして、それ以外の Hasura に関する設定は metadata
ディレクトリに .yaml
や .graphql
ファイルとして分けて出力されるようになりました。
(config v1 では全て同じディレクトリ内にあったので、 v2 になって分かりやすくなりパフォーマンスも向上しました)
Allow List に関係するのはmetadata/allow_list.yaml
と metadata/query_collections.yaml
という 2 つのファイルです。
metadata/allow_list.yaml
は以下の内容で基本的に更新されることはありません。
- collection: allowed-queries
実際に Allow List に登録されたクエリは以下のような内容で metadata/query_collections.yaml
に保存されていきます。
- name: allowed-queries definition: queries: - name: Posts query: | query Posts { posts: post { id body created_at } } - name: CreatePost query: | mutation CreatePost($object: post_insert_input!) { insert_post_one(object: $object) { id body created_at } }
metadata/query_collections.yaml
は Allow List だけでなくクエリを保存しておく全般の用途で使われるようですが、現状で Allow List 以外に使われることがあるのかは不明です。
この metadata/query_collections.yaml
を自動生成することができればかなり楽ができそうです。
(失敗した方法)GraphQL Code Generator の独自プラグインを実装する
現在 Hasura を採用している今のプロジェクトでは GraphQL Code Generator を使っているのですが、 GraphQL Code Generator では Custom Plugin として自分で書いたコードを差し込むことができるので、まずはこの仕組みに乗っかることができないか試してみました。
ドキュメントに書いてある通りにやってみたところ、かなり簡単に Custom Plugin を実装できました。
実際に yaml を返すプラグインを generate-allow-list.js
という名前で以下のように実装しました。(yaml
パッケージを利用しています)
const path = require("path"); const YAML = require("yaml"); module.exports = { plugin: (schema, documents, config, info) => YAML.stringify([ { name: "allowed-queries", definition: { queries: documents.map((doc) => ({ // doc.location が絶対パスとして入ってくるので相対パスに直し // 拡張子を削除してクエリ名に使う name: path.relative(__dirname, doc.location).replace(/\.graphql$/, ""), // query には生のクエリを渡す query: doc.rawSDL, })), }, }, ]), };
codegen.yaml
で以下のように指定し実装した Custom Plugin が動くように設定します。
overwrite: true schema: "http://localhost:8080/v1/graphql" documents: "**/*.graphql" generates: hasura/metadata/query_collections.yaml: - generate-allow-list.js
非常にスッキリ書けたのですが、実際に試しているうちに GraphQL Code Generator では同名の名前付き GraphQL オペレーションを複数読み込めないということが分かりました。
前回の記事で見たように Allow List を運用していくには同名のオペレーションを複数保存していって世代管理していきたかったので、これでは要件を満たすことができませんでした。
glob
と fs
を使って愚直にファイルを読み込む
もはや GraphQL の Codegen とは関係がなくなってしまっていますが、今回は GraphQL オペレーションの内容には興味がなくファイルの中身だけとれればいいので、 glob
と fs
を使って必要なファイルを読み込むだけでも十分使えるものが実装できました。
このときの generate-allow-list.js
は以下のようになります。
const fs = require("fs"); const glob = require("glob"); const YAML = require("yaml"); // クエリリストを生成 const paths = glob.sync("**/*.graphql"); const queries = paths.map((path) => ({ // 拡張子を削除してクエリ名に使う name: path.replace(/\.graphql$/, ""), // query にはファイルの中身をそのまま渡す query: fs.readFileSync(path, "utf8").toString(), })); // メタデータ保存 fs.writeFileSync( "hasura/metadata/query_collections.yaml", YAML.stringify([{ name: "allowed-queries", queries }]) );
GraphQL Code Generator とは関係ないので、 codegen
コマンド時に実行されるように package.json
の scripts
を修正する必要があります。
(GraphQL Code Generator の Lifecycle Hooks に設定してもいいかもしれません)
{ ... "scripts": { "codegen": "graphql-codegen && node generate-allow-list.js", } }
Allow List の世代管理を実装する
fs
を使えば更新前の metadata/query_collections.yaml
の内容を取得して比較することもできるので、ちょっと頑張って前回考察した命名規則を使った世代管理についても自動化できるように実装しました。
そこで使わなくなったオペレーションは
<クライアント名>/<ロール名>/<オペレーション名>~<使わなくなった日時>
のように名前を変更するとよさそうです。
最終的な generate-allow-list.js
は以下のようになりました。
const fs = require("fs"); const glob = require("glob"); const YAML = require("yaml"); const dayjs = require("dayjs"); const GLOB_PATTERN = "**/*.graphql"; const METADATA_PATH = "hasura/metadata/query_collections.yaml"; // クエリリストを生成 const paths = glob.sync(GLOB_PATTERN); const queries = paths.map((path) => ({ // 拡張子を削除してクエリ名に使う name: path.replace(/\.graphql$/, ""), // query にはファイルの中身をそのまま渡す query: fs.readFileSync(path, "utf8"), })); // 現在のクエリリストを取得 const currentQueries = YAML.parse(fs.readFileSync(METADATA_PATH, "utf8")).find( (item) => item.name === "allowed-queries" ).definition.queries; // 互換用クエリリストを生成 const timestamp = dayjs().format("YYYYMMDDHHmmss"); const compatQueries = currentQueries.flatMap((q) => queries.some(({ query }) => query === q.query) ? // 同じ内容のクエリがあれば無視する [] : // クエリ名にタイムスタンプが付いていなければ付与して保存 [ { name: q.name.match(/~\d+$/) ? q.name : `${q.name}~${timestamp}`, query: q.query, }, ] ); // クエリリストをマージして名前順にソートし直す const mergedQueries = [...queries, ...compatQueries].sort((q1, q2) => q1.name.localeCompare(q2.name) ); // メタデータ保存 fs.writeFileSync( METADATA_PATH, YAML.stringify([ { name: "allowed-queries", definition: { queries: mergedQueries, }, }, ]) );
この状態でメタデータを生成し hasura metadata apply
するといい感じに Allow List に反映できました。
一覧性もよく管理しやすそうです。
開発していると古いクエリがどんどん溜まっていくので、フロントエンドの更新が行き渡り完全に使われることが無くなってから metadata/query_collections.yaml
から手動で取り除くことで完全に削除されます。
まとめ
ようやく開発効率を落とさずに Allow List を導入できそうです!
Hasura Cloud を使うと Allow List 機能が強化されブロックしたオペレーションを確認してその場で Allow List に登録できるようになりますが、それでもデプロイ直後のエラーは避けられないので今回のやり方は有用かと思われます。
普通に使っていると Allow List の管理は面倒だと思うのですが、現実的な運用方法に関する情報がどこにも転がっていなく、ここまでたどり着くまでだいぶ苦戦しました。
これでしばらく運用してみて、良さそうだったら npm パッケージとして公開したり英語の記事も書きたいですね。