Use HarperDB Custom Functions With React

Last week, I had a chance to explore HarperDB – a fast, up-to-date database that allows you to develop full stack applications. With this in mind, I developed a ToDo React app with custom HarperDB functionality.

HarperDB is a distributed database that focuses on making data management easier.

  • It supports both SQL and NoSQL queries.
  • It also provides access to the database instance directly within the application on the client side.

In this article, let’s learn about HarperDB and how to create a React application using HarperDB custom functions!

Let’s talk about HarperDB custom functions

  • Add your API endpoints to a standalone API server within HarperDB.
  • Use HarperDB Core’s methods to interact with your data at lightning speed.
  • The custom functions are powered by Fastify, so they are very flexible.
  • Manage in HarperDB Studio, or use your own IDE and version management system.
  • Deploy your custom functions to all instances of HarperDB with a single click.

What do we build?

We are going to create a simple ToDo React app. When we’re done, it will look like this when it’s running in localhost:

Let’s look at how we can develop our To-Do React app

This ToDo app allows the user to create a task that needs to be completed by the user.

It has two countries

Users can filter the task list based on the task status as well. It will also allow the user to modify a task and delete a task as well.

So the main idea is any user generated task which you can see in ‘view all’ menu, all tasks will be saved in HarperDB with the help of custom jobs.

Project preparation overview

Creating a React app is the best way to start building a new one-page app in React.

npx create-react-app my-app
cd my-app
npm start

Dependencies used:

 "@emotion/react": "^11.5.0",
    "@emotion/styled": "^11.3.0",
    "@mui/icons-material": "^5.0.5",
    "@mui/material": "^5.0.6",
    "@testing-library/jest-dom": "^5.15.0",
    "@testing-library/react": "^11.2.7",
    "@testing-library/user-event": "^12.8.3",
    "axios": "^0.24.0",
    "classnames": "^2.3.1",
    "history": "^5.1.0",
    "lodash.debounce": "^4.0.8",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.0.1",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.1.2"

It only creates the front end build pipeline for this project, so we can use HarperDB in the backend.

Alternatively, you can clone the GitHub repository and use the start directory as the root of your project. It contains the basic setup of the project that will get you ready. In this CSS project you can refer to Tasks.css (srctodo-componentTasks.css)

Let’s talk about React components

This is the folder structure:

In the file structure, we can see that tasks are the container component where we manage the application state, here the application state means the data we get from HarperDB using API endpoints, and this data is shared across all child components through properties.

Task component (Tasks.jsx):

Here is the file reference in the project:

srctodo-componentTasks.jsx

This component acts as a container component (has a task list and task search as a child component)

); } “data-lang =” text / javascript “>

import React, { useEffect, useCallback, useState, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import TaskSearch from './task-search-component/TaskSearch';
import './Tasks.css';
import axios from 'axios';
import debounce from '@mui/utils/debounce';
import TaskItem from './task-list-component/TaskList';
import Snackbar from '@mui/material/Snackbar';

export default function Tasks() {
  const navigate = useNavigate();
  const [searchParams, setSearchParams] = useSearchParams();
  const [taskList, setTaskList] = useState([]);
  const [filteredList, setFilteredList] = useState([]);
  const [open, setOpen] = useState(false);
  const [msg, setMsg] = useState('')
  const selectedId = useRef();
  useEffect(() => {
    getFilteredList();
  }, [searchParams, taskList]);

  const setSelectedId = (task) => {
    selectedId.current = task;
  };
  const saveTask = async (taskName) => {
    if (taskName.length > 0) {
      try {
        await axios.post(
          'your_url_here',
          { taskTitle: taskName, taskStatus: 'ACTIVE', operation: 'sql' }
        );
        getTasks();
      } catch (ex) {
        showToast();
      }
    }
  };

  const updateTask = async (taskName) => {
    if (taskName.length > 0) {
      try {
        await axios.put(
          'your_url_here',
          {
            taskTitle: taskName,
            operation: 'sql',
            id: selectedId.current.id,
            taskStatus: selectedId.current.taskStatus,
          }
        );
        getTasks();
      } catch (ex) {
        showToast();
      }
    }
  };

  const doneTask = async (task) => {
    try {
      await axios.put(
        'your_url_here',
        {
          taskTitle: task.taskTitle,
          operation: 'sql',
          id: task.id,
          taskStatus: task.taskStatus,
        }
      );
      getTasks();
    } catch (ex) {
        showToast();
    }
  };

  const deleteTask = async (task) => {
    try {
      await axios.delete(
        `your_url_here/${task.id}`
      );
      getTasks();
    } catch (ex) {
        showToast();
    }
  };

  const getFilteredList = () => {
    if (searchParams.get('filter')) {
      const list = [...taskList];
      setFilteredList(
        list.filter(
          (item) => item.taskStatus === searchParams.get('filter').toUpperCase()
        )
      );
    } else {
      setFilteredList([...taskList]);
    }
  };

  useEffect(() => {
    getTasks();
  }, []);

  const getTasks = async () => {
    try {
    const res = await axios.get(
      'your_url_here'
    );
    console.log(res);
    setTaskList(res.data);
    } catch(ex) {
        showToast();
    }
  };

  const debounceSaveData = useCallback(debounce(saveTask, 500), []);
  const searchHandler = async (taskName) => {
    debounceSaveData(taskName);
  };

  const showToast = () => {
    setMsg('Oops. Something went wrong!');
    setOpen(true)
  }

  return (
    <div className="main">
      <TaskSearch searchHandler={searchHandler} />
      <ul className="task-filters">
        <li>
          <a
            href="https://dzone.com/articles/javascript:void(0)"
            onClick={() => navigate("https://dzone.com/")}
            className={!searchParams.get('filter') ? 'active' : ''}
          >
            View All
          </a>
        </li>
        <li>
          <a
            href="https://dzone.com/articles/javascript:void(0)"
            onClick={() => navigate('/?filter=active')}
            className={searchParams.get('filter') === 'active' ? 'active' : ''}
          >
            Active
          </a>
        </li>
        <li>
          <a
            href="https://dzone.com/articles/javascript:void(0)"
            onClick={() => navigate('/?filter=completed')}
            className={
              searchParams.get('filter') === 'completed' ? 'active' : ''
            }
          >
            Completed
          </a>
        </li>
      </ul>
      {filteredList.map((task) => (
        <TaskItem
          deleteTask={deleteTask}
          doneTask={doneTask}
          getSelectedId={setSelectedId}
          task={task}
          searchComponent={
            <TaskSearch
              searchHandler={updateTask}
              defaultValue={task.taskTitle}
            />
          }
        />
      ))}
      <Snackbar
        open={open}
        autoHideDuration={6000}
        onClose={() => setOpen(false)}
        message={msg}
      />
    </div>
  );
}

your_url_here = you should replace this with your HarperDB endpoint URL.

For an example URL, see below:

Task List (TaskList.jsx):

Here is the file reference in the project:

srctodo-componenttask-list-componentTaskList.jsx

This component is used to display all the list of tasks we get from HarperDB

import React, { useState } from 'react';
import classNames from 'classnames';
import IconButton from '@mui/material/IconButton';
import DoneIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteIcon from '@mui/icons-material/Delete';
import TextField from '@mui/material/TextField';

export default function TaskItem({ task, searchComponent, getSelectedId, doneTask, deleteTask }) {
  const [editing, setEditing] = useState(false);
  const [selectedTask, setSelectedTask] = useState();
  let containerClasses = classNames('task-item', {
    'task-item--completed': task.completed,
    'task-item--editing': editing,
  });

  const updateTask = () => {
      doneTask({...task, taskStatus: task.taskStatus === 'ACTIVE' ? 'COMPLETED' : 'ACTIVE'});
  }

  const renderTitle = task => {
    return (
      <div className="task-item__title" tabIndex="0">
        {task.taskTitle}
      </div>
    );
  }
  const resetField = () => {
      setEditing(false);
  }
  const renderTitleInput = task => {
    return (
    React.cloneElement(searchComponent, {resetField})
    );
  }

  return (
    <div className={containerClasses} tabIndex="0">
      <div className="cell">
        <IconButton color={task.taskStatus === 'COMPLETED' ? 'success': 'secondary'} aria-label="delete" onClick={updateTask} className={classNames('btn--icon', 'task-item__button', {
            active: task.completed,
            hide: editing,
          })} >
          <DoneIcon />
        </IconButton>
       </div>

      <div className="cell">
        {editing ? renderTitleInput(task) : renderTitle(task)}
      </div>

      <div className="cell">
      {!editing && <IconButton onClick={() => {setEditing(true); getSelectedId(task)}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <EditIcon />
        </IconButton> }
        {editing && <IconButton onClick={() => {setEditing(false); getSelectedId('');}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <ClearIcon />
        </IconButton> }
        {!editing && <IconButton onClick={() => deleteTask(task)} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
            hide: editing,
          })} >
          <DeleteIcon />
        </IconButton> }
       </div>
    </div>
  );
}

