tobb422のブログ

スタートアップエンジニアの奔走

分割しているswaggerの定義ファイルの変更を検知して、swagger-ui (Dockerコンテナ)で表示するまで

はじめに

業務で OpenAPI を利用しており、頑張って定義ファイルを書いているんですが、
エンドポイントが増えてくると1つのswagger.ymlでは管理が煩雑になってきます。
(いままでのプロジェクトが往々にしてそうなっていた)

そこで、今回は早めに手を打っておこうと思い、
以下の仕組みを作成したので、小ネタ記事としてまとめておきます。

  • swagger定義ファイルの分割管理
  • ファイル変更の検知 → 合成の仕組み
  • ファイル変更の監視を行いながら、swagger-ui で表示するコンテナの作成

swagger定義ファイルの分割管理

swagger-mergerというライブラリを利用することで実現しました。
以下のような構成でファイルを分割しています。

api-schema
├── components // schemaごとに定義ファイルを書きます
├── paths // パスごとに定義ファイルを書きます
├── root.yml // もととなるグローバルな定義を書きます

あとは、swagger-merger のドキュメントに沿って進めます。

paths:
  "/api/hoge":
    "$ref": paths/api/hoge.yml // 分割したファイルをこうして指定すればよい

あとは、コマンド実行で1つのswagger.ymlが完成です。

$ swagger-merger -i root.yml -o swagger.yml 

ファイル変更の検知 → 合成の仕組み

分割して管理できるようにはなりましたが、
ファイル変更するたびに毎回コマンド実行するのも面倒なので、自動化する仕組みを作ります。   今回は、node.js で作ってみました。
ファイル変更の検知に gaze というライブラリを利用して、自動化までやっていきたいと思います。

以下、作成したスクリプトです。

const gaze = require('gaze'); // 先程のライブラリ
const path = require('path');
const merger = require('swagger-merger');

const resolve_path = (dir) => path.join(__dirname, dir);

gaze(
  ['root.yml', 'paths/**/*.yml', 'components/**/*.yml'],
  { cwd: resolve_path('') },
  (_, watcher) => {
    watcher.on('all', () => {
      merger({
        input: resolve_path('root.yml'),
        output: resolve_path('swagger.yml'),
      })
      .then(() => console.log('merge!')) // マージしたときに叫びます
      .catch(err => console.error(err))
    });
  }
);

やっていることとしては、監視するファイル群を指定して、
変更があった場合の処理としてmergerを起動するようにしています。

"scripts": {
      "watch": "node file-merger.js"
}
$ npm run watch

上記コマンドで変更検知して合成する仕組みまでいけました。

ファイル変更の監視を行いながら、swagger-ui で表示するコンテナの作成

普段、docker-compose でコンテナ立ち上げて開発を進めているんですが、 せっかくなのでここまでの処理をまとめてコンテナ化してswagger-uiとして表示できるようにしてみました。
(npm run watchを別で実行しなきゃいけないのが面倒)

swaggerapi:swagger-ui というDockerイメージを使えば
定義ファイルさえあれば、簡単にswagger-uiを表示できるので、
このイメージを利用して作っていきます。
swagger-ui は、内部でnginxを立ち上げるようになっています。
今回は、定義ファイルの変更を検知する処理も同時に実行しておきたいので、node-foreman を利用しました。
(Dockerにおいて複数プロセスの起動はよろしくないとされてますが、目をつむりましょう。)

FROM swaggerapi/swagger-ui:latest

RUN apk add --update --no-cache bash nodejs npm // 後で気が付きましたが、元のイメージでnodejs 入っていたので冗長だった
RUN npm i -g npm@latest

ENV APP_ROOT /api-schema
WORKDIR $APP_ROOT

COPY . $APP_ROOT
RUN npm i
RUN npm i -g foreman // foreman導入
RUN npm run merge // 初回描画分を先に作っておきます

ENV API_URL swagger.yml
RUN cp swagger.yml /usr/share/nginx/html/swagger.yml  // 配信する場所にコピー

RUN rm -f /var/log/nginx/access.log /var/log/nginx/error.log // foreman経由で動かすと access.log, error.log が開けずエラー出るので一旦消します。(これでなぜ動くのかちゃんと見てません)

CMD ["nf", "start"] // forman のコマンドをセット

foreman 用のProcfileも忘れずに...

nginx: cd .. && sh /usr/share/nginx/run.sh // workdir を /api-schema にしちゃってるので...
watch: npm run watch

そして、docker-composeに組み込みます。

  swagger:
    build:
      context: ./api-schema
    ports:
      - '8080:8080'
    volumes:
      - ./api-schema:/api-schema:cached // スキーマ定義をファイル群をマウントしておくことで、コンテナ側のファイルも変更が走り、結果として監視対象になる
      - ./api-schema/swagger.yml:/usr/share/nginx/html/swagger.yml

