Blog

GraphQLを使用してリモートデバイスにアクセスし、監視する

2024年2月3日

はじめに

remote.itを使ったことがある人なら、remote.itがダッシュボードを提供し、デバイスやその状態を確認したり、接続したりできることをきっとご存知でしょう。しかし、特定のものを監視する必要がある場合はどうしたらよいでしょうか?

例えば、remote.itは、アカウントキーの認証情報を使って、デバイスに関するデータにアクセスするためのAPIを提供しています。

今回は、GraphQL API を活用して、遠隔地のデバイスやイベントを監視する小さな React App を構築する方法を学びます。

remote.it GraphQL API

実は、remote.itはREST APIとGraphQL APIも提供しているのです。

そこで、コーディングに入る前に、RESTとGraphQLの主な違いについて説明します。これは、私たちの選択を正当化するのに役立ちます。

組織図

RESTは、6つのアーキテクチャ上の制約に準拠している。

  • インターフェイスを統一する。
  • クライアント・サーバー
  • ステートレスです。
  • キャッシュ可能です。
  • レイヤードシステム。
  • コード・オン・デマンド(オプション)

しかし、GraphQLはデータの形状を記述するスキーマと、GraphQLアプリケーションで使用できる様々なデータ型を定義するための型システムで構成されています。

フォーマット

RESTは、XML、JSON、HTML、プレーンテキストなど、多くのフォーマットをサポートしています。GraphQLは、JSONのみをサポートしています。

データ取り込み

REST APIを使用してデータを取得するには、GET、POST、PUT、PATCH、DELETEを使用します。GraphQLでは、Query(データを取得する)またはmutations(オブジェクトを作成、削除、変更する)のタイプでPOSTリクエストを行うだけでよいのです。

以上が、RESTとGraphQLの大きな違いです。

コーディングに飛び込もう

シンプルなReactプロジェクト

まず、React Appのプロジェクトを作成しましょう。

yarn create react-app remoteit-react-graphql

これが終わってプロジェクトがインストールされたら、axios、boostrap、swr、axios-auth-refresh を追加します。

yarn add axios swr axios-auth-refresh react-bootstrap@next bootstrap@5.1.1 react-router-dom

Webページのスタイルにはbootstrapを、APIへのリクエストにはaxiosを使用する予定です。

axios-auth-refreshは、リクエストに使用されている現在のトークンが期限切れの場合、新しいトークンを取得するために使用されます。

1 - APIキーによる認証

RESTおよびGraphQL APIへのリクエストを行う前に、開発者用のAPIキーを使用する必要があります。

このAPIキーは、欲しいデータを取得するためのトークンと共に、各リクエストで送信されます。

