type holyshared = Engineer<mixed>

技術的なことなど色々

Victoryのアップグレード

f:id:holyshared:20210123195641p:plain
Victory

個人で運用しているアプリケーションでグラフライブラリのVictoryを使っていてバージョンが古くなっていたので32.3.x から35.4.xにあげてみました。
大きく仕様に変化はなかったようで、型エラーを直していくだけで対応が完了しました。

VictoryLine

線のスタイルやラベルのコールバックの仕様が変わっている様です。

  • strokeDasharray が配列ではなくなっていて、型としては StringOrNumberOrCallback に変更されている。
  • labels のコールバックの値がデータではなく、lineのオブジェクトになってる。
import { VictoryLine } from "victory";

const dottedLine = {
  data: {
    strokeDasharray: 1, // StringOrNumberOrCallbackになったので[1, 1] みたいなしてはできない
  }
};
const barData = [{ x: 1, y: 1 }];

// 前の様にしたければdatumに置き換える
const lineLabel = (value: { datum: { x: number; y: number; } }): number | null => {
  return (value.datum.y > 0 ? value.datum.y : null);
};

<VictoryLine style={dottedLine} data={barData} labels={lineLabel} />

VictoryChart

domainPaddingの型指定が変わった様です。

export type PaddingType = number | [number, number];
export type DomainPaddingPropType =
  | PaddingType
  | {
      x?: PaddingType;
      y?: PaddingType;
    };

値をリテラルで指定していたので、[0, 15] みたいな指定が number[] で解釈されていました。

import { VictoryChart, VictoryBar } from "victory";

const domainPadding = {
  x: [15, 15] as [number, number], // 型指定はDomainPaddingPropTypeなので
  y: [0, 15] as [number, number],
};
const barData = [{ x: 1, y; 1 }];

<VictoryChart domainPadding={domainPadding}>
  <VictoryBar data={barData} />
</VictoryChart>

VictoryAxis

grid.strokeのコールバック関数の引数が変わった様です。
strokeのコールバックはtickの値が引数になっていたので、args: CallbackArgsに置き換えました。

import { CallbackArgs } from "victory-core";
import { VictoryAxis } from "victory";

const valueTicks = [0, 5, 10, 15, 20];

const valueAxisStyle = {
  grid: {
    stroke: (args: CallbackArgs) => (args.tick === 0 ? colorTransparent : axisAndTickColor),  // 前はtick: numberだった
  },
};

<VictoryAxis dependentAxis tickValues={valueTicks} style={valueAxisStyle} />

GitHub ActionsでMongoDBのレプリカセット設定を行う

GitHub ActionsでMongoDBのレプリカセットを使用してテストを実行する方法を調べました。
docker-composeを使ってもよかったのですが、mongodb-github-actionがあったので試しに使用してみました。

パラメータとしては下記の2つのものが指定できるようです。

  • MongoDBのバージョン
  • レプリカセットの名前

バージョンとレプリカセットの名前しか指定できないので、接続URLにユーザー名などが指定されている場合接続できないので注意が必要です。

試したURLを載せておきます。

MONGO_URL=mongodb://localhost:27017/test?replicaSet=replset

docker-composeなどでローカルでコンテナを起動している場合はMONGO_INITDB_ROOT_USERNAMEMONGO_INITDB_ROOT_PASSWORDMONGO_INITDB_DATABASEなどを設定ファイルで指定することが多いので、これはこれでサクッと試せるのでこれはいいかなと思いました。

ただし、レプリカセットのメンバーは1つだけなので、複数ある場合のテストがある場合は今のところ使えなさそうではあります。

最後に.github/workflows/node.js.ymlの設定を載せておきます。

name: Node.js CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build:

    runs-on: ubuntu-20.04

    strategy:
      matrix:
        node-version: [14.8]
        mongodb-version: [4.2]

    steps:
    - name: Git checkout
      uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Start MongoDB
      uses: supercharge/mongodb-github-action@1.3.0
      with:
        mongodb-version: ${{ matrix.mongodb-version }}
        mongodb-replica-set: replset

    - run: yarn --frozen-lockfile
    - run: yarn build
    - run: yarn test

2020年の靴の着用回数を集計した

2019年から引き続き履いているもの、買い足したもの、手放したものを集計対象にしています。

2020年は900件を投稿していたようです。
一つの靴について、だいたい年間で20回ぐらいの着用、サンダルのみ使用頻度が高いので、倍ぐらいの着用回数になっています。