あとは、起動するだけ

まとめ

swaggerの定義ファイルをいい感じに管理することができそうです。

<子育てIoT> 時間を遡って、動画を保存してくれるカメラを作ってみた

はじめに

先日、待望の第一子が無事生まれました!
既にメロメロで、親バカを存分に発揮しているのですが...
せっかくプログラミングを趣味にしているので、子育て × プログラミングでなにか作れないか?と思い、
表題の通り、動画撮影をするカメラをラズパイ4を利用して作ってみました。

なぜ、時間を遡って動画保存がしたいかというと、
子供はいつ何をしてくれるかわからないもので、私も常にカメラを構えているわけではないので、
「いまのは、後世に残しておきたい!!」と思ったときには、時既に遅し...
という状態になりかねないからです。
(はじめから親バカ全開で行きます)

このカメラでできること

※ () 内は、私ですw

  • (あ、今の瞬間非常に可愛い!!!と思う)
  • buletooth ボタンを押下する
  • ボタンを押したタイミングから約30秒遡って、動画を保存する
  • 作成した動画を Slack に投稿する
  • (いつでも可愛い動画が見放題) & (この瞬間を逃さない)

用意するもの

※ プログラミングの知識は、書籍やらネット上で手に入るとして、入手困難なものは特になく、amazon で揃えられました!

f:id:tobb422:20200718015330p:plain

開発工程

(ラズパイのセットアップは、趣旨から外れるので省きます)

  • bluetooth ボタンと ラズパイをつなぎます → こちらの記事を参考にさせていただきました
  • カメラモジュールやマイクを取り付ける
    • カメラモジュールつけるの初めてだったので、つけるのちょっとドキドキしましたw
    • こちらの記事ですごく丁寧に説明があり、今回、カメラモジュールやマイクを扱うのは、初めてでしたが、割とすんなりとできました!
  • プログラミング頑張る

です。
開発は、ラズパイにsshでログインした状態で行いました!

プログラムの大まかな処理

実際にどんな処理の流れは、以下です。

  • カメラから取得できるコマ送り画像データを蓄積する
    • 今回は、30秒分ためて、あとは古いデータから捨てていくという感じです
  • マイクから取得できる音声データを蓄積する
  • bluetooth からの信号を検知して、30秒分のコマ送り画像から、動画を作成
  • 作成した動画と音声を組み合わせる(データ形式は、.mp4)
  • (今回は、iPhone で動画を主に見る予定だったので) 完成したデータ形式を .mov に変更する
  • slack に、完成した動画をアップロードする

以下で、各処理をもう少し詳細に書いていきます!

プログラムの詳細

※ 説明の都合上、前述したプログラムの処理の流れとは、順番が異なります。

bluetooth ボタンからの信号を、検知するプログラム

evdev というパッケージを利用しました。

参考資料:

from evdev import InputDevice

while True:
    device = InputDevice('/dev/input/event1')
    event = device.read_one()
    if event != None:
        # event があれば、ここにくる

カメラ, マイクから各データの取得 & 動画に加工する

OpenCV を利用して、カメラモジュールから画像データを読み取っています
動画データの形式変換や音声との合成には、ffmpeg を利用しています。

参考資料:

import cv2
import pyaudio
from pydub import AudioSegment
import wave
import ffmpeg
from evdev import InputDevice

device = InputDevice('/dev/input/event1')

# まずは、カメラモジュールからデータを読み取る
camera = cv2.VideoCapture(0)

# 出力する動画ファイルの設定を行う
fps = 9 # フレームレート, 1秒間でどれだけフレームを使用するか, 値は調整が必要 → 後述します
w = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
video = cv2.VideoWriter('sample.avi', fourcc, fps, (w, h))

# 出力する音声ファイルの設定を行う
P = pyaudio.PyAudio()
CHUNK = 1024*5
CHANNELS = 1
FORMAT = pyaudio.paInt16
RATE = 44100
stream = P.open(format=FORMAT, channels=CHANNELS, rate=RATE, frames_per_buffer=CHUNK, input=True, output=True)

