20210221のNode.jsに関する記事は9件です。

JavaScriptでcsvダウンロードを実装する方法

はじめに

フロント(React)でcsvダウンロードを実装する機会があったため、備忘録です。
どうぞご活用ください。

実装

  const handleDLcsv = async () => {
    //アイテムの定義
    const download_items = [
            {'id': 1, 'name': 'apple', 'price': 100},
            {'id': 2, 'name': 'orange', 'price': 120},
            {'id': 3, 'name': 'melon', 'price': 800}
    ];
    //csvヘッダー
    const array_data = [['id', 'name', 'price']];

    //文字コード
    const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

    //csv用データ作成
    download_items.map((item) => {
      const item_data = [item.id, item.name, item.price];
      array_data.push(item_data);
    })
    let csv_data = array_data.map(function(l){return l.join(',')}).join('\r\n');

    //BLOB生成してダウンロード実行
    const blob = new Blob([bom, csv_data], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'csv_file.csv';
    const clickHandler = () => {
      setTimeout(() => {
        URL.revokeObjectURL(url);
        a.removeEventListener('click', clickHandler);
      }, 150);
    };
    a.addEventListener('click', clickHandler, false);
    a.click();
  }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsのバージョン管理

node --verion で現在のNode.jsのバージョンを確認。

n ls-remote --allでダウングレード/アップグレードできるバージョンを確認。

sudo n 12.0.0で指定のバージョンにダウングレード/アップグレードできる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmでcb.apply is not a function エラーが発生した対処

備忘録として。

現象

npmで何かパッケージをインストールしようとすると、以下のようなエラーが発生。
npmをアップデートしろとのエラーだがnpm updatenpm instalが同じようなエラーで実行できない。

npm WARN npm npm does not support Node.js v15.9.0
npm WARN npm You should probably upgrade to a newer version of node as we
...
npm ERR! cb.apply is not a function 
...

環境

  • Windows 10 pro
  • Node.js バージョン 15.9.0 (Chocolateyからインストール)
  • npm バージョン 7.5.3

原因

原因は、私の場合、昔にインストールしていたNode.js(アンインストール済み)のキャッシュが残っていたためのようでした。
npmのバージョンをnpm -vで確認すると、本来7.5.3のはずが、6.0.0になっていました。残っていたキャッシュによりバージョン判定がうまくいっていなかったのでしょう。

対処

  • C:\Users\{ユーザー名}\AppData\Roamingからnpm, npm-cacheフォルダを削除。
  • 念のため、npm cache clean --forceでキャッシュ消去
  • Chocolateyからnode.jsを再インストール。
  • npmをインストール。 npm install -g npm@7.5.3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IoTデータの可視化サービス「Ambient」を試す(Node.js のパッケージを利用)

以前からサービスについて知っていたものの、クラウドサービスでのデータ可視化はあまりやってこなかったのもあり、まだ利用をしたことはなかった IoTデータ可視化サービスの「Ambient」
(これまでは、HTML+JavaScript でグラフ描画ライブラリを使っての可視化をやってた)

今回、よく自分が利用しているデバイス・サービスで利用する場合はどうすれば使えるのかを見てみたり、実際に Node.js のパッケージを使ってデータの送信と可視化の部分を試してみたりしました(送ったデータはランダムに生成したダミーデータを利用)。

Ambient の概要

既に Qiita でもタグがあり、記事もいくつも書かれていたりしますが、自分の理解を深めるためにも以下の公式ページをざっくり見ていって、その内容をメモしてみました。

●Ambient – IoTデータ可視化サービス
 https://ambidata.io/

構成図.jpg

可視化の例として、公式ページのトップに以下のような事例が出ています。
可視化の例.jpg

利用するための方法(情報を見てみる&チャネルを作成してみる)

Ambient を使うためのおおまかな手順は、以下の公式のチュートリアル(その中の1つ目)に書かれています。

●Ambientを使ってみる – Ambient
 https://ambidata.io/docs/gettingstarted/

【利用するための手順】
1. ユーザー登録(無料)
2. チャネル生成
3. マイコン側プログラミング
4. データー送信
5. 可視化(グラフ化)

ユーザ登録とチャネル作成

最初は公式サイト上でユーザ登録を行い、その後は可視化対象となるデータを管理するための「チャネル」を作成するところまでは、公式サイト上で操作を行っていけば良さそうです。

以下は、サイト上で「チャネルを作る」ボタンを押した後の状態です。
Ambient_チャネル一覧.jpg

チャネルが 1つ追加され、データ送信などに使うキーなどが発行されました。

マイコン側プログラミング

記事の途中で書いた手順では「マイコン側プログラミング」という項目で書かれてますが、公式情報の別のページ(以下のページ)を見ると、複数のテキストプログラミング言語・ビジュアルプログラミング環境からデータの送信を行えるようです。

●ライブラリー/リファレンス – Ambient
 https://ambidata.io/refs/

  • 上記のページに掲載されているもの
    • Arduino / C++
    • mbed / C++
    • Python / MicroPython
    • node.js
    • Node-RED

また、M5Stackシリーズをビジュアルプログラミングで扱える UIFlow についても、GitHub で公開された独自ブロックを使うとデータの送信を行えるようです。

●UIFlow(Blockly)でAmbientにデータを送る – Ambient
 https://ambidata.io/samples/m5stack/uiflow/
●GitHub - AmbientDataInc/UIFlow
 https://github.com/AmbientDataInc/UIFlow

自分が個人的によく利用しそうな方向だと「UIFlow・Node-RED・Node.js」あたりになりそうです。

データ送信・可視化

データ送信元として様々な環境が利用できる中で、今回は Node.js を使ってマイコンを利用せずに試してみようと思います。

●node.jsライブラリー ambient-lib – Ambient
 https://ambidata.io/refs/node-js/

Ambient を試す

現時点までで、 Ambient上でチャネルを作成するところまでは試しました。
チャネル作成時に発行されたキー等の情報を使い、Node.js から可視化用のデータを送ってみようと思います。

Node.js で簡単なお試しまで

公式の情報を見つつ、パッケージのインストールから始めていきます。

パッケージのインストール

$ npm install ambient-lib

プログラムからデータを送る

ライブラリの読み込みや接続・データ送信を行う処理は、以下のように書くようです。

// ライブラリの読み込み
var ambient = require('ambient-lib');
// Ambientへの接続
ambient.connect(チャネルId, ライトキー[, リードキー[, ユーザーキー]]);
// Ambientへのデータ送信
ambient.send(data, callback(err, res, body));

データ送信に関しては、例えば上記の data の部分に var data = {d1: 1.1, d2: 2.2}; という形などで指定するようです。2つのデータを送る場合のサンプルとして、以下の内容が公式に書かれていました。

ambient.send({d1: 1.1, d2: 2.2}, function(err, res, body) {
  if (err) {
    console.log(err);
  }
  console.log(res.statusCode);
});

上記の情報を活用しつつ、とりあえず d1 のみに 1回だけ数値を送る、ということをやってみます。
プログラムは以下のとおりで、 ambient.connect では「チャネルId・ライトキー」の 2つのみを指定しました。 d1 に送る数値は 1.1 を指定しています。

const ambient = require("ambient-lib");

ambient.connect(ご自身のチャネルId, "【ご自身のライトキー】");

ambient.send({ d1: 1.1 }, function (err, res, body) {
  if (err) {
    console.log(err);
  }
  console.log(res.statusCode);
});

上記を実行してみた後に Ambient のチャネルを見てみると、以下の画像のような表示がされていました。うまく数値を送信することができていそうです。
値を1つだけ送ってみる.jpg

Node.js でのお試し(続き)

Node.js でプログラムを書く

無事にデータを送ることができているようなので、次に連続したデータ送信を行ってみます。何かセンサー付きのデバイスから値を取得する方法もありますが、今回は簡単に試せるように Node.js上でセンサーの値に見立てた乱数を生成して、それを送信するという形にしてみています。

先ほどと別のチャネルを作り、以下の Node.js のプログラムを使ってデータを送ってみました。今回のプログラムでは、 d1d2 の 2種類のデータを送るようにしてみました(※ 送信する値は乱数で生成)。

const ambient = require("ambient-lib");

ambient.connect(ご自身のチャネルId, "【ご自身のライトキー】");

const intervalFunc = function () {
  ambient.send({ d1: Math.random() * 100, d2: Math.random() * 70 }, function (err, res, body) {
    if (err) {
      console.log(err);
    }
    console.log(res.statusCode);
  });
};

setInterval(intervalFunc, 5500);

データの送信間隔についてですが、以下の公式情報を見ると 「送信から次の送信まではチャネルごとに最低5秒空ける必要があります。それより短い間隔で送信したものは無視されます」 とあるため、5秒より少しだけ長い間隔を設定しました。

●諸元/制限 – Ambient
 https://ambidata.io/refs/spec/

また、1日に登録可能なデータ数の制限について 「1チャネルあたり1日3,000件までデーターを登録できます。平均すると28.8秒に1回のペースです。」 と書かれているため、1日じゅうデータを送る場合はデータの送信間隔をさらに長くする必要があるようです。

チャネルで 2つのデータを同一のグラフ内に表示させてみる

以下の公式情報の「チャートのカスタマイズ」の部分を参照しながら、グラフの表示方法を 1つのグラフ内で 2つのデータを同時に表示する形にしてみました。

●チャネルとチャートのカスタマイズ – Ambient
 https://ambidata.io/docs/customize/

表示方法の設定変更を行った後に、先ほどのプログラムをしばらくの間実行し続けると、以下のグラフが描画されました。
2種類のデータをグラフ化.jpg

まとめ

今回、IoTデータ可視化サービスの「Ambient」を使った数値データのグラフ化を行ってみました。

記事内でふれていたように、UIFlow・Node-RED や Node.js以外の他のプログラミング言語でも利用できるようなので、今後は何らか特定のデバイスを使ってセンサーで取得した値をグラフ化するようなこともやってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ZoomAPIを使って、ミーティングルームを作成してみる。(Create Zoom Meeting)

この記事について

表題の通り、ZoomのAPIを使って、ミーティングルームを作成したときのメモです。
Node.jsを使って作成しました。
認証方法はOAuthを利用しました。

ゴール

この記事では、2021年2月25日 10時~ 1時間のミーティングルームを作成することをゴールとします。

手順

1.「Zoom」アカウント作成
2.「Zoom API」利用のための事前準備
3.認証コードの取得
4.アクセストークンの発行
5.必要パラメータの作成
6.実行

1.「Zoom」アカウント作成

まだアカウントを取得していない方は、下記にアクセスしてアカウントを作成します。
https://zoom.us/jp-jp/meetings.html

2.「Zoom API」利用のための事前準備

2-1.クレデンシャル情報の取得
下記にアクセスします。
https://marketplace.zoom.us/
その後、右上の「Develop」から「Build App」をクリックします。

「Choose your app type」と聞かれるので、
今回は、「OAuth」の中の「Create」を選択します。

Create an OAuth app
「App Name」を入力します。 例)Create Meeting Test
「Choose app type」は「Account-level app」を選択します。
「Would you like to publish this app on Zoom App Marketplace?」は「OFF」にします。
 ※Zoom APP マーケットプレイスに公開したい場合はONにします。
