firebaseのチャットで画像を扱うために

LINEのようなチャットツールでは、チャットしている時系列に画像を表示したり、それとは別に、写真だけを一覧表示することができる。

これがRDBMSでは頭を捻らなくても実装することができるが、firebaseのようなNoSQLでこれを実装するためにはどのようにすれば良いか。

その場合は、 collectionGroup() を使用してすることで実装することができる。

そのために以下のようなコレクションを定義する。

※C: コレクション ※d: ドキュメント

rooms(C)
  - room1(d)
    - name
    - description
    - messages(C)
      - XXX(d)
        - body
          - images(C)
    - members(C)
      - member1(d)
        - name
        - photoUrl

写真を時系列のメッセージと共に流す際には、

db.collection("rooms").doc("room1").collection("messages")

のように普通通りに取得をして、画像だけを一覧したい場合は

db.collection("rooms").doc("room1")collectionGroup("images")

を実行する。

collectionGroupを使うことで、他のmessagesドキュメントの写真を串刺しにして取得することができる。

SequelizeでwhereHasを行う

include内のrequired値をtrueにすることでwhereHasと同じ挙動の動作をさせることができる。 falseの場合は、ORMのリレーションの結果が0件でも取得できる。

attributesを[]にすることで、whereHasだけを利用することも可能。

XXX.findAll({
    where: findAllParams,
    include: [
        {
            model: YYY,
            where: { YYYId: 3 },
            attributes: ['YYYId'], // カラムの絞り込み
            required: true
        }
    ],
    limit: limit,
    offset: offset
})

firebaseのAuthenticationとfirestoreを利用したセキュリティルール設定

ここではセキュリティルールを考えた際にfirestoreはどういったデータ構造にするか、ということを説明する。

詳細の設定については、 公式ブログ に譲る。

チャットアプリのような双方向の通信を実現する際は、 firebase を利用することで簡単に実装できる。

実際には、アプリケーション内で firestore のデータを監視して、データの追加/変更/削除が発生したら自身で用意したイベントを呼び出すことで、相手の 発信 を受け取ることができる。

ただ、 firestore は正しいセキュリティルールを設定しないと、 誰でも ストレージにアクセスができるので、必ずルールを設定しなければいけない。

ここで、 firestore を以下のように組み立てるとする(これは後に困るやり方)。

Collection Document Collection/data Document
chatRoom XXX member(C) YYY
message(C) ZZZ
name(d)
description(d)

アクセス制御を行うには、以下のような設定を行う。

match /chatRoom/{roomId} {
      allow read, write: if exists(/databases/$(database)/documents/chatRoom/$(roomId)/member/$(request.auth.uid));
}
// 上記以外全て拒否

このようにすることで、 /chatRoom にアクセスして全データを取得するときに、 /chatRoom/{roomId} の条件にヒットしないものは取得されない、と思ったが、このパス /chatRoom/{roomId} はリクエストパスを表現しているため、 このようにアクセス制御を行うと、 /chatRoom へのルールが設定されていないため、アクセス自体が拒否されてしまう。 /chatRoom の全取得で権限があるデータだけを取得する、という操作はできないことがわかる。

このことから、 firestore のデータ構造を以下のようにする必要がある。user コレクションには、 chatRoom の情報を非正規化して保持している。ただ、非正規化が行われるということは、チャットルーム名の変更などが発生した場合は、非正規化が発生した部分を全て変更しなければならない。firebaseでは、 collectionGroup() というメソッドでサブコレクションを串刺しにして取得することができるので、collectionGroup(YYY) とすることで、非正規化されたデータを一括で書き換えることができるようになる。

(※ user のサブコレクションの userRoomroom にしなかった理由はサブコレクションを串刺しにしたいときに、 room は他のサブコレクション名とバッティングする恐れがあるため、 userRoom とした)

Collection Document Collection/data Document data
chatRoom XXX message(C) ZZZ
name(d)
description(d)
user XXX userRoom(C) YYY name(d)
description(d)

このようにすることで、チャット一覧の取得は、 /user/{user}/userRoom で取得するようになる。このとき、認証されたユーザしか /user/{user}/userRoom のデータにアクセスできない。チャットの詳細を取得するときは、 /chatRoom/{roomId} にアクセスして、認証されているユーザがこのルームへのアクセス権限があるかを判定している。

