shimotsu tech

Webフロントエンドエンジニア @ to-R inc.

Node のバージョン管理を nodebrew から Volta に変える

これまで Node のバージョン管理ツールにはずっと nodebrew を使っていたのですが、プロジェクトごとにバージョンを変える必要がある際に不便だったのと、シンプルに飽きてきたというのもあり、比較的新しい Volta に変更してみました。

volta.sh

nodebrew および Node.js をアンインストール

Volta を導入するにあたって、まずは既存の nodebrew および Node.js をアンインストールしました。 Node.js の環境をいじるのは Mac に nodebrew をセットアップして以来でちょっとドキドキする作業でしたが、以下あたりの記事を参考にしつつ、問題なく進めることができました。

offlo.in

qiita.com

最後に、node -v でなにもバージョンが表示されなくなると、Node.js が無事アンインストールされているということなので、次のステップに進みます。

Volta をインストール

基本的には以下のコマンドのみでOKです。 node の他に、yarn も同様にインストールしておきました。

# install Volta
% curl https://get.volta.sh | bash

# install Node
% volta install node

## バージョン指定する場合
% volta install node@latest
% volta install node@16.13.1

# start using Node
% node

これで、Node.js がインストールできました。

% node -v
v16.13.1

プロジェクトごとにバージョンを固定する

Volta にはプロジェクトごとにツールのバージョンを指定できる volta pin コマンドが用意されています。 試しに、適当なプロジェクトで、volta pin node@16.13.1 と指定し実行します。(package.json が存在する前提)

すると、プロジェクトの package.json に以下の記述が追加されます。これによって Volta 側でバージョンを汲み取って指定のバージョンで実行することができます。特に、複数人で開発する際に力を発揮します。

"volta": {
  "node": "16.13.1",
}

参考リンク

zenn.dev

SupabaseのJavaScriptクライアントでテーブル結合を行う

Supabase の JavaScript クライアントでテーブル結合を行う方法をメモ。

www.npmjs.com

テーブルが以下のようにあるとします。ユーザーに紐づく投稿画像があり、その画像に対してコメントが付くようなイメージです。

# ユーザー
users:
  id: string
  fullname: string

# 投稿画像
photos:
  id: string
  userId: string(※foreign key relation: users.id)
  createdAt: Timestamp
  updatedAt: Timestamp
  title: string
  url: string

# 画像へのコメント
comments:
  id: string
  userId: string(※foreign key relation: users.id)
  createdAt: Timestamp
  updatedAt: Timestamp
  body: string
  photoId: number(※foreign key relation: photos.id)

photos を基準に comments と user を結合する

photos を基準に comments と user を結合する場合は以下のように記述します。

const { data: photos } = await supabase
    .from("photos")
    .select(`
      *,
      comments(*),
      user: userId(*), // こう書くことでプロパティ名を指定できる
    `)

photos に結合させた comments 内の user も結合させる(入れ子構造にする)

上記から少し発展させ、photos に結合させた comments に紐づく user も結合させる場合は以下のように記述します。シンプルに select 文を入れ子にすることで実現できます。

const { data: photos } = await supabase
    .from("photos")
    .select(`*, user: userId(*), comments(*, user: userId(*))`)

Supabase の storage から getPublicUrl() した値が404になる問題

Supabase では storage にアップロードしたリソースのパブリックなURLを取得するメソッド getPublicUrl() が用意されているのですが、このメソッドを使って取得した値が 404 になるケースに遭遇しました。

export const removeBucketPath = (key: string, bucketName: string) => {
  return key.slice(bucketName.length + 1) // "/"の分だけ加算している
}

// この結果が404になる
const { publicURL, error } = supabase.storage.from("photos").getPublicUrl(removeBucketPath(photo.url, "photos"))

storage のキーも合っているし、getPublicUrl() に渡す bucket のパスも合っているので、原因がなにか分からずしばらく途方に暮れていました。

しばらく Supabase のダッシュボードを眺めていたら、この問題に気がつきました。

▼これ f:id:zuboriradio:20211226204424j:plain

supabase の bucketprivatepublic かのいずれかの状態があるのですが、これが private になっていました(上記の画像は修正後なので public になっています)。

ここを public に変更したら、無事取得できるようになりました。

supabase.com

The bucket needs to be set to public, either via updateBucket() or by going to Storage on app.supabase.io, clicking the overflow menu on a bucket and choosing "Make public"

【TypeScript】Arrow Functions で Generics を使う方法

TypeScript でアロー関数(Arrow Functions)を書く際に、Generics を使おうとすると、素直に書いた場合うまくコンパイルされない場合があります。

例えば、以下のような T型 の値を引数に取り、そのまま引数を return する関数があるとします。

const identity = <T>(arg: T): T => { return arg; }

これを素直に書くと、React.createElement() メソッドとして認識されてしまい、エラーとなります。

f:id:zuboriradio:20211218164716j:plain

これを防ぐには、<T> の部分を <T, > もしくは <T extends unknown> のように小細工してやる必要があります。

const identity = <T, >(arg: T): T => { return arg; }

f:id:zuboriradio:20211218165148j:plain

const identity = <T extends unknown>(arg: T): T => { return arg; }

f:id:zuboriradio:20211218165204j:plain

【Firebase Auth】ソーシャルログインしたアカウントをすぐに削除しようとすると "auth/requires-recent-login" のエラーが出る

