yucatio@システムエンジニア

趣味で作ったものいろいろ

リファクタリング: 作成日時と更新日付の追加 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Firebase Realtime Databaseでは、データの作成日時・更新日時は自動で付与されないので、クライアント側(javascript)で付与します。これに伴い、Cloud FunctionsもDatabaseの更新日時を使用するように変更します。データの作成日時・更新日時は、サンプルのアプリでは特に必要がないですが、アプリをリリースした後、問い合わせがあった場合の調査などに重要になります。

actionの更新

タスク作成時に、作成時間_createdAt_updatedAtを付与し、更新時に_updatedAtを更新します。 時刻関連の処理には、使いやすさのため、momentモジュールを使用します。

src/actions/todoActions.js

import moment from 'moment'  // 追加
// 中略

export const addTodo = (uid, text) => {
  return (dispatch, getState, {getFirebase}) => {
    // 中略
    const createdAt = moment().valueOf();  // 追加
    firebase.push(`todos/${uid}`,{
      completed: false,
      text,
      _createdAt : createdAt,  // 追加
      _updatedAt : createdAt  // 追加
    })
    // 中略
  }
}

export const toggleTodo = (uid, id) => {
  return (dispatch, getState, {getFirebase}) => {
    // 中略
    const updatedAt = moment().valueOf();  // 追加
    firebase.update(`todos/${uid}/${id}`, {
      completed: ! todo.completed,
      _updatedAt : updatedAt  // 追加
    })
    // 中略
  }
}

database.rules.jsonの更新

_createdAt_updatedAtを登録できるようにデータベースルールファイルを変更します。

database.rules.json

{
  "rules": {
    "todos": {
      "$uid": {
        ".read": true,
        ".write": "$uid === auth.uid",
        "$todoId": {
          ".validate": "newData.hasChildren(['text', 'completed', '_createdAt', '_updatedAt'])",  // 変更
          "text": {
            ".validate": "newData.isString()"
          },
          "completed": {
            ".validate": "newData.isBoolean()"
           },
          "_createdAt": {  // 追加
            ".validate": "newData.isNumber()"
          },
          "_updatedAt": {  // 追加
            ".validate": "newData.isNumber()"
          },
          "$other": { ".validate": false }
        }
      }
    },
    // 中略
  }
}

変更を反映します。

firebase deploy --only database

".validate": "newData.hasChildren(['text', 'completed', '_createdAt', '_updatedAt’])”があるせいで、既存のタスクが更新できなくなってしまいました。今回は既存のタスクは削除します。次回からは初めから_createdAt_updatedAtは付与しましょう。

Cloud Functionsの修正

タスクの作成トリガを/todos/{uid}/{todoId}/_createdAtの作成、更新トリガを/todos/{uid}/{todoId}/_updatedAtの更新とするように変更します。 また、recentUpdatedTodos_updatedAtは、todos_updatedAtから取得するように変更します。

fuctions/index.js

const addRecentUpdate = (uid, todoId, todo, eventType) => {  // #1
  return admin.database().ref('/users/' + uid + '/displayName').once('value').then((snapshot) => {
    const displayName = snapshot.val();
    return (admin.database().ref('/recentUpdatedTodos/' + todoId).set({
      uid,
      displayName,
      text: todo.text,  // #2
      eventType,
      _updatedAt: todo._updatedAt  // #3
    }));
  });
}

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/_createdAt')  // #4
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;
    const todoId = context.params.todoId;

    return snapshot.ref.parent.once('value').then((snapshot) => {  // #5
      const todo = snapshot.val();
      return addRecentUpdate(uid, todoId, todo, 'CREATE');
    });
})