frames = []
voices = []
while True:
    # カメラからフレームを取得し続け、動画へ書き込む
    _, frame = camera.read()
    frames.append(frame)
    if len(frames) > 30:
        # 30秒くらいの動画撮影にしたいので、過去のものはどんどんしてる
        frames.pop(0)

    # 音声データを読み込み続けて、list に流す
    input = stream.read(CHUNK, exception_on_overflow=False)
    voices.append(input)
    if len(voices) > 30:
        # フレーム同様
        voices.pop(0)

    event = device.read_one()
    if event != None:
        # ここなかでためた framesやvoicesから動画を作っていきます

        # frames から元となる動画データを作成
        video = cv2.VideoWriter('sample.avi', fourcc, fps, (w, h))
        for f in frames:
            video.write(f)

        # voices から音声データを作成
        wf = wave.open('sample.wav', 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(P.get_sample_size(FORMAT))
        wf.setframerate(RATE)
        wf.writeframes(b''.join(voices))
        wf.close()

        # .wav → .mp3
        base_sound = AudioSegment.from_file('sample.wav', format='wav')
        base_sound.export(now+'-tmp.mp3', format="mp3")
        
        # 動画データと音声データを統合する
        audio_stream = ffmpeg.input('sample.mp3')
        video_stream = ffmpeg.input('sample.avi')
        ffmpeg.run(ffmpeg.output(video_stream, audio_stream, 'sample.mp4', vcodec="copy", acodec="aac"))

        # .mp4 → .mov
        ffmpeg.run(ffmpeg.output(ffmpeg.input('sample.mp4'), 'sample.mov', pix_fmt='yuv420p'))

動画ファイルをSlackへアップロード

参考資料

import requests

params = {
    'token': TOKEN, # SLACK API より取得
    'channels': CHANNEL, # 投稿したいチャンネルのID
    'initial_comment': '動画だよ'
}

files = { 'file': open('sample.mov', 'rb') }
requests.post('https://slack.com/api/files.upload', params=params, files=files)

f:id:tobb422:20200718015438p:plain

個人的にハマったポイント

映像と音声がずれる

前述もしたこちらの部分で、while 文の中でカメラ, マイクからデータを取得し続けているのですが、
このデータの粒度が違うため、合成したときに、音声が短いとか、映像が長すぎるみたいな問題が起きました。

# カメラからフレームを取得し続け、動画へ書き込む
_, frame = camera.read()
frames.append(frame)

# 音声データを読み込み続けて、list に流す
input = stream.read(CHUNK, exception_on_overflow=False)
voices.append(input)

fps(フレームレート)の設定を変えることで解決しました。
まず取得した音声データの長さを取得します。

base_sound = AudioSegment.from_file('sample.wav', format='wav')
base_sound.export(now+'-tmp.mp3', format="mp3")
# mp3ファイルへ変換した直後、以下で音声データの長さを取得できます
print(base.sound.duration_seconds) 

同じく読み取りを行っている frames に溜まったスタックしたフレーム数がわかるはずなので(list に追加しているため)
ここで、
音声データの長さ / フレーム数 → 最適なfps(フレームレート = 1秒間で使用するフレーム数) が取得できます。
ここで取得した fps を設定しておくことで、映像と音声を統合したときのズレが解消されます。

動画の加工処理を行っていると、次の読み取りにいかず、現実の時間とズレが生じる

サンプルとしてあげたコードでは、動画加工を行っている間、
while文が止まっているため、次のデータの読み取りにいかないです。

動画加工 → slack へのアップロードが終わってから、溜まっているデータの読み取りにいくので、
次回、動画加工に入るタイミングでは、現実よりも動画加工の時間分、遅れてデータが frames や voices に溜まっていることとなります。

解決策としては、動画加工 ~ slack へのアップロードを threading モジュールを利用して、別スレッドで実行するように修正しました。

def worker(frames, voices):
    # 動画加工やslackのアップロード処理を書いておく

while True:
    # カメラからフレームを取得し続ける
    _, frame = camera.read()
    frames.append(frame)
    if len(frames) > 30:
        frames.pop(0)

    # 音声データを読み込み続ける
    input = stream.read(CHUNK, exception_on_overflow=False)
    if len(voices) > 30:
        voices.pop(0)

    device = InputDevice('/dev/input/event1')
    event = device.read_one()
    if event != None:
        fs = copy.copy(frames)
        vs = copy.copy(voices)
        # 別 thread にて動画加工を実行することで、遅延をなくし、処理を回し続けられるようにする
        t = threading.Thread(target=worker, args=(fs, vs))
        t.start()

まとめ

非常に長い記事になってしまいましたが、
以上が「時間を遡って動画保存をするカメラ」の開発ログでした!

ラズパイがむき出しの状態は、子供向けにはちょっと可愛らしさが足りないので、
猿のぬいぐるみをケース代わりに作成してもらった(嫁作)のですが、
出産に間に合わず、現在この状態で止まっております。首を狩られた姿...w
(いつか完成させてくれるはず...)
f:id:tobb422:20200726144906p:plain

せっかく作ったので、
子育ての中で、しっかり活用していきたいと思います!

fp-ts, io-ts を使ってみた所感

はじめに

ざっと、10ヶ月ぶりの更新....

最近、業務でAPIサーバーを TypeScript で開発するという案件がありました。
プロジェクトの主要な開発は、Scala で開発しており Cats という関数型ライブラリを使用しています。
そこで、 TypeScript でも同じように書けないかな?と探していたところ、 fp-ts, io-ts というライブラリを発見したので、早速試してみました!

fp-ts, io-ts キャッチアップ!

これらのライブラリでは、
関数型言語で使用されるような型や型クラス, それらを扱うための関数が、内包されています。

fp-ts

Either を例にして、どうやってキャッチアップしたかの紹介です。

// 型定義と関数を一部抜粋しています。
export declare type Either<E, A> = Left<E> | Right<A>

export declare function fold<E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B): (ma: Either<E, A>) => B

今回は、シンプルなので Either を例にしましたが、
プロジェクトでは、Task, EitherT, Traversable 辺りを取り入れました!

io-ts

fp-ts で、一通り揃っている感じはするのですが、

Runtime type system for IO decoding/encoding

上記の部分を io-ts がカバーしてくれています!
私が利用したのは、サーバーが受け取ったリクエストボディの内容が型と一致しているかバリデーションを行う箇所で利用しました。
少々クセがあるように感じましたが、js のランタイムで型の恩恵が得られるのは、良いなという印象です。
fp-ts 同様で、ドキュメントテストコードを見ながらキャッチアップする方法が、理解しやすいかなと感じました。

import * as t from 'io-ts';

const types = t.type({
  name: t.string,
  email: t.string,
  age: t.number,
});

types.decode(request.body);
// 与えられた body が types で定義した型と違うとバリデーションエラーを返す
// decode は、 Validation<A> ( = Either<Errors, A>) を返す ※ A は、定義した型

fp-ts-contrib (fp-ts をパワーアップする)

fp-ts, io-ts を導入するだけでも、関数型の恩恵を受けられるところは大きいのですが、
上記のライブラリをさらに使いやすくするライブラリもあったので、そちらも合わせて試してみました。
fp-ts-contrib

A community driven utility package for fp-ts

GitHub の説明としては、上記のように書いてあります。
私の場合、今回は Do という概念を利用しました。
Haskell の Do や Scala の for...yield と同じようなことを実現できるので、
普段、関数型で書かれている方には取っ付き易いと思いますし、
TypeScript を書ける方で、これから関数型も触ってみたいと思われている方には、導入としてぴったりだと感じています。

こちらもキャッチアップに関しては、ドキュメントテストコードが非常に充実しているので、そちらから読み解いていくのが良さそうに思います。

// テストコードを参照し、一部コードを付け加えている
const user = Do(option)
  .bindL('name', () => some('bob'))
  .bindL('email', () => some('bsmith@example.com'))
  .let('constant', 10)
  .bindL('nameLen', ({ name }) => some(name.length))
  .letL('emailLen', ({ email }) => email.length)
  .doL(() => Either.right('nothing'))
  .return(({ name, email, nameLen, emailLen, constant }) => ({ name, email, nameLen, emailLen, constant }))

// let: 引き渡す変数の定義
// bind: 関数の結果を引き渡すことができる
// do: 関数の実行
// return: 最終的な返り値 
// サフィックスのL → 値ではなく、関数を引数に取る

まとめ

fp-ts, io-ts を利用することで、TypeScript で関数型拡張ができると思います。
絶対使ったほうがいいということはないんですが....
要所要所で利用すれば、スッキリ書くこともできそうだなと考えています。
また、関数型に普段から慣れている方は、 fp-ts-contrib を使うことで、よりリッチに書けると思います。
(あまり TypeScript 書くことはないかもしれませんが...)

関数型に入門する際に、言語も概念もまとめてキャッチアップするのは、なかなか骨が折れると思うのですが、
JavaScript, TypeScript に書き慣れている方にとっては、
書き慣れた言語で新しい概念に触れることができるので、入門するには最適なのかな?と思いました!

<読書メモ> React Design Patterns and Best Practices 2nd Edition

はじめに

先日、React + TypeScript を使った新規プロジェクトを担当することになりました。
TypeScript は普段から使用していることもあり、
まあ大丈夫かなと思っていたのですが、React が約2年ぶり?くらいだったため、
思い出すことを目的に React Design Patterns and Best Practices という本を読みました。
本投稿は、その読書メモです。
※ 読書メモなので、私が読むことを想定して書き殴ります。
ターゲットとしている層は、「既にReactでアプリケーションを書いている人がステップアップするという位置づけ」らしいです。

↑ あとで気がついたが、なぜかこの書籍だけはてなブログの「Amazon商品紹介」で選択しても表示できない...
https://www.amazon.co.jp/gp/product/1789530172

雑感としては、知らなかった概念や機能を知るきっかけになったが、
プロダクトに導入するには、この書籍だけでは理解を深めるのが難しかったです。
(英語力の問題では....?w)

