Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8934 articles
Browse latest View live

最近のNode.jsでBLEのアドバタイジングパケットを補足してみる

$
0
0

はじめに

nobleが虫の息というか、もう息の根を止めてあげていい状態なので他に移行しなければならない。さて次は何がいいだろうかと調べてみてラズパイ用途で行き当たるのは以下の2つくらいじゃないだろうか。

node-ble
node-bluez

node-bleが開発が活発に見えるが、node-bluezもシンプルでよさげ。両方やってみることにする。試した環境はこちら

uname-a
Linux raspberrypi.local 5.8.0-1016-raspi #19-Ubuntu SMP PREEMPT Tue Feb 9 20:12:43 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux

前準備

node-bluzが依存するライブラリとnode.jsをインストールする

sudo apt install-y libglib2.0-dev libdbus-1-dev
sudo apt install-y nodejs npm

ここで入るnodeのバージョンは12.xで古い。nで最新を入れる

sudo npm i -g n
sudo n latest
sudo apt remove --purge nodejs
sudo apt autoremove

/usr/local/binにlatest versionが入るのでパスを通しておく

~/.profile
export"$HOME/usr/local/bin:$PATH"
source ~/.profile
node -v
v15.13.0

TypeScriptの準備

mkdir-p work &&cd$_
npm init -f
npm i -S typescript ts-node bluez node-ble sleep-promise
npx tsc --init

Promiseまわりのエラー回避で tsconfig.jsonのlibを以下にする

tsconfig.json
lib:["ES2015","dom"],

node-bleでやってみた

node-ble.ts
import{createBluetooth}from'node-ble';importsleepfrom'sleep-promise';constmain=async()=>{const{bluetooth,destroy}:any=createBluetooth();constadapter:any=awaitbluetooth.defaultAdapter();if(!awaitadapter.isDiscovering()){console.log('Discovering:');awaitadapter.startDiscovery();while(true){constdevices:any=awaitadapter.devices();console.log(devices);awaitsleep(3000);}}};main();

実行する

npx ts-node node-ble.ts 
Discovering:
[ '4E:2E:8E:F1:63:91', 'E1:A6:93:4A:ED:C3' ]
[ '4E:2E:8E:F1:63:91', '5B:A0:F3:EB:BB:0D', 'E1:A6:93:4A:ED:C3' ]
[ '4E:2E:8E:F1:63:91', '5B:A0:F3:EB:BB:0D' ]
[ '4E:2E:8E:F1:63:91', '5B:A0:F3:EB:BB:0D' ]
[ '4E:2E:8E:F1:63:91', '5B:A0:F3:EB:BB:0D', 'E1:A6:93:4A:ED:C3' ]
[ '4E:2E:8E:F1:63:91', '5B:A0:F3:EB:BB:0D', 'E1:A6:93:4A:ED:C3' ]

とれた。Adapterにイベントハンドラがないので Adapter.device()を一定時間ループで回してとる手法。nobleでイベントドリブンな取得に慣れていたのでこれ思いつかなくてしばらくハマった。一回device()しただけじゃ絶対とれないし。これはこれでコールバックじゃない分制御の汎用性は高いと思うけど。好みの問題だろうか。

node-bluezでやってみた

node-bluez.ts
importBluezfrom'bluez';constbluetooth=newBluez();consthandleOn=async(address:string,props:any)=>{console.log(`[NEW] Device: ${address}, ${JSON.stringify(props.Name)}`);constdev=awaitbluetooth.getDevice(address);dev.on("PropertiesChanged",(props)=>{console.log(`[CHG] Device: ${address}, ${props}`);});};constmain=async()=>{try{bluetooth.on('device',handleOn);awaitbluetooth.init();constadapter=awaitbluetooth.getAdapter();awaitadapter.StartDiscovery();console.log("Discovering");}catch(e){thrownewError(e);}}main();

実行する

npx ts-node node-bluez.ts 
Discovering
[NEW] Device: 54:AB:0C:6F:15:E6, undefined
[NEW] Device: E1:A6:93:4A:ED:C3, "tkr"[NEW] Device: D1:1C:63:61:E0:01, undefined
[NEW] Device: E1:A6:93:4A:ED:C3, "tkr"[NEW] Device: FB:33:A1:32:7F:75, "tkr"[NEW] Device: E1:A6:93:4A:ED:C3, "tkr"

とれた。こちらはAdaptorにイベントハンドラがあって受信したタイミングでパケットが降ってくる。tkrは財布とかに入れてある忘れ物防止タグのTrackRだ。

おわりに

どちらもざっくりコードをみるかぎりDbus APIのラッパーな感じ。アドバタイジングとるだけならイベントドリブンなnode-bluezのほうが好み。ケースバイケースで使っていこうと思う。やっとnobleの葬式ができそう。


Puppeteer(パペティア)でcsvファイルをダウンロードする

$
0
0

概要

ずっとやりたかったPuppeteerでファイルダウンロードをやってみた。
世の中いろんなやり方が書かれていて混乱したが、やってみたら簡単にできたので共有。

参考 - 公式issue
https://github.com/puppeteer/puppeteer/issues/299

環境

  • Ubuntu18.04
  • Node.js v14.15.1
  • Puppeteer 8.0.0

ソース

意外と普通にできました!
何か罠が待っている気もしなくもなく。

