(個人用)mac初期設定

新規端末を設定する必要があったのでまとめておきます。
OSはVenturaです。
随時更新

バイス周り

ディスプレイ

解像度を上げる
システム環境設定->ディスプレイ->解像度:サイズ調整->スペースを拡大

サブモニター
あれば繋ぐ

ダークモード

システム環境設定->外観 -> 外観モード -> ダーク

Dock

左側に置く
林檎マーク -> システム設定 -> デスクトップとDock -> 画面上の位置を左に変更

Dockを隠す 林檎マーク -> システム設定 -> デスクトップとDock -> Dockを自動的に表示/非表示

Dock表示のアニメーションを切る
terminalで

//アニメーション無効化
defaults write com.apple.dock autohide-time-modifier -int 0
killall Dock

//戻したい時
defaults write com.apple.dock autohide-time-modifier -int 1
killall Dock

Finder

下部にパスを出す

Finderを有効にする -> 表示 -> パスバーを表示

マウス

マウスカーソル大きく
林檎マーク -> システム設定 -> アクセシビリティ -> ディスプレイ -> カーソルのサイズ(半分ぐらい)

スクロールの向きを逆にする
システム設定 -> マウス -> ナチュラルなスクロールをオフ
(mos入れた方が良さげ)

ホットコーナー

画面の隅にマウスカーソルを持って行った時の挙動の設定
林檎マーク -> システム設定 -> デスクトップとDock -> ホットコーナー(一番下)

左上にデスクトップ、右上にMission Control設定

キーボード

(US配列用)日本語切り替え
control + スペース

CapsLockをcontrolに入れ替え

Macで修飾キーの動作を変更する - Apple サポート (日本)

開発ツール周り

VScode

CLIから使えるようにする

Cmd + Shift + P -> shellで検索 -> Install 'code' command in PATH
(再起動すると消える?)

terminal

Docker

設定から自動で起動するようにする
Docker Desktopの歯車アイコン -> General -> start Docker Decktop when you log in

node

slack

通知のバッジをアイコンに表示
slackアプリ側: 環境設定 -> 通知 -> 新しいアクティビティがあった時にバッジで知らせる
mac側: システム設定 -> 通知 -> slackを許可

GraphQLとDataloader

gqlgenのサンプルコードがスッと入ってこなかったので整理

参考URL

Optimizing N+1 database queries using Dataloaders — gqlgen

GitHub - graph-gophers/dataloader: Implementation of Facebook's DataLoader in Golang

【GraphQL × Go】 N+1問題を解決するgqlgen + dataloaderの実装方法とCacheの実装オプション - LayerX エンジニアブログ

gqlgenのGetting Startedで使っているスキーマを使用します。

Building GraphQL servers in golang — gqlgen

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
}

Dataloaderって何

データ取得をまとめ(Batch)たりキャッシュしたりする仕組み。
GraphQLではlazy loadingによるN+1問題の回避に使われる。

resolverの分割とN+1問題

todo resolver内でuserを取得するのではなく、userを取得するresolverを分けることができる。
こうすることでuserフィールドが必要な時だけuser resolverが呼ばれるため、userフィールドが不要なクエリに対してuserの取得処理をスキップすることができる。

一方で取得したN件のtodosに対して、それぞれuser resolverが実行されるためN+1問題が発生する。

lazy loading

短い時間で必要なidを貯めて、時間経過後にデータをまとめて取得する方法。
GraphQLではそれぞれ実行されたuser resolverでidを貯めていき、時間経過後にidを全て受け取った上で取得処理(SQLなど)を実行する。
N件発行されていたSELECT FROM users WHERE id=?SELECT FROM users WHERE id IN (?, ?, ?, ?)の1クエリにまとめるイメージ。

その他の対処法

GraphQLでは有効ではないが一般的なN+1問題回避法について

いずれもresolverの分割と相性が悪く、userフィールドが必要な場合のみuserを読み込むという方法が難しいためGraphQLでは微妙になる

eager loading

Todoを取得するSELECT文と、Userを取得するSELECT文を実行して両方の結果をコード(ORM)側で繋ぎ合わせる方法

JOIN

TodoでのSelect時にUserをJOINで結合してしまう方法
柔軟にフィールドを取捨選択するようなクエリではなかったのと、resolverの仕組みを理解していなかったため私は実務でこれをやっていました。

Go(gqlgenとgraph-gophers/dataloader)での例

loaderの生成

lazy loadingの仕組みから、user resolverで実行するidを貯めていく処理貯めたidからuserをまとめて取得する処理の実装が必要になりそうである。
その前にdataloaderからloaderを作成する。

//dataloaderドキュメントから
// create Loader with an in-memory cache
loader := dataloader.NewBatchedLoader(batchFn)

