TypeScript & React & Firebase で何かつくってみる10 Firestore 2
タイトルの「何か」の部分が徐々に固まってきた. この先も続けられそうならタイトルを変えようかな.
rules 内でデータベース内の内容を参照する
今の所 漠然と「トランプを操作するアプリ」をつくっているが, 目指しているのは 特定のトランプゲーム ではない.
一応ゴールは「任意のカードゲームができるアプリ」だ. ここで詳細は省くが, 「どの山からどの山への移動を 許可・禁止するか」 は 最終的には利用者が自分で設定できるようにしたい.
つまり, ユーザーが用意したカスタムルールを読みこめるようにしたい. firestore.rules でそれができそうだ.
rules 内で get() を使えば データベース内の任意のデータを読むことができる.
match /actions/{actionId} {
allow read;
allow write: if request.resource.data.to in get(/databases/{database}/documents/actions/rules).data.moves[request.resource.data.from];
}
なお, 上の書き方は間違っているため真似してはならない. 正しくはこうだ.
match /actions/{actionId} {
allow read;
allow write: if request.resource.data.to in get(/databases/$(database)/documents/actions/rules).data.moves[request.resource.data.from];
}
何が違うかおわかりだろうか. 私自身ここにつまづいて少々時間をロスしてしまった.
答えは, get() の引数が {database} か $(database) かの違いだ.
私は完全に勘違いしていた. どちらも ワイルドカード だと思っていたが, {database} はワイルドカードで $(database) は変数の埋め込みだ.
つまり意味が逆だ. {database} は本来そこにあった文字列を database という変数に設定している. 一方 $(database) は 変数 database の値をパスに埋め込んでいる.
間違えてもだれも指摘してくれないし, デバッグログも出せないので 複雑な ルールはデバッグが難しそうだ.
さて.
先の書き込みルール適用にあたり, actions/rules に以下のようなドキュメントを登録しておく.
moves は Map<string, string[]> のような構造にしてある. 上の例は
・ alice から deck への移動を許可
・ deck から alice , bob への移動を許可
という意味で書いた.
このデータは get(/databases/$(database)/documents/actions/rules).data によって参照することができる.
先の移動条件式
if request.resource.data.to in get(/databases/$(database)/documents/actions/rules).data.moves[request.resource.data.from];
長すぎるので略記する
if request..to in get(..).data.moves[request..from];
これで, 「カスタムルールから許可する宛先一覧を取り出して その中に含まれれば OK」 というルールになっているはずだ.
動作確認して意図どおりとなることが確認できた.
rules 内でのドキュメント参照先を動的に切り替える
先に述べたとおり, get() の引数には変数を埋め込むことができる.
そこでこんな実験をしてみた.
'fields/deck', 'fields/alice', 'fields/bob' に allowedDest 変数を追加する.
このようにカードの山自体に 「許可する移動先」 を設定して, これを rules から参照してみる.
function getAllowedDest(deckName) {
return get(/databases/$(database)/documents/fields/$(deckName)).data.allowedDest;
}
match /actions/{actionId} {
allow read;
allow write: if request.resource.data.to in getAllowedDest(request.resource.data.from);
}
これでも意図したとおりに動くことが確認できた.
おいおいユーザー権限的なものも考える必要がありそうだが・・・それは後回しにする.
actions のトリガー実行に統一する
以前実装したシャッフルも 'actions' を経由して実行する方式にする.
こちらに統一すればコマンドごとに WebAPI 公開せずに済むし, 'actions' を残しておけばそれがそのままユーザーアクションの記録にもなる.
ちょくちょく反応が鈍いのが気になるが, あとで改善できることを期待する.
CardGame.tsx (クライアント側コード) では ユーザーアクションを区別するために 追加するデータに type 要素を加える.
const actionsRef = firestore.collection('actions');
const shuffle = (target: string) : React.MouseEventHandler<HTMLButtonElement> => async (e) => {
const value = await actionsRef.add({type: 'shuffle', target});
};
firestore.rules は この type を見て判断する.
match /actions/{actionId} {
allow read;
allow write: if request.resource.data.type == 'move' && request.resource.data.to in getAllowedDest(request.resource.data.from);
allow write: if request.resource.data.type == 'shuffle';
}
allow を複数個書くと OR になる. いずれかが条件を満たせば OK だ.
ひとまず, shuffle は無条件にOK とする.
サーバ(Cloud Functions) 側も この type に応じて実行する処理を変えるように修正したい. (コードは省略)
説明した以外にも コードを整理・修正している.
・ 山の生成処理を共通化して, どの山もシャッフルできるようにした.
・ 同じやり方で ソート もできるようにした.
・ 「1枚引く」アクションが 何番目のカードを引くか指定できるようにした.
(内部的にはできるが UI はまだつくってない)
・ サーバ側で 実行が終わった アクションにステータスやタイムスタンプを加えて残すようにしてみた.
時系列でソートできるので, うまく作ればリプレイとかもできるかな?
さて, 次はいよいよカードを裏返したいのだが・・・
これが結構たいへんそうなんだよなぁ.
おまけにGW終わるし. モチベーションが維持できるかどうか.
この記事が気に入ったらサポートをしてみませんか?