ReactとAmazon Cognitoで認証機能を作ってみる
Amazon Cognitoを使ったのでその覚書ついでに:pencil2: 細かい説明とかはないです:relaxed:
ユーザー管理機能を設定する前に
Amazon Cognitoでユーザーを管理できるようにしますので、とりあえず、Reactのアプリを作成します。
$ npx create-react-app react-aws-cognito --typescript
アプリが作成されたら、srcディレクトリの中に、Cognitoを利用するためのコンフィグファイルを作成します。
// awsConfiguration.ts const awsConfiguration = { region: 'ap-northeast-1', IdentityPoolId: 'ap-northeast-1:**********', UserPoolId: 'ap-northeast-1_**********', ClientId: '**********', } export default awsConfiguration
**********となっている部分はこれから説明します。
以下もインストールしてください。
$ yarn add amazon-cognito-identity-js
Cognitoをjsで楽にいじられるモジュールです。 https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js
ユーザープールを作成する
Cognitoに入ると上のような画面になるので、まずは「ユーザープールの管理」に入ります。 「ユーザープールを作成する」ボタンがあると思いますので、そこから作成画面へ入っていきます。
こんな具合で設定画面が出てくるので、順に設定します。
ユーザープールはそのままの意味で、ユーザーのみずたまりです。 ユーザーの倉庫のようなものです。
今回はデフォルトで作ります。
デフォルトの場合こんな設定になります。 必須の属性はemailとなり、これはサインアップ時に必須の項目となります(細かく設定するとこの辺りも変えられます)。
パスワードの最小長や多要素認証、また、登録時に送信するEメールやSMSの内容を編集したりもできます。 lambdaと連携させることも可能です:gear:
次にIDプールを作ります。
IDプールを作成する
フェデレーテッドアイデンティティから「新しいIDプールの作成」をクリックすると、以下のような画面が表示されます。
IDプール名を入力し、認証プロバイダーのところでCognitoタブを選択して必要な情報を入れます。
ユーザープールID
作成したユーザープールの「全般設定」(ユーザープールから作成したのを選択すると表示されます)にプールIDがあるのでこれを入力してください、
IDプール
作成したユーザープールの「アプリクライアント」からアプリクライアントを作成すると表示される、アプリクライアントIDを設定してください。
今回はJavascriptSDKを利用するのですが、その場合は「クライアントシークレットを生成」のチェックを外してください。JSのSDKでの対応ができていないようですね。。。 https://qiita.com/noobar/items/6615501b035e47792227
入力したら、「プールの作成」を押し、IAMロールを作成します。 ここのロールは、Authenticated UserとUnauthenticated Userそれぞれのアプリケーションの利用可能範囲を定めたものを表します。
Authenticated Userには、アプリケーションに登録したユーザーとして実行可能な範囲のアクションと、そのアクションに伴うAWSリソースへのアクセスを認めるべきです。 反対に、Unauthenticated UserにはAuthenticated Userと比較してより厳しい範囲でのアプリケーションの利用を認めるようにした方が無難です。
IDプールに紐づくIAMロールを作成するので、「許可」を押して次へ行きましょう。 作成が成功したら、「Amazon Cognitoでの作業開始」が出てくるので、ドロップダウンからjavascriptを選びます。 すると、js用のコードが出てきて、その中に、IdentityPoolIdが表記されているので、それをコピーしておきます。
ここまでで今一度、 IdentityPoolId、UserPoolId、 ClientIdを整理しておきましょう(冒頭のコンフィグファイルの項目です)。
IdentityPoolId ・・・ IDプール作った時に出てきたやつ。 UserPoolId ・・・ ユーザープールの「全般設定」に表示されるプールID。 ClientId ・・・ ユーザープールの「アプリクライアント」に表示されるアプリクライアントID。
上記は全てコピーして、冒頭で書いたawsConfiguration.tsに書き込みます。 srcディレクトリ内はこんな感じになります。:point_down_tone2:
. ├── auth(SignUp.tsx, Verification.tsx, SignIn.tsx, SignOut.tsx) ├── App.css ├── App.test.tsx ├── App.tsx ├── awsConfiguration.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts └── serviceWorker.ts
サインアップ
tsxで書いたサインアップのコンポーネントです。 肝はSignUp関数です。
必須の属性として設定されていたemailとパスワードを入力すると、入力したemailあてにメールが届きます(もちろん電話番号で設定するとSMSでも届けられるように設定できます)。
このメールの文言もAWSコンソールで設定できます。 コードの有効期限はデフォルトでは7日になっているようです。
ちなみに、パスワードのポリシーはデフォルトで以下のようになっているので、注意してください。
// SignUp.tsx import React from 'react' import '../App.css' import { CognitoUserPool, CognitoUserAttribute } from "amazon-cognito-identity-js" import awsConfiguration from '../awsConfiguration' const userPool = new CognitoUserPool({ UserPoolId: awsConfiguration.UserPoolId, ClientId: awsConfiguration.ClientId, }) const SignUp: React.FC = () => { const [email, setEmail] = React.useState<string>('') const [password, setPassword] = React.useState<string>('') const changedEmailHandler = (event: any) => setEmail(event.target.value) const changedPasswordHandler = (event: any) => setPassword(event.target.value) const signUp = () => { const attributeList = [ new CognitoUserAttribute({ Name: 'email', Value: email }) ] userPool.signUp(email, password, attributeList, [], (err, result) => { if (err) { console.error(err) return } setEmail('') setPassword('') }) } return ( <div className="SignUp"> <h1 style={{ textAlign: 'left' }}>SignUp</h1> <input type="text" placeholder="email" onChange={changedEmailHandler} /> <input type="text" placeholder="password" onChange={changedPasswordHandler} /> <button onClick={signUp}>SignUp</button> </div> ) } export default SignUp
検証
サインアップで受け取った検証コードを使ってサインアップを完了しましょう。
検証コードと必須の属性であるemailを入力する以下のコードを実行すると、アカウントのステータスがUNCONFIRMEDだったのが画像のようにCONFIRMEDに変わります(ユーザープール/全般設定/ユーザーとグループ)。
// Verification.tsx import React from 'react' import '../App.css' import { CognitoUserPool, CognitoUser } from "amazon-cognito-identity-js" import awsConfiguration from '../awsConfiguration' const userPool = new CognitoUserPool({ UserPoolId: awsConfiguration.UserPoolId, ClientId: awsConfiguration.ClientId, }) const Verification: React.FC = () => { const [email, setEmail] = React.useState<string>('') const [verificationCode, setVerificationCode] = React.useState<string>('') const changedEmailHandler = (event: any) => setEmail(event.target.value) const changedVerificationCodeHandler = (event: any) => setAuthCode(event.target.value) const verifyCode = () => { const cognitoUser = new CognitoUser({ Username: email, Pool: userPool }) cognitoUser.confirmRegistration(authCode, true, (err: any) => { if (err) { console.log(err) return } console.log('verification succeeded') setEmail('') setAuthCode('') }) } return ( <div className="Verification"> <h1>Authenticate</h1> <input type="text" placeholder="verification code" onChange={changedVerificationCodeHandler} /> <input type="text" placeholder='email' onChange={changedEmailHandler} /> <button onClick={verifyCode}>Authenticate</button> </div> ) } export default Verification
サインイン
それでは検証成功したアカウントでサインインしてみます。 SignIn関数でサインインを行い、認証が成功すればonSuccessを通ってresultを返します。 ここではresultからaccessTokenを取り出してログに出しています。
// SignIn.tsx import React from 'react' import '../App.css' import { CognitoUserPool, CognitoUser, AuthenticationDetails } from "amazon-cognito-identity-js" import awsConfiguration from '../awsConfiguration' const userPool = new CognitoUserPool({ UserPoolId: awsConfiguration.UserPoolId, ClientId: awsConfiguration.ClientId, }) const SignIn: React.FC = () => { const [email, setEmail] = React.useState<string>('') const [password, setPassword] = React.useState<string>('') const changedEmailHaldler = (e: any) => setEmail(e.target.value) const changedPasswordHandler = (e: any) => setPassword(e.target.value) const signIn = () => { const authenticationDetails = new AuthenticationDetails({ Username : email, Password : password }) const cognitoUser = new CognitoUser({ Username: email, Pool: userPool }) cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) => { console.log('result: ' + result) const accessToken = result.getAccessToken().getJwtToken() console.log('AccessToken: ' + accessToken) setEmail('') setPassword('') }, onFailure: (err) => { console.error(err) } }) } return ( <div className="SignIn"> <h1>SingIn</h1> <input type="text" placeholder='email' onChange={changedEmailHaldler}/> <input type="text" placeholder='password' onChange={changedPasswordHandler}/> <button onClick={signIn}>Sign In</button> </div> ) } export default SignIn
サインイン中のアカウントを検知する場合は、
const cognitoUser = userPool.getCurrentUser() if (cognitoUser) { // sign inしている状態 console.log('signing in') } else { // sign inしていない状態 console.log('no signing in') }
という感じで行い、サインアウト時もこれがtrueの時に処理できるようにします。
サインアウト
サインアウトはこちら。 userPool.getCurrentUser()で現在サインイン中のアカウントを検知し、 サインインされていればcognitoUser.signOut()を、 そうでなければとりあえず念の為localStorageを綺麗にしています(サインインするとlocalStorageにもaccessToken等のデータが入るので)。
// SignOut.tsx import React from 'react' import '../App.css' import { CognitoUserPool } from "amazon-cognito-identity-js" import awsConfiguration from '../awsConfiguration' const userPool = new CognitoUserPool({ UserPoolId: awsConfiguration.UserPoolId, ClientId: awsConfiguration.ClientId, }) const SignOut: React.FC = () => { const signOut = () => { const cognitoUser = userPool.getCurrentUser() if (cognitoUser) { cognitoUser.signOut() localStorage.clear() console.log('signed out') } else { localStorage.clear() console.log('no user signing in') } } return ( <div className="SignOut"> <h1>SignOut</h1> <button onClick={signOut}>Sign Out</button> </div> ) } export default SignOut
全部まとめて整理するとこんな感じ?:point_down_tone2:
// App.tsx import React from 'react' import './App.css' // components import SignUp from './auth/SignUp' import Verification from './auth/Verification' import SignIn from './auth/SignIn' import SignOut from './auth/SignOut' import { CognitoUserPool } from "amazon-cognito-identity-js" import awsConfiguration from './awsConfiguration' const userPool = new CognitoUserPool({ UserPoolId: awsConfiguration.UserPoolId, ClientId: awsConfiguration.ClientId, }) const App: React.FC = () => { const authentication = () => { const cognitoUser = userPool.getCurrentUser() // サインインユーザーがいればアプリのメイン画面へ、 // いなければサインアップ、検証、サインイン画面を表示する。 if (cognitoUser) { return ( <div className="authorizedMode"> <SignOut /> </div> ) } else { return ( <div className="unauthorizedMode"> <SignUp /> <Verification /> <SignIn /> </div> ) } } return ( <div className="App"> <header /> { authentication() } </div> ) } export default App
グループ(IAMロール)の設定
Cognitoでは特定のIAMロールを持つグループを作成することができ、 そのグループに属するユーザーはそのIAMロールのポリシーに従ってAWSリソースへのアクセスが制限されます。
IAMロールから割り当てたいロールを選択してグループを作成します。 以降、AWSコンソールからグループにユーザーを割り当てることができるようになります。
ですが、これ、毎回手動でAWSコンソールをいじるのも面倒すぎるので、JavascripSDKを利用してやりたいです。 adminAddUserToGroupを利用して実現します。 https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminAddUserToGroup-property
import AWS from 'aws-sdk' const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider() const params = { GroupName: 'STRING_VALUE', /* required */ UserPoolId: 'STRING_VALUE', /* required */ Username: 'STRING_VALUE' /* required */ } cognitoidentityserviceprovider.adminAddUserToGroup(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response })
上のように書くみたいです(試してませんが)。 検証が成功した後にこの処理を挟めば良いと思います。
Usernameはおそらくメールアドレスを入力すれば問題ないかと思います。 もしかしたら必須の属性設定によるかもしれませんが。。。
基本的には以上でCognitoを利用した認証機能を実装できるはずです:hugging:
デモ作ってみました。ここで説明したawsConfiguration.tsは入っていません。 https://github.com/yutaro1204/AmazonCognitoWithReact
EC2のNodeサーバーと連携するReactクライアントをS3にデプロイしてみる
AWS Summitに参加してからAWS関連でなんかしたいなあと思っていたので、 S3にReactで作ったクライアントを、EC2にNodeのサーバーを置いて、 簡単にアプリをデプロイしてみます:desktop:
以下の準備が整っていることを前提とします。 ・Reactでクライアントアプリを作っており、これと連携しているサーバーサイドも作られている。 ・EC2インスタンスを作成しており、sshでEC2に入れる。 ・作成したEC2のセキュリティグループのインバウンドの設定でhttpによる接続を受け付けられるようにしておく(これは後で設定しても良いですが)。
EC2にNodeで作ったサーバーを置く
まず、
をインストールします。 Nodeサーバーの場合なので、他の言語で書いていたりしたらそれに合わせてください。
$ yum update -y $ yum install git -y $ yum install nginx -y $ yum install python3 -y $ pip3 install aws-cli $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash $ vim ~/.bash_profile $ source ~/.bash_profile $ nvm install --lts $ nvm use --lts $ node -v
.bash_profileには以下を書き込みます。
# .bash_profile # /home/production/のところは個別に設定してください。アプリケーションのディレクトリを置くところとかでいいと思います。 export NVM_DIR="/home/production/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
次にサーバーのソースをクローンしてきます。 あと、nodeのプロセスを永続化するためにpm2を導入します。 pm2参考 => https://qiita.com/ikemura23/items/b3481393d4edca2d5188
$ git clone node-server $ npm install -g pm2 $ cd node-server $ pm2 start index.js --name node-server-on-ec2
EC2内で動いているサーバーにアクセスできるように、 外からやってきたhttpのリクエストをサーバーに転送してあげます。
ここで作ったindex.jsの設定ではlocalhost:3000をlistenしているので、 nginxでプロキシーの設定をして80番ポートにやってくるアクセスを3000番に転送します(localtion /を設定)。 nginxの設定参考 => https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967 (プロキシーさせるためにもEC2へのhttpの接続をセキュリティグループで有効にしておくこと)
$ vim /etc/nginx/nginx.conf
# nginx.conf server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { proxy_pass http://localhost:3000; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }
これでec2のパブリックIPに接続すると、EC2内で動いているnodeサーバーにアクセスできます。 エラーが出る場合、pm2やnginxの再起動などを行ってください。 セキュリティグループの設定も忘れずに:information_desk_person:
とりあえずここまででサーバー側のアプリをEC2に設置して、パブリックIPでアクセスできるようになります。
S3にReactで作ったクライアントを置く
1. バケット作成
S3でバケットを作成してください。 バケット名とリージョンを設定し、あとは基本的にデフォルトで良いです。 が、ブロックパブリックアクセスはオフにしておきます。
2. Static website hostingの設定
バケット作成後、Static website hostingを有効化するために、 Reactでbuildしたindex.htmlをインデックスドキュメントとして設定します。
3. バケットポリシーの設定
{ "Version":"2012-10-17", "Statement":[{ "Sid":"PublicReadForGetBucketObjects", "Effect":"Allow", "Principal": "*", "Action":["s3:GetObject"], "Resource":["arn:aws:s3:::example-bucket/*"] }] }
静的ウェブサイトをセットアップする => https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html
4. IAMでアクセス権限を持ったユーザーを作成する
IAMに入ってサイドペインからユーザーを選択、ユーザーを追加ボタンでユーザーを作成します。
ユーザー名を設定し、プログラムによるアクセスにチェックを入れます。
次に、既存のポリシーを直接アタッチでs3のアクセス権を選択し、これを付与します。
ここではAmazonS3FullAccessを選びますが、セキュリティなども考慮すればReadOnlyAccessの方が良かったり、時と場合によっては何が良いかが別れると思うので、その都度使い分けてください。
後の項目は基本デフォルトで良いです。 ユーザーを作成すると、AccessKeyIdとSecretAccessKeyの情報が記載されたCSVができると思うので、 その中にあるそれぞれの文字列を次に利用していきます。
5. EC2内でaws configureを行う
先ほど取得したIAMユーザーのAccessKeyIdとSecretAccessKeyをEC2にインストールしたawsコマンドで有効化します。
$ aws configure AWS Access Key ID ************** AWS Secret Access Key ************** Default region name ap-northeast-1 Default output format json
AccessKeyId, SecretAccessKey, region, output formatの入力を促されるので、それぞれ入力します。 output formatはjsonで良いです。
6. ReactでbuildしたファイルをS3にアップロード
ここまでできたらあとはbuildしたファイルたちをS3にアップロードするだけです。 Reactプロジェクトの中に入って、以下のコマンドを打ちます。 バケット名は自分ので埋めてください。
$ aws s3 sync ./build s3://{バケット名} --acl public-read
.DS_StoreとかがS3に入るのが嫌なら、以下のオプションを追加して省くこともできます。
--exclude '*.DS_Store'
また、今回はEC2でIAMユーザーを一つだけ設定したかったので、aws configureを使いましたが、 複数のIAMユーザーが必要なら、~/.awsの中にあるconfigファイルおよびcredentialファイルにそれぞれの情報を書き込みます。
[default]となっているプロファイル(IAMユーザー)はaws configureで設定したものと同じになるようです。 [profile_name]としてプロファイルを複数作成していけます。
# ~/.aws/credentials [default] aws_access_key_id = XXXXXXXXXX aws_secret_access_key = XXXXXXXXXX [test] aws_access_key_id = XXXXXXXXXX aws_secret_access_key = XXXXXXXXXX
特定のIAMユーザーの権限でs3へアップロードを行いたい場合などは以下のようにコマンドを実行します。
$ aws s3 sync ./build s3://{バケット名} --acl public-read --profile {profile_name}
以上でデプロイが完了ですので、以下のようなURLでアプリケーションに接続できます。
http://{s3バケット名}.s3-website-ap-northeast-1.amazonaws.com
自分の作ったアプリが表示されれば成功です! EC2に置いてあるサーバーとも連携が取れているか確認してみてください。
styled-componentsで再利用可能なグリッドレイアウトを作る
やること
Reactでコンポーネントを組み立ててUI作っていくのも楽なんですが、 styled-componentsでグリッドレイアウトのコンポーネントを作って、 それらを組み合わせて使うと非常に楽だということに気がつきました。
一度作ったやつを取っておけば再利用することもできますし:open_hands_tone2:
styled-componentsで書いたcssの記述も整理しやすいですし、おすすめです。 GitHub参考 => https://github.com/yutaro1204/styled-components-demo
前提
・styled-componentの使い方 ・CSSでのdisplay: grid;の使い方
TypeScriptでReactのコンポーネントを書いていきますので、tsxを使います。
導入
まず気楽にcreate-react-appを使います。
$ npx create-react-app app-name --typescript
create-react-appが入っている場合はnpxは不要です。
styled-componentsをインストールします。
$ yarn add styled-components @types/styled-components
アプリを立ち上げます。デフォルトはhttp://localhost:3000となります。
$ yarn start
コーディング
最終的にこんな感じの構造になります。
app-name/src ├── App.css ├── App.test.tsx ├── App.tsx ├── components │ ├── Header.tsx // 追加 │ ├── Main.tsx // 追加 │ └── Sidepane.tsx // 追加 ├── index.css ├── index.tsx ├── logo.svg ├── modules │ └── styled-components.ts // 追加 ├── react-app-env.d.ts └── serviceWorker.ts
デフォルトで入っているsrcディレクトリ配下にmodulesディレクトリみたいなのを作って、 styled-components用のファイルを作成します。
// styled-components.ts import style from 'styled-components' const styledComponents = { LayoutStyles: { Body: { Grid: style.div` display: grid; grid-template-columns: 300px 1fr; grid-template-rows: 80px 1fr; ` }, Header: { Grid: style.div` grid-column: 1 / 3; grid-row: 1 / 2; display: grid; grid-template-columns: 40px 1fr 1fr 1fr 1fr 40px; background-color: #008080; `, // 状況によってgrid-columnの値を変化させる Item: style.div` grid-column: ${ ({theme}) => theme.column }; max-height: 80px; ` }, Main: { Grid: style.div` grid-column: 2 / 3; grid-row: 2 / 3; display: grid; grid-template-columns: 100px 1fr 1fr 1fr 1fr 100px; height: calc(100vh - 80px); overflow-y: scroll; background-color: #ffe4c4; `, Item: style.div` grid-column: ${ ({theme}) => theme.column }; grid-row: ${ ({theme}) => theme.row }; height: 200px; `, }, Sidepane: { Grid: style.div` grid-column: 1 / 2; grid-row: 2 / 3; height: calc(100vh - 80px); overflow-y: scroll; background-color: #696969; `, Item: style.div` width: 100%; height: 200px; ` } }, ComponentStyles: { // As you prefer... } } export default styledComponents
ここでは、LayoutStylesにBodyやHeaderなどのCSS情報をまとめたものと、 その他の細かいコンポーネントのCSS情報をComponentsStylesにまとめるようにしています(ここには何も書いていません)。
上のstyled-components.tsに従ってtsxを書き換えるとこんな感じになりました:point_down_tone2:
// App.tsx import React from 'react' import styledComponents from './modules/styled-components' import Header from './components/Header' import Sidepane from './components/Sidepane' import Main from './components/Main' const BodyGrid = styledComponents.LayoutStyles.Body.Grid const App: React.FC = () => { return ( <BodyGrid> <Header /> <Sidepane /> <Main /> </BodyGrid> ) } export default App
んで、参考までにこのHeaderコンポーネントとなっているHeader.tsxはこんな感じに:point_down_tone2:
// Header.tsx import React from 'react' import styledComponents from '../modules/styled-components' const HeaderGrid = styledComponents.LayoutStyles.Header.Grid const HeaderItem = styledComponents.LayoutStyles.Header.Item const Header: React.FC = () => { return ( <HeaderGrid> <HeaderItem theme={{ column: '2 / 3' }}> <p>Grid1</p> </HeaderItem> <HeaderItem theme={{ column: '3 / 4' }}> <p>Grid2</p> </HeaderItem> <HeaderItem theme={{ column: '4 / 5' }}> <p>Grid3</p> </HeaderItem> <HeaderItem theme={{ column: '5 / 6' }}> <p>Grid4</p> </HeaderItem> </HeaderGrid> ) } export default Header
おんなじ要領でHeader、Sidepane、Mainを組み合わせると、こんな感じになります。
ちょっと地味ですが、ここから色々装飾すればそれなりになるかと:information_desk_person:
styled-componentsとreactDnDをつかってなんか作りたい今日この頃。。。
Goのmgoで$lookupと$projectを使って結合したデータを取得する
mongoDBでの結合とフィルター
mongoDBはドキュメント型のNoSQLですが、左外部結合ができます。 で、左外部結合したうえでフィルターにかけ、要求されたデータを返すことができます。
前半の説明は以下のページを参考にしています。 Mongodb Join on _id field from String to ObjectId MongoPlayground
$lookup
$lookup
により、join先の指定したフィールドのドキュメントから要求されたドキュメントを配列にし、join元の指定されたドキュメントに組み込みます。
{ $lookup: { from: <join先のコレクション>, localField: <join元のフィールド>, foreignField: <join先のフィールド>, as: <join元に組み込まれることになる配列フィールド> } }
mongoDB Documentation $lookup (aggregation)
$lookup
のようなAggregationPipelineStagesはdocument.aggregate()に指定して実行します。
AggregationPipeline
db.document.aggregate([{ここで指定する}])
すなわち、
db.original_collection.aggregate([ { "$lookup": { "from": "another_collection", "localField": "_id", "foreignField": "original_collection_id", "as": "another_collections" } } ])
このようにして使うことができます。
ですが、このままでは結合されたデータを取得することはできません。
取得するためにはもう一つ、$project
というステージを追加する必要があります。
$project
$project
では、取得するドキュメントに対して設定を割り当てることができます。
{ $project: { <設定> } }
設定の中では、どのフィールドの値を表示するかとか、特定のフィールドの値を変形させるなどして新しいフィールドに置き換えるとかなどを行えます。
ここでは、idとjoin元に紐づくjoin先のデータをまとめた配列を指定します。 この時、idはobjectIdなので、この中身だけを引っ張り出してきたstringに変換します。
したがって、$project
の設定は以下のようになります。
{ "$project": {"_id": { "$toString": "$_id" }} }
ここで$lookup
と$project
を組み合わせて、以下のクエリーを作成します。
db.original_collection.aggregate([ { "$project": {"_id": { "$toString": "$_id" }} }, { "$lookup": { "from": "another_collection", "localField": "_id", "foreignField": "original_collection_id", "as": "another_collections" } } ])
これを実行すると、おそらく以下のようなレスポンスが返ってくるでしょう(DBの中身が正しければ)。
{ "_id" : "5caae079e1382335f04c5846", "another_collections" : [ { "_id" : ObjectId("5caae1cce1382337367b5316"), "name" : "anotherCollection", "original_collection_id" : "5caae079e1382335f04c5846" } ] }
これをmgoでやりたい
上に述べたのは、mongoのDBに入って直接いじるやり方です。
Goのmgoでやるときは下のようにしてやります。 mgoの使い方とか細かい部分は端折って、typeの定義の部分と、dbでfindする部分だけ書きます。
// typeの定義 type SubCollection struct { // join先 Id bson.ObjectId `bson:"_id"` Name string `bson:"name"` OriginalCollectionId string `bson:"original_collection_id"` } type OriginalCollection struct { // join元 Id bson.ObjectId `bson:"_id"` Name string `bson:"name"` SubCollections []SubCollection } // 結果を取得する var AllOriginalCollections []OriginalCollection query := []bson.M{ { "$project": bson.M{ "_id": bson.M{"$toString": "$_id"}, }, }, { "$lookup": bson.M{ "from": "SubCollections", "localField": "_id", "foreignField": "original_collection_id", "as": "original_collections", }, }, } // コレクションdatabasesを指定し、AggregationPipelineを使うためのPipe関数をqueryを引数にして呼び出す pipe := db.C("databases").Pipe(query) pipe.All(&AllOriginalCollections) // APの設定を踏まえたクエリー(All:全取得)の実行 fmt.Println(AllOriginalCollections) // 結果表示
構造体であらかじめ$lookup
のasで指定するフィールドを設定しておかないといけない点と、AggregationPipelineを利用するためにPipe関数を使っている点以外は基本上で説明した通りです。
たぶん以下のような結果が返ってくると思います。
[ { ObjectIdHex("356361616530373965313338323333356630346335383436") sampleOriginalCollection [ { ObjectIdHex("5caae1cce1382337367b5316") sampleSubCollection 5caae079e1382335f04c5846 } ] } ]
ただ、この時ObjectIdHexがなぜかよくわからない数字の文字列になります。 原因はわかりませんが、以下のように$projectに結合用にObjectIdをstringに変換したidのカラムを作って、もともとのObjectIdのカラムはそのまま出力すれば、問題は解決されます。
// queryの部分だけ query := []bson.M{ { "$project": bson.M{ "_id": 1, "objectIdToString": bson.M{"$toString": "$_id"} }, }, { "$lookup": bson.M{ "from": "SubCollections", "localField": "objectIdToString", "foreignField": "original_collection_id", "as": "original_collections", }, }, }
objectIdHexが変形せずに出力されるようになります。
[ { ObjectIdHex("5caae079e1382335f04c5846") sampleOriginalCollection [ { ObjectIdHex("5caae1cce1382337367b5316") sampleSubCollection 5caae079e1382335f04c5846 } ] } ]
React HooksでReducerを使ったグローバルステイト管理
Reduxを使わずに、useReducerなどの機能だけを用いてReactでグローバルに値を管理します。 コーディングはTypeScriptでやります。
reactのプロジェクト作成
TypeScriptでReactのプロジェクトを作るなら、
create-react-app my-app --typescript
こいつを一発ぶちかましてやればいいです。
create-react-appをグローバルでインストールしてない場合は、
npx create-react-app my-app --typescript
でプロジェクトを作ります。
必要なものは全部入っていますが、私はUIにmaterial-uiを使うので、
$ yarn add @material-ui/core @material-ui/icons @material-ui/styles @types/material-ui
をしてあります、一応。 windowsでやりました。
グローバルに値を管理する
ここではグローバル値をグローバルステイトと呼んでみます。
まず、srcディレクトリ内に、reducer.tsを作成します。
// reducer.ts export const initialState = { // グローバルステイトの初期値 currentUser: { age: 99, name: "DefaultName", occupation: "DefaultOccupation", spouse: false } } export type State = typeof initialState // グローバルステイトの型定義 export interface IAction { // グローバルステイトの更新を行わせる指示の型定義 type: 'setUser', payload: { name: string, age: number, occupation: string, spouse: boolean } } export const reducer = (state: State, action: IAction) => { switch (action.type) { case 'setUser': return { // グローバルステイトの更新を行わせる指示のtypeが'setUser'の時の処理 ...state, currentUser: action.payload } default: return state } }
ここには指示のtypeがsetUserしかありませんが、一応switch文で書いておきます。 TSなのでreducerの引数にも型を設定してあげてください。
// Provider.tsx import * as React from 'react' import { IAction, initialState, reducer, State } from './reducer' const { createContext, useContext, useReducer } = React // グローバルステイトの初期値を引数として取り、state用のcontextを生成 const stateContext = createContext(initialState) // IAction型の引数を取る空の関数を初期値とし、dispatch用のcontextを生成 const dispatchContext = createContext((() => true) as React.Dispatch<IAction>) export const Provider:React.FunctionComponent = props => { const [state, dispatch] = useReducer(reducer, initialState) return ( <dispatchContext.Provider value={dispatch}> {/* dispatch用contextにdispatchを設置 */} <stateContext.Provider value={state}> {/* state用contextにstateを設置 */} {props.children} </stateContext.Provider> </dispatchContext.Provider> ) } // dispatch関数を利用できるようにする export const useDispatch = () => { return useContext(dispatchContext) } // グローバルステイトを利用できるようにする export const useGlobalState = <K extends keyof State>(property: K) => { const state = useContext(stateContext) return state[property] }
流れとしては、 1. Contextの生成 2. Contextをコンポーネントとしてまとめ、Providerとしてexportする 3. useDispatch関数からdispatch関数を使えるようにし、useGlobalState関数からグローバルステイトを使えるようにする
最後から4行目の<K extends keyof State>
では、
keyofでState(reducer.tsで定義したtype State)の要素のプロパティをunion型(共用体型)に変換し、
それをKが継承することになります(ここで書いたコードにはそもそもプロパティがcurrentUserしかないですが)。
useGlobalState関数はこのKの型の引数propertyを取ります。
union型では何個プロパティがあってもそのうちのいずれかに該当する引数を取ればいいので、propertyは上記のコードではcurrentUserでなければなりません。 したがって、その下ではuseContextで取ってきたグローバルステイトであるstateのcurrentUser(state[property]の部分)を取得しているわけです。
*この辺は自分で書いてて(参考ページを写経してて)、何やってんだこれ?となったので備忘録としても書いておきました。 React Hooks Tutorial on pure useReducer + useContext for global state like Redux and comparison with react-hooks-global-state
はい、ここまでくれば、あとはProvider.tsxをimportして、App.tsxに組み込みます。
// App.tsx // 上部省略 import { Provider } from './Provider' import User from './User' // グローバルステイトとやり取りするコンポーネントをUser.tsxとして作りました import logo from './logo.svg' const App = () => { return ( <Provider> <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> <User/> </div> </Provider> ) }
ProviderはすべてのDOMを包み込むように設置します。
以下のファイルでは、グローバルステイトとやり取りを行っています。
// User.tsx // 上部省略 // material-uiのコンポーネントのimport省略 // material-uiのスタイル設定省略 import { useDispatch, useGlobalState } from './Provider'; interface IUser { name: string, // ユーザー名 age: number, // 年齢 occupation: string, // 職業 spouse: boolean // 配偶者 } const User = () => { // style設定を定数classesとして扱う(上では省略しましたが、ここでスタイル設定を扱う関数を実行しています) const classes = useStyles() // グローバルステイトのプロパティとしてcurrentUserを指定してglobalState定数に入れる const globalState = useGlobalState('currentUser'); // userStateの定義とその初期値の設定、およびステイトの更新関数の定義 const [ userState, setUserState ] = React.useState<IUser>({ age: 0, name: "", occupation: "", spouse: false }) // input変更検知 function changeName(e: any) { setUserState({ ...userState, name: e.target.value }) } function changeAge(e: any) { setUserState({ ...userState, age: e.target.value }) } function changeOccupation(e: any) { setUserState({ ...userState, occupation: e.target.value }) } // 配偶者項目のみボタンでのswitch選択とする function HasSpouse() { setUserState({ ...userState, spouse: true }) } function HasNoSpouse() { setUserState({ ...userState, spouse: false }) } // ボタンを押されたときにreducerへdispatchしてグローバルステイトを更新 const dispatch = useDispatch(); function updateValue() { // グローバルステイトの更新を行わせる指示をdispatchの引数とする dispatch({ payload: { age: userState.age, name: userState.name, occupation: userState.occupation, spouse: userState.spouse }, type: 'setUser' }) } return ( <div> <Card className={classes.formCard}> <h3>RegisterUserAsGlobalState</h3> <TextField label="Name" type="text" value={userState.name} onChange={changeName}/> <TextField label="Age" type="text" value={userState.age} onChange={changeAge}/> <TextField label="Occupation" type="text" value={userState.occupation} onChange={changeOccupation}/> <div> <p>Spouse: {userState.spouse ? "Yes, I have" : "No, I do not"}</p> <Button className={classes.button} onClick={HasSpouse} variant="contained" color="secondary"> I have a spouse </Button> <Button className={classes.button} onClick={HasNoSpouse} variant="contained" color="secondary"> I do not have a spouse </Button> </div> <Button className={classes.button} type="button" onClick={updateValue} variant="contained" color="primary"> Update </Button> </Card> <Card className={classes.detailCard}> <h3>UserDetail</h3> <p>UserName: {globalState.name}</p> <p>UserAge: {globalState.age}</p> <p>UserOccupation: {globalState.occupation}</p> <p>UserSpouse: {globalState.spouse ? "Yes, I have" : "No, I do not"}</p> </Card> </div> ); }; export default User;
ここでyarn startすると、 私の環境では、以下のように表示されました。
NameとAgeとOccupationを入力して、I have a spouseボタンを押し、最後にUPDATEボタンを押しますと、
無事、光属性星8のドラゴン族、ブルーアイズホワイトドラゴンが登録されましたね。
次はサイバーエンドドラゴンとかも登録してみてください。
余談のmaterial-ui
私のmaterial-uiの設定はこんな感じになっていました。
// User.tsx // import系 import blueGrey from '@material-ui/core/colors/blueGrey' import Button from '@material-ui/core/Button' import Card from '@material-ui/core/Card' import TextField from '@material-ui/core/TextField' import { makeStyles } from '@material-ui/styles' // 具体的な設定系 const useStyles = makeStyles((theme: any) => ({ formCard: { width: "320px", padding: "20px", backgroundColor: blueGrey[900], left: "41%", position: "absolute" }, detailCard: { width: "320px", padding: "20px", backgroundColor: blueGrey[900], top: "60%", left: "41%", position: "absolute", textAlign: "left" }, button: { width: "80%" } }))
これをconst classes = useStyles()
で使っていく感じです。
material-uiの余談でした。
Reactで無理やり関数コンポーネントの親子間のprops受け渡しをしてみた
Reactで親コンポーネントから子コンポーネントへ値を渡すのは以下のようすれば十分だったが、子コンポーネントから親コンポーネントに値を渡す方法がわからなかったので調べてみた。
import React from 'react'; const App = () => { const greeting = 'Hello Function Component!'; // 子コンポーネントにvalueをセット return <Headline value={greeting} />; } // valueをFunctionComponentの引数として使用する設定 const Headline:React.FunctionComponent<{value: any}> = ({ value }) => { // valueを表示 return <h1>{value}</h1>; } export default App;
子コンポーネントから親コンポーネントに値を渡す方法とはつまり、vueでいうこんな感じのテクニック。 API - Vue.js vm.$emit( eventName, […args] )
// 子コンポーネントからemit // 第一引数が子コンポーネントに設定した要素名(親のテンプレートに@をつけて設置する)で、第二引数がコールバックの引数になる this.$emit('method', "this is argument") // 親コンポーネントで受け取り、コールバック関数を実行 <template> <div> <ChildComponent @method="callback"/> </div> </template> <script> // 省略 callback(argument) { console.log(argument) // this is argument } // 省略 </script>
結論から言って、Reactには子コンポーネントから親コンポーネントへ値を渡す方法はないらしい。 では、子から親へ値を渡して、子の変更時に親の方にも連動する変更を行いたい場合にはどうすれば良いのか......?
A. 親コンポーネントの関数を子コンポーネントへ渡し、子コンポーネントでその親の関数を実行する。
propsでやりとりするのではなく、関数を実行してpropsが必要な状況自体が必要ないようにするって考えておけばいいのだろうか。 それでもpropsが必要なんだが。。。みたいな状況になったら、つまりそれはReactでの開発思考的に間違った設計になっているだけの話なのかもしれない:thinking:
やっていることは親コンポーネントが子コンポーネントにpropsを渡しているのと変わりないが、渡しているものが親コンポーネントと繋がっている(実際に実行して確認してないので、コピペするだけではエラーが出るかもしれません)。
// parent const App = () => { const [flag, setFlag] = React.useState(false) function switchFlag() { setState(true) // flag = true } // 子コンポーネントにswitchFlag関数をセット return ( <div> <ChildComponent switchFlag={switchFlag}/> </div> ) } // child // switchFlag関数をFunctionComponentの引数として使用する設定 const ChildComponent:React.FunctionComponent<{switchFlag: any}> = ({switchFlag}) => { // switchFlagをボタンとして設置 return ( <div> <button onClick={switchFlag}>Click</button> </div> ) }
この状態でボタンを押すと、親コンポーネント側の関数が実行され、親コンポーネントのstateであるflagがtrueに変わる。 子コンポーネントでボタンが押されたときになんらかの処理を行ってから、そのあとにswitchFlag()で親コンポーネントの関数を実行するのものオッケー(下みたいな感じで):ok_hand_tone2:
// child const ChildComponent:React.FunctionComponent<{switchFlag: any}> = ({switchFlag}) => { function onClickHandler() { // なんらかの処理 switchFlag() // 親コンポーネントのswitchFlag関数の実行 } return ( <div> <button onClick={onClickHandler}>Click</button> </div> ) }
ここのページが参考になりました。:point_down_tone2::point_down_tone2::point_down_tone2: How to pass Props from child to parent Component? React Function Component: Callback Function
GoでGraphQLのMutationを実装する
このエントリーではGolang + MongoDBでGraphQLを使ってみるに書きつぐ形で、GraphQLのMutationについての説明を行います。
一度改めて、まとめを兼ねたGraphQLの説明をします。
GraphQLとは
GraphQLでは、これまでRESTfulで作ってきたように複数のエンドポイントを作成して通信を行う方法を行いません。 エンドポイントは最低一つだけで動くようになるのがGraphQLです。
基本的にはGraphQLの利点は、
・エンドポイント一つだけで良い ・型安全にデータベースとやりとりができる
の二点を心に留めておけば問題ないかと思います(補足があれば喜んでいただきます)。
さらっとした説明ですが、ここからは実装に関するお話。 と言ってもコードをつらつら書くわけではなく、概念的なものを考えていきます。
GraphQLの実装の考え方
まず、GraphQLの実装の流れは以下のような雰囲気で感じ取ってもらえればと思います(この流れはGolangに限らず一般的なものだと思いますたぶん)。
- GraphQLで扱うTypeの定義(GraphQLが扱うデータの設定)
- Fieldをデータの取得や作成の用途に合わせて定義(ここでTypeを利用する)
- Fieldをまとめてスキーマを作る
- GraphQLのリクエストを作成(GraphQLがデータベースに問い合わせる要求)
- スキーマとリクエストを合わせてGraphQLを実行
- 指定したデータが戻ってくる
Golang + MongoDBでGraphQLを使ってみるにも書きましたが、このリクエストには二つの概念が含まれており、その一つがこのエントリーのタイトルとなっているMutation(以下ミューテーション)なのです。 *リクエストは公式で使われている言葉ではありませんが、この方が伝わりやすいかなと思ったのでこの表現を使っています。 *流れの4に「データベースと問い合わせる要求」と書いていますが、GraphQLが直接的にデータベースとやりとりすることはありません(実装の仕方によってはデータベースと直接やりとりするかもしれませんが。。。)。GraphQLは飽くまで、データベースから取り出されたデータ群に対して実行されます。例えば、特定のUserを取得するGraphQLのクエリーを実施すると、それはすでにデータベースから取得されたUserテーブルの全てのレコード(ここでは取り出されてから構造体の配列に変換しています)に対して行われます。
リクエストにはクエリーとミューテーションが含まれます。
クエリー(いままでのGETのようなもの)・・・データの取得要求 ミューテーション(いままでのPOSTのようなもの)・・・データの更新要求
前回の記事ではクエリーを利用したデータの取得に関するGraphQLの使い方を解説しましたが、ここではミューテーションを扱って、データをInsertする方法を解説します。 (あとあとUpdateやDeleteに関しても書きつぐだろうと思います)
Golang + GraphQLでMutationを実装する
データベースはNoSQLのMongoDBを使います。
前回の記事では、GraphQLのスキーマの定義箇所のコードが以下のようになっていました。
rootQuery := graphql.ObjectConfig{ Name: "RootQuery", Fields: fields.UsersField, } schemaConfig := graphql.SchemaConfig{ Query: graphql.NewObject(rootQuery), // Query } schema, err := graphql.NewSchema(schemaConfig) if err != nil { log.Fatalf("failed to create new schema, error: %v", err) }
これがミューテーションを使う場合には以下のようになります。
rootQuery := graphql.ObjectConfig{ Name: "RootQuery", Fields: fields.UsersField, } rootMutation := graphql.ObjectConfig{ Name: "RootMutation", Fields: fields.CreateUserField, } schemaConfig := graphql.SchemaConfig{ Query: graphql.NewObject(rootQuery), // Query Mutation: graphql.NewObject(rootMutation), // Mutation } schema, err := graphql.NewSchema(schemaConfig) if err != nil { log.Fatalf("failed to create new schema, error: %v", err) }
rootMutation変数が作成され、SchemaConfigのMutationにこれを新しいGraphQLのオブジェクトとして追加しています。 *前回の記事では同じファイルにfieldsを定義していましたが、今回はファイルを分けているので、fields.UsersFieldやfields.CreateUserFieldのように書いています。
それでは、rootMutationの中身を説明していきます。 それほど複雑ではないので、楽に感じるかもしれません。
rootMutationはfields.CreateUserField(ここではfields/user.goにあるCreateUserField変数)をFieldsとして設定しています。
// fields/user.go var CreateUserField = graphql.Fields{ "createUser": &graphql.Field{ Type: types.UserType, Description: "Create new user", Args: graphql.FieldConfigArgument{ "name": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "email": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "password": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { name, _ := params.Args["name"].(string) email, _ := params.Args["email"].(string) password, _ := params.Args["password"].(string) database.CreateNewUser(name, email, password) return nil, nil }, }, }
クエリーで特定のユーザーを取得するときは、冒頭が"user"で始まっていましたが、ここでは"createUser"となっています。 ここの名前はあとでリクエストを送るときに利用します。
Argsについてもクエリーの時と同じように理解していただければ良いです。リクエストを出すときに指定できる要素です。 コードでは、name, email, passwordとなっているので、リクエストを出すときにはこれらの要素を設定できるということです。POSTのリクエストとしては普通のことですね。
最後から六行目のdatabase.CreateNewUser(name, email, password)
は、データベースとやりとりする箇所です。
ここではファイルを外出ししているので、外にあるdatabaseディレクトリのuser.goファイルにデータベースとのやりとりをしているコードがあるということです。
database/user.goは以下の通りです。
// database/user.go func CreateNewUser(name string, email string, password string) { // connection to mongoDB session, _ := mgo.Dial("mongodb://localhost/test") defer session.Close() db := session.DB("test") // insert newUser := &User{ Id: bson.NewObjectId(), Name: name, Email: email, Password: password, } col := db.C("users") if err := col.Insert(newUser); err != nil { log.Fatalln(err) } }
ここはMongoDBでのInsert方法を理解していれば問題なく読み取れると思います。 func (*Collection) Insert
はい、これでミューテーションの処理は終わりです。 ミューテーションの中の動きは以上となります:open_hands_tone2:
実装されたMutationを利用する
ミューテーションの機能を利用するには、それを実行するためのリクエストを送る必要があります。
クエリーのリクエストは以下のようなものでした。
request := ` { user(id: "5c94f4d7e803694b2d09da75") { id name email password } } `
これをミューテーションのリクエストに書き換えると、
request := ` mutation { createUser(name: "name", email: "email", password: "pass") { name email password } } `
fields/user.goのcreateUserFieldは"createUser"という名前で定義したので、ここでcreateUserでミューテーションを書いています。 そしてその引数で作成する新しいデータの要素を指定します。
それらの要素はcreateUserFieldのArgsのそれぞれに対応しており、それらがResolveの関数に渡ってInsertが実施されるわけです。 案外簡単な話だと思います。
mongo
でデータベースに入ってdb.users.find()とかで中身を覗くと、作成したドキュメントが追加されていると思います。
入ってなかったらちょっとググってみてください笑
まあ、GraphQLでは動き方がわかればだいたいの実装の方法がわかってくるようになると思います。 それと、実際にコードを書いて動かしてみることも大切ですね:sunglasses:
間違った表現や説明をしている箇所があれば、忌憚なくご指摘をお願いいたします。