masayuki-0319 / scrap

0 stars 0 forks source link

[Study] SDK 作りたい #22

Open masayuki-0319 opened 3 years ago

masayuki-0319 commented 3 years ago

Background

JS 用の firebase SDK がいつの間にか V9 で構造が変わってた。 https://github.com/firebase/firebase-js-sdk

モジュラー SDK と呼ばれて、package/* に firebase の機能単位で library が存在している。 https://firebase.google.com/docs/web/modular-upgrade https://zenn.dev/hiro__dev/articles/605161cd5a7875

lerna って library を採用して実現しているので、試してみる。

Goals

References

official

SDK 実装例 ( lerna )

SDK 実装例

実装イメージに近い https://github.com/dropbox/dropbox-sdk-js https://github.com/facebook/facebook-nodejs-business-sdk https://github.com/spree/spree-storefront-api-v2-js-sdk

jest https://github.com/contentful/contentful-sdk-core https://github.com/meilisearch/meilisearch-js

node-fetch https://github.com/entur/sdk

others https://github.com/dilame/instagram-private-api https://github.com/paypal/Checkout-NodeJS-SDK https://github.com/line/line-bot-sdk-nodejs https://github.com/square/square-nodejs-sdk https://github.com/Vonage/vonage-node-sdk https://github.com/plhery/node-twitter-api-v2 https://github.com/onelogin/onelogin-node-sdk https://github.com/heremaps/here-data-sdk-typescript https://github.com/strapi/strapi-sdk-javascript

hands-on

masayuki-0319 commented 3 years ago

題材

Speedrun.com https://github.com/speedruncomorg/api

1年前に Dart で触ってた https://github.com/masayuki-0319/dart-speedrun-api

他の実装例 https://github.com/jeremybanks/speedruns https://github.com/megadrive/node-speedrunapi https://github.com/SwitchbladeBot/node-speedrun

masayuki-0319 commented 3 years ago

SDK: プロジェクト生成

ディレクトリイメージ

作業手順

Express 初期設定

SDK 初期設定

masayuki-0319 commented 3 years ago

Express 初期設定

https://qiita.com/isihigameKoudai/items/4b790b5b2256dec27d1f

初期ファイル生成

yarn init -y
mkdir dist src webpack

typescript 設定

yarn add -D typescript ts-node nodemon
yarn tsc --init

Linter 設定

コードは元記事参照

yarn add -D eslint eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier
touch .eslintrc

Express 設定

yarn add express
yarn add -D @types/express body-parser

コードは元記事参照

touch src/index.ts

開発用サーバ設定

コードは元記事参照

touch nodemon.json

Webpack 設定

yarn add -D webpack webpack-cli webpack-merge  \
  webpack-node-externals ts-loader \
  @types/webpack-dev-server @types/webpack-node-externals
touch webpack/base.config.ts webpack/dev.config.ts webpack/prod.config.ts
webpack/base.config.ts
import { Configuration } from 'webpack';
import path from 'path';
import webpackNodeExternals from "webpack-node-externals"

const BUILD_ROOT = path.join(__dirname, "../dist");
const SRC_ROOT = path.join(__dirname, "../src");

export const baseConfig: Configuration = {
  context: SRC_ROOT,
  entry: path.resolve("src", "index.ts"),
  externals: [webpackNodeExternals()],
  output: {
    filename: "server.js",
    path: BUILD_ROOT
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: "ts-loader",
        options: {
          configFile: "tsconfig.json"
        }
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".js", ".json"],
    alias: {
      "@": path.join(__dirname, "/src/")
    }
  }
};
webpack/dev.config.ts

~TS にしたいけど Issue 未解決らしい~ ~https://shunbiboroku.com/post/webpack-config-ts-error~

@type/webpack-dev-server だけで解決した。 https://github.com/DefinitelyTyped/DefinitelyTyped/issues/27570

contentBase は、static に置き換わった。 https://ja.stackoverflow.com/questions/82189/webpack%E3%81%AEdevserver-%E3%81%AF%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E7%AB%8B%E3%81%A1%E4%B8%8A%E3%81%92%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%97%E3%82%87%E3%81%86%E3%81%8B

import { Configuration } from 'webpack';
import { merge } from "webpack-merge"

import { baseConfig } from "./base.config"

const devConfig: Configuration = {
  mode: "development",
  devtool: "inline-source-map",
  devServer: {
    host: "0.0.0.0",
    port: 3000,
    static: {
      directory: "dist",
    }
  }
};

const config = merge(baseConfig, devConfig);

export default config;
webpack/prod.config.ts
import { Configuration } from 'webpack';
import { merge } from "webpack-merge"

import { baseConfig } from "./base.config"

const prodConfig: Configuration = {
  mode: "production"
};

const config = merge(baseConfig, prodConfig);

export default config;
package.json の scripts 設定
{{
  "scripts": {
    "dev": "nodemon -L",
    "build": "webpack --config ./webpack/prod.config.ts",
    "start": "npm run build && node dist/server.js"
}}
git 設定
git init
touch .gitignore
# .gitignore
dist
node_modules
masayuki-0319 commented 3 years ago

Express logger 設定

Express は default logger として morgan を採用している。 しかし、高機能な他ライブラリと連携するケースが存在する様子。 https://www.npmtrends.com/morgan-vs-pino-vs-winston-vs-roarr

ここでは morgan + winston で設定してみる。

logger ライブラリ

https://github.com/expressjs/morgan https://github.com/winstonjs/winston https://github.com/pinojs/pino https://github.com/gajus/roarr

参考 URL

記事

実装

ライブラリ導入・設定

yarn add morgan winston
yarn add -D @types/morgan

ディレクトリ構成 スクリーンショット 2021-09-25 17 50 14

winston

import path from 'path';
import winston, { LoggerOptions } from 'winston';

const LOG_DIR = path.join(__dirname, '../../log');
const APP_ENV = process.env.NODE_ENV || 'development';

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
};

const level = () => {
  const isDevelopment = APP_ENV === 'development';

  return isDevelopment ? 'debug' : 'http';
};

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf((log) => `${log.timestamp},${log.level},${log.message}`)
);

const APPLICATION_LOGFILE = `${LOG_DIR}/${APP_ENV}.log`;
const ERROR_LOGFILE = `${LOG_DIR}/${APP_ENV}.error.log`;
const transports = [
  new winston.transports.Console(),
  new winston.transports.File({
    filename: ERROR_LOGFILE,
    level: 'error',
  }),
  new winston.transports.File({ filename: APPLICATION_LOGFILE }),
];

const loggerOptions: LoggerOptions = {
  level: level(),
  levels,
  format,
  transports,
};

const levelColorOption = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
};

winston.addColors(levelColorOption);
const logger = winston.createLogger(loggerOptions);

export { logger };

morgan

import morgan, { StreamOptions } from 'morgan';

import { logger } from './winston';

const stream: StreamOptions = {
  write: (message) => {
    logger.http(message);
  },
};

const skip = () => {
  const env = process.env.NODE_ENV || 'development';

  return env !== 'development';
};

const format = ':method :url :status :res[content-length] - :response-time ms';
const requestLogger = morgan(format, {
  stream,
  skip,
});

export default requestLogger;

config ファイル

import { Express } from 'express';
import { json, urlencoded } from 'body-parser';

import { requestLogger } from './logger/morgan';

const config = (server: Express) => {
  server.use(json());
  server.use(urlencoded({ extended: false }));
  server.use(requestLogger);
};

export default config;
masayuki-0319 commented 3 years ago

Express デバッグ環境の構築

初心者にとってデバッグ環境は重要。 自分の書いたコードがどのように実行されているのか、特定地点における scope の状態がイメージできないため。

なので、Rails の byebug-pry のように step-by-step でデバッグできるようにしたい。

参考 URL

https://qiita.com/nemutas/items/b6dcaea6aab9f64ecbe4#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0 https://code.visualstudio.com/docs/typescript/typescript-debugging

実装

mkdir .vscode
touch launch.json

launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Node: Nodemon",
      "processId": "${command:PickProcess}",
      "restart": true,
      "protocol": "inspector"
    }
  ]
}

デバッグモード起動

F5 ボタン押下するだけで起動する。 赤丸のブレークポイントで変数等を確認できる。

スクリーンショット 2021-09-25 18 50 08

masayuki-0319 commented 3 years ago

Express ディレクトリ分割

プロジェクトの構造がディレクトリの外観だけで理解できると素敵な気がする。 Rails に寄せても十分だけど、Express 特有の考え方あれば知りたい。

参考 URL

https://qiita.com/baby-degu/items/f1489dd94becd46ab523 https://qiita.com/nkjm/items/2016e331f74f1b8ab465 https://memo.sugyan.com/entry/20120110/1326197416 https://qiita.com/_takwat/items/558e721516a88071e962 https://teratail.com/questions/1409 https://swallow-incubate.com/archives/blog/20190425 https://t-yng.jp/post/consideration-typescript-monorepo

実装

SDK テスト用のモックプロジェクトなので、気にしないことにした。

masayuki-0319 commented 3 years ago

SDK プロジェクト生成

完成イメージは notion SDK https://github.com/makenotion/notion-sdk-js

参考 URL

npm モジュールの基本 https://qiita.com/TsutomuNakamura/items/f943e0490d509f128ae2

TS で npm モジュール作成 https://zenn.dev/arark/articles/cc58ba9bb79d80ec9cd3 https://infltech.com/articles/Qd8UZv https://qiita.com/i-tanaka730/items/c85daa3ee2dcde9bd728

npm 非公開モジュールを自分用で利用 https://final.hateblo.jp/entry/nodejs-publish-library

実装

npm モジュール雛形

mkdir speedrun-sdk-js
cd speedrun-sdk-js
yarn init -y
yarn add -D typescript
yarn tsc --init --target es6 --module esnext --declaration true --outDir ./build --rootDir ./src

.npmignore

/node_modules
/src
tsconfig.json

package.json

{
  "name": "speedrun-sdk-js",
  "version": "0.0.1",
  "main": "./build/index.js",
  "types": "./build/index.d.ts",
  "publishConfig": {
    "access": "public"
  },
  "type": "module",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^4.4.3"
  }
}

テスト導入 ( jest )

https://github.com/facebook/jest

https://qiita.com/mima_ita/items/558ec8cee2c0e1005ffd https://jestjs.io/docs/getting-started https://sbfl.net/blog/2019/01/20/javascript-unittest/

install

yarn add -D jest @types/jest
yarn run jest --init

yarn add -D babel-jest @babel/core @babel/preset-env  @babel/preset-typescript

babel.config.cjs 作成

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
};

test作成

// test/module.test.ts

import { IamExported } from "../src/module";

test("IamExported returns greeting", () => {
  expect(
    IamExported("arark")
  ).toContain("Hello, arark!!");
});

rootDir エラー対策 root directory に test を配置したいため、エラーが発生した。 https://qiita.com/masato_makino/items/bf640a253d56b708fe0b

{
  "compilerOptions": {...},
  "include": [
    "src"
  ],
  "exclude": [
    "build"
  ]
}

jest.config.js

/*
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/configuration
 */

