コーディング規約
Firebase
Firestore
CloudFirestore

Firestoreのruleのコーディング規約 (を考える)

Firestoreのruleを書く時に、どういう形式で記述するのが良いのか模索中なので、
現状自分が書いている書き方を規約という形で書き出してみる。

ということで早速本題に。

Cloud Firestoreのruleのコーディング規約

インデント

インデントはソフトタブのwidth:2で書く。

service cloud.firestore {
  match /databases/{database}/documents {
    match /foo/{fooID} {
      allow read: if <condition>;
    }
  }
}

行末

行末にはセミコロン;を挿入する。
なくても動作するが、一応公式のサンプルコードに倣っている。

allow read: if isAuthenticated();

大枠の部分

service cloud.firestore {
  match /databases/{database}/documents {
  }
}

ここまでの部分は初期のまま変数名等をいじったりはしない

オペレーションの記述順番

allow式に書くオペレーションは、

  • read
    • get
    • list
  • write
    • create
    • update
    • delete

に分類される。これらを使うときは基本的にはread→writeの順で記述していく。

  • 良い例
match /foo/{fooID} {
  allow read, write: if <condition>;
}
match /foo/{fooID} {
  allow get: if <condition>;
  allow create, update: if <condition>;
}
  • 悪い例
match /foo/{fooID} {
  allow write, read: if <condition>;
}
match /foo/{fooID} {
  allow create, update: if <condition>;
  allow get: if <condition>;
}

match文中の記述順番

match文中は、

  • そのpathに対するallow式
  • そのpath以降のサブコレクションのmatch式
  • そのpath内のスコープでのみ使用する関数

の順に書く。

match /foo/{fooID} {
  allow create, update: if <condition>;
  allow get: if someFunction();

  match /bar/{barID} {
    allow read, write: if <condition>;
  }

  function someFunction() {
  }
}

関数をmatch分の上に持ってくる書き方もあるが、個人的にはmatch分のすぐ下で、そのpathに対してどういうallow式が記述されているか確認しやすい方がいいのでこのようにしている。

再帰ワイルドカード構文(/{name=**})

FirestoreのruleはRealtimeDBのそれとは異なり、再帰ワイルドカード構文を使わない限りは、そのドキュメント以降のサブコレクションに対して再帰的に同じルールが適応されることはない。
再帰的に適応したい場合は以下のように再帰ワイルドカード構文を使用することになるが、よほどのことが無い限り使用は控える

  • 再帰ワイルドカード構文を使う場合
// `/foo` にも、`/foo`以下のサブコレクションのドキュメントにも同一のルールが適応される
// 今後`/foo` 以下にサブコレクションが増えても気にせず適応することができる
match /foo/{fooID=**} {
  allow create: if <condition>;
}
  • 再帰ワイルドカード構文を使わない場合
// 1つ1つ該当するサブコレクションのルールを書く。
// ここにないサブコレクションに対してはルールが許可されないので、必要になったタイミングで追記していく必要がある
match /foo/{fooID} {
  allow create: if <condition>;

  match /bar/{barID} {
    allow create: if <condition>;
  }

  match /baz/{bazID} {
    allow create: if <condition>;
  }
}

使用する方がサブコレクションに対して統一してルールを適応できるので開発が一見楽に見える。が、同時に意図しないサブコレクションに対しても同一のルールが適応されてしまうので、そのルールが正しくない場合に気づきづらかったりする場合がある。

また、以前投稿した記事のこちらでも触れているが、一度再帰ワイルドカード構文を使用すると、特定のサブコレクションに対して追加でmatch文を書いて記述することができなくなるので、融通がきかなくなる。

使わない場合の方が記述量は多くなるものの、指定したサブコレクションのみ許可することが可能になるので、セキュリティの観点から見ても安心した設計が行なえる。
また、それぞれに共通した条件式があるなら、functionとして切り出せばミスも起こしづらくなるので、活用すればそこまでつらくはならない。
実際自分も本番運用しているプロダクトでは再帰ワイルドカード構文は一切使用していない

(※コメントいただき追記しました)

条件が複数行になる場合

ifの条件が複数になる場合は、短く済むなら1行で、折り返す場合はifの下に &&|| を揃えて書く

match /foo/{fooID} {
  allow create: if <condition1> && <condition2>;
  allow update: if <long_long_condition3> 
                && <long_long_condition4>
                && <long_long_condition5>;
}

なやみ

実はこの部分、自分も悩んでいて、他の人の書き方を見ると他にはこんな書き方を見かける。

  • 後ろに条件演算子を付け、条件分の頭を揃えるパターン