入力を終えたら、「Create」をクリックします。

App Credentials
この画面の「Client ID」と「Client Secret」をメモしておきます。
「Redirect URL for OAuth」には、一旦、「http://localhost:8000」と入力しておきます。
「Whitelist URL」にも同じく「http://localhost:8000」と入力しておきます。
右下の「Continue」をクリックして、「Scopes」追加画面に移動します。

2-2.スコープの追加
Add Scopes
「+ Add Scopes」ボタンをクリックして、Meetingの「View and manage all user meetings」にチェックを入れ、「Done」をクリックします。
image.png

2-3.Client_ID:Client_Secretを「Base64」でエンコードする

Authorization The string "Basic" with your Client ID and Client Secret with a colon : in between, Base64 Encoded. For example, Client_ID:Client_Secret is Q2xpZW50X0lEOkNsaWVudF9TZWNyZXQ=.

下記にアクセスします。
https://www.base64decode.org/

先ほど、メモしておいた、「Client ID」と「Client Secret」を使い、
自分のClient ID:自分のClient Secretをエンコードします。
※ClientIDとClientSecretの間に「:」があるので、お忘れなく。

エンコードされた文字列をメモしておきます。

参考:https://marketplace.zoom.us/docs/guides/auth/oauth

以上で、事前準備は完了です。