靴は1年間で16足増えて、9足手放し、合計で36足になりました。

2020年に関しては、スニーカーが増えています。
BROTHER BRIDGEのBERLINを買ってから、スニーカーもいいなと思ったので、日本のメーカーのものをいくつか買ってみました。

また、2020年はコロナ禍の影響もあり自宅で仕事することが増え他ことにより運動することが減ったので散歩をすることが増えました。
大体毎日6〜7km位歩いているようです。

そのおかげが全体的な靴の着用回数が増えたようです。
2021年は30足位に抑えようと思います。

No. ブランド モデル 2020年までの着用回数 2020年の着用回数
1 Tokyo Sandals DOBULE MONK SANDAL 182 92
2 Tokyo Sandals Enginier Slip on 124 88
3 BROTHER BRIDGE JAMES 57 28
4 Rolling dub trio ROOTS 60 27
5 BROTHER BRIDGE BERLIN 27 27
6 RAMSEY 442 MILITARY CAP TOE OXFORD 57 26
7 Redwing Irish Setter 56 26
8 Crockett&Jones CONISTON 29 24
9 BROTHER BRIDGE McCLOUD 53 24
10 BROTHER BRIDGE Morgan 28 23
11 Redwing 1930s Sport Boot 30 23
12 Sanders Military Apron Derby Boots 33 22
13 SLOW WEAR LION OB-8593GT 栃木レザープレーンミッドブーツ 50 22
14 SLOW WEAR LION OB-8593G オイルドレザープレーンMIDブーツ 48 22
15 Redwing Irish Setter 51 22
16 Nicks 6inch HotShot 50 21
17 JOHN LOFGREN M-43 SERVICE SHOES 21 21
18 Wesco Custom Jobmaster 38LTT 28 20
19 Pistorelo Norwogian Split 43 20
20 Chippewa 6inch Service Boots 49 20
21 Dry Bones CYPRESS 20 20
22 NPS shoes CAMERON ESSENTIAL 20 20
23 Scotch Grain Spider 28 19
24 BROTHER BRIDGE Harry 19 19
25 Berwick Chukka boots 20 18
26 Berwick Wing tip 34 18
27 SLOW WEAR LION OB-8208G オイルドレザーサイドゴアブーツ 47 18
28 MoonStar HI BASKET W 17 17
29 Chippewa Suburban 35 16
30 Chippewa Bridgemen 40 15
31 ASAHI M022 14 14
32 Chippewa 6inch Boots Reverse Black Odessa 14 14
33 KATSUYA TOKUNAGA BOYS 14 14
34 Timberland GARRISON TRAIL GORE-TEX HIKING SHOES 14 14
35 Ballband Jackie 12 12
36 Onitsuka Tiger MEXICO 66 12 12
37 New Balance Postal 706V2 11 11
38 NPS shoes LAW 7 7
39 ASAHI M020 6 6
40 SOLOVAIR Black Grain 6 Eye Derby Boot 5 5
41 Chippewa 6inch Service Boots 33 5
42 Rolling dub trio HunterⅡ 4 4
43 Berwick Chukka boots 30 3
44 Chippewa Bridgemen 27 1
45 Hunter Chelsea Boots 30 0

Oura Ring買って見た

Oura Ring

ずっと気になっていたので買って見ました。
サイズキット付きで購入し、サイズを確認してオンラインでサイズ指定するだけでリングを届けてくれます。

届いたあとは充電して、つけてるだけでメトリクスが取れます。
気にしているのは睡眠のスコアと消費カロリーです。

自宅で仕事をすることが多くなったので、運動不足解消の為にウォーキングをしているのですが、毎日目標の距離と消費カロリーを提示してくれるので、どのくらい歩けばいいのか目安になっていいですね。

また睡眠スコアがよくて、熟睡している時間とか細かく見れます。
スコアの内訳をみて、熟睡時間が長かったから目覚めがいいのかなとか、寝返りが多かったからスコア低いんだなと色々わかります。

体調のコンディションがわかることで、寝る時間をコントロールしやすくなったので満足度はすごい高いです。
まだ使ったことない機能もあるので、色々試して行きたいです。

Microsoft Graphでユーザーのアバター画像を取得する

Microsoft Graphにはv1.0とbetaのエンドポイントがあります。
どちらにもユーザーのアバターを取得するAPIがあります。  

しかし、v1.0とbetaで取得できる場合とそうでない場合があるようです。

