プログラミングとSEOと暇つぶし

駆け出しエンジニアdallPのブログです。元SEOコンサルタントです。プログラミング、SEO、アフィリエイト、お金などについて役に立つかもしれない情報をやりたいように書きます。

MeteorJSのMongoDBポートが分からずDBのGUIツールの設定に躓いた話

タイトル通りです。

普段の仕事ではMySQLクライアントのSequel Proを使っているのですが、同じ感じでRobo 3T(旧 Robomongo)を使いたいなと思ったわけです。

しかしこれまで自分で一から環境を作ったことなんてなく、会社の先輩にやってもらった or マニュアル通りの設定で業務をこなしていたので、いざとなるとRobo 3TをどうやってローカルのmongoDBに接続すれば良いのか分からず笑

調べたら別になんてことはなかったのですが、一応困る人もいるかもなのでこちらに書いときます。

検索するにあたって、「コレ知ってたらもっと早く答えにたどり着いたな」って情報から書いていきます。

そもそもクライアントとかGUIとか何なの

少なくともデータベースの文脈でクライアント(Client)とかGUIとかの単語が出てきた場合、それは「データベースを人に分かりやすい形で表示してくれるツール」のことを表しています。

この記事にたどり着いた方なら、すくなくともコマンドライン・ターミナルでローカルのDBをいじったことはあるのでは無いでしょうか。

慣れてる人ならそうでもないのかもしれないですが、やっぱコマンドラインからいろいろやるのはめんどくさいです。

なので、 f:id:dallP:20180210224137p:plain こんなかんじでデータベースを一覧表示してくれたり、画面から直接データベースの内容をいじれたりするとすごく幸せなわけです。

DBクライアント、DB GUIって例えばどんなのがあるの

メジャーなDBMSとそのGUIツールだけ書き出しときます。

RDBMS

MySQL

MySQL Workbench

MySQL :: MySQL Workbench

phpMyAdmin

phpMyAdmin

Sequel Pro

Sequel Pro

HeidiSQL

HeidiSQL - MySQL, MSSQL and PostgreSQL made easy

PostgreSQL

postico

Postico – a modern PostgreSQL client for the Mac

pgAdmin

pgAdmin - PostgreSQL Tools

psequel

PSequel, a PostgreSQL GUI Tool for macOS

SQLite

DB Browser for SQLite

DB Browser for SQLite

Base 2

Menial » Base 2

NoSQL

MongoDB

Robo 3T

Robo 3T - formerly Robomongo — native MongoDB management tool (Admin UI)

MongoDB Compass

MongoDB Compass | MongoDB

mongo-express

GitHub - mongo-express/mongo-express: Web-based MongoDB admin interface, written with Node.js and express

Redis

FastoRedis

FastoRedis - cross-platform client for Redis, supported main Redis database features like: modules, cluster, sentinel, ssh tunneling.

Keylord

Keylord - GUI manager for Redis, Memcached and LevelDB key-value databases

Medis

Medis - GUI Manager for Redis

そもそもローカルの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のコマンドガイドに記載がありました。わかんねーよ笑


今回はこれまでです。

基礎知識無いとググるのも一苦労ですね。

参考

qiita.com

kumaweb-d.com

blog.44uk.net

qiita.com

akiyoko.hatenablog.jp

eng-entrance.com

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.publishMeteor.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) }>
          &times;
        </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) }>
          &times;
        </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) }>
          &times;
        </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) }>
          &times;
        </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コンポーネント内の

  • 要素のクラスを、タスクのprivacyステータスを反映するように更新しましょう。 このために、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) }>
              &times;
            </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) }>
              &times;
            </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つのウィンドウを隣り合わせに配置して、他のユーザーが閲覧できないようにタスクをプライベート化してみて下さい。 もう一度パブリックにすると、再表示されるはずです!

    セキュリティ機能を追加する

    プライベートタスク機能を完成させるため、deleteTasksetCheckedにチェック機能をつけましょう。 これでタスクの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