自分のキーにアクセスするには、ウェブポータルのアカウントセクション(https://app.remote.it/)にアクセスしてください。

完了したら、ディレクトリprojectに.envファイルを作成します。このファイルには、開発者のAPI KEYやAPI URLなどの機密情報を保存するために使用します。

react_app_api_url=https://api.remote.itREACT_APP_DEV_API_KEY=YOUR 開発者用apiキー

素晴らしい!では、axiosを使って独自のフェッチャーを作ってみましょう。なぜ今作るのか?期限切れのトークンを使ってリクエストを行うので、このトークンが期限切れになったら新しいトークンを請求するのが便利です。インターセプターを書くことにします。

src ディレクトリに axios.js というファイルを作成します。

import axios from “axios”;
import createAuthRefreshInterceptor from “axios-auth-refresh”;
import {useHistory} from “react-router-dom”;

const axiosService = axios.create({
   baseURL: process.env.REACT_APP_API_URL,
   headers: {
       ‘Content-Type’: ‘application/json’,
       ‘apikey’: process.env.REACT_APP_DEV_API_KEY
   }
});

axiosService.interceptors.request.use(async (config) => {
   const token = localStorage.getItem(‘token’);

   if (token){
       config.headers.token = token;
       console.debug(‘[Request]’, config.baseURL + config.url, JSON.stringify(token));
   }

   return config;
})


axiosService.interceptors.response.use(
   (res) => {
       console.debug(‘[Response]’, res.config.baseURL + res.config.url, res.status, res.data);
       return Promise.resolve(res);
   },
   (err) => {
       console.debug(
           ‘[Response]’,
           err.config.baseURL + err.config.url,
           err.response.status,
           err.response.data
       );
       return Promise.reject(err);
   }
);

const refreshAuthLogic = async (failedRequest) => {
   const authHash = localStorage.getItem(‘authHash’)
   const username = localStorage.getItem(‘username’);

   const history = useHistory();
   if (authHash) {
       return axios
           .post(
               ‘/apv/v27/user/login’,
               {
                   username: username,
                   authhash: authHash
               },
               {
                   baseURL: process.env.REACT_APP_API_URL
               }
           )
           .then((resp) => {
               const { token, service_authhash } = resp.data;
               failedRequest.response.config.headers.token = token;
               localStorage.setItem(“authHash”, service_authhash);
               localStorage.setItem(‘token’, token);
           })
           .catch((err) => {
               if (err.response && err.response.status === 401){
                   history.push(‘/login’);
               }
           });
   }
};

createAuthRefreshInterceptor(axiosService, refreshAuthLogic);

export function fetcher(url, data) {
   return axiosService.post(url, data).then((res) => res.data);
}

export default axiosService;

これができたら、今度はLoginページを作成します。

ログイン

ログインページでは、ユーザー名とパスワードの入力が可能です。リクエストが成功した場合は、ダッシュボードページにリダイレクトされます。

import React, {useState} from “react”;
import axios from “axios”;
import {useHistory} from “react-router-dom”;

const Login = (key, value) => {

   const history = useHistory();
   const [email, setEmail] = useState(“”);
   const [password, setPassword] = useState(“”);
   const [error, setError] = useState(“”);

   function handleSubmit(event) {
       event.preventDefault();
       axios.post(`${process.env.REACT_APP_API_URL}/apv/v27/user/login`, {
           username: email,
           password: password
       }, {
           headers: {
               “Content-Type”: “application/json”,
               “apikey”: process.env.REACT_APP_DEV_API_KEY
           }
       }).then( r => {
           localStorage.setItem(“username”, email);
           localStorage.setItem(“authHash”, r.data.service_authhash);
           localStorage.setItem(‘token’, r.data.token)
           history.push(‘/home’)
       }).catch(e => {
           setError(e.response.data.reason);
       })

   }

   return (
       <div className=”w-25 mh-100″>
           <form onSubmit={handleSubmit}>
               <div className=”form-group m-2″>
                   <label htmlFor=”email”>Email address</label>
                   <input
                       className=”form-control my-2″
                       required
                       id=”email”
                       type=”email”
                       name=”username”
                       placeholder=”Email address”
                       onChange={(e) => setEmail(e.target.value)}
                   />
               </div>
               <div className=”form-group m-2″>
                   <label htmlFor=”password”>Password</label>
                   <input
                       className=”form-control my-2″
                       id=”password”
                       required
                       type=”password”
                       name=”password”
                       placeholder=”Password”
                       onChange={(e) => setPassword(e.target.value)}
                   />
               </div>
               <button type=”submit” className=”btn btn-primary m-2″>Submit</button>
           </form>
           {error && <div>{error}</div>}
       </div>
   )
}

export default Login;

お気づきのように、リクエストが成功すると、トークンとservice_authhashがlocalstorageに登録されます。service_authhashは再ログインして新しいトークンを取得する際に使用されます。

使用方法

ログインページの準備ができました。あとは、react-routerをプロジェクトに統合して、ルートの定義を開始します。

import {
 BrowserRouter as Router,
 Switch,
 Route,
} from “react-router-dom”;
import Login from “./Login”;
import Home from “./Home”;
import “bootstrap/dist/css/bootstrap.min.css”;
function App() {
 return (
   <div className=”container-fluid”>
     <Router>
             <Switch>
                 <Route exact path=”/” component={Login} />
                 <Route exact path=”/home” component={Home} />
             </Switch>
     </Router>
   </div>
 );
}

export default App;

それでは、Homeページを追加してみましょう。

デバイスと直近のイベントログを表示する

Home Pageには、デバイスを表示するテーブルと、直近のイベントを表示するテーブルの2つが表示されます。

GraphQL APIでクエリを作成することになります。

ではまず、使用するクエリを書いてみましょう。

import React from “react”;
import useSWR from ‘swr’
import {fetcher} from “./axios”;


const devicesQuery = {
   query: `{
 login {
   email
   devices(size: 1000, from: 0) {
     total
     hasMore
     items {
       id
       name
       hardwareId
       created
       state        endpoint{geo{latitude longitude}}
     }
   }
 }
}
`
}

const eventsQuery = {
   query: `{
 login {
   events {
     hasMore
     total
     items {
       type
       owner {
         email
       }
       actor {
         email
       }
       target {
         created
         id
         name
       }
       users {
         email
       }
       timestamp
     }
   }
 }
}
`
}

早速、クエリについて説明しましょう。一般に、どちらのクエリも持っていることに気がつくでしょう。

  • login: クエリに認証が必要であることを意味し、ユーザーの電子メールを返します。
  • アイテム(デバイス/イベント)をサイズなどのパラメータで指定します。また、任意のフィルターをかけることができます。
  • totalはアイテムの総数を示し、取得されていない残りの数値が多い。

コンポーネントのロジックを書き始めることができます。

その前に、SWRとaxios.jsに記述したフェッチャーを使ってリクエストしてみましょう。

SWRはデータ取得のためのReactフックです。データのキャッシュと定期的な再バリデーションが可能です。ダッシュボードを定期的に更新するような場合に非常に便利です。

const Home = () => {

   const dataDevices = useSWR(‘devices’, () => fetcher(‘/graphql/v1’, devicesQuery));

   const dataEvents = useSWR(‘events’, () => fetcher(‘/graphql/v1’, eventsQuery));

 return <div></div>
}

そして最後にテンプレート化です。ダッシュボードとテーブルのUIを作成しましょう。

...
   return (
       <div>
           <div className=”m-5″>
               <h3 className=”h3″>Number of devices: {dataDevices.data?.data?.login?.devices?.total}</h3>
               <table className=”table table-hover”>
                   <thead>
                   <tr>
                       <th scope=”col”>Device id</th>
                       <th scope=”col”>Name</th>
                       <th scope=”col”>hardwareId</th>
                       <th scope=”col”>Created</th>
                       <th scope=”col”>State</th>                        <th scope=”col”>Geo localisation</th>
                   </tr>
                   </thead>
                   <tbody>
                   {
                       dataDevices.data?.data?.login?.devices?.items.map((device, index) =>
                           {
                               console.log(device);
                               return <tr>
                                   <th scope=”row”>{device.id}</th>
                                   <td>{device.name}</td>
                                   <td>{device.hardwareId}</td>
                                   <td>{formatDate(device.created)}</td>
                                   <td className={getStatusColor(device.state)}>{device.state}</td>                                    <td><a href={`https://www.google.com/maps/place/${device.endpoint.geo.latitude},${device.endpoint.geo.longitude}`} target=”_blank”
                                     rel=”noopener noreferrer”>See localisation</a></td>          
                               </tr>
                           }
                       )
                   }
                   </tbody>
               </table>
           </div>

           <hr />

           <div className=”m-5″>
               <h3 className=”h3″>Number of events: {dataEvents.data?.data?.login?.events?.total}</h3>
               <table className=”table table-hover”>
                   <thead>
                   <tr>
                       <th scope=”col”>Type</th>
                       <th scope=”col”>Owner</th>
                       <th scope=”col”>Actor</th>
                       <th scope=”col”>Target Name</th>
                       <th scope=”col”>Time</th>
                   </tr>
                   </thead>
                   <tbody>
                   {
                       dataEvents.data?.data?.login?.events?.items?.map((event, index) => (
                           <tr>
                               <th scope=”row”>{event.type}</th>
                               <td>{event.owner?.email}</td>
                               <td>{event.actor?.email}</td>
                               <td>{event.target?.map((target, index) => (
                                   <p>{target.name} |</p>
                               ))}</td>
                               <td>{event.timestamp}</td>
                           </tr>
                       ))
                   }
                   </tbody>
               </table>
           </div>
       </div>
   )

ダッシュボードはこのように表示されるはずです。

といった感じです。以上、remote.it GraphQL APIを使って、デバイスのイベントとステータスを監視する方法でした。

このAPIは、機器の状態やイベントを取得するだけでなく、より多くの機能を提供します。

また、:

  • remote.itから通知を受け取るためのwebhookを作成します。
  • 機器への接続開始、接続停止
  • アクセスキーを直接管理できます。

また、これらのリクエストはすべてRESTで行うことができることも忘れてはいけません。これらについては、こちらのドキュメントを自由にご覧ください。

まとめ

remote.it APIを使い始めるには、こちらのドキュメントをご覧ください。私たちは、あなたがremote.it APIを使ってどんな魅力的なインテグレーションを構築するか、ぜひ見てみたいと思っています。

また、このガイドのコードを見つけたい場合は、こちらのGitHubを参照してください。

関連ブログ