Hasura has recently implemented a way to create custom mutations called Actions. Want to handle complex business logic? Actions are the way to go!
To create a new action, you need to provide a definition, handler (REST endpoint), and specify the kind – sync or async. When you run the custom GraphQL mutation, Hasura makes a POST request to the specified handler with the mutation arguments and the session variables. If you want to know more about the machinery behind it, check out the docs or our article introducing actions.
In this article, we're going to explore how Hasura and Dark work together by creating a custom mutation and implementing a handler for it using Dark. And all of that will be done without leaving the browser!
What is Dark?
Developing services is a pretty complicated job. Before you actually start writing code, you need to decide on stuff like hosting, CI/CD pipeline, language, and then you need to stitch it all together. Another problematic thing around it is deployment. Making a commit, creating PR, running CI, actual deploy — it's a lot. So what Dark aspires to do is to take this whole complexity away from us, and leave developers to only worry about writing code.
As being a setupless solution, Dark consists of language, editor, runtime, and infrastructure, so that you don't need to spend time figuring all of that on your own. What's more, ease of writing deployless backends is one of Dark's primary concepts, so while your writing your code, every change is instantly deployed to the cloud!
The language itself is described as a statically-typed functional/imperative hybrid, based loosely on ML. The dark compiler was written in OCaml, and syntax wise, you may spot a resemblance between these two languages.
With Dark, you write your code in a structured editor that makes sure you won't write syntactically incorrect code. There's no parser included, which means no syntax errors — with every keystroke, you modify the AST directly.
Creating an action
Online mafia game
Do you know the Mafia game? It's an old-timey party game in which the objective is for the mafia to kill off civilians until they are the majority, or the civilians to kill off the entire mafia. As the rules can be easily extended, players could be assigned with many different roles.
Yet, for our example, let's take only three: mafioso, civilian, and a doctor. There are also some constraints regarding roles:
There can be only one doctor.
The ratio between mafia players and all players should be around 1/3.
The minimum number of mafia players is 2.
For example, for eight players, there should be one doctor, two mafiosos, and five civilians.
When a new player enters a game, I want to assign him a role. However, how do I make sure the above constraints aren't violated? How do I know what characters are still available?
In real life, someone probably would need to go through all the cards and choose the characters based on the players count, and then deal the cards to the players.
But what about an online version of the game? There is no game master among the players and I can't have information about other players' roles on the frontend, because it's secret. That's when actions come into play! I'm going to create a custom mutation that performs the following logic:
Fetch all taken roles from the DB, along with the number of players in the game.
Based on already present characters, check which are still available.
From a set of available roles, choose a random one.
Insert a new user into the database.
New Hasura Action
The first thing to do is to define the mutation and all the required types. The definitions below mean:
The name of the custom mutation is CreateNewPlayer.
It accepts two arguments: a string nickname and game_id of type CreateNewPlayerUuid, which is a custom scalar.
The mutation returns CreateNewPlayerOutput, which consists of newly created player info.
Next, I need to provide an HTTP endpoint to which Hasura will make POST requests whenever CreateNewPlayer mutation is called. In my case, I'll put a link to my Dark canvass with a dummy route (yet to be created) called new_player.
As the last step, I'm saying that the kind of the mutation is Synchronous, which means that Hasura would keep the request open till receiving a response from the handler.
Creating REST endpoint in Dark
If you want to create an HTTP handler, you probably follow these four steps:
Agree upon request body parameters.
Write an implementation.
Make endpoint accessible.
Test it by sending a request.
Dark took a different approach. One of the core concepts of Dark is Trace-Driven-Development. It allows you to develop your backends from incoming requests. In other words, you can send a request to the endpoint that doesn't even exist. Then, based on the received request, you can implement your handler. Let's see it in action!
I'm going to call my newly created mutation.
It will result in an error because I haven't implemented it yet in Dark. But I can go to my Dark canvass, check the 404 section and see that Dark captured the request that Hasura for CreateNewPlayer action.
Now, by clicking + button, I can convert nonexistent HTTP endpoint into a real handler. The handler is automatically hosted for me. I can also check what exactly was sent to Dark in a request body.
Implementation
I will skip some implementation details related to determining a new role. You can find screenshots with code here.
As the first step, I'm making a request to Hasura to extract the information I need – all the roles that are already taken and the number of players. As you can see in the screenshot below, there are already three civilians and one doctor.
The next step is to randomly choose a role from all available roles.
As I have a new role for the player, now I can make a call to Hasura and insert a new player to the database.
All the implementation is done, so now I can start using CreateNewPlayer mutation 🎉
Connecting Action with the graph
The custom mutation I just created returns information about the newly created player. The next thing I'd need is information about the game and other players. But I don't want to make another call to Hasura. Instead, I want to get all the additional info by calling only CreateNewPlayer mutation.
In order to obtain that, I'm going to modify the definition, so that it also returns gameId and create a new relationship between the Action and game table. I also need to return gameId from the handler.
Thanks to the new relationship, I can now fetch all the data about the game I want! I can fetch info about the players associated with the same game as well.
Summary
We explored how to add custom business logic to Hasura in a few steps, learned how to take advantage of the Trace Driven Development, and how quickly we can get a rest endpoint up and running with Dark. Moreover, we added a custom mutation with Hasura Actions, and took things to the next level by creating an relationship and connecting it to the graph!