match /foo/{fooID} {
  allow update: if <long_long_condition3> &&
                   <long_long_condition4> &&
                   <long_long_condition5>;
}
  • 後ろに条件演算子を付け、条件文の頭はインデントに従うパターン
match /foo/{fooID} {
  allow update: if <long_long_condition3> &&
    <long_long_condition4> &&
    <long_long_condition5>;
}

変数名、関数名

基本的には lower camel case で記述する

match /user/{userID} {
  allow get: if isAuthenticated();
  allow create: if request.resource.data.loginMethod == 'mail';
}

また、 database request resource といった変数名は使わないようにする(誤参照を防ぐ&firestoreが用意したものかどうか分かりづらくなるため)

関数

スコープ

関数のスコープは定義されたmatch文の中となる。
その中なら、サブコレクションに対するmatch文でも関数を使用することができる。

service cloud.firestore {
  match /databases/{database}/documents {
    match /foo/{fooID} {
      allow read: if someFunction(); // 使用できる

      match /baz/{bazID} {
        allow read: if someFunction(); // 使用できる
      }

      function someFunction() {
        return ...;
      }
    }

    match /bar/{barID} {
      allow read: if someFunction(); // 使えない
    }
  }
}

もし上位のスコープに同名の関数がある場合は、そのmatch文の関数が優先される

service cloud.firestore {
  match /databases/{database}/documents {
    match /foo/{fooID} {
      allow read: if someFunction(); // ここでは `/foo/`中のsomeFunctionが使われるので結果は `true`

      match /baz/{bazID} {
        allow read: if someFunction(); // ここでは `/baz/`中のsomeFunctionが使われるので結果は `false`

        function someFunction() {
          return false
        }
      }

      function someFunction() {
        return true
      }
    }
  }
}

引数

引数が2つ以上ある場合は、定義時も使用時も引数はカンマ区切りで、半角スペースをカンマの後にいれる

match /foo/{fooID} {
  allow create: if someFunction('1', '2');

  function someFunction(arg1, arg2) {
  }
}

関数の活用(ややtips的な感じ)

できる限り共通で使用する条件式、頻出する処理は関数化して使用すると良い。
また、現状ではFirestoreのruleにおいて変数・定数宣言が出来ず、typoミスも起こりうるので、関数を活用するという手もある。

以下は関数の活用の例

例1) request.resource.data, resource.dataの取得

自信も初期の頃よくハマったのだが、ドキュメントのデータを参照したいときに、
request.resource.data.nameとするところをrequest.resource.nameとしてしまうような事が多かったので、
こちらの記事で紹介されていた関数を活用し、ミスを減らしている

function existingData() {
  return resource.data;
}
function incomingData() {
  return request.resource.data;
}

// 使用するとき
allow create: if incomingData().name.size() < 50;

例2) 特定のドキュメントのpathの生成

特定のドキュメントのpath生成をするときに、毎回/databases/$(database)/documents/...と書くのは大変だし、
頻出するとtypoする可能性もでてくるので、関数化すると良い。
例えば、userIDを渡して、/user/xxxxのドキュメントのpathを返す関数を以下のように定義する

service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userID} {
      ...
    }

    match /post/{postID} {
      allow update: if isAuthor();
      function isAuthor() {
        return request.auth != nil 
          && request.auth.uid == incomingData().authorID
          && exists(getUserPath(incomingData().authorID));
      }
    }

    // /user/{userID} のドキュメントパスを返す
    function getUserPath(userID) {
      return /databases/$(database)/documents/user/$(userID);
    }
  }
}

この辺り、もう少し良いソリューションを模索中なので近々blogで書こうかなと思っている。

文字列

rule中で文字列を記述する場合は、シングルクォート ' を使う

match /user/{userID} {
  allow get: if isAuthenticated();
  allow delete: if isUserAuthenticated(userID) && request.resource.data.role == 'admin';
}

Map型の変数の値へのアクセス

JavaScript(TypeScript)でのお作法に準拠する。ブラケットがいい場合とドット記法がいい場合とがあるので、適宜使い分ける。
基本的に問題が無ければドット記法で書くことが多い。

match /user/{userID} {
  allow create: if request.resource.data.name; // 基本的にはこの書き方
  allow delete: if request.resource.data.prefectures['1']; // prefectures.1とは書けないのでブラケット使う
}

さいごに

「自分はこう書いているよ!」とか、「これも追記して欲しい」とかあったら是非コメントとかで書き方の例を交えて教えてください :pray:

参考