constpuppeteer=require('puppeteer');constfs=require('fs');constpath=require("path");(async()=>{constbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();constwidth=1980;constheight=1080;awaitpage.setViewport({width,height});awaitpage.goto('https://portal.local/');// gotoの後は、waitしなくてよさげ// ファイルダウンロードconstdownloadPath=path.resolve('/hoge/fuga/');// ここは絶対に絶対パスawaitpage._client.send('Page.setDownloadBehavior',{behavior:'allow',downloadPath:downloadPath});// csvダウンロードボタンクリックawaitpage.click('.csvDownload');awaitpage.waitForTimeout(5000);awaitbrowser.close();})();

参考

Express + Node.jsを使ったアプリ開発をTypeScriptで行う

$
0
0

Express + Node.jsを使ったアプリ開発をTypeScriptで行う

Azure使ったWebアプリを作ろうと思い立ち

このクイックスタートに沿って始めたが、型がない状態での開発に堪えられず、TypeScriptを導入することにしました。

1. まずはJavaScriptで

クイックスタートに沿ってExpressアプリを構築していく

>npx express-generator myExpressApp --view ejs --git

--view ejsの部分はクイックスタートでは--view pugとなっているが、これはhtmlを構成するテンプレートエンジンを選んでいる。僕は個人的に使いやすそうなのでejsにしました。
この辺の比較は

こちらが参考になりそうです。

とりあえずそのまま動かしてみます。新しく作成されたディレクトリに移動して

>npm install>npm start

http://localhost:3000/をブラウザで開くと、無事↓のように表示されました。
express_capture.png

2. TypeScript化

まず最初に、TypeScriptとExpressの型定義をインストールします。

> npm install -D typescript
> npm install -D @types/exrpess

無事インストールできたら

> npx tsc --init

を実行するとtsconfig.jsonが作成されます。

tsconfig.json
{"compilerOptions":{"target":"es5","module":"commonjs","outDir":"./dist","strict":true,"esModuleInterop":true,"allowJs":true}}

とりあえず、こんな感じにしました。途中のプロジェクトとかだと、いきなり全部tsにするのも大変なので、allowJs:trueにしておきました。
まずapp.jsからTypeScript化していきます。名前をapp.tsに変更して

app.ts
importcreateErrorfrom'http-errors';importexpressfrom"express";import{Request,Response,NextFunction}from'express';importpathfrom'path';importcookieParserfrom'cookie-parser';importloggerfrom'morgan';varindexRouter=require('./routes/index');varusersRouter=require('./routes/users');varapp=express();// view engine setupapp.set('views',path.join('views'));app.set('view engine','ejs');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join('public')));app.use('/',indexRouter);app.use('/users',usersRouter);// catch 404 and forward to error handlerapp.use(function(req:Request,res:Response,next:NextFunction){next(createError(404));});// error handlerapp.use(function(err:any,req:Request,res:Response,next:NextFunction){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// render the error pageres.status(err.status||500);res.render('error');});module.exports=app;

こんな感じになりました。ほとんどそのままですが、型の追加、importあたりが変わっています。あと、__dirnameを抹消しました。コンパイルされたjsファイルはdistディレクトリ下に置かれるので、相対パスの取り方が変わるからです。そこで、bin/wwwも変更する必要があります。

bin/www
// var app = require('../app'); ←変更前varapp=require('../dist/app');

dist配下のappを指定しなければいけません。

>npx tsc

でコンパイルしてみます。
http://localhost:3000/にアクセスしてみて問題なければ成功です。これでじゃんじゃんTypeScript化を進めることができます。

3. まとめとおまけ

これで快適にwebアプリ開発が進められそうです。
ちなみに

npx tsc --watch

--watchを付けることで、ファイルに変更があると自動でコンパイルしてくれます。
nodemonとかと組み合わせるともっと快適になりそうです。

2021/04/05:追記

package.json
"scripts":{"dev":"rd /s /q dist & tsc-watch --noClear --onSuccess 'node ./bin/www'","start":"node ./bin/www"}

のようにすることで、npm run devコマンドで自動コンパイルしながらnodeを起動しなおしてくれる快適な環境にできました。
!!注意!!
僕の環境がwindowsなので、rdコマンドを用いていますが、MacやLinuxの場合は

package.json
"scripts":{"dev":"rm -rf dist & tsc-watch --noClear --onSuccess 'node ./bin/www'","start":"node ./bin/www"}

のような感じになると思います。

参考

Google Cloud TranslationのAPIをローカルのNode.jsから試してみる

$
0
0

Google Cloud TranslationのAPIをローカルのNode.jsからアクセスしてみました。

前回(といってももう1年半前ですが)Pythonで試しましたが、今回はNode.jsです。

Cloud Translation APIを有効化

Google Cloudのコンソールにブラウザでアクセスして、使用するプロジェクトのCloud Translation APIを有効化します。

image.png

ちなみに、有効化をせずにアクセスすると、以下のようなエラーがでました。

PERMISSION_DENIED: Cloud Translation API has not been used in project xxxxxxxxxxxx before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/translate.googleapis.com/overview?project=xxxxxxxxxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

認証設定

Google Cloud Platformのコンソール画面のIAM & AdminのService accountsというメニューから操作して、Create keyでキーを作成し、JSONファイルをダウンロードしておきます。そのJSONファイルを以下のように環境変数で指定しておきます。

$ export GOOGLE_APPLICATION_CREDENTIALS=$HOME/path/to/key.json

パッケージインストール

$npm install @google-cloud/translate

ソースコード

const{TranslationServiceClient}=require("@google-cloud/translate").v3;constprojectId="xxxxxxxx";constlocation="us-central1";// 言語判定asyncfunctiondetectLanguage(text){consttranslationClient=newTranslationServiceClient();constreq={parent:translationClient.locationPath(projectId,location),content:text,mimeType:"text/plain",};constres=awaittranslationClient.detectLanguage(req);letsourceLang=null;for(constelemofres){if(elem==null)// なぜかnullがレスポンスに含まれるcontinue;returnelem["languages"][0]["languageCode"];}}// 翻訳asyncfunctiontranslate(text,sourceLang,targetLang){consttranslationClient=newTranslationServiceClient();constreq={parent:translationClient.locationPath(projectId,location),contents:[text],mimeType:"text/plain",sourceLanguageCode:sourceLang,targetLanguageCode:targetLang,};constres=awaittranslationClient.translateText(req);for(constelemofres){if(elem==null)// なぜかnullがレスポンスに含まれるcontinue;returnelem["translations"][0]["translatedText"];}}asyncfunctionsample(text){console.log("original: "+text);// 言語の判定constsourceLang=awaitdetectLanguage(text);// 翻訳for(consttargetLangof["en","ja","zh-TW","zh-CN","ko"]){if(targetLang==sourceLang)// Target language can't be equal to source language. というエラーを防ぐためcontinue;consttargetText=awaittranslate(text,sourceLang,targetLang);console.log(targetLang+": "+targetText);}console.log();}consttexts=["Hello, world!","Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business.","Vue是一套用于构建用户界面的渐进式框架。",];asyncfunctionmain(){for(consttextoftexts){awaitsample(text);}}main().catch(err=>{console.log(err);});

実行結果

$node sample.js
original: Hello, world!
ja: こんにちは世界!
zh-TW: 你好世界!
zh-CN: 你好世界!
ko: 안녕, 세상!

original: Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business.
ja: Firebaseは、高品質のアプリをすばやく開発してビジネスを成長させるのに役立つGoogleのモバイルプラットフォームです。
zh-TW: Firebase是Google的移動平台,可幫助您快速開發高質量的應用程序並發展業務。
zh-CN: Firebase是Google的移动平台,可帮助您快速开发高质量的应用程序并发展业务。
ko: Firebase는 고품질 앱을 빠르게 개발하고 비즈니스를 성장시키는 데 도움이되는 Google의 모바일 플랫폼입니다.

original: Vue是一套用于构建用户界面的渐进式框架。
en: Vue is a progressive framework for building user interfaces.
ja: Vueは、ユーザーインターフェイスを構築するための進歩的なフレームワークです。
zh-TW: Vue是一套用於構建用戶界面的漸進式框架。
ko: Vue는 사용자 인터페이스 구축을위한 진보적 인 프레임 워크입니다.

参考

JavaScript用のGoogle Cloud Translation APIレファレンスはこちら。

yarn(や〜ん)について

$
0
0

Railsチュートリアルで学んだyarnについて簡単にまとめてみました。

yarnとは何?

2016年にリリースされたNode.jsのパッケージマネージャです。
npmと同じくpackage.json、node_modules/で構成されています。

yarnの主な特徴・メリット

・インストールが速い
・セキュリティが高い
・npmの上位互換とされる
・npmと同じような感覚で使える
・npmと一緒に使える

パッケージマネージャとは何?

パッケージマネージャとは、コンピュータに何のソフトウェアがインストールされたかを記録し、新しいソフトウェアのインストールや新しいバージョンへのソフトウェアの更新、以前インストールした古いソフトウェアの削除を簡単に行えるようにするためのプログラムのことです。

パッケージというのは、どこかの誰かが作ってくれた便利ツールのことです。

アプリケーションを作るときには、便利なパッケージをいくつか組み合わせて開発をするというわけですね。

Node.jsの2大パッケージ管理ツールこそ、npmとyarnです。

yarnのインストール方法

$ npm install -g yarn

このコマンドを入力し、バージョンを確認できたら、インストール完了です。

$ yarn -v
1.16.0

npmとyarnどちらを使うべき?

好きな方を使えばいいかと思います。

開発してるときに、パッケージマネージャーを使ってる時間なんてそう多くないし、全体の工程からすると、ほんの少しだけです。

インストールの速さとかも別に気にするポイントではないのかなと・・・。

チームで開発するときにも、ある人はnpm。
またある人はyarn。
これでOKだと思います。

ちなみに僕は、Railsチュートリアルで最初に学んだのがyarnだったので、や〜んながらyarnを使ってます。

ここまで偉そうに書いておきながら、npmは一度しか使ったことありません(笑)

aws-sdkを用いてAWS SecretManagerからシークレットを取得する(Node.js TypeScript)

$
0
0

Node.js&TypeScript&lambda環境、aws-sdkを用いて、AWS SecretManagerからシークレットを取得する方法を解説します。

1. aws-sdkをinstall

npm install aws-sdk

2. コードを記述

referSecrets.ts
import*asAWSfrom"aws-sdk";exportconstreferSecrets=async()=>{try{constsecretsManager=newAWS.SecretsManager({region:"ap-northeast-1",})constresponse=awaitsecretsManager.getSecretValue({SecretId:SECRET_NAME,}).promise()returnJSON.parse(response.SecretString)}catch(err){returnJSON.stringify({err},null,2)}}

注意点

  • requireではなく、importを使用する。
    • typeが使用でき、vscodeがメソッドや型を推論してくれます。
  • SECRET_NAMEには、シークレット名を記述
  • iamroleなどの設定は省いています。

参考

【MongoDBを使う前にやりたい認証系の設定】

$
0
0

Node.jsでmongoDB使いたいな。と思って調べていると「セキュリティ設定しっかりやろう」という記事が多くあったので、実際に自分が行ったことをまとめてみました。

参考になれば嬉しいです。


1,そもそも設定を記述するファイルが見当たらない。

MongoDB自体はinstallされていたが、usr/local/etc/mongod.conf が見当たらない。おそらく、いつかの自分はbrewを使わずにインストールしていたのだろう。

というわけで、brew経由でmongo-Community というmongoのオープンソースリソースを利用するため以下コマンドを実行。
brew install mongodb-community
これで、usr/local/etc/mongod.conf が作られた。

2,さっそく設定ファイルにセキュリティ強化のための設定を追記しいく

先程作成したmongod.confに追記する。

・接続を許可するIPアドレスを指定する。
・port番号をデフォルトの27017から変更する。
・mongoDBの認証機能をONに設定する。

bindIp:127.0.0.1port:xxxxxsecurity:authorization:enabled

ここまで行ったら再起動。
brew services restart mongodb-community

接続。(bindIp: 127.0.0.1, port: 40000 の場合)
mongo --host localhost --port 40000

3,DBごとにアクセス制限を設ける

さらにセキュリティを高めるために、各DBにアクセスできるuserを作成していく。

・user管理を行うadminにアクセスできるuserを作成する。
・新規で作成したDBにアクセスできるuserを作成する。

>mongo--hostlocalhost--port40000//接続>useadmin//DB切り替え>db.createUser({user:"「作成したいユーザー名」",pwd:"「パスワード」",roles:[{"role":"root","db":"admin"//roleを割り当てるDBを選ぶ}]})>db.createUser({user:"「作成したいユーザー名」",pwd:"「パスワード」",roles:[{"role":"dbAdmin",//DB管理権限"db":"newDatabase"}]});

4,mongoDBのsecurityが機能しているか確認する。

以下のように、エラーが出ていればDBごとのアクセス権限が機能している。

name@MBP~%mongo--hostlocalhost--port40000>useadmin>db.system.users.find()Error:error:{"ok":0,"errmsg":"command find requires authentication","code":13,"codeName":"Unauthorized"}

以下のように接続できない感じになれば、port番号設定かIPアドレス設定が機能していると考えられる。

name@MBP~%mongoMongoDBshellversionv4.4.3connectingto:mongodb://127.0.0.1:27017/? ~Error:~connect@src~@(connect):2:6exception:connectfailedexitingwithcode1

【つまったこと】

・なんかポート指定してコマンドも間違えず打ってるはずなのに、接続できない!ってなったので解決した方法を載せておく。

結論 : ポート変更前のプロセスが動いていた。

1,mongoは期待したportで動いているのか?を確認。

name@MBP~%sudolsof-i-P|grep"LISTEN"//動いていれば以下のようになる。mongod~name~IPv4~TCPlocalhost:40000(LISTEN)

2,自分の場合、port変更前のプロセスが動いていたので、prosess killして、mongo-communityを再起動

name@MBP~%brewservicesrestartmongodb-community

これで接続できるようになりました。

【おまけ情報】
--dbpath オプションをつけることでDBの保存先を指定することができる。
-logappend オプションでログを追加する形で指定のファイルに残すことができる。
・以下のようにすると、指定ポートで指定ディレクトリにデータファイルを保存することができる。
mongod --dbpath ./data/db --port 40001
・これら設定を設定ファイルに書いておいて、設定ファイルの内容に沿って起動することもできる。

FirebaseのCloud FunctionsからGoogle Cloud Translationを試してみる

$
0
0

AWS EC2インスタンスからFirebaseの環境をセットアップして、Google Cloud FunctionsからGoogle Cloud Translationにアクセスしてみます。

前回はローカルのNode.js環境からGoogle Cloud TranslationのAPIにアクセスしましたが、今回はGoogle Cloud Functionsの環境からです。

firebaseインストールと認証設定

Node.jsはインストール済みとします。

$sudo npm install-g firebase-tools
$firebase --version9.8.0

インストール後、認証設定が必要です。

$firebase login

これは対話型のコマンドです。

Google認証用のURLが表示されますので、同じようにブラウザでアクセスします。

ブラウザ上で承認すると、gcloudの認証設定と違って http://localhost:9005/?...にリダイレクトされます。firebase loginコマンドをローカルで実行している場合は、コマンドが9005番ポートを一時的に待ち受けているので、ブラウザからコマンドに承認の情報が伝わって、勝手にコマンドが完了します。

firebase loginコマンドをリモートで実行し、ブラウザだけローカルの場合には、これができません。ブラウザに表示されているURLをコピーして、リモートに別ターミナルで接続して curl "http://localhost:9005/?..."を実行します。curlコマンドは以下のようなHTMLを出力します。これで認証設定が完了です。

<h1>Firebase CLI Login Successful</h1><p>You are logged in to the Firebase Command-Line interface. You can immediately close this window and continue using the CLI.</p>

firebaseプロジェクト作成

まず空のディレクトリに移ります。

$mkdir sample
$cd sample

プロジェクト設定。

$firebase init

また対話型のコマンドです。

Which Firebase CLI features do you want to set up for this folder?に対して Functions を選択。

Please select an optionに対しては、Google CloudのプロジェクトですでにFirebaseを使ったことがあれば Use an existing projectを選択します。Firebaseを使うのが初めてのGoogle Cloudプロジェクトであれば Create a new projectを選択します。いずれもGoogle Cloudのプロジェクトはすでに存在する前提です。

あとは、 JavaScript or TypeScript の言語選択など、聞かれたことを答えます。

すると、ディレクトリの中にソースコードなどが生成されています。 .gitignoreも生成されていますので、せっかくなので、

$git init
$git add  -A$git ls-files
.firebaserc
.gitignore
firebase.json
functions/.gitignore
functions/index.js
functions/package-lock.json
functions/package.json

生成されたソースコードはこの7ファイルのようです。

Cloud Translation APIを有効化

Google Cloudのコンソールにブラウザでアクセスして、使用するプロジェクトのCloud Translation APIを有効化します。

image.png

ソースコード編集

functions/index.jsに以下のコードを書きました。

constfunctions=require("firebase-functions");const{TranslationServiceClient}=require("@google-cloud/translate").v3;constprojectId="xxxxxxxx";constlocation="us-central1";// 言語判定asyncfunctiondetectLanguage(text){consttranslationClient=newTranslationServiceClient();constreq={parent:translationClient.locationPath(projectId,location),content:text,mimeType:"text/plain"};constres=awaittranslationClient.detectLanguage(req);letsourceLang=null;for(constelemofres){if(elem==null)// なぜかnullがレスポンスに含まれるcontinue;returnelem["languages"][0]["languageCode"];}}// 翻訳asyncfunctiontranslate(text,sourceLang,targetLang){consttranslationClient=newTranslationServiceClient();constreq={parent:translationClient.locationPath(projectId,location),contents:[text],mimeType:"text/plain",sourceLanguageCode:sourceLang,targetLanguageCode:targetLang,};constres=awaittranslationClient.translateText(req);for(constelemofres){if(elem==null)// なぜかnullがレスポンスに含まれるcontinue;returnelem["translations"][0]["translatedText"];}}asyncfunctionsample(text){constresult={};result["original"]=text;// 言語判定constsourceLang=awaitdetectLanguage(text);// 翻訳for(consttargetLangof["en","ja","zh-TW","zh-CN","ko"]){if(targetLang==sourceLang)// Target language can't be equal to source language. というエラーを防ぐためcontinue;consttargetText=awaittranslate(text,sourceLang,targetLang);result[targetLang]=targetText;}returnresult;}exports.translation_demo=functions.https.onRequest(async(request,response)=>{letquery=request.query["q"];if(!query)query="";sample(query).then(result=>{functions.logger.info(result);response.send(result);}).catch(err=>{functions.logger.info(err);});});

functions/package.jsonにはGoogle Cloud Translation用のライブライを追記します。

   "dependencies": {
     "firebase-admin": "^9.2.0",
-    "firebase-functions": "^3.11.0"
+    "firebase-functions": "^3.11.0",
+    "@google-cloud/translate": "*"
   },

デプロイ

デプロイ前に npm installが必要みたいです。

$(cd functions; npm install)

firebase deployします。これでGoogle Cloud Functionsが作成されます。JavaScriptのソースコード中に exports.translation_demoと書いていますので、 translation_demoという名前のFunctionが作られます。

$firebase deploy

(デプロイに時間がかかるのはどうにかならないだろうか・・・)

実行

$curl -Ssf'https://us-central1-xxxxxxxx.cloudfunctions.net/translation_demo?q=%E5%8F%91%E7%94%9F%E5%9C%A82021%E5%B9%B43%E6%9C%8823%E6%97%A5%E5%9F%83%E5%8F%8A%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4%E4%B8%8A%E5%8D%887%E6%97%B640%E5%88%86%EF%BC%8C400%E7%B1%B3%E9%95%BF%E7%9A%84%E9%95%BF%E8%8D%A3%E6%B5%B7%E8%BF%90%E9%9B%86%E8%A3%85%E7%AE%B1%E8%88%B9%E9%95%BF%E8%B5%90%E8%BD%AE%E5%9C%A8%E5%9F%83%E5%8F%8A%E8%8B%8F%E4%BC%8A%E5%A3%AB%E8%BF%90%E6%B2%B3%E6%90%81%E6%B5%85%E3%80%82' | jq .{
  "original": "发生在2021年3月23日埃及标准时间上午7时40分,400米长的长荣海运集装箱船长赐轮在埃及苏伊士运河搁浅。",
  "en": "At 7:40 am Egypt Standard Time on March 23, 2021, the 400-meter-long Evergreen container ship Captain Ci was stranded on the Suez Canal in Egypt.",
  "ja": "2021年3月23日のエジプト標準時午前7時40分、長さ400メートルのエバーグリーンコンテナ船のキャプテンCiがエジプトのスエズ運河で立ち往生しました。",
  "zh-TW": "發生在2021年3月23日埃及標準時間上午7時40分,400米長的長榮海運集裝箱船長賜輪在埃及蘇伊士運河擱淺。",
  "ko": "2021 년 3 월 23 일 이집트 표준시 오전 7시 40 분, 400 미터 길이의 에버그린 컨테이너 선 선장 Ci가 이집트의 수에즈 운하에 좌초했습니다."
}

(固有名詞の翻訳がうまくできてなさそうだけど、まあいいか・・・)


YouTube動画をダウンロードするWebアプリを作る

$
0
0

始めに

YouTube動画をダウンロードするにあたって、無料のWebサービスはありますが、怪しい広告が表示されたりして危なかったり、煩わしかったりします。
ライブラリを使えば自作も簡単にできますので、実装内容についてまとめました。
ここでは以下のものを使って実装しています。

  • Node.js
  • TypeScript
  • ytdl-core
  • Express
  • Heroku

ローカルでYouTube動画をダウンロードする

まず始めにローカルでYouTube動画をダウンロードできるようにします。

以下の記事を参考にして、スクリプトを書きました。ただしこちらではTypeScriptを使って実装しています(と言っても型定義全くないですけど。。)。

Node.jsでYoutube動画をDLして保存するytdl-coreを使ったみたメモ

download.ts
importytdlfrom'ytdl-core';importpathfrom'path';importfsfrom'fs';constBASE_URL='https://www.youtube.com/watch?v=';constYOUTUBE_ID='bnc1NjaXXXX';consturl=`${BASE_URL}${YOUTUBE_ID}`;constvideo=ytdl(url);video.pipe(fs.createWriteStream(path.resolve(__dirname,`./tmp/${YOUTUBE_ID}.mp4`)));video.on('end',()=>{console.log('file downloaded.');});

これでmp4ファイルがダウンロードされました。mp3はffmpegで変換する必要があるようなのでchild_processで変換します。

mp3に変換
importpathfrom'path';import{exec}from'child_process';video.on('end',()=>{constinputFilePath=path.resolve(__dirname,`./tmp/${YOUTUBE_ID}.mp4`);constoutputFilePath=path.resolve(__dirname,`./tmp/${YOUTUBE_ID}.mp3`);// ffmpegでmp3に変換する。yオプションで上書きができる(これがないと、出力先にファイルが存在している場合は止まってしまう)exec(`ffmpeg -y -i ${inputFilePath}${outputFilePath}`,(error,stdout,stderr)=>{if(error){console.error(error);return;}console.log(stdout);console.log(stderr);});});

ExpressでYouTube動画をダウンロードするAPIを作る

ローカルでダウンロードできるようになったので、次はExpressでサーバーを立てて、API経由でダウンロードして動画または音声を返すAPIを作ります。
/api/youtube/bnc1NjaXXXX?fileType=mp4みたいに送ったら対象のYouTubeIdをmp4またはmp3でダウンロードするようにします。

server.ts
// Youtubeのダウンロードapp.get('/api/youtube/:youtubeId',(req,res)=>{const{youtubeId}=req.params;constfileType=(req.query.fileType||'mp4')as'mp4'|'mp3';constdestFilePath=path.resolve(__dirname,`./tmp/${youtubeId}.mp4`);consturl=`https://www.youtube.com/watch?v=${youtubeId}`;conststream=ytdl(url,{quality:'highest'});stream.pipe(fs.createWriteStream(destFilePath));stream.on('error',(err)=>{console.error(err);res.status(400).send('download error!');});stream.on('end',()=>{console.log(`youtube file (${youtubeId}.mp4) downloaded.`);// mp4の場合はそのまま返すif(fileType==='mp4'){res.download(destFilePath);return;}// mp3の場合は変換してから返すconsole.log('transform mp4 -> mp3.');constmp3FilePath=path.resolve(__dirname,`./tmp/${youtubeId}.mp3`);exec(`ffmpeg -y -i ${destFilePath}${mp3FilePath}`,(err,stdout,stderr)=>{if(err){console.error(err);res.status(500).send('movie translation error!');return;}console.log(stdout);console.log(stderr);res.download(mp3FilePath);});});});

フロント側のリクエストは以下のようにしました。ファイルのダウンロードは直接URL遷移してもできますが、エラーの場合だとエラーページが出てしまうので、一度blobで受け取るようにしました。(どうでも良いですが、fetch APIのエラーハンドリングは一々response.okをチェックしないといけないのは面倒ですね。。)

console.log('downloading...');// ファイルダウンロードfetch(`/api/youtube/${youtubeId}?fileType=${fileType}`).then((response)=>{if(!response.ok){thrownewError('Network error');}returnresponse.blob();}).then((blob)=>{consturl=URL.createObjectURL(blob);consta=document.createElement('a');a.href=url;a.download=`${youtubeId}.${fileType}`;document.body.appendChild(a);a.click();a.remove();}).finally(()=>{console.log('finished.');});});

Herokuにデプロイする

最後にHerokuにデプロイします。ExpressをHerokuにデプロイするやり方とほぼ同じですが、今回はffmpegを入れる必要があります。こちらに書かれていたbuildpackを使用しましたが、heroku-cliを使うのが面倒だったのでGUIで入れました。

Herokuでffmpegを使えるようにする

Herokuのアプリ画面に入り、SettingsのBuildpacksというセクションでbuildpackを2つ入れます。今までは自動でheroku/nodejsが使われていたと思いますが、2つ入れる必要があるので明記する必要があります。

スクリーンショット 2021-04-04 15.17.51.png

これで無事デプロイされて動作すれば完了です。

終わりに

以上がYouTube動画をダウンロードするWebアプリを作る流れでした。今回Node.jsでYouTube動画をダウンロードしましたが、音声付きだと画質は360pしか選べなくて少し残念でした。昔PythonでダウンロードしたときはフルHDでも普通にダウンロードできた気がしたんですけどね。。

今回作成したソースとHerokuアプリは以下に上がっております。興味がある方は見てください😄

WSLでのnode管理をnからnodebrewに切り替え

$
0
0

はじめに

Windows10 ProにWSLを入れて開発をしています。
以前はnを使ってnodeのバージョン管理をしていたのですが、
ある問題にぶつかりnodebrewに切り替えました。
そのときのメモです。

問題とは、Windows側にインストールしたRunJSで、
npm packageのインストールができないことでした。
nodebrewに切り替えたら問題なくインストールできるようになりました。

環境

  • Windows 10 Pro
  • WSL
    • version 2
    • Ubuntu 20.04 LTS

方法

n のアンインストール

こちらの記事を参考にしました。

$ sudo n uninstall
$ sudo rm-rf /usr/local/n

nodebrewのインストール

$ curl -L git.io/nodebrew | perl - setup

.bash_profileに以下を環境変数の設定を記述

export PATH="$HOME/.nodebrew/current/bin:$PATH"

環境変数を適用

source .bash_profile

nodeのインストール

nodebrewでnodeをインストールします。
今回は最新の安定板をインストールしました。

$ nodebrew install-binary stable
$ nodebrew use stable
use v14.16.0

# [補足]インストール可能なバージョンの確認$ nodebrew ls-all

インストールされたnodeの確認

$ which node
/home/mypath/.nodebrew/current/bin/node
$ which npm
/home/mypath/.nodebrew/current/bin/npm
$ node -v
v14.16.0
$ npm -v-bash: /mnt/c/Program Files/nodejs/npm: /bin/sh^M: bad interpreter: No such file or directory

npmのパスを誤って認識しているらしい。

トラブルシューティング

.bash_profileのパスの設定を以下のように変更して解決

export PATH="$HOME/bin:$HOME/.local/bin:$HOME/.nodebrew/current/bin:$PATH"

これでWindows側でインストールしたRunJSでもnpm packageをインストールできるようになりました。

Atomのterminal-plusパッケージを動くようにする

$
0
0

事象

Atomのterminal-plusパッケージをインストール後、terminalを起動しようとしても、真っ暗なままでうんともすんとも言わない。
これをどうにかしたい。

(結論から言うと、たぶん terminusパッケージとか使ったほうが早いと思いますが、備忘として残しておきます)

環境

$ sw_vers                                                                                                      
ProductName:    macOS
ProductVersion: 11.2.3
BuildVersion:   20D91
$ node -v
v14.16.0
$ npm -v
6.14.11

% atom --version
Atom    : 1.55.0
Electron: 6.1.12
Chrome  : 76.0.3809.146
Node    : 12.4.0

% apm show terminal-plus
 terminal-plus
├── 0.14.5
├── https://github.com/jeremyramin/terminal-plus
├── A terminal package for Atom, complete with themes and more.
├── 999044 downloads
└── 709 stars

やったこと

~/.atom/packages/terminal-plusでの作業

  • package.json書き換え
package.json
"dependencies":{"atom-space-pen-views":"^2.1.0",// "pty.js": "git+https://github.com/jeremyramin/pty.js.git#28f2667", // この行はを削除"node-pty":">= 0.1.0",// この行を追加"term.js":"git+https://github.com/jeremyramin/term.js.git","underscore":"^1.8.3"},
  • requireするパッケージを変更
lib/process.coffee
# pty = require 'pty.js' # こっちはコメントアウトor削除pty=require'node-pty'#読み込むパッケージ名を変更したものを追加path=require'path'fs=require'fs'_=require'underscore'child=require'child_process'# (略)

$ npm install$ apm rebuildで事象が解消する。

何をしたか

  • pty.jsはterminal-plusで参照されるパッケージ。その名の通り擬似端末をforkしてくれるもの。
  • こちらはあまりメンテされておらず、最近のnode.jsではビルド1が(少なくとも手元の環境では)上手くいかない。
    • pty.jsが更に参照しているnanのバージョンが古いままで、node10系以降では対応できていなかったり、pty.js自体のコードも古かったり。
  • これを、pty.jsのフォークである node-ptyに置き換えてしまう。
    • node-ptyは割とアクティブにメンテされているようにみえるので、こちらを使うのがよさそう。

参考


  1. npm rebuildのこと。 そもそも、apm install terminal-plusの時点でエラーにならずインストール成功したり、terminal-plus起動時に明示的にエラーが出てこないのは何故かは理解できていない。。 

電力密度の計算方法

$
0
0

携帯電話の基地局から発射される電力密度の計算方法です。

等方(アイソロロピック)アンテナとしての計算です。
これは、1点から球状に電波が広がっていくとしたモデルです。

S: 電力密度 W/m^2
P: 電力 W (ワット)
r: 距離 m

S = P / (4 * pi * r^2)

計算例
電力 640W , 距離 5 m の場合

Node.js の例

$ node
Welcome to Node.js v15.13.0.
Type ".help" for more information.
> 640 / (4 * Math.PI * 5**2)
2.0371832715762603
> 

TypeScript の例

$ npx ts-node
> 640 / (4 * Math.PI * 5**2)
2.0371832715762603

Deno の例

$ deno
Deno 1.0.0
exit using ctrl+d or close()
> 640 / (4 * Math.PI * 5**2)
2.0371832715762603

python3 の例

$ python3
Python 3.9.2 (default, Feb 20 2021, 18:40:11) 
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import math
>>> 640 / (4 * math.pi * 5**2)
2.0371832715762603
>>> 

Go の例

$ gore
gore version 0.5.2  :help for help
gore> :import math
gore> 640.0 / (4.0 * math.Pi * math.Pow(5.0,2.0))
2.037183

2.04 W/m^2 は、0.204 mW/cm^2 になります。

参考、
 日本の規制値は、0.1 mW/cm^2
 ICNIRP の規制値は、0.09 mW/cm^2

指向性のアンテナの場合は、強い方向への補正と、弱い方向への補正が必要です。
コンクリートの壁などがあれば、弱い方向への補正が必要です。
コンクリートによる減衰は、材質、厚さにより異なります。

NestJS+PostgreSQL(+pgAdmin4)の開発環境をDockerで構築する

$
0
0

はじめに

NestJS + PostgreSQL(+ pgAdmin4)の開発環境を、dockerで構築する手順を紹介します。

バージョン情報

  • Docker : 19.03.13

  • Docker Compose : 1.27.4

  • Node.js : 14

  • PostgreSQL : 11.2

  • pgAdmin4 : 4.2

ディレクトリ構成

以下のような構成で作成します。

$ tree -L 2  
.├── api
│   └── Dockerfile
├── app    // コンテナ共有ディレクトリ
│   └── [ project directory ]├── db
│   ├── pgadmin4  // DBサーバ接続永続化用ディレクトリ
│   └── postgres  // DBサーバデータ永続化用ディレクトリ
└── docker-compose.yml

Dockerfileの作成

公開されているNode.jsのdockerイメージを使い、PostgreSQLのライブラリを導入します。
ついでにちょっとしたファイル編集用にvimをインストールしておきます。
(主要なファイルは共有フォルダに置くので無くてもよい)

FROM node:14 # share directoryENV ROOT="/app"# add path envENV PATH $PATH:/usr/local/bin# set home directoryRUN mkdir${ROOT}WORKDIR ${ROOT}RUN apt-get update && apt-get upgrade -y# install lib (opt : vim)RUN apt-get install-y build-essential libpq-dev postgresql-client vim
# install NestJSRUN npm install-g @nestjs/cli

docker-compose.ymlの作成

NestJSアプリケーションを構成するコンテナ情報を記載します。
データベース操作用にpgAdmin4を入れてますが、端末に既に入ってたり、使用しないのであれば要らないです。

version:"3.7"services:api:build:apicontainer_name:nest_apitty:trueports:-"3000:3000"volumes:-./app:/app:delegated# share directorylinks:-dbrestart:alwaysdb:image:postgres:11.2container_name:nest_dbports:-"5432:5432"volumes:-./db/postgres/init.d:/docker-entrypoint-initdb.d-./db/postgres/pgdata:/var/lib/postgresql/dataenvironment:POSTGRES_USER:root# DB USERPOSTGRES_PASSWORD:root# DB PasswordPOSTGRES_INITDB_ARGS:"--encoding=UTF-8"hostname:postgresuser:rootrestart:alwayspgadmin4:image:dpage/pgadmin4:4.2container_name:pgadmin4ports:-"8000:80"volumes:-./db/pgadmin4:/var/lib/pgadminenvironment:PGADMIN_DEFAULT_EMAIL:root# pgAdmin AddressPGADMIN_DEFAULT_PASSWORD:root# pgAdmin Passwordhostname:pgadmin4links:-dbrestart:always

postgreSQL(pgAdmin4)に設定しているアドレス、パスワードはrootにしてますけど、そこは各自お任せします。

コンテナを作成&起動する

早速コンテナを作成、および起動します。

docker-compose build
docker-compose up -d

起動後は、一応エラーがないか調べましょう。
(立ち上がらなかったら、原因調査に時間がかなり取られます)

docker ps -a
// 立ち上がってなかった場合、ログを見て原因調査
docker-compose logs

コンテナにログインする

起動後はコンテナにログインし、プロジェクト作成の準備をします。

docker-compose exec api bash
// 以下はログインした後にやってます。作業がやりやすいようにしてるだけなので必須ではないです。
echo'export LS_OPTIONS='\''--color=auto'\'>> ~/.bashrc
echo'alias ll='\''ls $LS_OPTIONS -l'\'>> ~/.bashrc
source ~/.bashrc

PostgreSQLに接続できるか確認

一応、ちゃんと設定できているか確認。

psql -h db -U root -d postgres
// rootを入力
Password for user root: 
psql (9.6.19, server 11.2 (Debian 11.2-1.pgdg90+1))
WARNING: psql major version 9.6, server major version 11.
         Some psql features might not work.
Type "help"for help.

// postgresのテーブルにログインできたら成功
postgres=# 

ん?なんか警告出てる。
クライアントとサーバ間のバージョン差異での警告みたい。
警告が出るだけで、特に問題ないからヨシ!

※ ヨシじゃねーだろ!って人は以下のコマンドでアップグレードしてください。

sh -c"echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' > /etc/apt/sources.list.d/pgdg.list"
wget --quiet-O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
apt-get update
apt-cache search postgresql | grep ^postgresql
// postgresqlのバージョンが増えてるはずなので、対象のパッケージをインストールします。
// サーバ側を11で入れてるのでクライアントも11をインストールします。
apt-get install-y postgresql-client-11

NestJS新規プロジェクトを作成する

コンテナ内でコマンドを発行します。

nest new [ New Project Name]
// npm か yarn どっちを使用するか聞かれるので好みの方を選択する。

NestJS起動

作成したプロジェクトのディレクトリに移動して、NestJSを起動します。

cd[ Project directory ]
// npm
npm run start
// yarn
yarn run start

起動した後は、実際にブラウザで確認します。
localhost:3000にアクセスします。Hello Worldが表示されていたらOK

おまけ: pgAdmin4の確認

pgAdmin4も接続確認してみます。
localhost:8000でアクセスできます。

アドレスとパスワードは、docker-compose.ymlに書いたrootで入ります。
ログインすると下記のような画面になります。

サーバを登録するための設定を行います。
Serversを選択した状態で右クリック→サーバの設定を行います。

サーバ名は自分が分かりやすい名前で。

Connectionタブでホスト名、ユーザ名、パスワードを入力して保存。

無事に接続できました。

最後に

dockerコンテナでNestJSの開発環境を構築しました。
NestJSはAngularに似てるらしいです。
(触ったことがないので私は分かりません。ControllerとServiceはSpringに似てるなぁと思いました)
ここからHTMLを返せるようにするもよし、TypeORMを入れるもよし。
どんどん使っていきましょ〜!

参考

NestJS公式
pgAdmin公式
水無瀬のプログラミング日記 - NestJS事始め

NestJSを触りながら学ぶ(TodoAPI作成)

$
0
0

NestJSとは

Nest (NestJS) is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

Under the hood, Nest makes use of robust HTTP Server frameworks like Express (the default) and optionally can be configured to use Fastify as well!

Nest provides a level of abstraction above these common Node.js frameworks (Express/Fastify), but also exposes their APIs directly to the developer. This gives developers the freedom to use the myriad of third-party modules which are available for the underlying platform.

Node.jsのバックエンドフレームワークです。
デフォルトでExpress(Node.jsのWebアプリケーションフレームワーク)が内部で使用されていますので、Expressの技術を流用できます。
設定変更でFastify(軽量なNode.jsフレームワーク)を使用することも可能です。
またTypescriptで構成されているので、フロントエンドの方でもすんなり入っていけると思われます。

NestJSのアーキテクチャ

NestJSは下図のような構成例で開発することができます。


構成例

  • Module
    • 依存関係の管理を行う。
    • ルートモジュール(上図でいうAppModule)が開始点となる。
  • Controller
    • クライアントからのリクエストを処理し、応答を返す。
    • 上図の構成だと実処理はServiceが担っていることが多く、リソースを取得するエンドポイントだけ定義している。
  • Service
    • NestJSにおけるプロバイダーの一種。依存性の注入に利用される。
    • 上図の構成だとControllerに機能提供している。

環境

  • Node.js v12.19.1
  • PostgreSQL 12.5
  • macOS Catalina

インストール

以下のコマンドでNest CLIをインストールし、プロジェクトを作成します。

$ npm i -g @nestjs/cli
$ nest new TodoApp        // nest new [プロジェクト名]
⚡  We will scaffold your app in a few seconds..

CREATE todo-app/.eslintrc.js (630 bytes)
CREATE todo-app/.prettierrc (51 bytes)
CREATE todo-app/README.md (3339 bytes)
CREATE todo-app/nest-cli.json (64 bytes)
CREATE todo-app/package.json (1962 bytes)
CREATE todo-app/tsconfig.build.json (97 bytes)
CREATE todo-app/tsconfig.json (339 bytes)
CREATE todo-app/src/app.controller.spec.ts (617 bytes)
CREATE todo-app/src/app.controller.ts (274 bytes)
CREATE todo-app/src/app.module.ts (249 bytes)
CREATE todo-app/src/app.service.ts (142 bytes)
CREATE todo-app/src/main.ts (208 bytes)
CREATE todo-app/test/app.e2e-spec.ts (630 bytes)
CREATE todo-app/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? (Use arrow keys)
❯ npm
  yarn
///// npm か yarnか選びます(今回はyarnを選んでる) ////

▹▸▹▹▹ Installation in progress... ☕


🚀  Successfully created project todo-app
👉  Get started with the following commands:

$ cd todo-app
$ yarn run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

出来上がる構成は下記のようになります。

todo-app
    ├── .eslintrc.js          // ESLintの設定ファイル
    ├── .git                  // gitリポジトリのスケルトンディレクトリ
    ├── .gitignore            // git管理除外設定ファイル
    ├── .prettierrc           // Prettierの設定ファイル
    ├── README.md             // NestJSのREADME
    ├── dist                  // ビルド結果出力先
    ├── nest-cli.json         // プロジェクトを整理、構築、デプロイするために必要なメタデータが書いてあるファイル(Monorepo)
    ├── node_modules          // プロジェクトで利用する npm ライブラリの配置先
    ├── package.json          // npm の設定ファイル
    ├── src                   // 実装していくコードの配置先
    ├── test                  // E2Eテストコード配置先
    ├── tsconfig.build.json   // tsconfig.jsonの分割ファイル(デフォルトはトランスパイ対象外ディレクトリが書いてある)
    ├── tsconfig.json         // TypeScript を JavaScript にトランスパイルための設定情報ファイル
    └── yarn.lock             // yarnの構成管理ファイル(npmだとpackage-lock.jsonが生成されてます)

動作確認だけします。

$ cd todo-app
$ yarn run start
yarn run v1.22.5
$ nest start
[Nest] 132   - 11/20/2020, 1:52:37 PM   [NestFactory] Starting Nest application...
[Nest] 132   - 11/20/2020, 1:52:37 PM   [InstanceLoader] AppModule dependencies initialized +56ms
[Nest] 132   - 11/20/2020, 1:52:37 PM   [RoutesResolver] AppController {}: +39ms
[Nest] 132   - 11/20/2020, 1:52:37 PM   [RouterExplorer] Mapped {, GET} route +19ms
[Nest] 132   - 11/20/2020, 1:52:38 PM   [NestApplication] Nest application successfully started +7ms

Nest application successfullyが出力されたらブラウザでhttp://localhost:3000/を開きHello World!が表示されていたらOK。
Ctrl + Cで終了できます。

準備

データベース作成

作成方法はコマンド、クライアントアプリ(pgAdminなど)の使用、どちらでも構いません。

コマンド

postgres=# create database todoapp;
CREATE DATABASE

SQLクライアントアプリ

ファイル削除

srcディレクトリの直下は以下のファイルが配置されています。
今回必要ないものは削ってしまいます。

  • app.controller.spec.ts

削除対象。AppControllerのユニットテスト用ファイル。

  • app.controller.ts

削除対象。デフォルトコントローラ。

  • app.module.ts

ルートモジュール。

  • app.service.ts

削除対象。デフォルトプロバイダ(サービス)。

  • main.ts

NestJSアプリケーションインスタンスを作成するエントリファイル。

main.tsapp.module.tsだけ残ります。
削除ついでにapp.module.tsも修正しておきます。

app.module.ts
import{Module}from'@nestjs/common';@Module({imports:[],})exportclassAppModule{}// AppController と AppService をファイル内から削除

必要パッケージインストール

npm なら npm install [パッケージ名] --save yarn なら yarn add [パッケージ名]で各種インストールします。

パッケージ用途
@nestjs/typeormTypeORM統合(NestJS専用パッケージ)
typeormTypeORM使用
pgPostgreSQL使用
@nestjs/config環境変数使用
class-validatorバリデーション用(パイプ機能で使用)
class-transformerオブジェクト変換用(パイプ機能で使用)

Module作成

構成

今回は下図のようなシンプルな構成で作っていこうと思います。

CLI(Genarate Module)

CLIを使用してModuleを作成します。
nest g module [module名]で自動生成されます。

$ nest g module tasks
CREATE src/tasks/tasks.module.ts (82 bytes)
UPDATE src/app.module.ts (312 bytes)

中身は以下のように、ほとんど空のような状態です。

tasks/tasks.module.ts
import{Module}from'@nestjs/common';@Module({})exportclassTasksModule{}

Controller作成

CLI(Genarate Controller)

こちらもCLIを使用してControllerを作成します。
nest g controller [controller名]で生成されます。
同時にテスト用のファイルも自動生成されます。今回は使用しないので--no-specオプションを追加して生成しないようにします。

$ nest g controller tasks --no-spec
CREATE src/tasks/tasks.controller.ts (99 bytes)
UPDATE src/tasks/tasks.module.ts (170 bytes)

この時点でtasks.module.tsに自動でControllerが追記されています。

tasks/tasks.module.ts
import{Module}from'@nestjs/common';import{TasksController}from'./tasks.controller';// 自動追加@Module({controllers:[TasksController]// 自動追加})exportclassTasksModule{}

ルーティング

各リクエストのルートマッピングは以下のように構成します。

URLHTTPMethodDecoratorContents
/tasksGETgetTasks@Get()登録されているタスク取得
/tasks/[id]GETgetTaskById@Get(/:id)[id]のタスク取得
/tasksPOSTcreateTask@Podt()新規タスク登録
/tasks/[id]DELETEdeleteTask@Delete(/:id)登録されている[id]のタスク削除
/tasks/[id]PATCHupdateTaskStatus@Patch(/:id)登録されている[id]のタスク更新

HTTPプロトコルの参考リンク

コントローラ内のメソッドにデコレータを付与してあげることで、ハンドラとしてマッピングします。

Controller修正

上記の構成で作成します。

tasks/tasks.controller.ts
import{Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,Post}from'@nestjs/common';@Controller('tasks')exportclassTasksController{@Get()getTasks(){return"getTasks Success!"}@Get('/:id')getTaskById(@Param('id',ParseIntPipe)id:number){return`getTaskById Success! Parameter [id:${id}]`}@Post()createTask(@Body('title')title:string,@Body('description')description:string){return`createTask Success! Prameter [title:${title}, descritpion:${description}]`}@Delete('/:id')deleteTask(@Param('id',ParseIntPipe)id:number){return`deleteTask Success! Prameter [id:${id}]`}@Patch('/:id')updateTask(@Param('id',ParseIntPipe)id:number,@Body('status')status:string){return`updateTask Success! Prameter [id:${id}, status:${status}]`}}

@Param()@Body()はHTTPリクエストを取得するためのデコレータです。
基本はExpressのリクエストオブジェクトのプロパティに沿っています。(Express Application Request)

ParseIntPipeはパイプという機能の1つです。
コントローラのメソッドに値が引き渡される前に変換、もしくは検証を行います。
@Param('id', ParseIntPipe)ではidを数値型へと変換します。(変換できなかった場合は例外をスローします)

動作確認

早速確認しましょう。
yarn run start:devでファイル更新があるたびに自動的に再起動してくれるので、開発中はそちらでNestJSを起動するのがオススメです。
APIの確認は素直にツールを使用した方が手間がないです。
参考:

もちろんcurlでも可能。

$ curl -X GET http://localhost:3000/tasks
getTasks Success!
$ curl -X GET http://localhost:3000/tasks/1
getTaskById Success! Parameter [id:1]
$ curl -X POST http://localhost:3000/tasks -d 'title=TEST' -d 'description=NestJS'
createTask Success! Prameter [title:TEST, descritpion:NestJS]
$ curl -X PATCH http://localhost:3000/tasks/1 -d 'status=DONE'
updateTask Success! Prameter [id:1, status:DONE]
$ curl -X DELETE http://localhost:3000/tasks/1
deleteTask Success! Prameter [id:1]

// パイプで変換できない場合、いい感じに返してくれる(内容変更可能)
$ curl -X GET http://localhost:3000/tasks/aaa
{"statusCode":400,"message":"Validation failed (numeric string is expected)","error":"Bad Request"}

リクエストオブジェクトから値が取得できていることが分かると思います。

DTO作成

値を渡す際のバリデーションを有効するためにDTOを定義していきます。

tasks/dto/task-property.dto.ts
import{IsNotEmpty}from"class-validator";exportclassTaskPropertyDto{@IsNotEmpty()title:string;@IsNotEmpty()description:string;}

デコレータの@IsNotEmptyは値に"",null,undefinedを受け入れません。

Pipe作成

statusに対するパイプ機能を実装します。

tasks/pipe/task-status.pipe.ts
import{BadRequestException,PipeTransform}from"@nestjs/common";exportclassTaskStatusPipeimplementsPipeTransform{readonlyallowStatus=['OPEN','PROGRESS','DONE'];transform(value:any){value=value.toUpperCase();if(!this.isStatusValid(value)){thrownewBadRequestException();}returnvalue;}privateisStatusValid(status:any){constresult=this.allowStatus.indexOf(status);returnresult!==-1;}}

PipeTransformを継承することによって検証機能をもつパイプを自ら作成することができます。
上記はstatus'OPEN','PROGRESS','DONE'のいづれかでないとエラーを返しています。

Controller修正(DTO、Pipe追加)

tasks/tasks.controller.ts
import{Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,UsePipes,ValidationPipe}from'@nestjs/common';import{TaskPropertyDto}from'./dto/task-property.dto';import{TaskStatusPipe}from'./pipe/task-status.pipe';@Controller('tasks')exportclassTasksController{@Get()getTasks(){return"getTasks Success!"}@Get('/:id')getTaskById(@Param('id',ParseIntPipe)id:number){return`getTaskById Success! Parameter [id:${id}]`}@Post()@UsePipes(ValidationPipe)createTask(@Body()taskPropertyDto:TaskPropertyDto){const{title,description}=taskPropertyDtoreturn`createTask Success! Prameter [title:${title}, descritpion:${description}]`}@Delete('/:id')deleteTask(@Param('id',ParseIntPipe)id:number){return`deleteTask Success! Prameter [id:${id}]`}@Patch('/:id')updateTask(@Param('id',ParseIntPipe)id:number,@Body('status',TaskStatusPipe)status:string){return`updateTask Success! Prameter [id:${id}, status:${status}]`}}

@UsePipesを付与したメソッドはバリデーションパイプ(今回でいうとTaskPropertyDtoで定義した値の空検証)が有効になります。
updateTaskで作成したパイプを定義していますが、この場合ですとstatusの値にだけおよびます。

Service作成

CLI(Genarate Service)

CLIでServiceを作成します。
nest g service [service名]で生成されます。
今回はテストファイルも作ってみましょう。

$ nest g service tasks
CREATE src/tasks/tasks.service.spec.ts (453 bytes)
CREATE src/tasks/tasks.service.ts (89 bytes)
UPDATE src/tasks/tasks.module.ts (247 bytes)

tasks.module.tsに自動で追記されてます。

tasks/tasks.module.ts
import{Module}from'@nestjs/common';import{TasksController}from'./tasks.controller';import{TasksService}from'./tasks.service';// 自動追加@Module({controllers:[TasksController],providers:[TasksService]// 自動追加})exportclassTasksModule{}

TypeORMの準備

Service実装の前に、DBとのやりとりのためにTypeORMの設定を行っておきます。

接続設定

DBとの接続を設定します。
app.module.tsに下記を追加します。

app.module.ts
import{Module}from'@nestjs/common';import{TypeOrmModule}from'@nestjs/typeorm';// 追加import{TasksModule}from'./tasks/tasks.module';@Module({imports:[TasksModule,// importsに追加TypeOrmModule.forRoot({type:'postgres',// DBの種類port:5432,// 使用ポートdatabase:'todoapp',// データベース名host:'localhost',// DBホスト名username:'root',// DBユーザ名password:'root',// DBパスワードsynchronize:true,// モデル同期(trueで同期)entities:[__dirname+'/**/*.entity.{js,ts}'],// ロードするエンティティ})],})exportclassAppModule{}

この状態でyarn run startをしてみましょう。

[Nest] 118   - 11/29/2020, 6:04:30 AM   [NestFactory] Starting Nest application...
[Nest] 118   - 11/29/2020, 6:04:31 AM   [InstanceLoader] TypeOrmModule dependencies initialized +933ms
[Nest] 118   - 11/29/2020, 6:04:31 AM   [InstanceLoader] AppModule dependencies initialized +4ms
[Nest] 118   - 11/29/2020, 6:04:31 AM   [InstanceLoader] TasksModule dependencies initialized +3ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [InstanceLoader] TypeOrmCoreModule dependencies initialized +1270ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RoutesResolver] TasksController {/tasks}: +3ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RouterExplorer] Mapped {/tasks, GET} route +3ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RouterExplorer] Mapped {/tasks/:id, GET} route +4ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RouterExplorer] Mapped {/tasks, POST} route +5ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RouterExplorer] Mapped {/tasks/:id, DELETE} route +4ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [RouterExplorer] Mapped {/tasks/:id, PATCH} route +2ms
[Nest] 118   - 11/29/2020, 6:04:32 AM   [NestApplication] Nest application successfully started +9ms