batchFn := func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
  var results []*dataloader.Result
  // do some async work to get data for specified keys
  // append to this list resolved values
  return results
}

貯めたidからuserをまとめて取得する処理

上記のbatchFnに相当し、gqlgenドキュメントにおけるgetUsersはこれを実装したもの。

SELECT * FROM users WHERE id IN()を実行し、型を合わせていく

//gqlgenドキュメントから
func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] {
//貯めたidからIN句を作る
//SELECT id, name FROM users WHERE id IN (?, ?, ?, ?, ?)
    stmt, err := u.db.PrepareContext(ctx, `SELECT id, name FROM users WHERE id IN (?`+strings.Repeat(",?", len(userIds)-1)+`)`)
    (略)
//result配列を作り
    result := make([]*dataloader.Result[*model.User], 0, len(userIds))
//SQLの結果をresultに詰めていく
    for rows.Next() {
                 (略)
        result = append(result, &dataloader.Result[*model.User]{Data: &user})
    }
    return result
}

idを貯めていく処理

dataloader導入前はuser resolver内でSELECT文を実行していたが、その代わりにloader.Load()を叩く
Loadはidを渡した後に処理がブロックされ、batchFnが実行されたタイミングで返り値としてuserを受け取るイメージ

//gqlgenドキュメントから
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    return loaders.GetUser(ctx, obj.UserID)
}

// GetUser returns single user by id efficiently
func GetUser(ctx context.Context, userID string) (*model.User, error) {
    loaders := For(ctx)//contextに詰めたloaderを取り出す
    return loaders.UserLoader.Load(ctx, userID)()
}

その他の処理

上記が主な処理であり、gqlgenドキュメントでの残りの処理は

  • 複数のloaderをloadersにまとめる(func NewLoaders)
  • リクエストに対してloadersを生成してcontextに仕込むmiddleware(func Middleware)
  • loadersをcontextから取り出す(func For)

など

実行してみる

userをいくつか生成し、Queryの各所にログを仕込んで処理の流れを確認しました。

// User is the resolver for the user field.
func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    fmt.Printf("user resolver for ID %s\n", obj.ID)
    user, err := loaders.GetUser(ctx, obj.UserID)
    fmt.Printf("user ID %s loaded!!\n", obj.ID)
    return user, err
}

batchFnであるgetUsersでは引数のuserIdをもとにUserを作って返します。

func (u *userReader) getUsers(ctx context.Context, userIds []string) []*dataloader.Result[*model.User] {
    fmt.Println("batchFn !!")
    fmt.Println("userIds: ", userIds)
    result := make([]*dataloader.Result[*model.User], 0, len(userIds))
    for i := 0; i < len(userIds); i++ {
        user := model.User{
            ID:   userIds[i],
            Name: "aaa",
        }
        result = append(result, &dataloader.Result[*model.User]{Data: &user})
    }
    return result
}

user resolverのLoadで処理が止まり、batchFnが1度実行されてからresolverに値が返されていることがわかります。

todos Query //func (r *queryResolver) Todos
user resolver for ID T21 //func (r *todoResolver) User(Load前)
user resolver for ID T5
user resolver for ID T61
user resolver for ID T2
user resolver for ID T85
user resolver for ID T19
user resolver for ID T7
user resolver for ID T90
batchFn !! //func (u *userReader) getUsers
user ID T61 loaded!! //func (r *todoResolver) User(Load後)
user ID T90 loaded!!
user ID T85 loaded!!
user ID T2 loaded!!
user ID T21 loaded!!
user ID T7 loaded!!
user ID T19 loaded!!
user ID T5 loaded!!

配列の場合はどうなる??

サンプルではtodoとuserが1対1でしたが、1対多の場合も気になったのでTaskに紐づくUserを単数のleaderと複数のmembersに分けました。

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  leader: User!
  members: [User!]!
}

同じbatchFnにleaderのidとmembersのidをごっちゃに入れてloadした時、どうなるでしょうか。
Loadの引数と返り値のIDが変化していないか見てみます。

todo作成時にleaderIdを引数で固定(1)、memberIdをランダムに2つ入れます。

 todo := &model.Todo{
        Text:      input.Text,
        ID:        fmt.Sprintf("T%d", randNumber),
        LeaderID:  input.LeaderID,
        MemberIDs: []string{fmt.Sprintf("%d", memberRand1), fmt.Sprintf("%d", memberRand2)},
    }

gqlgen.ymlにfieldを追加してgenerateすると空のresolver(leader, members)が生えます。

  Todo:
    fields:
      leader:
        resolver: true
      members:
        resolver: true

生成されたresolverを実装します。渡したidと帰ってきたidをprintでチェックします。