https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-1.0 https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-beta

v1.0とbetaで同じユーザーのアバターを取得してみる

アバターはサイズを指定して取得をできるので、必要なサイズのものを取得します。
データはバイナリで受け取るので、適当にBufferなどに変えてファイルに書き出すかData URI schemeに置き換えて、DBに格納したりすることができます。

ここではData URI schemeに変えるコードを載せておきます。

import fetch from 'node-fetch';

const v1Endpoint = 'https://graph.microsoft.com/v1.0';
const betaEndpoint = 'https://graph.microsoft.com/beta';

type Endpoint = "v1.0" | "beta";
type AvatarSize = "48x48" | "64x64" | "96x96";

type Avatar = {
  mimeType: string;
  url: string;
};

const userAvatar = (endpointType: Endpoint, size: AvatarSize) => async (userId: string): Promise<Avatar | undefined> => {
  const options = {
    method: "get",
    headers: {
      Authorization: `Bearer ${process.env.ACCESS_TOKEN}`
    }
  };

  const endpoint = endpointType === 'v1.0' ? v1Endpoint : betaEndpoint;
  const response = await fetch(`${endpoint}/users/${userId}/photos/${size}/$value`, options);

  if (!response.ok) {
    const failedBody = await response.json();
    console.log(failedBody);
    return;
  }

  const mimeType = response.headers.get('content-type') as string;

  const buffer = await response.buffer();
  const data = buffer.toString('base64');

  const url = `data:${mimeType};base64,${data}`;

  return {
    mimeType,
    url
  };
};

const v1Avatar = userAvatar("v1.0", "48x48");
const betaAvatar = userAvatar("beta", "48x48");

const main = async (userId: string) => {
  const v1 = await v1Avatar(userId);
  const beta = await betaAvatar(userId);
  console.log(v1);
  console.log(beta);
};

const userId = process.argv[2];

main(userId).then(() => {
  console.log('done');
}).catch(err => {
  console.log(err.stack);
});

実行するにはアクセストークンとユーザーIDを指定します。

ACCESS_TOKEN=[アクセストークン] \
node avatar.js [ユーザーID]

v1.0では一部のユーザーのアバターが取得できない場合がある

一部のユーザーではアバターを設定しているのにもかかわらず、アバターが取得できないユーザーがいました。
そのユーザーの場合はMailboxNotEnabledForRESTAPIがエラー本文に含まれていました。  

{
  error: {
    code: 'MailboxNotEnabledForRESTAPI',
    message: 'REST API is not yet supported for this mailbox.',
    innerError: {
      date: '2020-08-25T10:53:21',
      'request-id': xxxxxxxxxxxx
    }
  }
}

betaで問題が発生しないのは一部のAPIの制限がなくなったものと思われます。
OfficeなどのAPIがMicrosoft Graphに統合されているはずなのですが、まだ完全には終わっていないのかもしれないです。

Azure Bot ServiceのBot Framework SDKのkoa対応

最近はBot Framework v4を利用して、Botアプリケーションを作成しています。
Botアプリケーションを作成するには、SDKを利用するのですが、expressrestifyくらいしかWebアプリケーションのフレームワークが対応してないような気がします。

koaの場合

例えば、BotFrameworkAdapter.processActivity の要求にレスポンスオブジェクトを渡す必要があるのですが、 koacontext.response を渡すと status、send、end あたりのメソッドがない為に、ランタイムエラーが発生します。

これは、expressrestifyのレスポンスオブジェクトのインターフェースをBotFrameworkAdapterが期待しているからです。

また、koaの実装の場合、ミドルウェアを一通り処理した後に最終的な応答を返すので、サンプルでよくある実装をすると応答を正常に返せません。

// よくみるサンプルコード
// express/restifyの場合
app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await bot.run(context);
  });
});

Bot Emulatorを使用してデバッグすると、404を返した後に200を返したりします。

f:id:holyshared:20200802185533p:plain
Emulatorのログ

