yucatio@システムエンジニア

趣味で作ったものいろいろ

Firestoreのtransaction中にドキュメントをaddする

Firestoreではtransaction機能が使え、複数の書き込みをまとめることができます。

transactionを使用するときには、以下のコードのようにrunTransactionを利用します。2つ目の引数にトランザクションの内容を書いた関数を渡します。

await runTransaction(db, async (transaction) => {
  // TODO ここにトランザクションの内容を書く
})

コールバックに渡ってくるtransactionにはgetsetupdatedeleteが用意されています。addがありませんね。

f:id:yucatio:20211026160219p:plain

addはdoc()とset()を組み合わせる

add()doc()set()を組み合わせることで実現します。

await runTransaction(db, async (transaction) => {
  const docRef = doc(collection(db, "my_collection"))
  transaction.set(docRef, {
    text: "addded from transaction"
  })
})

こちらを実行すると、以下のようにランダムなIDで登録されているのが分かります。

f:id:yucatio:20211026192835p:plain

環境

firebase: 9.1.3

Firestoreで文字列を正規表現で制限するセキュリティルール

Firestoreのセキュリティルールでは、文字列を正規表現で制限することができます。悪意のあるユーザにおかしな値を登録されないようアプリと同じ制限をかけておきましょう。

String.matches(regex)

String.matchesを使うと、フィールドに登録される値が 指定された正規表現を満たす場合にのみ、DBに登録することができます。 記述はこのようになります。

// 電話番号(ハイフンなし)
allow create, update: if request.resource.data.tel.matches("^0[0-9]{9,10}$")
// メールアドレスっぽい文字列
// size()も組み合わせて使いましょう
allow create, update: if request.resource.data.email.matches("^[-a-zA-Z0-9_\\.]+@[-a-zA-Z0-9_\\.]+$") &&
                         request.resource.data.email.size() <= 255

String.matches()の使用例

例として参加登録フォームを作ってみます。

f:id:yucatio:20211026132400p:plain:w350

データ構造はこのようになっています。

f:id:yucatio:20211026150453p:plain

電話番号は0とそれに続く数字が9個か10個です。メールアドレスは半角英数と、記号は-_.が使えます。@マークは1つだけです。 セキュリティルールの全体はこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /applications/{application} {
      function validApplication(docData) {
        return docData.tel.matches("^0[0-9]{9,10}$") &&
               docData.email.matches("^[-a-zA-Z0-9_\\.]+@[-a-zA-Z0-9_\\.]+$") &&
               docData.email.size() <= 255;
      }
      allow create, update: if validApplication(request.resource.data)
    }
  }
}

正規表現に合うように登録してみます。

f:id:yucatio:20211026132400p:plain:w350

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211026141753p:plain

電話番号を12桁で登録してみます。

f:id:yucatio:20211026141917p:plain:w350

エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211017172609p:plain

次に、メールアドレスに使えない文字列を使ってみます。

f:id:yucatio:20211026143959p:plain:w350

こちらもエラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211017172609p:plain

リンク

正規表現はこちらのものを参考にしました。 qiita.com

Firestoreで数値の範囲を制限するセキュリティルール

Firestoreのセキュリティルールで数値の範囲を制限することができます。悪意のあるユーザにより思わぬ値を登録されてしまい、誤動作を起こすことを防げます。

フィールドの大小比較を使う

数値の範囲を指定するには、フィールドの値を大小比較します。 例えば、"age"(年齢)フィールドを18以上65以下に制限するには以下のように記述します。

allow create, update: if request.resource.data.age >= 18 &&
                         request.resource.data.age <= 65

数値の範囲の使用例

例として参加登録フォームを作ってみます。

f:id:yucatio:20211024160316p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211024172031p:plain

年齢を必須かつ18以上65以下という条件を、セキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /applications/{application} {
      function validApplication(docData) {
        return docData.age is int && 
               docData.age >= 18 &&
               docData.age <= 65;
      }
      allow create, update: if validApplication(request.resource.data)
    }
  }
}