TaskSearch (TaskSearch.jsx):

Here is the file reference in the project:

srctodo-componenttask-search-componentTaskSearch.jsx

This component provides a text box for users where users can enter the name of the task they need to perform. (same component we use while editing a task)

); } “data-lang =” text / javascript “>

import React from 'react';
import TextField from '@mui/material/TextField';

export default function TaskSearch({ searchHandler, defaultValue, resetField }) {
  const handleEnterKey = event => {
    if(event.keyCode === 13) {
      searchHandler(event.target.value);
      event.target.value="";
      if(resetField) {
        resetField();
      }
    }
  }

    return (
        <TextField
        id="filled-required"
        variant="standard"
        fullWidth 
        hiddenLabel
        placeholder="What needs to be done?"
        onKeyUp={handleEnterKey}
        defaultValue={defaultValue}
      />
    );
}

Here you can find the complete source code for the ToDo app.

In the Tasks.js component, you can see that we are making use of custom function APIs that allow us to save and edit data from HarperDB.

How do we develop an API using HarperDB custom functions

Let’s create the chart first

Created table

Create a project

advice: Before creating a project, you need to enable custom functions, once you click on functions, you will see a popup like below:

Click the green button “Enable custom function” will appear

Now let’s create a “ToDoApi” project, which will look like

Under the “/ToDoApi/Paths” section we will see one example.js file containing API endpoints.

Let’s write our API endpoints in order for:

  • Create a task
  • Edit task
  • delete a task
  • get a mission

Save task endpoint

Which is used to store data in a database

  server.route({
    url: '/saveTask',
    method: 'POST',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `insert into example_db.tasks (taskTitle, taskStatus) values('${request.body.taskTitle}', '${request.body.taskStatus}')`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Edit task endpoint

This is used to edit an existing record in your database, we are using the same endpoint as the save job but we have a different method type as PUT.

 server.route({
    url: '/saveTask',
    method: 'PUT',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `update example_db.tasks set taskTitle="${request.body.taskTitle}", taskStatus="${request.body.taskStatus}" where id='${request.body.id}'`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Delete the mission endpoint

server.route({
    url: '/deleteTask/:id',
    method: 'DELETE',
    // preValidation: hdbCore.preValidation,
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: `delete from example_db.tasks where id='${request.params.id}'`
      };
      return hdbCore.requestWithoutAuthentication(request);

    },
  });

Get the mission end point

// GET, WITH ASYNC THIRD-PARTY AUTH PREVALIDATION
  server.route({
    url: '/tasks',
    method: 'GET',
    // preValidation: (request) => customValidation(request, logger),
    handler: (request) => {
      request.body= {
        operation: 'sql',
        sql: 'select * from example_db.tasks'
      };

      /*
       * requestWithoutAuthentication bypasses the standard HarperDB authentication.
       * YOU MUST ADD YOUR OWN preValidation method above, or this method will be available to anyone.
       */
      return hdbCore.requestWithoutAuthentication(request);
    }
  });

All about helpers in custom jobs

In this we can implement our own custom validation using JWT.

In our ToDo React app on the user interface.

How to get the endpoint URL on the UI.

You can host a static web user interface

Your project must meet the details below to host your own Persistent UI

  • Index file located at /static/index.html
  • The correct path to any other files related to index.html
  • If your app uses client-side routing, it should be [project_name]/ static as its base (the base name of the interactive router, the base of the default router, etc.):
<Router basename="/dogs/static">
    <Switch>
        <Route path="/care" component={CarePage} />
        <Route path="/feeding" component={FeedingPage} />
    </Switch>
</Router>

The above example can be checked in HarperDB as well.

Custom job operations

There are 9 operations you can do in total:

  • Custom_functions_status(custom_functions_status)
  • get_custom_functions
  • get_custom_function
  • set_custom_function
  • Drop_custom_function
  • add_custom_function_project
  • drop_custom_function_project
  • package_custom_function_project
  • publish_custom_function_project

You can take a more in-depth look at each individual process in the HarperDB docs.

Server restart

For any changes you’ve made to your paths, helpers, or projects, you’ll need to restart the custom jobs server to see them take effect. HarperDB Studio does this automatically when you create or delete a project, or add, edit or edit a path or helper. If you need to start the custom jobs server by yourself, you can use the following process to do so:

{
    "operation": "restart_service",
    "service": "custom_functions"
}

This was for this blog.

I hope you learned something new today. If you did, please like/share so you can reach others as well.

.

Leave a Comment