Cloud Functions for Firebaseと「ユニークなユーザ名」の考え方

公開日:2019-06-05
最終更新:2019-06-06

ユーザ名に限った話ではないですが、UNIQUE制約のような他のドキュメントと重複しないユニークな値を持つフィールドの考え方についてです。ここでは、Cloud Functionsを用いて、ユーザがユニークなユーザ名を持つ状態について考えます。サンプルコードはこちらです。

コードの読み方

コレクションpostsにドキュメントを書き込む例。

const postId = 'id' // ドキュメントのID  

const postRef = firestore().collection('posts').doc(postId) // ドキュメントの参照  

const post = {id: postId} // ドキュメントのデータ  

postcRef.set(post) // 書き込み  

書き込みを考える

ユーザ名をusersコレクションに書き込むとして以下のようなモデルを考えます。

type User = { username: string }  

const user: User = { username: 'qrunch' }  

このuserをコレクションusersに書き込みますが、ドキュメントのIDをユーザのIDにすると、常にユーザ名から参照userRefが定義できます。

const userId = 'userId' // ユーザID  

const userRef = firestore()  
  .collection('users')  
  .doc(userId)  

参照userRefから、ユーザ名を持つドキュメントuserはこのように書き込みできます。

const userRef = firestore()  
    .collection('users')  
    .doc(userId)  

type User = { username: string }  

const user: User = { username: 'qrunch' }  

userRef.set(user)  

重複を確認する

ユーザ名qrunchがユニークになるように、既にこのユーザ名が存在しないか確認しなければいけません。ここでは、ユーザ名が格納されたコレクションusernamesを定義します。ユーザ名がユニークであることを、ドキュメントのIDがユニークであることで実現します。

ユーザ名がIDとなるドキュメントusernamesを考え、参照usernameRefを定義します。

const usernameId = 'qrunch'  

const usernameRef = firestore()  
  .collection('usernames')  
  .doc(usernameId)  

ユーザ名のドキュメントusernameを定義します。

type Username = { id: string, userId: string }  

const username: Username = { id: usernameId, userId }  

ドキュメントを取得して空であれば、それはユーザ名はまだ使用されていないことを意味します。

// ドキュメントのID  
const usernameId = 'qrunch'  

// ドキュメントの参照  
const usernameRef = firestore()  
  .collection('usernames')  
  .doc(usernameId)  

// ドキュメントのスナップショット  
const usernameSnap = await usernameRef.get()  

// 既に存在するのでエラー  
if (!usernameSnap.exists) {  
  throw new Error('already exists!')  
}  

!usernameSnap.emptyは「スナップショットが空でないこと」を意味してます。

トランザクションを考える

「ドキュメントが存在しないこと」を確認した後に、目的のドキュメントを更新することになります。しかし「ドキュメントの取得」と「ドキュメントの更新」の間に「ドキュメントの書き込み」が行われる可能性を考えなければなりません。

Cloud FirestoreではrunTransactionを用いてそれを防ぎます。トランザクションは「ドキュメントの読み取り」を終えてから「ドキュメントの書き込み」を実行する一連の処理をまとめて送信します。

  • ユーザ名のドキュメントを取得する
  • ユーザを取得する
  • ユーザがいない場合はエラーを返す
  • ユーザ名のドキュメントを取得する
  • ユーザ名のドキュメントが既に存在する場合はエラーを返す
  • ユーザが既にユーザ名を持っている場合は前のユーザ名を削除
  • ユーザ名のドキュメントを書き込む
  • ユーザのドキュメントにユーザ名を書き込む

エラー処理を無くすとこのように考えられます。

firestore().runTransaction(async t => {  
  // ユーザのスナップショットを取得  
  const userSnap = await t.get(userRef)  

  // ユーザを取得  
  const user = userSnap.data() as { username: string }  

  // ユーザ名のスナップショットを取得  
  const usernameSnap = await t.get(usernameRef)  

  // ユーザが既にユーザ名を持っている場合は、古いユーザ名を解放する  
  if (user.username) {  
    const lastUsernameRef = firestore()  
      .collection(USERNAMES)  
      .doc(user.username)  

   t.delete(lastUsernameRef)  
  }  

  // ユーザ名のドキュメントを書き込む  
 t.set(usernameRef,  {  
    username: data.username,  
    ownerId: userId  
  })  

  // ユーザのドキュメントを書き込む  
 t.set(userRef, { username: data.username },  { merge: true })  

  return { userId }  
})  

おまけ

ログイン認証

ユーザ名はonCallトリガーでは、https.CallableContextから取得できます。関数を呼び出したユーザがログインしていない場合はvoidになります。よってuserIdはユーザ名またはnullになります。

当然、ユーザIDの無い(ログインしていない)場合は、エラーを返して関数を落としてください。

const handler = async (_: any, context: https.CallableContext) => {  
  const userId = context.auth ? context.auth.uid : null  

  console.log(userId) // ユーザID or null  

  if (userId === null) {  
    throw new Error()  
  }  
}  

module.exports = https.onCall(handler)  

ユーザ名と正規表現

ユーザ名はドキュメントのIDになるので、IDとして扱えない場合は関数を落としてください。

const usernameId = 'qrunch'  

if (usernameId.match(/^[a-zA-Z0-9_\-.]{3,15}$/) === null) {  
  throw new Error()  
}  

エラーを返す

クライアントサイドに適切なエラーを返してあげてください。返せるエラーは決まっています。FunctionsErrorCodeに存在しないエラーコードは全てINTERNALになるので注意してください。予期しないエラーでクラウド関数の内部の情報が漏洩するのを防ぐためです。

const ALREADY_EXISTS: https.FunctionsErrorCode = 'already-exists'  

if (usernameSnap.exists) {  
  throw new https.HttpsError(ALREADY_EXISTS, `${ALREADY_EXISTS}:username`)  
}  
記事が少しでもいいなと思ったらクラップを送ってみよう!
50
+1
コンビニでバイトしてます。

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする