- 投稿日:2021-02-25T23:17:04+09:00
Node.js+MongoDB構成でGraphQLのお勉強
はじめに
現場でGraphQLを使用しているのですが、保守改修段階でプロジェクトに入ったのでスキーマ作成などの基本的なところをやったことがありませんでした。また、NoSQLデータベースも使ったことがなかったので、まとめて学んでみようということでNode.js+MongoDBの構成でGraphQLサーバをたててみることにしました。
今回作成したプロジェクトはGitHubにあります。
GraphQLのよさ
RESTとの比較記事がいたるところにあるので(例えばこちら)詳しく書きませんが、単一のエンドポイントであるというのがGraphQLの一番の特徴です。
GraphQLをつかうことで、RESTで必要なすべてのデータを取得しようとするときに発生する以下のような問題を解決することができます。
- 複数のエンドポイントへリクエストを行う必要がある
- 不要なデータも一緒に取得されてしまう
サーバ構築
適当なフォルダ(
graphql-server-practice
)を作成してyarn init
します。そのあと、以下の構成でフォルダおよびファイルを作成します。Schema, Query, Mutationを
Schema.js
に記載していきます。Schemaの作成
今回は練習のため、User, Hobby, Postの3つのSchemaをつくります。
Userはそれぞれ複数のHobbyやPostをもてるような関係になっています(One to Many relationship)。まずは
yarn add graphql
でgraphqlパッケージを導入し、schema.js
で以下を読み込みます。schema.jsconst graphql = require('graphql'); const { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLInt, GraphQLSchema, GraphQLNonNull, GraphQLList, } = graphql;UserのSchemaをUserTypeとして作成します。GraphQLObjectTypeでラッピングすることでSchemaのname, description, fieldsを定義することができます。
fieldsのid, name, age, professionをGraphQLID, GraphQLString, GraphQLInt, GraphQLStringというスカラー型で定義します。
schema.jsconst UserType = new GraphQLObjectType({ name: 'User', description: 'Documentation for user...', fields: () => ({ id: { type: GraphQLID }, name: { type: GraphQLString }, age: { type: GraphQLInt }, profession: { type: GraphQLString }, }), });HobbyTypeとPostTypeについても同様に作成します。
schema.jsconst HobbyType = new GraphQLObjectType({ name: 'Hobby', description: 'Hobby description', fields: () => ({ id: { type: GraphQLID }, title: { type: GraphQLString }, description: { type: GraphQLString }, }), }); const PostType = new GraphQLObjectType({ name: 'Post', description: 'Post description', fields: () => ({ id: { type: GraphQLID }, comment: { type: GraphQLString }, }), });3つのSchemaを作成しましたが、この状態ではまだUserとPost、UserとHobbyの関係が定義されていないので、これらのリレーションを考えてあげる必要があります。
UserとPostの例を考えてみます。
const UserType = new GraphQLObjectType({ name: 'User', description: 'Documentation for user...', fields: () => ({ id: { type: GraphQLID }, name: { type: GraphQLString }, age: { type: GraphQLInt }, profession: { type: GraphQLString }, posts: { type: new GraphQLList(PostType), resolve(parent, args) { return postsData.filter((data) => data.userId === parent.id); }, }, }), }); const PostType = new GraphQLObjectType({ name: 'Post', description: 'Post description', fields: () => ({ id: { type: GraphQLID }, comment: { type: GraphQLString }, user: { type: UserType, resolve(parent, args) { return usersData.find((data) => data.id === parent.userId); }, }, }), });User1つに対してPostは複数存在します。そのため、UserTypeのfieldsに新しく作られたpostsはPostTypeの配列型となり、
new GraphQLList(PostType)
と定義されます。posts: { type: new GraphQLList(PostType), resolve(parent, args) { return postsData.filter((data) => data.userId === parent.id); }, },また、resolveはどのUserに対するPostを表示するのかを定義するものです。parentは親のfields(ここではUserType)を指しており、以上の処理ではUserのidと等しいuserIdをもったPostのデータのみを取得するようになっています。
const usersData = [ { id: '1', name: '山田勝己', age: 36, profession: 'SASUKE' }, ]; const postsData = [ { id: '1', comment: '僕にはSASUKEしかないんです', userId: '1' }, { id: '2', comment: '完全制覇がしたいんです', userId: '1' }, ];Queryの作成
QueryはSchemaと同様に、GraphQLObjectTypeで定義を行います。試しに指定したidのUserを取得するuserクエリとすべてのUserを取得するusersクエリを作成してみます。
const RootQuery = new GraphQLObjectType({ name: 'RootQueryType', description: 'Description', fields: { user: { type: UserType, args: { id: { type: GraphQLID } }, resolve(parent, args) { return usersData.find((data) => data.id === args.id); }, }, users: { type: new GraphQLList(UserType), resolve(parent, args) { return usersData; }, }, }, });userクエリではidを引数(args)としてとるので、fieldsでargsの型定義を行っています。resolveではargsのidと同じデータだけ取得するような処理を書いています。
一方、usersクエリでは、すべてのデータを取得するだけなのでargsは必要ありません。
ローカルサーバをたててQueryの動作確認
Mutation作成とDB接続の前に、ローカルサーバをたててQueryの挙動を確認します。
作成したRootQueryをnew GraphQLSchemaでラッピングしてエクスポートします。
schema.jsmodule.exports = new GraphQLSchema({ query: RootQuery });
yarn add express express-graphql
で必要なパッケージを導入し、以下の設定を行います。app.jsconst express = require('express'); const { graphqlHTTP } = require('express-graphql'); const schema = require('./schema/schema'); const app = express(); app.use( '/graphql', graphqlHTTP({ graphiql: true, schema, }) ); app.listen(4000, () => { console.log('Listening for requests on my awesome port 4000'); });
node app
で4000ポートにサーバが立ち上がるのですが、ソースコードの修正をリアルタイムで反映させるためにyarn global add nodemon
でnodemon
を導入します。
nodemon app
でサーバを立ち上げ、http://localhost:4000/graphql
を開くと以下の画面が現れます。userクエリを試しに実行すると右側に取得データが表示されます。postsのデータも問題なく表示されています。
MongoDBとの接続
DBと接続してMutationを実装します。
MongoDB Atlasの設定
MongoDB Atlasのアカウントを作成します。Googleアカウントがあれば大丈夫です。
Projects内でClusterを作成します。今回、クラウドにはAWSを使用し、DB性能に関わるCluster Tierには無料のM0 Sandboxを使用します。
作成したClusterのCONNECTボタンを押して、"Connect using MongoDB Compass"を選択します。CompassはMongoDB用のGUIツールです。
Compassをダウンロードし、DB接続用のコードをコピーします。
Node.jsの設定
GraphQLとMongoDBと連携するために、Node.jsのmongooseというパッケージを使用します(
yarn add mongoose
)。
app.jsファイルを以下のようにします。mongoose.connect
でMongoDBとの接続、mongoose.connection.once
で接続が成功したことを確認するためのコンソールログを行っています。app.jsconst express = require('express'); const { graphqlHTTP } = require('express-graphql'); const mongoose = require('mongoose'); const schema = require('./schema/schema'); const app = express(); mongoose.connect( 'mongodb+srv://dbUser:<password>@cluster0.gjo5x.mongodb.net/test', // <password>には自分で設定したものを入力 { useNewUrlParser: true } ); mongoose.connection.once('open', () => { console.log('we are connected.'); }); app.use( '/graphql', graphqlHTTP({ graphiql: true, schema, }) ); app.listen(4000, () => { console.log('Listening for requests on my awesome port 4000'); });Modelの作成
DBのSchemaにあたるModelを作成していきます。GraphQLのSchemaとModelを関連付けることで、DBからデータを取得(Query)したり、登録・削除(Mutation)などを行うことができます。
modelフォルダ以下に新しいファイルを作成します。
UserのModelは以下のようになります。userSchemaは後ほどschema.jsで読み込むので、最後にエクスポートします。
user.jsconst mongoose = require('mongoose'); const MSchema = mongoose.Schema; const userSchema = new MSchema({ name: String, age: Number, profession: String, }); module.exports = mongoose.model('User', userSchema);Mutationの作成
Userデータの作成(CreateUser)、更新(UpdateUser)、削除(RemoveUser)のMutationsを作成します。
schema.jsconst graphql = require('graphql'); const User = require('../model/user'); ~中略~ const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { CreateUser: { type: UserType, args: { name: { type: new GraphQLNonNull(GraphQLString) }, age: { type: new GraphQLNonNull(GraphQLInt) }, profession: { type: GraphQLString }, }, resolve(parent, args) { let user = new User({ name: args.name, age: args.age, profession: args.profession, }); return user.save(); }, }, UpdateUser: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLString) }, name: { type: new GraphQLNonNull(GraphQLString) }, age: { type: GraphQLInt }, profession: { type: GraphQLString }, }, resolve(parent, args) { return (updatedUser = User.findByIdAndUpdate( args.id, { $set: { name: args.name, age: args.age, profession: args.profession, }, }, { new: true } )); }, }, RemoveUser: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLString) }, }, resolve(parent, args) { let removedUser = User.findByIdAndRemove(args.id).exec(); if (!removedUser) { throw new 'Error'(); } return removedUser; }, }, } ~中略~ module.exports = new GraphQLSchema({ query: RootQuery, mutation: Mutation, });resolve内でUserのModelを読み込み、データ登録用のメソッド(save)や更新用のメソッド(findByIdAndUpdate)を使用します。これらのメソッドについては、mongooseの公式Docsに使い方の詳細な説明が記載されています。
また、更新や削除では処理を行うデータを指定するため、argsのidはNon-nullとなります。Non-nullにしたいカラムについては、GraphQLNonNullをラッピングします。
最後にMutationのエクスポートも忘れずに行います。
Mutationの実行
localhost:4000/graphql
を開き、CreateUserを試しに実行してみます。
MongoDB Compassでデータが登録されたことを確認することができました。
おわりに
今回のサーバ構築作業には結構時間がかかったのですが、AWS AppSyncを使ったら一瞬で構築できました。なかなか衝撃的な体験だったので、別記事で書こうと思います。
また、PostやHobbyのModelやMutationなど、記載を省略した部分についてご興味がありましたら、GitHubをご確認いただければと思います。
- 投稿日:2021-02-25T20:58:08+09:00
DockerでNode.jsアプリを起動する
概要
先日作ったSlackBotを定期実行したいため、Dockerでnodejsの環境を構築します。
前提条件
$ docker --version Docker version 20.10.2, build 2291f61ジョブフローを毎日18:00に実行する設定済み
job.jsconst schedule = require('node-schedule'); schedule.scheduleJob(`00 00 18 * * 1,2,3,4,5`, run);Dockerイメージを作成する
1、プロジェクトフォルダー直下に
Dockerfile
を作成して、以下のコードを貼り付けるFROM node:12.20.1 WORKDIR /app COPY . . RUN npm install ENV TZ Asia/Tokyo EXPOSE 8888 CMD ["node" , "job.js" ]2、
.dockerignore
ファイルを設置してnode_modulesなどを転送対象から除外するnode_modules/ .gitignore Dockerfile package-lock.json3、Dockerイメージのビルド
実行コマンド(
magical-yuanxiao
はイメージ名)$ docker build -t magical-yuanxiao .ログ
Successfully built aebb1184bf44 Successfully tagged magical-yuanxiao:latestコンテナを起動状態で作成する
実行コマンド(
magical-yuanxiao
は先作ったイメージ)$ docker run -p 8888:8888 magical-yuanxiao最後に、
docker ps -a
コマンドでコンテナの稼働状況を確認します。自動的に実行される
参照サイト
- 投稿日:2021-02-25T14:25:05+09:00
【Node.js】日時処理を扱う方法
プログラミング勉強日記
2021年2月25日
日付処理を使うための準備
Node.jsで日付処理を扱うために、今回はdate-utilsを使用する。
date-utilsはnpmパッケージの1つで、簡単にインストールすることができる。date-utilsをインストールする$ npm install date-utils現在時刻を表示する
// date-utilsを呼び出す require('date-utils'); let now = new Date(); console.log(now.toFormat('YYYY年MM月DD日 HH24時MI分SS秒')); console.log(now.toFormat('YY年M月D日 H時MI分SS秒')); console.log(now.toFormat('DDD MMM DD YYYY HH24:MI:SS')); console.log(now.toFormat('M/D/YY'));実行結果2021年02月25日 01時52分13秒 21年2月25日 1時52分13秒 Thu Feb 25 2021 01:52:13 2/25/21参考文献
- 投稿日:2021-02-25T13:45:48+09:00
Node.js セキュリティアップデート + Node.js v15 について
Node.js セキュリティアップデートが出てた
2.23 に、Node.js の新しいバージョンがでたのでチェックしよう。
https://nodejs.org/en/blog/vulnerability/february-2021-security-releases/10.x, 12.x, 14.x 15.x 用のセキュリティアップデート
重大(1), 高い深刻度(1), 低い深刻度(1)
- HTTP/2 の unknownProtocol があまりにも多くなった時、DoS 攻撃を受ける可能性がある問題
- ファイルシステムが漏洩する可能性
- メモリリークを引き起こす可能性
- localhost6 がホワイトリストに含まれている場合、DoS 攻撃を受ける可能性がある問題
- /etc/hosts に localhost6 を含んでいる場合、この攻撃を受ける可能性がある https://jvndb.jvn.jp/ja/contents/2018/JVNDB-2018-005401.html
- OpenSSL の脆弱性が Node.js を介して悪用される可能性がある問題
適宜追従しましょう(フロントエンドだけなら別にって感じだけど)。
※Nodejs 10.x は 2021.4.30 にサポート終了予定、12.x 系は 2022.4.30 で終了予定
※奇数バージョンは機能開発用バージョンで、偶数バージョンは長期サポートバージョン。最新 15.x 以外の奇数はないNode.js のバグ報告について
nodejs-sec グループ
https://groups.google.com/g/nodejs-sec?pli=1バグ報告の方法
https://github.com/nodejs/node/blob/master/SECURITY.mdNodejs にバグを報告するとお金もらえるプログラム:バグバウンティプログラム
https://hackerone.com/nodejs
Node.js v15 について
Node.js v15 あまりキャッチアップしてなかったので、おさらいしてみました。
v15 のリリースは 2020.10.21。v16 は 2021.4 とかですかね。
公式 Medium
https://nodejs.medium.com/node-js-v15-0-0-is-here-deb00750f278codedamn さんのまとめ YouTube
https://www.youtube.com/watch?v=OIzGI5wFiXgめっちゃまとまってる
https://shisama.hatenablog.com/entry/2020/10/21/0046121. npm 7 が同梱
npm 7 が入った。
https://www.npmjs.com/package/npm
workspace
という概念が入った
- monorepo で作りたい時とかは便利そう
acceptDependencies
という概念も入ったpeerDependencies
の挙動変更npx
が実行前に確認されるようになったnpm audit
の表示方法が変更package.exports
が npm の内部モジュールを参照できないようになったnpm test
の表示内容が変更npm build
とnpm unbuild
が廃止https://blog.watilde.com/2020/10/14/npm-v7の主な変更点まとめ/
2. unhandledRejection の挙動変更
ブレイキングチェンジ!
unhandledRejection
の挙動が変わった。defaultwarn
だったのがthrow
になった。ただそもそもこれが起きてる時は以下のタイミングで
Promise
を使った非同期処理において想定外のエラーが起きた- エラーハンドリングが漏れている
- 単純なコードのエラー
マズイプログラムになっているはずなので、挙動が変更になることでより安全なコードが書けるようになった。
https://zenn.dev/kimamula/articles/b32d11d52c2b7a733119
unhandledRejection について
https://medium.com/@hagevvashi/unhandledrejectionについて-70739d2b6a60
Web サービスを運営していると、Sentry を使ってエラーハンドリングしたりすると思う
window.onerror
は、Promise.reject()
によって投げられたエラーは拾わない
(ただしconsole.error()
形式で表示はされる)
Promise
を使うエラー(Node.js でファイル操作だとかをするときの I/O 処理や、ブラウザで API 通信の失敗)を検知する時に使う。try { await axios.post("/some-resources"); } catch(e) { // error handling throw e; // this causes unhandled rejection }window.addEventListener("unhandledrejection", (e: PromiseRejectionEvent): void => { // an error loginng logError(e.reason); });process.on("unhandledRejection", (error: {} | null | undefined, promise: Promise<any>) => { // an error loging logError(error); })https://medium.com/@hagevvashi/unhandledrejectionについて-70739d2b6a60
3. ES2021 に対応
JavaScript エンジンの V8 が v8.6 にアップデート。まぁでもブラウザでは既に使えるので珍しくはない
Promise.any
Promise.all
は、全部がresolved
になった時 / どれかがrejected
になった時エラーconst promise1 = Promise.resolve(3); const promise2 = 42; const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'foo'); }); Promise.all([promise1, promise2, promise3]) .then((values) => { console.log(values); }) .catch(() => { console.log("error"); }); // expected output: Array [3, 42, "foo"]
Promise.race
は、どれかがresolved
になった時 / どれかがrejected
になった時エラー
Promise.allSettled
は、全部がresolved
かrejected
になった時 / エラーにはならない
Promise.any
は、どれかがresolved
になった時 / 全部がrejected
になった時エラー10 個の
Promise
を走らせて、9 個失敗しても、最後の 1 つが成功なら成功としたい時(race
だとどれかがreject
になるとエラーになっちゃう)に使うWeakRefs
Garbage Collection 時に破棄できるオブジェクトを作れる。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
var ref = new WeakRef(element) ref.deref() // 参照元を取り出す簡単にサンプルを作ってみた。↓
https://playcode.io/740665/タイマーが動き続けている。別のタイマーで DOM 要素を 5 秒後に
remove
する。元のタイマー関数では参照はできている。Garbage Collection されると参照がきえて止まる。メモリリークを防ぐコードが書ける。
AggregateError
複数のエラーをまとめたい時に使う。
try { throw new AggregateError([ new Error("some error"), ], 'Hello'); } catch (e) { console.log(e instanceof AggregateError); // true console.log(e.message); // "Hello" console.log(e.name); // "AggregateError" console.log(e.errors); // [ Error: "some error" ] }エラーを配列で与えられる。
Logical Assignment Operators 論理代入演算子
読みにくくなるからやめてほしいw
let a = true a &&= false // x && (x = y): x と y が true な値の場合、y の値が代入される // false a // false a ||= true // x || (x = y): x が false な値の場合、y の値が代入される // true a // true a = null a ??= 1 // x ?? (x = y): x が null or undefined な値の場合、y の値が代入される // 1 a // 1 a ??= 2 // 1 a // 1Numeric separators
桁が読みやすくなるから積極的に使おう。
123 === 1_2_3 // true 1_000_000_000.000_001 // 1000000000.000001ただこんなでかいマジックナンバー扱いたくないし扱う機会もない気がする。
String.prototype.replaceAll
ブラウザで使えるようになってたやつ。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
https://caniuse.com/mdn-javascript_builtins_string_replaceallIE 11 は Babel を通せば
replaceAll
が書かれたコードはトランスパイルしてくれる。preset-env (core-js)
を使うことで IE11 でも動く。https://v8.dev/features/string-replaceall
ちなみに Next.js は
@babel/preset-env
入ってないので IE11 対応させるならconfig
で指定する。4. Web Crypto API
JavaScript で暗号化・復号・署名・検証ができる Web 標準 API。
https://caniuse.com/cryptography
ブラウザでは昔っから使えたやつ。
Node は最近ブラウザで使えたやつが使えるようになってきているといういい話。5. AbortController
Web リクエストを中止することができるグローバルユーティリティオブジェクトの
AbortController
が Node.js 15 でも使えるようになった。const ac = new AbortController(); ac.signal.addEventListener('abort', () => console.log('Aborted!'), { once: true }); ac.abort(); console.log(ac.signal.aborted); // Prints True動画のダウンロードを中止する例
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
var controller = new AbortController(); var signal = controller.signal; var downloadBtn = document.querySelector('.download'); var abortBtn = document.querySelector('.abort'); downloadBtn.addEventListener('click', fetchVideo); abortBtn.addEventListener('click', function() { controller.abort(); console.log('Download aborted'); }); function fetchVideo() { ... fetch(url, {signal}).then(function(response) { ... }).catch(function(e) { reports.textContent = 'Download error: ' + e.message; }) }
fetch
はまだ Node.js に実装されてないけどね。フロントエンドの UI 開発で、ボタンに対しての mousedown/mouseup イベントを addEventListen していて、クリック後画面遷移してしまうので removeEventListen しておきたい時、abortController を使えば一発で解除できるテクがどっかで紹介された。
6. Event Target
ブラウザで使えた
EventTarget
が Node.js でも使えるようになった。Event
の発行・伝播はブラウザではEventTarget
、Node.js ではEventEmitter
を使っていた。https://nodejs.org/api/events.html#events_eventtarget_and_event_api
const target = new EventTarget(); target.addEventListener("foo", (event) => { console.log("foo is called"); }); const ev = new Event("foo"); target.dispatchEvent(ev);イベントが DOM ツリーを伝って伝播が行われないなど差はあるが、ユニバーサルなプログラムが書けるようになってきている。
7. N-API updated
Node.js のネイティブ拡張をするための Node API (通称 N-API) がアップデート。C 言語のインターフェース。まぁ使うことはあまりないのかなぁ。
https://nodejs.org/api/n-api.html#n_api_node_api
8. QUIC (実験的機能)
UDP ベースの通信プロトコル「QUIC」が使用できるようになった。
TLS 1.3 ベースでのセキュリティ、フロー制御、エラー訂正、接続、多重化とかとか。TLS 1.3 とは: https://kinsta.com/jp/blog/tls-1-3/
HTTP/3 サーバーを作ることができる。
https://blog.leko.jp/post/http-over-quic-on-nodejs15/むずすぎてよくわからんですね
かんそう
- せめて Node.js 14.x にはしとこう
- 投稿日:2021-02-25T00:54:53+09:00
Puppeteerでドラッグ&ドロップ
はじめに
最近、フロントテストにPuppeteerというライブラリを用いているのですが、ドラッグ・ドロップの動作においてかなり苦戦をしたので、これ以上犠牲者を増やさないために()、ここに解決策を共有したいと思います。
Puppeteerでドラッグ&ドロップができない!
「Puppeteer drag drop」というように検索をかけると、大体下記のようなコードが出てきます。
await page.mouse.move(x1, y1); await page.mouse.down(); await page.mouse.move(x2, y2); await page.mouse.up();座標(x1, y1)にカーソルを合わせ、掴む→(x2, y2)に移動し離すという意味のコードです。
しかしこれ...
動作しません!!!!!
正確に言うと、一瞬だけ対象物が動くんですけど、(x2, y2)まで移動してくれないんですよね...
調べてみると、このことに関して困っている方が、国内外問わず結構いました。await page.mouse.up();
この処理がうまく動作していないのが原因だと思っているが、どうなんだろう。
処理自体にエラーはなく、nodeも正常に終了しています。
しかし私の環境ではテスト終了後、エディタにカーソルをもっていくと常に何かをドラッグしている挙動が発生します。(Puppeteerで困っていることより引用)
解決策
解決策は、「mouse.moveにoptionsとしてstepsを適切に設定してあげる」です。
公式ドキュメントにはsteps <number> defaults to 1. Sends intermediate mousemove events.
と書かれています。
今回はこのstepsを20と設定します。
すなわち、先ほどのコードをawait page.mouse.move(x1, y1); await page.mouse.down(); await page.mouse.move(x2, y2,{steps: 20});//ここにoptionsとして{steps: 20}を追加した。 await page.mouse.up();のように書き換えると、期待通り動作します。
参考
・上記に引用として示したもの
・How to simulate Drag-Drop action in pupeteer