エラーが出なければ接続成功です。

Entitiyの作成

以下のようなテーブル内容で作成しようと思います。

Task
id: number
title: string
description: string
status: string

新規にtask.entity.tsを作成します。
TypeORMについての説明は省略させて頂きます。
参考: TypeORM(github)

tasks/tasks.entity.ts
import{BaseEntity,Column,Entity,PrimaryGeneratedColumn}from"typeorm";@Entity()exportclassTaskextendsBaseEntity{@PrimaryGeneratedColumn()id:number;@Column()title:string;@Column()description:string;@Column()status:string;}

yarn run startでNestJSを起動してDBの中を見てみます。
(下記はコマンドで確認してますが、確認できればなんでもよいです。)

todoapp=# \d
            List of relations
 Schema |    Name     |   Type   | Owner
--------+-------------+----------+-------
 public | task        | table    | root
 public | task_id_seq | sequence | root
(2 rows)

todoapp=# \d task
                                    Table "public.task"
   Column    |       Type        | Collation | Nullable |             Default

-------------+-------------------+-----------+----------+----------------------------------
 id          | integer           |           | not null | nextval('task_id_seq'::regclass)
 title       | character varying |           | not null |
 description | character varying |           | not null |
 status      | character varying |           | not null |
Indexes:
    "PK_fb213f79ee45060ba925ecd576e" PRIMARY KEY, btree (id)