18歳で登録してみます。

f:id:yucatio:20211024161032p:plain:w300

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211024164702p:plain

-(マイナス)30歳で登録してみると、エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211024163343p:plain:w300

f:id:yucatio:20211017172609p:plain

以上でFirestoreのセキュリティルールで数値の範囲を制限することができました。

Firestoreで文字列の長さを制限するセキュリティルール

Firestoreのセキュリティルールで文字列の長さを制限することができます。悪意のあるユーザによりとても長い文字列が登録されてしまい、結果過大な請求が発生するのを防ぐためにも、文字列の長さを制限しておくのはおすすめです。

string.size()

文字列のstring.size()メソッドを使用することで文字列の長さを取得できます。これと大小比較を組み合わせてルールを作成します。 例えば、"text"フィールドを16文字以下に制限するには以下のように記述します。

allow create, update: if request.resource.data.text.size() <= 16

String.size()の使用例

例としてTODOアプリを考えます。入力はタスク名とステータス(実施状況)です。

f:id:yucatio:20211017171414p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211017230600p:plain:w350

タスク名を必須かつ16文字以下という条件を、セキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todo} {
      function validTodo(docData) {
        return docData.text.size() > 0 &&
               docData.text.size() <= 16;
      }
      
      allow read: if true;

      allow create, update: if validTodo(request.resource.data);
    }
  }
}

6文字のタスク名で登録してみます。

f:id:yucatio:20211018121053p:plain:w250

Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211018121104p:plain

長いタスク名(18文字)で登録してみると、エラーになり、コンソールを確認すると登録されていないことが分かります。

f:id:yucatio:20211018120759p:plain

以上でFirestoreのセキュリティルールで文字列の長さを制限することができました。

FirestoreでEnum型風ルールを作成する

Firestoreではセキュリティルールでフィールドの型を指定できます。 フィールドの型にはboolintstringなどがあります。 しかしenum型は存在しません。

Firebaseのセキュリティルールでは、string型のフィールドに対して登録できる文字列を制限することができます。 この機能を使うことでenum型風のフィールドを作成することができます。

String.matches(regex)

String.matchesを使うと、フィールドに登録される値が 指定された正規表現を満たす場合にのみ、DBに登録することができます。 記述はこのようになります。

allow create, update: if request.resource.data.status.matches("^(TODO|IN_PROGRESS|DONE)$")

String.matches(regex)の使用例

例としてTODOアプリを考えます。入力はタスク名とステータス(実施状況)です。

f:id:yucatio:20211017171414p:plain:w300

データ構造はこのようになっています。

f:id:yucatio:20211017230600p:plain:w350

status(ステータス)には"TODO"(未着手)か"IN_PROGRESS"(作業中)か"DONE"(完了)のみ指定できます。

これをセキュリティルールで書くとこのようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todo} {
      function validTodo(docData) {
        return docData.status.matches("^(TODO|IN_PROGRESS|DONE)$")
      }
      
      allow read: if true;
      allow create, update: if validTodo(request.resource.data);
    }
  }
}

このセキュリティルールでステータスが"TODO"のタスクを登録します。 Firebaseのコンソールで確認すると登録されていることが分かります。

f:id:yucatio:20211017172045p:plain

もし"IN_PROGRESS"を"DOING"と間違えてしまった場合、"Missing or insufficient permissions."のエラーが出ます。DBを確認すると登録ができていないことが確認できます。

f:id:yucatio:20211017172609p:plain
google chromeのコンソール

最後に、念のため正規表現があっているか確かめておきましょう。

https://jex.im/regulex/#!flags=&re=%5E(TODO%7CIN_PROGRESS%7CDONE)%24

f:id:yucatio:20211017171755p:plain

大丈夫そうですね。

MUI(Material-UI)のアイコンにフチドリをつける

MUI(Material-UI)のアイコンにフチドリをつけます。

f:id:yucatio:20211017140609p:plain