(私が)初めて知った & 使えそうと思った概念?機能? をまとめてみる

React Hooks

今回の新規プロジェクトで既に使ってみています。
後述する React.memo とそうなんですが、関数で定義していくことに優しい世の中になっているように感じます。
結果、気がついたらほぼ全てのコンポーネントfunction で定義してました。
正直、自分の英語力と技術力ではよくわからなかったので、
以下の記事とドキュメントを利用して、キャッチアップしました。
(まだ、 useStateuseEffect くらいしか使えていないのですが....)
qiita.com ja.reactjs.org

React Context

コンポーネント間で渡すデータを props でやり取りするのではなく
コンポーネントツリー内で共通で利用できるデータといったイメージですかね...?
手元で動かしてみて、こんな感じかと理解した程度で、プロダクト内に取り入れられていないです...
ja.reactjs.org

Function As Child

HoC と比較して語られているケースを良く見ます。
要は、childrenコンポーネントを渡すのではなくて、関数を渡すということです。
最初この章を読んだときは、そんなことができるんだ〜と思いましたw
コンポーネントで切り替わっていく state 等で動的に子コンポーネントを変化させたいときに有効なのでしょうか?
テストで試した程度で、まだプロダクト内では使えていないのですが...
qiita.com

react-motion で実際に使用されているソースコードを見つけたので、
そちらを見るとなんとなく理解が深まるかもしれません。
該当のコンポーネント

Pure Component → React.memo

qiita.com PureComponent は、知っていたのですが
その関数版というか、 React.memo が公開関数になっており、
関数で定義したコンポーネントをラップして、返してくれるようです。
recomposepure みたいなものですね
ただ、なんでも React.memo を使えば正解というわけではなさそうです。用法用量お守りください。
qiita.com

React.Fragment

ja.reactjs.org ドキュメントに書いてあると通りなのですが、無駄なノードを追加しなくて済む書き方と認識しておくので問題なさそうです。

まとめ

非常に雑ですが、読書メモなのでこれで良しとします。
序盤でも触れましたが、普段開発してて知らない概念や機能に触れることができたのはよかったかな〜
と思っています。 ( → ドキュメントに書いてるけど...)

Go + Headless Chrome で HTML から PDF を生成する基盤を作成した

久々にブログを更新!!
(サボりがちなので、心機一転ここからまた継続して更新できるように頑張ります....)

はじめに

one visa というプロダクトの開発を行っており、
その中で、コア機能の一つとなる PDF 生成を Go + Headless Chrome で作成するという取り組みをしたので
その取り組みについて紹介いたします!

どうやって実現するか

  • 生成したい書類を Webページとして作成する(HTML + CSS
  • 作成した Webページをレンダリングする Webサーバーを立てる
    • → (今回は、Go縛りで Echo を使って Webサーバーを立てました!)
  • 該当ページに headless chrome でアクセスする
  • 表示したページを PDF として保存する

この一連の流れを作成します! 図で表すとこんな感じです。

f:id:tobb422:20190716010611p:plain
流れのイメージ

また、今回は異なるページを PDF として生成し、最後に 一枚の PDF として合体させる必要があったので、
ゴルーチンを利用して、上記で説明したプロセスを並列で行います!

利用するツール

今回は、Chrome DevTools Protocol を利用して、Chrome を操作します。
この Chrome DevTools Protocol を go で簡単に操作できるライブラリである chromedp を利用します!
また PDF の合成には、 pdftk という PDF ファイルを編集するコマンドラインツールを利用しました!

実装

上記を実現するためのサンプルです。
echo を使った Webページのレンダリングは省略しています。
また、私は Docker コンテナに必要なツールをインストールして稼働させました!
もちろん、必要なツールが揃っていれば、ローカルで直接稼働させることもできるはずです。
(必要なツール chromedriver chrome pdftk

Docker上で、Headless Chrome を動かすということ自体はよく記事になっているので、省略します。
qiita.com

また、 pdftkapt-get install で Docker に組み込めるのでこちらも省略します。

package main

import ...

func main() {
  // 生成したいPDFに該当するWebページのパスを持った構造体をマップでひとまとめにする
  // → 後述します
  builders := []builder.PDFBuilder{
    &Sample{ Path: "sample" }
  }

  // 各WebページをPDFとして保存して
  fileNames, err := createPDF(); if err != nil {
    return
  }

  // 生成されたPDFを1つのPDFへ合体します
  mergePDF(fileNames)

  // 残ってしまった不要なファイルを削除する
  deleteTmpFile(fileNames)
}

func createPDF(builders []builder.PDFBuilder) (fileNames []string, err error){

  var wg sync.WaitGroup
  wg.Add(len(builders))

  // ゴルーチンを利用して、各PDF生成を並列で処理します
  for _, builder := range builders {
    go func(builder builder.PDFFactoryServicer) {
      defer wg.Done()
      builder.Exec()  // PDF生成
    }(v)
  }

  wg.Wait()

  // 全てのPDFが生成されたことを確認しています
  for _, builder := range builders {
    _, err := os.Stat(builder.Name()); if err != nil {
      return nil, err
    }
    fileNames = append(fileNames, builder.Name())
  }

  return fileNames, nil
}

func mergePDF(fileNames []string) (mergePDFName string, err error) {
  mergePDFName = "merge.pdf"

  mergeCommand := append(fileNames, "cat", "output", mergePDFName)
  // exec.Command を利用して pdftk のコマンドを実行しています
  err = exec.Command("pdftk", mergeCommand...).Run(); if err != nil {
    return "", err
  }

  return mergeFileName, nil
}

// 各ページごとに生成したPDFを削除
func deleteTmpFile(fileNames []string) error {
  for _, v := range fileNames {
    if err = os.Remove(v); err != nil {
      return err
    }
  }
  return nil
}
// WEBページへアクセスし、PDFとして保存するロジックをまとめる
package print

func Screenshot(path string, output string) error {
  // chromedb を実行する
  ctx, cancel := chromedp.NewContext(context.Background())
  defer cancel()

  // chrome で実行したいタスクを走らせる
  var buf []byte
  err := chromedp.Run(ctx, createTask("URL"+ path, `div`, &buf)); if err != nil {
    return err
  }

  // 指定した名前でファイルを作成する
  file, err := os.Create(output); if err != nil {
    return err
  }
  defer file.Close()

  writer := bufio.NewWriter(file)
  writer.Write(buf)
  writer.Flush()
}

func createTask(url string, sel string, res *[]byte) chromedp.Tasks {
  return chromedp.Tasks{
    chromedp.Navigate(url), // URL へアクセスして
    chromedp.WaitVisible(sel, chromedp.ByQuery), // 表示されるのを待って
    printToPDF(res), // pdf として保存する
  }
}

// chromeでPDFとして保存させる処理
func printToPDF(pdfbuf *[]byte) chromedp.Action {
  return chromedp.ActionFunc(func(ctx context.Context) error {
    buf, err := page.PrintToPDF().WithPrintBackground(true).Do(ctx)
    if err != nil {
      return err
    }
    *pdfbuf = buf
    return nil
  })
}
// PDF 生成を担当する構造体
package builder

// WebページからPDFを生成機能を持ったインターフェース
type PDFBuilder interface {
  Exec()
  Name() string
}

// 以下のように、生成したい Path を持った構造体を作る
type Sample struct {
  Path string
}

func (service *Sample) Exec() {
  print.Screenshot(service.Path, service.Name())
}

func (service *Sample) Name() string {
  return service.Path + ".pdf"
}

完成イメージ

完成形は、以下のような処理の流れになります!

f:id:tobb422:20190716021116p:plain
完成イメージ

まとめ

PDF 生成を Webページから行う方法についてまとめてみました
今回は、go で実装しましたが、 chrome 操作を簡単に行うことができれば言語は問わないと思います。
最後、PDF に変換されるため、レイアウトをうまく調整することが難しかったりもしたのですが、
PaperCSS といったツールを使えば、その辺りもうまくできそうです。
(※ 私はゴリッと生のCSSでコーディングしたので、テキトーなことを言っています)
HTML + CSS の構成なので、デザインの変更は簡単に行なえますし、
動的なデータもWebサーバーを利用して簡単に生成可能です!
(今回、例では出していませんが実際はクエリを利用して、動的なデータの表示や生成するページを切り替えるといったことも可能です!)
ゴルーチンを利用すれば、一定の速度を保って簡単にスケールできる環境も作れると思うので
大量のPDF生成にお困りの方にはおすすめです!(← そもそもここの需要が少なそうw)

入門 TypeORM(2)~ リレーション ~

はじめに

入門 TypeORM (1) ~ テーブル定義と基本操作 ~ ← こちらの記事の続編です。
今回は、リレーションについてまとめていきたいと思います。
早速内容に入っていきます!
tobb422.hatenablog.com

github.com

1 : 1 のケース

前回の記事で利用した User クラスに関連するテーブルとして、 UserDetail クラスを作成したいと思います。

こちらは前回作成した User クラス(usersテーブル)です。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn
} from 'typeorm'

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @Column({ name: 'password', nullable: true })
  password: string

  @Column({ name: 'email', unique: true })
  email: string

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

ここで、UserDetail クラスを定義してみます。
UserDetail クラスの位置付けとしては、 User の補足情報ということにしておきます。
一旦、誕生日をプロパティとして持つ、UserDetail クラスを定義してみます。
(説明の便宜上、こうしておりますので、誕生日も User クラスのプロパティで良くね?というツッコミはナシということで....w)

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  OneToOne, JoinColumn
} from 'typeorm'
import { User } from 'user.entity'