export default {
  clearMocks: true,
};

git 設定 省略

masayuki-0319 commented 3 years ago

問題

開発環境において nodemon がファイルの変更を検知して自動更新してくれない

実現したいこと

上記の通り、自動更新できるようにしたい

解決策

ext が不足してた様子 https://chaika.hatenablog.com/entry/2020/09/28/083000

nodemon.json

{
  "restartable": "rs",
  "env": {
    "NODE_ENV": "development"
  },
  "ext": "ts, js, json",
  "watch": [
    "src"
  ],
  "ignore": [
    "tests/**/*.ts"
  ],
  "exec": "ts-node ./src/index.ts"
}
masayuki-0319 commented 3 years ago

SDK: 初期開発

第一歩として、1つの Endpoint に着目して開発してみる

作業手順

準備
実装

参考 URL

modular SDK の代表格 https://github.com/firebase/firebase-js-sdk https://github.com/aws/aws-sdk-js-v3

SDK の粒度的にかなり参考にできそう https://github.com/bizon/selling-partner-api-sdk https://github.com/contentful/contentful-sdk-core https://github.com/cosmos/cosmos-sdk https://github.com/onelogin/onelogin-node-sdk

modular SDK 使用例 https://firebase.google.com/docs/auth/web/start#web-version-9 https://zenn.dev/hiro__dev/articles/605161cd5a7875