通常のCSSのスタイルと同様にstrokestrokeWidthを指定します。strokeOpacitystrokeLinejoinなども指定できます。

コード

<StarIcon
  sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}}
/>

ソースコード

import React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import StarIcon from '@mui/icons-material/Star';
import FavoriteIcon from '@mui/icons-material/Favorite';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import AppleIcon from '@mui/icons-material/Apple';
import BedtimeIcon from '@mui/icons-material/Bedtime';
import CloudIcon from '@mui/icons-material/Cloud';
import { blue, grey, red, pink, yellow } from '@mui/material/colors';

function App() {
  return (
    <Box sx={{p:2}}>
      <Typography variant="h6">通常</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500]}} />
        <FavoriteIcon sx={{color: pink[500]}} />
        <AddCircleIcon sx={{color: blue[500]}} />
        <AppleIcon sx={{color: red[500]}} />
        <BedtimeIcon sx={{color: yellow[600]}} />
        <CloudIcon sx={{color: grey[500]}} />
      </Stack>

      <Typography variant="h6">フチドリあり</Typography>
      <Stack spacing={2} direction="row">
        <StarIcon sx={{color: yellow[500], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <FavoriteIcon sx={{color: pink[500], stroke: pink[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <AddCircleIcon sx={{color: blue[500], stroke: blue[800], strokeWidth:4, strokeOpacity: 0.2}} />
        <AppleIcon sx={{color: red[500], stroke:red[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <BedtimeIcon sx={{color: yellow[600], stroke: yellow[900], strokeWidth: 4, strokeOpacity: 0.2}} />
        <CloudIcon sx={{color: grey[500], stroke:grey[800], strokeWidth: 4, strokeOpacity: 0.2}} />
      </Stack>
    </Box>
  );
}
export default App;

環境

  • Material-UI: 5.0.4
  • React: 11.4.1

PythonでSetのSetを作成する

PythonでSetのSetを作成しようとするとエラーになります。

my_set = set()
my_set.add({1, 2, 3})
# => TypeError: unhashable type: 'set'

Pythonでは、Setの各要素はハッシュ可能(hashable)でなければいけません。 Pythonの組み込み型でハッシュ可能なのは、int, str, tuple, frozensetです。list. dict, setはunhashableハッシュ不可能(unhashable)です。

ハッシュ可能についてはこちらの記事を参考にしました。

qiita.com

frozensetを使う

pythonでsetのsetを作成するには、frozensetを使用します。frozensetはイミュータッブルでハッシュ可能な型です。

my_set = set()

my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({4, 5}))

print(my_set)
#=> {frozenset({1, 2, 3}), frozenset({4, 5})}

エラーなく動作しました。

もう少し挙動を確認してみます。

my_set = set()

# setにsetを追加する
my_set.add(frozenset({1, 2, 3}))
my_set.add(frozenset({1, 2}))
my_set.add(frozenset({2, 1}))  # 集合としては1つ上と同じ
my_set.add(frozenset({5, 4, 3}))
my_set.add(frozenset({4, 3, 5, 3, 4}))  # 集合としては1つ上と同じ
my_set.add(frozenset({4}))
my_set.add(frozenset({10, 2, 4, 7, 2}))
# addの回数は7回

print(len(my_set))
# => 5

print(my_set)
# => {frozenset({1, 2}), frozenset({10, 2, 4, 7}), frozenset({3, 4, 5}), frozenset({1, 2, 3}), frozenset({4})}
# {1, 2} と {2, 1} は同一のものなので、setには片方が登録されている
# {5, 4, 3} と {5, 4, 3, 3, 4} は同一のものなので、setには片方が登録されている

print({1, 2} in my_set)
# => True
# setとの比較は可能

print({3, 5, 4} in my_set)
# => True
# {3, 4, 5}と同一のものとして判定される

print({2} in my_set)
# => False
# 2が含まれる要素もあるが、my_setの要素としては一致しないのでFalseとなる

以上で、setのsetの作成と、動作確認ができました。