Employee leave maintenance using hasura actions ( part-2)

Hasura makes it super easy to scaffold a inquiry or mutation given a database model . In first part we looked at how hasura makes it easy to develop a small leave maintenance application with minimum coding effort . We just need to do a design for database modelling and most of our backend code is ready , voila 🔅 . Refer to first part over here .

However aEnterprise application requires more than simply inserting into a database model . Data validation is super critical . It would be important to check input data for correctness else database will get riddled with incorrect data. Some of the scenarios to consider from leave application point of view are

  • What if user has chosen leave from date which is of later date than leave to date .
  • What if user has chosen leave to date which is on weekend .
  • What if user does not have correct leave balance to apply for the week .

Surely some of these validations can be done on the client side , however you would prefer to do most of the validation work in api ( at server side ) . Tomorrow , if a third party application connects to leave application backend , then all these validations which will ensure that user can not create invalid leave requests.

Hasura action

Hasura action provides a graphql (mutation or query interface) which is user defined . This user defined graphql mutation , then serves as front api facing a web hook . Inside a web hook , developer has a freedom to do following

  • we can perform business validations
  • Call auto generated mutation or auto generated query.
Hasura action flow

Hasura action flow

  • User interface calls the hasura actions ,labelled as (1) in the diagram.
  • Hasura graphql server calls a web hook (2), which is configured for action . We can consider web hook as REST end point
  • REST hook implementation is completely under developers control .
  • In Web hook , developer will perform any validation and finally will call auto generated mutation or auto generated query of hasura (3)
  • Auto generated mutation are nothing but mutation which are available out of box in hasura ( i.e mutation which get generated when a you create a table create using hasura console

A concrete example

If we consider leave application as example , leave validation and application is good example to implement using hasura action .

Head to hasura console , go to Actions tab , add following action

  • Action is based on mutation . name of the action leaveValidateAndApply
  • Input type is LeaveInput . Definition of input type is to entered in new types definition . It is a complete user defined type and developer has full control over inputs and its mandatory ness .
  • Output type is LeaveOutput . Action will return structure which has one value ( message ) . Please note that in case of error , webhook or action handler is supposed to return status code as 4xx and error structure with message and optionally a code representing error situation . Output type LeaveOutput is not relevant due to error condition

handler represents the web hook , action handler . Hasura graphql server will forward the request to the handler.

Below is the url of the web hook . ACTION_BASE_ENDPOINT is environment variable .ACTION_BASE_ENDPOINT can be defined conveniently in docker-compose.yaml

{{ACTION_BASE_ENDPOINT}}/api/leaveApply

docker-compose.yaml

Let us look at the docker-compose.yaml. There are 3 services defined here

  • hasura graphql server : Hasura graphql engine , will require to understand where the action application is running . This is controlled by environment variable , ACTION_BASE_ENDPOINT . Please note that action application in this case is a application running on port 3000 . host.docker.internal is way to call application running in localhost . Since i am running graphql inside docker and actions are without docker . i need to use a special syntax (host.docker.internal ) in mac machine to reach application running localhost.
ACTION_BASE_ENDPOINT : http://host.docker.internal:3000
  • postgres database : postgres database is also launched through a docker image (image: postgres:12)
  • Actions application ( i,e implementation of webhook) is implemented as seperate node.js service . Action service is launched through a docker image (leave-action-app/experimental:0.1) . Actions service is started on port 3000. Please note that , it is started on the expected port as per the set up done for graphql engine ( ACTION_BASE_ENDPOINT) . This way graphql engine can forward the request to actions REST end point .

docker-compose.yaml

version: '3.6'
services:
postgres:
image: postgres: 12
ports:
-"5432:5432"
restart: always
volumes:
-db_data: /var/lib / postgresql / data
environment:
POSTGRES_PASSWORD: postgrespassword
graphql - engine:
image: hasura / graphql - engine: v1 .2 .2
ports:
-"8080:8080"
depends_on:
-"postgres"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres: //postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
# set to "false"
to disable console
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http - log, webhook - log, websocket - log, query - log
HASURA_GRAPHQL_DEV_MODE: "true"
ACTION_BASE_ENDPOINT: http: //host.docker.internal:3000
# # uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
hasura - actions:
image: leave - action - app / experimental: 0.1
ports:
-"3000:3000"
restart: always
environment:
GRAPHQL_HOST: http: //host.docker.internal:8080/v1/graphql
volumes:
db_data:

Implmenting action code

For the leave application , action code is simple node.js express server which does validation of input data through graphql queries and performs final mutation using graphql mutation

Give a special attention to input . all input variables are available under input structure (e.g req.body.input.arg1.fromDate)

Web hook code

// POST /api/leaveApply gets JSON bodies
app.post('/api/leaveApply', jsonParser, async function(req, res) {
// check for from date and to date
let from_date = req.body.input.arg1.fromDate;
let to_date = req.body.input.arg1.toDate;
let type_of_leave = req.body.input.arg1.typeOfLeave;
let reason = req.body.input.arg1.reason;
let user_id = req.body.input.arg1.userId;
let retObj = {};
console.log("from date " + from_date);
console.log("to date " + to_date);
console.log("type of leave " + type_of_leave);
try {
//check if from_date is later than to_date
retObj = await val.validate_and_apply_leave(from_date, to_date, user_id, type_of_leave, reason);
console.log("return value " + JSON.stringify(retObj));
if (retObj.error.code != "") {
res.status(422).json(retObj.error);
} else {
// port to success message format .
let finalObj = {};
finalObj.message = retObj.message;
res.json(finalObj);
}
} catch (e) {
console.log("System Error in leave application" + e);
res.status(500).send(e.message);
}
});

Handling output and error

Success scenario : In case of success scenarios , output LeaveOut needs to have message member . Look at the else part of the code . finalObj which is returned as response has message element inside it .

Error scenario : In case of error scenario , hasura expects a 4xx response . We are returning 422 response with (error ) code and message.

try {
//check if from_date is later than to_date
retObj = await val.validate_and_apply_leave(from_date, to_date, user_id, type_of_leave, reason);
console.log("return value " + JSON.stringify(retObj));
if (retObj.error.code != "") {
res.status(422).json(retObj.error);
}
else {
// port to success message format .
let finalObj = {};
finalObj.message = retObj.message;
res.json(finalObj);
}
}

Deep inside action code

  • Do date validations and other business validation using moment library ( e.g from date less than to date )
  • Check whether user has sufficient leave balance . This validation requires hasura query (leave_app_employee_leavebal) to check for leave balance. More details of the query will come below . So hasura actions can combine power of programming and hasura scaffolded queries in way to create a more powerful graphql end point.
let moment = require('moment');
let work = require('./work_days');
let util = require('../util/util');
/* Api to perform leave validation and do the following
1. check whether leave is falling on holiday .
2. check whether there is sufficient leave balance
3. Apply the leave , update leave tables
4. update the leave balance
*/
async function validate_and_apply_leave(from_date, to_date, user_id, leave_type, reason) {
let retObj = { error: {} };
let leaves_requested = 0 ;
let leaves_available = 0;
//check if from_date is later than to_date
if (moment(from_date).isAfter(to_date)) {
console.log("invalid from date");
retObj.error.message = `Leave from date [ ${from_date} ] needs to earlier than leave to date [ ${to_date} ] `;
retObj.error.code = 'E0001';
}
else {
// check if leave from date falls on weekend.
console.log("checking for weekend for from date");
retObj.error = validate_date_weekend(from_date, " from date");
if (retObj.error.code !== "")
return retObj;
/* check if leave to date falls on weekend.
if error , return
*/
console.log("checking for weekend for to date");
retObj.error = validate_date_weekend(to_date, "to date");
if (retObj.error.code !== "")
return retObj;
// check for leave balance before proceeding
retObj = await check_leave_balance(from_date, to_date, user_id, leave_type);
if (retObj.error.code == "") {
// save leave requested and available , will get overwritten in next call
leaves_requested = retObj.leaves_requested ;
leaves_available = retObj.leaves_available;
console.log(" leave requested "+leaves_requested + " available "+leaves_available);
// no error insert leave application
retObj = await apply_leave(from_date, to_date, user_id, leave_type, reason, leaves_requested);
// no error update leave balance
if(retObj.error.code == "")
retObj = await leave_bal_update(user_id, leaves_requested, leaves_available,leave_type);
}
}
// If we have come so far , and error is null , leave application is success
if ( retObj.error.code == "" ) {
retObj.message = `Leave application (${leaveDescription(leave_type)}) successful from ${from_date} to ${to_date} for ${leaves_requested} day ,current leave balance ${retObj.curBal}`;
retObj.error = { code: '' };
}
console.log("validate_and_apply_leave " + JSON.stringify(retObj));
enrichErrors(retObj,from_date,to_date,leave_type);
return retObj;
}

Using hasura queries to check leave balance

/* function calculates the working days between from date and to date 
* checks also leave balance for the user id.
* if balance is insufficient , it will send error back
*/
async function check_leave_balance(from_date ,to_date ,user_id ,leave_type) {
let retObj = {
error: { code: '' }
};
let leave_objs = await work.work_days(from_date, to_date, user_id);
console.log("Leave's requested " + leave_objs.leaves_requested);
let leaves_available = await fetch_leaves(user_id, leave_type);
console.log("Leave's available " + leaves_available);
if (leave_objs.leaves_requested > leaves_available) {
retObj.error.code = 'E0003';
retObj.error.message = `You do not have sufficient leave (${leave_type}) balance available ,leave requested [ ${leave_objs.leaves_requested}] , Leave available [ ${leaves_available} ]`;
return retObj;
}
else {
retObj.leaves_requested = leave_objs.leaves_requested;
retObj.leaves_available = leaves_available;
}
return retObj;
}async function fetch_leaves(user_id, leave_type) {
let query = util.getGraphQLQueryStr('src/graphql/get_user_leave_bal.gql');
let variables = {
userId: user_id
};
let response = await util.executeGraphQLQuery(query, variables);
if (response.data && response.data.leave_app_employee_leavebal) {
for (let leave_rec of response.data.leave_app_employee_leavebal) {
if (leave_rec.leave_type === leave_type) {
return leave_rec.bal;
}
}
}
return 0;
}

leave balance query (get_user_leave_bal.gql)

  • Note this is auto generated query from hasura . We do not require any implementation for resolvers .
query leave_balance($userId: Int) {
leave_app_employee_leavebal(where: {
emp_id: {
_eq: $userId
}
}) {
bal
leave_type
emp_id
}
}

Code repository

Hasura actions code for leave application

https://github.com/tanmaypatil/leave-app-actions

Summary

  • Hasura actions make it easy for developers to incorporate custom business logic and validations inside your application .
  • It is way to combine a hasura power of auto generated graphql and program power.

Interests : software design ,architecture , search, open banking , machine learning ,mobility