response の型定義 https://github.com/SlackAPI/node-slack-sdk

なぜ modular SDK が良いのか 以前からの課題として firebase の bundle size が巨大なため、通信が遅いユーザにとって UX が悪くなるらしい。 https://dev.to/chroline/why-the-new-firebase-web-v9-modular-sdk-is-a-game-changer-nph

masayuki-0319 commented 3 years ago

問題: test のディレクトリをどうすべきか

案1: テスト対象 module と同じディレクトリに設置

案2: test ディレクトリに全て設定

参考 URL

private function もテストしたい場合 module の振る舞いを維持できれば良いと思うため、 export しない function は流石にテストの必要性が低い気もする。 また、同じファイルに https://zenn.dev/ptpadan/articles/jest-same-test-file

しかし、テスト対象を近いディレクトリに設置するのは、非常に共感できる。 https://github.com/YasushiKobayashi/samples/pull/59/files#diff-921972e127dc76b6da29b456d7b53e73cc3965a32e30d4cd980df497abe041b0R27-R31

jest 公式が tests ディレクトリを紹介 https://jestjs.io/ja/docs/configuration https://qiita.com/hogesuke_1/items/8da7b63ff1d420b4253f https://golang.hateblo.jp/entry/2021/03/12/214501

結論

慣例及び jest のチュートリアルに従い、__tests__ ディレクトリを選択する。

