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

node.jsでgoogleapisを使い、gmailでメール送信

$
0
0

事前準備

https://developers.google.com/gmail/api/quickstart/nodejs
とりあえずこの通りにして、ラベルが表示されるまでやってみる

スコープの変更

サンプルコードだとreadonlyだけなので、メール送信出来るようにスコープを変える

constSCOPES=['https://www.googleapis.com/auth/gmail.readonly'];

を下のように変更

constSCOPES=['https://www.googleapis.com/auth/gmail.send'];

既に出来上がっているtoken.jsonはスコープが最初のままなので、このファイルを消す

メールの作成

メールは素のテキストで書くか、何らかのライブラリを使って組み立てるかして、最終的にはテキストにして、さらにBase64でエンコードしたものを使う必要がある
参考1
参考2

探した所、nodemailerのmail-composerが良さそうだった

importmailComposerfrom"nodemailer/lib/mail-composer"constcreateMail=async(subject,to,text)=>{constmail=awaitnewmailComposer({to,text,subject,textEncoding:"base64",}).compile().build()constraw=Buffer.from(mail).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")returnraw}

メールの送信

サンプルコードのlistLabelsの要領で、gmailを作っておく

参考1
参考2これを見る限り、rawrequestBodyの中に入れるっぽいし、typescriptの定義を見てもそうらしい(巷のサンプルコードはresourceだったりする)
(今回インストールのバージョンはnpm install googleapis@39 --save)

constsendMail=async(subject,to,text)=>{constraw=awaitcreateMail(subject,to,text)awaitgmail.users.messages.send({userId:"me",requestBody:{raw,},})}

自分のメールアドレスを動的に取る

fromアドレスをハードコードしたくない場合

constFROM_ADDRESS_PATH="from.json"gmail.users.getProfile({userId:"me"}).then(({data:{emailAddress}})=>{fs.writeFile(FROM_ADDRESS_PATH,JSON.stringify({from:emailAddress}),(err)=>{if(err)returnconsole.error(err)console.log("from address stored:",FROM_ADDRESS_PATH)})})

こんな感じで自分のプロファイルを呼んで、ファイルに落とし込んでおいたりしてメール作成時にFROMの設定をこの書き出したファイルからやるとか、出来る


Keystonejs Tutorials #8/Custom Field2

$
0
0

前ページ https://qiita.com/bontensuzuki/items/8f91e3f0128b6be1ecbb

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/custom-fields

実装

ここにあるソースを説明しています
https://github.com/keystonejs/keystone/tree/master/demo-projects/custom-fields/

まず、Implementation.jsを見てみましょう。

const{Integer}=require('@keystonejs/fields');classStarsextendsInteger.implementation{extendAdminMeta(meta){return{...meta,starCount:this.config.starCount||5};}}module.exports={Stars,MongoIntegerInterface:Integer.adapters.mongoose,KnexIntegerInterface:Integer.adapters.knex,};

アダプターインターフェイスと共にフィールド実装クラスをエクスポートするための既存のフィールドタイプとの規則。

アダプターは、フィールドとデータベース間のインターフェースを提供します。ここでは、クエリとミューテーションをSQLまたはMongoDBアクションに変換する方法を定義します。繰り返しますが、Startsの実装はIntegerフィールドタイプと同じになるため、Integerフィールドタイプのアダプタを再エクスポートするだけで済みます。

データの保存方法については何も変更しないので、ここでは、Integerフィールドのアダプターを再エクスポートします。

実装クラスは、Keystoneバックエンドで使用されます。それは多くのものを定義します。まず、タイプ、クエリ、リゾルバを含むGraphQLインターフェース。次に、フィールドが順序付け可能かどうかなどのフィールドプロパティ。最後に、Keystoneが管理UIに渡す必要のあるデータ。

Starsフィールドの場合、Integerの実装から変更したいのは、星の数の構成オプションを追加することだけです。このため、Integer実装クラスを拡張し、メソッドextendAdminMetaをオーバーライドできます。

ビュー(Views)

これでバックエンドインターフェイスが用意できたので、UIコンポーネントを見てみましょう。

これは通常、コントローラーから始まります。コントローラーは、フィルタリング、デフォルト値、データのシリアル化、ラベルリゾルバー、一部のGraphQlオプションなど、フロントエンド機能の動作を定義します。この例のフィルタリングでは、デフォルト値とコントローラーのアスペクトはすべてIntegerフィールドと同じになります。もう一度、Integerコントローラーを参照するだけにします。

注:Integer.views.Controllerもnode_modulesフォルダー内のパスに解決され、ビルド時にKeystoneによってバンドルされます。

残りのビューは、管理UIでレンダリングされるReactコンポーネントに関連しています。まず、カウントと値の小道具に応じて、塗りつぶされたまたは輪郭が描かれた星の数をレンダリングする一般的なコンポーネントを作成します。したがって、5つの評価のうち3つ星は次のようになります。

<Starscount={5}value={3}>

このコンポーネントの例は、custom-fieldsデモプロジェクトにあります。

星評価用のコンポーネントを作成したら、これをKeystoneで使用できます。

Cell

セルコンポーネントは、Keystone管理UIのアイテムのリストを示すテーブルにレンダリングされます。作成したstarコンポーネントを使用します。セルコンポーネントは、整数値になるデータと、Implementation.jsファイルに追加したstarCountフィールド構成オプションを使用します。セルを操作して、コンポーネントにonChangeプロップを提供しないようにすることができます。

import{jsx}from'@emotion/core';importStarsfrom'./Stars';exportdefaultfunctionStarsCell({field,data}){const{starCount}=field.config;return<Starscount={starCount}value={data}/>;}

Field

フィールドコンポーネントは、Keystoneでリストアイテムを作成または編集するときに使用されるメインインターフェイスを提供します。一貫性のある外観を得るために、@ arch-ui / fieldsからラッピングコンポーネントをインポートします。これはKeystoneのUIライブラリです。これにより、ラベルがレンダリングされ、一定の間隔が確保されます。これ以外は、ユーザーが星をクリックするときに値を更新できるように、onChangeイベントを星に委任するだけです。

import{jsx}from'@emotion/core';import{FieldContainer,FieldLabel,FieldInput}from'@arch-ui/fields';importStarsfrom'./Stars';constStarsField=({field,value,errors,onChange})=>(<FieldContainer><FieldLabelhtmlFor={`ks-input-${field.path}`}field={field}errors={errors}/><FieldInput><Starscount={field.config.starCount}value={value}onChange={onChange}/></FieldInput></FieldContainer>);exportdefaultStarsField;

これで、基本的なカスタムフィールドが作成されました。

前ページ https://qiita.com/bontensuzuki/items/8f91e3f0128b6be1ecbb

Keystonejs Tutorials #3/Seeding data

$
0
0

次ページ https://qiita.com/bontensuzuki/items/7cd598a7075f6c970f14

Seeding data

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/initial-data

このガイドでは、createItemsメソッドを使用してユーザーリストを作成し、それに初期データを追加する方法を示します。このプロセスはSeedingとも呼ばれます。

注:前の章では、コードは個別のファイルに分割されましたが、実際のコードベースではこれが推奨されますが、この部分では、明確にするためにすべてが1つのファイルに入れられています。

リスト設定

パッケージをインストールする

この章では、前の章とは異なるユーザースキーマを使用し、Todoリストの代わりに投稿リストを使用します。新しいプロジェクトから始めて、空のデータベースから始める(前の章のデータを削除する)のが最善です。また、次のパッケージがすべてインストールされていることを確認してください。

mkdirnew-project3cdnew-project3yarninityarnadd@keystonejs/keystone
yarnadd@keystonejs/adapter-mongoose
yarnadd@keystonejs/app-graphql
yarnadd@keystonejs/fields
yarnadd@keystonejs/app-admin-ui
yarnadd@keystonejs/auth-password

準備

まず、ユーザーリストを作成し、PasswordAuthStrategyを追加します。
index.jsのコード:

const{Keystone}=require('@keystonejs/keystone');const{PasswordAuthStrategy}=require('@keystonejs/auth-password');const{Text,Checkbox,Password}=require('@keystonejs/fields');const{GraphQLApp}=require('@keystonejs/app-graphql');const{AdminUIApp}=require('@keystonejs/app-admin-ui');const{MongooseAdapter}=require('@keystonejs/adapter-mongoose');constkeystone=newKeystone({name:'example-project',adapter:newMongooseAdapter(),});keystone.createList('User',{fields:{name:{type:Text},email:{type:Text,isUnique:true,},isAdmin:{type:Checkbox},password:{type:Password,},},});constauthStrategy=keystone.createAuthStrategy({type:PasswordAuthStrategy,list:'User',});module.exports={keystone,apps:[newGraphQLApp(),newAdminUIApp({enableDefaultRoute:true,authStrategy})],};

ヒント:Keystone CLIを実行してkeystone-appを作成し、「スターター(ユーザー+認証)」を選択すると、同様の設定を行うことができます。このスタータープロジェクトには、ユーザーリスト、PasswordAuthStrategy、および構成済みのデータベースのシードがあります。とりあえず、手動で進めます。

メンバー登録

http://localhost:3000/admin/api

mutation{createUser(data:{name:"Mike",email:"test@test.test",password:"test1234",isAdmin:true}){name}}

メンバーを登録できたか確認してみましょう

query{allUsers{id,name}}

出力

{"data":{"allUsers":[{"id":"5eae81ad78af33930bac2700","name":"Mike"}]}}

adminUIで確認
Keystone Admin UI: http://localhost:3000/admin

スクリーンショット 2020-05-03 17.44.02.png

スクリーンショット 2020-05-03 17.42.43.png

登録が確認できました

アイテムを作成する

createItemsメソッドには、キーがリストキーであり、値が挿入するアイテムの配列であるオブジェクトが必要です。例えば:

keystone.createItems({User:[{name:'John Duck',email:'john@duck.com',password:'dolphins'},{name:'Barry',email:'bartduisters@bartduisters.com',password:'dolphins'},],});

注:データの形式は、keystone.createList()への呼び出しでのスキーマ設定と一致する必要があります。スキーマの例として、電子メールフィールドにはisUnique:trueが含まれているため、上記のコードでは、生成する必要がある各ユーザーに対して同じ電子メールを使用することはできません。

データベース接続時にデータをシードする方法の例:

constkeystone=newKeystone({name:'New Project',adapter:newMongooseAdapter(),onConnect:asynckeystone=>{awaitkeystone.createItems({User:[{name:'John Duck',email:'john@duck.com',password:'dolphins'},{name:'Barry',email:'bartduisters@bartduisters.com',password:'dolphins'},],});},});

アプリケーションを起動し、管理UIにアクセスします。起動時に2人のユーザーが利用できます。

注:この例では、スタートアップごとに同じ2人のユーザーが生成されます。メールは一意である必要があるため、重複したエラーが表示されます。これを回避するには、Keystoneを開始する前にデータベースをクリアします。

次ページ https://qiita.com/bontensuzuki/items/7cd598a7075f6c970f14

Keystonejs Tutorials #4/Creating relationships

$
0
0

前ページ https://qiita.com/bontensuzuki/items/670c97af7d0e55bbfe93
次ページ https://qiita.com/bontensuzuki/items/7f306f0d6bed05adc891

Creating relationships

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/relationships

関係を作る(Creating relationships)

1 新しいプロジェクトを作成する
2 リストを追加する

単一の関係(To-single relationship)

関係を設定して、TodoリストとUserリストをリンクします。 Todo.jsの担当者フィールドを調整して、次のコードと一致させます。

Import the Relationship field:
/lists/Todo.js

const{CalendarDay,Checkbox,Relationship,Text}=require('@keystonejs/fields');

フィールドタイプを[Text]から[Relationship]に更新し、フィールドが関連付けられているリストを指すrefを設定します。

assignee:{type:Relationship,ref:'User',isRequired:true,

refオプションは、関連するリストを定義します。オプションに割り当てられる名前は、createListに渡される名前と同じです。Admin UIで、作成したユーザーの1人を選択して、タスクを完了する責任を持たせることができます。

双方向の単一関係(Two-way to-single relationship)

ユーザーにタスクを割り当てることができるようになりましたが、タスクにユーザーを割り当てることはできません。そこでUser.jsに次のフィールドを追加します。

/lists/User.js

task:{type:Relationship,ref:'Todo',}

これで、管理パネルからユーザーのタスクを設定できます。
管理者を登録します

mutation{createUser(data:{username:"Sato",email:"test@test.test",isAdmin:true,password:"test12345"}){id}}

このあとで管理パネルでTODOを登録します

しかし、何かがおかしい!ユーザーのタスクを選択してからこのタスクを確認すると、担当者が正しくありません。これは、2つの別々の片側関係を作成したためです。私たちが望むのは、単一の両面関係です。

スクリーンショット 2020-05-04 11.05.21.png

(Assigneeの欄がUserIdで表示されています...)

index.js
const{Keystone}=require('@keystonejs/keystone');const{MongooseAdapter}=require('@keystonejs/adapter-mongoose');const{PasswordAuthStrategy}=require('@keystonejs/auth-password');const{GraphQLApp}=require('@keystonejs/app-graphql');const{AdminUIApp}=require('@keystonejs/app-admin-ui');const{Text,CalendarDay,Checkbox,Password}=require('@keystonejs/fields');constTodoSchema=require('./lists/Todo.js');constUserSchema=require('./lists/User.js');constkeystone=newKeystone({name:'New Project 3',adapter:newMongooseAdapter(),});keystone.createList('Todo',TodoSchema);keystone.createList('User',UserSchema);constauthStrategy=keystone.createAuthStrategy({type:PasswordAuthStrategy,list:'User',});module.exports={keystone,apps:[newGraphQLApp(),newAdminUIApp({enableDefaultRoute:true,authStrategy})],};


lists/Todo.js
const{CalendarDay,Checkbox,Relationship,Text}=require('@keystonejs/fields');module.exports={fields:{// existing fieldsdescription:{type:Text,isRequired:true,},isComplete:{type:Checkbox,defaultValue:false,},// added fieldsdeadline:{type:CalendarDay,format:'Do MMMM YYYY',yearRangeFrom:'2019',yearRangeTo:'2029',isRequired:false,defaultValue:newDate().toISOString('YYYY-MM-DD').substring(0,10),},assignee:{type:Relationship,ref:'User',isRequired:true,},},};```javascript

</div></details>
<details><summary>lists/User.js </summary><div>

```javascriptconst{Text,Password,Checkbox,Relationship}=require('@keystonejs/fields');module.exports={fields:{username:{type:Text,isRequired:true,},email:{type:Text,isUnique:true,},isAdmin:{type:Checkbox},password:{type:Password,isRequired:true,},task:{type:Relationship,ref:'Todo',},},};

前ページ https://qiita.com/bontensuzuki/items/670c97af7d0e55bbfe93
次ページ https://qiita.com/bontensuzuki/items/7f306f0d6bed05adc891

Keystonejs Tutorials #5/Creating relationships2

$
0
0

前ページ https://qiita.com/bontensuzuki/items/7cd598a7075f6c970f14
次ページ https://qiita.com/bontensuzuki/items/9982196b4f7956ba0fd9

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/relationships

ユーザーとTodoの間に双方向の関係を設定する(Setting up a two-sided relationship between User and Todo)

タスクと担当者が単一の関係の2つの異なる側面にすぎないことを示すために、設定を更新する必要があります。User.jsでタスクフィールドを次のように調整します。

/lists/User.js

task:{type:Relationship,-ref:'Todo',+ref:'Todo.assignee',}

Todo.jsの、担当者(assignee)フィールドを更新します。

assignee:{type:Relationship,-ref:'User',+ref:'User.task',}

管理UIを開始し、Todoを作成してユーザーを割り当てます。ユーザーのタスクフィールドを確認して、すでに設定されていることを確認してください。

管理UI/Todo

スクリーンショット 2020-05-04 14.45.15.png

スクリーンショット 2020-05-04 14.45.25.png

管理UI/Users
スクリーンショット 2020-05-04 14.45.34.png

スクリーンショット 2020-05-04 14.45.47.png

前ページ https://qiita.com/bontensuzuki/items/7cd598a7075f6c970f14
次ページ https://qiita.com/bontensuzuki/items/9982196b4f7956ba0fd9

Keystonejs Tutorials #6/Creating relationships3

$
0
0

前ページ https://qiita.com/bontensuzuki/items/7f306f0d6bed05adc891
次ページ https://qiita.com/bontensuzuki/items/8f91e3f0128b6be1ecbb

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/relationships

多対多の関係(To-many relationship)

ユーザーが複数のタスクを実行できる必要がある場合はどうなりますか? Keystoneはこれを簡単に行う方法を提供します。 User.jsの次のコードを見てください。

/lists/User.js

tasks:{type:Relationship,ref:'Todo.assignee',many:true,}

many:trueオプションは、ユーザーがタスクへの複数の参照を保存できることを示します。

/lists/Todo.js

assignee:{type:Relationship,-ref:'User.task',+ref:'User.tasks',}

注:フィールドの名前をtaskからtasksに更新し、関係の性質を示しました。

スクリーンショット 2020-05-04 15.25.46.png

taskが2つ登録できました

index.js
const{Keystone}=require('@keystonejs/keystone');const{MongooseAdapter}=require('@keystonejs/adapter-mongoose');const{PasswordAuthStrategy}=require('@keystonejs/auth-password');const{GraphQLApp}=require('@keystonejs/app-graphql');const{AdminUIApp}=require('@keystonejs/app-admin-ui');const{Text,CalendarDay,Checkbox,Password}=require('@keystonejs/fields');constTodoSchema=require('./lists/Todo.js');constUserSchema=require('./lists/User.js');constkeystone=newKeystone({name:'New Project 3',adapter:newMongooseAdapter(),});keystone.createList('Todo',TodoSchema);keystone.createList('User',UserSchema);constauthStrategy=keystone.createAuthStrategy({type:PasswordAuthStrategy,list:'User',});module.exports={keystone,apps:[newGraphQLApp(),newAdminUIApp({enableDefaultRoute:true,authStrategy})],};

lists/Todo.js
const{CalendarDay,Checkbox,Relationship,Text}=require('@keystonejs/fields');module.exports={fields:{// existing fieldsdescription:{type:Text,isRequired:true,},isComplete:{type:Checkbox,defaultValue:false,},// added fieldsdeadline:{type:CalendarDay,format:'Do MMMM YYYY',yearRangeFrom:'2019',yearRangeTo:'2029',isRequired:false,defaultValue:newDate().toISOString('YYYY-MM-DD').substring(0,10),},assignee:{type:Relationship,ref:'User.tasks',isRequired:true,},},};

lists/User.js
const{Text,Password,Checkbox,Relationship}=require('@keystonejs/fields');module.exports={fields:{username:{type:Text,isRequired:true,},email:{type:Text,isUnique:true,},isAdmin:{type:Checkbox},password:{type:Password,isRequired:true,},tasks:{type:Relationship,ref:'Todo.assignee',many:true,},},};

前ページ https://qiita.com/bontensuzuki/items/7f306f0d6bed05adc891
次ページ https://qiita.com/bontensuzuki/items/8f91e3f0128b6be1ecbb

Keystonejs Tutorials #7/Custom Field1

$
0
0

前ページ https://qiita.com/bontensuzuki/items/9982196b4f7956ba0fd9
次ページ https://qiita.com/bontensuzuki/items/7e9b5b7483b30324e9da

Keystonejsのtutorialsから学んでいきます。(ほとんど機械翻訳です)
https://www.keystonejs.com/tutorials/custom-fields

カスタムフィールド:星

このチュートリアルでは、星評価用のシンプルなカスタムフィールドタイプを作成します⭐️⭐️⭐️⭐️⭐️

このコンポーネントの場合、データ要件は単純です。星の数を表す整数を格納する必要があります。組み込みのInteger型を拡張して、その実装を活用し、必要に応じてカスタム動作とUIコンポーネントのみを提供できます。

フィールドタイプの定義

これが完了すると、カスタムフィールドのディレクトリは次のようになります。

.
└── Stars
    ├── index.js
    ├── Implementation.js
    └── views
        ├── Cell.js
        ├── Field.js
        ├── Filter.js
        ├── Stars.js
        ├── star-empty.svg
        └── star-full.svg

カスタムフィールドには、フィールド定義をエクスポートするindex.jsファイルが必要です。フィールド定義は、フロントエンドコードとバックエンドコードを含むフィールドを構成するすべての部分をまとめます。

スターフィールドの場合、次のようになります。

const{Stars,MongoIntegerInterface,KnexIntegerInterface}=require('./Implementation');const{Integer}=require('@keystonejs/fields');module.exports={type:'Stars',implementation:Stars,adapters:{mongoose:MongoIntegerInterface,knex:KnexIntegerInterface,},views:{Controller:Integer.views.Controller,Field:require.resolve('./views/Field'),Filter:Integer.views.Filter,Cell:require.resolve('./views/Cell'),},};

実装とアダプターはKeystoneによって使用されるバックエンドコードを参照し、ビューの下のすべては、管理UIまたはGraphQLアプリのいずれかで使用されるフロントエンドコードを参照します。

フロントエンドとバックエンドのコードを同じファイルにバンドルできないことに気付いたかもしれません。そのため、フロントエンドコードをインポートするのではなく、require.resolveを使用して文字列値を提供します。文字列値は、ファイルの場所への参照です。 Keystoneには、フィールドタイプのフロントエンドコードをコンパイルする特別なビルドステップがあります。

注:独自のプロジェクトの外で使用するためにフィールドタイプをパッケージ化する場合は、追加の手順が必要ですが、これらはこのチュートリアルの範囲外です。

前ページ https://qiita.com/bontensuzuki/items/9982196b4f7956ba0fd9
次ページ https://qiita.com/bontensuzuki/items/7e9b5b7483b30324e9da

google home miniにしゃべらせる。

$
0
0

流れ

  • Google Home Miniを特定する
  • テキストから音声ファイルを生成する
  • Google Home Miniをchromecastデバイスとして、へ音声ファイルをキャストする

Google Home Miniを特定する

2パターンある。ただし、前提として、Google HomeへアクセスできるLAN内の端末であること。
自分の環境では、有線LANと無線LANを両方とも有効化した状態で確認できた。

  • Google Homeアプリで設定から端末のipを特定する。 要固定IP化。→DHCPでMACアドレスとIPで固定IPにする。
  • mDNSで端末を動的に特定して、アドレスを都度得る。
    Google Home 端末を操作するなら、前者の方が現状、楽。

簡易的に端末までの経路が確立されているかを確認する方法

いずれもmDNSでネットワーク内の端末を列挙する方法

Windowsなら、bonjourbrowserで検索

bonjourbrowserで検索できる。

ubuntuなら、Avahi

avahi-browse -alrで一覧出来た。

Chrome拡張のMDNS Browser

私の環境では捕捉出来なかった。

テキストから音声ファイルを生成する

Google Home Miniをchromecastとして、へ音声ファイルをキャストする

castv2-clientを使う。

この方の実装方法が良いかと思われる。
Google Homeに話をさせる

cstv2-clientにて、デフォルトのメディアレシーバーのインタフェースを得ているように見える。

google-home-notifierはおすすめしない

Google Home端末のip特定からメッセージ送信までワンパッケージになっているgoogle-home-notifierがあるが、
ローカル環境へのライブラリインストールや依存ライブラリのバージョン修正、依存ライブラリの実装修正などの手作業が多いため、おすすめしない。

GoogleのCloud API

テキストを送信して、話させるためのCloud APIはわからなかった。
ユーザの発話をトリガーにするか、ルーティーンでの時刻起動をトリガーにする方法はあったが。
自分としては、異常検知のような使い方をしたかったため、上記の使い方を模索していた。


MongoDB(Mongoose)上級者への道

$
0
0

MongoDBはNoSQLなため、列の定義の拡張が難しいRDBMSに対して柔軟に列定義の拡張が可能です。(マイグレーションが楽)
また一般的にNoSQLはRDBMSに対してパフォーマンス面で優位です。
NodeJS+Mongooseで使う前提でお話します。
セットアップから検索、書き込み、パフォーマンス向上テクニックからトランザクションまでよく使う機能に関してまとめました。
Node v12、MongoDB v4.2.5以上を前提に書いてます。

GitHubサンプル

セットアップ

MongoDBインストール

$ brew tap mongodb/brew
$ brew install mongodb-community
$ brew services start mongodb-community

mongooseインストール

$ npm install mongoose 
# もしくは$ yarn add mongoose

mongooseでmongoに接続します。

constmongoose=require('mongoose')mongoose.Promise=global.Promiseconstdbname='dbname'mongoose.connect(`mongodb://localhost/${dbname}`,{useCreateIndex:true,useNewUrlParser:true,useUnifiedTopology:true,useFindAndModify:false})

モデルの作成

modelsフォルダを作成してmodels/simple.js内にシンプルなモデルを作成します。
Schemaにモデルのフィールドを定義します。
文字列型のフィールドはString, 数値型のフィールドはNumber,真偽型のフィールドはBooleanで定義します。
Schema.Types.ObjectId型はObjectId型を指定します、これは外部スキーマのObjectId(参照)を保存します。refには外部スキーマ名を指定します。
(実際に使う際はpopulateメソッドなどでJOINします。)
配列やオブジェクト、子スキーマもフィールドに指定することができます。
スキーマを定義し、mongoose.modelでスキーマモデル名を指定してスキーマを作成します。

models/simple.js
constmongoose=require('mongoose')constSchema=mongoose.SchemaconstsubSchema=newSchema({str:String,})constschema=newSchema({str:{type:String},s:String,// {type: String}の省略記法、オプションなしの場合のみ可能num:{type:Number},m:Number,// {type: Number}の省略記法、オプションなしの場合のみ可能bool:{type:Boolean},b:Boolean,// {type: Boolean}の省略記法、オプションなしの場合のみ可能ref:{type:Schema.Types.ObjectId,ref:'User'},arr:[{type:String}],obj:{a:{type:String},b:{type:Number},},refs:[{type:Schema.Types.ObjectId,ref:'User'}],sub:subSchema,})module.exports=mongoose.model('Simple',schema)

modelsフォルダ以下のモデル定義を一括でmodule.exportsして外部から参照できるようにmodels/index.jsを作成します。

models/index.js
'use strict'require('fs').readdirSync(__dirname).forEach(e=>{constname=/^([a-z]+)\.js$/i.test(e)&&RegExp.$1if(name&&name!=='index'){constmodel=require('./'+name)module.exports[model.modelName]=model}})

フィールドのオプション

mongooseの各フィールドにはオプションをつけることができます。
フィールドオブジェクトにオプションを追加します。
よく使うものに関して説明してます。
他のオプションは公式を参照してください。

required

必須フィールドです。ドキュメント(レコード)作成時に該当フィールドが必須かのフラグです。
必須パラメータがない場合はドキュメントを作成できず、エラーになります。
デフォルト(無指定)はfalseです。

user.js
required:true

unique

全ドキュメントの中でユニーク(重複なし)かどうかのフラグです。
ドキュメント作成時、もしくは更新時に重複してる場合はエラーとなります。
使う際はrequiredオプションと組み合わせがほぼ必須です。(undefinedも重複対象となるため)
デフォルトはfalseです。

user.js
unique:true

select

find時に指定のフィールドを返却するか否かのフラグです。
falseにすることで明示的にselectメソッドで該当のフィールドを指定しない限り、取得できません。
ユーザの個人情報などを隠蔽する際に使えます。
デフォルトはtrueです。

user.js
select:false

validate

フィールドに保存する前にチェックするオプションです。
validatorにはチェックする関数を指定します。戻り値がtrueであれば正常に保存、falseの場合にエラーとなります。
messageにはエラー時のエラーメッセージを指定します。
デフォルトでは設定されていません。

user.js
validate:{validator:(v)=>validator.isEmail(v),message:props=>`${props.value}は正しいメールアドレスではありません。`}

min,max

フィールドに指定できる最小値、最大値(Number型のみ可)です。
この値の範囲を超えて指定した場合はエラーとなります。
デフォルトでは設定されていません。

user.js
rating:{type:Number,required:true,min:1,max:5},

enum

フィールドに指定できる値の列挙(String型でよく使います)で、指定の値以外は保存できなくします。
デフォルトでは設定されていません。

user.js
enum:['normal','super','ultra'],

default

デフォルトの値(Model.create時やnew Model時にフィールドの値が指定されていない場合でも入る値)です。
デフォルトでは設定されていません。

user.js
default:false

また、関数を指定することもできます。この場合、thisは操作対象のドキュメントそのものを指します。
ちなみにアロー関数にするとthisは取得できないので注意です。(スコープの巻き上げ)

user.js
default:function(){returnjwt.sign(this._id.toString(),secret)}

refPath

refと違い、refPathを使うことで複数のスキーマの参照(ObjectId)を同一フィールドに格納することができます。
ドキュメント作成時にスキーマのtypeと該当するObjectIdの指定が必須です。
(typeとセットでないとどのスキーマの参照か判別できないためJOINできない)

user.js
role:{model:{type:Schema.Types.ObjectId,refPath:'role.model'},type:{type:String,enum:['Programmer','ProductManager']},},

スキーマのオプション

スキーマ自体のオプションです。
Schemaオブジェクトの第2引数に指定します。

versionKey

バージョン管理情報を保存するか否かのフラグです。
mongooseのドキュメントオブジェクトは__vフィールドという
特殊なフィールドでバージョン管理を行っています。(デフォルトだと自動で生成される)
配列フィールドの要素を削除や追加してmodel.save()した場合に__vの値をインクリメントするという特性があります。
配列フィールドをスキーマに持つ場合などに、更新や削除の処理が並列に走ると要素がずれるという問題を防ぐ意図があります。
(といってもVersionErrorを発生させるだけなのでデータの不整合は防げるかもしれないけど、根本的な解決にはなりません)

参考:Mongooseのバージョニング

versionKeyフラグをfalseにすることで、バージョニングをせず__vフィールドは作成されません。
デフォルトはtrueです。

user.js
versionKey:false

どのみち処理順番が大事な場合はupdatedAtなどで更新順番を管理する実装をする必要があったり、
(管理画面とユーザで複数人が同じデータを更新し合う場合に発生する)
MongoDB v4から実装されたトランザクションを使ったほうが無難です。

ちなみにsaveメソッド以外のfindByOneAndUpdate(findByIdAndUpdate)やupdateManyやupdateOneの場合はバージョニングを無視して上書きします。
以上のような問題をはらんでいるため、上記のversionKeyを無効にするかいっそsaveメソッドを使わないほうが無難です。

timestamps

createdAt、updatedAtフィールドを自動的に作成します。
createdAtはドキュメントが作られた日時が一度のみ入ります。
updatedAtはドキュメントが更新する度に同時に日時が更新されます。
特に理由がなければ、どのスキーマも基本的にtrueにして運用することが多いです。
デフォルトはfalseです。

user.js
timestamp:true

toObject

モデルのtoObjectメソッドを呼ぶときのオプションです。
後述のfindなどでドキュメント取得する際に呼ばれます。
(ただし、後述のlean時は呼ばれません)
minimizedはフィールドが空のオブジェクトの場合でも{}の値を返却します。trueにしておくのをおすすめします。
virtualsはvirtualsメソッドのフィールドも返却するかのフラグです。スキーマにvirtualsなフィールドを作成した場合、trueにする必要があります。

user.js
toObject:{minimize:true,virtuals:true,transform:function(doc,user){console.log('toObject')transform(doc,user)},}

transformはドキュメント取得時にフィールドの値を変換してから返却する関数です。
例えば、ユーザの個人情報やパスワード情報などは、マスクして生の情報を返却しないようにします。

user.js
functiontransform(doc,user){deleteuser.iduser.password=!!user.passwordif(user.isDeleted){deleteuser.tokenuser.email=!!user.emailuser.phone=!!user.phone}returnuser}

他のオプションに関して公式を参照してください

toJSON

ドキュメントのtoJSONメソッドを呼ぶときのオプションです。
指定できるオプションはtoObjectと同じです。

user.js
toJSON:{minimize:true,virtuals:true,transform:function(doc,user){console.log('toObject')transform(doc,user)},}

expressサーバなどでjsonレスポンスを返すタイミングでもtoJSONが呼ばれます。
(ただし、後述のlean時したオブジェクトはPOJO化されるため、呼ばれません)

app.js
app.get('/api/user',async(req,res)=>{constuser=awaitUser.findOne()res.json(user)})

virtuals

仮想的なフィールドを作成することができます。
MongoDBには直接保存されませんが、ドキュメント取得時に付与されます。
主にファイルのパスなどを保存する際に役立ちます。(ファイル保存場所を変更するとパスも変える必要があるため)

user.js
// 仮想的なフィールドschema.virtual('image').get(function(){constwebServer='http://localhost'// 保存の場所を変えた場合に差し替えできるようにするreturn`${webServer}/images/${this._id}`})

methods

ドキュメントにメソッドを定義することができます。
取得したドキュメント毎に付与されます。
呼び出し元のオブジェクトのthisなため、アロー関数は使えません。

user.js
schema.method('showName',function(){console.log(this.name)})

使い方はnewしたドキュメントもしくはfindしたドキュメントに対して行えます。

constuser=newUser({name:'test',email:'test@gmail.com',password:'pw'})user.showName()

statics

スキーマに静的なメソッドを定義することができます。

user.js
schema.static('showName',function(doc){console.log(doc.name)})

使い方はModel.静的メソッド名で呼び出しできます。

constuser=newUser({name:'test',email:'test@gmail.com',password:'pw'})user.showName()

pre

preフックは保存前に処理を差し込みます。
差し込むメソッド単位で指定します。
ここではupdate、findOneAndUpdateの処理前にオプションを必ず追加するようにしています。
updateやfindOneAndUpdateの第3引数に毎回付けるのが大変なのでpreフックを使うことで実行直前に必ずつけることができます。
runValidatorsオプションは保存データの型がフィールドのデータ型と正しいかのチェックやvalidatorのチェックを行います。
newオプションは実行後に更新後のドキュメントを返却するか否かのフラグです。trueにすることで更新後のドキュメントを返却します。

user.js
schema.pre('update',asyncfunction(next){this.setOptions({runValidators:true,})returnnext()})schema.pre('findOneAndUpdate',asyncfunction(next){this.setOptions({runValidators:true,new:true,})returnnext()})

post

postフックはドキュメント保存後に行う操作です。
差し込むメソッド単位で指定します。
主にドキュメント更新後にログや通知など行うのに適しています。

user.js
constpostSave=asyncfunction(doc,next){console.log(`updated ${doc._id}`)next()}schema.post('findOneAndUpdate',postSave)schema.post('save',postSave)

エラーハンドリング用のミドルウェア
https://mongoosejs.com/docs/middleware.html#error-handling-middleware

REPL

つぎのようなREPLを作成しておくと、CLI上で挙動の確認やDBメンテナンスの作業する際に捗ります。
HISTORY_DIRECTORYには過去の実行記録を残します。(上キーで過去の実行を参照できる)

repl.js
constmongoose=require('mongoose')mongoose.Promise=global.Promiseconstdbname='dbname'mongoose.connect(`mongodb://localhost/${dbname}`,{useCreateIndex:true,useNewUrlParser:true,useUnifiedTopology:true,useFindAndModify:false})constmoment=require('moment')constrepl=require('repl')constreplInstance=repl.start({prompt:'> '})replInstance.context.moment=momentconstHISTORY_DIRECTORY=__dirname+'/.ym_history'// require node version above v11.10.0replInstance.setupHistory(HISTORY_DIRECTORY,(err)=>{if(err)console.log(err)})constmodels=require('./models')for(constnameinmodels){replInstance.context[name]=models[name]}replInstance.on('exit',()=>{mongoose.disconnect()})

setupHistoryはv11.10以降の機能でコマンドの履歴を.ym_historyに保存します。
トップレベルawaitを使いたいため、--experimental-repl-awaitフラグをつけて実行します

$ node --experimental-repl-await repl.js

次のようなreplで実際のJSコード同様にmongooseを実行することができるようになります。

>awaitSimple.create({str:'abc'})>awaitSimple.find()

create

ドキュメントを作成します。requiredのフィールドが存在する場合はそのフィールドのパラメータも指定しないとエラーになります。
フォーマットはModel.create(フィールドのパラメータ)のように指定します。
ドキュメントが生成される際、ユニークなドキュメントのID(ObjectId)を自動生成します。
ドキュメントのIDは_idフィールドに格納されます。(24桁16進数の値)

>awaitSimple.create({str:'abc',arr:['a','b']}){arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0}

後の説明用に色々データを作成しておきます。
timestampが定義されているスキーマのドキュメントはcreatedAt, updatedAtが自動的に作成されます(UTCで保存されるので注意)。
JOINをする場合はrefフィールドに対象のObjectIdが必要なので、作成後ドキュメントのIDを指定します。
refPathの場合はスキーマのタイプもObjectIdとセットで指定する必要があります。
requiredされているフィールドは必須フィールドなので、入れない場合はエラーになります。
defaultが指定されているフィールドは挿入するデータを明示しない場合、自動的にdefaultの値が入ります。
create時には保存後にpostフックが実行されます(後述)。また、戻り値としてMongooseオブジェクトが返却されるためtoObjectのtransformが実行されます。この際、virtualsなフィールドも付与され返却されます。

>awaitProgrammer.create({skill:['frontend','backend']}){skill:['frontend','backend'],_id:5eaeb551da27f19dfbfa60c7,createdAt:2020-05-03T12:13:05.468Z,updatedAt:2020-05-03T12:13:05.468Z}>awaitProductManager.create({skill:['frontend','backend','infra'],programmers:['5eaeb551da27f19dfbfa60c7']}){skill:['frontend','backend','infra'],programmers:[5eaeb551da27f19dfbfa60c7],_id:5eaeb598da27f19dfbfa60c8,createdAt:2020-05-03T12:14:16.750Z,updatedAt:2020-05-03T12:14:16.750Z}>awaitUser.create({name:'myname',email:'test@gmail.com',password:'pw',role:{model:'ProductManager',type:'5eaeb598da27f19dfbfa60c8'}})updated5eaeb78dda27f19dfbfa60c9toObject{invites:[],isAdmin:false,isDeleted:false,_id:5eaeb78dda27f19dfbfa60c9,name:'myname',email:'test@gmail.com',password:true,role:{model:'ProductManager',type:5eaeb598da27f19dfbfa60c8},token:'eyJhbGciOiJIUzI1NiJ9.NWVhZWI3OGRkYTI3ZjE5ZGZiZmE2MGM5.E7huFLFvRWdUTu-StH2ayF543oBSiq-hlwzT_NSAhD0',reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9'}>awaitUser.create({name:'yourname',email:'example@gmail.com',password:'pw',role:{model:'Programmer',type:'5eaeb551da27f19dfbfa60c7'},reviews:[{rating:5,comment:'abc'}],invitedFrom:'5eaeb78dda27f19dfbfa60c9'})updated5eaeb8feda27f19dfbfa60cctoObject{invites:[],isAdmin:false,isDeleted:false,_id:5eaeb8feda27f19dfbfa60cc,name:'yourname',email:'example@gmail.com',password:true,role:{model:'Programmer',type:5eaeb551da27f19dfbfa60c7},reviews:[{_id:5eaeb8feda27f19dfbfa60cd,rating:5,comment:'abc',createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z}],invitedFrom:5eaeb78dda27f19dfbfa60c9,token:'eyJhbGciOiJIUzI1NiJ9.NWVhZWI4ZmVkYTI3ZjE5ZGZiZmE2MGNj.Ea7OndfPFhZstDVR6LSQfilQsxLNsZz5Fai3ohcqnoA',createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z,image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc'}

また、次のようにnew Model()してからsaveを呼んで作成する方法もあります。
newした時点で_idは発行されますが、実際のMongoDBへの保存タイミングはsaveを読んだタイミングなので注意です。
(さらにsaveはバージョニングの問題もはらんでいるので注意が必要です。)

>constsimple=newSimple({str:'abc',arr:['a','b']})>simple{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc'}>awaitsimple.save()

この他にもupdate系メソッドでupsertオプションを指定してドキュメントを作成する方法があります。

find、findOne、findById

findは検索条件に合致するドキュメントを全て配列形式で返します。
findOneは検索条件に合致するドキュメントを1つのみ返します。
findByIdは指定のモデルの_idに合致するドキュメントを1つのみ返します。(findOneのidのみ指定版)

検索条件

基本的にAND条件で検索となります。
findは配列形式でドキュメントが取得できます、該当ドキュメントが存在しない場合は空の配列が返ります。
findOne、findByIdは該当ドキュメントが存在しない場合はnullが返ります。

全件検索

findの検索条件なしは全件検索となり、配列形式で全てのドキュメントが取得できます。
findOne、findByIdは先頭のドキュメント1つの取得となります。

>awaitSimple.find()[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]>awaitSimple.findOne(){arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0}

指定のフィールド別に検索条件を指定する

指定のフィールドの値に合致するドキュメントを取得します。
複数フィールド指定した場合はand条件となります。

>awaitSimple.find({str:'abc'})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]>awaitSimple.find({str:'ab'})[]

配列フィールドの場合、指定の値が配列に含まれているドキュメントを取得します。

>awaitSimple.find({arr:'a'})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]>awaitSimple.find({arr:'c'})[]

オブジェクトや子スキーマなどでネストされている場合は.でフィールドをたどります。
ネストされているものが配列の場合はさらに配列の中に含まれている条件で検索します。

>awaitUser.findOne({'reviews.rating':5})toObject{role:{model:'Programmer',type:5eaeb551da27f19dfbfa60c7},invites:[],isAdmin:false,isDeleted:false,_id:5eaeb8feda27f19dfbfa60cc,name:'yourname',password:true,reviews:[{_id:5eaeb8feda27f19dfbfa60cd,rating:5,comment:'abc',createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z}],invitedFrom:5eaeb598da27f19dfbfa60c8,createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z,image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc'}

$ne

指定の値に一致しないものを取得します。

>awaitSimple.find({str:{$ne:'abc'}})[]>awaitSimple.find({str:{$ne:'ab'}})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]

配列フィールドの場合、指定の値が配列に含まれていないドキュメントを取得します。

>awaitSimple.find({arr:{$ne:'a'}})[]>awaitSimple.find({arr:{$ne:'c'}})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]

$exists

指定のフィールドの値が存在しているかで検索します。

>awaitSimple.findOne({str:{$exists:true}}){arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0}>awaitSimple.findOne({num:{$exists:true}})null

配列フィールドの場合、配列の添字を指定すると配列の長さがx以上のドキュメントを返します。
(次の例は配列の長さが2以上)

>awaitSimple.findOne({'arr.1':{$exists:true}}){arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0}

$in

配列指定の値のいずれかに一致するドキュメントを返します。

>awaitSimple.find({_id:{$in:['5eae9847da27f19dfbfa60c5','5eae9857da27f19dfbfa60c6']}})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]

配列フィールドの場合は配列指定の値のいずれかかが配列に含まれるドキュメントを返します。

>awaitSimple.find({arr:{$in:['b','c']}})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]

$nin

配列指定の値のいずれも一致しないドキュメントを返します。

>awaitSimple.find({_id:{$nin:['5eae9847da27f19dfbfa60c5','5eae9857da27f19dfbfa60c6']}})[]

配列フィールドの場合、配列指定の値のいずれも配列に含まないドキュメントを返します。

>awaitSimple.find({arr:{$nin:['b','c']}})[]>awaitSimple.find({arr:{$nin:['c','d']}})[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0},{arr:['a','b'],refs:[],_id:5eae9857da27f19dfbfa60c6,str:'abc',__v:0}]

$gt、$gte, $lt, $lte

指定の値に対し$gtはより大きい、$ltは未満の条件のドキュメントを検索します。
なお、$gteは以上、$lteは以下となり、等号(=)を含みます。
例えば、指定の日時範囲で検索したい場合などに使います。
時刻はUTCで保存されているため、日本時刻だと+9時間ずれています。

>awaitProgrammer.find({createdAt:{$gt:moment('2020-05-03 21:13:04').toDate(),$lt:moment('2020-05-03 21:13:06').toDate()}})[{skill:['frontend','backend'],_id:5eaeb551da27f19dfbfa60c7,createdAt:2020-05-03T12:13:05.468Z,updatedAt:2020-05-03T12:13:05.468Z}]

$or

いずれかの条件に当てはまるドキュメントを返します。
配列形式で条件を指定します。

>awaitUser.find({$or:[{'role.model':'ProductManager'},{'role.model':'Programmer'}]})toObjecttoObject[{invites:[],isAdmin:false,isDeleted:false,_id:5eaeb78dda27f19dfbfa60c9,name:'myname',email:'test@gmail.com',password:true,role:{model:'ProductManager',type:5eaeb598da27f19dfbfa60c8},token:'eyJhbGciOiJIUzI1NiJ9.NWVhZWI3OGRkYTI3ZjE5ZGZiZmE2MGM5.E7huFLFvRWdUTu-StH2ayF543oBSiq-hlwzT_NSAhD0',reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9'},{invites:[],isAdmin:false,isDeleted:false,_id:5eaeb8feda27f19dfbfa60cc,name:'yourname',email:'example@gmail.com',password:true,role:{model:'Programmer',type:5eaeb551da27f19dfbfa60c7},reviews:[{_id:5eaeb8feda27f19dfbfa60cd,rating:5,comment:'abc',createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z}],invitedFrom:5eaeb78dda27f19dfbfa60c9,token:'eyJhbGciOiJIUzI1NiJ9.NWVhZWI4ZmVkYTI3ZjE5ZGZiZmE2MGNj.Ea7OndfPFhZstDVR6LSQfilQsxLNsZz5Fai3ohcqnoA',createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z,image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc'}]

パイプライン

findXXX系のメソッドは検索条件での取得時にさらに処理を続けることができます。
selectpopulatelimitなど複数のオペレーションをつなげることもできます。

select

取得フィールドを選択します。
空白スペース区切り、配列での指定の両方使えます。

>awaitSimple.findOne().select('str arr'){arr:['a','b'],_id:5eae9847da27f19dfbfa60c5,str:'abc'}>awaitSimple.findOne().select(['str','arr']){arr:['a','b'],_id:5eae9847da27f19dfbfa60c5,str:'abc'}

-fieldのようにマイナスをつけることでそのフィールドだけ取得しないという指定もできます。

>awaitSimple.findOne().select('-str'){arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,__v:0}

ちなみにtoObjectのtransformで操作している場合やvirtualsフィールドは実行されるため、次の例だとselectしたフィールドにさらに付与されます。

>awaitUser.findOne().select('name isAdmin')toObject{isAdmin:false,_id:5eaeb78dda27f19dfbfa60c9,name:'myname',image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false}

populate

refフィールドのObjectIdに紐付いた別スキーマのドキュメントをJOINします。
(RDBでのJOINと同様)

文字列で指定するとJOINしたドキュメントのフィールドがすべて取得できます。
指定ドキュメントに存在しない場合、JOINされません。(undefinedならundefinedのまま、ObjectIdならObjectIdのままになる)

>awaitUser.find().select('invitedFrom').populate('invitedFrom')toObjecttoObjecttoObject[{_id:5eaeb78dda27f19dfbfa60c9,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false},{_id:5eaeb8feda27f19dfbfa60cc,invitedFrom:{role:[Object],invites:[],isAdmin:false,isDeleted:false,_id:5eaeb78dda27f19dfbfa60c9,name:'myname',password:true,reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9'},image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc',password:false}]

更にオブジェクト形式でオプションをつけることで細かい指定ができます。

  • path: populateするフィールド名を指定します。
  • match: populate先ドキュメントの検索条件です。条件に一致しない場合はJOINされず、該当のrefフィールドはnullになります。(配列のrefフィールドの場合は配列の中身から除外)
  • select: populate先ドキュメントの取得フィールドを選択します。
  • populate: populate先ドキュメントから指定フィールドをさらにpopulateする場合に使います。
>awaitUser.find().select('invitedFrom').populate({path:'invitedFrom',select:'name',match:{isDeleted:{$ne:true}}})toObjecttoObjecttoObject[{_id:5eaeb78dda27f19dfbfa60c9,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false},{_id:5eaeb8feda27f19dfbfa60cc,invitedFrom:{_id:5eaeb78dda27f19dfbfa60c9,name:'myname',image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false},image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc',password:false}]>awaitUser.find().select('invitedFrom').populate({path:'invitedFrom',select:'name',match:{isDeleted:true}})toObjecttoObject[{_id:5eaeb78dda27f19dfbfa60c9,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false},{_id:5eaeb8feda27f19dfbfa60cc,invitedFrom:null,image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc',password:false}]

refPathの場合、指定の先のドキュメントが異なる場合でも同時にpopulateできます。
(matchで指定のスキーマのドキュメントのみ取得も可能)

>constusers=awaitUser.find().select('role').populate({path:'role.type',populate:{path:'programmers',match:{skill:'frontend'}}}).select('role')undefined>userstoObjecttoObject[{role:{model:'ProductManager',type:[Object]},_id:5eaeeb23b546bbae29537adc,image:'http://localhost/images/5eaeeb23b546bbae29537adc',password:false},{role:{model:'Programmer',type:[Object]},_id:5eaeeb33b546bbae29537add,image:'http://localhost/images/5eaeeb33b546bbae29537add',password:false}]>users.map(u=>u.role.type.programmers)[[{"skill":["frontend","backend"],"_id":"5eaeb551da27f19dfbfa60c7","createdAt":"2020-05-03T12:13:05.468Z","updatedAt":"2020-05-03T12:13:05.468Z"}],undefined]#populateinpopulateの該当する検索条件に当てはまらなかった場合>constusers=awaitUser.find().select('role').populate({path:'role.type',populate:{path:'programmers',match:{skill:'infra'}}}).select('role')undefined>users.map(u=>u.role.type.programmers)[[],undefined]

sort

findで取得したドキュメントを指定のフィールドでソートします。
1で昇順、-1で降順となります。

>awaitUser.find().sort({createdAt:-1})toObjecttoObject[{role:{type:'Programmer',model:5eaeb551da27f19dfbfa60c7},invites:[],isAdmin:false,isDeleted:false,_id:5eaeb8feda27f19dfbfa60cc,name:'yourname',password:true,reviews:[[Object]],invitedFrom:5eaeb598da27f19dfbfa60c8,createdAt:2020-05-03T12:28:46.061Z,updatedAt:2020-05-03T12:28:46.061Z,image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc'},{role:{type:'ProductManager',model:5eaeb598da27f19dfbfa60c8},invites:[],isAdmin:false,isDeleted:false,_id:5eaeb78dda27f19dfbfa60c9,name:'myname',password:true,reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9'}]

findOneやfindByIdの場合は無意味です。

limit

findで取得する上限のドキュメント数を指定します。

>awaitSimple.find().limit(1)[{arr:['a','b'],refs:[],_id:5eae9847da27f19dfbfa60c5,str:'abc',__v:0}]

findOneやfindByIdの場合は無意味です。

skip

検索結果の指定のドキュメントまでスキップします。ページネーションなどに使えます。
limitと組み合わせることが多いです。

>awaitUser.find().select('name').skip(0).limit(1)toObject[{_id:5eaeb78dda27f19dfbfa60c9,name:'myname',image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',password:false}]>awaitUser.find().select('name').skip(1).limit(1)toObject[{_id:5eaeb8feda27f19dfbfa60cc,name:'yourname',image:'http://localhost/images/5eaeb8feda27f19dfbfa60cc',password:false}]

distinct

指定のフィールドの値の重複を除いて配列形式で指定フィールドの値のみ取得します。(指定できるフィールドは1つのみです。)

>awaitSimple.find().distinct('str')['abc']

lean

ドキュメントをMongooseドキュメントオブジェクトではなく、
プレーンなJavaScript形式で返却します。
model.idmodel.saveなどのMongooseドキュメントオブジェクトに付随しているメソッドが参照できなくなる代わりにデータの取得が大幅に高速化されます。(推奨)
vitualsやdefaultやmethodsなども省略されてしまうため、
mongoose-lean-virtualsやmongoose-lean-defaultsなどのプラグインを導入することでlean時にもオプションで付与することができます。

leanのmethodsはプラグインがないので、
次のようなプラグインを作成することでmethods: trueをleanの引数にわたすことで付与できるようになります。

mongoose-lean-methods.js
constmpath=require('mpath')module.exports=functionmongooseLeanMethods(schema){constfn=attachMethodsMiddleware(schema)schema.pre('find',function(){if(typeofthis.map==='function'){this.map((res)=>{fn.call(this,res)returnres})}else{this.options.transform=(res)=>{fn.call(this,res)returnres}}})schema.post('find',fn)schema.post('findOne',fn)schema.post('findOneAndUpdate',fn)schema.post('findOneAndRemove',fn)schema.post('findOneAndDelete',fn)}functionattachMethodsMiddleware(schema){returnfunction(res){attachMethods.call(this,schema,res)}}functionattachMethods(schema,res){if(res==null){return}if(this._mongooseOptions.lean&&this._mongooseOptions.lean.methods){constmethods={}for(constkeyinschema.methods){if(key==='initializeTimestamps')continuemethods[key]=schema.methods[key]}if(Array.isArray(res)){constlen=res.lengthfor(leti=0;i<len;++i){res[i]=attachMethodsToDoc(res[i],methods)}}else{res=attachMethodsToDoc(res,methods)}for(leti=0;i<schema.childSchemas.length;++i){const_path=schema.childSchemas[i].model.pathconst_schema=schema.childSchemas[i].schemaconst_doc=mpath.get(_path,res)if(_doc==null){continue}attachMethods.call(this,_schema,_doc)}returnres}else{returnres}}functionattachMethodsToDoc(doc,methods){if(Object.keys(methods).length===0){returndoc}for(constkeyinmethods){doc[key]=methods[key]}returndoc}

スキーマにてプラグインを指定します。

user.ts
constmongooseLeanVirtuals=require('mongoose-lean-virtuals')constmongooseLeanDefaults=require('mongoose-lean-defaults')constmongooseLeanMethods=require('../mongoose-lean-methods')constschema=newSchema({})schema.plugin(mongooseLeanVirtuals)schema.plugin(mongooseLeanDefaults)schema.plugin(mongooseLeanMethods)

オプションを使う場合はleanの引数を指定します。

>awaitUser.findOne().lean(){_id:5eaeb78dda27f19dfbfa60c9,invites:[],isAdmin:false,isDeleted:false,name:'myname',password:'pw',role:{type:'ProductManager',model:5eaeb598da27f19dfbfa60c8},reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z}>awaitUser.findOne().lean({virtuals:true,defaults:true,methods:true}){_id:5eaeb78dda27f19dfbfa60c9,invites:[],isAdmin:false,isDeleted:false,name:'myname',password:'pw',role:{type:'ProductManager',model:5eaeb598da27f19dfbfa60c8},reviews:[],createdAt:2020-05-03T12:22:37.723Z,updatedAt:2020-05-03T12:22:37.723Z,image:'http://localhost/images/5eaeb78dda27f19dfbfa60c9',id:'5eaeb78dda27f19dfbfa60c9',showName:[Function]}

aggregate

findのパイプラインと別にaggregateを使ったパイプラインで検索、集計することが可能です。
Mongooseドキュメントではなくlean同様、プレーンなJavaScriptとして返ります。(mongoose特有のvirtualsなどの機能は無視されるので注意)
aggregate専用の機能として主に$groupで集計処理を行うことができるため、
redashなどのBIツールで集計用などに使います。
よく使う操作として以下のものがあります。

  • $match: 検索条件で絞り込みを行います。findと等価です。
  • $project: フィールドの絞り込みやマッピングを行います。selectに似ていますが、計算結果などを新しいフィールドとして定義することができます。
  • $lookup: JOINを行うことができます。動作としてはpopulateと似ていますが記述方法が異なります。
  • $group: 指定のフィールドに対し、グルーピングすることで集計することができます。SQLのグループ文に似た機能です。
  • $unwind: 主に$lookupとセットで使います。$lookupすると配列でネストするため、配列を展開します。

他の操作は公式を参考にしてください。

>awaitUser.aggregate([{$match:{name:{$exists:true}}},{$project:{name:true,reviews:true,invitedFrom:true,createdAt:true}},{$lookup:{from:'users',localField:'invitedFrom',foreignField:'_id',as:'invitedFrom'}},{$unwind:'$invitedFrom'},{$project:{invitedRating:{$sum:'$invitedFrom.reviews.rating'},rating:{$sum:'$reviews.rating'}}}])[{_id:5eaeb8feda27f19dfbfa60cc,invitedRating:0,rating:5}]>awaitUser.aggregate([{$group:{_id:'$isAdmin',total:{$sum:1}}}])[{_id:false,total:2}]

exists

ドキュメントの存在確認を行います。
指定の条件にあったドキュメントが存在すればtrue、存在しなければfalseが返ります。
フォーマットはModel.exists(検索条件)となります。

>awaitSimple.exists({str:'abc'})true>awaitSimple.exists({str:'ab'})false

countDocuments

検索条件にあったドキュメントの数を返します。存在しなければ0が返ります。
フォーマットはModel.countDocuments(検索条件)となります。

>awaitSimple.countDocuments({str:'abc'})2>awaitSimple.countDocuments({str:'ab'})0

更新系の処理

findOneAndUpdate(findByIdUpdate)は戻り値としてドキュメントを返しますが、
update系(updateMany,updateOne)のメソッドは戻り値としてドキュメントを返しません。

findOneAndUpdate、findByIdAndUpdate

findOneAndUpdateは該当のドキュメントを1つのみ検索し、更新し、該当のドキュメントを返却します。
特に検索条件が_idのときはfindByIdAndUpdateが使えます。
フォーマットはModel.findOneAndUpdate('検索条件', '更新操作', 'オプション')のようになっています。

>awaitUser.findOneAndUpdate({name:'myname'},{$set:{phone:'09011112222'},$push:{reviews:[{rating:2}]},$unset:{isDeleted:true}},{runValidator:true,new:true,projection:'name phone reviews isDeleted'}).lean()updated5eaeb78dda27f19dfbfa60c9{_id:5eaeb78dda27f19dfbfa60c9,name:'myname',reviews:[{_id:5eaee4296a87b4a9dfe3c860,rating:2}],phone:'09011112222'}

更新操作

よく使うオペレーターとして以下があります。
複数のオペレータを組み合わせることも可能です。
ただし、$addToSetのフィールドを$pullのフィールドに指定するというように同じフィールドを複数のオペレーターで指定するのは不可です。

  • $set: 指定フィールドに指定の値をセットする。{$set: {フィールド名: 更新する値}}のように指定する。(配列の場合は配列まるごと上書きになるので注意)
  • $unset: 指定フィールドの値を削除する。{$unset: {フィールド名: true}}のように指定する
  • $push: 指定の配列フィールドに値を追加する。{$unset: {フィールド名: 追加する値}}のように指定する。
  • $addToSet: 指定の配列フィールドに値を追加する。{$addToSet: {フィールド名: 追加する値}}}のように指定する。$pushとの違いは配列内の値の重複を許さない(重複がある場合は追加されない)
  • $pull: 指定の配列フィールドから該当する値を削除する。{$pull: {フィールド名: 削除する値}}}のように指定する。

オプション

よく使うオプションは以下のものがあります。

  • runValidator: フィールドの型チェックやvalidateチェックを行います。ほぼ必須なため、preフックなどでオプションのつけ忘れがないように指定するといいでしょう。
  • new: 更新後のドキュメントを返すかのフラグです。ほぼ必須なため、preフックなどでオプションのつけ忘れがないように指定するといいでしょう。
  • upsert: 検索条件に一致しない場合にドキュメントを作成するかのフラグです。
  • projection: 返り値のドキュメントで返すフィールドを指定します。selectと似ています。

updateMany、updateOne

updateManyは検索条件で該当する複数のドキュメントを一括更新します。
updateOneは該当する1つのドキュメントを更新します。
フォーマットはModel.updateMany('検索条件', '更新操作', 'オプション')のようになっています。

>awaitUser.updateMany({isAdmin:false},{$set:{isAdmin:true}},{runValidator:true}){n:2,nModified:2,ok:1}

更新操作

findOneAndUpdate(findByIdAndUpdate)と同じです。
オペレーターに$set,$unset,$push,$addToSet,$pullがあります。

オプション

戻り値でドキュメントを返さないので、よく使うオプションはrunValidatorくらいです。

  • runValidator: フィールドの型チェックやvalidateチェックを行います。ほぼ必須なため、preフックなどでオプションのつけ忘れがないように指定するといいでしょう。

削除系の処理

findOneAndDelete(findByIdDelete)は戻り値としてドキュメントを返しますが、
delete系(deleteMany,deleteOne)のメソッドは戻り値としてドキュメントを返しません。

findOneAndDelete、findByIdAndDelete

findOneAndDeleteは検索条件に一致するドキュメントを1つのみ削除します。
findByIdAndDeleteは_idに一致するドキュメントを削除します。
削除後に削除したドキュメントを返却します。

>awaitSimple.findByIdAndDelete('5eae9847da27f19dfbfa60c5').select('_id').lean(){_id:5eae9847da27f19dfbfa60c5}

deleteMany、deleteOne

deleteManyは検索条件に一致するドキュメントをすべて削除します。
deleteOneは該当のドキュメントを1つのみ削除します。

>awaitSimple.deleteMany({str:'abc'}){n:1,ok:1,deletedCount:1}

bulkWrite

updateOne、updateMeny、deleteOne、deleteManyなどを織り交ぜて一括で操作することができます、
配列形式で複数の操作を指定できます。
オプションのorderdフラグは配列の順番に実行するかを指定するフラグです。
orderdをfalseにすることで実行順番の保証はなくなりますが、高速に実行されます。
主にバッチ処理でデータマイグレーションする際などに使用します。

>constupsertOne={updateOne:{filter:{name:'othername'},update:{$set:{name:'othername',email:'other@gmail.com',password:'pw'}},upsert:true}}>constdeleteOne={deleteOne:{filter:{name:'yourname'}}}>awaitUser.bulkWrite([upsertOne,deleteOne],{ordered:false})BulkWriteResult{result:{ok:1,writeErrors:[],writeConcernErrors:[],insertedIds:[],nInserted:0,nUpserted:1,nMatched:0,nModified:0,nRemoved:1,upserted:[[Object]]},insertedCount:0,matchedCount:0,modifiedCount:0,deletedCount:1,upsertedCount:1,upsertedIds:{'0':5eaf78bc7826e38e558a596f},insertedIds:{},n:0}

インデックス

スキーマのフィールドをindex指定することで指定のフィールドを検索条件に指定した場合の検索が高速になります。
特にドキュメント数が多くなる想定が見込まれる場合、indexを指定することで検索速度を著しく向上させることができます。
uniqueなデータであることが保証されている場合はuniqueオプションをtrueにするとさらに速度が向上します。

user.js
schema.index({email:1},{unique:true})

インデックスが実際に設定されているかの確認はgetIndexesを使います。
フォーマットはModel.collection.getIndexes()となります。
ちなみに_idでのみの検索は自動的インデックスでの検索となります。
(_idのindexは自動的に生成される)

>awaitUser.collection.getIndexes(){_id_:[['_id',1]],email_1:[['email',1]]}

インデックスの指定を更新した場合などに既存のインデックスを削除するにはdropIndexesを指定します。
フォーマットはModel.collection.dropIndexes()となります。

>awaitUser.collection.dropIndexes()

スロークエリの解析

explainを指定することで実際に検索はせず、スロークエリの実行計画を確認することができます。
検索条件時に設定したインデックスを利用しているかどうか確認するために使えます。
さらにexecutionStatsをオプションを指定することで詳細なログが確認できます。

参考:MongoDB v4 explain 結果をちゃんと理解してクエリ改善 (前半:explain結果の見方)

IXSCANが含まれていればインデックスを使っての検索がされています。
COLLSCANの場合、インデックスを使ってないフルスキャンとなっています。

>(awaitUser.find({email:'pdm@gmail.com'}).select('email').explain('executionStats'))[0].queryPlanner.winningPlan{stage:'PROJECTION_SIMPLE',transformBy:{email:1},inputStage:{stage:'FETCH',inputStage:{stage:'IXSCAN',keyPattern:[Object],indexName:'email_1',isMultiKey:false,multiKeyPaths:[Object],isUnique:true,isSparse:false,isPartial:false,indexVersion:2,direction:'forward',indexBounds:[Object]}}}

ReplicaSet

MongoDBはmaster slaveの複数台構成にすることができます。
書き込み系をmaster、参照系をslaveにすることで負荷分散することができます。
また、masterに障害が発生した場合、slaveをmasterに昇格させることができます。

参考:MongoDBのReplica Setsについての概要
参考:MongoDB で 3台構成 の レプリカセット を 構築する 方法
参考:Minimal MongoDB Replica Set (OSX)

マルチドキュメントトランザクション

MongoDB 4.0からの新機能です。
これはRDBMSでお馴染みのTransactionと同等の機能です。
NoSQLであるMongoDBは単一ドキュメント操作に対してはACID特性を持っていましたが、
複数のドキュメント操作が完了するまで、実際の書き込みが発生せず、エラー時には元の状態にロールバックすることができます。
Transaction機能を使うためにはReplicaSet構成が前提です。
別記事にまとめましたのでそちらを参考にしてください。

MongoDB 4.0の待望の新機能Multiple Documents Transactionを試す

テスト

jestでのテストを行います。
mongodb-memory-serverを使うことで、メモリ上でMongoDBを起動することができます。
jest.config.jsにて全テスト前の前処理と前テスト後の後処理を設定できます。

jest.config.js
module.exports={// A path to a module which exports an async function that is triggered once before all test suitesglobalSetup:"./test/global-setup.js",// A path to a module which exports an async function that is triggered once after all test suitesglobalTeardown:"./test/global-teardown.js",}

テストの前処理はglobal-setup.jsに定義します。
global変数はglobalTeardownでは参照できます。
各テストでmongoose接続するため、process.envに保存します。
(globalSetup内でmongoose接続しても各テストでconnectionが持ち越しできない)

global-setup.js
const{MongoMemoryServer}=require('mongodb-memory-server')constmongoServer=newMongoMemoryServer()asyncfunctionsetup(){constmongoUri=awaitmongoServer.getUri()process.env.mongoUri=mongoUriglobal.mongoServer=mongoServer}module.exports=setup

各テストでMongoDBにmongooseから接続を行います。

mongo.js
constmongoose=require('mongoose')mongoose.Promise=global.Promisemongoose.connection.on('error',(e)=>{if(e.message.code==='ETIMEDOUT'){console.log(e);mongoose.connect(mongoUri,mongooseOpts)}console.log(e)})mongoose.deleteAll=async()=>{for(constkeyinmongoose.models){constmodel=mongoose.models[key]awaitmodel.deleteMany()}}constmongooseOpts={useCreateIndex:true,useNewUrlParser:true,useUnifiedTopology:true,useFindAndModify:false,}constmongoUri=process.env.mongoUrimongoose.connect(mongoUri,mongooseOpts)module.exports=mongoose

各テスト終了時に利用してるドキュメントをすべて削除します。
テスト終了時にMongoose接続を破棄します。

mongo.test.js
constmongo=require('./mongo')const{User,ProductManager,Programmer}=require('../models')afterEach(async()=>{awaitmongo.deleteAll()})afterAll(async()=>{awaitmongo.disconnect()})

全テスト終了時にMongoDBを破棄します。

global-teardown.js
asyncfunctionteardown(){awaitglobal.mongoServer.stop()}module.exports=teardown

Node.js at Windows10

$
0
0

環境設定したのでそのメモです。
複数バージョンを切り替えられたほうがいいかなーと思ったので、以下の手順で設定しました。

nvmインストール

まずnvmをインストールする

Node Version Manager (nvm) for Windows
https://qiita.com/spiderx_jp/items/2ddc2f8a5a010ff78b1d
https://github.com/coreybutler/nvm-windows

インストール後

C:\>nvm list

No installations recognized.

C:\>nvm list available

|   CURRENT    |     LTS      |  OLD STABLE  | OLD UNSTABLE |
|--------------|--------------|--------------|--------------|
|    14.1.0    |   12.16.3    |   0.12.18    |   0.11.16    |
|    14.0.0    |   12.16.2    |   0.12.17    |   0.11.15    |
|   13.14.0    |   12.16.1    |   0.12.16    |   0.11.14    |
|   13.13.0    |   12.16.0    |   0.12.15    |   0.11.13    |
|   13.12.0    |   12.15.0    |   0.12.14    |   0.11.12    |
|   13.11.0    |   12.14.1    |   0.12.13    |   0.11.11    |
|   13.10.1    |   12.14.0    |   0.12.12    |   0.11.10    |
|   13.10.0    |   12.13.1    |   0.12.11    |    0.11.9    |
|    13.9.0    |   12.13.0    |   0.12.10    |    0.11.8    |
|    13.8.0    |   10.20.1    |    0.12.9    |    0.11.7    |
|    13.7.0    |   10.20.0    |    0.12.8    |    0.11.6    |
|    13.6.0    |   10.19.0    |    0.12.7    |    0.11.5    |
|    13.5.0    |   10.18.1    |    0.12.6    |    0.11.4    |
|    13.4.0    |   10.18.0    |    0.12.5    |    0.11.3    |
|    13.3.0    |   10.17.0    |    0.12.4    |    0.11.2    |
|    13.2.0    |   10.16.3    |    0.12.3    |    0.11.1    |
|    13.1.0    |   10.16.2    |    0.12.2    |    0.11.0    |
|    13.0.1    |   10.16.1    |    0.12.1    |    0.9.12    |
|    13.0.0    |   10.16.0    |    0.12.0    |    0.9.11    |
|   12.12.0    |   10.15.3    |   0.10.48    |    0.9.10    |

This is a partial list. For a complete list, visit https://nodejs.org/download/release

C:\>nvm arch
System Default: 64-bit.
Currently Configured: -bit.

node.js インストール

C:\>nvm install latest
Downloading node.js version 14.1.0 (64-bit)...
Complete
Creating C:\Users\xxx\AppData\Roaming\nvm\temp

Downloading npm version 6.14.4... Complete
Installing npm v6.14.4...

Installation complete. If you want to use this version, type

nvm use 14.1.0
C:\>nvm use 14.1.0
Now using node v14.1.0 (64-bit)

C:\>node --version
v14.1.0

C:\>nvm install 12.16.3
Downloading node.js version 12.16.3 (64-bit)...
Complete
Creating C:\Users\xxx\AppData\Roaming\nvm\temp

Downloading npm version 6.14.4... Complete
Installing npm v6.14.4...

Installation complete. If you want to use this version, type

nvm use 12.16.3

C:\>nvm list

  * 14.1.0 (Currently using 64-bit executable)
    12.16.3

C:\>nvm use 12.16.3
Now using node v12.16.3 (64-bit)

C:\>node -v
v12.16.3

C:\>npm -v
6.14.4

C:\>

Visual Studio Code の設定

以下参照
Visual Studio CodeとNode.jsの導入について
https://qiita.com/GRGSIBERIA/items/b8cd4a2b3635d1bb0391

その他、参考サイト

Configuring your Windows development environment
https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules

Mac のターミナル・Node.js でのカメラ画像取得: imagesnap とnode-webcam( #GWアドベントカレンダー 5/4 )

$
0
0

こちらは、下記の #GWアドベントカレンダー の 6日目の記事です。

●日数分だけ記事を書く!( @youtoy) | GWアドベントカレンダー
 https://gw-advent.9wick.com/calendars/2020/69

はじめに

この記事に書いた内容をやろうとしたきっかけは、以下のハンズオンに参加したことです。
もう少し説明を書くと、このときはラズパイで USBカメラを利用してコマンドでカメラ画像を取得していたのですが、カメラ画像取得を node.js で行ったり、Mac の内蔵のカメラでラズパイ上でやったのと似たようなことを行ったりできないかが気になったためです。

以下で、試した内容を備忘録も兼ねて書いていこうと思います。

Mac のターミナル上で 内蔵カメラの画像を取得

Mac のターミナル上でコマンドを使ってカメラ画像の取得ができないか調べました。

いろいろとググってみた結果、自分の今の Mac の環境で手軽に実現する方法は Homebrewを使って「imagesnap」を利用するのが良さそうでした。

●rharder/imagesnap: Capture Images from the Command Line
 https://github.com/rharder/imagesnap

imagesnap の導入

まず、imagesnap を導入します。
自分は Homebrew をセッティング済みだったため、以下のコマンドを実行するだけで導入は完了しました。

$ brew install imagesnap

この後、実際にカメラ画像を取得してみます。

カメラ画像の取得(オプションなし)

公式の情報を見ると、シンプルな画像の取得と、出力する画像のファイル名を指定するコマンドは、それぞれ以下のとおりでした。

# シンプルなコマンドで画像取得$ imagesnap
# 出力する画像のファイル名を指定しての画像取得$ imagesnap output.jpg

自分の環境で上記を試すと、ターミナルを実行しているフォルダに画像が出力されたのですが、意図通りの画像を取得できませんでした。

その原因は仮想カメラの CamTwistを導入していたためです。
上記のコマンドで取得できた画像は以下のようなものになっており、内蔵カメラより優先して CamTwist が使われたようでした。
test.jpg

カメラ画像の取得(特定のカメラデバイスを指定)

公式のサイトを見直すと、複数のカメラがある場合にリストを得るコマンドと、カメラを指定するコマンドが書かれていました。

# カメラのリストを得る$ imagesnap -l# カメラを指定して画像を取得する$ imagesnap -d【カメラ名】

出力を確認すると「FaceTime HD Camera」という名称で指定すれば良さそうなことが分かったため、以下のコマンドを実行しました。
「FaceTime HD Camera」の名称が半角スペースを含むため、コマンドではダブルクォーテーションで囲んで実行しました。

$ imagesnap -d"FaceTime HD Camera" output.jpg

上記を実行するとコマンドを実行したフォルダ内に 1280x720 のサイズの画像が出力されましたが、画像の中身が黒い領域ばかりのおかしなデータになっていました。

カメラ画像の取得(カメラの初期化待ちを加える)

そして、imagesnap についての記事をググったところ、とある日本語の記事で以下の記載を見つけました。

しかし、カメラの初期化には多少の時間が必要なようで、デフォルトの状態では真っ黒なJPEGが生成されてしまう。これを避けるためには、「-w」オプションで撮影を遅延させればいい(単位は秒、小数点可)。試したところ、0.2秒(-w 0.2)あたりから薄暗く映るようになり、0.7秒(-w 0.7)も経てばまずまずの明るさで撮影できる。1秒(-w 1)を指定しておけば確実だろう。なお、一度確認すればデバイス名を表示する必要はないので、ついでに「-q」オプションで出力を止めてしまおう。

これに従ってコマンドの内容をさらに修正し、以下を実行することで無事に Mac の内蔵カメラで画像を取得できました。

$ imagesnap -d"FaceTime HD Camera"-w 1 output.jpg

以下が実際に取得した画像です(※ 少しクリッピング・リサイズをしています)。
output.jpg

様々なオプション

なおコマンドでヘルプを見ることもでき、他にも今回使っていないオプションが複数ありました。

$ imagesnap -h#【途中省略】-h          This help message
  -v          Verbose mode
  -l          List available video devices
  -t x.xx     Take a picture every x.xx seconds
  -q          Quiet mode. Do not output any text
  -w x.xx     Warmup. Delay snapshot x.xx seconds after turning on camera
  -d device   Use named video device

Node.js でのカメラ画像取得

試すものの選定

Node.js のパッケージでカメラを利用するものを探してみて、見つかったものの中の 1つに以下がありました。

●node-webcam - npm
 https://www.npmjs.com/package/node-webcam

そして、上記のページ内でちょうど以下のような記載もあったので、こちらを試してみることにしました。

Mac OSX
 #Mac OSX relies on imagesnap
 #Repo https://github.com/rharder/imagesnap
 #Avaliable through brew

 brew install imagesnap

なお、Linux の場合は「fswebcam」を利用し、Windows の場合は「CommandCam(exe は同梱)」を利用する形のようです。

導入してサンプルを動かしてみる

とりあえず、以下のコマンドを実行します。

$ npm install node-webcam

その後、適当な名前で JavaScript のファイルを作成し、公式ページにある以下のプログラムをそのまま実行してみました(なんだか、やたらと改行が多いサンプルだったので、それは適宜削りました)。

varNodeWebcam=require("node-webcam");//Default optionsvaropts={//Picture relatedwidth:1280,height:720,quality:100,//Delay in seconds to take shot//if the platform supports miliseconds//use a float (0.1)//Currently only on windowsdelay:0,//Save shots in memorysaveShots:true,// [jpeg, png] support varies// Webcam.OutputTypesoutput:"jpeg",//Which camera to use//Use Webcam.list() for results//false for default devicedevice:false,// [location, buffer, base64]// Webcam.CallbackReturnTypescallbackReturn:"location",//Loggingverbose:false};//Creates webcam instancevarWebcam=NodeWebcam.create(opts);//Will automatically append location output typeWebcam.capture("test_picture",function(err,data){});//Also available for quick useNodeWebcam.capture("test_picture",opts,function(err,data){});//Get list of camerasWebcam.list(function(list){//Use another devicevaranotherCam=NodeWebcam.create({device:list[0]});});//Return type with base 64 imagevaropts={callbackReturn:"base64"};NodeWebcam.capture("test_picture",opts,function(err,data){varimage="<img src='"+data+"'>";});

そして、内容に手を加えずに上記を実行すると「test_picture.jpg」というファイルは出力されたのですが、予想通り imagesnap を直接使った場合と同じで CamTwist から取得した画像が出力されていました。

オプションに変更を加える

カメラの指定は以下が関係しそうなので、GitHub上のソースコードで以下の指定に関係しそうな部分を見てみました。

//Which camera to use//Use Webcam.list() for results//false for default devicedevice:false,

そうすると、「/node-webcam/blob/master/src/webcams/ImageSnapWebcam.js」の 72行目あたりに以下の部分がありました。
これを見ると、カメラのデバイス名をそのまま指定してやれば良さそうです。
ImageSnapWebcam_js.jpg

そこで、サンプルのソースコード中の「device: false」の部分を「device: "FaceTime HD Camera"」に変更して、再度プログラムを実行しました。
その結果、無事に以下の画像を得ることができました(サンプルを動かした時と同じく、コマンドを実行したフォルダ内に「test_picture.jpg」というファイル名で出力されました)。
test_picture.jpg
なお、imagesnap を直接ターミナル上で使った時はカメラの初期化待ちのディレイを 1秒いれる必要がありそうでしたが、JavaScript のプログラムを実行したときのオプションではデフォルトの「delay: 0」で問題なく動作しました(プログラムを実行してから画像が出力されるまでは、ディレイが入ったような待ちが入った気もしますが)。

おわりに

今回、Mac のターミナル上で Mac内蔵のカメラから画像を取得する方法と、それを利用した Node.js のパッケージを利用して画像を取得する方法とを試しました。

今後、さらに取得した画像を単に出力するだけでなく、取得した画像を処理に用いる JavaScript のプログラムを作ったりしてみたいと思います。

Node + TypeScriptの複数の実行方法(node, ts-node, nodemon, pm2)

$
0
0

プライベート開発時に使うmicrosoft/TypeScript-Node-Starterの開発環境がwatchなどあり快適。
社内の開発環境もその水準にするために調べた際のメモです。

nodeコマンドで実行する

tscでトランスパイルして、nodeコマンドで実行します。

tsc
node dist/app.js

プロセスを確認

xxx     20864   0.0  0.4  4594780  67820 s004  S+    6:38PM   0:00.21 node /usr/local/Cellar/yarn/1.17.3/libexec/bin/yarn.js start

ts-node

tsc + nodeのハッピーセット :fries::hamburger:

ts-node src/app.ts

プロセスを確認

xxx     23344   0.0  0.8  4637472 140320 s004  S+   11:23PM   0:03.45 node /usr/local/bin/ts-node --files src/server.ts

nodemon

node.jsアプリケーションの変更を監視し、サーバーを自動的に再起動します。開発に最適です。

最初はプロセス管理のモジュールだと思ってたのですが、開発用のファイル監視&再起動モジュールだったようです。

プロセスを確認

ps aux | grep nodemon
xxx     22182   0.0  0.2  4560652  30024 s004  S+    9:19PM   0:00.18 /usr/local/bin/node /Users/xxx/works/tsoa-project/node_modules/.bin/nodemon

pm2

PM2 は、ロード・バランサーが組み込まれた、Node.js アプリケーション用の実動プロセス・マネージャーです。PM2 では、アプリケーションの稼働を永続的に維持して、ダウン時間を発生させずに再ロードすることができる。

pm2でトランスパイルを使うことができるが、本番環境では非推奨とのことです。
PM2 - Transpilers | Integration | PM2 Documentation

pm2コマンドだけでts-nodeの代わりにもなり、watchオプションを使えばnodemonの代わりとしても使えます。(watchの設定はnodemonのほうが使いやすかった)

プロセスを確認

ps aux | grep pm2
xxx      1594   0.0  0.4  4614480  59928   ??  Ss    2:56PM   0:04.33 PM2 v4.4.0: God Daemon (/Users/xxx/.pm2) 

God Daemon(神)

まとめ

コマンド役割
tscTypeScriptをJavaScriptにトランスパイル
ts-nodeTypeScriptを直接実行
nodemonファイル監視、再実行
pm2プロセス永続化、ファイル監視

開発時の実行環境は tsc -w + nodemonまたは ts-node + nodemonがよさげ。
本番のプロセス管理にはpm2やfoeverなどを使う。

Windows 10 で express, nodist, node, yarn

$
0
0

インストール

  • windows 10 home (64bit) を用意
  • git bash をインストール
  • nodist をインストール
  • nodeをインストール
  • yarn をインストール
  • express をインストール
  • express-generator をインストール

手順

git for windows をインストール

git bash のためだけに入れます。Power Shell でもできますが、以下では基本的に git bash を使うものとして書いてます。

上記からダウンロードしてインストールします。必要に応じて日本語の設定をする。

Nodist のインストール

下記からインストールできます。

下のほうにある、with installer の "Download the installer from the releases page" と書かれたところを押すと、windows 用のインストーラ(NodistSetup-x.xx.exe) があるので、それをダウンロードしてインストールします。もちろんソースコードをとってきて build するのでも OK。

image.png

node のインストール

基本的に以下の記事の通り。
- [Node.js] Node.js の導入(Windows編)

git bash を開いて、下記のコマンドを実行し、インストールできるバージョンを確認する。

$ nodist dist

たとえば 12.16.3をインストールする場合は、下記のようにする。

$ nodist + 12.16.3

使用するバージョンの変更は、単にバージョンを書くだけ。インストールしただけでは、使用するバージョンは変更されない。

$ nodist 12.16.3
$ node -v
12.16.3

使用するバージョンが本当に変更されているかは確認しておく。

最初に nodist をインストールしたとくは、npm をアップグレードしておく。

npm install npm -g

npm のアップデートについては、git bash で update しても Power Shell 側ではアップデートされていない状態になることがあるようなので、git bash で update したら、ずっと git bash で使ったほうがよさそう。

yarn のインストール

npm でインストールする、しかないぽい。

$ npm install yarn -g
$ yarn -v
1.22.4

express, express-generator のインストール

$ yarn add global express
$ yarn add global express-generator

express-generator までインストールしないと express コマンドは使えない。

express-generator のテスト

$ cd ~/workspace
$ express express_app --view=pug
$ cd express_app
$ yarn install
$ PORT=8000 yarn start

ファイアーウォールのブロックの解除を許可するか、というようなウィンドウがでたら「許可」する。express--view=pugオプションを付けないと、警告が出てテンプレートの拡張子が .jadeになる。将来的に jade は pug に置き換わって、jade 名前は使われなくなるかもしれないので、--view=pugとしておいたほうが良い気がする。

yarn startしたら、ブラウザで http://localhost:8000を開いて下記のような表示がされたら成功している。

image.png

つづく?

DockerでNuxt.jsを起動するまで

$
0
0

実施環境

・macOS 10.15.4
・Docker Desktop Community 2.2.0.5

この記事ではDockerのインストール方法は記述していません。
各自インストールした状態で記事を読み進めてください。

Dockerの準備

アプリを作成したい、好みのディレクトリに移動します。
私はMyNuxtAppとしました。

まず、Docker関連のファイルを作成します。

terminal
% mkdir MyNuxtApp
% cd MyNuxtApp
% touch Dockerfile docker_compose.yml

Dockerfile

Dockerfile
FROM node:12.4.0-alpineWORKDIR /app# コマンド実行# linux 最新化、gitインストール、npm最新化、vue-cliインストールRUN apk update &&\
 apk add git &&\
 npm install-g npm &&\
 npm install-g vue-cli

EXPOSE 9000

ブラウザで接続する時に9000番を使用するために、9000番を開放しています。

docker-compose.yml

docker-compose.yml
# Dockerのバージョン
version: '3'

services:
 nuxt:
   container_name: nuxt_app
   build: .

   # イメージ名
   image: nuxt_app_image

   # ホストOSとコンテナ内でソースコードを共有
   volumes:
    - ./my_nuxt_app:/app
   tty: true

   # コンテナ内部の9000を外部から9000でアクセス
   ports:
    - "9000:9000"

vue-cliをインストールしている時に、「非推奨です」と怒られたので、他の方法を検討する必要がありそうです。
npm WARN deprecated vue-cli@2.9.6: This package has been deprecated in favour of @vue/cli
npm WARN deprecated coffee-script@1.12.7: CoffeeScript on NPM has moved to "coffeescript" (no hyphen)
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142

コンテナ起動

まず、dockerイメージを構築するため、ビルドします。
バックグラウンドで起動するため、-d コマンドをつけています。

terminal
$docekr-compose build
$docker-compose up -d

一番最初はインストールが必要で時間がかかります。しばらく待ちましょう。
インストールが完了したら、イメージがあるか、コンテナが起動しているかを確認します。

terminal

% docker images
REPOSITORY     TAG    IMAGE ID     CREATED        SIZE
nuxt_app_image latest ee8b24df4da9 15 minutes ago 155MB

% docker ps
CONTAINER ID IMAGE          COMMAND                CREATED        STATUS       PORTS              NAMES
bf8510d55dc2 nuxt_app_image "docker-entrypoint.s…" 19 minutes ago Up 4 minutes 0.0.0.0:9000->9000/tcp nuxt_app

docker-compose.ymlで指定したイメージ名・コンテナ名になっているはずです。

コンテナ接続

terminal
% docker exec -it nuxt_app sh
/app #

接続に成功すれば、/appディレクトリに入ります。

Nuxt.jsのプロジェクトの作成・起動

最後に、Nuxtのプロジェクトを作成します。
ここではNuxtのコミュニティが作成しているテンプレートを使用します。

terminal
/app #vue init nuxt-community/starter-template my_nuxt_app

プロジェクト名や説明、作者などを質問されます。面倒な場合はEnter連打で問題ないです。

完了すると、my_nuxt_appディレクトリが作成されるので、こちらに移動します。

このテンプレートにはまだnode_modulesが存在しないので、インストールしてあげます。

terminal
/app #cd my_nuxt_app
/app/my_nuxt_app #npm install

起動設定

コンテナ内でNuxt.jsを立ち上げるだけでは、ブラウザからはアクセスできません。
設定ファイルでポートとホストを指定する必要があります。

作成したmy_nuxt_app直下にあるnuxt.config.jsを編集します。

nuxt.config.js
module.exports={// ここから追記server:{port:9000,host:'0.0.0.0'},// ここまで......

ここで9000番を指定します。
docker-compose.ymlで指定したポート番号と揃えましょう。

これでいよいよ起動できます。

terminal
/app/my_nuxt_app #npm run dev

コンパイルが完了すると、
ℹ Listening on: http://172.20.0.2:9000/
などと表示されます。

Google Chromeでhttp://localhost:9000/を表示させると、、、

スクリーンショット 2020-05-05 1.07.25.png

おめでとうございます!
ひとまずNuxt.jsのプロジェクトを立ち上げることができました。

次は、このプロジェクトを使って実装していきます。
最後までご覧いただきありがとうございました。

#node command run script file – Error: Cannot find module '/not-exist-file.js' ( internal/modules/cjs/loader.js )

$
0
0
  • 存在しないファイルを実行しようとした時に、このエラーが発生した
  • 実行ファイルを指定するとモジュール扱いになる?
  • あまり親切なメッセージで教えてくれたりはしないっぽい
$ node --version
v12.16.1

$ node /not-exist-file.js
internal/modules/cjs/loader.js:985
  throw err;
  ^

Error: Cannot find module '/not-exist-file.js'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:982:15)
    at Function.Module._load (internal/modules/cjs/loader.js:864:27)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
    at internal/main/run_main_module.js:18:47 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3127


nodeのバージョンを下げる@ubuntu18.04

$
0
0

nodeの互換でエラーが出て困った

  • nodeを最新バージョンで入れていて、そのバージョンでアプリが動かない
  • その時の対処法のメモです。

そもそもnodeのインストール方法をメモ

全てのバージョンを確認する

sudo n ls-remote --all

試すバージョンを切り替える

sudo n 11.15.0

「$ firebase list」のエラーに関して

$
0
0

はじめまして、初投稿になります。
ただいま、ポートフォリオサイトをVue.jsのSPAを用いて鋭意作成中です。

その中で、
「Vue.js + Firebase functionsでお問い合わせフォームを作成する」
を参照させて頂きながら、組み込もうと考えておりました。

大変些細な一件ですが、Firebase CLIをインストール中に起きたエラーの対応法を記事にさせて頂きます。

「firebase list」でエラーが返ってくる

Firebase CLI リファレンスに沿って、インストールしていると、

「Firebase プロジェクトを一覧表示し、CLI が正しくインストールされていて、アカウントにアクセスしていることをテストします。次のコマンドを実行します。」と出てきます。

しかしは、

$ firebase list

しても

Error: list is not a Firebase command

と返される。


解決法

どうやら

$ firebase projects:list

に変更されたようです。


それだけのお話でした、初投稿もということもありテストも兼ねて失礼します。
(公式リファレンスに載せられている通りに行ってもエラーになり、なかなかググっても出てこなかったので記事にさせて頂きました。)

npxコマンドの-p -cオプション指定時のNode.jsスクリプトのデバッグ

$
0
0

はじめに

npxコマンドで -p -cオプションを使った場合にNode.jsスクリプトのinspectを使ったデバッグ実行ができなかったので、これの回避策を書きたいと思います。

ここでは、アスキーアートを生成するcowsayコマンドを例に使います。

通常の実行

npxコマンドにオプションを指定しない場合の通常の実行例です。

$ npx cowsay ほげほげ
npx: 10個のパッケージを1.831秒でインストールしました。
 __________
< ほげほげ >
 ----------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

npxコマンドに -n inspectあるいは --node-arg=inspectを渡すと、nodeのインスペクタが起動し、デバッグを行うことができます。

$ npx -n inspect cowsay ほげほげ
npx: 10個のパッケージを1.376秒でインストールしました。
< Debugger listening on ws://127.0.0.1:9229/0edce88f-2821-4ecf-bf09-6c0e1caba8ce
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in /Users/mh-mobile/.npm/_npx/29719/lib/node_modules/cowsay/cli.js:2
  1 #!/usr/bin/env node
> 2 var argv = require("optimist")
  3 .usage("Usage: $0 [-e eye_string] [-f cowfile] [-h] [-l] [-n] [-T tongue_string] [-W column] [-bdgpstwy] text\n\n" +
  4     "If any command-line arguments are left over after all switches have been processed, they become the cow's message.\n\n" +
debug>

-p -cオプションを指定した実行

次は、-p -cオプションを指定して実行した例です。
通常の実行同様にcowsayコマンドが動作します。

$ npx -p cowsay -c "cowsay ふがふが"
npx: 10個のパッケージを1.448秒でインストールしました。
 __________
< ふがふが >
 ----------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ここで、通常の実行同様に、-n inspectを指定すると、ERROR: --node-arg/-n can only be used on packages with node scripts.のエラーが表示され、nodeのインスペクタを使用できません。

$ npx -n inspect -p cowsay -c "cowsay ふがふが"
npx: 10個のパッケージを1.244秒でインストールしました。
ERROR: --node-arg/-n can only be used on packages with node scripts.

回避策

nodeコマンドにinspectの引数を渡して、cowsayコマンドを実行することができれば、デバッグができそうです。ただし、npxコマンドのcオプションにnode inspect cowsayと指定してもインスペクタが動作しません。これは、cowsayコマンドのパスをカレントディレクトリから探していることが原因のようです。

$ npx -p cowsay -c "node inspect cowsay ふがふが"
npx: 10個のパッケージを1.626秒でインストールしました。
< Debugger listening on ws://127.0.0.1:9229/0d44c33e-5152-48fa-823f-d7b9ec8739ee
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
< Waiting for the debugger to disconnect...
$ npx -p cowsay -c "node cowsay ふがふが"
npx: 10個のパッケージを1.236秒でインストールしました。
internal/modules/cjs/loader.js:960
  throw err;
  ^

Error: Cannot find module '/Users/mh-mobile/work/node.js/cowsay'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:957:15)
    at Function.Module._load (internal/modules/cjs/loader.js:840:27)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
    at internal/main/run_main_module.js:18:47 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

試行錯誤の上、whichコマンドを使うと、cowsayコマンドの実行パスを取得できることが分かりました。この実行パスをnode inspectに渡せば、インスペクタを実行できそうです。

$ npx -p cowsay -c "which cowsay"
npx: 10個のパッケージを1.266秒でインストールしました。
/Users/mh-mobile/.npm/_npx/29834/bin/cowsay

nodeコマンドにcowsayコマンドの実行パスを渡すために、xargsコマンドを使用します。xargsコマンドの-Iオプションを指定することで、実行パスをnodeコマンドの直後に渡すことができます。

これで、通常の実行同様にcowsayコマンドが動作します。

$ npx -p cowsay -c "which cowsay | xargs -I{} env node {} ふがふが"
npx: 10個のパッケージを1.64秒でインストールしました。
 __________
< ふがふが >
 ----------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

次に、nodeコマンドにinspectを渡して、実行します。

$ npx -p cowsay -c "which cowsay | xargs -I{} env node inspect {} ふがふが"
npx: 10個のパッケージを1.497秒でインストールしました。
< Debugger listening on ws://127.0.0.1:9229/4461f637-f92d-4b4a-9458-f65c9ff53e83
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in /Users/mh-mobile/.npm/_npx/29918/lib/node_modules/cowsay/cli.js:2
  1 #!/usr/bin/env node
> 2 var argv = require("optimist")
  3 .usage("Usage: $0 [-e eye_string] [-f cowfile] [-h] [-l] [-n] [-T tongue_string] [-W column] [-bdgpstwy] text\n\n" +
  4     "If any command-line arguments are left over after all switches have been processed, they become the cow's message.\n\n" +
debug>

これで、コンソールでデバッグ停止するようになりましたが、私の環境ではステップ実行などの操作がコンソール上で動作しませんでした(笑)。

ただし、Chromeのインスペクタをchrome://inspectで起動することでデバッグ実行ができるようになります。

image.png

Use Spotify Sign In with Firebase

$
0
0

Use Spotify Sign In with Firebase
https://github.com/firebase/functions-samples/tree/master/spotify-auth
をやってみた

環境

  • Windows10
  • PowerShell
  • Visual Studio Code

作業フォルダ作成

適当な場所にgithubの内容をコピー

https://github.com/firebase/functions-samples

Cloud Functionsの初期化

Prerequisites
To learn how to get started with Cloud Functions for Firebase by having a look at our Getting Started Guide, trying the quickstart samples and looking at the documentation.

まずこれ読めとあるのでやっていく
https://firebase.google.com/docs/functions/get-started

Firebaseでプロジェクトを作成した(割愛)

PSC:\>npminstall-gfirebase-toolsPSC:\>firebaseloginAlreadyloggedinasXXXPSC:\>firebaseinitfunctions

この時に、Firebaseで作成したプロジェクトを選択する

Enable Billing on your Firebase project by switching to the Blaze or Flame plan, this is currently needed to be able to perform HTTP requests to external services from a Cloud Function. See the pricing page for more details.

function で外部URLへリクエストを投げるため、ライセンスを変更する。

Googleサービスアカウントの作成

https://firebase.google.com/docs/admin/setup#add_firebase_to_your_app

サービス アカウント用の秘密鍵ファイルを生成するには:
1.Firebase コンソールで、[設定] > [サービス アカウント] を開きます。
2.[新しい秘密鍵の生成] をクリックし、[キーを生成] をクリックして確定します。
3.キーを含む JSON ファイルを安全に保管します。

ダウンロードしたJSONファイルを./functions/service-account.jsonに保存する

Spotify側でアプリ登録

Create a Spotify app in the Spotify Developers website.

登録する

Add the URL https://.firebaseapp.com/popup.html to the Redirect URIs of your Spotify app.

このタイミングでは何のことかわからなかったので、適当に登録する

Copy the Client ID and Client Secret of your Spotify app and use them to set the spotify.client_id and spotify.client_secret Google Cloud environment variables. For this use:

クライアントID、クライアントシークレットを取得して、firebaseに登録する

PSC:\>firebasefunctions:config:setspotify.client_id="XXX"spotify.client_secret="XXX"+Functionsconfigupdated.Pleasedeployyourfunctionsforthechangetotakeeffectbyrunningfirebasedeploy--onlyfunctions

Deploy

上で、firebase deploy --only functionsと出たのでやる

PSC:\>firebasedeploy--onlyfunctions===Deployingto'xxx-xxx'...ideployingfunctionsRunningcommand:npm--prefix"$RESOURCE_DIR"runlint>xxx-functions@lintC:\xxx\functions>eslint--max-warnings=0.+functions:Finishedrunningpredeployscript.ifunctions:ensuringrequiredAPIcloudfunctions.googleapis.comisenabled...+functions:requiredAPIcloudfunctions.googleapis.comisenabledifunctions:preparingfunctionsdirectoryforuploading...ifunctions:packagedfunctions(47.52KB)foruploading+functions:functionsfolderuploadedsuccessfullyifunctions:creatingNode.js8functionredirect(us-central1)...ifunctions:creatingNode.js8functiontoken(us-central1)...+functions[redirect(us-central1)]:Successfulcreateoperation.FunctionURL(redirect):https://us-central1-xxx.cloudfunctions.net/redirect+functions[token(us-central1)]:Successfulcreateoperation.FunctionURL(token):https://us-central1-xxx.cloudfunctions.net/token+Deploycomplete!ProjectConsole:https://console.firebase.google.com/project/xxx/overview

??? 何が起きたかよくわからなかった。

Run firebase deploy to effectively deploy the sample. The first time the Functions are deployed the process can take several minutes.

とあったので、今度は、firebase deployを実行する。
→エラー、firebase databaseあたりのエラー

firebase WEBコンソールから、firebase database を有効にする

firebase deployを再実行する

PSC:\>firebasedeploy===Deployingto'xxx-xxx'...ideployingdatabase,functions,hostingRunningcommand:npm--prefix"$RESOURCE_DIR"runlint>xxx-functions@lintC:\xxx\functions>eslint--max-warnings=0.+functions:Finishedrunningpredeployscript.idatabase:checkingrulessyntax...+database:rulessyntaxfordatabasexxx-xxxisvalidifunctions:ensuringrequiredAPIcloudfunctions.googleapis.comisenabled...+functions:requiredAPIcloudfunctions.googleapis.comisenabledifunctions:preparingfunctionsdirectoryforuploading...ifunctions:packagedfunctions(47.52KB)foruploading+functions:functionsfolderuploadedsuccessfullyihosting[xxx-xxx]:beginningdeploy...ihosting[xxx-xxx]:found6filesinpublic+hosting[xxx-xxx]:fileuploadcompleteidatabase:releasingrules...+database:rulesfordatabasexxx-xxxreleasedsuccessfullyifunctions:updatingNode.js8functionredirect(us-central1)...ifunctions:updatingNode.js8functiontoken(us-central1)...+functions[redirect(us-central1)]:Successfulupdateoperation.+functions[token(us-central1)]:Successfulupdateoperation.ihosting[xxx-xxx]:finalizingversion...+hosting[xxx-xxx]:versionfinalizedihosting[xxx-xxx]:releasingnewversion...+hosting[xxx-xxx]:releasecomplete+Deploycomplete!ProjectConsole:https://console.firebase.google.com/project/xxx-xxx/overviewHostingURL:https://xxx-xxx.web.app

動作確認 1

https://xxx-xxx.web.appにアクセスしてみる。表示される。
Spotifyでログインする。エラーで止まる。。。
エラー内容

TypeError: Cannot read property 'url' of undefined
    at Spotify.getMe (/srv/index.js:88:59)

spotify APIを実行した結果を処理する部分でエラーが発生している様子
https://developer.spotify.com/console/get-current-user/

spotifyでAPIを実行して、結果が確認できる(上のURLで)ので、内容を確認すると、imagesがカラ

"images": [],

しかし、コード部分の記述はこう

index.js
constprofilePic=userResults.body['images'][0]['url'];

なのでこうした。
未定義の場合をカラ文字とした場合、別の場所でエラーが発生したので、画像のURLを適当に充てることにした

index.js
constprofilePic=userResults.body['images'][0]?userResults.body['images'][0]['url']:'https://placehold.jp/50x50.png';

これでエラーは出なくなった。
しかし、ログイン済みの画面に切り替わらない。。。

動作確認 2

ログイン済みの画面に切り替わらない問題の解決

firebase hostingでは、二つのホスト名が割り当てられます

  • プロジェクト名.web.app
  • プロジェクト名.firebaseapp.com

どちらのホスト名でも動作するのですが、デプロイ後に表示されるメッセージは、https://xxx-xxx.web.appとなっており、そのホスト名で動作確認を行っていたことが原因でした。
Spotify側に登録したリダイレクトURIと同じホスト名を使って動作確認する必要がありました。
(おそらくCookieの影響範囲あたりの問題だったと思う)

動作確認 3

ログアウト。

Firebase側のアプリのログアウトはできました。
再度ログインでSpotifyへリダイレクトされると、Spotiry側はログイン済みで、Firebase側へ戻ってきました。

OAuthの手続きとしてはそういうものかなーと、今のところ思ってます。

ExpressのAsync Errorのハンドリング

$
0
0

問題点: asyncでリクエストハンドラを定義して処理中に例外が発生してもハンドリングできない

たとえば、http://localhost:3333/notfoundrouteのように存在しないルートにアクセスしたら、ステータスコード:404、ボディコンテンツにRoute not foundを返したい場合において、以下のコードを実装したとする。

importexpress,{Request,Response,NextFunction}from"express";constapp=express();app.get("/hi",(req,res)=>{res.send("Say hi");});// 問題の箇所// ここで例外を発生させるapp.get("*",async(req,res)=>{thrownewError("Route not found");});// エラーを扱うためのハンドラapp.use((err:Error,req:Request,res:Response,next:NextFunction)=>{returnres.status(404).send(err.message);});app.listen(3333,()=>console.log("Listenin on port 3333"));

しかし、実行結果としては、レスポンスが返ってこない

curl -v localhost:3333/notfoundroute
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3333 (#0)
> GET /notfoundroute HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.64.1
> Accept: */*
>

サーバ側のログ出力も以下のようになっていて、Promiseを上手く扱えていない。

(node:11743) UnhandledPromiseRejectionWarning: Error: Route not found
...
(node:11743) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:11743) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

解決策1: nextで例外をラップする。

問題の箇所を以下のように変更する。

...app.get("*",async(req,res,next)=>{next(newError("Route not found"));});...

実行結果は、期待した結果が得られている。

curl -v localhost:3333/notfoundroute
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3333 (#0)
> GET /notfoundroute HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 15
< ETag: W/"f-f4JcZAomq7bAiiL2cdorz+qCRSw"
< Date: Tue, 05 May 2020 07:00:15 GMT
< Connection: keep-alive
< 
* Connection #0 to host localhost left intact
Route not found* Closing connection 0

解決策2: express-async-errorsパッケージをインポートする。

必要なパッケージをインストールする。

yarn add express-async-errors

問題の箇所は変更しなくてもよいが、express-async-errorsをインポートする。

importexpress,{Request,Response,NextFunction}from"express";import"express-async-errors";...app.get("*",async(req,res)=>{thrownewError("Route not found");});...

こちらも実行結果は、期待した結果が得られている。

curl -v localhost:3333/notfoundroute
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3333 (#0)
> GET /notfoundroute HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 15
< ETag: W/"f-f4JcZAomq7bAiiL2cdorz+qCRSw"
< Date: Tue, 05 May 2020 07:00:15 GMT
< Connection: keep-alive
< 
* Connection #0 to host localhost left intact
Route not found* Closing connection 0
Viewing all 8868 articles
Browse latest View live