exports.addRecentUpdateOnUpdate = functions.database.ref('/todos/{uid}/{todoId}/_updatedAt')  // #6
  .onUpdate((change, context) => {
    const todoId = context.params.todoId;
    const uid = context.params.uid;

    return change.after.ref.parent.once('value').then((snapshot) => {  // #7
      const todo = snapshot.val();
      return addRecentUpdate(uid, todoId, todo, 'UPDATE');
    });
})
  • addRecentUpdateの引数のうち、texttodoに変更します(#1)。
  • texttodo.textで取得します(#2)。
  • recentUpdatedTodos_updatedAtを、todo_updatedAtから取得します(#3)。
  • データパスを、/todos/{uid}/{todoId}/textから/todos/{uid}/{todoId}/_createdAtに変更します(#4)。
  • 親ノードを取得する処理を追加します(#5)。
  • 関数名を、addRecentUpdateOnUpdateCompletedからaddRecentUpdateOnUpdateに変更し、データパスを、/todos/{uid}/{todoId}/completedから/todos/{uid}/{todoId}/_updatedAtに変更します(#6)。
  • textノードを取得する処理から、親ノードを取得する処理に変更します(#7)。

デプロイします。

$ firebase deploy --only functions

=== Deploying to 'todo-sample-xxxxx’…

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint

> functions@ lint /Users/yuka/react/todo-sample/functions
> eslint .

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (56.34 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function addRecentUpdateOnUpdate(us-central1)...
i  functions: updating Node.js 6 function addRecentUpdateOnCreate(us-central1)...
i  functions: updating Node.js 6 function limitRecentUpdatedTodos(us-central1)...

今回、functionの名前を変更したので、途中でaddRecentUpdateOnUpdateCompletedを削除するか聞かれます。”y”を選択します。

The following functions are found in your project but do not exist in your local source code:
    addRecentUpdateOnUpdateCompleted(us-central1)

If you are renaming a function or changing its region, it is recommended that you create the new function first before deleting the old one to prevent event loss. For more info, visit https://firebase.google.com/docs/functions/manage-functions#modify
? Would you like to proceed with deletion? Selecting no will continue the rest o
f the deployments. Yes
i  functions: deleting function addRecentUpdateOnUpdateCompleted(us-central1)...
✔  functions[addRecentUpdateOnUpdateCompleted(us-central1)]: Successful delete operation. 
✔  functions[addRecentUpdateOnCreate(us-central1)]: Successful update operation. 
✔  functions[limitRecentUpdatedTodos(us-central1)]: Successful update operation. 
✔  functions[addRecentUpdateOnUpdate(us-central1)]: Successful create operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/todo-sample-xxxxx/overview

functionのデプロイが完了しました。

動作確認

動作確認をします。

タスクを新規登録します。

f:id:yucatio:20181101144012p:plain:w200

データベースを確認します。

f:id:yucatio:20181101145256p:plain

_createdAt_updatedAtが追加されています。

recentUpdatedTodos_updatedAttodosのものと同じ時刻です。

タスクを更新します。

f:id:yucatio:20181101144642p:plain:w200

データベースを確認します。

f:id:yucatio:20181101144900p:plain

_updatedAtが更新されました。recentUpdatedTodos_updatedAttodosの時刻で更新されています。

以上で作成日時と更新日付の追加の処理の完了です。Firebase Realtime Databaseを使用する際は、最初から全てのデータに作成日時と更新日時を付与することを強くおすすめします。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

リファクタリング : componentsとcontainersの階層化 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

componentのファイルが多くなり、管理しづらくなってきたので、画面パーツごとにディレクトリを分けます。

現在のディレクトリ構成と変更後のディレクトリ構成です。

f:id:yucatio:20181029142629p:plain

階層の移動と構成

以下に、src/components以下のファイルの移動先を示します。

ファイル名 移動先
App.js App.js (変更なし)
NoMatch.js NoMatch.js (変更なし)
Dashboard.js dashboard/index.js
RecentUpdatedTodos.js dashboard/recentUpdatedTodos/index.js
UserUpdatedTodo.js dashboard/recentUpdatedTodos/UserUpdatedTodo.js
Login.js login/index.js
Navbar.js navbar/index.js
Footer.js todos/Footer.js
Link.js todos/Link.js
NoticeForTodo.js todos/Notice.js
Todo.js todos/Todo.js
TodoList.js todos/TodoList.js
TodoComponent.js todos/index.js

同様にsrc/containersも移動します。

ファイル名 移動先
AddTodo.js todos/AddTodo.js
FilterLink.js todos/FilterLink.js
VisibleTodoList.js todos/VisibleTodoList.js

ソースコードの修正

階層を変更したので、ソースコード中のパスも書き換えます。

src/components/App.js

import Login from './login/'
import Navbar from './navbar/'
import Dashboard from './dashboard/'
import TodoComponent from './todos/'

src/components/dashboard/index.js

import RecentUpdatedTodos from './recentUpdatedTodos/'

src/components/login/index.js

import { loginWithGoogle, logout } from '../../actions/authActions'

src/components/todos/Footer.js

import FilterLink from '../../containers/todos/FilterLink'
import { VisibilityFilters } from '../../actions/visibilityFilterActions'

src/components/todos/index.js

// 前略
import { locationChangeOnTodos } from '../../actions/todoActions'
import Footer from './Footer'
import Notice from './Notice'
import AddTodo from '../../containers/todos/AddTodo'
import VisibleTodoList from '../../containers/todos/VisibleTodoList'

class TodoComponent extends React.Component {
  // 中略
  render() {
    // 中略
    return (
      <div>
        {isOwnTodos && <AddTodo uid={uid} />}
        <Notice />  {/* 変更 */}
        <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
        <Footer />
      </div>
    )
  }
}

src/containers/todos/AddTodo.js

import { addTodo } from '../../actions/todoActions'

src/containers/todos/FilterLink.js

import { setVisibilityFilter } from '../../actions/visibilityFilterActions'
import Link from '../../components/todos/Link'

src/containers/todos/VisibleTodoList.js

import { toggleTodo } from '../../actions/todoActions'
import TodoList from '../../components/todos/TodoList'

以上でcomponentsとcontainersの階層化ができました。1つのディレクトリのファイル数が10を超えたらディレクトリ分割を検討するとよいです。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

"最近の更新"を表示する (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

/recentUpdatedTodosDashboardに表示します。Dashboardの下にRecentUpdatedTodosコンポーネントを新規作成します。

moment.jsモジュールのインストール

各タスクの更新時間を表示は、「○分前」「○時間前」など、現在からの経過時間を概算で表示します。これにはmoment.jsを利用するので、インストールします。

yarn add moment

"最近の更新”の表示

Dashboardの下にRecentUpdatedTodosコンポーネントを新規作成し、さらにその下にUserUpdatedTodoコンポーネントを作成します。

src/components/Dashboard.js

// 前略
import RecentUpdatedTodos from './RecentUpdatedTodos'  // 追加
 
const Dashboard = () => (
  <div>
    <RecentUpdatedTodos />
  </div>
)

// 後略

src/components/RecentUpdatedTodos.js (新規作成) (実行前にコメントは削除してください)

import React from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { firebaseConnect, isEmpty, isLoaded } from 'react-redux-firebase'
import PropTypes from 'prop-types'
import UserUpdatedTodos from './UserUpdatedTodo'

const RecentUpdatedList = (todos) => {
  if (!isLoaded(todos)) {
    return <div>読み込み中…</div>
  }
  if (isEmpty(todos)) {
    return <div>データがありません。</div>
  }
  return (
    <ul>
      {todos.map(({key, value:todo}) =>  // #4
        <UserUpdatedTodos  key={key} {...todo}/>
      )}
    </ul>
  )
}

let RecentUpdatedTodos = ({todos}) => {
  return (
    <div>
      <h1>最近の更新</h1>
      {RecentUpdatedList(todos)}
    </div>
  )
}
RecentUpdatedTodos.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      key: PropTypes.string.isRequired,
      value: PropTypes.object.isRequired,
    })
  ),
}

const firebaseQueries = ({uid}) => (
  [
    {path: `recentUpdatedTodos`, type: 'once', queryParams: [ 'orderByChild=_updatedAt', 'limitToLast=10' ]}  // #1
  ]
)

const mapStateToProps = ({firebase: {ordered : {recentUpdatedTodos}}}) => {  // #2
  return {
    todos: recentUpdatedTodos && recentUpdatedTodos.reverse()  // #3
  }
}

RecentUpdatedTodos = compose(
  firebaseConnect(firebaseQueries),
  connect(
   mapStateToProps
))(RecentUpdatedTodos)

export default RecentUpdatedTodos;
  • 並び替えられたデータはfirebase.orderedで取得できます(#2)。
  • /recentUpdatedTodosonceで取得します(#1)。_updatedAtの昇順で並び替え、最後の10件を取得します。
  • Viewで降順で表示するため、データが存在する場合はreverseして逆順に並び替えます(#3)。
  • 並び替えられたデータは、{key: recentUpdatedTodosのキー, value: recentUpdatedTodosの値}というオブジェクトの配列になっているので、todos.mapで各ノードのキーと値が入ったオブジェクトを取得します(#4)。

src/components/UserUpdatedTodo.js (新規作成)

import React from 'react'
import { Link } from 'react-router-dom'
import moment from 'moment'
import 'moment/locale/ja'  // #1
import PropTypes from 'prop-types'

const UserUpdatedTodo = ({text, eventType, uid, displayName, _updatedAt}) => (
  <li>
    <Link to={`/users/${uid}/todos`}>{displayName}</Link>さんが {text} を{ eventType === 'CREATE' ? '作成' : '更新'}  {/* #2 */}
    しました。 ({ moment(_updatedAt).fromNow()})  {/* #3 */}
  </li>
)

UserUpdatedTodo.propTypes = {
  text: PropTypes.string.isRequired,
  eventType: PropTypes.string.isRequired,
  uid: PropTypes.string.isRequired,
  displayName: PropTypes.string.isRequired,
  _updatedAt: PropTypes.number.isRequired
}

export default UserUpdatedTodo;
  • import 'moment/locale/ja’でmoment.jsの日本語化をします(#1)。
  • displayNameに、そのユーザのタスク一覧へのリンクを貼ります(#2)。
  • moment#fromNow()で経過時間を概算を取得します(#3)。

preserveOnLogoutへの設定追加

react-redux-firebaseのpreserveOnLogoutの設定も忘れずに行います。

src/index.js

const createStoreWithFirebase = compose(
  applyMiddleware(thunk.withExtraArgument({getFirebase})),
  reactReduxFirebase(firebase, {userProfile: 'users', preserveOnLogout: ['todos', 'users', 'recentUpdatedTodos']})  // 変更
)(createStore);

動作確認

http://localhost:3000/ を表示します。

f:id:yucatio:20181028225616p:plain

最近作成更新したタスクが、新しいものが上になるように表示されました。

ユーザ名をクリックします。

f:id:yucatio:20181029134921p:plain

ユーザのタスク一覧に遷移します。

ログアウトします。

f:id:yucatio:20181029140124p:plain

ログアウトしても最新の更新一覧が表示されます。

おめでとうございます!これでSTEP 3の全ての機能が実装できました!

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Cloud Functions for Firebaseを使用して、"最近の更新"の件数を制限する (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

前回までで、/recentUpdatedTodosへのデータの追加を行うことはできましたが、これではどんどんデータが溜まって行ってしまうので、個数を制限します。制限を超えたら古いデータを削除します。

firebaseのサンプル(limit-children)があるので、このコードをもとにします。

実装方針

/recentUpdatedTodosに新しくデータが登録された時に、古いデータを削除します。何件のデータを残すかは、MAX_RECENT_UPDATED_TODOSで制御します。 動作確認を簡単にするため、MAX_RECENT_UPDATED_TODOSは一旦”3”にします。

“/recentUpdatedTodos”の子ノードの数の取得

はじめに、/recentUpdatedTodosの子ノードの数の取得をするコードを書きます。削除処理が必要なのは子の数がMAX_RECENT_UPDATED_TODOSより大きい場合なので、それ以下の場合は早期にreturnします。

functions/index.js

const MAX_RECENT_UPDATED_TODOS = 3;

exports.limitRecentUpdatedTodos = functions.database.ref('/recentUpdatedTodos/{todoId}/_updatedAt')  // #1
  .onCreate((snapshot, context) => {
    const recentUpdatedTodosRef = snapshot.ref.parent.parent;  // #2
    return recentUpdatedTodosRef.once('value').then((todosSnapshot) => {  // #3

      console.log('numChildren: ' + todosSnapshot.numChildren());  // #4

      if (todosSnapshot.numChildren() <= MAX_RECENT_UPDATED_TODOS) {
        console.log('Less than or equals to ' + MAX_RECENT_UPDATED_TODOS);
        return null;
      }

      console.log('More than ' + MAX_RECENT_UPDATED_TODOS);
      return null;
    });
})
  • データ追加のトリガを/recentUpdatedTodos/{todoId}/_updatedAtに設定します(#1)。
  • /recentUpdatedTodosは、_updatedAtから見て親の親なので、/recentUpdatedTodosのリファレンスはsnapshot.ref.parent.parentで取得することができます(#2)。
  • /recentUpdatedTodosのデータを取得します(#3)。
  • 子ノードの個数はnumChildren()で取得します(#4)。

子ノード数取得の動作確認

デプロイして動作確認します。

$ cd todo-sample
$ firebase deploy --only functions

動作確認のため、一旦DBの/recentUpdatedTodosの子ノードを削除します。

f:id:yucatio:20181026225707p:plain

タスクを作成・更新して、/recentUpdatedTodosにデータを追加します。

f:id:yucatio:20181027214107p:plain

Cloud Functionsのログを確認します。

f:id:yucatio:20181027215027p:plain

/recentUpdatedTodos```の子ノードの数と、MAX_RECENT_UPDATED_TODOS``より大きいか小さいかが表示されました。

制限件数を超えたら、古いデータを削除する

制限件数を超えたら、古いデータから削除するように修正します。データを_updatedAtで並び替え、最初の方のデータから削除します。

functions/index.js

const MAX_RECENT_UPDATED_TODOS = 3;

exports.limitRecentUpdatedTodos = functions.database.ref('/recentUpdatedTodos/{todoId}/_updatedAt')
  .onCreate((snapshot, context) => {
    const recentUpdatedTodosRef = snapshot.ref.parent.parent;
    return recentUpdatedTodosRef.orderByChild('_updatedAt').once('value').then((todosSnapshot) => { // #1

      if (todosSnapshot.numChildren() <= MAX_RECENT_UPDATED_TODOS) {
        return null;
      }

      let childCount = 0;
      const updates = {};
      todosSnapshot.forEach((child) => {
        // remove old updates
        if (++childCount <= todosSnapshot.numChildren() - MAX_RECENT_UPDATED_TODOS) {  // #2
          updates[child.key] = null;  // #3
        }
      });
      return recentUpdatedTodosRef.update(updates);  // #4
    });
})
  • 最近の更新のデータを最終更新時間順に並び変えるため、orderByChild('_updatedAt’を追加します(#1)。データは昇順に並びます。
  • 子ノードの個数 - MAX_RECENT_UPDATED_TODOSぶん、/recentUpdatedTodosの子ノードを削除します(#2)。
  • 削除したいデータの値をnullにします(#3)。
  • 更新内容を、updateの引数として渡します(#4)。

件数の制限の動作確認

デプロイして動作確認します。

$ cd todo-sample
$ firebase deploy --only functions

実行前の/recentUpdatedTodosです。

f:id:yucatio:20181027220309p:plain

_updateAtの古い順に①②③④と番号を振りました。

タスクを1件追加します。

f:id:yucatio:20181027220709p:plain:w200

追加後の/recentUpdatedTodosです。

f:id:yucatio:20181027221319p:plain

①と②のデータが消えて、件数が3件に制限されました。件数制限のスクリプトがうまく動いています。

保存件数の変更

今回は最新の10件をビューで表示するので、MAX_RECENT_UPDATED_TODOSの値を”10”に変更します。

const MAX_RECENT_UPDATED_TODOS = 10;

デプロイします。

$ cd todo-sample
$ firebase deploy --only functions

以上で、Cloud Functions for Firebaseを使用して、"最近の更新"の件数を制限することができました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Cloud Functions for Firebaseを使用して、タスク更新時に"最近の更新"へデータを追加する (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

引き続き、addRecentUpdateOnUpdateCompletedを実装します。タスクが更新された時に、/recentUpdatedTodosにデータを追加します。

タスク更新時に"最近の更新"へデータを追加する

addRecentUpdateOnUpdateCompletedの処理は、addRecentUpdateOnCreateとほぼ同様です。textの取得処理を追加します。

functions/index.js

exports.addRecentUpdateOnUpdateCompleted = functions.database.ref('/todos/{uid}/{todoId}/completed')
  .onUpdate((change, context) => {
    const todoId = context.params.todoId;
    const uid = context.params.uid;
    let text;

    return change.after.ref.parent.child('text').once('value').then((snapshot) => {  // #1
      text = snapshot.val();
      return admin.database().ref('/users/' + uid + '/displayName').once('value');
    }).then((snapshot) => {
      const displayName = snapshot.val();
      return (admin.database().ref('/recentUpdatedTodos/' + todoId).set({
        uid,
        displayName,
        text,
        eventType: 'UPDATE',
        _updatedAt: admin.database.ServerValue.TIMESTAMP
      }));
    });
})
  • change.after.refで更新後の値へのreferenceを取得します。parentで親ノードを取得し、さらにchild('text')でtextノードを取得します(#1)。

動作確認

デプロイして動作確認します。

$ cd todo-sample
$ firebase deploy --only functions

タスクの完了フラグを切り替えます。

f:id:yucatio:20181024222007p:plain:w200

データベースのデータを確認します。

f:id:yucatio:20181024222309p:plain

/recentUpdatedTodosにtodoIdをキーとして、uidとdisplayName、text、eventType('UPDATE')、更新時間(_updatedAt)が追加されました。

リファクタリング

/recentUpdatedTodosへの書き込みは、タスクの作成・更新で共通なので、共通部分を関数として切り出します。

functions/index.js

const addRecentUpdate = (uid, todoId, text, eventType) => {
  return admin.database().ref('/users/' + uid + '/displayName').once('value').then((snapshot) => {
    const displayName = snapshot.val();
    return (admin.database().ref('/recentUpdatedTodos/' + todoId).set({
      uid,
      displayName,
      text,
      eventType,
      _updatedAt: admin.database.ServerValue.TIMESTAMP
    }));
  });
}

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/text')
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;
    const todoId = context.params.todoId;
    const text =  snapshot.val();

    return addRecentUpdate(uid, todoId, text, 'CREATE');
})

exports.addRecentUpdateOnUpdateCompleted = functions.database.ref('/todos/{uid}/{todoId}/completed')
  .onUpdate((change, context) => {
    const todoId = context.params.todoId;
    const uid = context.params.uid;

    return change.after.ref.parent.child('text').once('value').then((snapshot) => {
      const text = snapshot.val();
      return addRecentUpdate(uid, todoId, text, 'UPDATE');
    });
})

動作確認

デプロイして動作確認します。

$ cd todo-sample
$ firebase deploy --only functions

データを作成・更新してデータを確認します。

f:id:yucatio:20181024223030p:plain

タスクの作成・更新ともに/recentUpdatedTodosへ正常にデータが作成されました。

以上でタスク作成・更新時に"最近の更新"へデータを追加する処理が完成しました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Cloud Functions for Firebaseを使用して、タスク作成時に"最近の更新"へデータを追加する (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

addRecentUpdateOnCreateを実装します。タスクが作成された時に、/recentUpdatedTodosにデータを追加します。

表示したいデータ

“最近の更新”は、以下のようになります。ユーザ名をクリックすると、ユーザのタスク一覧へ遷移します。

f:id:yucatio:20181011124946p:plain

これを表示するのに以下のデータが必要になります。

  • uid (ユーザID)
  • displayName (ユーザ名)
  • todo.text (タスク名)
  • 作成か更新か
  • 更新時間

/recentUpdatedTodosの下にtodoIdをキーとしてデータを作成するので、

  • todoId

のデータも必要です。/recentUpdatedTodosの下にtodoIdをキーとしてデータを作成することによって、1つのタスクが短時間に何度も更新されても、画面への表示は最後の1回だけになります。また、べき等性も担保されます。

必要なデータについては、

  • uidとtodoIdについてはパスパラメータの値を使用します。
  • displayNameは/users/{uid}/displayNameから取得します。
  • todo.textは作成についてはそのまま値を取得することができますが、更新の場合は一旦親ノードを取得してからtextの値を取得する必要があります。
  • 作成か更新かについては、eventTypeというプロパティを用意し、CREATEまたはUPDATEをセットするようにします。
  • 更新時間はadmin.database.ServerValue.TIMESTAMPで取得します。

タスク作成時に"最近の更新"へデータを追加する(displayName以外)

最初に、displayName以外の、

  • uid
  • todo.text
  • 作成か更新か
  • 更新時間

をtodoIdをキーとして/recentUpdatedTodosに登録します。

functions/index.js

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/text')
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;
    const todoId = context.params.todoId;
    const text = snapshot.val();  // #1

    return admin.database().ref('/recentUpdatedTodos/' + todoId).set({  // #2
      uid,
      text,
      eventType: 'CREATE',
      _updatedAt: admin.database.ServerValue.TIMESTAMP
    });
})
  • 指定したpathの値は、snapshot.val()で取得できます(#1)。
  • /recentUpdatedTodos/{todoId}にデータをセットします(#2)。

動作確認(displayName以外)

デプロイします。

$ cd todo-sample
$ firebase deploy --only functions

タスクを追加します。

f:id:yucatio:20181024112600p:plain:w200

データベースのデータを確認します。

f:id:yucatio:20181024113107p:plain

/recentUpdatedTodosにtodoIdをキーとして、uidとtext、eventType('CREATE')、更新時間(_updatedAt)が追加されました。

データが追加されない場合は、 Firebase console > Functions > ログで何かエラーが出ていないか確認してください。

タスク作成時に"最近の更新"へデータを追加する(displayNameを含める)

displayNameを追加します。displayNameは、/users/{uid}/displayNameで取得します。取得処理は非同期になります。

functions/index.js

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/text')
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;
    const todoId = context.params.todoId;
    const text = snapshot.val();

    return admin.database().ref('/users/' + uid + '/displayName').once('value').then((snapshot) => {  // #1
      const displayName = snapshot.val();
      return admin.database().ref('/recentUpdatedTodos/' + todoId).set({
        uid,
        displayName,
        text,
        eventType: 'CREATE',
        _updatedAt: admin.database.ServerValue.TIMESTAMP
      });
    });
})
  • /users/{uid}/displayNameonce()で取得します(#1)。

動作確認(displayNameを含める)

デプロイして動作確認します。

$ cd todo-sample
$ firebase deploy --only functions

タスクを追加します。

f:id:yucatio:20181024115013p:plain:w200

データベースのデータを確認します。

f:id:yucatio:20181024115550p:plain /recentUpdatedTodosにtodoIdをキーとして、uidとdisplayName、text、eventType('CREATE')、更新時間(_updatedAt)が追加されました。

以上でタスク作成時に"最近の更新"へデータを追加することができました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Cloud Functions for Firebase用の最初のコードの記述とデプロイ (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Cloud Functions for Firebaseで最初に小さいコードを書いて動作確認をします。 手始めに、DBの/todos/{uid}の下にタスクが追加された時と、タスクが更新された時にコンソールへメッセージを表示します。

トリガの動作確認用コードの作成

/todos/{uid}/の下にタスクが追加された時と、タスクが更新された時にコンソールにメッセージを表示します。

functions/index.js

const functions = require('firebase-functions');  // #1
const admin = require('firebase-admin');  // #2
admin.initializeApp();  // #3

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/text')  // #4
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;  // #5
    const todoId = context.params.todoId;  // #6

    console.log('addRecentUpdateOnCreate called. uid=' + uid + ', todoId=' + todoId);  // #7

    return null;  // #8
})

exports.addRecentUpdateOnUpdateCompleted = functions.database.ref('/todos/{uid}/{todoId}/completed')  // #9
  .onUpdate((change, context) => {
    const todoId = context.params.todoId;
    const uid = context.params.uid;

    console.log('addRecentUpdateOnUpdateCompleted called. uid=' + uid + ', todoId=' + todoId);

    return null;
})
  • firebase-functionsを使用します(#1)。このモジュールはCloud Functionを使用するのに必要です。
  • firebase-adminを使用します(#2)。このモジュールはFirebase Realtime Databaseを利用するのに必要です。
  • admin.initializeAppでadminを初期化します(#3)。
  • タスクが追加された時に実行される関数を定義します(#4)。functions.database.ref('/todos/{uid}/{todoId}/text’).onCreate()/todos/{uid}/{todoId}/text’が新規作成された時に、onCreateの引数で与えた関数を実行します。パスは、/todos/{uid}/{todoId}でなく、/todos/{uid}/{todoId}/textです。なるべく深い所にトリガを設定します。
  • pathで波括弧で囲った部分はパラメータとして扱われ、context.params.{パラメータ名}で取得することができます(#5, #6)。
  • console.logで、Firebaseのコンソールにログを出力することが可能になります(#7)。
  • 関数はPromiseか値を返す必要があります。特に返す値がない場合はnullでよいでしょう(#8)。処理が完了した時に何も返さないとエラーになります。
  • タスクが更新された時に実行される関数を定義します(#9)。今回はcompletedの値しか変化しないので、pathは/todos/{uid}/{todoId}/completedです。

Cloud Functionsのデプロイ

functions/index.jsをデプロイします。

$ cd todo-sample
$ firebase deploy --only functions

=== Deploying to 'todo-sample-xxxxx’…

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint

> functions@ lint /Users/yuka/react/todo-sample/functions
> eslint .

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (55.94 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function addRecentUpdateOnCreate(us-central1)...
i  functions: creating Node.js 6 function exports.addRecentUpdateOnUpdateCompleted(us-central1)...
✔  functions[addRecentUpdateOnCreate(us-central1)]: Successful create operation. 
✔  functions[exports.addRecentUpdateOnUpdateCompleted(us-central1)]: Successful create operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/todo-sample-xxxxx/overview

デプロイが完了しました。

Firebaseコンソールでデプロイした関数を確認します。

f:id:yucatio:20181023132227p:plain

動作確認

タスクを追加します。

f:id:yucatio:20181023132805p:plain:w200

コンソールを確認します。

f:id:yucatio:20181023133600p:plain

addRecentUpdateOnCreate called.と、uidとtodoIdが表示されていることが確認できました。

タスクを更新します。

f:id:yucatio:20181023134415p:plain:w200

コンソールを確認します。

f:id:yucatio:20181023133909p:plain

addRecentUpdateOnUpdateCompleted called.と、uidとtodoIdが表示されていることが確認できました。

以上で、/todos/{uid}の下にタスクが追加された時と、タスクが更新された時にコンソールへメッセージを表示することができました。

作成トリガと更新トリガは、更新日時を使用するのがおすすめ

今回は、タスクの作成トリガは/todos/{uid}/{todoId}/textの作成、更新トリガは/todos/{uid}/{todoId}/completedの更新としました。問題なく動きますが、将来的に項目が増えた時にトリガを追加し忘れる恐れがあります。 また、データごとにトリガになるプロパティが違うのは面倒です。

この問題を解決するには、すべてのデータに、作成日付を表す_createdAtプロパティと更新日付を表す_updatedAtプロパティを付与します。データの作成は、_cretedAtの作成をトリガにし、データの更新は、_updatedAtの更新をトリガにします。

以下の記事でこれの対応をしています。

yucatio.hatenablog.com

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com