20211128のNode.jsに関する記事は5件です。

【NestJS】ServiceクラスにServiceクラスを依存性注入(DI)する方法

はじめに 最近、実務でNestJSを書き始めました! NestJSに関する日本語の記事がまだまだ少ないので、自分用メモも兼ねてどんどん記事を書いていきたいです。 使用技術 NestJS(TypeScript) GraphQL TypeORM クラス図 HogeServiceにFugaServiceを注入して、HogeModuleを外部から利用したいというユースケースです。HogeResolverにFugaServiceを利用します。 Serviceの実装 fuga.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import FugaEntity from 'src/entities/fuga'; @Injectable() export class FugaService { constructor( @InjectRepository(FugaEntity) private fugaRepository: Repository<FugaEntity>, // Fugaエンティティを注入 ) {} async find(id: number): Promise<FugaEntity> { return this.fugaRepository.findOne(id); } } hoge.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import HogeEntity from 'src/entities/hoge'; import { FugaService } from './fuga.service'; @Injectable() export class HogeService { constructor( @InjectRepository(HogeEntity) private hogeRepository: Repository<HogeEntity>, // Hogeエンティティを注入 private fugaService: FugaService, // Serviceを注入 ) {} async find(id: number): Promise<HogeEntity> { const fuga = await this.fugaService.find(1); // 注入したインスタンスを利用可能に! return this.hogeRepository.findOne(id); } } 動かしてみると... [Nest] 877 - 11/28/2021, 2:45:15 AM ERROR [ExceptionHandler] Nest can't resolve dependencies of the FugaService (?). Please make sure that the argument FugaRepository at index [0] is available in the HogeModule context. Potential solutions: - If FugaRepository is a provider, is it part of the current HogeModule? - If FugaRepository is exported from a separate @Module, is that module imported within HogeModule? @Module({ imports: [ /* the Module containing FugaRepository */ ] }) 一見、これだけで動きそうなものなんですが、NestJSはmoduleで依存関係を定義してあげないとエラーが出ます。 moduleの実装 fuga.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FugaService } from 'src/services/fuga.service'; import FugaEntity from 'src/entities/fuga'; @Module({ imports: [TypeOrmModule.forFeature([FugaEntity])], // エンティティを明記 exports: [FugaModule, TypeOrmModule], }) export class FugaModule {} hoge.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { HogeService } from 'src/services/hoge.service'; import HogeEntity from 'src/entities/hoge'; import { FugaModule } from './fuga.module'; import { FugaService } from 'src/services/fuga.service'; import { HogeResolver } from 'src/resolvers/hoge.resolver'; @Module({ imports: [ TypeOrmModule.forFeature([HogeEntity]), // エンティティを明記 FugaModule, // !!! 利用するモジュールを追記 !!! ], providers: [ HogeService, HogeResolver, FugaService, // !!! 利用するサービスを追記 !!! ], exports: [HogeModule, TypeOrmModule], }) export class HogeModule {} あとは、使う側にmoduleを読み込んであればOK! app.module.ts import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApiModule } from './api.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ GraphQLModule.forRoot({ autoSchemaFile: true, }), TypeOrmModule.forRoot(), HogeModule, // !!! モジュールを読み込む !!! ], controllers: [AppController], providers: [AppService], }) export class AppModule {} Resolverの実装 hoge.resolver.ts import { Args, Int, Query, Resolver } from '@nestjs/graphql'; import Hoge from 'src/entities/hoge'; import { HogeService } from 'src/services/hoge.service'; @Resolver(() => Hoge) export class HogeResolver { constructor(private hogeService: HogeService) {} @Query(() => Hoge) async getHoge(@Args({ type: () => Int }) id: number) { return this.hogeService.find(id); } } コンストラクタでHogeServiceを注入して、Resolverを実装することができます。ここには、Fugaserviceは出てきません! 終わりに コードをテスタブルにするために、ちゃんとService層を依存性注入(DI)するようにしたいですよね。NestJSはこれが簡単にできる仕組みを備えているので、ドキュメントをちゃんと読んで使いこなせるようになりたいです! moduleに書かないといけないことによるメリットは、正直よくわかっていないです笑。 appendix package.json "dependencies": { "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", "@nestjs/graphql": "^9.0.4", "@nestjs/platform-express": "^8.0.0", "@nestjs/typeorm": "^8.0.2", "apollo-server-express": "^3.3.0", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", "graphql": "^15.5.3", "graphql-tools": "^8.2.0", "pg": "^8.7.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", "typeorm": "^0.2.37" }, "devDependencies": { "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", "eslint": "^7.30.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "jest": "^27.0.6", "prettier": "^2.3.2", "supertest": "^6.1.3", "ts-jest": "^27.0.3", "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", "typescript": "^4.3.5" },
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Volta + GitHub Actions】ワークフロー内での Node.js のバージョン管理を自動化するアクションを作った話

本記事は GitHub Actions Advent Calendar 2021 の7日目の記事です? 作ったきっかけ プロダクトを運用していると、定期的に言語やライブラリのバージョンアップ対応が必要になると思います。Volta を使用している場合、Node.js のバージョンを上げる時に必要な作業はこの辺りでしょうか。 volta pin コマンドを実行して、Node.js のバージョンを上げる GitHub Actions で Node.js のバージョンを指定している箇所を修正する etc... 複数箇所に Node.js のバージョンが記述されていると、いざバージョンを上げよう!となった時に修正すべきファイルが増えてしまうので、一元管理する方法を検討していました。 そこで、今回は Volta + GitHub Actions を使っている場合に Node.js のバージョンアップ業が少しだけ楽になるアクションを作りました。 Volta とは Volta は Node.js などのバージョンを管理するためのツールです。本記事では触れませんが、 Node.js 以外に npm や yarn なども管理することができます。詳細については公式のドキュメントを参照ください。 似たツールとしては nodenv や nvm などがあります。以下の記事が参考になると思います。 作ったアクション 今回作ったアクションはこちらになります。 具体的な使い方については後述しますが、 package.json に記述されている Node.js のバージョンを取得することができます。 使い方 Volta をインストール 下記を参考にしつつ、環境に応じてインストールしてください。 Volta 経由で Node.js をインストールする volta install コマンドを実行して、任意のバージョンの Node.js をインストールします。 volta install node@16.13.0 プロジェクト内で使用する Node.js のバージョンを固定する 次に volta pin コマンドを実行します。 volta pin node@16.13.0 上記を実行することで、package.json に Node.js のバージョンが追記されます。 これで プロジェクト内で使用する Node.js のバージョンが固定されました。 "volta": { "node": "16.13.0" } get-node-version-from-volta アクションをワークフローに組み込む ここから get-node-version-from-volta アクションを使用していきます。 ワークフローの steps に下記を追加することで、package.json で指定されている Node.js のバージョンを後続の step で取得できるようになります。ここで指定した id は後続の step で使用します。 - name: Get node version from volta id: get-node-version uses: keita-hino/get-node-version-from-volta@main Volta で指定した Node.js のバージョンを取得する 今回の場合、steps.get-node-version.outputs.nodeVersionから volta で指定した Node.js のバージョンを参照できます。(get-node-versionには、 uses した時に指定した id が入ります。) 今回は例として、取得した Node.js のバージョンを setup-nodeアクション に渡します。 これで、volta pinコマンドを使って Node.js のバージョンを変えると、ワークフロー内の Node.js のバージョンも自動で追従されるようになるため、ワークフローの修正が不要になります? - uses: actions/setup-node@v2 with: node-version: ${{ steps.get-node-version.outputs.nodeVersion }} 全体像のサンプル 下記のサンプルは Node.js をインストールして終わっていますが、実際には後続の step でテストなどを実行します。 name: get node version on: push jobs: buid: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Get node version from volta id: get-node-version uses: keita-hino/get-node-version-from-volta@main - uses: actions/setup-node@v2 with: node-version: ${{ steps.get-node-version.outputs.nodeVersion }} ... おわりに 今回は、 Node.js のバージョンを上げる時にワークフローの修正が不要になるアクションを作成しました。まだ実プロダクトで試せてないのですが、今後機会があれば投入してみたいと思っています。もし、Volta を使って Node.js のバージョンを管理している方がいれば使ってみていただけると嬉しいです!? 気になった技術について Twitter で雑に呟いているので、よかったらフォローお願いします!@_kt15_ 参考 https://volta.sh https://qiita.com/tora_oba/items/8f8b7d3e5fb62bc96a3f https://github.com/actions/setup-node
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm install のエラー(zlib: incorrect data check)をRosettaで解決する(M1 Mac)

最近M1チップ搭載のMacBook Airを買い、環境構築を進めていたところで、このエラーにぶつかりました。 前々からM1チップはバグが多いみたいなことを聞いていましたが、これまでのIntel製チップと何が違うのかあまり理解していませんでした。 今回のエラーを通じて、M1チップというものがなんとなくわかったので、エラーの対処法と共に書いてみたいと思います。 このエラーに該当していない人でも、Rosettaの設定をしておくとM1チップの意味のわからないエラーも解決できるようになるかもしれないのでみてみるといいと思います。 ぶつかったエラー npm install -g typescript nvmを用いてインストールをしたNode.js v14でtypescriptをグローバルにインストールすると下のようなエラーが出てしまいました。 zlib: incorrect data check パッケージの中には普通にインストールできるものもあれば、同じようにzlibのエラーが出てしまうものもありました。 環境 MacBook Air (M1, 2020) macOS Monterey v12.0.1 試したこと 同じようなエラーが出ていた記事によると、v14.18.xのバグであると書いてあったので、v14.17に切り替えたり、また、npmのキャッシュを消したりと色々試してみたのですが、まったく解決には至らず途方に暮れていました。 解決方法 結論を先に書くと、Rosetta2を用いてx86アーキテクチャのhomebrewでnvmをインストールすることで解決しました。 ここからRosettaについて長々と書くので、Rosettaは知ってるよって人はここからお読みください。 そもそもRosettaとは Intel製チップとM1チップの違いの一つとして、CPUのアーキテクチャが違います。詳しく理解しているわけではないですが、 Intel製のチップ(x86)は命令セットがCISC(1命令が複雑なやつ)なのに対して、M1チップ(arm)はRISC(1命令が単純なやつ)らしいです。 しかし、世の中のソフトウェアはこれまでのIntel製のアーキテクチャ用に書かれたバイナリも多く、命令が違うということはそれらのソフトウェアをM1チップでは動かすことができないのかということになってしまいます。 そこで活躍するのがRosettaです。元々Rosettaは昔MacのCPUのアーキテクチャがIntel製に変わる時に前のアーキテクチャのバイナリをx86アーキテクチャに変換してくれるものとして生まれたらしいです。そして今回新たにM1チップでarmアーキテクチャに変わるということで、Rosetta2としてx86用のバイナリをarm用に変換してくれるものが生まれました。 こいつを使うことで、これまでx86にしか対応していないソフトウェアもM1チップで動かすことができます。(これ以降Rosettaとは正確にはRosetta2のことです。) Rosetta2は最初から装備されているわけではないですが、アプリケーションを入れるときに必要があればmacが自動的にインストールしてくれます。(公式の解説) ターミナルをRosettaを通して動かす コマンドをRosettaで動かす際は二つ方法があります。個人的にはM1チップをせっかく買ったのなら基本的にはarmで動かしたいと思うので、2番目のやり方おすすめです。 GUIによる設定 Finderでターミナルアプリのアイコンを右クリックして、情報を見るを選択します。すると、下のようなウィンドウが出てくるので「Rosettaを使用して開く」にチェックを入れます。この状態でこのウィンドウを閉じ、ターミナルアプリを開くとx86アーキテクチャで動くようになります。armに戻したい際は同じ手順でチェックを外せばOKです。 コマンドライン上での切り替え コマンドを実行する際に頭にarch -x86_64をつけるだけです。 例えば、 $ arch -x86_64 ls とすればx86で実行されることになります。(lsコマンドはあくまで例です) また、多くのコマンドをx86で実行したければ、 $ arch -x86_64 zsh とすることで、Rosetta環境のzshが起動し、普通にコマンドを打てばRosettaを通して処理をしてくれます。 また、反対にarch -arm64を頭につければもとのarmアーキテクチャに戻すことができます。 これらをいちいち打つのはめんどくさいと思うので、下のようにエイリアスを設定するのがおすすめです。 .zshrc alias x86='arch -x86_64 zsh' alias arm='arch -arm64 zsh' これでx86またはarmと打つだけで切り替えが可能です。 また、今どちらの状態が使われているかは下のコマンドで確認できます。 # arm64 または x86のどちらかが出力される $ uname -m Rosettaを通してnvmをインストールする 長くなりましたが、エラーの具体的な解決方法を説明していきます。 エラーの原因かどうかはわかりませんが、自分はhomebrewを普通にRosettaを通さずにダウンロードし、それを用いてnvmをインストールしていました。しかし、調べたところによるとnodeはv15以降しかarmに対応していないようでした。実際nvmを用いてv14.16.1をダウンロードし、以下のコマンドを実行すると、プロセスがx86であることが確認できます。 # v14以下だとx64と出力される $ node -p process.arch これは別にarmのhomebrewでインストールしたものでこうだったので、brewがarmであることがエラーの原因ではない気がするのですが、何をやっても解決しないので、Rosettaを通したbrewでインストールしてみることにしました。 インストール方法はこちらの記事を参考にしました。 簡単に説明すると、 x86環境でのhomebrewをインストールする。(x86にzshを切り替えるまたは頭にarch -x86_64をつける) .zshrcに環境によって使うbrewを分けるように設定する。(これによって、二つのbrewの衝突を避けることができます) これでRosetta環境にhomebrewがインストールできたら、あとは普通にnvmをインストール、nodeのバージョンを指定し、npmで必要なパッケージをインストールします。 # nvmのインストール(このコマンドの出力に指示が書かれているのでそれに従う) $ brew install nvm # nodeのインストール(自分に必要なバージョンを指定) $ nvm install v14.16.1 # パッケージをインストール $ npm install -g typescript この方法により、エラーが出ることなくインストールすることができました! まとめ M1 Macを使っていて、「これはM1特有のバグっぽいな」と思ったらRosetta環境で試してみると解決することがありそうです。 (全部Rosettaでやってもいいんですが、せっかくのM1ならarmを主としたいよって感じです) 感想 このエラーに4日くらい費やしましたが、おかげでCPUのアーキテクチャの違いの他にも、あまり理解できていなかったzshの設定ファイルの書き方、Nodeのバージョンの切り替えなどいろんなことを知れました! 記事の書き方も知れてよかったです!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js・ブラウザ(JavaScript)で処理時間を測る方法のいくつか

JavaScript で処理時間を計測する場合の話で、複数の方法があるので、そのいくつかをまとめてみました。 Node.js は 2通り、ブラウザ(Chrome を利用)は 3通りの方法を試しています。 Console API 最初は Console API です。 ●Console API reference - Chrome Developers  https://developer.chrome.com/docs/devtools/console/api/#time 以下、ブラウザと Node.js でそれぞれ動かしてみたものです。 ブラウザ <html> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> </body> <script> console.log("start"); console.time("test"); let count; const num = 2000000000; for (let i = 0; i < num; i++) { let square = i ** 2; } console.timeEnd("test"); console.log("end"); </script> </html> 以下の出力が得られました。 Node.js console.log("start"); console.time("test"); let count; const num = 200000000; for (let i = 0; i < num; i++) { let square = i ** 2; } console.timeEnd("test"); console.log("end"); 実行結果は以下の通りです。 Performance 次は以下を試します。 ●Performance - Web API | MDN  https://developer.mozilla.org/ja/docs/Web/API/Performance ●Performance measurement APIs | Node.js v17.1.0 Documentation  https://nodejs.org/api/perf_hooks.html 以下、ブラウザと Node.js でそれぞれ動かしてみたものです。 Node.js の場合は const performance = require("perf_hooks").performance; という部分を追加しています。 ブラウザ <html> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> </body> <script> console.log("start"); const startTime = performance.now(); let count; const num = 2000000000; for (let i = 0; i < num; i++) { let square = i ** 2; } console.log(`${performance.now() - startTime}`); console.log("end"); </script> </html> Node.js const performance = require("perf_hooks").performance; console.log("start"); const startTime = performance.now(); let count; const num = 200000000; for (let i = 0; i < num; i++) { let square = i ** 2; } console.log(`${performance.now() - startTime}`); console.log("end"); 出力は省略しています。 ブラウザ(Chrome)の開発者ツール 以下の記事に、Chrome の開発者ツールを使った例が書かれていたので、それを試してみました。 ●JavaScriptで任意の処理にかかる時間を計測する  https://sbfl.net/blog/2017/12/01/javascript-measure-time/ 手順は以下となります。 Chrome で開発者ツールを開く Performanceタブを開く ⇒ 左上にある丸型のアイコンかその隣のアイコンを押して計測開始4 Stopボタンを押す」 以下は、計測結果が表示された場所のキャプチャです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alexaスキル開発でGoogleカレンダーAPIと連携してみた

この記事は ZOZO #1 Advent Calendar 2021 16日目の記事になります。 はじめに Alexaスキル開発にはアカウントリンクと呼ばれる機能があり、GoogleやAmazon、GitHubなどが提供する外部サービスとOAuth認証を使って連携することができます。 今回はGoogleカレンダーAPIと連携し、Alexaがカレンダーに登録されたイベントを読み上げるスキルを開発したので紹介します。 例) 今日の予定を教えて ▷ 今日の予定は2件あります。1件目は、10時からのQiita投稿、2件目は、12時30分からのランチです。 明日の予定 ▷ 今日の予定は1件あります。1件目は、10時からの歯磨きです。 昨日の予定を教えて ▷ スケジュールがありませんでした。 Alexaスキル そもそもAlexaスキルとは、EchoデバイスやAlexa上で利用できるAlexa専用のアプリのようなものです。ユーザは音声を使ってスキルを操作することで、例えば明日の気象情報を聞いたり、バスの運行情報を聞いたりできます。 代表的なスキルとして以下のようなものがあります。 radiko 次のバス ヤマト運輸 料金 AWSが提供するAlexaスキル開発におけるリソース利用料金については、AWSの無料利用枠内に制限されます。 Alexa開発者アカウントごとに、Amazonはリソース使用量を次の制限に設定します。 ・ AWS Lambda: Alexaがホストするエンドポイントで使用される無制限の無料のAWSLambdaリクエスト。 ・ Amazon S3: 25GBのAmazonS3ストレージ、および1か月あたり250GBのデータ転送。 ・ Amazon DynamoDB: 25GBのAmazonDynamoDBストレージ、1か月あたり1,000万回の読み取りと書き込み。 ・ AWS CodeCommit:50GB-月間ストレージと10,000Gitリクエスト/月。 出典:https://developer.amazon.com/ja-JP/docs/alexa/hosted-skills/usage-limits.html Alexaスキル作成 Googleカレンダーのイベントを読み上げるスキルを作成します。 Alexa Developer Consoleにアクセスしてログイン後、「スキルの作成」を押します。 スキル名に「サンプルカレンダー」と入力し、「スキル作成」を押します。 テンプレート選択画面が表示されるので、「スクラッチで作成」を選択し、「テンプレートで続ける」を押します。 ビルドが行われ、しばらく待つとAlexaスキルが作成されます。 作成したスキルはAWS Lambda上にデプロイされ、お手持ちのEchoデバイスから「サンプルカレンダーを開いて」と話しかけることでスキルを呼び出せます。 作成したAlexaスキルでHello World 作成したAlexaスキルの動作チェックのために、「テスト」タブを選択し、「非公開」になっているところを「開発中」へ変更します。 Alexa Developer Consoleでは、Echoデバイスを使わなくてもAlexaシュミレータを使うことでテキストベースで簡単に動作確認できます。 テキストフィールドに「ハロー」と入力して応答が返ってくることを確認してください。 アカウントリンクの有効化 GoogleカレンダーAPIと連携するために「アカウントリンク」を有効化します。 「ビルド」タブを選択し、「アカウントリンク」を選択します。 アカウントリンクページに表示されるスイッチを画像の通り有効化します。 「Alexaのリダイレクト先URL」は後でGCP上でOAuthクライアントIDを発行するとき使うので、ページをそのまま開いておくかメモしておきましょう。 Google OAuth GoogleカレンダーAPIをAlexaスキルから扱えるようにするため、GCPで新規プロジェクトを作成し、OAuthの設定を行います。 まずは、GCPにログインします。 GCPプロジェクトの作成 新規プロジェクトを作成します。 ここでプロジェクト名は「SampleCalendar」としていますが、何でもOKです。 GoogleカレンダーAPIの有効化 作成したプロジェクトのダッシュボードに切り替わっていることを確認し、次に、APIの有効化を行います。 検索窓に「Google Calendar API」と入力しサジェストされたページへ遷移します。 「有効にする」を押して、新規作成したプロジェクトでGoogleカレンダーAPIを使えるようにします。 OAuth同意画面 「OAuth同意画面」の設定を行います。 メニューから「APIとサービス」->「OAuth同意画面」を選択します。 User Typeは、「外部」を選択し、「作成」を押します。 アプリ名に「SampleCalendar」を入力、「ユーザサポートメール」、「デベロッパーの連絡先情報」にご自身のメールアドレスを入力して「保存して次へ」を選択します。 次に、「スコープを追加または削除」を押してOAuthのスコープを設定します。 今回は、GoogleカレンダーAPIのイベントを読み込めればいいので、フィルターで「calendar.events.readonly」を検索して表示されたスコープを選択し追加します。 (参考:GoogleカレンダーAPIドキュメント) 次に、テストユーザの追加を行います。 ここで追加するユーザはカレンダー情報を取得したいご自身のメールアドレスになります。 Google OAuth認証情報の作成 左のタブから「認証情報」を選択し「認証情報を作成」>「OAuthクライアントID」を選択します。 アプリケーションの種類は「ウェブアプリケーション」を選択し、名前は適当に付けましょう。 そして、承認済みのリダイレクトURIですが、先程のアカウントリンクのページでメモしておいたものをここに貼り付けます。 最後に、作成を押すと、クライアントIDとクライアントシークレットが発行されます。 次のアカウントリンクの設定(続き)で使うのでメモしておきましょう。 アカウントリンクの設定(続き) 先程作成したクライアントIDやシークレットとその他情報をアカウントリンクページの赤枠で囲われた箇所に入力し、保存します。 Authorization Grant種別を選択 -> Auth Code Grant Web認証画面のURI -> https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force アクセストークンのURI -> https://accounts.google.com/o/oauth2/token ユーザのクライアントID -> 発行したクライアントID Client Secret -> 発行したクライアントシークレット ユーザの認証スキーム -> HTTP Basic認証(推奨) スコープ -> https://www.googleapis.com/auth/calendar.events.readonly スマホでアカウントリンク設定 これまでの設定で「サンプルカレンダー」スキルでは、アカウントリンクを使ってGoogle OAuth認証できるようになっていると思うので、上手くできるか検証してみましょう。 スマホのAlexaアプリから「スキルとゲーム」を選択し、「有効なスキル」>「開発」>「サンプルカレンダー」を選択し、設定を押します。 「アカウントをリンク」を選択するとGoogleログインが求められるので進めていきます。 このとき設定していたスコープ「calendar.events.readonly」であることを確認しましょう。 エラーなく「アカウントをリンク済み」と表示されていれば成功です。 Alexaスキル開発 準備はこれで終了。これからコーディングを行います。 以下の流れで開発を行います。 音声トリガー(インテント)の作成 作成したインテントが呼ばれた際の振る舞いをコーディング デプロイ テスト インテント インテントとは インテントとは、ユーザがスキルに求める動作を意味します。 例えば、ユーザがスキルを使ってカレンダーの予定を聞いた場合、どのような予定があるかユーザに教えるインテントが起動します。 一方で、ユーザが予定を登録したい場合、求める動作は異なるので別のインテントを用意しなければいけません。 インテントの作成 Alexaスキルでは、インテントごとに音声による起動トリガーを用意します。 今回は予定を読み込むインテントを作成します。 ページ上部の「ビルド」タブを選択し、左の「対話モデル」>「インテント」を押します。 「インテントを追加」を選択、名前を「ReadEventsIntent」として「カスタムインテントを作成」します。 インテントの起動トリガー追加 サンプル発話を5つほど画像の通り追加します。 「予定を教えて」、「予定」などのワードをトリガーにこのインテントは呼ばれるようになります。 最後に「モデルをビルド」を選択し、音声モデルを作成しましょう。 日付情報を受け取れるようにする 今日だけだと、限定的な使い方しかできないので、柔軟に日付を変更できるようにしましょう。 サンプル発話に追加したもので「今日」と書かれたワードを範囲選択すると「既存のスロットを選択」と表示されます。 今日の部分を変数化して、明日や昨日といったワードを受け取れるようにしたいので「dateTime」と入力し新しいスロットを追加します。 ページ下部にインテントスロットとして先程追加した「dateTime」が現れるので、スロットタイプを「AMAZON.DATE」に変更しましょう。 これで、「今日」、「明日」といったワードだけではなく、クリスマス、こどもの日、12月25日といった日付に関連するワードを"12-25"のような文字列に変換してくれます。 最後に、必ず「ビルドを実行」を押します。(これしないとエラーになります) コーディング 「コードエディタ」タブを選択し、いよいよプログラムを書いていきます。 依存関係の追加 まず、APIや日付を扱うために「axios」と「moment-timezone」パッケージを追加します。 package.json { "name": "hello-world", "version": "1.2.0", "description": "alexa utility for quickly building skills", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Amazon Alexa", "license": "Apache License", "dependencies": { "ask-sdk-core": "^2.7.0", "ask-sdk-model": "^1.19.0", "aws-sdk": "^2.326.0", "axios": "^0.24.0", "moment-timezone": "^0.5.34" } } index.jsの編集 作成した「ReadEventsIntent」をindex.jsに追加したり、API処理を書いたり色々編集します。(コピペでOK) 詳しい説明は後述します。 index.js /* * * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2). * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management, * session persistence, api calls, and more. * */ const Alexa = require('ask-sdk-core'); const axios = require('axios'); const moment = require('moment-timezone'); const LaunchRequestHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; }, handle(handlerInput) { const speakOutput = 'あなたのカレンダーの予定を読み上げます。予定を教えて、と聞いてください。'; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } }; const ReadEventsIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'ReadEventsIntent'; }, async handle(handlerInput) { const timeZone = 'Asia/Tokyo'; // インテント作成時追加した「dateTime」スロットに入った値を取得します「11-23」、「12-24」などの日付が入っている。 const requestedDateTime = handlerInput.requestEnvelope.request.intent.slots.dateTime.value; // 指定された日付と現在の日付との差分を取得 const offsetDay = getDateTimeDiff(timeZone, requestedDateTime); // 日付を「今日」、「11月23日」などAlexaが自然に読めるようにフォーマットする const dateText = getDateTimeText(timeZone, requestedDateTime, offsetDay); // GoogleカレンダーAPIを叩くためのアクセストークン const accessToken = handlerInput.requestEnvelope.session.user.accessToken; if (accessToken === undefined) { // リンクする必要があることをユーザに伝えるためにアカウントリンクカードを返す const speechText = "スキルを利用するにはグーグルでログインを許可してください"; return handlerInput.responseBuilder .speak(speechText) .withLinkAccountCard() .getResponse(); } // Google Calendar APIを叩いてイベント取得 const events = await getEvents(accessToken, timeZone, offsetDay); if (!events.length) { const speakOutput = "スケジュールがありませんでした。"; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } // スケジュールを読める形にフォーマットする const speakOutput = createSpeackTextforCheckingSchedule(events, timeZone, dateText); return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; /* 指定された日付と現在の日付との差分を取得 */ const getDateTimeDiff = (timeZone, dateTime) => { // undefinedのときは0(今日)とする return dateTime === undefined ? 0 : moment.tz(dateTime, timeZone).diff(moment.tz(timeZone).startOf('days'), 'days'); }; /* 読み上げるための日付テキストを取得 */ const getDateTimeText = (timeZone, dateTime, offsetDay) => { const dateTexts = {"-2": "一昨日", "-1": "昨日", "0": "今日", "1": "明日", "2": "明後日"}; // 対応する日付用語があればこれで読み上げる if (offsetDay in dateTexts) { return dateTexts[offsetDay]; } return moment.tz(dateTime, timeZone).format('MM月DD日'); }; /* カレンダーの予定を読むためのテキストを生成 */ const createSpeackTextforCheckingSchedule = (events, timeZone, dateText) => { const scheduleText = events.map((event, index) => { const title = event.summary; // 終日や開始日時が取得できないイベントは、件名だけ読み上げる if (!event.start.dateTime) { return `${index+1}件目は、${title}`; } const startDateTime = moment.tz(event.start.dateTime, timeZone); const hour = startDateTime.get('hour'); const minute = startDateTime.get('minute'); if (minute === 0) { return `${index+1}件目は、${hour}時からの${event.summary}` } return `${index+1}件目は、${hour}時${minute}分からの${event.summary}` }).join('、'); const speakText = events.length > 0 ? `${dateText}の予定は、${events.length}件あります。${scheduleText}です。` : `${dateText}の予定はありません。`; return speakText; } /* カレンダーのイベント情報を取得 */ const getEvents = async (accessToken, timeZone, offsetDay=0) => { // GETリクエストの作成 const apiURL = 'https://www.googleapis.com/calendar/v3/calendars/primary/events'; const now = moment.tz(timeZone); // offsetDayが0であれば今日、1であれば明日の日時でオブジェクトを生成する const dateTime = now.set('day', now.get('day') + offsetDay); // 指定されたタイムゾーンにおいての0時と23時59分59秒をutc時刻に変換 const timeMin = moment(dateTime.startOf("day").format()).utc().format(); const timeMax = moment(dateTime.endOf("day").format()).utc().format(); // 日付関係なく定期イベントの初回が含まれてしまうので呼ばれないようにする const singleEvents = true; const params = {singleEvents, timeMin, timeMax}; const headers = { Authorization: `Bearer ${accessToken}` }; // API コール const events = await axios.get(apiURL, {params, headers}) .then(response => response.data.items); return events; }; /* ----------------HelpやErrorなど既存のものを使う(省略)------------------*/ /** index.jsの最下部 * This handler acts as the entry point for your skill, routing all request and response * payloads to the handlers above. Make sure any new handlers or interceptors you've * defined are included below. The order matters - they're processed top to bottom * */ exports.handler = Alexa.SkillBuilders.custom() .addRequestHandlers( LaunchRequestHandler, ReadEventsIntentHandler, // HelloWorldIntentHandlerと入れ替える HelpIntentHandler, CancelAndStopIntentHandler, FallbackIntentHandler, SessionEndedRequestHandler, IntentReflectorHandler) .addErrorHandlers( ErrorHandler) .withCustomUserAgent('sample/hello-world/v1.2') .lambda(); デプロイ 最後に「デプロイ」を選択し、ここまでの変更を反映しましょう。 コードの説明 スキル起動時、最初に呼ばれる箇所 LaunchRequestHandlerはスキルがユーザから呼ばれた際に最初に動作するハンドラーになります。 ここでは、ユーザにスキルの操作方法を伝えると親切なので、「あなたのカレンダーの予定を読み上げます。予定を教えて、と聞いてください。」を話すようにしています。 const LaunchRequestHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; }, handle(handlerInput) { const speakOutput = 'あなたのカレンダーの予定を読み上げます。予定を教えて、と聞いてください。'; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } }; ReadEventsIntentの追加 ReadEventsIntentが呼ばれたい際の動作を書いています。 大まかな流れは以下の通りです 「dateTime」スロットに入った日付を取得 アカウントリンクによって生成されたAccessTokenを取得 AccessTokenを使って指定された日付のスケジュールをGoogle Calendar APIから取得 APIレスポンスをAlexaが読める形にフォーマット Alexaに読んでもらう const ReadEventsIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'ReadEventsIntent'; }, async handle(handlerInput) { const timeZone = 'Asia/Tokyo'; // インテント作成時追加した「dateTime」スロットに入った値を取得します「11-23」、「12-24」などの日付が入っている。 const requestedDateTime = handlerInput.requestEnvelope.request.intent.slots.dateTime.value; // 指定された日付と現在の日付との差分を取得 const offsetDay = getDateTimeDiff(timeZone, requestedDateTime); // 日付を「今日」、「11月23日」などAlexaが自然に読めるようにフォーマットする const dateText = getDateTimeText(timeZone, requestedDateTime, offsetDay); // GoogleカレンダーAPIを叩くためのアクセストークン const accessToken = handlerInput.requestEnvelope.session.user.accessToken; if (accessToken === undefined) { // リンクする必要があることをユーザに伝えるためにアカウントリンクカードを返す const speechText = "スキルを利用するにはグーグルでログインを許可してください"; return handlerInput.responseBuilder .speak(speechText) .withLinkAccountCard() .getResponse(); } // Google Calendar APIを叩いてイベント取得 const events = await getEvents(accessToken, timeZone, offsetDay); if (!events.length) { const speakOutput = "スケジュールがありませんでした。"; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } // スケジュールを読める形にフォーマットする const speakOutput = createSpeackTextforCheckingSchedule(events, timeZone, dateText); return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; 日付のフォーマット 「dateTime」スロットで取得した「12-23」や「11-23」といった文字列ではコード上で扱いづらいので、現在からその日付はどの程度離れているのかを表す差分を取得します。 /* 指定された日付と現在の日付との差分を取得 */ const getDateTimeDiff = (timeZone, dateTime) => { // undefinedのときは0(今日)とする return dateTime === undefined ? 0 : moment.tz(dateTime, timeZone).diff(moment.tz(timeZone).startOf('days'), 'days'); }; 差分にすることで、相対的に日付を扱えるようになるので、フォーマットも楽になります。 dateTextsでは差分をキーとして昨日、今日、明日といった用語を紐付けています。 /* 読み上げるための日付テキストを取得 */ const getDateTimeText = (timeZone, dateTime, offsetDay) => { const dateTexts = {"-2": "一昨日", "-1": "昨日", "0": "今日", "1": "明日", "2": "明後日"}; // 対応する日付用語があればこれで読み上げる if (offsetDay in dateTexts) { return dateTexts[offsetDay]; } return moment.tz(dateTime, timeZone).format('MM月DD日'); }; また、package.jsonに追加したmoment-timezoneを12月01日といったフォーマットに利用しています。ただのmoment.jsを使わない理由としては、Alexaスキルのデプロイ先がAWS Lambdaで、実行時タイムゾーンがUSとなり意図せず時刻にズレが生じる恐れがあったためです。 moment-timezoneはその名の通りタイムゾーンを使った日付の操作を容易にしてくれるので採用しています。 APIによるイベント取得 package.jsonに追加したaxiosを使ってAPIを叩きます。 標準で搭載されているものを使って叩くこともできますが、axiosのほうがクエリストリングをオブジェクトとして渡せる点で綺麗に書けるので採用しています。 ここでもタイムゾーンを気にしてmoment-timezoneを使って日付をAPIに渡せる用にフォーマットしています。また、Google Calendar APIではUTC形式で渡さないとエラーになる点注意が必要です。 ヘッダーには、Authorizationにアクセスリンクによって生成されたAccessTokenを渡しています。 /* カレンダーのイベント情報を取得 */ const getEvents = async (accessToken, timeZone, offsetDay=0) => { // GETリクエストの作成 const apiURL = 'https://www.googleapis.com/calendar/v3/calendars/primary/events'; const now = moment.tz(timeZone); // offsetDayが0であれば今日、1であれば明日の日時でオブジェクトを生成する const dateTime = now.set('day', now.get('day') + offsetDay); // 指定されたタイムゾーンにおいての0時と23時59分59秒をutc時刻に変換 const timeMin = moment(dateTime.startOf("day").format()).utc().format(); const timeMax = moment(dateTime.endOf("day").format()).utc().format(); // 日付関係なく定期イベントの初回が含まれてしまうので呼ばれないようにする const singleEvents = true; const params = {singleEvents, timeMin, timeMax}; const headers = { Authorization: `Bearer ${accessToken}` }; // API コール const events = await axios.get(apiURL, {params, headers}) .then(response => response.data.items); return events; }; 詳しいリクエストやレスポンス情報を知りたい場合は、こちらの公式ドキュメントをご参照ください。 Alexaが読める形にテキストをフォーマット APIで受け取ったイベント情報から「イベントのタイトル」と「開始時刻」を抜き出し 何時何分からどのようなイベントがあるか書き出しています。 タイトル:event.summary 開始時刻:event.startDateTime /* カレンダーの予定を読むためのテキストを生成 */ const createSpeackTextforCheckingSchedule = (events, timeZone, dateText) => { const scheduleText = events.map((event, index) => { const title = event.summary; // 終日や開始日時が取得できないイベントは、件名だけ読み上げる if (!event.start.dateTime) { return `${index+1}件目は、${title}`; } const startDateTime = moment.tz(event.start.dateTime, timeZone); const hour = startDateTime.get('hour'); const minute = startDateTime.get('minute'); if (minute === 0) { return `${index+1}件目は、${hour}時からの${event.summary}` } return `${index+1}件目は、${hour}時${minute}分からの${event.summary}` }).join('、'); const speakText = events.length > 0 ? `${dateText}の予定は、${events.length}件あります。${scheduleText}です。` : `${dateText}の予定はありません。`; return speakText; } 作成したハンドラーをエクスポート index.jsのページ下部に書いているエクスポートでHelloWorldIntentHandlerをReadEventsIntentHandlerに置き換えれば作業は終了です。 /* ----------------省略------------------*/ /** index.jsの最下部 * This handler acts as the entry point for your skill, routing all request and response * payloads to the handlers above. Make sure any new handlers or interceptors you've * defined are included below. The order matters - they're processed top to bottom * */ exports.handler = Alexa.SkillBuilders.custom() .addRequestHandlers( LaunchRequestHandler, ReadEventsIntentHandler, // HelloWorldIntentHandlerと入れ替える HelpIntentHandler, CancelAndStopIntentHandler, FallbackIntentHandler, SessionEndedRequestHandler, IntentReflectorHandler) .addErrorHandlers( ErrorHandler) .withCustomUserAgent('sample/hello-world/v1.2') .lambda(); 結果 「テスト」タブを選択し、動作確認します。 Googleカレンダーには今日と明日にそれぞれ件名「テスト1」と「テスト2」を入れています。 特定のスキルを呼び出すには「スキル名+を開いて」と聞けば良いので、今回の場合「サンプルカレンダーを開いて」と入力しています。 インテントで設定したReadEventsIntentの起動トリガーである「今日の予定を教えて」と入力したところ、期待したとおり本日の開始日時と件名「テスト1」が返ってきました。明日の予定も同様に件名「テスト2」と開始日時が期待したとおり返ってきました。 ちなみにクリスマスの予定を聞いて見ましたが、スケジュールは登録されていないようでした・・・バグですかね・・・(違う おわりに AlexaスキルとGoogle Calendar APIとの連携方法を紹介しました。 Google OAuth周りの設定は大変でしたが、コード側で独自にOAuth認証を実装しなくてもAlexaスキル開発環境が提供するアカウントリンク機能が使えるので、開発者側の負担を抑えて開発に専念できました。 また、複雑な言語周りの処理を実装しなくても、ユーザから発せられた音声情報に含まれる「今日」や「明日」などのワードをスキル側で日付と判断し取得できるので、処理を柔軟に変えユーザが求める結果を返すことができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む