Apollo ServerからFireStoreのデータを取得する
概要
この記事は前回の記事の続きになります。
前回はCloud Functions for Firebaseを使ってApollo Serverを構築しました。今回は、FireStoreのデータをGraphQLから取得するところまでやります。
サンプルは以下のリポジトリにまとめてます。
データの準備
サンプルデータはインターネットムービーデータベースを使用します。 GraphQL Schemaは以下のように定義しました。
type Movie { title: String year: Int info: MovieInfo } type MovieInfo { directors: [String] release_date: String rating: Float genres: [String] image_url: String plot: String rank: Int running_time_secs: Int actors: [String] }
Movie型がタイトルと公開年と MovieInfo型を持っています。MovieInfoには詳細な映画情報が入力されています。 上記サンプルデータをFireStoreに追加します。とりあえず追加したかったので愚直にFor文を回す実装をしました。
import * as admin from "firebase-admin"; import MovieData from "./moviedata.json"; admin.initializeApp(); const db = admin.firestore(); MovieData.forEach(async movie => { await db.collection("movies").add(movie); });
Firebaseのコンソールからデータを確認できれば完了です。
クエリスキーマ
データが準備できたので、SchemaのクエリとFireStreを接続してみます。今回、Schemaのクエリは以下の二つを実装します。
type MovieListPayload { movies: [Movie] lastKey: String } type Query { popularMovies: [Movie] movieList(lastKey: String): MovieListPayload }
popularMovies
クエリは映画の順位から上位10個のデータを返すクエリです。movieList
は映画一覧を10件ずつ返し、次の10件を取得するためのキー( lastKey
)を一緒に返します。
それぞれリゾルバーを実装してみます。
映画上位ランキング一覧
ランキング順に並べ替えたいので FireStore の orderBy
を info.rank
フィールドに対して実行します。その後 limit
をかければ上位10位以内の映画一覧を取得できます。
Apollo ServerのリゾルバーはSchemaのフィールド名とTypeScript内のオブジェクトキー名が一致している必要があります。(今回は popularMovies
)
リゾルバーを実装すると以下になります。
const resolvers = { Query: { popularMovies: async () => { const snapshot = await db .collection("movies") .orderBy("info.rank") .limit(10) .get(); return snapshot.docs.map(doc => { return doc.data(); }); } } }
デプロイしてクエリを実行すると以下のようになりました。
ランキング順に取得できています。(1位が欠損しているのが気になりますが。)
映画一覧
映画一覧はソートするキーを映画公開日にします。一回のクエリでは10件固定で取得でき、簡易的なページネーションを実装して、次の10件を取得できるようにします。 (ここでは任意のページや任意の映画数をクライアントから指定できる実装はしません。)
クエリ引数として lastKey
を受け取れますが、nullableのためFireStoreへのクエリに条件分岐を追加します。
オブジェクトへの変換処理は映画上位ランキング一覧と同じです。
movieList: async (_: any, args: MovieListInput) => { let snapshot; const query = db .collection("movies") .orderBy("info.release_date", "desc"); if (args.lastKey) { snapshot = await query .startAfter(args.lastKey) .limit(10) .get(); } else { snapshot = await query.limit(10).get(); } const movies = snapshot.docs.map(doc => { return doc.data(); }); }
レスポンスにも lastKey
を含める必要があります。レスポンスの場合は取得した映画一覧が空かどうかで lastKey
をnullにするかを決めます。
let lastKey: string | null = null; if (movies.length) { lastKey = movies[movies.length - 1].info.release_date; } return { movies, lastKey };
デプロイして動作確認してみます。クエリ引数を空にして実行すると以下のようになりました。
映画一覧を公開順に取得できています。取得できた lastKey
を基に再実行してみます。
1回目とは異なるデータを取得できました。このままだと映画の公開日が一緒の場合、データが欠損してしまうので今後修正していきたいと思います。
まとめ
Cloud Functions for Firebaseを使ってApollo Serverを構築する
はじめに
GraphQLのバックエンド実装は以下の方法があります。
Apollo ServerはNode.jsによるGraphQLサーバーを構築するOSSです。 クライアントだけではなくバックエンドも複数のデータソースから選択して実装することができます。 今回はCloud Functons for Firebaseを使ってApollo Serverを実装してみました。
Firebase初期設定
FirebaseのプロジェクトにCloud Functionsを追加します。言語はTypeScriptを選択しました。
$ firebase init functionsd === Functions Setup A functions directory will be created in your project with a Node.js package pre-configured. Functions can be deployed with firebase deploy. ? What language would you like to use to write Cloud Functions? TypeScript ? Do you want to use TSLint to catch probable bugs and enforce style? No ✔ Wrote functions/package.json ✔ Wrote functions/tsconfig.json ✔ Wrote functions/src/index.ts ✔ Wrote functions/.gitignore ? Do you want to install dependencies with npm now? Yes
TypeScript初期設定
TypeScriptの設定を行います。Cloud Functions for Firebaseを使用するため、 functions/
ディレクトリ内で作業します。
$ cd functins/ $ npm i -D @types/graphql @types/node typescript
tsconfig.json
は以下のように編集しました。
- JavaScriptファイルを
build
ディレクトリに出力 - Node.js のCommonJS方式を使用するため、
"module": "commonjs"
に変更
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "strict": true, "importHelpers": true, "moduleResolution": "node", "sourceMap": true, "resolveJsonModule": true, "baseUrl": ".", "types": [ "node" ], "paths": { "@/*": [ "src/*" ] }, "outDir": "build" }, "include": [ "src" ], "exclude": [ "node_modules" ] }
Apollo Server初期設定
Cloud Functions向けのプラグインである apollo-server-cloud-functions
をインストールします。
$ npm i apollo-server-cloud-functions graphql $ npm i -D @types/graphql
サンプルとして src/index.ts
にApollo Serverのサンプルと同じスキーマを実装します。プラグインはGCPのCloud Functionsのため、インスタンス化した ApolloServer
を functions.https.onRequest
関数の引数に代入しています。
import { ApolloServer, gql } from "apollo-server-cloud-functions"; import * as functions from 'firebase-functions'; const typeDefs = gql` # Comments in GraphQL strings (such as this one) start with the hash (#) symbol. type Book { title: String author: String } type Query { books: [Book] } `; const books = [ { title: 'Harry Potter and the Chamber of Secrets', author: 'J.K. Rowling', }, { title: 'Jurassic Park', author: 'Michael Crichton', }, ]; const resolvers = { Query: { books: () => books, }, }; const server = new ApolloServer({ typeDefs, resolvers, context: ({ req, res }) => ({ headers: req.headers, req, res }) }); exports.graphql = functions.https.onRequest(server.createHandler());
ローカルで動かす準備ができたので、Firebase emulatorsを起動してみます。TypeScriptがトランスパイルされていないので、 functions/
配下でNPMコマンドを実行してます。
$ npm run serve ✔ functions: Emulator started at http://localhost:5000 i functions: Watching "/Users/numatakeisuke/working/poc/apollo-google-cloud-functions/functions" for Cloud Functions... ✔ functions[graphql]: http function initialized (http://localhost:5000/apollo-server-sample/us-central1/graphql).
表示されたURL(http://localhost:5000/apollo-server-sample/us-central1/graphql)をブラウザで開くとGraphQLクライアントが表示されます。 Booksからタイトルを取得すると画像のようになりました。
まとめ
Cloud Functions for Firebaseをバックエンドに選択してApollo Serverを実装してみました。 Cloud Functions のデプロイが簡単なので、予想しいていたより早く実装できました。
今後はFireStoreのデータを返す仕組みを作ってみます。
参考
JavaScriptでURLエンコード
URLクエリ内に別URLをもたせたい時にURLをエンコードする方法についてまとめます。 今回はJavaScriptで実装しました。
encodeURI
はじめ、JavaScriptのencodeURIを試してみました。 本ブログのURLを引数に実行してみます。
> encodeURI('https://keinumata.hatenablog.com/') 'https://keinumata.hatenablog.com/'
結果は引数と何も変わらないです。これはencodeURI関数がURLを構成する予約語(;,/?:@&=+$)およびエンコードせず使用できる非予約語(-_.!~*'())はエンコードしないためです。 試しに空白を入れて実行してみます。
> encodeURI('https://keinumata.hatenablog.com/? a') 'https://keinumata.hatenablog.com/?%20a'
日本語もエンコードされます。
> encodeURI('https://keinumata.hatenablog.com/ジャヴァスクリプト') 'https://keinumata.hatenablog.com/%E3%82%B8%E3%83%A3%E3%83%B4%E3%82%A1%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88'
今回はURLのクエリに使用するため、予約語もエンコードしたいので他の関数を探してみます。
encodeURIComponent
encodeURIComponentはencodeURIと比較してURLを構成する予約語(;,/?:@&=+$)もエンコードします。エンコードせず使用できる非予約語(-_.!~*'())はエンコードしません。 試しにencodeURIで試したURLを実行してみます。
> encodeURIComponent('https://keinumata.hatenablog.com/') 'https%3A%2F%2Fkeinumata.hatenablog.com%2F'
スラッシュやコロンもエンコードできました。
結論
- URLクエリにURLを使用する場合は
encodeURI
ではなくencodeURIComponent
を使う
参考
Vue Composition APIとTypeScriptの組み合わせ
この記事はギルドワークスAdvent Calendarの3日目の記事です。 Vue.jsのバージョン3にてリリース予定のComposition APIとTypeScriptを組み合わせについて紹介します。
Composition APIとは
Composition APIは関数ベースでコンポーネントを実装する機能です。関数ベースのため、機能の再利用性を高める役割を持っています。他のライブラリではReact hooksやSvelteに近い機能です。
公式のRFCに記載されているモチベーションの1つにTypeScriptとの相性を高めることが書かれています。本記事ではサンプルコードを踏まえてなぜTypeScriptと相性がよくなるのか考えてみます。
https://vue-composition-api-rfc.netlify.com/#better-type-inference
準備
Vue2系で試す場合はプラグインが提供されているので、インストールする。
https://github.com/vuejs/composition-api
$ npm install @vue/composition-api --save or $ yarn add @vue/composition-api
インストールが完了したらVue.use
を追加します。
import Vue from 'vue'; import VueCompositionApi from '@vue/composition-api'; Vue.use(VueCompositionApi);
TypeScriptと併せて使用する、かつVS CodeでVeturを使用している場合、設定を以下のように変更する必要があります。
{ "vetur.useWorkspaceDependencies": true }
Options APIとComposition APIの比較
TypeScriptとの併用がやりやすくなるかを見るために、基本的なVue.jsのAPIであるOptions APIと比較してみたいと思います。 今回は両方のAPIを使って同じ機能を持ったモーダルコンポーネントを実装してみます。 機能としては以下になります。
- モーダルの表示・非表示ができる
- モーダル内に日付を表示し、ボタンによってカウントアップできる
- 日付が条件に達したらメッセージを表示
以下のGitHubにても公開しています。
https://github.com/keinuma/compare-options-and-composition
テンプレート
テンプレートおよびスタイルは共通のコードを使用します。(スタイルのコードは今回の記事では紹介しません。)
<template> <div> <div :class="{ 'modal-overlay': showModal }"> <div v-if="showModal" class="modal-isshow modal-open" > <div class="modal-container"> <button class="close-btn btn btn-outline-info" @click="closeModal"> 閉じる </button> <slot></slot> <div> <div v-show="isLastDate" class="alert alert-danger" role="alert"> <p class="alert-last-date"> クリスマスになりました。<br /> アドベントカレンダーの最終日です。 </p> </div> <p>この記事はギルドワークスアドベントカレンダーの{{ date }}日目の記事です。</p> <button type="button" class="btn btn-info" @click="countUpDate" :disabled="isLastDate"> 次の日 </button> </div> </div> </div> </div> </div> </template>
Options API
Options APIの場合は以下になります。
TypeScript対応は Vue.extend
を使用しています。
import Vue from 'vue' export default Vue.extend({ name: 'ModalOptions', props: { showModal: { type: Boolean, required: true, default: false } }, data() { return { date: 1 } }, computed: { isLastDate() { return this.date === 25 } }, methods: { countUpDate() { if (this.isLastDate) { return } this.date++ }, closeModal(e: Event) { e.stopPropagation() this.date = 1 this.$emit("onCloseModal") } } })
Composition API
Composition APIの場合は以下になります。
基本的に setup
関数の中でデータやメソッドの定義を行います。
リアクティブなデータはComposition APIで定義されている ref
や reactive
関数から定義できます。
モーダルを閉じるメソッドで使用しているように、Vue
インスタンスへは context
を使用してアクセスできます。
import { createComponent, ref, computed } from '@vue/composition-api' type Props = { showModal: boolean } export default createComponent({ props: { showModal: { type: Boolean, required: true, default: false } }, setup(props: Props, context) { const date = ref(1) const isLastDate = computed(() => { return date.value === 25 }) const countUpDate = () => { if (isLastDate.value) { return } date.value++ } const closeModal = (e: Event) => { e.stopPropagation() date.value = 1 context.emit("onCloseModal") } return { showModal: props.showModal, date, isLastDate, countUpDate, closeModal } } })
考察
2つのコードを比較すると、Composition APIはOptions APIに比べて this
がなくなっています。
これまで Vue
の this
にはデータやメソッドのような開発者が定義したアプリケーション的意味合いと、filter
やVue RouterなどのVue
のインスタンス自身の2つの意味を持っていました。Composition APIの場合、前者を setup
関数内で定義、後者を context
に持たせることで this
の責務を分離しています。
責務を分離することで型定義を推論を効かせしやすくしていると考えられます。
また、 Options APIの methods
でも同じことはできるのですが、相互参照が見えやすい setup
関数だからこそ機能としての再利用性を意識しやすくなります。そのため、外部モジュールへの抽出や構造化が促進されると考えられます。
まとめ
- Composition APIによってTypeScriptが使いやすくなる
- Vue 3待ち遠しい
DevLOVE X参加記録
先日DevLOVE Xというイベントに参加してきた。とても豪華なスピーカー陣が5トラック並行にセッションするため、何を聞きにいくか一番悩んだイベントだったと思う。
色々な分野の有力者の方々から深い話をたくさん聞けて面白く、多くの学びがあった。
特に印象に残った講演をふりかえっていく。(以下敬称略
エンジニア、エンジニアリングマネージャーとして成長するために必要なこととは? by 安西 剛
今回のイベントで一番考えさせられたセンションだった。 最近の開発ではチームに着眼したふりかえりを実施するものの、個人に焦点を当てたふりかえりはなかなか広まっていない。 しかし、エンジニア個人の成長を考えると一人のふりかえりがより重要ではないかというお話だった。 私自身一人のふりかえりを実施していたが、以下の新しい観点を得られた。
私は自分の嫌な一面やみたくないところと向き合いたくないから感情を言語化することを避けてきていたように感じる。 セッションを聞いて、感情と向き合うことで自分のモチベーションを知ることができ、成長につながることがわかった。 自分のことは十分知っているだろうという認知バイアスにかかっていたことを自覚させられた瞬間だった。
個人のふりかえりを共有することは気恥ずかしさがあり一人ではやるという発想にすらいたっていなかった。 しかし、自分が見えない観点をフィードバックしてもらえることは重要だと気付かされた。
ふりかえりの結果、学んだことは習慣化しないと意味がなくなる。 無意識におこなわれる習慣にうまく組み込めるようにルーチンを作っていきたい。
それはYAGNIか? それとも思考停止か? by 川島 義隆
www.slideshare.net
最近私はエンジニアとしての働き方、役割が変わり、言われた物を作るよりも価値のあるものを作ることにコミットするようになってきている。
そんな中で YAGNI
を間違って認識し、作り始めるスピードを正としてきはじめた自分にはいい教訓になった。
考えることを安易に止めず、本当はもっといい設計があるのではないか、考えることに時間を使う方が投資対効果が高いこともある。
なので、どこまでが本当にユーザーに問わないとわからない領域でどこまでは思考することで最善を尽くせるのかバランスを意識しないといけないと解釈した。
具体的な設計では状態や区分を実装するときにインターフェースを切る考えを学んだ。 ホットスポットを見つけるツールも使っていきたい。
自分に向いている楽しめる仕事をするための目標設定 -失敗のストーリーに分析と対策を添えて- by 川鯉 光起
過去の経験から自分を知り、学びを抽象化する具体的な方法を知ることができた。 エンジニアの自分にとっては「エンジニアリングが好き」で考えを止めず、さらに目的を深掘りしていくことが学びになった。 自分なりに解釈すると、以下のようなる。
- 経験してきた中で印象に残るストーリーを切り出す
- ストーリーから事実(仕事)と感情を洗い出す
- 並べた事実と感情がなぜ起きた/感じたかを問い直す
- 問いに答えてさらに問い直すことを重ねて学びに抽象化する
これらから自分は作ることよりも価値を届けること、使ってもらえることにモチベーションを感じることが改めてわかった。 他にも紹介されていたキャリアアンカーやストレングスファインダーをやってみたいと思う。
アウトカムを出す! 楽しむ! 両方やるために経験を味方にする! by 高橋 陽太郎
アウトカムは重要であると同時にエンジニアの楽しさにも経験を通して繋がっていることを学んだ。
セッションを聞くまでプロダクト作りは成果にコミットできいないと意味がないけれども、全員のエンジニアの楽しさにつながるわけではないのではないかと思っていた。 エンジニアは技術領域と何を作るかを決める領域のそれぞれ比重を持っていたからだ。 センションを聞いたおかげで、足りないのは視座を高めたり詳細まで降りたりすることを共有することだと学んだ。
コーディングという観点だけを見ると限りなく綺麗にしていきたいし、最適化したくなる。 ただ、プロダクトとして何を目指していて、現在どういう状況なのかを共有し、理解することでチーム全員が同じ方向を向けるのではないかと思った。 なので、エンジニアに限らずチームメンバーがどの分野に興味があり、どこにギャップがあるか観察して思いを共有していきたい。
まとめ
今回のイベントからは一貫して学びを得ることの重要性を知れた。 学びは自分だけでなく、チームやプロダクトにも当てはめることができる。 これからは積極的に学びを得るためのフィードバックサイクルを早くしていきたい。
最後に、ここまで濃ゆい時間はもう過ごせないと感じるほど素敵な二日間を提供してくださった運営の方々に圧倒的感謝…
使いやすいプロダクトを目指して
使いにくいプロダクト
ある案件で開発したプロダクトがユーザーにとって使いにくいと評価されることがあった。
プロダクトの受け入れ条件を満たし、チームの中で認められたプロダクトであってもユーザーにとって価値が高いわけではない。
頭でわかっていたつもりであってもはじめて経験したときに少なからず戸惑いがあった。
どうすればユーザーにとって使いやすいプロダクトを開発できるのか試行錯誤している考えをまとめてみる。
エンジニアとしての開発
多くの開発では素早く正確に実装することが求められている。
また、何を作れば正解かわからないプロダクト開発では変更容易性も兼ね備えなければならない。
そのため私は以下のことを考えながらプロダクトを開発していた。
- 設計指針から外れていないか
- 責任を明確に分離できているか
- ミスをしていないか
- 可読性の高いコードになっているか
この時点で意識しないといけないことは多い…
ふりかえるとエンジニアとして開発するときはミクロな目線で糸を縫うような作業をしているようだなと感じた。
このうえでユーザビリティを高めるためにはユーザー目線を得る必要があると思った。
ユーザー目線の獲得
ユーザーの視点を借りるためにはどうすれば良いか。一番はユーザーにプロダクトを体験してもらうことだと思う。
体験してもらっている様子を観察して、問いかけたり、プロダクトの感想を伝えてもらって方針を変えたり機能を追加していく。
しかし、ターゲットユーザーに実際にプロダクトを提供するためには、多くの人を巻き込みプロダクトを一定レベルまで完成しないといけないためコストが大きい。
プロダクトの方針一つ一つをユーザーに問いかける訳にはいかないので、一定レベルまでエンジニアがプロダクトを形作っていかなければならない。
一定レベルまで仕上げるためにはエンジニアがユーザー目線を持たないといけない。
エンジニアがユーザー目線を得るためにはユーザーに共感する必要がでてくる。
共感するためには、バックグラウンドや感じる課題、プロダクトを使用するときに気にかけることなどを知らないといけない。
これらからユーザー目線の獲得は一朝一夕でなくユーザーと対話を重ねることで作り手側にユーザー目線を宿していく深い経験ではないだろうか。
理想の姿とは感じつつも常にエンジニアがユーザーと対話できる状況ではない。対話なしでもエンジニア個人の基準でプロダクトを作り続けられるようになりたい。
違和感を感じる
私が考えたユーザー目線に近づくためのアプローチはプロダクトの違和感を拾うことだった。
違和感はプロダクトを初めて見たときに感じやすい。そのため開発を続けているとプロダクトの当たり前が自分の中に浸透してしまう。
今まで開発してきたプロダクトではリストの見せ方やボタン配置・配色、ページ遷移時のインタラクションなどを当たり前にしてしまっていたように思う。
この当たり前がチームの中に浸透してしまうと、ユーザーに見せるまで誰も疑問に思うことなくプロダクトを改善する機会を失ってしまうことになる。 なのでプロダクトを改善するためには意識的にユーザーになりきって違和感を感じることが大事なのではないかと考えた。
目線を意識的に切り替える
違和感を感じるためにユーザーになりきったまま細やかな開発を遂行するのは難易度が高い。私自身ミクロな目線とマクロな目線を同時に持ち合わせられるほど器用になれない。
なので、まずは実装に集中するとき(開発モード)とユーザーになりきるとき(ユーザーモード)の二つを意識している。
ユーザーとしてプロダクトを触って違和感を感じたらプロダクトバックログに追加する。追加したプロダクトバックログアイテムを元に使いやすさを求めて開発を進めていく。 目線切り替えを繰り返していくと個人の中でプロダクトの改善サイクルが回り始める。
改善を続けていても一人では限界がある。この限界は開発チーム全員が互いに目線を切り替えて、議論を重ねることで超えられるのではないだろうか。 壁を超え続けたプロダクトはきっとユーザーにとっての価値に近づくと信じている。
AppSyncのデプロイについてまとめてみた
はじめに
ある案件でAppSyncを使うことになったが、商用利用の事例が少なくデプロイ方法がわからなかった。
自動でデプロイできる仕組みを探したところ、Serverless FrameworkのAppSyncプラグインがあったので使ってみた。
AppSyncとは
AppSyncはAWSで利用できるGraphQLベースのマネージドサービスである。
特徴として
- バックエンドサービスを自由に切り替えることができる
- GraphQLベースのため、フロントエンドに必要なデータを定義できる
などがある。
特にバックエンドにはLambdaやDynamoDB、HTTPなど多様なリソースを選択できる。
前提
今回の案件では以下の条件があった。
- AppSyncをLambdaバックエンドとHTTPバックエンドの二種類を実装
- フロントエンドはアプリ(iOS, Android) とWebアプリ(JavaScript)
- 環境は開発環境・検証環境・本番環境の3つ
- CloudFormation経験は薄く、Sceptreを使ってECS(Fargate)を作成した経験のみ
デプロイ方法の選定
現状AppSyncを商用利用して、デプロイするためには以下の手段が考えられた。
- CloudFormationを書く
- Amplify CLIを使う
CloudFormationを使ってデプロイするためにはYaml地獄と戦い、トライアンドエラーを繰り返す必要がある。 時間的猶予がなかったため却下した。
Amplify CLIはコマンド一つでAWSリソースを作ることができる強者である。 しかし、以下の懸念点があった。
- 開発当初は
env
機能がなかったため、環境を複数設定できない - 複数のフロントエンドで共有することが難しい
そのため、リソースの作成にAmplify CLIを使用しなかった。
他に良いデプロイ方法はないか探していたら、以下の記事に遭遇し、ServerlessFrameworkのAppSyncプラグインを知った。
read.acloud.guru
Serverless Frameworkを使う予定があったので、試してみたら思いのほか手軽だった。
ここからは実際にデプロイした手順を記録していく。
Serverless FrameworkでAppSyncデプロイ
今回はAppSyncのバックエンドにLambdaを使用する。 GraphQL SchemaにはブログWebアプリケーションを想定に実装する。
サンプルはGitHubにもあげているので併せてみていただきたい。
CLIインストール
NPMでインストールする。
$ npm install -g serverless
プロジェクト初期化
プロジェクトを初期化する。
このタイミングでは package.json
は作成されていない。
$ serverless create --template aws-nodejs --path appsync-deploy $ cd appsync-deploy $ ls -a .gitignore handler.js serverless.yml
create
コマンドを実行するには template
オプションが必要である。
template
の種類は以下に詳細が記載されている。
成果物のうち serverless.yml
はCloudFormationのもとになるYaml定義。
handler.js
はAWS Lambdaを使用する場合の実行ファイルのため、AppSyncのデプロイだけでは不要である。
プラグインのインストール
ServerlessFrameworkのプラグインはNPMパッケージでまとめられているため、 package.json
でプラグインを管理していく。
Node環境がまだないので、改めてNPM初期化を実施する
$ git init $ npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (appsync-deploy) version: (1.0.0) description: entry point: (handler.js) test command: git repository: keywords: author: license: (ISC) About to write to appsync-deploy/package.json: { "name": "appsync-deploy", "version": "1.0.0", "description": "", "main": "handler.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
AppSyncプラグインをインストールする。
$ npm i serverless serverless-appsync-plugin
デプロイコマンドをNPMの scripts
に定義する。
"scripts": { "deploy": "serverless deploy -v" },
これでデプロイを実行する準備が整った。
serverless.yml
を整えながら、AppSyncの設定を加えていく。
Serverless Framework共通設定
はじめに基本設定を記述していく。
serverless.yml
を開くと例が記載されているが、今回必要な部分は少ないので消す。
service: appsync-deploy provider: name: aws region: ap-northeast-1 plugins: - serverless-appsync-plugin
service
はCloudFormationのStack Nameに使用される部分であり、
デプロイ環境の総称となる。今回はプロジェクトのディレクトリ名を使用する。
provider
はCloud Providers(AWS)およびリージョンの設定をする。
plugins
にはインストールしたAppSyncのプラグインを記述する。
AppSyncの基本設定
このサンプルではAppSyncの認証に Amazon Cognitoを選択する。
Schemaは簡易的にするために Query
と Mutation
を一つずつにした。
Schemaの詳細を以下に記載する。
schema { query: Query mutation: Mutation } type Query { listArticle: ArticlesResponse } type Mutation { createArticle(title: String, content: String): ArticlesResponse } type Article { id: Int title: String content: String } type ArticlesResponse { errorCode: String articles: [Article] }
Schemaと認証設定を serverless.yml
に追記する。
custom: accountId: ${env:AWS_ACCOUNT_ID} appSync: name: BlogApp # AppSyncのAPI名 authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択 userPoolConfig: # Cognitoの設定 awsRegion: ap-northeast-1 defaultAction: ALLOW userPoolId: ap-northeast-1_XXXXX schema: schema.graphql
Data Sources
バックエンドにはLambdaを使用するため、二つのARNが必要になる。
- LambdaそのもののARN
- AppSyncがLambdaを実行するRoleのARN
Roleはデフォルトの設定で作成されないため、事前にコンソールから作るか、
Serverless Frameworkの resources
機能で作る。
DataSourcesを serverless.yml
に反映すると以下になる。
custom: accountId: ${env:AWS_ACCOUNT_ID} appSync: name: BlogApp # AppSyncのAPI名 authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択 userPoolConfig: # Cognitoの設定 awsRegion: ap-northeast-1 defaultAction: ALLOW userPoolId: ap-northeast-1_XXXXX schema: schema.graphql dataSources: - type: AWS_LAMBDA name: BlogAppResolver config: functionName: blog-app-resolver lambdaFunctionArn: "arn:aws:lambda:ap-northeast-1:${env:AWS_ACCOUNT_ID}:function:blog-app-resolver" serviceRoleArn: "arn:aws:iam::${env:AWS_ACCOUNT_ID}:role/service-role/appsync-ds-lam-xxxxxxxxxxxxx
Mapping Templates
最後にMapping Templatesを整える。
Mapping Templatesのリクエストとレスポンスそれぞれをファイル化し、 mapping-templates
ディレクトリに格納することでAppSyncに反映する仕組みになっている。
レスポンスのMapping Templatesは共通化させて設定した。
レスポンス
common-response.vtl
$util.toJson($context.result)
Queryのリクエスト
query-list-blog-request.vtl
{ "version" : "2017-02-28", "operation": "Invoke", "payload": { "path": "list_blog", "data": $util.toJson($context.args) } }
Mutationのリクエスト
mutation-create-blog-request.vtl
{ "version" : "2017-02-28", "operation": "Invoke", "payload": { "path": "create_blog", "data": $util.toJson($context.args) } }
これらを mapping-templates
ディレクトリ以下に格納する。ディレクトリ名は変更することもできる。
Mapping Templatesの設定を serverless.yml
に反映すると以下のようになる。
custom: accountId: ${env:AWS_ACCOUNT_ID} appSync: name: BlogApp # AppSyncのAPI名 authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択 userPoolConfig: # Cognitoの設定 awsRegion: ap-northeast-1 defaultAction: ALLOW userPoolId: ap-northeast-1_H8OHxpYAa schema: schema.graphql dataSources: - type: AWS_LAMBDA name: BlogAppResolber config: functionName: blog-app-resolver lambdaFunctionArn: "arn:aws:lambda:ap-northeast-1:${env:AWS_ACCOUNT_ID}:function:blog-app-resolver" serviceRoleArn: "arn:aws:iam::${env:AWS_ACCOUNT_ID}:role/service-role/appsync-ds-lam-ln4jdz-blog-app-resolver" mappingTemplates: - dataSources: BlogAppResolber type: Query # Query, Mutation, Subscription field: listArticle # Schema内のフィールド名 request: "query-list-blog-request.vtl" response: "common-response.vtl" - dataSources: BlogAppResolber type: Mutation field: createArticle request: "mutation-create-blog-request.vtl" response: "common-response.vtl"
デプロイ実行
package.json
に登録したコマンドでデプロイを実行する。
サンプルはCognitoやRole ARNを仮で記載しているので、編集してから実行する。
$ npm run deploy
エラーが出る場合は環境変数に SLS_DEBUG
を追加することで、実行の詳細を確認することができる。
$ export SLS_DEBUG=*
Serverless Frameworkは最初に、 serverless.yml
ファイルをコンパイルして、CloudFormationに変換し、S3バケットに格納する。
成果物は .serverless
ディレクトリにも保存される。
S3に格納されたら、CloudFormationが実行され、定義したAWSリソースが生成される。
まとめ
- AppSyncの情報は少ない
- デプロイはServerless Frameworkが便利