今作っているアプリケーションで、Firebase Authentication のソーシャルログインを活用しているものがある。

そこで、ソーシャルログインしたのち(この時点でFirebase Authentication 上にユーザー情報が作られる)、場合によってすぐにユーザー情報を削除する必要が出てきた。

しかし、ユーザー作成後すぐに同ユーザーを削除しようとすると、"auth/requires-recent-login" のエラーが出ることがわかった。

エラーの内容は、公式ドキュメントによると、以下のように書いてある。

Thrown if the user's last sign-in time does not meet the security threshold. Use firebase.User.reauthenticateWithCredential to resolve. This does not apply if the user is anonymous.

つまり、そういう重要な操作は firebase.User.reauthenticateWithCredential で再認証をしてから行ってくれ、ということらしい。

参照:

firebase.google.com

そこで、ひと手間はかかるが、以下のように一度サインインし、そこで取得した credential を用いて再認証することで、ユーザー情報削除を実行できるようにした。

  /** 会員情報削除 */
  const deleteAuth = async () => {
    try {
      // 削除には再認証が必要なのでここで実行
      firebase
        .auth()
        .signInWithPopup(provider)
        .then((result) => {
          const user = firebase.auth().currentUser

          if (!result || !result.credential) return

          user.reauthenticateWithCredential(result.credential) // credential を渡して削除を実行
            .then(async () => {
              await user.delete()
              alert("会員情報を削除しました")
            })
        })
    } catch (error) {
      // eslint-disable-next-line no-console
      console.log(error)
    }
  }

これで無事ユーザー情報が削除されるようになった。

【Next】Firebase Authentication で取得したユーザ情報を整形してアプリケーション内で使う

Firebase Authentication を活用したアプリケーションにおいて、取得したユーザ情報を useContext などを使ってグローバルに保持するケースがあると思います。

具体的には以下のような実装です。

import { FC, createContext, useEffect, useState } from 'react';
import firebase from '../lib/firebase';

type AuthContextProps = {
  currentUser: firebase.User | null | undefined
}

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined });

const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<firebase.User | null | undefined>(undefined)

  useEffect(() => {
    // ログイン状態をウォッチ
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        // ユーザ情報を格納する
        setCurrentUser(user)
      }
    })
  },[])

  return (
    <AuthContext.Provider value={{ currentUser: currentUser }}>
      {children}
    </AuthContext.Provider>
  )
}

export { AuthContext, AuthProvider }

これでもユーザ情報を格納する目的は果たされるのですが、不要なプロパティを含んだ未整形のユーザオブジェクトがごそっと保持されてしまい、使い勝手がよくありません。

その場合、ユーザ情報を格納する際には、アプリケーション内で必要なプロパティのみを取捨選択した新たなオブジェクト(mappedUser)を作り、それを格納する使い方が有効です。

(略)

export type User = {
  uid: string
  displayName: string | null
  email: string | null
  emailVerified: boolean
  isAnonymous: boolean
  phoneNumber: string | null
  photoURL: string | null
}

useEffect(() => {
    // ログイン状態をウォッチ
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {

        // 必要なプロパティのみを集めた `mappedUser` オブジェクトを定義
        const mappedUser = {
          uid: user.uid,
          displayName: user.displayName,
          email: user.email,
          emailVerified: user.emailVerified,
          isAnonymous: user.isAnonymous,
          phoneNumber: user.phoneNumber,
          photoURL: user.photoURL
        }

        setCurrentUser(mappedUser)
      }
    })
  },[])

(略)

Next.js でクライアントから環境変数が読み込めない

Next.js で今個人的に作っているアプリケーションにおいて、クライアント(ブラウザ)から環境変数(.env.*)が読めないという事象に遭遇したので、その原因と解決法をメモしておきます。

やりたいことは、ページコンポーネント上で、アクセス時に毎回 Firebase の匿名認証を実行するというもの。

useEffect(() => {
    // 匿名ログイン実行
    async function initFirebase() {
      firebase.auth().onAuthStateChanged(async (user) => {
        if (!user) {
          await firebase.auth().signInAnonymously()
        } else {
          console.log(user)
        }
      })
    }

    initFirebase()
  }, [])

この実行時に "auth/invalid-api-key" というエラーが出てしまいました。

ずばり原因としては、.env ファイルで設定した環境変数に問題があり、Firebase の 設定用ファイルにおいて、それを正しく読み込めていませんでした。

本来、Next.js でブラウザから環境変数を呼ぶ際は、以下のように 'NEXT_PUBLIC_****' と命名しなければいけないのですが、

NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=xxxxxxx
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=xxxxxxx
NEXT_PUBLIC_FIREBASE_PROJECT_ID=xxxxxxx
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=xxxxxxx
NEXT_PUBLIC_FIREBASE_APP_ID=xxxxxxx

以下のようにしてしまっておりました。

FIREBASE_PUBLIC_API_KEY=xxxxxxx
FIREBASE_AUTH_DOMAIN=xxxxxxx
FIREBASE_PROJECT_ID=xxxxxxx
FIREBASE_MESSAGING_SENDER_ID=xxxxxxx
FIREBASE_APP_ID=xxxxxxx

上記のように環境変数名を変更すると、無事読み込むことができました。めでたしめでたし。