MeteorJSのMongoDBポートが分からずDBのGUIツールの設定に躓いた話
タイトル通りです。
普段の仕事ではMySQLクライアントのSequel Proを使っているのですが、同じ感じでRobo 3T(旧 Robomongo)を使いたいなと思ったわけです。
しかしこれまで自分で一から環境を作ったことなんてなく、会社の先輩にやってもらった or マニュアル通りの設定で業務をこなしていたので、いざとなるとRobo 3TをどうやってローカルのmongoDBに接続すれば良いのか分からず笑
調べたら別になんてことはなかったのですが、一応困る人もいるかもなのでこちらに書いときます。
検索するにあたって、「コレ知ってたらもっと早く答えにたどり着いたな」って情報から書いていきます。
- そもそもクライアントとかGUIとか何なの
- DBクライアント、DB GUIって例えばどんなのがあるの
- そもそもローカルのDBに接続するには何の情報があれば良いのか
- MeteorのMongoが使ってるポート番号は結局なんなの
- 参考
そもそもクライアントとかGUIとか何なの
少なくともデータベースの文脈でクライアント(Client)とかGUIとかの単語が出てきた場合、それは「データベースを人に分かりやすい形で表示してくれるツール」のことを表しています。
この記事にたどり着いた方なら、すくなくともコマンドライン・ターミナルでローカルのDBをいじったことはあるのでは無いでしょうか。
慣れてる人ならそうでもないのかもしれないですが、やっぱコマンドラインからいろいろやるのはめんどくさいです。
なので、 こんなかんじでデータベースを一覧表示してくれたり、画面から直接データベースの内容をいじれたりするとすごく幸せなわけです。
DBクライアント、DB GUIって例えばどんなのがあるの
RDBMS
MySQL
MySQL Workbench
phpMyAdmin
Sequel Pro
HeidiSQL
HeidiSQL - MySQL, MSSQL and PostgreSQL made easy
PostgreSQL
postico
Postico – a modern PostgreSQL client for the Mac
pgAdmin
psequel
PSequel, a PostgreSQL GUI Tool for macOS
SQLite
DB Browser for SQLite
Base 2
NoSQL
MongoDB
Robo 3T
Robo 3T - formerly Robomongo — native MongoDB management tool (Admin UI)
MongoDB Compass
mongo-express
Redis
FastoRedis
Keylord
Keylord - GUI manager for Redis, Memcached and LevelDB key-value databases
Medis
そもそもローカルのDBに接続するには何の情報があれば良いのか
これ知らないとググりようが無いんですよね。 今回の場合だと、僕はMeteorで使ってるMongoDBの「ポート番号」が分からなくて困っていたんですが、ぱっとその名前が出てこなかった。 これが敗因です。
さておき、基本的にローカルホストのDBに接続する際には、ホストとポート番号だけ分かっていれば良いはずです。
ホスト
データベースが格納されているコンピュータです。 今回のケースで言うと、開発中のアプリケーションのデータベースなので、ホストは自分のパソコンです。 なのでホストは「ローカルホスト」ですね。
ただ、実稼働しているアプリケーションに関しては殆どの場合AWSやさくらインターネットなどにデータベースを置いているのではないでしょうか。
その場合は、各データベースを置いてあるコンピュータのホスト名を入力する必要があります。
基本的にはホスト名はIPアドレス、もしくはドメイン名を指定します。
IPアドレスとは「183.79.135.206」のような文字列です。 ドメイン名とは「www.yahoo.co.jp」のような文字列です。
ドメイン名はIPアドレスを人間に分かりやすい言葉で表したものなので、見た目は違えど両者は同値です。
ためしに上記のIPアドレスとドメイン名をそれぞれブラウザのアドレスバーに入れてみて下さい。
ポート番号
ポート番号とは、ホストであるコンピュータが特定のデータをやり取りする際や特定のプログラムを動かす際に、その動作(プロセスといいます)に対して割り振っている番号のことです。
「このコンピュータのこの番号のプロセスを見ればデータベースの情報が取得できるよ」という感じです。
今回は、ローカルホストなのは分かっていても、MeteorのMongoDBがその中でどのポート番号を利用しているのかが分からなかったため困っていた、というわけです。
MeteorのMongoが使ってるポート番号は結局なんなの
やっと本題に入れました笑
目的のポート番号はおそらく人によって異なりますが、確認の方法はあります。
まずはターミナルで、開発中のmeteorプロジェクトのディレクトリに移動して、meteorを立ち上げましょう。
meteor run
そうしたら、新しいターミナルのウィンドウを立ち上げて、そちらでも同じプロジェクトのディレクトリに移動し、下記のコマンドを打ちましょう。
meteor mongo
すると、下記のような情報が表示されるかと思います。
MongoDB shell version: 3.2.15 connecting to: 127.0.0.1:3001/meteor meteor:PRIMARY>
この「connecting to」の部分が、ローカルのmongoDBが接続している先ですね!
127.0.0.1がローカルホストのIPアドレス、3001がローカルのポート番号です。
僕の場合は3001で動いていますが、これは人によって違うようなので、必ずご自分で確認して下さい。
なお、Meteorのドキュメントを読む限りでは、meteorそのものが使用しているポート番号 + 1がmongoDBに割り振られるようです。
ちなみに、meteor mongo
コマンドについてはMeteorのコマンドガイドに記載がありました。わかんねーよ笑
今回はこれまでです。
基礎知識無いとググるのも一苦労ですね。
参考
MeteorJSとReactを勉強してみる その11: テストコードを書いてみよう【公式翻訳】
本稿で公式のチュートリアルは実質最後になります。
ここまで見てくださった方、ありがとうございましたm(. .)m
その他必要な項目については引き続き翻訳していきますので、今しばらくお付き合いください。
テストコードを書いてみよう
ここまででいくつかの機能を作り上げましたので、コードの悪化を防ぎ、予期しない動作を防ぐためにもテストコードを追加しましょう。
これから、APIのうち「書き込み」の部分のメソッドを動かすテストを書き、正しく動作することを確かめましょう。
そのために、MochaというJavascriptのテストフレームワークのドライバを追加し、同時にテストの前提を構築するアサーションのライブラリも追加しましょう。
meteor add meteortesting:mocha meteor npm install --save-dev chai
コマンドを利用してテストドライバを指定することで、「テストモード」でアプリを動かすことが出来るようになりました。
そのためには、通常のアプリを停止させるか、--port XYZ
で別のポートを開く必要があります。
TEST_WATCH=1 meteor test --driver-package meteortesting:mocha
上記コマンドを叩くと、ターミナルウィンドウに「0 passing」のメッセージが表示されるはずです。
まずは単純なテストを追加してみましょう。 この時点ではこのテストコードは何も行いません。
imports/api/tasks.tests.js
/* eslint-env mocha */ import { Meteor } from 'meteor/meteor'; if (Meteor.isServer) { describe('Tasks', () => { describe('methods', () => { it('can delete owned task', () => { }); }); }); }
どんなテストにおいても、実行する前にデータベースが想定通りの状態になっていることが保証されている必要があります。 そのためには、Mochaの「beforeEach」構文を使うのが簡単です。
imports/api/tasks.tests.js
before
/* eslint-env mocha */ import { Meteor } from 'meteor/meteor'; if (Meteor.isServer) { describe('Tasks', () => { describe('methods', () => { it('can delete owned task', () => { }); }); }); }
after
/* eslint-env mocha */ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { Tasks } from './tasks.js'; if (Meteor.isServer) { describe('Tasks', () => { describe('methods', () => { const userId = Random.id(); let taskId; beforeEach(() => { Tasks.remove({}); taskId = Tasks.insert({ text: 'test task', createdAt: new Date(), owner: userId, username: 'tmeasday', }); }); it('can delete owned task', () => { }); }); }); }
これで、毎回のテスト実行時に異なるuserIdを生成して実行されるタスクが作れました。
さて、自動生成されるユーザーとしてtask.remove
を実行し、タスクが削除されることを保証するテストを書いていきましょう。
imports/api/tasks.tests.js before
/* eslint-env mocha */ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { Tasks } from './tasks.js'; if (Meteor.isServer) { describe('Tasks', () => { describe('methods', () => { const userId = Random.id(); let taskId; beforeEach(() => { Tasks.remove({}); taskId = Tasks.insert({ text: 'test task', createdAt: new Date(), owner: userId, username: 'tmeasday', }); }); it('can delete owned task', () => { }); }); }); }
after
/* eslint-env mocha */ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { Tasks } from './tasks.js'; if (Meteor.isServer) { describe('Tasks', () => { describe('methods', () => { const userId = Random.id(); let taskId; beforeEach(() => { Tasks.remove({}); taskId = Tasks.insert({ text: 'test task', createdAt: new Date(), owner: userId, username: 'tmeasday', }); }); it('can delete owned task', () => { // Find the internal implementation of the task method so we can // test it in isolation const deleteTask = Meteor.server.method_handlers['tasks.remove']; // Set up a fake method invocation that looks like what the method expects const invocation = { userId }; // Run the method with `this` set to the fake invocation deleteTask.apply(invocation, [taskId]); // Verify that the method does what we expected assert.equal(Tasks.find().count(), 0); }); }); }); }
There's a lot more you can do in a Meteor test! You can read more about it in the Meteor Guide article on testing.
Meteorのテストではもっとたくさん出来ることがあります! テストについて詳しくは、Meteor Guideのテストに関する記事でチェックしてみてくださいね。
前回の更新からかなり時間が経ってしまいました。
もし読んでくれている方がいましたら申し訳ありませんmm
チュートリアル自体はこれで終了ですが、meteorについては引き続き個人的に触っていくつもりなので、折に触れて記事を投稿していきたいと思います。
気が向いたらちらっとでも見てやって下さい。
お疲れ様でした!
MeteorJSとReactを勉強してみる その10: publishとsubscribeで表示するデータをフィルタリングしよう【公式翻訳】
MeteorJSを勉強してみるシリーズ第10回です。
チュートリアルとしての実装は今回で終了となります。
publishとsubscribeでデータをフィルタリングする
アプリのセンシティブなコードは全てメソッドに移したので、これからMeteorのセキュリティの後半を学習しましょう。
ここまでは、データベース全体がクライアントに表示される想定でコーディングしてきました。
これはつまり、Tasks.find()
するとコレクション内の全てのタスクが表示されてしまうということです。
しかし、もしユーザーがプライベートなデータを保存しておきたいとしたらこれはよくありません。 Meteorがどのデータをクライアントに送るのかをコントロールする方法が必要です。
前回のステップのinsecure
と同様に、Meteorアプリはデフォルトでautopublish
パッケージを含みます。
これはデータベースの情報を全てクライアントに自動的に同期させる機能です。
autopublish
を外すとどうなるか見てみましょう。
meteor remove autopublish
アプリが更新されると、タスクリストが空になっていると思います。 autopublishパッケージが無い状態だと、サーバーからクライアントになんのデータを送るのか、明確に示さないとなりません。
Meteorでそれを行う関数として、Meteor.publish
とMeteor.subscribe
が用意されています。
まずは、全てのタスクにpublicationを追加しましょう。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
次に、Appコンポーネントができた時点でこのpublicationをsubscribeしましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
これらのコードを追加すると、全タスクが再度表示されるでしょう。
Meteor.publish
をサーバーで呼ぶと、「tasks」という名前のpublicationが登録されます。
Meteor.subscribe
をクライアントでpublicationの名前付きで呼ぶと、クライアントはそのpublicationから全てのデータをsubscribeします。
つまり今回のケースでは、データベース内にある全てのtasksデータです。
publish/subsribeモデルの真価を確認するために、ユーザーがタスクを「プライベート」化し、他のユーザーには見えなくする機能を開発してみましょう。
タスクをプライベート化するボタンを追加する
tasksに新しい「private」というプロパティと、ユーザーがタスクをプライベート化できるボタンを作成しましょう。 このボタンはそのタスクのownerにしか表示されるべきではありません。 さらに、タスクがpublicかprivateか、どちらの状態なのかを示すラベルも追加しましょう。
まず、タスクのprivateステータスをセットできるメソッドを追加します。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
さて、表示されているタスクにprivateボタンを表示するかどうかを判定するために、タスクには新しいプロパティを渡しておかなくてはいけません。 ボタンは、現在ログイン中のユーザーがそのタスクのownerである場合のみ、表示されるべきです。
imports/ui/App.js
before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( <Task key={task._id} task={task} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; import AccountsUIWrapper from './AccountsUIWrapper.js'; // App Component - represnts the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter((task) => !task.checked); } return filteredTasks.map((task) => ( const currentUserId = this.props.currentUser && this.props.currentUser._id; const showPrivateButton = task.owner === currentUserId; <Task key={task._id} task={task} showPrivateButton={showPrivateButton} /> )); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user() }; })(App);
ボタンを追加し、この新しく追加されたプロパティを使って表示可否を判断するようにしましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
ボタン押下時に呼ばれるイベントハンドラも定義しておきましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
最後に、Taskコンポーネント内の
classnames
のnpmパッケージを使います。
meteor npm install --save classnames
タスクのレンダリング中にクラスが選択されるようにパッケージを使ってみましょう。
imports/ui/Task.js
before
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
after
import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import classnames from 'classnames'; import { Tasks } from '../api/tasks.js'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); }; togglePrivate() { Method.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); }; deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); }; render() { // give tasks a different className when they are checked off, // so that we can style them nicely in CSS. const taskClassName = classnames({ checked: this.props.task.checked, private: this.props.task.private }); return ( <li className={ taskClassName }> <button className="delete" onClick={ this.deleteThisTask.bind(this) }> × </button> <input type="checkbox" readOnly checked={ !!this.props.task.checked } onClick={ this.toggleChecked.bind(this) } /> { this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> { this.props.task.private ? 'Private' : 'Public' } </button> ) : '' } <span className="text"> <strong>{ this.props.task.username }</strong>: { this.props.task.text } </span> </li> ); } }
privacyステータスに応じてタスクを選択して表示する
タスクをプライベートにすることができるようになったので、publication関数で各ユーザーが閲覧権限を持つタスクだけを出力するように変更しましょう。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
この機能が動いていることを確かめるためには、ブラウザーのプライベートブラウジングモード(Chromeならシークレットブラウザー)を使って別ユーザーとしてログインしてみればよいでしょう。
2つのウィンドウを隣り合わせに配置して、他のユーザーが閲覧できないようにタスクをプライベート化してみて下さい。 もう一度パブリックにすると、再表示されるはずです!
セキュリティ機能を追加する
プライベートタスク機能を完成させるため、deleteTask
とsetChecked
にチェック機能をつけましょう。
これでタスクのownerのみがプライベートタスクを削除したり、チェックを外したり出来るようになります。
imports/api/tasks.js
before
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
after
import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId } ] }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the use is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(); owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.userId) { // If the task is private, make sure only the owner can delete it throw new Meteor.Error('not-authorized'); } Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.userId) { // If the task is private, make sure only the owner can check it off throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }) } });
ちなみに、このままではパブリックなタスクであればownerが誰であろうと、誰でもタスクを削除できてしまいます。 ownerのみがタスクを削除出来るようにするためには簡単な修正を加えれば良いだけですので、試してみても良いでしょう。
これでプライベートタスク機能は完成です! 攻撃者が誰かのプライベートタスクを覗いたり修正したりすることを防げるようになりました。
第10回の内容は以上です。
Meteorのチュートリアルも残すところあと1つとなりました。
見てくださっている方がもしいましたら、是非最後までお付き合いくださいm(. .)m