3.認証コードの取得

Client_idに自分のクライアントIDを入力して、認証コード生成用のURLを作成します。
例)↓
https://zoom.us/oauth/authorize?response_type=code&client_id=あなたのクライアントID&redirect_uri=http://localhost:8000
URLにアクセスすると、Zoom API の認証が始まります。
「Authorize」をクリックして、自分のアカウントでのZoomの操作をZoom APIを通して行えるようにします。
認証に成功すると、画面遷移するので、code以下の部分をメモしておきます。
image.png

4.アクセストークンの発行

アクセストークンは以下のコードで発行できます。
(先にローカルPCの実行環境でnpm install requestを実行しておきます。)

getAccessToken.js
var request = require("request");

var options = {
  method: "POST",
  url: "https://zoom.us/oauth/token",
  qs: {
    grant_type: "authorization_code",
    code: "we473poKUZ_3dXwRRJrSumqswISsV4q1g", // 3.認証コードの取得 で取得したコードを入力する。
    redirect_uri: "http://localhost:8000",
  },
  headers: {
    Authorization:
      "Basic NjZxT3dmWElTN205RVdfdXV4UWFTZzppc21nQWVud3I0SmcyN0Z3YnE0UUE1TFB4QW1tSVBzdg", // さっきメモしたエンコードした文字列を「Basic 」の後に入れる。
  },
};

request(options, function (error, response, body) {
  if (error) throw new Error(error);

  console.log(body);
});

node getAccessToken.jsを実行します。
すると下記のような結果を得られます。

$ node getAccessToken.js
{"access_token":"eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI2YjhkMDg4Ny0xNWViLTRiODAtYWM2NC0yNTgzNDdiOTQ4NTIifQ.eyJ2ZXIiOjcsImF1aWQiOiI3ZGJkM2Q3NzVlMGQ3NTFjYzljZWQxMTUzMGJjYmM2ZiIsImNvZGUiOiJ3ZTQ3M3BvS1VaXzNkWHdSUkpyU3VtcXN3SVNzVjRxMWciLCJpc3MiOiJ6bTpjaWQ6NjZxT3dmWElTN205RVdfdXV4UWFTZyIsImdubyI6MCwidHlwZSI6MCwidGlkIjowLCJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiIzZFh3UlJKclN1bXFzd0lTc1Y0cTFnIiwibmJmIjoxNjEzODMxODQzLCJleHAiOjE2MTM4MzU0NDMsImlhdCI6MTYxMzgzMTg0MywiYWlkIjoidEtoaHhtZVVUZG1VT3lfaGF3Sko3dyIsImp0aSI6IjBiZDdhODQ3LTVmYTktNDRiNC1iYTM3LThkNDNkNjdjOThjNSJ9.MwILIjAoWSTQJCfh1TYc3BJIVK7ptyCVv4g5h82oNc_GLLL6_S9RHD_D4MUSvZMKzt_vtLHK7DszMb3rYD5tig","token_type":"bearer","refresh_token":"eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiJmNGQ3ODMxNC1lYmM3LTQzYTQtOThiMC1jODc1OGMzOTAxMzUifQ.eyJ2ZXIiOjcsImF1aWQiOiI3ZGJkM2Q3NzVlMGQ3NTFjYzljZWQxMTUzMGJjYmM2ZiIsImNvZGUiOiJ3ZTQ3M3BvS1VaXzNkWHdSUkpyU3VtcXN3SVNzVjRxMWciLCJpc3MiOiJ6bTpjaWQ6NjZxT3dmWElTN205RVdfdXV4UWFTZyIsImdubyI6MCwidHlwZSI6MSwidGlkIjowLCJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiIzZFh3UlJKclN1bXFzd0lTc1Y0cTFnIiwibmJmIjoxNjEzODMxODQzLCJleHAiOjIwODY4NzE4NDMsImlhdCI6MTYxMzgzMTg0MywiYWlkIjoidEtoaHhtZVVUZG1VT3lfaGF3Sko3dyIsImp0aSI6ImRmZTExNDgwLWY1NGQtNDFkMy04YTAyLWZhMTIzODUxYWZjNCJ9.CMSJQL9R6NCJFqisJ9dmz4r5yQCJB5-cotFgfWHXz_RosDVJ4ct9LJ25weixveL_7ZaXGmr5SHCZxqREov7kXA","expires_in":3599,"scope":"meeting:write:admin"}

