LoginSignup
0
0

More than 3 years have passed since last update.

Firebase Realtime Databaseで読み込み場所と書き込み場所を分けたい場合のテンプレ

Posted at

前提

なんとなーくRealtime Databaseを使っているが、Firestoreでも同じだと思う。

概要

ある程度複雑なFirebaseアプリを作っていると、書き込み地点と読み込み地点を分けたくなってくる。
rule.jsonが複雑になってくるからだ。

こういう構成にしたい。

  1. Cloud FunctionsがDatabaseの/requestsをlisten
  2. フロントエンドが/requestsにpushする
  3. Cloud FunctionsがRequestを受け取り、/dataを更新する
  4. フロントエンドは/dataをlistenしており、Viewに表示する
  5. なお、Cloud FunctionsがRequestに対してお返事があるような場合は/responseに書き込む

こうしておけばアクセス権とか複雑なことは何も考えなくて良くなる。

最近同じようなコードを何度も書いている気がするので、テンプレ化した。

テンプレ

rule.json

rule.json
{
  "rules": {
    ".read": false,
    ".write": false,
    "data" : {
     ".read" : true,
    },
    "requests" : {
      ".write" :true
    },
    "responses": {
      "$uid": {
        ".read": "$uid === auth.uid"
      }
    }
  }
}

サーバーサイド

functions.ts

export interface Request {
  method: string;
  payload: any;
}

export const onRequest = functions
  .database.ref("/requests/{event_id}")
  .onCreate(async (snapshot, context) => {
    await onCreate(snapshot, context);
    await snapshot.ref.remove();
  });

async function onCreate(snapshot: DataSnapshot, context: EventContext) {
  const req = snapshot.val() as Request;
  const requestID = snapshot.key;
  const fs = new FirebaseServiceImple(snapshot.ref.root);
  const userID = context.auth?.uid;
  if(!userID) return;
  const data = await readData(snapshot);
  const res = update(fs, model, req, requestID, userID);
  snapshot.ref.root.child(`responses/${userID}`).push(res);
}

async function readData(snapshot: DataSnapshot): Promise<Data> {
  return new Promise<any>((resolve) => {
    snapshot.ref.root.child("data").once("value", (ss) => {
      const data = cleansing(ss.val());
      resolve(data);
    });
  });
}

update.ts
// こういうのを作っておくとテストしやすい。
export interface FirebaseService {
  set(path: string, value: any): void;
  push(path: string, value: any): string;
  remove(path: string): void;
}

export function update(
  fs: FirebaseService,
  models: Models,
  request: Request,
  requestID: string,
  userID: string
): Response {
  const method = request.method;
  const payload = request.payload;

  if (method == "POST_MESSAGE") {
    ...
    fs.push( "/data/messages", message);
    return messageID;
  } else {
    throw new Error("method ga nai");
  }
}

フロントエンド

fontend.ts

// Cloud Functionsからの戻り値をPromiseで紐付ける
export class Requests {
  private ps: Map<string, Callback> = new Map<string, Callback>();

  constructor(private app: firebase.app.App) {
    const path = `responses/${this.app.auth().currentUser?.uid || ""}`;
    this.app.database().ref(path).remove();
    this.app
      .database()
      .ref(path)
      .on("child_added", (snapshot) => {
        const res = snapshot.val() as Response;
        const key = res.requestID;
        const callback = this.ps.get(key);
        if (!callback) return;
        const result = res.result as string;
        callback(result);
        this.ps.delete(key);
      });
  }

  public postMessage(userID: string, user: User): Promies<string> {
    const req: Request = {
      method: "POST_MESSAGE",
      payload: user,
    };
    const res = await this.push(req);
    return res.messageID;
  }

  private push(request: Request): Promise<string> {
    const r = this.app.database().ref("requests").push(request);
    return new Promise((resolve) => {
      this.on(r.key!, (result) => resolve(result));
    });
  }

  private on(key: string, callback: Callback) {
    this.ps.set(key, callback);
  }
}

type Callback = (result: string) => void;

async function main() {
  const app = firebase.initializeApp(
    await (await fetch("/__/firebase/init.json")).json()
  );
  const req = new Requests(app);
  app
    .database()
    .ref("data")
    .on("value", (ss) => {
      const initialData: Data = { messages: {}, users: {} };
      const data = {...initialData, ...(ss.val() || {})} as Data; // Realtime Databaseは空オブジェを削除するので
      ...
    });

}

その他

楽観的UIがしたければ、RequestにPushする時にローカルのdataも書き換えておけばいい。
そのうち最新のデータで上書きされる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0