// Leader is the resolver for the leader field.
func (r *todoResolver) Leader(ctx context.Context, obj *model.Todo) (*model.User, error) {
    user, err := loaders.GetUser(ctx, obj.LeaderID)
    fmt.Printf("leader resolver loaded. before_leaderId = %s, after_leaderId= %s\n", obj.LeaderID, user.ID)
    return user, err
}

// Members is the resolver for the members field.
func (r *todoResolver) Members(ctx context.Context, obj *model.Todo) ([]*model.User, error) {
    users, _ := loaders.GetUsers(ctx, obj.MemberIDs)

    var userIDs []string
    for _, v := range users {
        userIDs = append(userIDs, v.ID)
    }

    fmt.Printf("member resolver loaded. before_MemberIDs = %v, after_MemberIDs= %v\n", obj.MemberIDs, userIDs)

    return users, nil
}

loaders.GetUsersの内部ではLoadの複数版であるLoadManyを叩いています。

// GetUsers returns many users by ids efficiently
func GetUsers(ctx context.Context, userIDs []string) ([]*model.User, []error) {
    fmt.Println("Load memberIds: ", userIDs)
    loaders := For(ctx)
    return loaders.UserLoader.LoadMany(ctx, userIDs)()
}

3件入れた後、queryを実行しました。

入力([leaderId, memberID1, memberId2])
userIds:  [1 58 34]
userIds:  [1 84 23]
userIds:  [1 3 46]
todos Query
Load leaderId:  1
Load leaderId:  1
Load memberIds:  [58 34]
Load leaderId:  1
Load memberIds:  [84 23]
Load memberIds:  [3 46]
batchFn !!
userIds:  [1 58 34 84 23 3 46] // batchFnに渡されたids
leader resolver loaded. before_leaderId = 1, after_leaderId= 1
leader resolver loaded. before_leaderId = 1, after_leaderId= 1
member resolver loaded. before_MemberIDs = [3 46], after_MemberIDs= [3 46]
member resolver loaded. before_MemberIDs = [58 34], after_MemberIDs= [58 34]
member resolver loaded. before_MemberIDs = [84 23], after_MemberIDs= [84 23]
leader resolver loaded. before_leaderId = 1, after_leaderId= 1

というわけでbatchFn内でまとめてデータをfetchしても、内部でうまいこと対応するデータを返してくれているようです。

docker-compose.yamlで起動させるサービスを制御

各サービスでprofilesを記述しておくとdocker compose upでは起動せず、docker compose --profile {指定したprofile名} upの時だけ起動するようにできるとのこと。
profilesを記述しなかったサービスについてはコマンド中にprofile指定に関係なく常に起動するようです。

stackoverflow.com

docs.docker.com

(prisma)Can't reach database server at

nest.jsの学習を始めたところdocker composeでprismaコンテナとpostgresコンテナを立てている状態で、prismaからDBに接続できませんでした。
慣れた作業のはずなのに何故

結論

タイムアウトが原因でした。。。 for.kobayashiii.dev こちらの情報無かったら一生気づかなかったと思います。ありがとうございます。


以下調べてた時の様子になります。

DB設定(docker-compose.yaml)

  dev-postgres:
    image: postgres:14.4-alpine
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: passwd
      POSTGRES_DB: db
    restart: always

prismaコンテナ内からサービス名dev-postgresで接続できる(warning出てますが)

root@a8bdedef3739:/app# psql -h dev-postgres -p 5432 -U user -d db
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = (unset),
        LC_ALL = (unset),
        LANG = "en_US.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
Password for user user: 
psql (11.21 (Debian 11.21-0+deb10u2), server 14.4)
WARNING: psql major version 11, server major version 14.
         Some psql features might not work.
Type "help" for help.

db=# 

prisma設定箇所
調査のため一時的に直書き

datasource db {
  provider = "postgresql"
  //url      = env("DATABASE_URL")
  url      = "postgresql://user:passwd@dev-postgres:5432/db?schema=public"
}

prisma migrateでエラー

root@a8bdedef3739:/app# npx prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "db", schema "public" at "dev-postgres:5432"

Error: P1001

Can't reach database server at `dev-postgres`:`5432`

Please make sure your database server is running at `dev-postgres`:`5432`.

こういうエラーで時間取られると辛いですね。。

Cognitoへの移行

ログイン周りの処理を自前で実装してある程度理解したのでcognitoに移行させたい

ユーザーIDはcognito側から払い出されたものを自前のユーザーテーブルで二重管理するイメージになるんだろうか

IDプロバイダ側(Googleなど)の設定
Adding social identity providers to a user pool - Amazon Cognito

react向けチュートリアル
Amazon Cognito と AWS Amplify を使用して React アプリケーションユーザーを認証する - AWS 規範的ガイダンス

以下に実装しながら気づいた点などメモします。

Hosted UIを使うかどうか

AWS側で用意されたページに飛ばされるので使わないケースの方が多そう

AmplifyのUIコンポーネントを使うかどうか