↑ここで発行されたaccess_tokenを下記「create_meetings.js」のheadersに「authorization: "Bearer [access_token]"」となるように貼り付けます。

create_meetings.js
var http = require("https");

var options = {
  method: "POST",
  hostname: "api.zoom.us",
  port: null,
  path: "/v2/users/me/meetings",
  headers: {
    "content-type": "application/json",
    authorization:
      "Bearer eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiIwNzNjODYyZS05NGZjLTRiODYtYTM1Yy1lNTdjNjJkZTVjYjcifQ.eyJ2ZXIiOjcsImF1aWQiOiJiODZkMzNmNDQwOWNjNzI1MWFjNTA1OTI1YWU5ZWU1MCIsImNvZGUiOiJUQnpzeHFlMTJJXzNkWHdSUkpyU3VtcXN3SVNzVjRxMWciLCJpc3MiOiJ6bTpjaWQ6Y2NFdmFTMERUYnFQR3JVQTRBMVVudyIsImdubyI6MCwidHlwZSI6MCwidGlkIjowLCJhdWQiOiJodHRwczovL29hdXRoLnpvb20udXMiLCJ1aWQiOiIzZFh3UlJKclN1bXFzd0lTc1Y0cTFnIiwibmJmIjoxNjEzNjU3ODAzLCJleHAiOjE2MTM2NjE0MDMsImlhdCI6MTYxMzY1NzgwMywiYWlkIjoidEtoaHhtZVVUZG1VT3lfaGF3Sko3dyIsImp0aSI6IjBlNTNlZTQ2LWMwMjMtNDI0Ny1iNTgxLWYyYThkYTkyYjAwYSJ9.A6jiK0MIAw6EBurd0Gg0Z3me3MuhTvfwEvZ77RgTgtiODjZTmwkA0vPzCKbyGXjcMbeY96oY2Y5uWKvvBY2sYA",
  },
};

var req = http.request(options, function (res) {
  var chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function () {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
  });
});

req.write(
  JSON.stringify({
    // 作成するZoomイベントの詳細を入力します。
    // 設定値については、下記URLの「Request Body」の「Schema」という個所を参照してください。
    // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
    topic: "[Zoom API]Node.js sample - ミーティングルームの作成",
    type: 2, 
    start_time: "2021-02-25T10:00:00Z",
    duration: 60,
    timezone: "Asia/Tokyo",
    password: "12345",
    agenda: "Zoom API のテスト",
    settings: {
      host_video: true,
      participant_video: false,
      cn_meeting: false,
      in_meeting: false,
      join_before_host: true,
      jbh_time: 0,
      mute_upon_entry: true,
      watermark: true,
      use_pmi: false,
      approval_type: 0,
      audio: "both",
      auto_recording: "none",
    },
  })
);
req.end();

node create_meeting.jsを実行します。

$ node create_meeting.js
{"uuid":............................,"id":.........,}

成功すると、上記の用に、作成されたミーティングルームに紐づくいろいろな情報が取得できます。

また、Zoom Meetings Pageで下記画像のように、ミーティングが作成さているはずです。
image.png
以上です。

参考にしたサイト

https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
https://marketplace.zoom.us/docs/guides/auth/oauth

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】WindowsでNode.jsの環境構築をする

プログラミング勉強日記

2021年2月21日
友人にNode.jsの環境構築を教えるために一度調べたので、そのやり方をまとめる。

Node.jsをインストールする

 Node.jsの公式サイトからNode.jsのインストーラーをダウンロードする。(今回は推奨版をダウンロードした。)

image.png

 インストーラーをダウンロードしたら、開いて実行する。その後指示にしたがtt進めていく。基本的にはそのままNextをクリックして進めていく。最後にFinishを押して終了する。

Node.jsのバージョンを確認する

 それぞれコマンドで下記を実行すると、バージョンを確認することができる。バージョンが表示されない場合は、インストールができていない。

$ node ^v
$ npm -v
バージョンが表示されている場合
C:\>node -v
v12.18.4

C:\>npm -v
6.14.6
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HerokuでNode.jsのアプリからPostgreSQLに接続できない時の対処

N予備校 【2020年度】プログラミング入門 Webアプリ
[第4章24節 【サービス開発9】セキュリティ対策と公開]

Herokuの設定で詰まったので解決した手順を記載。

Database / GitHubのOAuthなどの設定を終えてHerokuのアプリを起動し、ログインをしようとすると下記のエラーが発生