個人的には、テスト対象と同じディレクトリに置きたい。 しかし、jest は初めて触れるため、基本に従う。

masayuki-0319 commented 3 years ago

問題: jest.config.js を typescript で設定したい。

参考 URL

https://jestjs.io/ja/docs/configuration https://github.com/facebook/jest/issues/11453#issuecomment-877653950

解決策

ts-node が必要

yarn add -D ts-node @types/node

tsconfig.json 編集

{
  "compilerOptions": {...},
+  "ts-node": {
+    "moduleTypes": {
+      "jest.config.ts": "cjs"
+    }
+  },
}
masayuki-0319 commented 3 years ago

問題: javascript における HTTP client は何が良いか

参考 URL

他にもライブラリあるけど、axios がデファクトなのかも https://nodejs.libhunt.com/axios-alternatives

結論

axios で良し

追伸

いや...node-fetch が押してる? SDK も node-fetch による実装が多い。 https://www.npmtrends.com/axios-vs-node-fetch

しかし、高機能なのはやはり axios https://blog.openreplay.com/fetch-vs-axios-which-is-the-best-library-for-making-http-requests

一部ライブラリでは、node-fetch を default として、option で設定可能にしてる事例もある。 後から node-fetch に切り替えられるようにコードを分離すること。

masayuki-0319 commented 3 years ago

問題: SDK の使用方法を考える

結論として modular SDK で進める。

調査

API 情報の管理

全ての path を型定義 https://github.com/firebase/firebase-js-sdk/blob/d3041d875a/packages/auth/src/api/index.ts

endpoint の振る舞いを定義 ( params, Request, Response ) https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/api/authentication/sign_up.ts

HTTP リクエスト管理

共通の HTTP Client https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/api/index.ts#L80

HTTP ライブラリの使用箇所 platform 毎にライブラリが異なるため、FetchProvider.fetch() で隠してる。 https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/api/index.ts#L116-L124