@Entity('user_details')
export class UserDetail extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'birthday' })
  birthday: Date

  // この記述が、1:1の関係を表している
  @OneToOne(type => User, user => user.userDetail)
  @JoinColumn()
  user: User
  

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

前回、テーブル定義では、デコレーターを利用することに触れましたが、
リレーションもデコレーターを利用して定義します。
OneToOne という名前からも 1:1 の関係を表していることが明白です。
また JoinColumn デコレーターが、このカラムがリレーションで利用するものだということを明示してくれています。

User クラス側にも、追加の定義が必要です。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  OneToOne
} from 'typeorm'
import { UserDetail } from 'user-detail.entity'

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @Column({ name: 'password', nullable: true })
  password: string

  @Column({ name: 'email', unique: true })
  email: string

  // 追加で定義
  @OneToOne(type => UserDetail, userDetail => userDetail.user)
  userDetail: UserDetail

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

ほぼ、 UserDetail クラス で実装したものと同じことを User クラスでも定義しています。
違うこととしては、 User クラス側(usersテーブル)では、追加カラムが必要ないので、
JoinColumn デコレーターを定義していないことだと思います。

1 : N のケース

1:1 の関係を定義するのに、 OneToOne デコレーターを利用しましたが、
1:N では、 OneToMany ManyToOne デコレーターを利用します。

User クラスが、Card クラスを複数持つということを想定して、実装してみます。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  ManyToOne, JoinColumn
} from 'typeorm'
import { User } from 'user.entity'

@Entity('cards')
export class Card extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  // 1:Nを表してくれるデコレーター
  @ManyToOne(type => User, user => user.cards)
  @JoinColumn({ name: 'user_id' })
  user: User

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

1:N でも、 1:1 の時と同様に User クラス側への追加実装が必要です。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  OneToOne, OneToMany
} from 'typeorm'
import { UserDetail } from 'user-detail.entity'
import { Card } from 'card.entity'

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @Column({ name: 'password', nullable: true })
  password: string

  @Column({ name: 'email', unique: true })
  email: string

  @OneToOne(type => UserDetail, userDetail => userDetail.user)
  @JoinColumn({ name: 'user_detail' })
  userDetail: UserDetail

  // 追加で定義 
  @OneToMany(type => Card, card => card.user, { cascade: true, nullable: true })
  cards?: Card[]

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

M : N のケース

1:1 や 1:N に比べると、若干特殊な感じがするのが M:N の実装です。
今回は、 先程作成した Card クラスと新たに Label クラスを作成して説明します。
カード毎に、複数のラベルを付与することができるといった感じです。

M:N のリレーションを定義するためにデコレーターを利用するということは同じで
今回のケースでは、 ManyToMany デコレーターを利用します。
特殊なのは、 先程まではカラムとして保持していたのですが、
今回は、「cardsテーブルとlabelsテーブルをつなぎ合わせる中間テーブルが必要」という点です。

では、実装を見ていきます。 まずは、Label クラスです。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  ManyToMany
} from 'typeorm'
import { Card } from 'card.entity'

@Entity('labels')
export class Label extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @ManyToMany(type => Card, card => card.labels, { nullable: true })
  cards?: Card[]

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

Label クラスでは、先程紹介した M:N を表す ManyToMany デコレーターを利用しただけで、目新しい実装はありません。
続けて、Card クラスに実装を追加していきます。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn,
  ManyToOne, JoinColumn, ManyToMany, JoinTable
} from 'typeorm'
import { User } from 'user.entity'
import { Label } from 'label.entity'

