もう間違えない! Cloud Firestore rulesのresource, request.resource, writeFields

[2020/10 更新]
`writeFields`に関しては現在削除され使用不可能になっており、代わりに`MapDiff`を使う必要があります。
詳しくは[Cloud Firestoreのセキュリティルールにかつて存在した`writeFields`を覚えていますか?](https://zenn.dev/sgr_ksmt/articles/bcf56f511769c4b1ccf9)を御覧ください

--------


Cloud Firestoreのrulsを書く上でややこしい

- `resource` 変数
- `request.resource` 変数
- `writeFields` 変数

のそれぞれの違いを説明しようと思います。これを見ればもう間違えることはなくなるはず。

# TL;DR
- `resource`: 現在DBに格納されているドキュメント
- `request.resource`: 書き込みを行った場合に期待される将来のドキュメント（の状態）
- `writeFields`: 書き込みを行う場合に、書き込もうとしているフィールドのkeyの一覧

# それぞれの違い
## `resource`

ドキュメントによると、

> resource 変数は要求されたドキュメントを参照し、resource.data はドキュメントに格納されているすべてのフィールドと値のマップです。

とあるので、現在のドキュメントに格納されているフィールドと値のmapを参照できます。
rulesのそれぞれのオペレーションでは以下の通り。

- read(get, list): 現在のDBに格納されているドキュメント
- create: undefined
- update: 現在のDBに格納されているドキュメント
- delete: 現在のDBに格納されているドキュメント

## `request.resource`
> ルールセットが保留中の書き込みを許可する場合は、request.resource 変数にはドキュメントの将来の状態が含められます。ドキュメント フィールドのサブセットのみを変更する update オペレーションの場合、request.resource 変数にはオペレーション後の保留中のドキュメントの状態が含まれます。

つまり、書き込みのオペレーション(crate, update, delete)を実行した後の(保留)ドキュメント状態がmapとして格納される。なので

- read(get, list): `undefined`(writeオペレーション時のみ存在するため)
- create: 新規で書き込むドキュメントのフィールドと値のmap
- update: 値の追加更新削除をした後の **将来の状態の** フィールドと値のmap。
- delete: `undefined`

となる。 `update` の場合は、追加更新削除後の **将来の状態** を表すので、あるオペレーションで既存のデータ `{A, B}` に `{C}` を書き加えて更新する場合、 `request.resource.data` には `{A, B, C}` が含まれる。
一方で

## `writeFields`

> List of fields being written in a write request.

とあり、意訳すると、「書き込みリクエスト時に書き込まれるであろうフィールドのリスト」となる。

- read(get, list): undefined
- create: 書き込むフィールドのkey (`request.resource.data.keys()` と一致)
- update: 今回更新追加削除するフィールドのkey (`request.resource.data.keys()` と **必ずしも一致しない** 。update時に変更しようとしているフィールドのkeyのみ)
- delete: undefined


## 表にしてみる
実際にそれぞれのオペレーションで、現在のフィールド、書き込むフィールドが与えられた時に、`resource`, `request.resourcet`, `writeFields` の値がどう変化するか表にまとめます。
### read(get, list)

現在のフィールド | 書き込むフィールド | `resource` | `request.resource` | `writeFields`
---|---|---|---|---
{<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | undefined | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | undefined | undefined

### create

現在のフィールド | 書き込むフィールド | `resource` | `request.resource` | `writeFields`
---|---|---|---|---
undefined | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | undefined | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | ['name', 'age']

### update

現在のフィールド | 書き込むフィールド | `resource` | `request.resource` | `writeFields`
---|---|---|---|---
{<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | {<br>&nbsp;&nbsp;company: 'Foobar Inc.'<br>} | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25,<br>&nbsp;&nbsp;company: 'Foobar Inc.'<br>} | ['company']


### delete

現在のフィールド | 書き込むフィールド | `resource` | `request.resource` | `writeFields`
---|---|---|---|---
{<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | undefined | {<br>&nbsp;&nbsp;name: 'mike',<br>&nbsp;&nbsp;age: 25<br>} | undefined | undefined


## 参考
- [セキュリティ ルールの記述条件 - データの検証](https://firebase.google.com/docs/firestore/security/rules-conditions?hl=ja#data_validation)
- [rules.firestore.Request](https://firebase.google.com/docs/reference/rules/rules.firestore.Request)
- [rules.firestore.Resource](https://firebase.google.com/docs/reference/rules/rules.firestore.Resource)