match /user/{user}/userRoom {
      allow read, write: if user == request.auth.uid;
}
match /chatRoom/{roomId} {
      allow read, write: if exists(/databases/$(database)/documents/user/$(request.auth.uid)/userRoom/$(roomId));
}
// 上記以外全て拒否

これでセキュアな状態(非正規化ではあるもののデータ更新に困らない)のモノになるのではないだろうか。

H2Oを利用してFlaskをFastCGIとして動かすまで

とりあえず、Flaskは動くことを前提としている。

Flaskが動く環境が整ったら、UNIXドメインソケットを利用してFastCGIデーモンを動かすようにしてあげる。

UNIXドメインソケットは、以下のようにすることで作成される。以下をxxx.fcgiと、自分で名前を付けてFlaskのプロジェクトの最上位に置く(最上位でなくてもよいが)。 これは公式ドキュメントに書いている。

#!/usr/bin/python3
from flup.server.fcgi import WSGIServer
from yourapplication import app

if __name__ == '__main__':
    WSGIServer(app, bindAddress='/tmp/fcgi.sock').run()

この際に、xxx.fcgiのシバンの指定しているpythonの記載についpython3を使いたければpython3を指定し、2系を利用したければpythonでよい。 また、私のDockerの環境だと、python/usr/local/binに入っていて、公式ドキュメントのシバンでは参照できなかったので、which python3でシバンを正しく設定してあげる。

次に、FastCGIデーモンを動かす。デーモンとして動かすので、screenコマンドを使えるようにする。 ところで、デーモンとして動かす前に、FastCGIとして動かすためには、以下のコマンドで動かすことができる。

/yourapplication/xxx.fcgi

これで、動くのだが、私のDockerの環境では(bashではなくshだからか?)以下のようにしなければ動かなかったので注意。

python3 /yourapplication/xxx.fcgi

上記を利用して、screenでデーモンとして実行してあげればOK。

H2O側のFastCGIを待ち受けるソケットへの接続は以下でOK。

listen:
  port: 8080
user: root

hosts:
  "localhost":
    paths:
      /:
        fastcgi.connect:
          port: /tmp/fcgi.sock
          type: unix

access-log: /app/logs/access-log
error-log: /app/logs/error-log                          

これで、127.0.0.1:8080へアクセスすれば、UNIXソケットを利用してFlaskを動かすことができる。

ReactはNativeでtypescript対応したらしい

下記のQiita記事でReactをTSで開発する時の初期状態を作るコマンドが乗っていた。

ReactとTSの環境を整えるにはReactやTSのパッケージをいれたり、としないといけなかったのに、コマンド一つでプロジェクトを作成することができ、随分と楽になったんだなぁ、と思った。

qiita.com

そこでは、以下のコマンドを実行する、と書いていた。

npx create-react-app rg2-front --scripts-version=react-scripts-ts

しかし、実行してみると、以下のように言われた。

The react-scripts-ts package is deprecated. TypeScript is now supported natively in Create React App. You can use the
 --typescript option instead when generating your app to include TypeScript support. Would you like to continue using r
eact-scripts-ts

なんと、NativeでTS対応をしていると。

以下のコマンドでプロジェクトを作った。良い時代になったな、と思った。

npx create-react-app rg2-front --typescript

TypeScriptで"SyntaxError: Unexpected identifier "と言われたら

以下のようなエラーにさいなまれた際の対応方法。

SyntaxError: Unexpected identifier
or
SyntaxError: Unexpected *
or
SyntaxError: Unexpected token import

この問題が発生する原因は、トランスパイルされた js ファイルをNodeが実行できないため。

試しに、 node トランスパイルされたjsのエントリファイル で、そのファイルを実行してみるとよい。同じエラーがでる。

tsconfig.jsoncompilerOptionsmodule の値を esX(Xは任意の値) から commonjs に変更すると、実行環境のNodeが実行できる出力になる。

Use object destructuring prefer-destructuring

以下のような場合、eslintがエラーが報告してくる。

async post(req: Request, res: Response) {
  const body = req.body;
}

そのため、以下のように修正することでエラーがなくなる。 プロパティをドット区切りで取得するのではなく、変数を展開して格納してあげる。

async post(req: Request, res: Response) {
  const { body } = req;
}