前提
なんとなーくRealtime Databaseを使っているが、Firestoreでも同じだと思う。
概要
ある程度複雑なFirebaseアプリを作っていると、書き込み地点と読み込み地点を分けたくなってくる。
rule.jsonが複雑になってくるからだ。
こういう構成にしたい。
- Cloud FunctionsがDatabaseの/requestsをlisten
- フロントエンドが/requestsにpushする
- Cloud FunctionsがRequestを受け取り、/dataを更新する
- フロントエンドは/dataをlistenしており、Viewに表示する
- なお、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も書き換えておけばいい。
そのうち最新のデータで上書きされる。