// 素直にkoaでサンプルのコードを実装した例
// routerはkoa-routerのインスタンス
router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const { request: req, response: res } = ctx;
  adaptor.processActivity(req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

この現象の理由は、processActivityをawaitしてない為、コールバック関数を抜けた後に、404を直ぐに返し、その後にBotのメッセージ応答が正常に終了し、200などの応答を返す為だと思います。

express/restifyの場合はawaitしなくてもレスポンスオブジェクトのendを実行したタイミングで応答が完了するので問題ないのですが、koaの場合はそうではなく、最終的な応答を返すのがフレームワーク側 なのでうまく行きません。

サンプルのコードとかだと、ただのコールバック関数で処理しているだけに見えたので若干困りました。

最終的な対応

原因がわかったので、適当なexpress/restifyのレスポンスオブジェクトを渡しつつ、processActivityをawaitして対応することにしました。

スマートな解決方法ではないですが、テキストメッセージをユーザーに送るくらいだったらこれで良さそうです。
また、WebResponseのインターフェース仕様を読む限り、end、send、statusだけ実装されてれば良いぽいのでこれであってそうではあります。

import Koa from 'koa';
import Router, { RouterContext } from 'koa-router';
import { BotFrameworkAdapter, TurnContext, WebResponse } from 'botbuilder';
import { Bot } from './bot';

const app = new Koa();
const router = new Router();

const adaptor = new BotFrameworkAdapter({
  appId: process.env.BOT_APP_CLIENT_ID,
  appPassword: process.env.BOT_APP_CLIENT_PASSWORD
});

const bot = new Bot();

const createWebResponse = (ctx: RouterContext<{}, {}>): WebResponse => ({
  status(code: number): void {
    ctx.status = code;
  },
  send(body: string): void {
    ctx.body = body;
  },
  end(): void {
  }
});

router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const res = createWebResponse(ctx);
  await adaptor.processActivity(ctx.req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

app.use(router.routes())
  .use(router.allowedMethods())
  .listen(3000);

HerokuのmLabからMongoDB Atlasへ

自分が運用している365shoes.styleはHerokuを使用しています。 そして、mLabアドオンを使用していました。

mLabアドオンはアナウンスがあったように利用できなくなります。
なので、何かしらの方法で移行する必要がありました。

移行先は一番移行が楽そうな、MongoDB Atlasを選びました。
MongoDB AtlasはmLabからの移行をサポートしています。

https://docs.mlab.com/how-to-migrate-to-atlas/#complete-migration-wizard-for-specific-deployment

ステージング環境の移行

ステージング環境のMongoDBをMongoDB Atlas側でリンクさせて、手順通りに進めました。
クラスタをタイプを指定し、マイグレーションを実行するとmLabのデータをまるっとMongoDB Atlasの環境に移してくれます。

データの内容を確認した後に、アプリケーションの環境変数を変えて、新しい方のMongoDBのデータベースをアクセスしているか確認します。
接続できるかはマイグレーションのステップで、接続確認をするステップがあるので、そこで接続できれば特に問題ないような気がします。

本番環境の移行

ステージング環境の移行が済んだので、次は本番環境なのですが、ちょっと手順が代わります。
MongoDB Atlasからは一つのDBしかリンクできないようで、一旦ステージングとのリンクを切って、本番用の方にリンクし直す必要があります。

「Disconnect from mLab」というのがあるので、それをクリックすればOKです。

f:id:holyshared:20200727005431p:plain
MongoDB Atlasの管理画面

そのあとは、ステージング環境と同じように移行するだけなのですが、下記の2つの問題がありました。

  1. ステージング環境と同じプロジェクトにしたら、ユーザー名がステージング環境のユーザー名になって、データが正常にインポートできなかった。
  2. 移行後のユーザーの権限がreadだけになってしまった。

1に関してはユーザー名が本番環境のやつと違うけど、大丈夫なのかなと思いつつ、データインポートの処理を実行したら、データがインポートされないので、試しに別の新しいプロジェクトを作成して、試したところ正常に完了しました。

2に関しては、マイグレーションのステップを途中までやっていて、最後まで完了させなかったのが問題のようです。 データ移行まで終わって、安心したのか最後らへんの1、2ステップを見落としていたような気がします。
途中でタブ閉じちゃったのがよくなかったと思います。

ステップとしては移行元の書き込みを停止させるためにユーザーの権限を読み取り権限だけにするようなプロセスになっているようです。
https://docs.mlab.com/how-to-migrate-to-atlas/#importing-from-source-to-target

移行作業のまとめ

ウィザード形式で移行が進むので手順は楽でとてもよかったです。
その代わり、現状どういう状態なのかを確認して、各ステップでどうならなければならないといけないかを意識して作業しないと見落としがでそうです。

通常では移行作業自体は、手順を整理してこういう細かいことをやるのですが、あらかじめ手順が誰かに用意されている場合、意識することは少ないので気をつけたいです。