ちゃんと同期がとれてますね。

環境毎での設定変更

envファイル

開発時点ではこれでいいのですが、本番環境では同期するとエライ目にあうので、環境変数を読み込むように変更します。
プロジェクトのルートディレクトリ直下に.envディレクトリを新規に作成し、各環境毎の構成ファイルを格納します。

.env/default.env
API_PORT=3000
DB_PORT=5432
DB_NAME=todoapp
DB_HOSTNAME=localhost
DB_USERNAME=root
DB_PASSWORD=root
.env/development.env
DB_SYNC=true
.env/production.env
DB_SYNC=false

環境毎にdevlopment.envproduction.envを切り替えて使います(default.envは他envファイルで上書き可能な共通定義を書いてます)

TypeORMのConfigService

この定義を読み込むTypeORMのConfigServiceを作成します。
srcディレクトリにconfigディレクトリを作成し格納します。

config/typeorm-config.service.ts
import{Injectable}from"@nestjs/common";import{ConfigService}from"@nestjs/config";import{TypeOrmModuleOptions,TypeOrmOptionsFactory}from"@nestjs/typeorm";@Injectable()exportclassTypeOrmConfigServiceimplementsTypeOrmOptionsFactory{createTypeOrmOptions():TypeOrmModuleOptions{constconfigService=newConfigService();return{type:'postgres',host:configService.get<string>('DB_HOSTNAME'),port:configService.get<number>('DB_PORT'),username:configService.get<string>('DB_USERNAME'),password:configService.get<string>('DB_PASSWORD'),database:configService.get<string>('DB_NAME'),entities:[__dirname+'/../**/*.entity.{js,ts}'],synchronize:this.strToBoolean(configService.get<string>('DB_SYNC','false'))};}// get<boolean>が上手く変換してくれないため泣く泣く対応privatestrToBoolean(boolStr:string):boolean{switch(boolStr.toLowerCase().trim()){case"true":case"yes":case"1":returntrue;case"false":case"no":case"0":casenull:returnfalse;default:returnboolStrasunknownasboolean}}}

ConfigServiceのgetメソッドで環境変数を取得できます。
さきほどapp.module.tsに直接記述していたオプション達をここにまとめておきます。

AppModule修正

app.module.ts
import{Module}from'@nestjs/common';import{ConfigModule}from'@nestjs/config';import{TypeOrmModule}from'@nestjs/typeorm';import{TypeOrmConfigService}from'./config/typeorm-config.service';import{TasksModule}from'./tasks/tasks.module';@Module({imports:[/** env読み込み
    *   環境変数NODE_ENVの値によって読み込むファイルを切り替える。
    *   default.envは後続で呼ばれる。同じ変数がある場合は先に定義されているものが優先される。
    */ConfigModule.forRoot({envFilePath:[`.env/${process.env.NODE_ENV}.env`,'.env/default.env'],isGlobal:true,}),// TypeORMの設定を非同期取得に変更TypeOrmModule.forRootAsync({imports:[ConfigModule],useClass:TypeOrmConfigService,}),TasksModule,],})exportclassAppModule{}

これで環境ごとに接続情報を書き換える手間を省けます。

動作確認

分かりやすくコンソールに表示されるようmain.tsを書き換えましょう(ついでlistenポートもenvを参照するようにしちゃいましょう)

main.ts
import{ConfigService}from'@nestjs/config';// 追加import{NestFactory}from'@nestjs/core';import{AppModule}from'./app.module';asyncfunctionbootstrap(){constapp=awaitNestFactory.create(AppModule);constconfigService=newConfigService();// 追加constsync=configService.get('DB_SYNC');// 追加console.log(`TypeORM synchronize is [ ${sync} ]`);// 追加constport=configService.get('API_PORT'); // 追加awaitapp.listen(port);// 3000をportに変更}bootstrap();

NODE_ENVを設定して起動してみます。

// dev
$ NODE_ENV=development yarn run start
yarn run v1.22.5
$ nest start
[Nest] 195   - 11/29/2020, 3:06:23 PM   [NestFactory] Starting Nest application...
~~~ 略 ~~~~
[Nest] 195   - 11/29/2020, 3:06:24 PM   [InstanceLoader] TasksModule dependencies initialized +1ms
[Nest] 195   - 11/29/2020, 3:06:25 PM   [InstanceLoader] TypeOrmCoreModule dependencies initialized +560ms
TypeORM synchronize is [ true ]
~~~ 略 ~~~~

// prod
$ NODE_ENV=production yarn run start
yarn run v1.22.5
$ nest start
[Nest] 195   - 11/29/2020, 3:06:23 PM   [NestFactory] Starting Nest application...
~~~ 略 ~~~~
TypeORM synchronize is [ false ]
~~~ 略 ~~~~

// ランタイム環境に既に環境変数が存在する場合、そちらが優先される
$ NODE_ENV=development DB_SYNC=false yarn run start
yarn run v1.22.5
$ nest start
[Nest] 195   - 11/29/2020, 3:06:23 PM   [NestFactory] Starting Nest application...
~~~ 略 ~~~~
TypeORM synchronize is [ false ]
~~~ 略 ~~~~

(ここまで書いてなんだが、node-config使った方が綺麗にまとまりそうですね・・・)

Serviceの修正

tasks/tasks.service.ts
import{Injectable,InternalServerErrorException,NotFoundException}from'@nestjs/common';import{InjectRepository}from'@nestjs/typeorm';import{Repository}from'typeorm';import{TaskPropertyDto}from'./dto/task-property.dto';import{Task}from'./task.entity';@Injectable()exportclassTasksService{constructor(@InjectRepository(Task)privatetaskRepository:Repository<Task>,){}asyncgetTasks():Promise<Task[]>{returnthis.taskRepository.find();}asyncgetTaskById(id:number):Promise<Task>{constfound=awaitthis.taskRepository.findOne(id);if(!found){thrownewNotFoundException();}returnfound;}asynccreateTask(taskPropertyDto:TaskPropertyDto):Promise<Task>{const{title,description}=taskPropertyDto;consttask=newTask();task.title=title;task.description=description;task.status='OPEN';try{awaitthis.taskRepository.save(task);}catch(error){thrownewInternalServerErrorException();}returntask;}asyncdeleteTask(id:number):Promise<void>{constresult=awaitthis.taskRepository.delete(id);if(result.affected===0){thrownewNotFoundException();}}asyncupdateTask(id:number,status:string):Promise<Task>{consttask=awaitthis.getTaskById(id);task.status=status;awaitthis.taskRepository.save(task);returntask;}}

Controllerから受け取った値で実処理するよう構成しています。

あとはControllerのファイルを修正すれば完成なのですが、Service作成時にテストファイルを作成したので、テストを行っていきましょう。

テスト作成

テストはJestがデフォルトになっています。
NestJSはテストツールに制限があるわけでないので、お好みでよいようです。
今回はデフォルトのJestでいきます。

tasks/tasks.service.spec.ts
import{NotFoundException}from'@nestjs/common';import{Test,TestingModule}from'@nestjs/testing';import{getRepositoryToken}from'@nestjs/typeorm';import{Task}from'./task.entity';import{TasksService}from'./tasks.service';constmockRepository=()=>({find:jest.fn(),findOne:jest.fn(),save:jest.fn(),delete:jest.fn(),});describe('TasksService',()=>{lettasksService:TasksService;lettaskRepository;beforeEach(async()=>{constmodule:TestingModule=awaitTest.createTestingModule({providers:[TasksService,{provide:getRepositoryToken(Task),useFactory:mockRepository}]}).compile();tasksService=awaitmodule.get<TasksService>(TasksService);taskRepository=awaitmodule.get(getRepositoryToken(Task));});describe('getTasks',()=>{it('get all tasks',async()=>{taskRepository.find.mockResolvedValue('mockTask');expect(taskRepository.find).not.toHaveBeenCalled();constresult=awaittasksService.getTasks()expect(taskRepository.find).toHaveBeenCalled();expect(result).toEqual('mockTask');});});describe('getTaskById',()=>{it('find success',async()=>{constmockTask={title:'mockTitle',description:'mockDesc'};taskRepository.findOne.mockResolvedValue(mockTask);expect(taskRepository.findOne).not.toHaveBeenCalled();constmockId:number=1;constresult=awaittasksService.getTaskById(mockId);expect(taskRepository.findOne).toHaveBeenCalled();expect(result).toEqual(mockTask);});it('task is not found',async()=>{constmockId:number=1;taskRepository.findOne.mockResolvedValue(null);expect(tasksService.getTaskById(mockId)).rejects.toThrow(NotFoundException);});});describe('createTask',()=>{it('insert task',async()=>{constmockTask={title:'mockTitle',description:'mockDesc'};taskRepository.save.mockResolvedValue(mockTask);expect(taskRepository.save).not.toHaveBeenCalled();constresult=awaittasksService.createTask(mockTask.title,mockTask.description);expect(taskRepository.save).toHaveBeenCalled();expect(result).toEqual({title:mockTask.title,description:mockTask.description,status:'OPEN'});});});describe('deleteTask',()=>{it('delete task',async()=>{taskRepository.delete.mockResolvedValue({affected:1});expect(taskRepository.delete).not.toHaveBeenCalled();constmockId:number=1;awaittasksService.deleteTask(mockId);expect(taskRepository.delete).toHaveBeenCalledWith(mockId);});it('delete error',async()=>{taskRepository.delete.mockResolvedValue({affected:0});constmockId:number=1;expect(tasksService.deleteTask(mockId)).rejects.toThrow(NotFoundException);});});describe('updateTask',()=>{it('update status',async()=>{constmockStatus='DONE';tasksService.getTaskById=jest.fn().mockResolvedValue({status:'OPEN'});expect(tasksService.getTaskById).not.toHaveBeenCalled();constmockId:number=1;constresult=awaittasksService.updateTask(mockId,mockStatus);expect(tasksService.getTaskById).toHaveBeenCalled();expect(taskRepository.save).toHaveBeenCalled();expect(result.status).toEqual(mockStatus);});});});

beforeEachで呼ばれているTest.createTestingModuleはモックやオーバーライドなど、クラスインスタンスの管理を容易にしてくれるフックの機能があります。
今回はDBのアクセスはモックにするので、Repositoryクラスのfindsaveはオーバライドしています。

テスト実行

yarn run testで実行しましょう。(yarn run test:watchでファイルが更新する度テストをしてくれることも出来ます)

Watch Usage: Press w to show more.
 PASS  src/tasks/tasks.service.spec.ts (21.055 s)
  TasksService
    getTasks
      ✓ get all tasks (9 ms)
    getTaskById
      ✓ find success (5 ms)
      ✓ task is not found (19 ms)
    createTask
      ✓ insert task (6 ms)
    deleteTask
      ✓ delete task (5 ms)
      ✓ delete error (5 ms)
    updateTask
      ✓ update status (5 ms)

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        21.135 s, estimated 22 s

なんだが良く分からないけどエラーがないのでヨシ!(テストムズカシイ・・・)

Module & Controller修正

ControllerはServiceに値を渡すよう修正しましょう。

tasks/task.controller.ts
import{Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,Post,UsePipes,ValidationPipe}from'@nestjs/common';import{TaskPropertyDto}from'./dto/task-property.dto';import{TaskStatusPipe}from'./pipe/task-status.pipe';import{Task}from'./task.entity';import{TasksService}from'./tasks.service';@Controller('tasks')exportclassTasksController{constructor(privatetasksService:TasksService){}@Get()getTasks():Promise<Task[]>{returnthis.tasksService.getTasks();}@Get('/:id')getTaskById(@Param('id',ParseIntPipe)id:number):Promise<Task>{returnthis.tasksService.getTaskById(id);}@Post()@UsePipes(ValidationPipe)createTask(@Body()taskPropertyDto:TaskPropertyDto):Promise<Task>{returnthis.tasksService.createTask(taskPropertyDto);}@Delete('/:id')deleteTask(@Param('id',ParseIntPipe)id:number):Promise<void>{returnthis.tasksService.deleteTask(id);}@Patch('/:id')updateTask(@Param('id',ParseIntPipe)id:number,@Body('status',TaskStatusPipe)status:string):Promise<Task>{returnthis.tasksService.updateTask(id,status);}}

Moduleはリポジトリをimportしておきます。

tasks/tasks.module.ts
import{Module}from'@nestjs/common';import{TypeOrmModule}from'@nestjs/typeorm';// 追加import{Task}from'./task.entity';// 追加import{TasksController}from'./tasks.controller';import{TasksService}from'./tasks.service';@Module({imports:[TypeOrmModule.forFeature([Task])// 追加],controllers:[TasksController],providers:[TasksService]})exportclassTasksModule{}

確認

yarn run startで起動して確認します。

// get (登録なし)
$ curl -X GET http://localhost:3000/tasks
[]
// create (1)
$ curl -X POST http://localhost:3000/tasks -d 'title=Test' -d 'description=testDesc'
{"title":"Test","description":"testDesc","status":"OPEN","id":1}
// get (登録あり)
$ curl -X GET http://localhost:3000/tasks
[{"id":1,"title":"Test","description":"testDesc","status":"OPEN"}]
// create (2)
$ curl -X POST http://localhost:3000/tasks -d 'title=Nest' -d 'description=nestjs'
{"title":"Nest","description":"nestjs","status":"OPEN","id":2}
// get (id指定)
$ curl -X GET http://localhost:3000/tasks/2
{"id":2,"title":"Nest","description":"nestjs","status":"OPEN"}
// get (複数登録あり)
$ curl -X GET http://localhost:3000/tasks
[{"id":1,"title":"Test","description":"testDesc","status":"OPEN"},{"id":2,"title":"Nest","description":"nestjs","status":"OPEN"}]
// update
$ curl -X PATCH http://localhost:3000/tasks/2 -d 'status=DONE'
{"id":2,"title":"Nest","description":"nestjs","status":"DONE"}
// get (update確認)
$ curl -X GET http://localhost:3000/tasks/2
{"id":2,"title":"Nest","description":"nestjs","status":"DONE"}
// delete
$ curl -X DELETE http://localhost:3000/tasks/2
// get (delete確認)
$ curl -X GET http://localhost:3000/tasks
[{"id":1,"title":"Test","description":"testDesc","status":"OPEN"}]
// 不正な値の確認
$ curl -X POST http://localhost:3000/tasks -d 'title=Nest' -d 'description='
{"statusCode":400,"message":["description should not be empty"],"error":"Bad Request"}
$ curl -X PATCH http://localhost:3000/tasks/2 -d 'status=aaa'
{"statusCode":400,"message":"Bad Request"}

まとめ

NestJSを使って必要最低限なREST方式で実装してみました。
ここに紹介しているだけじゃなくNestJSには、もっと便利な機能がありますし拡張性が高いです。
(書いている途中で記事じゃなく本の方が見やすかったと後悔・・・)
唯一日本語ドキュメントが少ないという意見を良く見かけますが、今では日本語の記事も多くなってきて調べやすい環境になっているのかなと思います。

Lambda関数で「AWS-SDK」をローカル環境でのみ読み込み、本番デプロイでは除外する方法

$
0
0

結論

こうすればいい

cd ~
# グローバルパッケージのインストール先確認
npm root -g# グローバルインストール
npm install-g aws-sdk
# グローバルパッケージが本当にインストールされたかを確認する
npm list -g--depth=0
# ローカルパッケージとして読み込みたいプロジェクトディレクトリに移動するcd ~/environment/myLambdaProjectName
# グローバルパッケージとローカルパッケージのシンボリックリンクを作成する
npm link aws-sdk
# ローカルパッケージとして読み込まれているかを確認する
npm list --depth=0

あとはこれをZIPファイルにまとめてデプロイするなり、なんなりと

何がしたいか

私が開発したあるプロジェクトは、DynamoDBからレコードを取り出して、
RdsDBにINSERTするという行為をLambda関数で実現するというものでした。

本番環境のLambdaは最初からAWS-SDKパッケージが用意されているので、
ローカルパッケージとしてZIPファイルに含める必要はありません。ファイルサイズが増えるだけです。

しかし、ローカル環境(Cloud9)にAWS-SDKパッケージがないので、
今までは npm install aws-sdkして毎回AWS-SDKをローカルインストールして、
本番デプロイ前に手動でaws-sdkディレクトリを削除するという何ともアレな運用をしていました。

何か、ローカル環境だけAWS-SDKを読み込ませて、
本番デプロイ時は手動削除しないで良い方法を探したら、上の方法を見つけた次第です。

他に良い方法をご存じの方がいましたら、教えてください。

追記

Cloud9環境で、ローカルからLambdaにデプロイする場合は、次の手順で行います

cd ~/environment/myLambdaProjectName
zip --symlinks-r hoge.zip .

左サイドバー > AWS > AWS: Explorer > Asia Pacific (Tokyo) > Lambda > {myFunctionName}

アップロードしたいLambda関数名を右クリック、Upload Lambda > ZIP Archiveを選択、
先ほどZIP圧縮したファイルを選択して、OKボタンを押すと、デプロイできます

うまくいけば、画面右上にちっさく Successfully uploaded Lambda function {myFunctionName}が表示されます

本当にAWS-SDKが除外されてアップロードされているかは、以下の手順で確認できます

  1. Lambdaコンソール画面に移動

  1. Lambda > 関数 > {myFunctionName} に移動

  2. 画面右上、アクション > 関数のエクスポート > デプロイパッケージのダウンロード

  3. ダウンロードしたZIPファイルを開き、node_modulesディレクトリ内にaws-sdkディレクトリがないことを確認する

Upload Lambda > Directory を使うことでもデプロイはできますが、
そちらはシンボリックリンクを辿って、
実ファイルを含んだ状態でアップロードしてしまうので、
ファイルサイズを抑えることができません。


今まで起きたエラーを管理するアプリ作ってみた

$
0
0

フロントをReact、サーバーサイドをNode、Expressで作成しました。

DBとしてFirebase Cloud Firestoreを使用し、フロントをFirebase Hosting、サーバーをHerokuにデプロイしました。

作ったアプリについて

私は現在エンジニアとしてインターンを2年間やっており、エラーが出たときに「このエラーどっかで見たなぁ」みたいなことが最初の頃よくありました。

今回はこのようなことを解消するためのアプリを作成しました。

使用方法

  1. 起きたエラーを入力
  2. カテゴリーを選択(React,Nodeなど)
  3. 解決できているならば参考になったURLを記入
  4. メモを書く

という流れになります。

今までに起きたエラーを蓄積していくことで、もし同じエラーが起きた場合は、エラーを入力した際に知らせてくれて再び解決策を探す作業が省くことができます。

image.png

image.png

image.png

こちらが作ったアプリ( Error Manager )になりますが

現在はサインアップを実装しておらず、私だけが使えるテスト段階です。

詰まったところ

Reactでのログイン認証

色々と調べたところ、しっかりとしたアプリならばセキュリティを考慮して自分でログイン認証を作成するべきではなく、Auth0などのサービスを使用したほうがいいというのが結論でした。

ただ、今回のような小規模な重要な情報を保持しないアプリであれば、LocalStorageやCookieを使って実装してもよいという意見もあり、CookieとFirebase Authenticationで実装しました。

Firebase Authenticationでログイン認証を行い、ログインしたらCookieにID、Passwordを保持し、ログアウトしたらCookieのID、Passwordを削除するという方法になります。

Firebase Authenticationのログイン認証の実装に関しては公式のドキュメントがわかりやすいので詳しい設定はこちらを参考に。

サインイン画面でSubmitボタンを押すと、以下のsigninFunc()が動き、
signInWithEmailAndPasswordでFirebase Authenticationの認証が成功したらCookies.setでemail、password、uid(Firebase Authentication側で勝手に割り振られるID)をCookieに書き込みます。
認証失敗したらEmailもしくはPasswordが間違っている可能性があることをユーザーに知らせます。
Cookieを操作するライブラリはjs-cookieを使用しました。

signin.js
constsigninFunc=()=>{constemail=document.getElementById("email").valueconstpassword=document.getElementById("password").valuefirebase.auth().signInWithEmailAndPassword(email,password).then((user)=>{setOpenErr(false)setOpenSuc(true)Cookies.set("email",email,{"expires":7})Cookies.set("password",password,{"expires":7})Cookies.set("uid",user.user.uid,{"expires":7})document.location="/"}).catch((error)=>{setOpenErr(true)varerrorMessage=error.message;console.log(errorMessage)});}

ログイン認証後は、Cookieにあるemail、password、uidを使ってホーム画面に遷移する際に認証します。

App.js
import'./App.css';importSignInfrom'./pages/signIn';importSignUpfrom'./pages/signUp';importMainfrom'./pages/main'import{BrowserRouter,Route,Switch}from'react-router-dom';importAuthfrom'./components/Auth';functionApp(){return(<BrowserRouter><Switch><Routepath="/signin"component={SignIn}/>
<Auth><Switch><Routepath="/"exactcomponent={Main}/>
</Switch>
</Auth>
</Switch>
</BrowserRouter>
);}exportdefaultApp;
User.js
importCookiesfrom'js-cookie'importfirebasefrom'../firebase/firebase'exportfunctionisLoggedIn(){constemail=Cookies.get("email")constpassword=Cookies.get("password")if(!(email&&password))returnfalseconstisLogin=firebase.auth().signInWithEmailAndPassword(email,password).then((user)=>true).catch((error)=>{consterrorMessage=error.message;returnfalse});returnisLogin;}exportfunctionlogoutFunc(){Cookies.remove("email")Cookies.remove("password")Cookies.remove("uid")document.location="/signin"}

ログインしていないとホーム画面に遷移できないようにする方法はこちらの記事を参考にしました。

n (Node.js管理) のインストール手順

$
0
0

n

nvmやnodebrewなどと同じく、Node.jsのバージョン管理ツールの1つです。

対象読者

・MacOSユーザー
※nはWindowsをサポートしていません。

前提

Homebrewがインストール済み

インストール済みの場合は、バージョンが表示されます。

$ brew -v
Homebrew 3.0.4

バージョンが表示されなかった方は、インストールを行います。

/bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

インストール

brewコマンドを使って、nをインストールします。

brew install n

バージョンの確認

コマンドを実行しバージョンが表示されれば、正常にインストールされています。

$ n -V
7.0.0

Node.js LTS版のインストール

試しにNode.jsのLTS(最新安定版)をインストールしてみます。

準備

nはデフォルトで/usr/local以下に、n/versionsディレクトを作成し、そこでバージョンを管理を行います。

/usr/local以下はroot権限のディレクトリに該当し、通常ユーザーではアクセスできないディレクトリです。

nコマンドでNode.jsをインストールする前に、以下のいずれかの対応が必要です。

  1. sudo コマンド で実行
  2. 環境変数 N_PREFIXに、ホームディレクトリなどをインストール先を指定
  3. /usr/local/nを作成し、ユーザーの読み取り権限を付与

今回は、nコマンド実行時に毎回sudoを叩きたくない&環境変数を増やしたくないので、
3./usr/local/n を作成し、ユーザーの読み取り権限を付与を行います。

# make cache folder (if missing) and take ownership$ sudo mkdir-p /usr/local/n
$ sudo chown-R$(whoami) /usr/local/n

README.mdから引用

LTSのバージョン確認

インストールコマンドを叩く前に、LTSのバージョンを確認します。

$ n --lts
14.16.0

インストール

nコマンドの引数にltsを渡して実行します。

$ n lts

正常にインストールされると以下のような表示となります。

  installing : node-v14.16.0
       mkdir : /usr/local/n/versions/node/14.16.0
       fetch : https://nodejs.org/dist/v14.16.0/node-v14.16.0-darwin-x64.tar.xz
   installed : v14.16.0 (with npm 6.14.11)

失敗した場合

権限が無いと以下の通り怒られるので、権限付与のコマンドを実行して再実行するか、管理者権限で実行してください。

  installing : node-v14.16.0
       mkdir : /usr/local/n/versions/node/14.16.0
mkdir: /usr/local/n/versions/node/14.16.0: Permission denied

  Error: sudo required (or change ownership, or define N_PREFIX)

Node.js 最新版のインストール

※LTS版ではなく、最新版を使いたい場合はこちら

最新バージョンの確認

n ls-remote latest

インストール

引数にlatestもしくは、current を渡して実行します。

$ n latest
## もしくは$ n current

Node.js バージョン指定してインストール

インストール可能なバージョンを確認

# インストール可能なバージョンを表示(最新から20件)$ n lsr 

# 全てのインストール可能なバージョンを表示$ n --all lsr

インストール

一覧に表示された中から、インストールしたいバージョンを引数で指定します。

$ n 15.10.0

バージョンの切り替え

すでにインストールしている中から、自由に切り替えることができます。

$ sudo n

nコマンドを実行するとインストール済みのバージョン一覧が表示されます。
上下キーでバージョンを選択し、Returnキーで切り替えることが可能です。

正常に切り替わると以下のように表示されます。

  installed : v14.16.0 (with npm 6.14.11)

失敗した場合

以下のようにズラズラ〜っとPermission deniedが出たあとに、
installedが表示された場合は正常に切り替わっていません。

sudoでnコマンドを実行し直してください。

cp: /usr/local/lib/node_modules/npm/lib/uninstall.js: Permission denied
cp: /usr/local/lib/node_modules/npm/.mailmap: Permission denied
cp: /usr/local/lib/node_modules/npm/.travis.yml: Permission denied
cp: /usr/local/lib/node_modules/npm/tap-snapshots/test-tap-fund.js-TAP.test.js: Permission denied
cp: /usr/local/lib/node_modules/npm/tap-snapshots/test-tap-repo.js-TAP.test.js: Permission denied
cp: /usr/local/lib/node_modules/npm/.licensee.json: Permission denied
  installed : v14.16.0 (with npm 6.14.11)

(おまけ) nコマンドでsudoしたくない人向け

以下のコマンドでユーザーに権限を付与することでsudoが不要になります。

# take ownership of node install destination folderssudo chown-R$(whoami) /usr/local/bin /usr/local/lib /usr/local/include /usr/local/share

README.mdから引用

感想

nの一文字だけでスマートにNode.jsのバージョンを切り替える事がメリットだと感じました。

ただし、切り替え速度に関して、自環境ではバージョン切り替えが完了するまで、8~10秒くらいかかりました。※環境に左右されると思います。

バージョン切替時の処理内容が、/usr/local/lib/node_modules/npm以下を全削除したあと、
指定のバージョンのNode.jsを入れ直しているため、そのぶん時間がかかってると思われます。

少し切替速度は気になりましたが、nコマンドでスマートにバージョン切り替えをしてみたいという方は、ぜひ参考にしてみてください。

【JavaScript】分割代入について

$
0
0

配列やオブジェクトの値を分割して代入したいときに有効

配列

sample.js
constarray=[1,2,3];// e0...array[0]...e1...array[1]...e2...array[2]............const[e0,e1,e2]=array;console.log(e0)// 1console.log(e1)// 2console.log(e2)// 3

オブジェクト

オブジェクトを変数に代入するときに分割して代入することができる

sample.js
constobj={a:10,b:20,c:30};const{a,c}=obj;console.log(a);// 10console.log(c);// 30

動作確認

docker run -it --rm node:lts-alpine sh
/# vi test.js
# ファイルをコピペ
/# node test.js

Node.js入門編

$
0
0

Node.jsとは?

フロントエンドエンジニアですが、技術の幅を広げる為にNode.jsをやろうと思い、まずは概要を簡単ですがまとめました。

download.png
Node.jsはサーバーサイドのJavaScript実行環境と言われてますが、これだけだとピンとこないですね。さらに詳しく説明すると、
Node.jsは「V8」というJavaScript実行エンジンで作られてる。
(V8はGoogleが開発しているオープンソースのJavaScript実行エンジンです。V8はNode.jsに限らず、GoogleChromeでも採用されています。)

つまり、Node.js環境をサーバー上に作る
→ サーバーサイドでJavaScriptを実行できる
JavaScriptでサーバーサイドを実装できるという事です。

もちろんNode.js固有の文法やAPIが沢山あるので、フロントエンドのJS力だけで良いという訳ではないですが、慣れ親しんだJSでサーバーも作れるのは全く違う言語を覚えるよりはとっつきやすいかもしれません。(ただ本気でNode.jsを習得するのと他のサーバー言語を習得するコストはあまり変わらないとも言われてます。。)

ちなみに、他ブラウザのJavaScript実行エンジンは、
・Firefox: SpiderMonkey
・Safari: JavaScriptCore
・MicrosoftEdge: Chakra/V8
らしいです。

クライアントの開発環境としても使われる
ReactやVueなどの開発に使われるwebpack-dev-serverを使ってローカルサーバーを立てるのにNode.jsは使われています。

npmとは?

Node Package Managerの略で、Node.jsの実装はnpmを活用して行ないます。

npmに登録されてるパッケージであれば(=Node.jsパッケージとして流通してる)、npmを使って簡単にインストールできます。npmでインストールしたパッケージはnode_modules配下に入りimport, requireで使えます。

「パッケージ」というのはライブラリやフレームワークのことで、わざわざライブラリのLPに行ってダウンロードして、プロジェクトディレクトリに置いて、なんて事しなくてもnpmコマンドを使えば一発で取り込めると言うことです。

yarn: Facebook製のパッケージマネージャー
npmでインストールする各種パッケージは本体はリモートにあるので、それを取得してインストールするクライアントはnpmじゃなくてもいい。yarnは高速で信頼性が高く、依存関係も安全に保てる。

余談:
以前業務でフロントのプロジェクトの中に組み込まれていた結構大きめな処理を外に出して、npmに登録し、外部ライブラリとして取り込んで使う運用になった事がありました。

Express

Expressは、Node.jsで一番人気なフレームワークです。
プレーンなNode.jsだと大変な開発を楽にしてくれる優れものです。

以下、基本手順のコードだけを抜粋して紹介します。
・Expressオブジェクトの用意
・appオブジェクトの作成
・ルートの設定
・待ち受け開始

//Expressオブジェクト、appオブジェクトの用意import*asexpressfrom"express";constapp=express();//第1引数のアドレスにアクセスした(リクエストが来た)時に、第2引数の関数を呼ぶapp.use('/users',users)

ルートの設定は、routesディレクトリに各ルートのJSファイルを用意して設定します。

constrouter=express.Router();router.get('/',(req,res)=>{//この中で処理をして、最終的に何かしらのレスポンスをする//res.send()であれば、引数の値をレスポンスとして返すconstresponseData="Welcome to Express";res.send(responseData);})
app.listen(3000)

これでプログラムを実行して、http://localhost:3000にアクセスすると「Welcome to Express」と表示されます。

Express Generator
Expressでアプリケーションを作っても、必要なファイル、モジュールなどは全て自前で用意しないといけないのですが、「基本セット」を最初から用意してくれるライブラリがExpress Generatorです。
Reactでいうところのcreate-react-appみたいなものかと思います。個人開発や学習の為なら間違いなくこれを使ってやるべきですね。

まとめ

本編では、あくまでも基本的な概念やExpressの基本のまとめです。
次は実際に定番のToDoアプリを作ってデプロイまでやってみますので、そこでは詳しいコードを交えた記事にしたいと思います。

Fastify+Let's Encryptでhttpsサーバーを立てる

$
0
0

はじめに

そういえばFastifyでLet's Encryptを導入したことなかったのを思い出したので検証のためにやってみた。ついでにLet's Encryptの記事を書いてなかった気がするので証明書作成の方法も丁寧に書いておこうと思う。

環境
- AWS EC2 Ubuntu 20.04の適当なインスタンス
- セキュリティグループで HTTPS(443)を開けておく
- インスタンスにドメインを割り当てておく今回はテスト用に letsencrypttest.bathtimefish.comをRoute53で割り当てた

Let's Encryptで証明書を発行する

まずは certbotをインストールする

sudo apt update
sudo apt upgrade
sudo apt install-y certbot

対話形式でStandaloneな証明書を発行する

sudo certbot certonly --standalone

いろいろ聞かれる。メールアドレスを入力

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Enter email address (used for urgent renewal and security notices)(Enter 'c' to
cancel): hoge@bathtimefish.com

同意する

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

はい

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

ドメイン名を入力する

lease enter in your domain name(s)(comma and/or space separated)(Enter 'c'
to cancel): letsencrypttest.bathtimefish.com

証明書ができた

Obtaining a new certificate
Performing the following challenges:
http-01 challenge for letsencrypttest.bathtimefish.com
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/letsencrypttest.bathtimefish.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/letsencrypttest.bathtimefish.com/privkey.pem
   Your cert will expire on 2021-07-03. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

証明書は /etc/letsencrypt/live/[ドメイン名]下に作成される

sudo ls /etc/letsencrypt/live/letsencrypttest.bathtimefish.com
README  cert.pem  chain.pem  fullchain.pem  privkey.pem

要はこいつをFastifyで読み込めば良い

httpsサーバーを作成する

node.jsをセットアップする

sudo apt install-y nodejs npm
sudo npm i -g n
sudo n latest
sudo npm i -S typescript ts-node 
sudo apt remove nodejs
sudo apt autoclean

サーバー開発に必要なものをセットアップする

mkdir-p ./work &&cd$_
npm i -S fastify
npm i -D @types/node
npx tsc --init

コードは以下

index.ts
import*asfsfrom'fs';import*ashttp2from'http2';importfastifyfrom'fastify';constcertPath='/etc/letsencrypt/live/letsencrypttest.bathtimefish.com';constserver=fastify({logger:true,http2:true,https:{key:fs.readFileSync(`${certPath}/privkey.pem`),cert:fs.readFileSync(`${certPath}/fullchain.pem`)},});server.get('/',function(request,reply){reply.code(200).send({hello:'world'});});server.listen(443,'0.0.0.0',(err,address)=>{if(err){console.error(err);process.exit(1);}});

実行する

証明書が/etc下にあるのとポート443を使うのでsudo付きで実行する。

sudo ts-node index.ts

ローカルでhttpsアクセスしてみる

curl https://letsencrypttest.bathtimefish.com/
{"hello":"world"}

うまくいった。

おわりに

expressと似たような感じでFastifyでも簡単にhttps化できる。Fastifyのノウハウってけっこう少ない気がするのでこういうtipsもちょっとずつ書いていったほうがいいかな。忘れるし。

Viewing all 8934 articles
Browse latest View live