Implementing a Hasura GraphiQl Infrastructure

Build an AirBnB clone app with GraphiQl REST endpoints on the Hasura platform.

This article is a submission for the Hasura-Hashnode hackathon.

Well, here we go. We are going to build a simple Flutter app that will interact with Hasura Graphiql infrastructure. Hasura provides a robust and stable backend for applications with seamless integration. By simple app I mean, lets make a clone of the AirBnB app and implement its major features. We are going to include the following:

  • Sign up and Login

  • Adding of new Spaces.

  • Exploring spaces that have been added

  • Booking of a space

Here`s a breakdown of this article:

  1. Database design

  2. Database, Table Schema and Creation

  3. Authorization and Permissions

  4. GraphiQl queries and REST api

  5. Making Api calls from our app

Database design

Our database will have 3 tables namely: app_users, spaces and bookings.

Our database rules include:

  • Only a registered user can add or book a space.

  • A user can have multiple spaces enlisted.

  • A user can only book a single space at a time.

With that in mind lets create our database.

Database, Table Schema and Creation

Please checkout this link for more details and explanations on how to setup the Hasura platform. After completing the above setup, we can now create our tables.

Here is the structure of our app_users table. user.png Set the id column as the primary key as shown below prime.png

Next we create the spaces table, which will have a foreign key linking a row to a specific user. Also set the id column as the primary key as shown previously. spaces.png spac.png

Finally we create the bookings table, which will have two foreign keys. One to reference the app_users table and the other to reference the spaces table. Also set the id column as the primary key as shown previously. bookings.png userb.png

spaceb.png

Authorization and Permissions

Permissions are used to enforce who has what kind of access to our data and what operations they can do with the data. For our app we have two permission roles: public and user .

public

  • Only insert and select a user.

  • Only select enlisted spaces

    user

  • Has full access to database

Head over to the app_users table and click on the permissions tab. Currently there should be a single role which is admin. The admin role has full access to the database.

Add a new role public with the following permissions for insert and select:

insert.png

select.png

Head over to the spaces table and onto the Permissions tab. The new role public will have been created already with all permissions denied. Make the following changes on the select permissions.

inssss.png

Next we add the user role. Click on the Permissions tab on any of the tables. Add the new role with full access to insert, select, delete and update operations. Perform this action on all tables. You should have something close to this afterwards:

pppp.png

GraphiQl queries and REST api

Register new user Here is our graphiql mutation to register a new user. A user can choose to use their phone number or email address to register on the app. With that in mind, we need to create two mutations to handle that.

Phone Registration

mutation MyMutation($phone: numeric,$first_name: String,$last_name: String){
  insert_app_users_one(object: {first_name: $first_name, last_name: $last_name, phone_number: $phone}) {
    created_at
    email_address
    first_name
    last_name
    phone_number
    updated_at
    id
  }
}

Email Registration

mutation MyMutation($email: String,$first_name: String,$last_name: String){
  insert_app_users_one(object: {email_address: $email, first_name: $first_name, last_name: $last_name}) {
    created_at
    email_address
    first_name
    last_name
    phone_number
    updated_at
    id
  }
}

With the mutations above we can create REST endpoints at this location register-app-user-email and register-app-user-phone which will accept POST requests. With that we can gracefully register new users.

Login a user

Since the email address and phone number are unique, any of them can be passed from the client and when a record is matched we return a user object. We can then create a REST endpoint to access this query at login-app-user which will accept POST requests.

query MyQuery($email: String,$phone: numeric) {
  app_users(limit: 1, where: {
_or: [
{email_address: {_eq: $email}},
{phone_number: {_eq: $phone}}
]
  }) {
    email_address
    created_at
    first_name
    id
    last_name
    phone_number
    updated_at
  }
}

Adding a New Space

Any logged in user can create a new space which other users can explore. To enable this we setup a REST endpoint at new-space which will execute the following mutation.

mutation MyMutation($name:String,$app_user_id:Int,$cost_per_night:money,$description:String,$images:String,$latitude:String,$longitude:String,$location:String,$availability:Boolean) {
  insert_spaces_one(object: {app_user_id: $app_user_id, cost_per_night: $cost_per_night, description: $description, images: $images, latitude: $latitude, location: $location, longitude: $longitude, name: $name,availability:$availability}) {
    app_user_id
    availability
    name
    longitude
    location
    latitude
    images
    id
    description
    created_at
    cost_per_night
  }
}

Explore Page

The apps explore page displays spaces that have been created by all users and are available for booking. We also set a limit on the number of spaces we will get back. Lets setup a REST endpoint at explore-spaces which will give us a result after executing the following query.

query MyQuery {
  spaces(limit: 20, where: {availability: {_eq: true}}) {
    name
    longitude
    location
    latitude
    images
    id
    description
    created_at
    cost_per_night
    availability
    app_user_id
    app_user {
      email_address
      first_name
      last_name
      phone_number
    }
  }
}

Check Space availability and User eligibility

The query below should return a single space if its available or an empty array if none is found. The bookings array should be empty for the user to be eligible.

query MyQuery($space_id:Int,$date:date) {
  spaces(where: {id: {_eq: $space_id}, availability: {_eq: true}}) {
    id
  }
  bookings(where: {end_stay: {_gte: $date}}) {
    id
  }
}

Book a Space

The mutation below inserts a single row on the bookings table and also updates the space availability status.

mutation MyMutation($app_user_id: Int, $cost: money, $end_stay: date, $space_id: Int!, $start_stay: date) {
  insert_bookings_one(object: {app_user_id: $app_user_id, cost: $cost, end_stay: $end_stay, paid: false, space_id: $space_id, start_stay: $start_stay}) {
    id
    end_stay
    app_user_id
    cost
    created_at
    paid
    space_id
    start_stay
  }
  update_spaces_by_pk(pk_columns: {id: $space_id}, _set: {availability: false}) {
    id
  }
}

Making Api calls

The functions making the api calls will be built as below. Please checkout this Repository for the rest of the code implementation.


  @override
  Future<dynamic> login({required String? phone,required String? email}) async {
    log.i('$phone $email');
    return await _post(loginUri, body:jsonEncode({
      'email':email ?? '',
      'phone':phone?? 0
    }) , headers: {'X-Hasura-Role': 'public'});
  }

  @override
  Future<dynamic> signup(
      {required String phone,
      required String email,
      required String fName,
      required String lName}) async {

   return await _post(
    email.isNotEmpty ? signupUriEmail : signupUriPhone,
      body:jsonEncode(
     email.isNotEmpty ?   {
      'email':email,
      'first_name':fName,
      'last_name':lName
    } :  {
      'phone':phone,
      'first_name':fName,
      'last_name':lName
    }
    ) , headers: {'X-Hasura-Role': 'public'});
  }

  @override
  Future addSpace({required ListedSpace space}) async {
    return await _post(
    newSpaceUri,
      body:jsonEncode(space.toJson()) , headers: {'X-Hasura-Role': 'user'});
  }

  @override
  Future exploreSpaces() async {
    return await _post(
      exploreSpacesUri,
      body:'' , headers: {'X-Hasura-Role': 'user'});
  }

  @override
  Future checkSpace({required spaceId, required date}) async {
    return await _post(
      checkSpacesUri,
      body:jsonEncode({
      'space_id':spaceId,
      'date':date,
    }), headers: {'X-Hasura-Role': 'user'});
  }

  @override
  Future bookSpace({required BookSpace space}) async {
    return await _post(
    bookSpacesUri,
      body:jsonEncode(space.toJson()) , headers: {'X-Hasura-Role': 'user'});
  }

Thats all for today guys. I hope you enjoyed reading this as I did writing and exploring the Hasura GraphiQl infrastructure. Please share this article and also leave a comment if you have any🧿.

Here is the link to the Flutter implementation repository.

Here is a quick video of how it works.

Goodbye👋👋👋