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  に以下のようなドキュメントを登録しておく.

FireShot Capture 019 - card-game-field - Database - Firebase コンソール - console.firebase.google.com

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 変数を追加する.

FireShot Capture 021 - card-game-field - Database - Firebase コンソール - console.firebase.google.com

このようにカードの山自体に 「許可する移動先」 を設定して, これを 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 に応じて実行する処理を変えるように修正したい. (コードは省略)

画像3

説明した以外にも コードを整理・修正している.
・ 山の生成処理を共通化して, どの山もシャッフルできるようにした.
・ 同じやり方で ソート もできるようにした.
・ 「1枚引く」アクションが 何番目のカードを引くか指定できるようにした.
      (内部的にはできるが UI はまだつくってない)
・ サーバ側で 実行が終わった アクションにステータスやタイムスタンプを加えて残すようにしてみた.

FireShot Capture 024 - card-game-field - Database - Firebase コンソール - console.firebase.google.com

時系列でソートできるので, うまく作ればリプレイとかもできるかな?


さて, 次はいよいよカードを裏返したいのだが・・・
これが結構たいへんそうなんだよなぁ.

おまけにGW終わるし. モチベーションが維持できるかどうか.

この記事が気に入ったらサポートをしてみませんか?