platform 毎に HTTP ライブラリを initialize してる。 firebase では内部的に node-fetch を使用してる。 https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/platform_node/index.ts#L33-L37

HTTP Client に必要な Config 情報

https://github.com/onelogin/onelogin-node-sdk/blob/master/lib/http_clients/onelogin_http_client.ts https://github.com/contentful/contentful-sdk-core/blob/master/src/create-http-client.ts https://github.com/bizon/selling-partner-api-sdk/blob/master/packages/auth/src/selling-partner-api-auth.ts

masayuki-0319 commented 3 years ago

問題: Jest の非同期テスト方法を調査

参考 URL

公式 https://jestjs.io/ja/docs/tutorial-async

実装例 外部 API https://tech.bitbank.cc/lets-test-by-jest/ https://zenn.dev/chida/articles/cec625e3b6aa7b https://medium.com/@lachlanmiller_52885/%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92jest%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88-e7f40fb6cffa

内部 API https://www.agent-grow.com/self20percent/2019/03/25/only-express-and-jest-testing/ https://dev.to/franciscomendes10866/testing-express-api-with-jest-and-supertest-3gf

解決策

公式ドキュメントが十分すぎる。

masayuki-0319 commented 3 years ago

実装 Endpoint 選定

https://github.com/speedruncomorg/api

対応

全てのベースとなる games を選定 https://github.com/speedruncomorg/api/blob/master/version1/games.md

masayuki-0319 commented 3 years ago

問題: json-schema って何?

確か API の response を定義するとか聞いたことある。 https://github.com/speedruncomorg/api/blob/master/version1/json-schema/definitions.json

参考 URL

http://json-schema.org/

https://tech.degica.com/ja/2015/10/16/json-schema-ja/ https://dev.classmethod.jp/articles/json-validation/ https://myenigma.hatenablog.com/entry/2019/05/12/195935 https://qiita.com/arumi8go/items/a9530cbd39ff545a7bbb

結論

自由に設定できてしまう json を定義するツール

OpenAPI と同じ目的 https://blog.stoplight.io/openapi-json-schema

Speedrun.com のリポジトリ見ると、ファイルも少ないので気にしなくてよさそう

masayuki-0319 commented 3 years ago

テスト用の response 調査

mock 用の json を手に入れる

参考 URL

games https://www.speedrun.com/api/v1/games

games/:id ( SMS ) https://www.speedrun.com/api/v1/games/v1pxjz68

※ 他にもあるが最初は2つのみで良い

解決策

...

masayuki-0319 commented 3 years ago

問題: SDK のテスト項目とは?

以下の通りで良いか?

参考 URL

Twitter 取得対象の object が必要な key の所有確認 https://github.com/plhery/node-twitter-api-v2/blob/master/test/tweet.v2.test.ts#L14-L38

楽天 リクエストの stub 用意して、 mock 用 JSON をレスポンスを設置して、 module の実行結果が、期待する JSON であるか否かをテストしてる。 これがいつものイメージに近い。 https://github.com/rakuten-ws/rws-ruby-sdk/blob/master/spec/rakuten_web_service/books/book_spec.rb

here map 楽天と同じ。 ただ、mock コードは分離してた方が好みだが、JS で用意するのが普通なのか? https://github.com/heremaps/here-data-sdk-typescript/blob/master/tests/functional/StreamLayerReadData.test.ts

DMM リクエストの path, query、コールバック処理のみ。 最低限のテストっぽい。 https://github.com/dmmlabo/dmm-js-sdk/blob/master/test/genre_spec.js

square sandbox にリクエスト投げてる...!リッチ! https://github.com/square/square-nodejs-sdk/blob/master/test/testClient.ts

解決策

参考 URL

正常系 https://github.com/dropbox/dropbox-sdk-js/blob/main/test/integration/user.js#L49-L64

異常系 https://github.com/dropbox/dropbox-sdk-js/blob/main/test/integration/user.js#L130-L154