2021-02-21T03:05:26.710859+00:00 app[web.1]: Unhandled rejection SequelizeConnectionError: no pg_hba.conf entry for host "XX.XXX.XXX.XXX", user "xxxxxxxxxx", database "xxxxxxxxxxxx", SSL off
2021-02-21T03:05:26.710870+00:00 app[web.1]: at connection.connect.err (/app/node_modules/sequelize/lib/dialects/postgres/connection-manager.js:182:24)
2021-02-21T03:05:26.710870+00:00 app[web.1]: at Connection.connectingErrorHandler (/app/node_modules/pg/lib/client.js:194:14)
2021-02-21T03:05:26.710871+00:00 app[web.1]: at Connection.emit (events.js:198:13)
2021-02-21T03:05:26.710872+00:00 app[web.1]: at Socket.<anonymous> (/app/node_modules/pg/lib/connection.js:134:12)
2021-02-21T03:05:26.710872+00:00 app[web.1]: at Socket.emit (events.js:198:13)
2021-02-21T03:05:26.710872+00:00 app[web.1]: at addChunk (_stream_readable.js:288:12)
2021-02-21T03:05:26.710873+00:00 app[web.1]: at readableAddChunk (_stream_readable.js:269:11)
2021-02-21T03:05:26.710874+00:00 app[web.1]: at Socket.Readable.push (_stream_readable.js:224:10)
2021-02-21T03:05:26.710874+00:00 app[web.1]: at TCP.onStreamRead [as onread] (internal/stream_base_commons.js:94:17)
2021-02-21T03:05:56.475031+00:00 heroku[router]: at=error code=H12 desc="Request timeout" method=GET path="/auth/github/callback?code=025f7e4e57093209889a" host=limitless-lowlands-54630.herokuapp.com request_id=31dc51eb-fd89-48fb-9155-a08b997709de fwd="153.207.171.208" dyno=web.1 connect=1ms service=30001ms status=503 bytes=0 protocol=https

・アプリからDB接続ができない (pg_hba.confの設定が足りない?)
・GitHubの認証がタイムアウトする (一つ前のDB接続エラー起因かはこの時点では不明)

エラーメッセージと pg_hba.conf、Herokuでぐぐってみたところ、解決方法としてHerokuの環境変数でDB接続をSSL化するか、node-jsのソース側でこのエラー制御を無視することができるようでした。

環境変数の追加が簡単そうだったので、方法1で対処しました。

【方法1】

参考にした記事
https://qiita.com/hiro93n/items/fe015ce9517f139edbeb

Herokuに環境変数を設定する

heroku config:set PGSSLMODE=require

環境変数を更新したらHerokuが再起動して接続できるようになり、GitHub認証のタイムアウトエラーも解消しました。

2021-02-21T03:14:46.096182+00:00 app[api]: Release v11 created by user xxxxxxxxxx
2021-02-21T03:14:46.096182+00:00 app[api]: Set PGSSLMODE config vars by user xxxxxxxxxx
2021-02-21T03:14:46.573285+00:00 heroku[web.1]: Restarting
2021-02-21T03:14:46.590224+00:00 heroku[web.1]: State changed from up to starting
2021-02-21T03:14:48.006684+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2021-02-21T03:14:48.223861+00:00 heroku[web.1]: Process exited with status 143
2021-02-21T03:14:50.316993+00:00 heroku[web.1]: Starting process with command `npm start`

Warningの中に、node-jsが理由を説明しているログがありました。

2021-02-21T03:14:53.846841+00:00 app[web.1]: (node:21) DeprecationWarning: 
Implicit disabling of certificate verification is deprecated and will be removed in pg 8. 
Specify `rejectUnauthorized: true` to require a valid CA or `rejectUnauthorized: false`
 to explicitly opt out of MITM protection.

暗黙的な証明書検証の無効化は非推奨となっていますので、pg 8で削除されます。rejectUnauthorized: true で証明書の検証をしているものをrejectUnauthorized: falseと指定することで、明示的に中間者攻撃防御を解除してください。

【方法2】

Herokuの公式ドキュメント[Heroku Posgres]
https://devcenter.heroku.com/articles/heroku-postgresql#connecting-in-node-js

先ほどの理由からPosgreのDB接続用のインスタンスを生成するときに、ssl証明書検証の設定をfalseに明示的に設定する。

const client = new Client({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false
  }
});

今回は方法1で接続できましたが、方法2も公式ドキュメントの情報なので問題なく解決できそうです。

もし4章の最後の節で詰まってる方がいましたらご参考まで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Slackにバーンダウンチャートを定期的に送信するボットの作成方法

概要

近年、スクラムを導入した開発チームが増えつつあります。
スクラムチームの質を向上させるため、プロジェクト管理だけではなくて、チームメンバーが開発状況を把握しやすくすることが重要だと思います。

今回は、JIRAのREST APIを使って、最新のスプリント情報を取得すると、バーンダウンチャートを生成して、日常的にSlackのChannelに送信するBotを紹介します。

要求仕様

node 12.16.1
npm 6.13.4

JIRA REST API テスト

まずはChrome API Tester Toolを使って、JIRA APIのテストを行う。(スクラムボードのみ支持)
METHOD: GET
SCHEMA: https://{hostname}.atlassian.net/rest/agile/1.0/board/{rapidViewId}/sprint/{sprintId}/issue

こんな感じのレスポンスが問題ないと思います。
jira-api-test.png

Slack Botを作成する

Botの作り方はワークスペースで利用するボットの作成を参照してください。
現在、元宵節を迎えることになりますので、MagicalYuanxiao(中国語:神奇小元宵)と名付けました。
slack-app.png
次は、作成済みのAppを送信したいChannelに連携する。
addapp.png