@Entity('cards')
export class Card extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @ManyToOne(type => User, user => user.cards)
  @JoinColumn({ name: 'user_id' })
  user: User

  // 追加実装
  @ManyToMany(type => Label, label => label.cards)
  @JoinTable({
    name: 'card_labels',
    joinColumn: { name: 'card_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'label_id', referencedColumnName: 'id' },
  })
  labels?: Label[]

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

ManyToMany デコレーターの追加は、 これまで通り、この Card クラスが Label クラスと M:N の関係にあることを示しています。
新しく登場した JoinTable というデコレーターが、先程説明した中間テーブル作成の役割を果たします。

実際に マイグレーション処理(マイグレーションに関しては、別途他の記事 → をご参考ください)を行うと、
card_labels という中間テーブルが作成されます。

(Typescript + TypeORMセットアップ & migration世代管理 & expressへの組み込み)

まとめ

リレーションの定義方法について、まとめました。
このリレーションを使ったDB操作まで記述しようと考えていたのですが、定義について書くだけで力付きてしまったので、
気が向いたときに追記 or 別の記事で紹介したいと思います。
(たぶん、書かないのだろう....w)

入門 TypeORM (1) ~ テーブル定義と基本操作 ~

はじめに

現在、Nest.js というTypeScript を利用した Webフレームワークを使って、アプリケーションを開発しているのですが、
Database とのやりとりに TypeORM という ORM を使用しています。
(Database には、 postgresql を採用しています!)
まだ、簡単なTodoアプリを作成してみた程度なのですが、一通り触ってみたので、メモがてらまとめておきます。
今回は、その中でも初めの一歩として、TypeORMの概要, テーブル定義, そして基本的なDB操作についてまとめました! github.com

TypeORMとは

Node.js の ORM です。Node.js で ORM というと他には、Sequelize などが有名だと思います。
TypeORM の特徴としては、RailsActiveRecord のような書き方ができる点なのかなと思っています。
そのため、以前 Rails を利用したアプリケーションをいくつか書いていた経験がある方には、すぐ書ける気がします。
正確には、 TypeORM を利用した実装パターンを大きく2つあり、そのうちの1つが ActiveRecord のような記述になっています。
詳しくは、Github をご覧になっていただければ、概要を把握できると思います。
前置きとして、以下の説明では、 ActiveRecord っぽい実装を採用しています。

テーブルの定義

まず、テーブル定義についてまとめていきます。
実際の実装をみるほうがわかりやすいと思うので、以下のコードを順に追っていく形で説明します。

import {
  Entity, Column, PrimaryGeneratedColumn,
  BaseEntity, CreateDateColumn, UpdateDateColumn
} from 'typeorm'

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ name: 'name' })
  name: string

  @Column({ name: 'password', nullable: true })
  password: string

  @Column({ name: 'email', unique: true })
  email: string

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date
}

まず、 @Entity('users')@Column({ name: 'name' }) についてですが、
こちらは、デコレーターと呼ばれるもので、TypeORM では、このデコレーターを定義することで、テーブル定義を行うことが可能となります。
このデコレーターを定義していくことで、目的としているテーブルを定義することができます。

@Entity() では、このテーブルの名前を定義しています。
@Column() では、このプロパティがカラムであることを定義しています。
どちらも、名前を付けなくても TypeORM がテーブルを作成する際に、クラス名やプロパティ名から名前を付けてくれるのですが、
たとえば、createdAt などは、 そのまま createdAt になっており、私としては、テーブルのカラム名は、 created_at という形にしておきたいという理由から
少し冗長的ですが、 @Column({ name: 'created_at' }) といった形で、明示的に定義するようにしています。
テーブル名も同様で、 @Entity('users') とテーブル名を定義しています。
ここまでで、どういったテーブルに、どういった名前のカラムが定義されるのかがわかるようになります。

続けて、 @PrimaryGeneratedColumn()@CreateDateColumn @UpdateDateColumn とまだ触れていないデコレーターについて説明いたします。
これらのデコレーターは、そのカラムの振る舞いを定義していくれるデコレーターと考えると良いと思います。
@PrimaryGeneratedColumn() であれば、その名の通りなのですが プライマリキーとして連番を振ってくれるカラムになります。

このように TypeORM では、デコレーターを利用することで、そのテーブルやカラムの定義や振る舞いを作っていくことになります。

最後に class User extends BaseEntity こちらの部分の説明ですが、
BaseEntity を継承してクラスを作成います。この BaseEntity を継承することで、
User クラスは、DB操作(データの取得, 作成, 更新, 削除) といったことができるメソッドを持つことができます。

User.find() // users テーブルの全件取得

上記のように、 ActiveRecord っぽく扱えます。

基本的なDB操作

続いて、基本的なDB操作についてまとめます。
先程の User クラスを例に説明していきます。

取得

User.find() // 全件取得
User.findOne(id) // id を指定して、取得
User.findOne({ email: 'typeorm@test.com' }) // メールアドレスを指定して、取得

新規作成・更新

■ 新規作成

const user = new User()
user.name = 'test'
user.email = 'typeorm@test.com'
user.password = 'typeorm'
user.save()

■ 更新

const user = await User.findOne(1) // 実際のメソッドで async を定義していると仮定して....
user.email = 'test@typeorm.com'
user.save()

削除

const user = await User.findOne(1)
user.remove()

以上が、基本的なDB操作の例です。
私の主観としては、非常に直感的でわかりやすいな〜といった印象です。

まとめ

今回は、 TypeORM の概要, テーブル定義, そして、基本的なDB操作についてまとめました。
次回は、リレーションについてまとめたいと思います。
書いた → 入門 TypeORM(2)~ リレーション ~