Hosted UIと同じでカスタマイズ性に欠けるので理想的には使わないのが良さそうだが、UIの作成と認証処理をかなり省けそう(+安全)なので一旦使ってみる
特にIDプロバイダからのサインインは作り方がさっぱりなので助かる

トークンのバックエンドでの検証

JSON Web トークンの検証 - Amazon Cognito

Amplify.configureの引数について

もしかするとCLIで作成するのがマナーなのかも

https://docs.amplify.aws/lib/client-configuration/configuring-amplify-categories/q/platform/js/#scoped-configuration

user_idに相当する値

accessTokenのusernameがユニークな値なのでこれが使える
またidTokenのcognito:usernameも同じ値だと思われる

ユーザープール属性 - Amazon Cognito

お客様のアプリケーションでユーザー名が不要の場合は、ユーザーにユーザー名を指定するように求める必要はありません。アプリでユーザー用の一意のユーザー名をバックグラウンドで作成できます。  
username はユーザープール内で一意である必要があります。username は再利用できますが、削除されて使用されなくなった場合のみです。

またcognitoのユーザープールを作成する際にサインインオプションとしてusernameとemailを必須にすると、usernameが固有な代わりに同じemailで複数のユーザーが登録できてしまい、想定する仕様(emailがunique)と異なるのでやめました。
ただcognitoでの(フォーム経由の)登録とソーシャルIdPからの登録が同じemailだった場合、それは重複してしまうようです。

どのトークンをバックエンドの認証に使うのか

cognitoから渡されるトークンはaccessToken, idToken, refreshTokenの3種類。
もちろんaccessTokenだろ!と思ってたのですが、それぞれの役割やpayloadを見てみると、IDトークンを使うのが正しそうです。

IDトークン:連携サービスの認証(例:API Gateway)と認証されたユーザー情報の参照
アクセストークン:Cognitoユーザープールのユーザー属性更新
更新トークン:新しいIDトークン、アクセストークンの取得

Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する | DevelopersIO

accessTokenはあくまでもcognitoに対するアクセスを許可するトークンという意味合いっぽいですね。

トークンのリフレッシュ処理について

アクセストークンの寿命を最短(5分)にして確認したところ、amplifyを使ってれば自動で更新してくれるようです。ありがとう。

googleソーシャルログインに審査が要るのかどうか

機密性の高いデータにアクセスする場合は必要だそうですが、機密性の高いデータとは何だろうか。
emailだけ取れれば十分なんですが・・・

AWS側のドキュメントによると最低限必要なscopeは3つ(email, profile, openid)ですが、OAuth同意画面を再確認したところ全て非機密のスコープにカテゴリされていたので審査は必要なさそうです。

terraformでaws_cognito_identity_providerを管理しようとすると差分が出る

authorize_url, oidc_issuerなどのドキュメントのexampleに載ってないパラメータがnullになる差分出ますが、applyして動作を確認したところ問題なさそう。

      ~ provider_details  = {
          - "attributes_url"                = "https://people.googleapis.com/v1/people/me?personFields=" -> null
          - "attributes_url_add_attributes" = "true" -> null
          ~ "authorize_scopes"              = "openid email profile" -> "profile email openid"
          - "authorize_url"                 = "https://accounts.google.com/o/oauth2/v2/auth" -> null
          - "oidc_issuer"                   = "https://accounts.google.com" -> null
          - "token_request_method"          = "POST" -> null
          - "token_url"                     = "https://www.googleapis.com/oauth2/v4/token" -> null

とはいえapplyする度に差分が出るので、ignore_changesで無視しようとのこと。 "aws_cognito_identity_provider" always have some changes on "provider_details" when "provider_type" is "Google" or "Facebook" · Issue #4831 · hashicorp/terraform-provider-aws · GitHub

cognito関連のuser_pool_idなどをどこに保管するか??

払い出されるid等はsecretとは書かれてないとはいえenvファイルに含めない方が良い感じがしますが、reactでは(webフロントエンド全般?).envをコミットしないようにしても最終的にビルドに埋め込まれるため閲覧可能になってしまうようです。

stackoverflow.com

じゃあどうすんのよって話ですが、仮に外部からcognitoへ不正アクセスされても、サインアップ後のemailやSMS検証で弾けるから大丈夫とのこと。(検証してなかったらアウト)

stackoverflow.com

というわけで.envに書いてgit管理しても致命的な状況にはならなそうですが、念のためCICD中に埋め込むようにしときます。

後記

移行完了しました。

バックエンド側で実装したトークン認証処理をcognito用に置き換えましたが、このトークンを自前で生成できないのとパースするために外部通信が必要でテストに困りました。

同じpayloadを持つJWTを作ってパースできれば認証処理以外のテストは通せるかということで、結局移行前に実装した処理をテストで引き続き使っています。