Slack Bot送信テスト

HTTPクライアントのインストール

$ npm install axios

メッセージを送信する

sendMsg.js
// test file
// Run "node sendMsg.js" to test access slack token
// If send message to channel is successfully, the connection is successful
const axios = require("axios");
const MSG_URL = "https://slack.com/api/chat.postMessage";
const slackToken= "Your slack token"; // *Slack Bot User OAuth Access Token*

async function run() {
  const res = await axios.post(
    MSG_URL,
    {
      channel: "#test", // *send to target channel*
      text: "Hello, I am magical yuanxiao!", // message content
    },
    {
      headers: {
        authorization: `Bearer ${slackToken}`,
      },
    }
  );
  console.log("Done", res.data);
}

run().catch((err) => console.log(err));

実行コマンド

$ node sendMsg.js

テスト結果
image.png

ライブラリの導入

・アクセスJIRA(https://www.npmjs.com/package/jira-client)
・ファイルとディレクトリの読み取りと書き込み(https://www.npmjs.com/package/fs)
・HTTPリクエスト(https://www.npmjs.com/package/request)
・時間データの操作(https://www.npmjs.com/package/moment)
・グラフ描画(https://github.com/SeanSobey/ChartjsNodeCanvas)
・環境変数の設定(https://www.npmjs.com/package/dotenv)

スプリントデータを保存する

app.js
// jira API options
const jira = new JiraApi({
  protocol: "https",
  host: jiraHost,
  username: jiraUsername,
  password: jiraPassword,
  apiVersion: "3",
  strictSSL: true,
});

// today's date
const YYYYMMDD = moment().format("YYYYMMDD");

// step1: get sprint info to generate json from jira
  let sprint = {};
  await jira
    .getSprintIssues(rapidViewId, sprintId)
    .then(function (issues) {
      sprint["id"] = issues.sprint.id; // スプリントID
      sprint["name"] = issues.sprint.name; // スプリント名
      sprint["goal"] = issues.sprint.goal ? issues.sprint.goal : 0; // 目標点数
      sprint["startDate"] = moment(issues.sprint.isoStartDate).format(
        "YYYYMMDD"
      ); // 開始日
      sprint["endDate"] = moment(issues.sprint.isoEndDate).format("YYYYMMDD"); // 終了日
      sprint["issuesPointSum"] = issues.contents.completedIssuesEstimateSum
        .value
        ? issues.contents.completedIssuesEstimateSum.value
        : 0 + issues.contents.issuesNotCompletedEstimateSum.value
        ? issues.contents.issuesNotCompletedEstimateSum.value
        : 0; // ストーリー点数合計
      sprint["notCompletedIssuesPointSum"] = issues.contents
        .issuesNotCompletedEstimateSum.value
        ? issues.contents.issuesNotCompletedEstimateSum.value
        : 0; // 未完了ストーリー点数合計(include: todo, doing, review...)
    })
    .catch(function (err) {
      console.error(err);
    });

  // json data to be written
  let jsonData = {
    code: 0,
    data: sprint,
    updateDate: moment().format("YYYY/MM/DD HH:mm:ss"),
    msg: "success",
  };
  // format json
  let text = JSON.stringify(jsonData);
  // directory and file name
  let file = path.join("./output/", YYYYMMDD + "_sprint_data.json");
  // write into json
  await fs.writeFile(file, text, function (err) {
    if (err) {
      console.log(err);
    } else {
      console.log("File was successfully created: " + file);
    }
  });

チャートデータを用意する

app.js
 // step2: get data for line chart
  var startAndEndDateDiff = moment(sprint.endDate).diff(
    moment(sprint.startDate),
    "days"
  );
  // X axis labels for lint chart
  var xLables = [];
  for (let i = 0; i <= startAndEndDateDiff; i++) {
    xLables[i] = moment(sprint.startDate).add(i, "days").format("MM/DD");
  }

  // Y axis values for lint chart
  var yValues = [];
  // from 0 to date difference(from today to startDate)
  var dateDifferenceArray = [];
  for (let i = 0; i <= moment().diff(moment(sprint.startDate), "days"); i++) {
    dateDifferenceArray.push(i);
  }
  var jsonArray = dateDifferenceArray.reverse().map(getJsonAsync);
  await Promise.all(jsonArray)
    .then(function (jsonData) {
      // 本スプリントの日別残ポイントデータを埋め込む
      yValues = jsonData.map((s) => s.data.notCompletedIssuesPointSum);
    })
    .catch(function (err) {
      console.error(err);
    });

  // guideline Y axis values
  var guidelineValues = [];
  for (let i = 0; i <= startAndEndDateDiff; i++) {
    guidelineValues.push(
      sprint.goal - Math.floor((sprint.goal * i) / startAndEndDateDiff)
    );
  }

バーンダウンチャートを作る

app.js
 // step3: create burn down chart image
  const height = 400;
  const width = 700;
  const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height });
  (async () => {
    const configuration = {
      type: "line",
      data: {
        labels: xLables,
        datasets: [
          {
            label: "Story Points remaining",
            borderColor: "rgba(255, 100, 100, 1)",
            data: yValues,
            fill: false,
          },
          {
            label: "Guideline",
            borderColor: "rgba(122, 122, 122, 1)",
            borderDash: [10, 3], // dotted line
            data: guidelineValues,
            fill: false,
            borderWidth: 1,
          },
        ],
      },
      options: {
        scales: {
          yAxes: [
            {
              ticks: {
                beginAtZero: true, // set Y min value to 0
              },
            },
          ],
        },
        elements: {
          point: {
            radius: 0, // do not show points
          },
        },
        title: {
          display: true,
          fontSize: 16,
          text:
            sprint.name +
            "(" +
            moment(sprint.startDate).format("MM/DD") +
            "" +
            moment(sprint.endDate).format("MM/DD") +
            ")", // chart title
        },
      },
    };
    const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration);
    const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
    await fs.writeFile(
      "./output/" + YYYYMMDD + "_burn_down_chart.png",
      base64Data,
      "base64",
      function (err) {
        if (err) {
          console.log(err);
        }
      }
    );
  })();

Slackにバーンダウンチャートを送信する

app.js
// step4: upload created chart to slack
  request.post(
    {
      url: UPLOAD_URL,
      formData: {
        file: fs.createReadStream(
          "./output/" + YYYYMMDD + "_burn_down_chart.png"
        ),
        token: slackToken,
        filetype: "png",
        filename: YYYYMMDD + "_burn_down_chart.png",
        channels: slackChannel, // send to XXX channel
        title: YYYYMMDD + "_burn_down_chart.png", // show this name in slack
      },
    },
    function (error, response, body) {
      if (error) {
        console.log(error);
      } else {
        console.log("Send burn down chart to slack.");
        // console.log(body);
      }
    }
  );

Slackスクリーンショット
burndown.png
以上でnodejsの実装部分は完成です。
あとは定時バッチなど行えば定期的にSlackにpostする予定です。

Source Code (GitHub)

そのうちリポジトリのコード載せる(予定)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バーンダウンチャートを送信するSlackBotの作成

概要

近年、スクラムを導入した開発チームが増えつつあります。
スクラムチームの質を向上させるため、プロジェクト管理者だけではなくて、チームメンバーにも開発状況を把握しやすくすることが重要だと思います。

今回は、JIRA REST APIを使って、最新のスプリント情報を取得すると、バーンダウンチャートを生成して、日常的にSlackのChannelに送信するBotを紹介します。

要求仕様

node 12.20.1
npm 6.14.10

JIRA REST API テスト

まずはChrome API Tester Toolを使って、JIRA Agile REST APIのテストを行う。(スクラムボードのみ支持)
METHOD: GET
URL: https://{your-domain}.atlassian.net/rest/agile/1.0/board/{boardId}/sprint/{sprintId}/issue

こんな感じのレスポンスで問題ないかと思います。
jira-api-test.png

SlackBot を作成する

Botの作り方はワークスペースで利用するボットの作成を参照してください。
現在、元宵節を迎えることになりますので、MagicalYuanxiao(中国語:神奇小元宵)と名付けました。
slack-app.png
次に、アプリにスコープを設定するように(chat:writefiles:write)の権限を追加しました。
設定後:
image.png
さらに、作成済みのAppを送信したいChannelに追加しました。
addapp.png

SlackBot 送信テスト

HTTPクライアントのインストール

$ npm install axios

メッセージを送信する

sendMsg.js
// test file
// Run "node sendMsg.js" to test access slack token
// If send message to channel is successful, the token is OK
const axios = require("axios");
const MSG_URL = "https://slack.com/api/chat.postMessage";
const slackToken = "Your slack token"; // *Slack Bot User OAuth Access Token*

async function run() {
  const res = await axios.post(
    MSG_URL,
    {
      channel: "#test", // *send to target channel*
      text: "Hello, I am magical yuanxiao!", // message content
    },
    {
      headers: {
        authorization: `Bearer ${slackToken}`,
      },
    }
  );
  console.log("Done", res.data);
}

run().catch((err) => console.log(err));

実行コマンド

$ node sendMsg.js

テスト結果
image.png

SlackBot 設計と実装

ここからバーンダウンチャートを送信するSlackBot MagicalYuanxiaoのワークフローや実装部分を簡単に説明します。

ワークフロー

image.png

ライブラリの導入

・JIRAアクセス(https://www.npmjs.com/package/jira-client)
・ファイルとディレクトリの読み取りと書き込み(https://www.npmjs.com/package/fs)
・HTTPリクエスト(https://www.npmjs.com/package/request)
・時間データの操作(https://www.npmjs.com/package/moment)
・グラフ描画(https://github.com/SeanSobey/ChartjsNodeCanvas)
・環境変数の設定(https://www.npmjs.com/package/dotenv)

スプリントデータを保存する

app.js
// jira API options
const jira = new JiraApi({
  protocol: "https",
  host: jiraHost,
  username: jiraUsername,
  password: jiraPassword,
  apiVersion: "3",
  strictSSL: true,
});

// today's date
const YYYYMMDD = moment().format("YYYYMMDD");

// step1: get sprint info to generate json from jira
  let sprint = {};
  await jira
    .getSprintIssues(rapidViewId, sprintId)
    .then(function (issues) {
      sprint["id"] = issues.sprint.id; // スプリントID
      sprint["name"] = issues.sprint.name; // スプリント名
      sprint["goal"] = issues.sprint.goal ? issues.sprint.goal : 0; // 目標点数
      sprint["startDate"] = moment(issues.sprint.isoStartDate).format(
        "YYYYMMDD"
      ); // 開始日
      sprint["endDate"] = moment(issues.sprint.isoEndDate).format("YYYYMMDD"); // 終了日
      sprint["issuesPointSum"] = issues.contents.completedIssuesEstimateSum
        .value
        ? issues.contents.completedIssuesEstimateSum.value
        : 0 + issues.contents.issuesNotCompletedEstimateSum.value
        ? issues.contents.issuesNotCompletedEstimateSum.value
        : 0; // ストーリー点数合計
      sprint["notCompletedIssuesPointSum"] = issues.contents
        .issuesNotCompletedEstimateSum.value
        ? issues.contents.issuesNotCompletedEstimateSum.value
        : 0; // 未完了ストーリー点数合計(include: todo, doing, review...)
    })
    .catch(function (err) {
      console.error(err);
    });

  // json data to be written
  let jsonData = {
    code: 0,
    data: sprint,
    updateDate: moment().format("YYYY/MM/DD HH:mm:ss"),
    msg: "success",
  };
  // format json
  let text = JSON.stringify(jsonData);
  // params: directory and file name
  let file = path.join("./output/", YYYYMMDD + "_sprint_data.json");
  // write into json
  await fs.writeFile(file, text, function (err) {
    if (err) {
      console.log(err);
    } else {
      console.log("File was successfully created: " + file);
    }
  });

チャートデータを用意する

app.js
 // step2: get data for line chart
  var startAndEndDateDiff = moment(sprint.endDate).diff(
    moment(sprint.startDate),
    "days"
  );
  // X axis labels for line chart
  var xLabels = [];
  for (let i = 0; i <= startAndEndDateDiff; i++) {
    xLabels[i] = moment(sprint.startDate).add(i, "days").format("MM/DD");
  }

  // Y axis values for line chart
  var yValues = [];
  // from 0 to date difference(from today to startDate)
  var dateDifferenceArray = [];
  for (let i = 0; i <= moment().diff(moment(sprint.startDate), "days"); i++) {
    dateDifferenceArray.push(i);
  }
  var jsonArray = dateDifferenceArray.reverse().map(getJsonAsync);
  await Promise.all(jsonArray)
    .then(function (jsonData) {
      // 本スプリントの日別残ポイントデータを埋め込む
      yValues = jsonData.map((s) => s.data.notCompletedIssuesPointSum);
    })
    .catch(function (err) {
      console.error(err);
    });

  // guideline Y axis values
  var guidelineValues = [];
  for (let i = 0; i <= startAndEndDateDiff; i++) {
    guidelineValues.push(
      sprint.goal - Math.floor((sprint.goal * i) / startAndEndDateDiff)
    );
  }

バーンダウンチャートを作成する

app.js
 // step3: create burn down chart image
  const height = 400;
  const width = 700;
  const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height });
  (async () => {
    const configuration = {
      type: "line",
      data: {
        labels: xLabels,
        datasets: [
          {
            label: "Story Points remaining",
            borderColor: "rgba(255, 100, 100, 1)",
            data: yValues,
            fill: false,
            tension: 0, // straight line
          },
          {
            label: "Guideline",
            borderColor: "rgba(122, 122, 122, 1)",
            borderDash: [10, 3], // dotted line
            data: guidelineValues,
            fill: false,
            borderWidth: 1,
            tension: 0,
          },
        ],
      },
      options: {
        scales: {
          yAxes: [
            {
              ticks: {
                beginAtZero: true, // set Y min value to 0
              },
            },
          ],
        },
        elements: {
          point: {
            radius: 0, // do not show points
          },
        },
        title: {
          display: true,
          fontSize: 16,
          text:
            sprint.name +
            "(" +
            moment(sprint.startDate).format("MM/DD") +
            "" +
            moment(sprint.endDate).format("MM/DD") +
            ")", // chart title
        },
      },
    };
    const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration);
    const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
    await fs.writeFile(
      "./output/" + YYYYMMDD + "_burn_down_chart.png",
      base64Data,
      "base64",
      function (err) {
        if (err) {
          console.log(err);
        }
      }
    );
  })();

Slackにバーンダウンチャートを送信する

app.js
// step4: upload created chart to slack
  request.post(
    {
      url: UPLOAD_URL,
      formData: {
        file: fs.createReadStream(
          "./output/" + YYYYMMDD + "_burn_down_chart.png"
        ),
        token: slackToken,
        filetype: "png",
        filename: YYYYMMDD + "_burn_down_chart.png",
        channels: slackChannel, // send to XXX channel
        title: YYYYMMDD + "_burn_down_chart.png", // show this name in slack
      },
    },
    function (error, response, body) {
      if (error) {
        console.log(error);
      } else {
        console.log("Send burn down chart to slack.");
      }
    }
  );

実行結果

実行コマンド

$ node app.js

Slackのスクリーンショット
burndown.png
以上でnodejsの実装部分は完成です。

あとは定時ジョブ行えば日常的にSlackにpostする予定です。

Source Code (GitHub)

https://github.com/peepa857/magical-yuanxiao

参照サイト

Python編記事-Slackに定期的にバーンダウンチャートを投稿するBotの作成方法
Jira Software でバーンダウンチャートを使用する方法
JIRA REST APIで課題情報を取得(jira-client-npmを使用)
Working with the Slack API in Node.js
node.jsでファイルの入出力操作
Chart.jsによるチャート作成

検討事項

・ガイドラインについて、祝日と休日の判定処理
・JSON読み取りについて、本スプリント先日のデータが存在していないエラーの対処方法(期間中のみ)
・カスタム看板に対して戻り値が正しいかどうか検証する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む