本記事では、ReactJS、ReactFlow、ExpressJS、GridDB、NodeJSなどの技術を使用してフルスタックのWebマインドマップアプリケーションを構築することで、可視化の力と実用性を探究します。
GridDBは、IoTやビッグデータに最適化された、スケーラビリティに優れたインメモリNoSQLの時系列データベースです。GridDBはIoTやビッグデータに最適化されていますが、ゲームやウェブアプリケーションなど、他の用途にも使用できます。
ソースコード
GitHubで入手可能:
`$ git clone https://github.com/griddbnet/Blogs.git –branch mind-map
アプリケーション
GridDBは、Windows、Linux、MacのいずれのOSでも利用できます。
私は、WindowsにWSL(Windows Subsystem for Linux)をインストールして、自分のマシン上のLinux(Ubuntu)にアクセスできるようにしましたが、DockerやMacOSを使用してこのチュートリアルを進めることもできます。GridDBドキュメントでは、データベースを自分のコンピューターに正常にインストールするための詳細なインストール手順が提供されています。
また、文書よりも動画の方が好みだという方には、YouTubeチャンネルに優れたYouTube動画も用意されています。ウェブアプリケーションの全コードはGithubで入手できます。
ターミナルを開き、次のコマンドでレポジトリをクローンします。
git clone https://github.com/Babajide777/grid-db-mind-map.git
それから、
cd grid-db-mind-map
グリッドdbマインドマップアプリのディレクトリに変更します。
アプリケーションの分類
このアプリケーションは3つの部分に分かれています。
- バックエンド
- フロントエンド
- フロントエンドとバックエンドの接続
前提条件
- GridDB version 5.3.0
- Node v12.22.9
フロントエンド
このマインドマップアプリのUIでは、ユーザーは新しいマップアイテムの追加、マップアイテムの編集、マップアイテムの削除を行うことができます。このプロジェクトのUIを構築するために、以下のライブラリが使用されました。
ReactJs:
ReactJSは、Metaが構築・管理するユーザーインターフェース構築用のJavaScriptライブラリです。
Material UI:
Material UI は、Google の Material Design システムからのコンポーネントの包括的なライブラリです。
RTK Query:
Redux Toolkit は、状態管理ライブラリです。
React Flow:
React Flow は、ノードベースのエディタやインタラクティブなダイアグラムを構築するためのカスタマイズ可能な React コンポーネントです。
アプリのフロントエンドを表示するには、クライアントディレクトリに変更します。
cd client
必要な依存関係をインストールします。
npm i
次に、アプリを実行します。
npm start
右上には、新しいアイデアの詳細を入力できるフォームがあります。入力フィールドは4つあります。ソースは、アイデアがリンクされるノードのドロップダウンです。例えば、「フロントエンド開発者」のソースは「ソフトウェア開発者」です。
positionXはノードのX軸上の位置、positionYはノードのY軸上の位置で、いずれも数値です。一方、labelはノードの名称です。
左下には4つのボタンがあり、+記号はズームイン、-記号はズームアウト、ボックス記号はすべてのノードを中央に配置、南京錠記号はロックまたはロック解除を意味します。右下には、キャンバス全体のミニチュア版を表示するミニマップがあります。
各ノードには、ノードの削除と編集を行うためのボタンがあります。
バックエンド
このプロジェクトのバックエンドでは、フロントエンドから正しいマップアイテムのデータが取得され、GridDBデータベースに保存されることを保証します。GridDBデータベースを使用して機能を実現することができます。
適切なCRUD機能はアプリで実行されます。
バックエンドを構築するために必要なパッケージは次のとおりです。
-
ExpressJs:RESTful APIの構築に使用される最小限のNodeJSフレームワーク。
-
Morgan: HTTPリクエストをログに記録するために使用されるNodeJSミドルウェア。
-
GridDB Node API: NodeJS用GridDBクライアント
-
Joi: JavaScript用のスキーマ記述言語およびデータ検証ツール
食事プランアプリの構築ステップバイステップガイド
以下に説明する手順に従ってください。
ステップ1:サーバーフォルダの作成
「サーバー」フォルダを作成し、npmを初期化してpackage.jsonファイルを生成します。フォルダの名前は自由に決めることができます。
npm i
ステップ2:必要なパッケージのインストール
以下のコードを実行して、必要なパッケージをすべて一度にインストールします。
npm i express morgan joi griddb-node-api cors
追加
nodemonをインストールする必要はありませんが、開発時にはあると便利です。変更が保存されるたびにサーバーが自動的に再起動されます。これは、開発環境の依存パッケージとしてnodemonをインストールするコマンドです。
npm i -D nodemon
{
"name": "grid-db-mind-map-server",
"version": "1.0.0",
"description": "backend for grid db mind-map",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server.js"
},
"keywords": [
"mind-map",
"griddb",
"griddb_node"
],
"author": "Oyafemi Babajide",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"griddb_node": "^0.8.4",
"griddb-node-api": "^0.8.6",
"joi": "^17.11.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
ステップ3:Server.jsファイルの作成
server.jsファイルを作成し、以下のコードを挿入します。
const express = require("express");
const morgan = require("morgan");
const app = express();
const cors = require("cors");
const PORT = 4000 || process.env.PORT;
app.use(morgan("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cors("*"));
app.get("/", (req, res) => {
res.send("GridDB mind map Backend API");
});
app.use("/api", require("./routes/mindMapRoutes"));
app.listen(PORT, () => {
console.log(`Server started on ${PORT}`);
});
nodemon を開発依存パッケージとしてインストールした場合は、package.json ファイルの「scripts」セクションに次のコードを追加する必要があります。
"dev": "nodemon index.js"
ステップ4:アプリケーションの実行
nodemonをインストールした場合は、 ‘npm start’を使用してアプリケーションを起動できますが、変更を加えるたびにアプリケーションを再起動する必要があり、nodemonの本来の目的に反することになります。以下の方法では、変更を加えるたびにアプリケーションを再起動する必要はありません(nodemonの利点)。
npm run dev
ステップ5:GridDBデータベースの設定
griddb-node-apiパッケージを使用してGridDBデータベースに接続します。次に、プロジェクトのコンテナ名を設定します。私はプロジェクトに関連して「mind-map」と名付けましたが、好きなように呼べます。
const griddb = require("griddb-node-api");
const containerName = "mind-map";
const initStore = async () => {
const factory = griddb.StoreFactory.getInstance();
try {
// Connect to GridDB Cluster
const store = await factory.getStore({
host: "127.0.0.1",
port: 10001,
clusterName: "myCluster",
username: "admin",
password: "admin",
});
return store;
} catch (e) {
throw e;
}
};
function initContainer() {
const conInfo = new griddb.ContainerInfo({
name: containerName,
columnInfoList: [
["id", griddb.Type.STRING],
["source", griddb.Type.STRING],
["target", griddb.Type.STRING],
["x", griddb.Type.DOUBLE],
["y", griddb.Type.DOUBLE],
["label", griddb.Type.STRING],
["lineId", griddb.Type.STRING],
],
type: griddb.ContainerType.COLLECTION,
rowKey: true,
});
return conInfo;
}
async function createContainer(store, conInfo) {
try {
const collectionDB = await store.putContainer(conInfo);
return collectionDB;
} catch (err) {
console.error(err);
throw err;
}
}
async function initGridDbTS() {
try {
const store = await initStore();
const conInfo = await initContainer();
const collectionDb = await createContainer(store, conInfo);
return { collectionDb, store, conInfo };
} catch (err) {
console.error(err);
throw err;
}
}
async function containersInfo(store) {
for (
var index = 0;
index < store.partitionController.partitionCount;
index++
) {
store.partitionController
.getContainerNames(index, 0, -1)
.then((nameList) => {
nameList.forEach((element) => {
// Get container information
store.getContainerInfo(element).then((info) => {
if (info.name === containerName) {
console.log("Container Info: \n💽 %s", info.name);
if (info.type == griddb.ContainerType.COLLECTION) {
console.log("📦 Type: Collection");
} else {
console.log("📦 Type: TimeSeries");
}
//console.log("rowKeyAssigned=%s", info.rowKey.toString());
console.log("🛢️ Column Count: %d", info.columnInfoList.length);
info.columnInfoList.forEach((element) =>
console.log("🔖 Column (%s, %d)", element[0], element[1])
);
}
});
});
return true;
})
.catch((err) => {
if (err.constructor.name == "GSException") {
for (var i = 0; i < err.getErrorStackSize(); i++) {
console.log("[%d]", i);
console.log(err.getErrorCode(i));
console.log(err.getMessage(i));
}
} else {
console.log(err);
}
});
}
}
/**
* Insert data to GridDB
*/
async function insert(data, container) {
try {
let savedData = await container.put(data);
console.log(savedData);
return { status: true };
} catch (err) {
if (err.constructor.name == "GSException") {
for (var i = 0; i < err.getErrorStackSize(); i++) {
console.log("[%d]", i);
console.log(err.getErrorCode(i));
console.log(err.getMessage(i));
}
return { status: false, error: err.toString() };
} else {
console.log(err);
return { status: false, error: err };
}
}
}
async function multiInsert(data, db) {
try {
await db.multiPut(data);
return { ok: true };
} catch (err) {
console.log(err);
return { ok: false, error: err };
}
}
async function queryAll(conInfo, store) {
const sql = `SELECT *`;
const cont = await store.putContainer(conInfo);
const query = await cont.query(sql);
try {
const rowset = await query.fetch();
const results = [];
while (rowset.hasNext()) {
const row = rowset.next();
results.push(row);
}
return { results, length: results.length };
} catch (err) {
console.log(err);
return err;
}
}
async function queryByID(id, conInfo, store) {
try {
const cont = await store.putContainer(conInfo);
const row = await cont.get(id);
return row;
} catch (err) {
console.log(err, "here");
}
}
// Delete container
async function dropContainer(store, containerName) {
store
.dropContainer(containerName)
.then(() => {
console.log("drop ok");
return store.putContainer(conInfo);
})
.catch((err) => {
if (err.constructor.name == "GSException") {
for (var i = 0; i < err.getErrorStackSize(); i++) {
console.log("[%d]", i);
console.log(err.getErrorCode(i));
console.log(err.getMessage(i));
}
} else {
console.log(err);
}
});
}
//Delete entry
const deleteByID = async (store, id, conInfo) => {
try {
const cont = await store.putContainer(conInfo);
let res = await cont.remove(id);
return [true, res];
} catch (error) {
return [false, error];
}
};
const editByID = async (store, conInfo, data) => {
try {
const cont = await store.putContainer(conInfo);
const res = await cont.put(data);
return [true, ""];
} catch (err) {
return [false, err];
}
};
module.exports = {
initStore,
initContainer,
initGridDbTS,
createContainer,
insert,
multiInsert,
queryAll,
dropContainer,
containersInfo,
containerName,
queryByID,
deleteByID,
editByID,
};
initStore 関数は、ホスト、ポート、clusterName、ユーザ名、パスワードを使用して、アプリをGridDB Clusterに接続します。
initContainer 関数は、コンテナのカラムと、異なるカラムのデータタイプを設定するために使用されます。
createContainer はコンテナを作成し、initGridDbTS はデータベース接続を初期化します。
ステップ6: 食事プランの作成
マインドマップアイテムを作成するには
router.post("/add-meal", addMealPlan);
Joiパッケージを使用して、フロントエンドから送信されたリクエストボディを検証し、ステップ6で作成したコンテナに挿入します。
const Joi = require("joi");
//map item validation rules
const mapItemValidation = async (field) => {
const schema = Joi.object({
id: Joi.string().required(),
source: Joi.string().required(),
target: Joi.string().required(),
x: Joi.number().integer().required(),
y: Joi.number().integer().required(),
label: Joi.string().required(),
lineId: Joi.string().required(),
});
try {
return await schema.validateAsync(field, { abortEarly: false });
} catch (err) {
return err;
}
};
module.exports = {
mapItemValidation,
};
async function insert(data, container) {
try {
let savedData = await container.put(data);
console.log(savedData);
return { status: true };
} catch (err) {
if (err.constructor.name == "GSException") {
for (var i = 0; i < err.getErrorStackSize(); i++) {
console.log("[%d]", i);
console.log(err.getErrorCode(i));
console.log(err.getMessage(i));
}
return { status: false, error: err.toString() };
} else {
console.log(err);
return { status: false, error: err };
}
}
}
idとlineIdはフロントエンドでランダムに生成され、ソース、ターゲット、x、y、ラベルとともに送信されます。マップアイテムがデータベースに保存された後、作成されたランダムidを使用してマップアイテムを照会し、保存されたマップアイテムの詳細を取得します。
async function queryByID(id, conInfo, store) {
try {
const cont = await store.putContainer(conInfo);
const row = await cont.get(id);
return row;
} catch (err) {
console.log(err, "here");
}
}
const addMealItem = async (req, res) => {
//validate req.body
const { collectionDb, store, conInfo } = await initGridDbTS();
const { details } = await mapItemValidation(req.body);
if (details) {
let allErrors = details.map((detail) => detail.message.replace(/"/g, ""));
return responseHandler(res, allErrors, 400, false, "");
}
try {
const { id, source, target, x, y, label, lineId } = req.body;
const data = [id, source, target, x, y, label, lineId];
const saveStatus = await insert(data, collectionDb);
if (saveStatus.status) {
const result = await queryByID(id, conInfo, store);
let returnData = {
id: result[0],
source: result[1],
target: result[2],
x: result[3],
y: result[4],
label: result[5],
lineId: result[6],
};
return responseHandler(
res,
"Map Item saved successfully",
201,
true,
returnData
);
}
return responseHandler(
res,
"Unable to save map item",
400,
false,
saveStatus.error
);
} catch (error) {
responseHandler(res, "Error saving map item", 400, false, error.message);
}
};
その後、地図プランの項目詳細が、json形式のレスポンスとしてフロントエンドに送信されます。
ステップ 7: マップアイテムの詳細を取得
必要なマップアイテムの ID は、リクエストデータのパラメータから取得します。
router.get("/map-detail/:id", mapItemDetails);
次に、データベース内のデータを使用してマップ項目のIDが照会され、マップ項目が見つかれば、マップ項目データを含む200応答が送信されます。
このルートは、主に編集対象のマップ項目の詳細を取得するために使用されます。
const mapItemDetails = async (req, res) => {
try {
const { store, conInfo } = await initGridDbTS();
const { id } = req.params;
const result = await queryByID(id, conInfo, store);
let returnData = {
id: result[0],
source: result[1],
target: result[2],
x: result[3],
y: result[4],
label: result[5],
lineId: result[6],
};
return result
? responseHandler(res, "map item detail found", 200, true, returnData)
: responseHandler(res, "No map item detail found", 400, false, "");
} catch (error) {
responseHandler(res, "Error saving map item", 400, false, error.message);
}
};
ステップ8:マップアイテムの編集
router.put("/edit-map-item/:id", editMapItem);
マップアイテムを編集するには、必要なマップアイテムのIDをリクエストのパラメータとして再度送信します。指定されたIDを使用してマップアイテムが照会され、マップアイテムの古い詳細が新しい詳細に置き換えられます。
const editMapItem = async (req, res) => {
try {
const { store, conInfo } = await initGridDbTS();
const { id } = req.params;
const result = await queryByID(id, conInfo, store);
if (!result) {
return responseHandler(res, "incorrect map item ID", 400, false, "");
}
const { source, target, x, y, label, lineId } = req.body;
const data = [id, source, target, x, y, label, lineId];
const check = await editByID(store, conInfo, data);
if (check[0]) {
const result2 = await queryByID(id, conInfo, store);
let returnData = {
id: result2[0],
source: result2[1],
target: result2[2],
x: result2[3],
y: result2[4],
label: result2[5],
lineId: result2[6],
};
return responseHandler(
res,
"map item edited successfully",
200,
true,
returnData
);
}
return responseHandler(res, "Error editing map item ", 400, false, "");
} catch (error) {
responseHandler(res, "Error saving map item", 400, false, error.message);
}
};
const editByID = async (store, conInfo, data) => {
try {
const cont = await store.putContainer(conInfo);
const res = await cont.put(data);
return [true, ""];
} catch (err) {
return [false, err];
}
};
ステップ9:マップ項目の削除
router.delete("/delete-map-item/:id", deleteMapItem);
id はパラメータまたはリクエストデータから取得され、指定されたマップアイテムを含む行を削除するために使用されます。
const deleteMapItem = async (req, res) => {
try {
const { store, conInfo } = await initGridDbTS();
const { id } = req.params;
const result = await deleteByID(store, id, conInfo);
return result[0]
? responseHandler(res, "map item deleted successfully", 200, true, "")
: responseHandler(res, "Error deleting map item", 400, false, "");
} catch (error) {
responseHandler(res, "Error saving map item", 400, false, error.message);
}
};
const deleteByID = async (store, id, conInfo) => {
try {
const cont = await store.putContainer(conInfo);
let res = await cont.remove(id);
return [true, res];
} catch (error) {
return [false, error];
}
};
ステップ 10: データベース内のすべてのマップアイテムのリストを取得する
データベース内のすべてのマップアイテムのリストを取得するには、以下の手順を実行する必要があります。
router.get("/all-map-items", getAllMapItems);
これにより、データベース内のすべてのマップアイテムが返されます。
const getAllMapItems = async (req, res) => {
try {
const { store, conInfo } = await initGridDbTS();
const result = await queryAll(conInfo, store);
let data = [];
result.results.forEach((result) => {
let returnData = {
id: result[0],
source: result[1],
target: result[2],
x: result[3],
y: result[4],
label: result[5],
lineId: result[6],
};
data.push(returnData);
return result;
});
return responseHandler(
res,
"all map items in the database successfully retrieved",
200,
true,
data
);
} catch (error) {
return responseHandler(
res,
"Unable to retrieve meal plans",
400,
false,
""
);
}
};
async function queryAll(conInfo, store) {
const sql = `SELECT *`;
const cont = await store.putContainer(conInfo);
const query = await cont.query(sql);
try {
const rowset = await query.fetch();
const results = [];
while (rowset.hasNext()) {
const row = rowset.next();
results.push(row);
}
return { results, length: results.length };
} catch (err) {
console.log(err);
return err;
}
}
フロントエンドとバックエンドの接続
このアプリの本質は、アプリが完全に機能することを保証するために、バックエンドとフロントエンドを接続することです。私は、アプリの機能をテストするために、短いソフトウェア開発ロードマップを作成しました。
結論
この包括的なガイドでは、シンプルなアイデアを革新的な現実へと変えた歴史の創造者たちにインスピレーションを得て、私たちは革新の旅に出ました。
チャールズ・バベッジの基礎的な研究から、トーマス・エジソン、スティーブ・ジョブズ、ビル・ゲイツといった現代の革新的なイノベーターたちの画期的な業績に至るまで、私たちはアイデアをマッピングし、それを具体的な成果へと変える力の存在を目の当たりにしてきました。
フロントエンドソフトウェアエンジニアリングの領域に深く踏み込むことで、私たちの生活や身の回りの世界を再形成する可能性を探求してきました。ReactJS、ReactFlow、ExpressJS、GridDB、NodeJSなどの最新技術を活用し、私たちはフルスタックのウェブマインドマップアプリケーションの作成に着手し、想像と実装のギャップを埋める取り組みを行っています。
私たちの旅の中心には、IoTやビッグデータアプリケーションに最適化された、汎用性と拡張性に優れたNoSQLの時系列データベースであるGridDBがあります。このプロジェクトにGridDBを統合することで、データの視覚化と管理の新たな可能性が切り開かれ、私たちのアイデアを正確かつ効率的に実現できるようになりました。
インストールプロセスからフロントエンドおよびバックエンドコンポーネントの開発まで、私たちは明確な目的意識を持って各ステップを進んでいきました。私たちのステップバイステップガイドに従うことで、デジタル環境でインパクトのあるソリューションを創造するスキルを身につけ、イノベーションの旅を始めるために必要な知識とツールを得ることができます。
私たちの探求を締めくくるにあたり、イノベーションに境界はないということを覚えておいてください。
熟練の開発者であれ、開発に興味を持ち始めたばかりの人であれ、発見への道は開かれています。創造性を発揮し、現状に挑戦し、アイデアを膨らませましょう。
決意と忍耐があれば、未来を形作る力はあなたにもあります。
革新を続け、インスピレーションを与え、世界を変えていきましょう。
ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb