Using JWTs for GraphQL Authorization with Hasura

Using JWTs for GraphQL Authorization with Hasura

Learn about the Hasura platform and how their GraphQL APIs can be secured using JSON Web Tokens

Learn about the Hasura platform and how their GraphQL APIs can be secured using JSON Web Tokens

If you’re developing an API today for consumption by third-parties, you’d be more than forgiven for choosing the REST interface to develop the API, as it is a tried and tested standard for delivering data to consumers, as well as accepting changes to the data in a structured way. In more recent times, GraphQL has become an attractive alternative for developing APIs thanks to its flexible, semantic, and performant nature.

But what if you’d like finer control over the details of the data exchange, including integration with third-party authorization servers using JWTs, and being able to quickly and easily set role-based permissions at the field level?

That’s what Hasura have achieved with their GraphQL platform that generates a GraphQL API over a PostgreSQL database. To highlight just a few of the features:

[“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”> [“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”## A Short Introduction to JSON Web Tokens

From jwt.io:

[“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”
What this looks like in practice is a string that is made up of three parts separated by a period:

A JWT is issued as a result of a user authenticating with an authorization server. In the case of the Hasura platform, this JWT can then be used to authorize the request and as such can make queries and mutations depending on the claims present in the token. We will see how this works in the demo a little bit later in the article.

The compact nature of a JWT makes it easy to pass around in query strings, headers, and request bodies, or store in a cookie.

What is GraphQL? An Overview

GraphQL is essentially a query language that allows you to define your API in terms of types. It is datastore-agnostic and has server-side support for many different platforms and languages. Instead of defining your API in terms of status codes and semantic URLs, you define the types and then construct queries using a familiar but powerful syntax. As an example:

{
  article {
    id,
    title,
    author {
      name
    }
  }
}


This describes a query for articles, returning the id, title, and the author’s name. As you can see, it supports nested database objects (the author is another table in the schema, linked to from an article). Mutations can also be defined, which describe how to change data.

In addition, multiple queries can be made in a single request, which is most useful when a client wants to retrieve lots of possibly unrelated data points from the server.

The Hasura Approach

The Hasura platform takes advantage of these technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.

The Hasura console allows the user to create a database, define its schema, and populate it with data. Hasura will then generate a GraphQL API on top of this database, automatically generating the appropriate types, queries, and mutations that allow the client to completely query for and manipulate the underlying data.

Hasura takes a role-based approach to authorization, and as such an individual role’s access can be narrowed down to only a subset of the overall schema (even down to the field level). As each role is mapped to certain authorization rules, it can even be prevented from running certain mutations if so desired. Furthermore, queries can be delegated to an upstream service based on the claims present in the authorization token.

Custom claims inside the JWT are used to tell Hasura about the role of the caller, so that Hasura may enforce the necessary authorization rules to decide what the caller can and cannot do.

[Source]

An example of the claims used by Hasura may look like the following:

{
  "https://hasura.io/jwt/claims": {
    "x-hasura-default-role": "anonymous",
    "x-hasura-allowed-roles": [
      "user",
      "anonymous"
    ]
}


These two keys define the possible roles as well as the default role in the absence of the “X-Hasura-Role” header. In the case of Auth0, these custom claims can be put in place using Rules, a platform feature that can be used to dynamically enhance tokens as users are authenticated.

Demo: Querying Data

Let’s have a look at how authorization with Hasura and JWTs works in practice. A demo has been set up that will allow you to construct queries and see the role-based permissions system in action.

This demo has been pre-configured with a database that has authors, articles, tags, and a relationship table that joins many articles to many tags. Let’s see how we can query for some of this data.

Executing anonymous queries

Browse to https://auth0-hasura-demo.herokuapp.com, and use “hasurademo” as the access key.

Next, you will be presented with the Hasura console:

The interface gives you the opportunity to specify some request headers that will be used when querying the GraphQL interface. To demonstrate how the user’s role affects their ability to query and modify data, remove the X-Hasura-Access-Key header by clicking on the cross on the right-hand side, and enter a new header X-Hasura-Role with a value of “anonymous”:

Now, let’s make a query. Use the following query inside the Graphiql query entry area in the lower half of the screen:

{
  article {
    id,
    title
  }
}


To run the query, press Ctrl+Enter (Cmd+Enter if you’re on a Mac), or click on the large ‘play’ button above the query panel. You will see the results of the query on the right-hand side of the screen; you have successfully managed to retrieve the results for two articles that are currently in the database!

Now let’s try a mutation. Alter the query so that it executes the following mutation:

mutation {
  insert_tag(objects:[{name:"<YOUR TAG NAME HERE>"}]) {
    returning {
      name
    }
  }
}


This is a mutation that inserts a new tag with your specified tag name into the database. You should replace the value <YOUR TAG NAME HERE> with a value of your choosing.

Running this mutation right now will return an error, letting you know that the mutation does not exist.

This is correct since the permissions have effectively been set up so that the anonymous role cannot make changes to the database. This is also indicated by the GraphQL API docs, as it does not show any mutations that can be called by this role. To see this, click the “< Docs” button on the right-hand side of the query window to open the API documentation.

Only queries and subscriptions are available as root types here, but no mutations.

Let’s have a closer look at how permissions are set within the Hasura interface.

Validating query permissions

To view the permissions matrix for the “tag” table:

  1. Click the “data” tab at the top of the screen
  2. Click the “tag” table on the left-hand side
  3. Click the “permissions” tab in the main part of the interface

You will arrive at a matrix that describes all the role permissions for the “tag” table. On this screen we can see a number of things:

You can see here that, since the “anonymous” user cannot modify or insert any data, it makes sense that there are no mutations available.

Let’s now switch to the “user” role and see how we can make this mutation work.

Executing Authorized Queries

Click the “Graphiql” heading at the top of the screen to return to the query interface. Now, change the “X-Hasura-Role” request header so that its value is “user”.

If you try to run the mutation again now, you will notice that it will fail for the same reason as before. This is because the “user” role needs to also provide a valid JWT in order to be properly authorized to run this mutation.

[“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”> [“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”### Generating a valid JWT

For the purposes of this demo, a tool has been provided to make it easy for you to generate a JWT and run this mutation.

Browse to https://auth0-hasura-demo.now.sh/ and log in with your Auth0 account. If you do not currently have one, you can register for a free account.

[“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”

Once logged in, you will be able to see your token. Select the entire token string and press Ctrl+C (or Cmd+C on a Mac) to copy it into your clipboard.

Before we head back to the Hasura console, let’s inspect the token to see if it contains the claims that we inspect. Head to http://jwt.io, scroll down and paste the token into the left-hand side of the screen. The right-hand side will automatically update to show you the data contained within the token, and you should be able to see that the correct Hasura claims have been included:

With the token still in your clipboard, head back to the Hasura console. The way we attach this token to the request is through the standard “Authorization” request header.

Add a new header into the “headers” section, specifying the Authorization header and the JWT that you have in your clipboard as the bearer token:

Next, try running the mutation again. You should find that the query now works and that the name of the tag is being played back to you in the response:

[“The Hasura platform takes advantage of JWT and GraphQL technologies to provide a platform for querying and manipulating data in a PostgreSQL database, taking into account the role specified in the authorization token.”
Before we head away from this screen, let’s expand the API docs again to show that the “mutations” root now appears and that you can browse through all the available mutations given that you are now authorized to do so:

Finally, to prove that the mutation has worked we can explore the raw data in the database. Once again, click the “Data” heading at the top of the screen and then click on the “tags” table on the left.

The resulting screen will show you the data that is present in the table, and you can see that the “example-tag” entry that was created by the mutation is there.

Wrapping Up

In this post, you learned a bit about GraphQL and the main differences from a traditional REST API. You also had an introduction to JSON Web Tokens, how they are made up, and how easy they are to pass around.

Next, you discovered how Hasura tackles the idea of a role-based architecture when it comes to securing GraphQL APIs, and how it uses JWTs to authorize incoming requests and grant permissions to specific areas of the schema, even down to the field level.

Finally, you had a practical look at the Hasura platform, learning how to perform anonymous queries, and how to perform authorized mutations using a JWT that was issued to you by Auth0.

Learn More

Build a Simple Web App with Express, Angular, and GraphQL

GraphQL Tutorial: Understanding Spring Data JPA/SpringBoot

How to set up GraphQL with Golang: a deep dive from basics to advanced

Build a Health Tracking App with React, GraphQL, and User Authentication

The Modern GraphQL Bootcamp (Advanced Node.js)

NodeJS - The Complete Guide (incl. MVC, REST APIs, GraphQL)

GraphQL with React: The Complete Developers Guide

GraphQL with Angular & Apollo - The Full-stack Guide

GraphQL: Learning GraphQL with Node.Js

Complete guide to building a GraphQL API

GraphQL: Introduction to GraphQL for beginners

Mobile App Development Company India | Ecommerce Web Development Company India

Mobile App Development Company India | Ecommerce Web Development Company India

Best Mobile App Development Company India, WebClues Global is one of the leading web and mobile app development company. Our team offers complete IT solutions including Cross-Platform App Development, CMS & E-Commerce, and UI/UX Design.

We are custom eCommerce Development Company working with all types of industry verticals and providing them end-to-end solutions for their eCommerce store development.

Know more about Top E-Commerce Web Development Company

Practical Rust Web Development - GraphQL

Practical Rust Web Development - GraphQL

Practical Rust Web Development / GraphQL - According to the official homepage, GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data...

Originally published by Werner Echezuría  at dev.to

GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

One advantage of GraphQL is the flexibility it provides, in one query you can obtain everything you need allowing easy maintenance of the code over time and easier communication between the server and the client.

Juniper is a crate that allows the creation of a GraphQL server, we'll be using it in our project.

Let's continue with our online store, we're going to need to control our sales in the site, so, let's create a sales module that will receive a GraphQL query.

migrations/2019-07-28-191653_add_sales/up.sql:

CREATE TABLE sales (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  sale_date DATE NOT NULL,
  total FLOAT NOT NULL
);

CREATE TABLE sale_products (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
sale_id INTEGER NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
amount FLOAT NOT NULL,
discount INTEGER NOT NULL,
tax INTEGER NOT NULL,
price INTEGER NOT NULL, --representing cents
total FLOAT NOT NULL
)

Then, we're gonna need the endpoint that will receive all our queries, this would be a post request.

src/graphql.rs:


pub fn graphql(
st: web::Data<Arc<Schema>>,
data: web::Json<GraphQLRequest>,
user: LoggedUser,
pool: web::Data<PgPool>
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || {
let pg_pool = pool
.get()
.map_err(|e| {
serde_json::Error::custom(e)
})?;

    let ctx = create_context(user.id, pg_pool);

    let res = data.execute(&amp;st, &amp;ctx);
    Ok::&lt;_, serde_json::error::Error&gt;(serde_json::to_string(&amp;res)?)
})
.map_err(Error::from)
.and_then(|user| {
    Ok(HttpResponse::Ok()
        .content_type("application/json")
        .body(user))
})

}

src/main.rs:

    HttpServer::new(
move || App::new()
.service(
web::resource("/graphql").route(web::post().to_async(graphql))
)

In order to output a response that reads the data we need a query, but if we need to modify state we'll be needing a mutation, let's add both resources in our sales module.

src/models/sale.rs:

use diesel::PgConnection;
use diesel::BelongingToDsl;
use diesel::sql_types;
use chrono::NaiveDate;
use juniper::{FieldResult};
use crate::schema;
use crate::schema::sales;
use crate::schema::sale_products;
use crate::db_connection::PgPooledConnection;
use crate::models::product::{ Product, PRODUCT_COLUMNS };
use crate::errors::MyStoreError;

#[derive(Identifiable, Queryable, Debug, Clone, PartialEq)]
#[table_name="sales"]
#[derive(juniper::GraphQLObject)]
#[graphql(description="Sale Bill")]
pub struct Sale {
pub id: i32,
pub user_id: i32,
pub sale_date: NaiveDate,
pub total: f64,
pub bill_number: Option<String>
}

#[derive(Insertable, Deserialize, Serialize, AsChangeset, Debug, Clone, PartialEq)]
#[table_name="sales"]
#[derive(juniper::GraphQLInputObject)]
#[graphql(description="Sale Bill")]
pub struct NewSale {
pub id: Option<i32>,
pub sale_date: Option<NaiveDate>,
pub user_id: Option<i32>,
pub total: Option<f64>,
pub bill_number: Option<String>
}

use crate::models::sale_product::{ SaleProduct, NewSaleProduct, NewSaleProducts, FullSaleProduct,FullNewSaleProduct };

#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct FullSale {
pub sale: Sale,
pub sale_products: Vec<FullSaleProduct>
}

#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct FullNewSale {
pub sale: NewSale,
pub sale_products: Vec<FullNewSaleProduct>
}

#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct ListSale {
pub data: Vec<FullSale>
}

use std::sync::Arc;

pub struct Context {
pub user_id: i32,
pub conn: Arc<PgPooledConnection>,
}

impl juniper::Context for Context {}

pub struct Query;

type BoxedQuery<'a> =
diesel::query_builder::BoxedSelectStatement<'a, (sql_types::Integer,
sql_types::Integer,
sql_types::Date,
sql_types::Float8,
sql_types::Nullable<sql_types::Text>
),
schema::sales::table, diesel::pg::Pg>;

impl Sale {
fn searching_records<'a>(search: Option<NewSale>) -> BoxedQuery<'a> {
use diesel::QueryDsl;
use diesel::ExpressionMethods;
use crate::schema::sales::dsl::*;

    let mut query = schema::sales::table.into_boxed::&lt;diesel::pg::Pg&gt;();

    if let Some(sale) = search {
        if let Some(sale_sale_date) = sale.sale_date {
            query = query.filter(sale_date.eq(sale_sale_date));
        }
        if let Some(sale_bill_number) = sale.bill_number {
            query = query.filter(bill_number.eq(sale_bill_number));
        }
    }

    query
}

}

#[juniper::object(
Context = Context,
)]
impl Query {

fn listSale(context: &amp;Context, search: Option&lt;NewSale&gt;, limit: i32) 
    -&gt; FieldResult&lt;ListSale&gt; {
        use diesel::{ QueryDsl, RunQueryDsl, ExpressionMethods, GroupedBy };
        use crate::models::sale_product::SaleProduct;
        let conn: &amp;PgConnection = &amp;context.conn;
        let query = Sale::searching_records(search);

        let query_sales: Vec&lt;Sale&gt; =
            query
                .filter(sales::dsl::user_id.eq(context.user_id))
                .limit(limit.into())
                .load::&lt;Sale&gt;(conn)?;

        let query_products = 
            schema::products::table
                .inner_join(schema::sale_products::table)
                .select((PRODUCT_COLUMNS, 
                        (schema::sale_products::id, 
                         schema::sale_products::product_id, 
                         schema::sale_products::sale_id, 
                         schema::sale_products::amount,
                         schema::sale_products::discount,
                         schema::sale_products::tax,
                         schema::sale_products::price,
                         schema::sale_products::total)))
                .load::&lt;(Product, SaleProduct)&gt;(conn)?;

        let query_sale_products = 
            SaleProduct::belonging_to(&amp;query_sales)
                .inner_join(schema::products::table)
                .select(((schema::sale_products::id, 
                         schema::sale_products::product_id, 
                         schema::sale_products::sale_id, 
                         schema::sale_products::amount,
                         schema::sale_products::discount,
                         schema::sale_products::tax,
                         schema::sale_products::price,
                         schema::sale_products::total),
                         PRODUCT_COLUMNS))
                .load::&lt;(SaleProduct, Product)&gt;(conn)?
                .grouped_by(&amp;query_sales);

        let tuple_full_sale: Vec&lt;(Sale, Vec&lt;(SaleProduct, Product)&gt;)&gt; = 
            query_sales
                .into_iter()
                .zip(query_sale_products)
                .collect::&lt;Vec&lt;(Sale, Vec&lt;(SaleProduct, Product)&gt;)&gt;&gt;();

        let vec_full_sale = tuple_full_sale.iter().map (|tuple_sale| {
            let full_sale_product = tuple_sale.1.iter().map(|tuple_sale_product| {
                FullSaleProduct {
                    sale_product: tuple_sale_product.0.clone(),
                    product: tuple_sale_product.1.clone()
                }
            }).collect();
            FullSale {
                sale: tuple_sale.0.clone(),
                sale_products: full_sale_product
            }
        }).collect();

        Ok(ListSale { data: vec_full_sale })
    }

fn sale(context: &amp;Context, sale_id: i32) -&gt; FieldResult&lt;FullSale&gt; {
    use diesel::{ ExpressionMethods, QueryDsl, RunQueryDsl };

    let conn: &amp;PgConnection = &amp;context.conn;
    let sale: Sale =
        schema::sales::table
            .filter(sales::dsl::user_id.eq(context.user_id))
            .find(sale_id)
            .first::&lt;Sale&gt;(conn)?;

    let sale_products = 
        SaleProduct::belonging_to(&amp;sale)
            .inner_join(schema::products::table)
            .select(((schema::sale_products::id, 
                        schema::sale_products::product_id, 
                        schema::sale_products::sale_id, 
                        schema::sale_products::amount,
                        schema::sale_products::discount,
                        schema::sale_products::tax,
                        schema::sale_products::price,
                        schema::sale_products::total),
                        PRODUCT_COLUMNS))
            .load::&lt;(SaleProduct, Product)&gt;(conn)?
            .iter()
            .map(|tuple| {
                FullSaleProduct {
                    sale_product: tuple.0.clone(),
                    product: tuple.1.clone()
                }
            })
            .collect();
    Ok(FullSale{ sale, sale_products })
}

}

pub struct Mutation;

#[juniper::object(
Context = Context,
)]
impl Mutation {

fn createSale(context: &amp;Context, param_new_sale: NewSale, param_new_sale_products: NewSaleProducts) 
    -&gt; FieldResult&lt;FullSale&gt; {
        use diesel::{ RunQueryDsl, Connection, QueryDsl };

        let conn: &amp;PgConnection = &amp;context.conn;

        let new_sale = NewSale {
            user_id: Some(context.user_id),
            ..param_new_sale
        };

        conn.transaction(|| {
            let sale = 
                diesel::insert_into(schema::sales::table)
                    .values(new_sale)
                    .returning(
                        (
                            sales::dsl::id,
                            sales::dsl::user_id,
                            sales::dsl::sale_date,
                            sales::dsl::total,
                            sales::dsl::bill_number
                        )
                    )
                    .get_result::&lt;Sale&gt;(conn)?;

            let sale_products: Result&lt;Vec&lt;FullSaleProduct&gt;, _&gt; =
                param_new_sale_products.data.into_iter().map(|param_new_sale_product| {
                    let new_sale_product = NewSaleProduct {
                        sale_id: Some(sale.id),
                        ..param_new_sale_product.sale_product
                    };
                    let sale_product =
                        diesel::insert_into(schema::sale_products::table)
                            .values(new_sale_product)
                            .returning(
                                (
                                    sale_products::dsl::id,
                                    sale_products::dsl::product_id,
                                    sale_products::dsl::sale_id,
                                    sale_products::dsl::amount,
                                    sale_products::dsl::discount,
                                    sale_products::dsl::tax,
                                    sale_products::dsl::price,
                                    sale_products::dsl::total
                                )
                            )
                            .get_result::&lt;SaleProduct&gt;(conn);

                    if let Some(param_product_id) = param_new_sale_product.sale_product.product_id {
                        let product = 
                            schema::products::table
                                .select(PRODUCT_COLUMNS)
                                .find(param_product_id)
                                .first(conn);

                        Ok(
                            FullSaleProduct {
                                 sale_product: sale_product?, 
                                 product: product? 
                            }
                        )
                    } else {
                        Err(MyStoreError::PGConnectionError)
                    }
                }).collect();

            Ok(FullSale{ sale, sale_products: sale_products? })
        })
    }


fn updateSale(context: &amp;Context, param_sale: NewSale, param_sale_products: NewSaleProducts) 
    -&gt; FieldResult&lt;FullSale&gt; {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use diesel::ExpressionMethods;
        use diesel::Connection;
        use crate::schema::sales::dsl;

        let conn: &amp;PgConnection = &amp;context.conn;
        let sale_id = param_sale.id.ok_or(
            diesel::result::Error::QueryBuilderError("missing id".into())
        )?;

        conn.transaction(|| {
            let sale = 
                diesel::update(dsl::sales
                                   .filter(dsl::user_id.eq(context.user_id))
                                   .find(sale_id))
                    .set(&amp;param_sale)
                    .get_result::&lt;Sale&gt;(conn)?;

            let sale_products: Result&lt;Vec&lt;FullSaleProduct&gt;, _&gt; =
                param_sale_products.data.into_iter().map (|param_sale_product| {
                    let sale_product =
                        diesel::update(schema::sale_products::table)
                            .set(&amp;param_sale_product.sale_product)
                            .get_result::&lt;SaleProduct&gt;(conn);

                    if let Some(param_product_id) = param_sale_product.sale_product.product_id {
                        let product = 
                            schema::products::table
                                .select(PRODUCT_COLUMNS)
                                .find(param_product_id)
                                .first(conn);

                        Ok(
                            FullSaleProduct {
                                 sale_product: sale_product?, 
                                 product: product? 
                            }
                        )
                    } else {
                        Err(MyStoreError::PGConnectionError)
                    }

                }).collect();

            Ok(FullSale{ sale, sale_products: sale_products? })
        })
    }

fn destroySale(context: &amp;Context, sale_id: i32) 
    -&gt; FieldResult&lt;i32&gt; {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use diesel::ExpressionMethods;
        use crate::schema::sales::dsl;

        let conn: &amp;PgConnection = &amp;context.conn;
        diesel::delete(dsl::sales.filter(dsl::user_id.eq(context.user_id)).find(sale_id))
            .execute(conn)?;
        Ok(sale_id)
    }

}

pub type Schema = juniper::RootNode<'static, Query, Mutation>;

pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {})
}

pub fn create_context(logged_user_id: i32, pg_pool: PgPooledConnection) -> Context {
Context { user_id: logged_user_id, conn: Arc::new(pg_pool)}
}

As you can see our business logic is very similar to a REST endpoint, what could change is the way we query the data, we then export the schema using create_schema function.

src/main.rs:

 let schema = std::sync::Arc::new(create_schema());

HttpServer::new(
move || App::new()
    .data(schema.clone())

Now, how can we fetch the data and performs our mutation?, we then need to write some tests and see if everything works as expected.

tests/sale_test.rs:

...
// create a sale:
let query =
format!(
r#"
{{
"query": "
mutation CreateSale($paramNewSale: NewSale!, $paramNewSaleProducts: NewSaleProducts!) {{
createSale(paramNewSale: $paramNewSale, paramNewSaleProducts: $paramNewSaleProducts) {{
sale {{
id
userId
saleDate
total
}}
saleProducts {{
product {{
name
}}
saleProduct {{
id
productId
amount
discount
tax
price
total
}}
}}
}}
}}
",
"variables": {{
"paramNewSale": {{
"saleDate": "{}",
"total": {}
}},
"paramNewSaleProducts": {{
"data":
[{{
"product": {{ }},
"saleProduct": {{
"amount": {},
"discount": {},
"price": {},
"productId": {},
"tax": {},
"total": {}
}}
}}]
}}
}}
}}"#,
...

// show a sale:

   let query = format!(r#"
        {{
            "query": "
                query ShowASale($saleId: Int!) {{
                    sale(saleId: $saleId) {{
                        sale {{
                            id
                            userId
                            saleDate
                            total
                        }}
                        saleProducts {{
                            product {{ name }}
                            saleProduct {{
                                id
                                productId
                                amount
                                discount
                                tax
                                price
                                total
                            }}
                        }}
                    }}
                }}
            ",
            "variables": {{
                "saleId": {}
            }}
        }}
    "#, id).replace("\n", "");

...

// update a sale

    let query = 
        format!(
        r#"
        {{
            "query": "
                mutation UpdateSale($paramSale: NewSale!, $paramSaleProducts: NewSaleProducts!) {{
                        updateSale(paramSale: $paramSale, paramSaleProducts: $paramSaleProducts) {{
                            sale {{
                                id
                                saleDate
                                total
                            }}
                            saleProducts {{
                                product {{ name }}
                                saleProduct {{
                                    id
                                    productId
                                    amount
                                    discount
                                    tax
                                    price
                                    total
                                }}
                            }}
                        }}
                }}
            ",
            "variables": {{
                "paramSale": {{
                    "id": {},
                    "saleDate": "{}",
                    "total": {}
                }},
                "paramSaleProducts": {{
                    "data":
                        [{{
                            "product": {{}},
                            "saleProduct": 
                            {{
                                "amount": {},
                                "discount": {},
                                "price": {},
                                "productId": {},
                                "tax": {},
                                "total": {}
                            }}
                        }}]
                }}
            }}
        }}"#

...

// delete a sale:

    let query = format!(r#"
        {{
            "query": "
                mutation DestroyASale($saleId: Int!) {{
                    destroySale(saleId: $saleId)
                }}
            ",
            "variables": {{
                "saleId": {}
            }}
        }}
    "#, id).replace("\n", "");

...

// search for a sale with specific date:

   let query = format!(r#"
        {{
            "query": "
                query ListSale($search: NewSale!, $limit: Int!) {{
                    listSale(search: $search, limit: $limit) {{
                        data {{
                            sale {{
                                id
                                saleDate
                                total
                            }}
                            saleProducts {{
                                product {{
                                    name
                                }}
                                saleProduct {{
                                    amount
                                    price
                                }}
                            }}
                        }}
                    }}
                }}
            ",
            "variables": {{
                "search": {{
                    "saleDate": "2019-11-10"
                }},
                "limit": 10
            }}
        }}
    "#).replace("\n", "");

You can take a look at the full source code here.

Originally published by Werner Echezuría  at dev.to

========================================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ GraphQL with React: The Complete Developers Guide

☞ The Modern GraphQL Bootcamp (with Node.js and Apollo)

☞ Build a Realtime App with React Hooks and GraphQL

☞ Full-Stack React with GraphQL and Apollo Boost

☞ The Rust Programming Language

☞ Rust: Building Reusable Code with Rust from Scratch

☞  Programming in Rust: the good, the bad, the ugly.

☞  An introduction to Web Development with Rust for Node.js Developers

☞ Intro to Web Development with Rust for NodeJS Developers

☞ Introducing the Rust Crash Course


Develop this one fundamental skill if you want to become a successful developer

Throughout my career, a multitude of people have asked me&nbsp;<em>what does it take to become a successful developer?</em>

Throughout my career, a multitude of people have asked me what does it take to become a successful developer?

It’s a common question newbies and those looking to switch careers often ask — mostly because they see the potential paycheck. There is also a Hollywood level of coolness attached to working with computers nowadays. Being a programmer or developer is akin to being a doctor or lawyer. There is job security.

But a lot of people who try to enter the profession don’t make it. So what is it that separates those who make it and those who don’t? 

Read full article here